Add api keys

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2025-11-03 09:15:14 +01:00
parent 845652c382
commit 5212d0c18f
47 changed files with 6009 additions and 3648 deletions

View File

@@ -1,12 +1,13 @@
import { RouterProvider } from "react-router";
import { router } from "./routes";
import { Toasts } from "@probo/ui";
import { Toasts, ConfirmDialog } from "@probo/ui";
export function App() {
return (
<>
<RouterProvider router={router} />
<Toasts />
<ConfirmDialog />
</>
);
}

View File

@@ -34,6 +34,7 @@ import {
Avatar,
IconPeopleAdd,
Badge,
IconKey,
IconLock,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
@@ -227,6 +228,11 @@ function UserDropdown({ organizationId }: { organizationId: string }) {
return (
<UserDropdownRoot fullName={user.fullName} email={user.email}>
<UserDropdownItem
to="/api-keys"
icon={IconKey}
label={__("API Keys")}
/>
<UserDropdownItem
to="mailto:support@getprobo.com"
icon={IconCircleQuestionmark}

View File

@@ -0,0 +1,799 @@
import { useState, useEffect } from "react";
import {
Button,
Card,
Dialog,
DialogContent,
DialogFooter,
Field,
IconTrashCan,
IconPlusLarge,
IconPencil,
IconSquareBehindSquare2,
Label,
Select,
Badge,
useConfirm,
useDialogRef,
Option,
useToast,
Checkbox,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
Input,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { formatDate } from "@probo/helpers";
import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { Controller } from "react-hook-form";
import { z } from "zod";
import { UnAuthenticatedError } from "/providers/RelayProviders";
interface APIKey {
id: string;
name: string;
expiresAt: string;
createdAt: string;
organizations: APIKeyOrganization[];
}
interface APIKeyOrganization {
organizationId: string;
organizationName: string;
role: string;
}
interface Organization {
id: string;
name: string;
authStatus: "authenticated" | "unauthenticated" | "expired";
}
const createSchema = z.object({
name: z.string().min(1, "Name is required"),
expiresIn: z.enum(["1month", "3months", "6months", "1year"]),
organizations: z.array(z.object({
organizationId: z.string(),
role: z.string(),
})).min(1, "At least one organization is required"),
});
type CreateFormData = z.infer<typeof createSchema>;
export default function APIKeysPage() {
const { __ } = useTranslate();
const { toast } = useToast();
const confirm = useConfirm();
const dialogRef = useDialogRef();
const editDialogRef = useDialogRef();
const keyDialogRef = useDialogRef();
const [currentKey, setCurrentKey] = useState<string | null>(null);
const [isLoadingKey, setIsLoadingKey] = useState(false);
const [apiKeys, setApiKeys] = useState<APIKey[]>([]);
const [organizations, setOrganizations] = useState<Organization[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isCreating, setIsCreating] = useState(false);
const [isUpdating, setIsUpdating] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [selectedOrganizations, setSelectedOrganizations] = useState<string[]>([]);
const [organizationRoles, setOrganizationRoles] = useState<Record<string, string>>({});
const [editingAPIKey, setEditingAPIKey] = useState<APIKey | null>(null);
const [editingName, setEditingName] = useState<string>("");
const [error, setError] = useState<Error | null>(null);
const { formState, handleSubmit, reset, control, setValue, register } = useFormWithSchema(createSchema, {
defaultValues: {
name: new Date().toISOString().split('T')[0],
expiresIn: "1month",
organizations: [],
},
});
if (error) {
throw error;
}
const fetchAPIKeys = async () => {
try {
const response = await fetch('/connect/api-keys', { credentials: 'include' });
if (!response.ok) {
throw new Error('Failed to fetch API keys');
}
const data: { apiKeys: APIKey[] } = await response.json();
setApiKeys(data.apiKeys);
} catch (err) {
console.error('Failed to fetch API keys:', err);
toast({
title: __("Error"),
description: __("Failed to load API keys"),
variant: "error",
});
}
};
useEffect(() => {
const fetchData = async () => {
try {
const [apiKeysResponse, organizationsResponse] = await Promise.all([
fetch('/connect/api-keys', { credentials: 'include' }),
fetch('/connect/organizations', { credentials: 'include' }),
]);
if (apiKeysResponse.status === 401 || organizationsResponse.status === 401) {
setError(new UnAuthenticatedError());
return;
}
if (!apiKeysResponse.ok) {
throw new Error('Failed to fetch API keys');
}
if (!organizationsResponse.ok) {
throw new Error('Failed to fetch organizations');
}
const apiKeysData: { apiKeys: APIKey[] } = await apiKeysResponse.json();
const orgsData: { organizations: Organization[] } = await organizationsResponse.json();
const authenticatedOrgs = orgsData.organizations.filter(org => org.authStatus === "authenticated");
setApiKeys(apiKeysData.apiKeys);
setOrganizations(authenticatedOrgs);
} catch (err) {
console.error('Failed to fetch data:', err);
toast({
title: __("Error"),
description: __("Failed to load data"),
variant: "error",
});
} finally {
setIsLoading(false);
}
};
fetchData();
}, [__, toast]);
const handleCreate = async (formData: CreateFormData) => {
const now = new Date();
const expiresAt = new Date(now);
switch (formData.expiresIn) {
case "1month":
expiresAt.setMonth(now.getMonth() + 1);
break;
case "3months":
expiresAt.setMonth(now.getMonth() + 3);
break;
case "6months":
expiresAt.setMonth(now.getMonth() + 6);
break;
case "1year":
expiresAt.setFullYear(now.getFullYear() + 1);
break;
}
setIsCreating(true);
try {
const response = await fetch('/connect/api-keys', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
name: formData.name,
expiresAt: expiresAt.toISOString(),
organizations: formData.organizations,
}),
});
if (!response.ok) {
throw new Error('Failed to create API key');
}
const data: { apiKey: APIKey; key: string } = await response.json();
await fetchAPIKeys();
dialogRef.current?.close();
reset();
setSelectedOrganizations([]);
setOrganizationRoles({});
setCurrentKey(data.key);
keyDialogRef.current?.open();
toast({
title: __("Success"),
description: __("API Key created successfully"),
variant: "success",
});
} catch (error) {
toast({
title: __("Error"),
description: (error as Error).message,
variant: "error",
});
} finally {
setIsCreating(false);
}
};
const handleEdit = (apiKey: APIKey) => {
setEditingAPIKey(apiKey);
setEditingName(apiKey.name);
const orgIds = apiKey.organizations.map(org => org.organizationId);
const roles: Record<string, string> = {};
apiKey.organizations.forEach(org => {
roles[org.organizationId] = org.role;
});
setSelectedOrganizations(orgIds);
setOrganizationRoles(roles);
editDialogRef.current?.open();
};
const handleUpdate = async () => {
if (!editingAPIKey) return;
setIsUpdating(true);
try {
const response = await fetch('/connect/api-keys', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
id: editingAPIKey.id,
name: editingName,
organizations: selectedOrganizations.map(id => ({
organizationId: id,
role: organizationRoles[id] || "FULL",
})),
}),
});
if (!response.ok) {
throw new Error('Failed to update API key');
}
await fetchAPIKeys();
editDialogRef.current?.close();
setEditingAPIKey(null);
setEditingName("");
setSelectedOrganizations([]);
setOrganizationRoles({});
toast({
title: __("Success"),
description: __("API Key updated successfully"),
variant: "success",
});
} catch (error) {
toast({
title: __("Error"),
description: (error as Error).message,
variant: "error",
});
} finally {
setIsUpdating(false);
}
};
const handleDelete = (id: string, name: string) => {
confirm(
async () => {
setIsDeleting(true);
try {
const response = await fetch('/connect/api-keys', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({ id }),
});
if (!response.ok) {
throw new Error('Failed to delete API key');
}
setApiKeys(apiKeys.filter(key => key.id !== id));
toast({
title: __("Success"),
description: __("API Key deleted successfully"),
variant: "success",
});
} catch (error) {
toast({
title: __("Error"),
description: (error as Error).message,
variant: "error",
});
throw error;
} finally {
setIsDeleting(false);
}
},
{
message: __(`Are you sure you want to delete the API key "${name}"? This action cannot be undone.`),
}
);
};
const handleShowToken = async (id: string) => {
setIsLoadingKey(true);
try {
const response = await fetch(`/connect/api-keys/${id}`, {
credentials: 'include',
});
if (!response.ok) {
throw new Error('Failed to load API key');
}
const data: { key: string } = await response.json();
setCurrentKey(data.key);
keyDialogRef.current?.open();
} catch (error) {
toast({
title: __("Error"),
description: __("Failed to load API key"),
variant: "error",
});
} finally {
setIsLoadingKey(false);
}
};
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
toast({
title: __("Success"),
description: __("API key copied to clipboard"),
variant: "success",
});
} catch (error) {
toast({
title: __("Error"),
description: __("Failed to copy to clipboard"),
variant: "error",
});
}
};
const isExpired = (expiresAt: string) => {
return new Date(expiresAt) < new Date();
};
if (isLoading) {
return (
<div className="space-y-6 w-full py-6">
<h1 className="text-3xl font-bold text-center">{__("API Keys")}</h1>
<Card padded>
<div className="text-center py-8">
<p className="text-txt-tertiary">{__("Loading...")}</p>
</div>
</Card>
</div>
);
}
return (
<div className="space-y-6 w-full py-6">
<h1 className="text-3xl font-bold text-center">{__("API Keys")}</h1>
<div className="space-y-4 w-full">
{apiKeys.length === 0 ? (
<Card padded>
<div className="text-center py-8">
<p className="text-txt-tertiary mb-4">
{__("No API keys yet. Create one to get started.")}
</p>
</div>
</Card>
) : (
apiKeys.map((apiKey) => {
const expired = isExpired(apiKey.expiresAt);
return (
<Card key={apiKey.id} padded className="w-full">
<div className="flex items-start justify-between gap-4">
<div className="flex-1 space-y-2">
<div className="flex items-center gap-2">
<h3 className="text-lg font-semibold">{apiKey.name}</h3>
{expired ? (
<Badge variant="danger">{__("Expired")}</Badge>
) : (
<Badge variant="success">{__("Active")}</Badge>
)}
</div>
<div className="flex items-center gap-4 text-sm text-txt-secondary">
<span>
{__("Created")}: {formatDate(apiKey.createdAt)}
</span>
<span></span>
<span>
{__("Expires")}: {formatDate(apiKey.expiresAt)}
</span>
</div>
{apiKey.organizations.length > 0 && (
<div className="flex flex-wrap gap-2 mt-2">
{apiKey.organizations.map((org) => (
<Badge key={org.organizationId} variant="neutral">
{org.organizationName} ({org.role})
</Badge>
))}
</div>
)}
</div>
<div className="flex items-center gap-2">
<Button
variant="secondary"
onClick={() => handleShowToken(apiKey.id)}
disabled={isLoadingKey}
title={__("Show Token")}
>
{__("Show")}
</Button>
<Button
variant="secondary"
onClick={() => handleEdit(apiKey)}
disabled={isLoadingKey}
icon={IconPencil}
title={__("Edit")}
/>
<Button
variant="danger"
onClick={() => handleDelete(apiKey.id, apiKey.name)}
disabled={isDeleting}
icon={IconTrashCan}
title={__("Delete")}
/>
</div>
</div>
</Card>
);
})
)}
<Card padded>
<h2 className="text-xl font-semibold mb-1">
{__("Create an API key")}
</h2>
<p className="text-txt-tertiary mb-4">
{__("Generate a new API key for programmatic access to your organization")}
</p>
<Button
onClick={() => dialogRef.current?.open()}
variant="quaternary"
icon={IconPlusLarge}
className="w-full"
>
{__("Create API Key")}
</Button>
</Card>
</div>
<Dialog ref={dialogRef} title={__("Create API Key")}>
<form onSubmit={handleSubmit(handleCreate)}>
<DialogContent padded className="space-y-4">
<Field error={formState.errors.name?.message}>
<Label>{__("Name")}</Label>
<Input
{...register("name")}
placeholder={__("e.g., Production API Key")}
/>
</Field>
<Field error={formState.errors.expiresIn?.message}>
<Label>{__("Expires In")}</Label>
<Controller
control={control}
name="expiresIn"
render={({ field }) => (
<Select
{...field}
onValueChange={field.onChange}
value={field.value}
>
<Option value="1month">{__("1 Month")}</Option>
<Option value="3months">{__("3 Months")}</Option>
<Option value="6months">{__("6 Months")}</Option>
<Option value="1year">{__("1 Year")}</Option>
</Select>
)}
/>
</Field>
<Field error={formState.errors.organizations?.message}>
<div className="flex justify-between items-center mb-4">
<h4 className="font-medium text-txt-primary">
{__("Organizations")}
</h4>
{organizations.length > 0 && (
<Button
type="button"
variant="tertiary"
onClick={() => {
const allSelected = selectedOrganizations.length === organizations.length;
if (allSelected) {
setSelectedOrganizations([]);
setOrganizationRoles({});
setValue("organizations", []);
} else {
const allOrgIds = organizations.map(org => org.id);
const newRoles: Record<string, string> = {};
allOrgIds.forEach(id => {
newRoles[id] = organizationRoles[id] || "FULL";
});
setSelectedOrganizations(allOrgIds);
setOrganizationRoles(newRoles);
setValue(
"organizations",
allOrgIds.map(id => ({
organizationId: id,
role: newRoles[id]
}))
);
}
}}
className="text-xs h-7 min-h-7"
>
{selectedOrganizations.length === organizations.length ? __("Clear All") : __("Select All")}
</Button>
)}
</div>
{organizations.length === 0 ? (
<div className="text-center text-txt-tertiary py-8">
{__("No organizations available")}
</div>
) : (
<div className="bg-bg-secondary rounded-lg overflow-hidden">
<Table>
<Thead>
<Tr>
<Th>{__("Name")}</Th>
<Th width={180}>{__("Role")}</Th>
<Th width={100}>
<div className="flex justify-end">
{__("Access")}
</div>
</Th>
</Tr>
</Thead>
<Tbody>
{organizations.map((org) => (
<Tr key={org.id}>
<Td>
<div className="font-medium text-txt-primary">
{org.name}
</div>
</Td>
<Td>
<div className="min-h-[36px] flex items-center">
{selectedOrganizations.includes(org.id) ? (
<Select
value={organizationRoles[org.id] || "FULL"}
onValueChange={(role) => {
const newRoles = { ...organizationRoles, [org.id]: role };
setOrganizationRoles(newRoles);
setValue(
"organizations",
selectedOrganizations.map(id => ({
organizationId: id,
role: newRoles[id] || "FULL"
}))
);
}}
>
<Option value="FULL">{__("Full")}</Option>
</Select>
) : (
<span className="text-txt-tertiary"></span>
)}
</div>
</Td>
<Td>
<div className="flex justify-end">
<Checkbox
checked={selectedOrganizations.includes(org.id)}
onChange={(checked: boolean) => {
let newSelected: string[];
const newRoles = { ...organizationRoles };
if (checked) {
newSelected = [...selectedOrganizations, org.id];
if (!newRoles[org.id]) {
newRoles[org.id] = "FULL";
}
} else {
newSelected = selectedOrganizations.filter(id => id !== org.id);
delete newRoles[org.id];
}
setSelectedOrganizations(newSelected);
setOrganizationRoles(newRoles);
setValue(
"organizations",
newSelected.map(id => ({
organizationId: id,
role: newRoles[id] || "FULL"
}))
);
}}
/>
</div>
</Td>
</Tr>
))}
</Tbody>
</Table>
</div>
)}
</Field>
</DialogContent>
<DialogFooter>
<Button type="submit" disabled={isCreating}>
{isCreating ? __("Creating...") : __("Create")}
</Button>
</DialogFooter>
</form>
</Dialog>
<Dialog ref={editDialogRef} title={__("Edit API Key")}>
<DialogContent padded className="space-y-6">
<Field>
<Label>{__("Name")}</Label>
<Input
value={editingName}
onChange={(e) => setEditingName(e.target.value)}
placeholder={__("API Key Name")}
/>
</Field>
<div>
<div className="flex justify-between items-center mb-4">
<h4 className="font-medium text-txt-primary">
{__("Organizations")}
</h4>
{organizations.length > 0 && (
<Button
type="button"
variant="tertiary"
onClick={() => {
const allSelected = selectedOrganizations.length === organizations.length;
if (allSelected) {
setSelectedOrganizations([]);
setOrganizationRoles({});
} else {
const allOrgIds = organizations.map(org => org.id);
const newRoles: Record<string, string> = {};
allOrgIds.forEach(id => {
newRoles[id] = organizationRoles[id] || "FULL";
});
setSelectedOrganizations(allOrgIds);
setOrganizationRoles(newRoles);
}
}}
className="text-xs h-7 min-h-7"
>
{selectedOrganizations.length === organizations.length ? __("Clear All") : __("Select All")}
</Button>
)}
</div>
{organizations.length === 0 ? (
<div className="text-center text-txt-tertiary py-8">
{__("No organizations available")}
</div>
) : (
<div className="bg-bg-secondary rounded-lg overflow-hidden">
<Table>
<Thead>
<Tr>
<Th>{__("Name")}</Th>
<Th width={180}>{__("Role")}</Th>
<Th width={100}>
<div className="flex justify-end">
{__("Access")}
</div>
</Th>
</Tr>
</Thead>
<Tbody>
{organizations.map((org) => (
<Tr key={org.id}>
<Td>
<div className="font-medium text-txt-primary">
{org.name}
</div>
</Td>
<Td>
<div className="min-h-[36px] flex items-center">
{selectedOrganizations.includes(org.id) ? (
<Select
value={organizationRoles[org.id] || "FULL"}
onValueChange={(role) => {
const newRoles = { ...organizationRoles, [org.id]: role };
setOrganizationRoles(newRoles);
}}
>
<Option value="FULL">{__("Full")}</Option>
</Select>
) : (
<span className="text-txt-tertiary"></span>
)}
</div>
</Td>
<Td>
<div className="flex justify-end">
<Checkbox
checked={selectedOrganizations.includes(org.id)}
onChange={(checked: boolean) => {
let newSelected: string[];
const newRoles = { ...organizationRoles };
if (checked) {
newSelected = [...selectedOrganizations, org.id];
if (!newRoles[org.id]) {
newRoles[org.id] = "FULL";
}
} else {
newSelected = selectedOrganizations.filter(id => id !== org.id);
delete newRoles[org.id];
}
setSelectedOrganizations(newSelected);
setOrganizationRoles(newRoles);
}}
/>
</div>
</Td>
</Tr>
))}
</Tbody>
</Table>
</div>
)}
</div>
</DialogContent>
<DialogFooter>
<Button onClick={handleUpdate} disabled={isUpdating || (selectedOrganizations.length === 0 && !editingName.trim())}>
{isUpdating ? __("Updating...") : __("Update")}
</Button>
</DialogFooter>
</Dialog>
<Dialog ref={keyDialogRef} title={__("API Key")}>
<DialogContent padded className="space-y-4">
<p className="text-sm text-txt-tertiary">
{__("Please save this API key securely.")}
</p>
<div className="bg-gray-100 p-4 rounded-lg flex items-center gap-2">
<code className="text-sm font-mono break-all flex-1">
{currentKey || ""}
</code>
<Button
onClick={() => {
if (currentKey) {
copyToClipboard(currentKey);
}
}}
variant="secondary"
disabled={!currentKey}
title={__("Copy to Clipboard")}
>
<IconSquareBehindSquare2 size={16} />
</Button>
</div>
</DialogContent>
<DialogFooter>
<Button
onClick={() => {
keyDialogRef.current?.close();
setCurrentKey(null);
}}
>
{__("Done")}
</Button>
</DialogFooter>
</Dialog>
</div>
);
}

View File

@@ -114,6 +114,10 @@ const routes = [
() => import("./pages/DocumentSigningRequestsPage.tsx")
),
},
{
path: "api-keys",
Component: lazy(() => import("./pages/APIKeysPage")),
},
],
},
{

View File

@@ -0,0 +1,22 @@
import type { IconProps } from "./type.ts";
// https://lucide.dev/icons/key-round
export function IconKey({ size = 24, className }: IconProps) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
className={className}
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z" />
<circle cx="16.5" cy="7.5" r=".5" fill="currentColor" />
</svg>
);
}

View File

@@ -15,6 +15,7 @@ export { IconCrossLargeX } from "./IconCrossLargeX.tsx";
export { IconUpload } from "./IconUpload.tsx";
export { IconListStack } from "./IconListStack.tsx";
export { IconLock } from "./IconLock.tsx";
export { IconKey } from "./IconKey.tsx";
export { IconCircleCheck1 } from "./IconCircleCheck1.tsx";
export { IconClock } from "./IconClock.tsx";
export { IconImport2 } from "./IconImport2.tsx";

View File

@@ -54,7 +54,7 @@ export {
useDialogRef,
type DialogRef,
} from "./Molecules/Dialog/Dialog";
export { useConfirm } from "./Molecules/Dialog/ConfirmDialog";
export { useConfirm, ConfirmDialog } from "./Molecules/Dialog/ConfirmDialog";
export { RiskBadge } from "./Molecules/Badge/RiskBadge";
export { SeverityBadge } from "./Molecules/Badge/SeverityBadge.tsx";
export { DocumentVersionBadge } from "./Molecules/Badge/DocumentVersionBadge.tsx";

View File

@@ -24,6 +24,8 @@ import (
"net/mail"
"time"
"github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/packages/emails"
"go.probo.inc/probo/pkg/baseurl"
"go.probo.inc/probo/pkg/coredata"
@@ -31,8 +33,6 @@ import (
"go.probo.inc/probo/pkg/crypto/passwdhash"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/statelesstoken"
"github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg"
)
type (
@@ -124,11 +124,32 @@ type (
OrganizationID gid.GID
RedirectURL string
}
UserAPIKeyMembershipRequest struct {
MembershipID gid.GID
Role coredata.APIRole
}
UserAPIKeyOrganizationRequest struct {
OrganizationID gid.GID
Role coredata.APIRole
}
UserAPIKeyWithMembershipsResponse struct {
UserAPIKey *coredata.UserAPIKey
Memberships []*coredata.UserAPIKeyMembership
}
UserAPIKeyTokenData struct {
ID gid.GID `json:"id"`
CreatedAt time.Time `json:"created_at"`
}
)
const (
TokenTypeEmailConfirmation = "email_confirmation"
TokenTypePasswordReset = "password_reset"
TokenTypeAPIKey = "api_key"
)
func (e ErrInvalidCredentials) Error() string {
@@ -1235,3 +1256,388 @@ func verifyDomainOwnership(ctx context.Context, domain, expectedToken string) (b
return false, nil
}
func (s *Service) ValidateAndBuildUserAPIKeyMemberships(
ctx context.Context,
userID gid.GID,
organizationRequests []UserAPIKeyOrganizationRequest,
) ([]UserAPIKeyMembershipRequest, error) {
memberships := make([]UserAPIKeyMembershipRequest, 0, len(organizationRequests))
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
for _, org := range organizationRequests {
tenantID := org.OrganizationID.TenantID()
var membership coredata.Membership
if err := membership.LoadByUserAndOrg(ctx, conn, coredata.NewScope(tenantID), userID, org.OrganizationID); err != nil {
return fmt.Errorf("you do not have access to organization %s", org.OrganizationID)
}
var role coredata.APIRole
switch org.Role {
case coredata.APIRoleFull:
role = coredata.APIRoleFull
default:
return fmt.Errorf("invalid role: %s", org.Role)
}
memberships = append(memberships, UserAPIKeyMembershipRequest{
MembershipID: membership.ID,
Role: role,
})
}
return nil
},
)
if err != nil {
return nil, err
}
return memberships, nil
}
func (s *Service) CreateUserAPIKey(
ctx context.Context,
userID gid.GID,
name string,
expiresAt time.Time,
memberships []UserAPIKeyMembershipRequest,
) (*coredata.UserAPIKey, string, error) {
var userAPIKey *coredata.UserAPIKey
var token string
err := s.pg.WithTx(
ctx,
func(tx pg.Conn) error {
now := time.Now()
userAPIKey = &coredata.UserAPIKey{
ID: gid.New(gid.NilTenant, coredata.UserAPIKeyEntityType),
UserID: userID,
Name: name,
ExpiresAt: expiresAt,
CreatedAt: now,
UpdatedAt: now,
}
if err := userAPIKey.Insert(ctx, tx); err != nil {
return fmt.Errorf("cannot insert user api key: %w", err)
}
if err := userAPIKey.LoadByID(ctx, tx, userAPIKey.ID); err != nil {
return fmt.Errorf("cannot load user api key: %w", err)
}
tokenData := UserAPIKeyTokenData{
ID: userAPIKey.ID,
CreatedAt: userAPIKey.CreatedAt,
}
generatedToken, err := statelesstoken.NewDeterministicToken(
s.tokenSecret,
TokenTypeAPIKey,
userAPIKey.ExpiresAt,
userAPIKey.CreatedAt,
tokenData,
)
if err != nil {
return fmt.Errorf("cannot generate user api key token: %w", err)
}
token = generatedToken
for _, membership := range memberships {
scope := coredata.NewScope(membership.MembershipID.TenantID())
userAPIKeyMembership := &coredata.UserAPIKeyMembership{
ID: gid.New(membership.MembershipID.TenantID(), coredata.UserAPIKeyMembershipEntityType),
UserAPIKeyID: userAPIKey.ID,
MembershipID: membership.MembershipID,
Role: membership.Role,
CreatedAt: now,
UpdatedAt: now,
}
if err := userAPIKeyMembership.Insert(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot insert user api key membership: %w", err)
}
}
return nil
},
)
if err != nil {
return nil, "", err
}
return userAPIKey, token, nil
}
func (s *Service) ListUserAPIKeys(
ctx context.Context,
userID gid.GID,
) ([]*coredata.UserAPIKey, error) {
var userAPIKeys coredata.UserAPIKeys
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
if err := userAPIKeys.LoadByUserID(ctx, conn, userID); err != nil {
return fmt.Errorf("cannot load user api keys: %w", err)
}
return nil
},
)
return userAPIKeys, err
}
func (s *Service) ListUserAPIKeysWithMemberships(
ctx context.Context,
userID gid.GID,
tenantIDs []gid.TenantID,
) ([]UserAPIKeyWithMembershipsResponse, error) {
var userAPIKeys coredata.UserAPIKeys
var result []UserAPIKeyWithMembershipsResponse
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
if err := userAPIKeys.LoadByUserID(ctx, conn, userID); err != nil {
return fmt.Errorf("cannot load user api keys: %w", err)
}
result = make([]UserAPIKeyWithMembershipsResponse, 0, len(userAPIKeys))
for _, userAPIKey := range userAPIKeys {
keyWithMemberships := UserAPIKeyWithMembershipsResponse{
UserAPIKey: userAPIKey,
Memberships: make([]*coredata.UserAPIKeyMembership, 0),
}
for _, tenantID := range tenantIDs {
scope := coredata.NewScope(tenantID)
var memberships coredata.UserAPIKeyMemberships
if err := memberships.LoadByUserAPIKeyID(ctx, conn, scope, userAPIKey.ID); err != nil {
return fmt.Errorf("cannot load user api key memberships: %w", err)
}
keyWithMemberships.Memberships = append(keyWithMemberships.Memberships, memberships...)
}
result = append(result, keyWithMemberships)
}
return nil
},
)
if err != nil {
return nil, err
}
return result, nil
}
func (s *Service) GetUserAPIKey(
ctx context.Context,
userAPIKeyID gid.GID,
userID gid.GID,
) (string, error) {
var token string
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
userAPIKey := &coredata.UserAPIKey{}
if err := userAPIKey.LoadByID(ctx, conn, userAPIKeyID); err != nil {
return fmt.Errorf("cannot load user api key: %w", err)
}
if userAPIKey.UserID != userID {
return fmt.Errorf("user api key does not belong to user")
}
tokenData := UserAPIKeyTokenData{
ID: userAPIKey.ID,
CreatedAt: userAPIKey.CreatedAt,
}
generatedToken, err := statelesstoken.NewDeterministicToken(
s.tokenSecret,
TokenTypeAPIKey,
userAPIKey.ExpiresAt,
userAPIKey.CreatedAt,
tokenData,
)
if err != nil {
return fmt.Errorf("cannot generate user api key token: %w", err)
}
token = generatedToken
return nil
},
)
if err != nil {
return "", err
}
return token, nil
}
func (s *Service) UpdateUserAPIKeyMemberships(
ctx context.Context,
userAPIKeyID gid.GID,
userID gid.GID,
memberships []UserAPIKeyMembershipRequest,
) error {
return s.pg.WithTx(
ctx,
func(tx pg.Conn) error {
userAPIKey := &coredata.UserAPIKey{}
if err := userAPIKey.LoadByID(ctx, tx, userAPIKeyID); err != nil {
return fmt.Errorf("cannot load user api key: %w", err)
}
if userAPIKey.UserID != userID {
return fmt.Errorf("user api key does not belong to user")
}
if err := coredata.DeleteAllUserAPIKeyMembershipsByUserAPIKeyID(ctx, tx, userAPIKeyID); err != nil {
return fmt.Errorf("cannot delete existing memberships: %w", err)
}
now := time.Now()
for _, membership := range memberships {
scope := coredata.NewScope(membership.MembershipID.TenantID())
userAPIKeyMembership := &coredata.UserAPIKeyMembership{
ID: gid.New(membership.MembershipID.TenantID(), coredata.UserAPIKeyMembershipEntityType),
UserAPIKeyID: userAPIKey.ID,
MembershipID: membership.MembershipID,
Role: membership.Role,
CreatedAt: now,
UpdatedAt: now,
}
if err := userAPIKeyMembership.Insert(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot insert user api key membership: %w", err)
}
}
return nil
},
)
}
func (s *Service) UpdateUserAPIKeyName(
ctx context.Context,
userAPIKeyID gid.GID,
userID gid.GID,
name string,
) error {
return s.pg.WithTx(
ctx,
func(tx pg.Conn) error {
userAPIKey := &coredata.UserAPIKey{}
if err := userAPIKey.LoadByID(ctx, tx, userAPIKeyID); err != nil {
return fmt.Errorf("cannot load user api key: %w", err)
}
if userAPIKey.UserID != userID {
return fmt.Errorf("user api key does not belong to user")
}
userAPIKey.Name = name
userAPIKey.UpdatedAt = time.Now()
if err := userAPIKey.Update(ctx, tx); err != nil {
return fmt.Errorf("cannot update user api key: %w", err)
}
return nil
},
)
}
func (s *Service) DeleteUserAPIKey(
ctx context.Context,
userAPIKeyID gid.GID,
userID gid.GID,
) error {
return s.pg.WithTx(
ctx,
func(tx pg.Conn) error {
userAPIKey := &coredata.UserAPIKey{}
if err := userAPIKey.LoadByID(ctx, tx, userAPIKeyID); err != nil {
return fmt.Errorf("cannot load user api key: %w", err)
}
if userAPIKey.UserID != userID {
return fmt.Errorf("user api key does not belong to user")
}
if err := userAPIKey.Delete(ctx, tx); err != nil {
return fmt.Errorf("cannot delete user api key: %w", err)
}
return nil
},
)
}
func (s *Service) ValidateUserAPIKey(
ctx context.Context,
token string,
) (*coredata.User, *coredata.UserAPIKey, error) {
payload, err := statelesstoken.ValidateToken[UserAPIKeyTokenData](
s.tokenSecret,
TokenTypeAPIKey,
token,
)
if err != nil {
return nil, nil, &ErrInvalidCredentials{message: "invalid user api key"}
}
tokenData := payload.Data
var user *coredata.User
var userAPIKey *coredata.UserAPIKey
err = s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
userAPIKey = &coredata.UserAPIKey{}
if err := userAPIKey.LoadByID(ctx, conn, tokenData.ID); err != nil {
var errNotFound *coredata.ErrUserAPIKeyNotFound
if errors.As(err, &errNotFound) {
return &ErrInvalidCredentials{message: "invalid user api key"}
}
return fmt.Errorf("cannot load user api key: %w", err)
}
if !userAPIKey.CreatedAt.Equal(tokenData.CreatedAt) {
return &ErrInvalidCredentials{message: "invalid user api key"}
}
if time.Now().After(userAPIKey.ExpiresAt) {
return &ErrInvalidCredentials{message: "user api key expired"}
}
user = &coredata.User{}
if err := user.LoadByID(ctx, conn, userAPIKey.UserID); err != nil {
return fmt.Errorf("cannot load user: %w", err)
}
return nil
},
)
if err != nil {
return nil, nil, err
}
return user, userAPIKey, nil
}

View File

@@ -21,12 +21,12 @@ import (
"net/url"
"time"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/packages/emails"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/statelesstoken"
"go.gearno.de/kit/pg"
)
type TenantAccessError struct {
@@ -103,6 +103,26 @@ func (s *Service) GetAllUserOrganizations(
return organizations, err
}
func (s *Service) GetAllOrganizationsForUserAPIKeyId(
ctx context.Context,
userAPIKeyID gid.GID,
) (coredata.Organizations, error) {
organizations := coredata.Organizations{}
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
if err := organizations.LoadAllByUserAPIKeyID(ctx, conn, userAPIKeyID); err != nil {
return fmt.Errorf("cannot load user api key organizations: %w", err)
}
return nil
},
)
return organizations, err
}
func (s *Service) GetUserOrganizations(
ctx context.Context,
userID gid.GID,

View File

@@ -0,0 +1,180 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package coredata
import (
"context"
"fmt"
"maps"
"time"
"github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/gid"
)
type (
UserAPIKeyMembership struct {
ID gid.GID `db:"id"`
UserAPIKeyID gid.GID `db:"auth_user_api_key_id"`
MembershipID gid.GID `db:"membership_id"`
Role APIRole `db:"role"`
OrganizationID gid.GID `db:"organization_id"`
OrganizationName string `db:"organization_name"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
UserAPIKeyMemberships []*UserAPIKeyMembership
)
func (a *UserAPIKeyMembership) Insert(
ctx context.Context,
conn pg.Conn,
scope Scoper,
) error {
q := `
INSERT INTO
authz_api_keys_memberships (id, tenant_id, auth_user_api_key_id, membership_id, role, created_at, updated_at)
VALUES (
@id,
@tenant_id,
@auth_user_api_key_id,
@membership_id,
@role,
@created_at,
@updated_at
)
`
args := pgx.StrictNamedArgs{
"id": a.ID,
"tenant_id": scope.GetTenantID(),
"auth_user_api_key_id": a.UserAPIKeyID,
"membership_id": a.MembershipID,
"role": a.Role,
"created_at": a.CreatedAt,
"updated_at": a.UpdatedAt,
}
_, err := conn.Exec(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot insert user api key membership: %w", err)
}
return nil
}
func (a *UserAPIKeyMemberships) LoadByUserAPIKeyID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
userAPIKeyID gid.GID,
) error {
q := `
SELECT
akm.id,
akm.auth_user_api_key_id,
akm.membership_id,
akm.role,
akm.created_at,
akm.updated_at,
m.organization_id,
o.name as organization_name
FROM
authz_api_keys_memberships akm
JOIN
authz_memberships m ON akm.membership_id = m.id
JOIN
organizations o ON m.organization_id = o.id
WHERE
akm.auth_user_api_key_id = @auth_user_api_key_id
AND m.%s
ORDER BY akm.created_at DESC
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"auth_user_api_key_id": userAPIKeyID,
}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query user api key memberships: %w", err)
}
memberships, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[UserAPIKeyMembership])
if err != nil {
return fmt.Errorf("cannot collect user api key memberships: %w", err)
}
*a = memberships
return nil
}
func (a *UserAPIKeyMembership) Delete(
ctx context.Context,
conn pg.Conn,
scope Scoper,
) error {
q := `
DELETE FROM
authz_api_keys_memberships
WHERE
id = @id
AND %s
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"id": a.ID,
}
maps.Copy(args, scope.SQLArguments())
_, err := conn.Exec(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot delete user api key membership: %w", err)
}
return nil
}
func DeleteAllUserAPIKeyMembershipsByUserAPIKeyID(
ctx context.Context,
conn pg.Conn,
userAPIKeyID gid.GID,
) error {
q := `
DELETE FROM
authz_api_keys_memberships
WHERE
auth_user_api_key_id = @auth_user_api_key_id
`
args := pgx.StrictNamedArgs{
"auth_user_api_key_id": userAPIKeyID,
}
_, err := conn.Exec(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot delete user api key memberships: %w", err)
}
return nil
}

54
pkg/coredata/api_role.go Normal file
View File

@@ -0,0 +1,54 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package coredata
import (
"database/sql/driver"
"fmt"
)
type APIRole string
const (
APIRoleFull APIRole = "FULL"
)
func (r APIRole) String() string {
return string(r)
}
func (r *APIRole) Scan(value any) error {
var s string
switch v := value.(type) {
case string:
s = v
case []byte:
s = string(v)
default:
return fmt.Errorf("unsupported type for APIRole: %T", value)
}
switch s {
case "FULL":
*r = APIRoleFull
default:
return fmt.Errorf("invalid APIRole value: %q", s)
}
return nil
}
func (r APIRole) Value() (driver.Value, error) {
return r.String(), nil
}

View File

@@ -64,4 +64,6 @@ const (
SlackMessageEntityType
TrustCenterFileEntityType
SAMLConfigurationEntityType
UserAPIKeyEntityType
UserAPIKeyMembershipEntityType
)

View File

@@ -0,0 +1,26 @@
CREATE TYPE authz_api_role AS ENUM ('FULL');
CREATE TABLE auth_user_api_keys (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE,
name TEXT NOT NULL,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL
);
CREATE INDEX auth_user_api_keys_user_id_idx ON auth_user_api_keys(user_id);
CREATE TABLE authz_api_keys_memberships (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
auth_user_api_key_id TEXT NOT NULL REFERENCES auth_user_api_keys(id) ON UPDATE CASCADE ON DELETE CASCADE,
membership_id TEXT NOT NULL REFERENCES authz_memberships(id) ON UPDATE CASCADE ON DELETE CASCADE,
role authz_api_role NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
UNIQUE(auth_user_api_key_id, membership_id)
);
CREATE INDEX authz_api_keys_memberships_auth_user_api_key_id_idx ON authz_api_keys_memberships(auth_user_api_key_id);
CREATE INDEX authz_api_keys_memberships_membership_id_idx ON authz_api_keys_memberships(membership_id);

View File

@@ -21,10 +21,10 @@ import (
"maps"
"time"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
)
type (
@@ -161,7 +161,7 @@ FROM
INNER JOIN
user_org ON organizations.id = user_org.organization_id
WHERE
%S
%s
AND %s
`
@@ -237,6 +237,60 @@ ORDER BY
return nil
}
func (o *Organizations) LoadAllByUserAPIKeyID(
ctx context.Context,
conn pg.Conn,
userAPIKeyID gid.GID,
) error {
q := `
WITH user_api_key_org AS (
SELECT
am.organization_id
FROM
authz_api_keys_memberships akm
INNER JOIN
authz_memberships am ON akm.membership_id = am.id
WHERE
akm.auth_user_api_key_id = @auth_user_api_key_id
)
SELECT
tenant_id,
id,
name,
description,
website_url,
email,
headquarter_address,
custom_domain_id,
logo_file_id,
horizontal_logo_file_id,
created_at,
updated_at
FROM
organizations
INNER JOIN
user_api_key_org ON organizations.id = user_api_key_org.organization_id
ORDER BY
name ASC
`
args := pgx.StrictNamedArgs{"auth_user_api_key_id": userAPIKeyID}
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query organizations: %w", err)
}
organizations, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Organization])
if err != nil {
return fmt.Errorf("cannot collect organizations: %w", err)
}
*o = organizations
return nil
}
func (o *Organization) Insert(
ctx context.Context,
conn pg.Conn,

View File

@@ -0,0 +1,210 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package coredata
import (
"context"
"errors"
"fmt"
"time"
"github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/gid"
)
type (
UserAPIKey struct {
ID gid.GID `db:"id"`
UserID gid.GID `db:"user_id"`
Name string `db:"name"`
ExpiresAt time.Time `db:"expires_at"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
UserAPIKeys []*UserAPIKey
ErrUserAPIKeyNotFound struct {
Identifier string
}
)
func (e ErrUserAPIKeyNotFound) Error() string {
return fmt.Sprintf("user api key not found: %q", e.Identifier)
}
func (a *UserAPIKey) LoadByID(
ctx context.Context,
conn pg.Conn,
apiKeyID gid.GID,
) error {
q := `
SELECT
id,
user_id,
name,
expires_at,
created_at,
updated_at
FROM
auth_user_api_keys
WHERE
id = @api_key_id
LIMIT 1;
`
args := pgx.StrictNamedArgs{"api_key_id": apiKeyID}
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query user api key: %w", err)
}
apiKey, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[UserAPIKey])
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return &ErrUserAPIKeyNotFound{Identifier: apiKeyID.String()}
}
return fmt.Errorf("cannot collect user api key: %w", err)
}
*a = apiKey
return nil
}
func (a *UserAPIKeys) LoadByUserID(
ctx context.Context,
conn pg.Conn,
userID gid.GID,
) error {
q := `
SELECT
id,
user_id,
name,
expires_at,
created_at,
updated_at
FROM
auth_user_api_keys
WHERE
user_id = @user_id
ORDER BY created_at DESC;
`
args := pgx.StrictNamedArgs{"user_id": userID}
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query user api keys: %w", err)
}
apiKeys, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[UserAPIKey])
if err != nil {
return fmt.Errorf("cannot collect user api keys: %w", err)
}
*a = apiKeys
return nil
}
func (a *UserAPIKey) Insert(
ctx context.Context,
conn pg.Conn,
) error {
q := `
INSERT INTO
auth_user_api_keys (id, user_id, name, expires_at, created_at, updated_at)
VALUES (
@api_key_id,
@user_id,
@name,
@expires_at,
@created_at,
@updated_at
)
`
args := pgx.StrictNamedArgs{
"api_key_id": a.ID,
"user_id": a.UserID,
"name": a.Name,
"expires_at": a.ExpiresAt,
"created_at": a.CreatedAt,
"updated_at": a.UpdatedAt,
}
_, err := conn.Exec(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot insert user api key: %w", err)
}
return nil
}
func (a *UserAPIKey) Update(
ctx context.Context,
conn pg.Conn,
) error {
q := `
UPDATE
auth_user_api_keys
SET
name = @name,
expires_at = @expires_at,
updated_at = @updated_at
WHERE
id = @api_key_id
`
args := pgx.StrictNamedArgs{
"api_key_id": a.ID,
"name": a.Name,
"expires_at": a.ExpiresAt,
"updated_at": a.UpdatedAt,
}
_, err := conn.Exec(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot update user api key: %w", err)
}
return nil
}
func (a *UserAPIKey) Delete(
ctx context.Context,
conn pg.Conn,
) error {
q := `
DELETE FROM
auth_user_api_keys
WHERE
id = @api_key_id
`
args := pgx.StrictNamedArgs{"api_key_id": a.ID}
_, err := conn.Exec(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot delete user api key: %w", err)
}
return nil
}

View File

@@ -15,6 +15,10 @@ import (
"unicode"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/jackc/pgx/v5"
"go.gearno.de/crypto/uuid"
"go.gearno.de/kit/pg"
"go.gearno.de/x/ref"
"go.probo.inc/probo/packages/emails"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/docgen"
@@ -23,10 +27,6 @@ import (
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/statelesstoken"
"go.probo.inc/probo/pkg/watermarkpdf"
"github.com/jackc/pgx/v5"
"go.gearno.de/crypto/uuid"
"go.gearno.de/kit/pg"
"go.gearno.de/x/ref"
)
type (

View File

@@ -21,12 +21,12 @@ import (
"net/url"
"time"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/packages/emails"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/statelesstoken"
"go.gearno.de/kit/pg"
)
type (

View File

@@ -20,16 +20,16 @@ call_argument_directives_with_null: true
models:
ID:
model:
- "github.com/getprobo/probo/pkg/server/graphql/types/gid.GIDScalar"
- "go.probo.inc/probo/pkg/server/gqlutils/types/gid.GIDScalar"
Datetime:
model:
- "github.com/99designs/gqlgen/graphql.Time"
CursorKey:
model:
- "github.com/getprobo/probo/pkg/server/graphql/types/cursor.CursorKeyScalar"
- "go.probo.inc/probo/pkg/server/gqlutils/types/cursor.CursorKeyScalar"
Duration:
model:
- "github.com/99designs/gqlgen/graphql.Duration"
BigInt:
model:
- "github.com/getprobo/probo/pkg/server/graphql/types/bigint.BigIntScalar"
- "go.probo.inc/probo/pkg/server/gqlutils/types/bigint.BigIntScalar"

View File

@@ -30,6 +30,11 @@ import (
"github.com/99designs/gqlgen/graphql/handler/extension"
"github.com/99designs/gqlgen/graphql/handler/transport"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/go-chi/chi/v5"
"github.com/vektah/gqlparser/v2/gqlerror"
"go.gearno.de/crypto/uuid"
"go.gearno.de/kit/httpserver"
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/auth"
"go.probo.inc/probo/pkg/authz"
"go.probo.inc/probo/pkg/connector"
@@ -38,14 +43,9 @@ import (
"go.probo.inc/probo/pkg/probo"
"go.probo.inc/probo/pkg/saferedirect"
"go.probo.inc/probo/pkg/server/api/console/v1/schema"
gqlutils "go.probo.inc/probo/pkg/server/graphql"
"go.probo.inc/probo/pkg/server/gqlutils"
"go.probo.inc/probo/pkg/server/session"
"go.probo.inc/probo/pkg/statelesstoken"
"github.com/go-chi/chi/v5"
"github.com/vektah/gqlparser/v2/gqlerror"
"go.gearno.de/crypto/uuid"
"go.gearno.de/kit/httpserver"
"go.gearno.de/kit/log"
)
type (
@@ -77,6 +77,7 @@ var (
sessionContextKey = &ctxKey{name: "session"}
userContextKey = &ctxKey{name: "user"}
userTenantContextKey = &ctxKey{name: "user_tenants"}
userAPIKeyContextKey = &ctxKey{name: "user_api_key"}
)
func SessionFromContext(ctx context.Context) *coredata.Session {
@@ -89,6 +90,11 @@ func UserFromContext(ctx context.Context) *coredata.User {
return user
}
func UserAPIKeyFromContext(ctx context.Context) *coredata.UserAPIKey {
userAPIKey, _ := ctx.Value(userAPIKeyContextKey).(*coredata.UserAPIKey)
return userAPIKey
}
func NewMux(
logger *log.Logger,
proboSvc *probo.Service,
@@ -357,6 +363,11 @@ func WithSession(authSvc *auth.Service, authzSvc *authz.Service, authCfg AuthCon
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if authCtx := tryAPIKeyAuth(ctx, r, authSvc, authzSvc); authCtx != nil {
next(w, r.WithContext(authCtx))
return
}
sessionAuthCfg := session.AuthConfig{
CookieName: authCfg.CookieName,
CookieSecret: authCfg.CookieSecret,
@@ -402,6 +413,43 @@ func WithSession(authSvc *auth.Service, authzSvc *authz.Service, authCfg AuthCon
}
}
func tryAPIKeyAuth(ctx context.Context, r *http.Request, authSvc *auth.Service, authzSvc *authz.Service) context.Context {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
return nil
}
if !strings.HasPrefix(authHeader, "Bearer ") {
return nil
}
apiKeyString := strings.TrimPrefix(authHeader, "Bearer ")
user, userAPIKey, err := authSvc.ValidateUserAPIKey(ctx, apiKeyString)
if err != nil {
return nil
}
organizations, err := authzSvc.GetAllOrganizationsForUserAPIKeyId(ctx, userAPIKey.ID)
if err != nil {
return nil
}
tenantIDs := make([]gid.TenantID, 0, len(organizations))
for _, org := range organizations {
tenantIDs = append(tenantIDs, org.ID.TenantID())
}
ctx = context.WithValue(ctx, userContextKey, user)
ctx = context.WithValue(ctx, userAPIKeyContextKey, userAPIKey)
ctx = context.WithValue(ctx, userTenantContextKey, &userTenantAccess{
tenantIDs: tenantIDs,
authErrors: make(map[gid.TenantID]error),
})
return ctx
}
func (r *Resolver) ProboService(ctx context.Context, tenantID gid.TenantID) *probo.TenantService {
return GetTenantService(ctx, r.proboSvc, tenantID)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -16,7 +16,7 @@ package types
import (
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/server/graphql/types/cursor"
"go.probo.inc/probo/pkg/server/gqlutils/types/cursor"
)
func NewCursor[O page.OrderField](

View File

@@ -16,7 +16,7 @@ package types
import (
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/server/graphql/types/pageinfo"
"go.probo.inc/probo/pkg/server/gqlutils/types/pageinfo"
)
func NewPageInfo[T page.Paginable[O], O page.OrderField](p *page.Page[T, O]) *PageInfo {

View File

@@ -13,6 +13,8 @@ import (
"strings"
"time"
pgx "github.com/jackc/pgx/v5"
"github.com/vektah/gqlparser/v2/gqlerror"
"go.probo.inc/probo/pkg/auth"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
@@ -20,9 +22,7 @@ import (
"go.probo.inc/probo/pkg/probo"
"go.probo.inc/probo/pkg/server/api/console/v1/schema"
"go.probo.inc/probo/pkg/server/api/console/v1/types"
"go.probo.inc/probo/pkg/server/graphql"
pgx "github.com/jackc/pgx/v5"
"github.com/vektah/gqlparser/v2/gqlerror"
"go.probo.inc/probo/pkg/server/gqlutils"
)
// Owner is the resolver for the owner field.
@@ -33,7 +33,7 @@ func (r *assetResolver) Owner(ctx context.Context, obj *types.Asset) (*types.Peo
if err != nil {
var errNotFound *coredata.ErrAssetNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get asset: %w", err))
}
@@ -42,7 +42,7 @@ func (r *assetResolver) Owner(ctx context.Context, obj *types.Asset) (*types.Peo
if err != nil {
var errNotFound *coredata.ErrPeopleNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get owner: %w", err))
}
@@ -103,7 +103,7 @@ func (r *assetResolver) Organization(ctx context.Context, obj *types.Asset) (*ty
if err != nil {
var errNotFound *coredata.ErrOrganizationNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get organization: %w", err))
}
@@ -140,7 +140,7 @@ func (r *auditResolver) Organization(ctx context.Context, obj *types.Audit) (*ty
if err != nil {
var errNotFound *coredata.ErrAuditNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot load audit: %w", err))
}
@@ -149,7 +149,7 @@ func (r *auditResolver) Organization(ctx context.Context, obj *types.Audit) (*ty
if err != nil {
var errNotFound *coredata.ErrOrganizationNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot load organization: %w", err))
}
@@ -165,7 +165,7 @@ func (r *auditResolver) Framework(ctx context.Context, obj *types.Audit) (*types
if err != nil {
var errNotFound *coredata.ErrAuditNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot load audit: %w", err))
}
@@ -174,7 +174,7 @@ func (r *auditResolver) Framework(ctx context.Context, obj *types.Audit) (*types
if err != nil {
var errNotFound *coredata.ErrFrameworkNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot load framework: %w", err))
}
@@ -190,7 +190,7 @@ func (r *auditResolver) Report(ctx context.Context, obj *types.Audit) (*types.Re
if err != nil {
var errNotFound *coredata.ErrAuditNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot load audit: %w", err))
}
@@ -277,7 +277,7 @@ func (r *continualImprovementResolver) Organization(ctx context.Context, obj *ty
if err != nil {
var errNotFound *coredata.ErrOrganizationNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get continual improvement organization: %w", err))
}
@@ -298,7 +298,7 @@ func (r *continualImprovementResolver) Owner(ctx context.Context, obj *types.Con
if err != nil {
var errNotFound *coredata.ErrPeopleNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get continual improvement owner: %w", err))
}
@@ -335,7 +335,7 @@ func (r *controlResolver) Framework(ctx context.Context, obj *types.Control) (*t
if err != nil {
var errNotFound *coredata.ErrControlNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get control: %w", err))
}
@@ -344,7 +344,7 @@ func (r *controlResolver) Framework(ctx context.Context, obj *types.Control) (*t
if err != nil {
var errNotFound *coredata.ErrFrameworkNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get framework: %w", err))
}
@@ -516,7 +516,7 @@ func (r *datumResolver) Owner(ctx context.Context, obj *types.Datum) (*types.Peo
if err != nil {
var errNotFound *coredata.ErrPeopleNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
return nil, fmt.Errorf("cannot get owner: %w", err)
}
@@ -557,7 +557,7 @@ func (r *datumResolver) Organization(ctx context.Context, obj *types.Datum) (*ty
if err != nil {
var errNotFound *coredata.ErrOrganizationNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get organization: %w", err))
}
@@ -594,7 +594,7 @@ func (r *documentResolver) Owner(ctx context.Context, obj *types.Document) (*typ
if err != nil {
var errNotFound *coredata.ErrDocumentNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get document: %w", err))
}
@@ -604,7 +604,7 @@ func (r *documentResolver) Owner(ctx context.Context, obj *types.Document) (*typ
if err != nil {
var errNotFound *coredata.ErrPeopleNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get owner: %w", err))
}
@@ -620,7 +620,7 @@ func (r *documentResolver) Organization(ctx context.Context, obj *types.Document
if err != nil {
var errNotFound *coredata.ErrDocumentNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get document: %w", err))
}
@@ -629,7 +629,7 @@ func (r *documentResolver) Organization(ctx context.Context, obj *types.Document
if err != nil {
var errNotFound *coredata.ErrOrganizationNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get organization: %w", err))
}
@@ -733,7 +733,7 @@ func (r *documentVersionResolver) Document(ctx context.Context, obj *types.Docum
if err != nil {
var errNotFound *coredata.ErrDocumentNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get document: %w", err))
}
@@ -754,7 +754,7 @@ func (r *documentVersionResolver) Owner(ctx context.Context, obj *types.Document
if err != nil {
var errNotFound *coredata.ErrPeopleNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get owner: %w", err))
}
@@ -823,7 +823,7 @@ func (r *documentVersionSignatureResolver) SignedBy(ctx context.Context, obj *ty
if err != nil {
var errNotFound *coredata.ErrPeopleNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get people: %w", err))
}
@@ -848,7 +848,7 @@ func (r *evidenceResolver) File(ctx context.Context, obj *types.Evidence) (*type
if err != nil {
var errNotFound *coredata.ErrFileNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot load evidence file: %w", err))
}
@@ -873,7 +873,7 @@ func (r *evidenceResolver) Task(ctx context.Context, obj *types.Evidence) (*type
if err != nil {
var errNotFound *coredata.ErrTaskNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot load task: %w", err))
}
@@ -894,7 +894,7 @@ func (r *evidenceResolver) Measure(ctx context.Context, obj *types.Evidence) (*t
if err != nil {
var errNotFound *coredata.ErrMeasureNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot load measure: %w", err))
}
@@ -944,7 +944,7 @@ func (r *frameworkResolver) Organization(ctx context.Context, obj *types.Framewo
if err != nil {
var errNotFound *coredata.ErrFrameworkNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot load framework: %w", err))
}
@@ -953,7 +953,7 @@ func (r *frameworkResolver) Organization(ctx context.Context, obj *types.Framewo
if err != nil {
var errNotFound *coredata.ErrOrganizationNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot load organization: %w", err))
}
@@ -1227,11 +1227,11 @@ func (r *mutationResolver) CreateOrganization(ctx context.Context, input types.C
if err != nil {
var errAlreadyExists *coredata.ErrOrganizationAlreadyExists
if errors.As(err, &errAlreadyExists) {
return nil, graphql.Conflict(errAlreadyExists)
return nil, gqlutils.Conflict(errAlreadyExists)
}
var errTrustCenterAlreadyExists *coredata.ErrTrustCenterAlreadyExists
if errors.As(err, &errTrustCenterAlreadyExists) {
return nil, graphql.Conflict(errTrustCenterAlreadyExists)
return nil, gqlutils.Conflict(errTrustCenterAlreadyExists)
}
panic(fmt.Errorf("cannot create organization: %w", err))
}
@@ -1260,7 +1260,7 @@ func (r *mutationResolver) CreateOrganization(ctx context.Context, input types.C
if err != nil {
var errAlreadyExists *coredata.ErrPeopleAlreadyExists
if errors.As(err, &errAlreadyExists) {
return nil, graphql.Conflict(errAlreadyExists)
return nil, gqlutils.Conflict(errAlreadyExists)
}
panic(fmt.Errorf("cannot create people: %w", err))
}
@@ -1408,7 +1408,7 @@ func (r *mutationResolver) CreateTrustCenterAccess(ctx context.Context, input ty
if err != nil {
var errAlreadyExists *coredata.ErrTrustCenterAccessAlreadyExists
if errors.As(err, &errAlreadyExists) {
return nil, graphql.Conflict(errAlreadyExists)
return nil, gqlutils.Conflict(errAlreadyExists)
}
panic(fmt.Errorf("cannot create trust center access: %w", err))
}
@@ -1632,7 +1632,7 @@ func (r *mutationResolver) InviteUser(ctx context.Context, input types.InviteUse
if err != nil {
var errAlreadyExists *coredata.ErrPeopleAlreadyExists
if errors.As(err, &errAlreadyExists) {
return nil, graphql.Conflict(errAlreadyExists)
return nil, gqlutils.Conflict(errAlreadyExists)
}
return nil, fmt.Errorf("cannot create people record: %w", err)
}
@@ -1697,7 +1697,7 @@ func (r *mutationResolver) CreatePeople(ctx context.Context, input types.CreateP
if err != nil {
var errAlreadyExists *coredata.ErrPeopleAlreadyExists
if errors.As(err, &errAlreadyExists) {
return nil, graphql.Conflict(errAlreadyExists)
return nil, gqlutils.Conflict(errAlreadyExists)
}
panic(fmt.Errorf("cannot create people: %w", err))
}
@@ -1776,7 +1776,7 @@ func (r *mutationResolver) CreateVendor(ctx context.Context, input types.CreateV
if err != nil {
var errAlreadyExists *coredata.ErrVendorAlreadyExists
if errors.As(err, &errAlreadyExists) {
return nil, graphql.Conflict(errAlreadyExists)
return nil, gqlutils.Conflict(errAlreadyExists)
}
return nil, fmt.Errorf("cannot create vendor: %w", err)
}
@@ -2079,7 +2079,7 @@ func (r *mutationResolver) CreateControl(ctx context.Context, input types.Create
if err != nil {
var errAlreadyExists *coredata.ErrControlAlreadyExists
if errors.As(err, &errAlreadyExists) {
return nil, graphql.Conflict(errAlreadyExists)
return nil, gqlutils.Conflict(errAlreadyExists)
}
panic(fmt.Errorf("cannot create control: %w", err))
}
@@ -2105,7 +2105,7 @@ func (r *mutationResolver) UpdateControl(ctx context.Context, input types.Update
if err != nil {
var errAlreadyExists *coredata.ErrControlAlreadyExists
if errors.As(err, &errAlreadyExists) {
return nil, graphql.Conflict(errAlreadyExists)
return nil, gqlutils.Conflict(errAlreadyExists)
}
panic(fmt.Errorf("cannot update control: %w", err))
}
@@ -2142,7 +2142,7 @@ func (r *mutationResolver) CreateMeasure(ctx context.Context, input types.Create
if err != nil {
var errAlreadyExists *coredata.ErrMeasureAlreadyExists
if errors.As(err, &errAlreadyExists) {
return nil, graphql.Conflict(errAlreadyExists)
return nil, gqlutils.Conflict(errAlreadyExists)
}
panic(fmt.Errorf("cannot create measure: %w", err))
}
@@ -2233,7 +2233,7 @@ func (r *mutationResolver) CreateControlDocumentMapping(ctx context.Context, inp
if err != nil {
var errMappingExists *coredata.ErrControlDocumentMappingAlreadyExists
if errors.As(err, &errMappingExists) {
return nil, graphql.Conflict(errMappingExists)
return nil, gqlutils.Conflict(errMappingExists)
}
panic(fmt.Errorf("cannot create control document mapping: %w", err))
}
@@ -2349,7 +2349,7 @@ func (r *mutationResolver) CreateTask(ctx context.Context, input types.CreateTas
if err != nil {
var errAlreadyExists *coredata.ErrTaskAlreadyExists
if errors.As(err, &errAlreadyExists) {
return nil, graphql.Conflict(errAlreadyExists)
return nil, gqlutils.Conflict(errAlreadyExists)
}
panic(fmt.Errorf("cannot create task: %w", err))
}
@@ -2445,7 +2445,7 @@ func (r *mutationResolver) CreateRisk(ctx context.Context, input types.CreateRis
if err != nil {
var errAlreadyExists *coredata.ErrRiskAlreadyExists
if errors.As(err, &errAlreadyExists) {
return nil, graphql.Conflict(errAlreadyExists)
return nil, gqlutils.Conflict(errAlreadyExists)
}
panic(fmt.Errorf("cannot create risk: %w", err))
}
@@ -2799,7 +2799,7 @@ func (r *mutationResolver) CreateDocument(ctx context.Context, input types.Creat
if err != nil {
var errAlreadyExists *coredata.ErrDocumentAlreadyExists
if errors.As(err, &errAlreadyExists) {
return nil, graphql.Conflict(errAlreadyExists)
return nil, gqlutils.Conflict(errAlreadyExists)
}
panic(fmt.Errorf("cannot create document: %w", err))
}
@@ -2858,7 +2858,7 @@ func (r *mutationResolver) PublishDocumentVersion(ctx context.Context, input typ
if err != nil {
var errNoChanges *coredata.ErrDocumentVersionNoChanges
if errors.As(err, &errNoChanges) {
return nil, graphql.Invalid(errNoChanges)
return nil, gqlutils.Invalid(errNoChanges)
}
panic(fmt.Errorf("cannot publish document version: %w", err))
}
@@ -3939,7 +3939,7 @@ func (r *nonconformityResolver) Organization(ctx context.Context, obj *types.Non
if err != nil {
var errNotFound *coredata.ErrOrganizationNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get nonconformity organization: %w", err))
}
@@ -3960,7 +3960,7 @@ func (r *nonconformityResolver) Audit(ctx context.Context, obj *types.Nonconform
if err != nil {
var errNotFound *coredata.ErrAuditNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get nonconformity audit: %w", err))
}
@@ -3981,7 +3981,7 @@ func (r *nonconformityResolver) Owner(ctx context.Context, obj *types.Nonconform
if err != nil {
var errNotFound *coredata.ErrPeopleNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get nonconformity owner: %w", err))
}
@@ -4023,7 +4023,7 @@ func (r *obligationResolver) Organization(ctx context.Context, obj *types.Obliga
if err != nil {
var errNotFound *coredata.ErrOrganizationNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get obligation organization: %w", err))
}
@@ -4044,7 +4044,7 @@ func (r *obligationResolver) Owner(ctx context.Context, obj *types.Obligation) (
if err != nil {
var errNotFound *coredata.ErrPeopleNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get obligation owner: %w", err))
}
@@ -4742,7 +4742,7 @@ func (r *processingActivityResolver) Organization(ctx context.Context, obj *type
if err != nil {
var errNotFound *coredata.ErrOrganizationNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get organization: %w", err))
}
@@ -4806,7 +4806,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error
if err != nil {
var errNotFound *coredata.ErrOrganizationNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get organization: %w", err))
}
@@ -4817,7 +4817,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error
if err != nil {
var errNotFound *coredata.ErrPeopleNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get people: %w", err))
}
@@ -4828,7 +4828,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error
if err != nil {
var errNotFound *coredata.ErrVendorNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get vendor: %w", err))
}
@@ -4839,7 +4839,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error
if err != nil {
var errNotFound *coredata.ErrFrameworkNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get framework: %w", err))
}
@@ -4850,7 +4850,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error
if err != nil {
var errNotFound *coredata.ErrMeasureNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get measure: %w", err))
}
@@ -4861,7 +4861,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error
if err != nil {
var errNotFound *coredata.ErrTaskNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get task: %w", err))
}
@@ -4879,7 +4879,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error
if err != nil {
var errNotFound *coredata.ErrDocumentNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get document: %w", err))
}
@@ -4889,7 +4889,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error
if err != nil {
var errNotFound *coredata.ErrControlNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get control: %w", err))
}
@@ -4900,7 +4900,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error
if err != nil {
var errNotFound *coredata.ErrRiskNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get risk: %w", err))
}
@@ -4940,7 +4940,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error
if err != nil {
var errNotFound *coredata.ErrAssetNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get asset: %w", err))
}
@@ -4956,7 +4956,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error
if err != nil {
var errNotFound *coredata.ErrAuditNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get audit: %w", err))
}
@@ -5021,9 +5021,19 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error
func (r *queryResolver) Viewer(ctx context.Context) (*types.Viewer, error) {
user := UserFromContext(ctx)
session := SessionFromContext(ctx)
apiKey := UserAPIKeyFromContext(ctx)
var viewerID gid.GID
if session != nil {
viewerID = session.ID
} else if apiKey != nil {
viewerID = apiKey.ID
} else {
viewerID = user.ID
}
return &types.Viewer{
ID: session.ID,
ID: viewerID,
User: types.NewUser(user),
}, nil
}
@@ -5060,7 +5070,7 @@ func (r *riskResolver) Owner(ctx context.Context, obj *types.Risk) (*types.Peopl
if err != nil {
var errNotFound *coredata.ErrRiskNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get risk: %w", err))
}
@@ -5073,7 +5083,7 @@ func (r *riskResolver) Owner(ctx context.Context, obj *types.Risk) (*types.Peopl
if err != nil {
var errNotFound *coredata.ErrPeopleNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get owner: %w", err))
}
@@ -5089,7 +5099,7 @@ func (r *riskResolver) Organization(ctx context.Context, obj *types.Risk) (*type
if err != nil {
var errNotFound *coredata.ErrRiskNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get risk: %w", err))
}
@@ -5098,7 +5108,7 @@ func (r *riskResolver) Organization(ctx context.Context, obj *types.Risk) (*type
if err != nil {
var errNotFound *coredata.ErrOrganizationNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get organization: %w", err))
}
@@ -5296,7 +5306,7 @@ func (r *snapshotResolver) Organization(ctx context.Context, obj *types.Snapshot
if err != nil {
var errNotFound *coredata.ErrOrganizationNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get organization: %w", err))
}
@@ -5358,7 +5368,7 @@ func (r *taskResolver) AssignedTo(ctx context.Context, obj *types.Task) (*types.
if err != nil {
var errNotFound *coredata.ErrTaskNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get task: %w", err))
}
@@ -5371,7 +5381,7 @@ func (r *taskResolver) AssignedTo(ctx context.Context, obj *types.Task) (*types.
if err != nil {
var errNotFound *coredata.ErrPeopleNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get assigned to: %w", err))
}
@@ -5387,7 +5397,7 @@ func (r *taskResolver) Organization(ctx context.Context, obj *types.Task) (*type
if err != nil {
var errNotFound *coredata.ErrTaskNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get task: %w", err))
}
@@ -5396,7 +5406,7 @@ func (r *taskResolver) Organization(ctx context.Context, obj *types.Task) (*type
if err != nil {
var errNotFound *coredata.ErrOrganizationNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get organization: %w", err))
}
@@ -5412,7 +5422,7 @@ func (r *taskResolver) Measure(ctx context.Context, obj *types.Task) (*types.Mea
if err != nil {
var errNotFound *coredata.ErrTaskNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get task: %w", err))
}
@@ -5421,7 +5431,7 @@ func (r *taskResolver) Measure(ctx context.Context, obj *types.Task) (*types.Mea
if err != nil {
var errNotFound *coredata.ErrMeasureNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get measure: %w", err))
}
@@ -5495,7 +5505,7 @@ func (r *trustCenterResolver) Organization(ctx context.Context, obj *types.Trust
if err != nil {
var errNotFound *coredata.ErrOrganizationNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get organization: %w", err))
}
@@ -5614,7 +5624,7 @@ func (r *trustCenterDocumentAccessResolver) Document(ctx context.Context, obj *t
if err != nil {
var errNotFound *coredata.ErrDocumentNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
return nil, fmt.Errorf("cannot load document: %w", err)
}
@@ -5691,7 +5701,7 @@ func (r *trustCenterFileResolver) Organization(ctx context.Context, obj *types.T
if err != nil {
var errNotFound *coredata.ErrOrganizationNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get organization: %w", err))
}
@@ -5756,7 +5766,7 @@ func (r *vendorResolver) Organization(ctx context.Context, obj *types.Vendor) (*
if err != nil {
var errNotFound *coredata.ErrVendorNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get vendor: %w", err))
}
@@ -5765,7 +5775,7 @@ func (r *vendorResolver) Organization(ctx context.Context, obj *types.Vendor) (*
if err != nil {
var errNotFound *coredata.ErrOrganizationNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get organization: %w", err))
}
@@ -5913,7 +5923,7 @@ func (r *vendorResolver) BusinessOwner(ctx context.Context, obj *types.Vendor) (
if err != nil {
var errNotFound *coredata.ErrVendorNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get vendor: %w", err))
}
@@ -5926,7 +5936,7 @@ func (r *vendorResolver) BusinessOwner(ctx context.Context, obj *types.Vendor) (
if err != nil {
var errNotFound *coredata.ErrPeopleNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get business owner: %w", err))
}
@@ -5941,7 +5951,7 @@ func (r *vendorResolver) SecurityOwner(ctx context.Context, obj *types.Vendor) (
if err != nil {
var errNotFound *coredata.ErrVendorNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get vendor: %w", err))
}
@@ -5954,7 +5964,7 @@ func (r *vendorResolver) SecurityOwner(ctx context.Context, obj *types.Vendor) (
if err != nil {
var errNotFound *coredata.ErrPeopleNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get security owner: %w", err))
}
@@ -5970,7 +5980,7 @@ func (r *vendorBusinessAssociateAgreementResolver) Vendor(ctx context.Context, o
if err != nil {
var errNotFound *coredata.ErrVendorNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
return nil, fmt.Errorf("cannot get vendor: %w", err)
}
@@ -5998,7 +6008,7 @@ func (r *vendorComplianceReportResolver) Vendor(ctx context.Context, obj *types.
if err != nil {
var errNotFound *coredata.ErrVendorNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get vendor: %w", err))
}
@@ -6023,7 +6033,7 @@ func (r *vendorComplianceReportResolver) File(ctx context.Context, obj *types.Ve
if err != nil {
var errNotFound *coredata.ErrFileNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot load evidence file: %w", err))
}
@@ -6073,7 +6083,7 @@ func (r *vendorContactResolver) Vendor(ctx context.Context, obj *types.VendorCon
if err != nil {
var errNotFound *coredata.ErrVendorNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get vendor: %w", err))
}
@@ -6089,7 +6099,7 @@ func (r *vendorDataPrivacyAgreementResolver) Vendor(ctx context.Context, obj *ty
if err != nil {
var errNotFound *coredata.ErrVendorNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get vendor: %w", err))
}
@@ -6117,7 +6127,7 @@ func (r *vendorRiskAssessmentResolver) Vendor(ctx context.Context, obj *types.Ve
if err != nil {
var errNotFound *coredata.ErrVendorNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get vendor: %w", err))
}
@@ -6139,7 +6149,7 @@ func (r *vendorServiceResolver) Vendor(ctx context.Context, obj *types.VendorSer
if err != nil {
var errNotFound *coredata.ErrVendorNotFound
if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound)
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get vendor: %w", err))
}

View File

@@ -20,10 +20,10 @@ call_argument_directives_with_null: true
models:
ID:
model:
- "github.com/getprobo/probo/pkg/server/graphql/types/gid.GIDScalar"
- "go.probo.inc/probo/pkg/server/gqlutils/types/gid.GIDScalar"
Datetime:
model:
- "github.com/99designs/gqlgen/graphql.Time"
CursorKey:
model:
- "github.com/getprobo/probo/pkg/server/graphql/types/cursor.CursorKeyScalar"
- "go.probo.inc/probo/pkg/server/gqlutils/types/cursor.CursorKeyScalar"

View File

@@ -26,6 +26,8 @@ import (
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/handler/extension"
"github.com/99designs/gqlgen/graphql/handler/transport"
"github.com/go-chi/chi/v5"
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/auth"
"go.probo.inc/probo/pkg/authz"
"go.probo.inc/probo/pkg/coredata"
@@ -34,12 +36,10 @@ import (
console_v1 "go.probo.inc/probo/pkg/server/api/console/v1"
"go.probo.inc/probo/pkg/server/api/trust/v1/schema"
"go.probo.inc/probo/pkg/server/api/trust/v1/trustauth"
gqlutils "go.probo.inc/probo/pkg/server/graphql"
"go.probo.inc/probo/pkg/server/gqlutils"
"go.probo.inc/probo/pkg/server/session"
"go.probo.inc/probo/pkg/statelesstoken"
"go.probo.inc/probo/pkg/trust"
"github.com/go-chi/chi/v5"
"go.gearno.de/kit/log"
)
type (

View File

@@ -45,14 +45,14 @@ type Organization implements Node {
}
enum DocumentType
@goModel(model: "github.com/getprobo/probo/pkg/coredata.DocumentType") {
@goModel(model: "go.probo.inc/probo/pkg/coredata.DocumentType") {
OTHER
@goEnum(value: "github.com/getprobo/probo/pkg/coredata.DocumentTypeOther")
ISMS @goEnum(value: "github.com/getprobo/probo/pkg/coredata.DocumentTypeISMS")
@goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypeOther")
ISMS @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypeISMS")
POLICY
@goEnum(value: "github.com/getprobo/probo/pkg/coredata.DocumentTypePolicy")
@goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypePolicy")
PROCEDURE
@goEnum(value: "github.com/getprobo/probo/pkg/coredata.DocumentTypeProcedure")
@goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentTypeProcedure")
}
type Document implements Node {
@@ -102,340 +102,340 @@ type AuditEdge {
}
enum CountryCode
@goModel(model: "github.com/getprobo/probo/pkg/coredata.CountryCode") {
AD @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeAD")
AE @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeAE")
AF @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeAF")
AG @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeAG")
AI @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeAI")
AL @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeAL")
AM @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeAM")
AO @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeAO")
AQ @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeAQ")
AR @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeAR")
AS @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeAS")
AT @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeAT")
AU @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeAU")
AW @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeAW")
AX @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeAX")
AZ @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeAZ")
BA @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeBA")
BB @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeBB")
BD @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeBD")
BE @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeBE")
BF @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeBF")
BG @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeBG")
BH @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeBH")
BI @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeBI")
BJ @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeBJ")
BL @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeBL")
BM @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeBM")
BN @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeBN")
BO @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeBO")
BQ @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeBQ")
BR @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeBR")
BS @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeBS")
BT @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeBT")
BV @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeBV")
BW @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeBW")
BY @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeBY")
BZ @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeBZ")
CA @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeCA")
CC @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeCC")
CD @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeCD")
CF @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeCF")
CG @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeCG")
CH @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeCH")
CI @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeCI")
CK @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeCK")
CL @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeCL")
CM @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeCM")
CN @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeCN")
CO @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeCO")
CR @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeCR")
CU @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeCU")
CV @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeCV")
CW @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeCW")
CX @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeCX")
CY @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeCY")
CZ @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeCZ")
DE @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeDE")
DJ @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeDJ")
DK @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeDK")
DM @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeDM")
DO @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeDO")
DZ @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeDZ")
EC @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeEC")
EE @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeEE")
EG @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeEG")
EH @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeEH")
ER @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeER")
ES @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeES")
ET @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeET")
EU @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeEU")
FI @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeFI")
FJ @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeFJ")
FK @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeFK")
FM @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeFM")
FO @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeFO")
FR @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeFR")
GA @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeGA")
GB @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeGB")
GD @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeGD")
GE @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeGE")
GF @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeGF")
GG @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeGG")
GH @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeGH")
GI @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeGI")
GL @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeGL")
GM @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeGM")
GN @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeGN")
GP @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeGP")
GQ @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeGQ")
GR @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeGR")
GT @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeGT")
GU @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeGU")
GW @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeGW")
GY @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeGY")
HK @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeHK")
HM @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeHM")
HN @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeHN")
HR @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeHR")
HT @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeHT")
HU @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeHU")
ID @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeID")
IE @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeIE")
IL @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeIL")
IM @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeIM")
IN @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeIN")
IO @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeIO")
IQ @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeIQ")
IR @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeIR")
IS @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeIS")
IT @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeIT")
JE @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeJE")
JM @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeJM")
JO @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeJO")
JP @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeJP")
KE @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeKE")
KG @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeKG")
KH @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeKH")
KI @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeKI")
KM @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeKM")
KN @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeKN")
KP @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeKP")
KR @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeKR")
KW @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeKW")
KY @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeKY")
KZ @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeKZ")
LA @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeLA")
LB @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeLB")
LC @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeLC")
LI @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeLI")
LK @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeLK")
LR @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeLR")
LS @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeLS")
LT @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeLT")
LU @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeLU")
LV @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeLV")
LY @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeLY")
MA @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeMA")
MC @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeMC")
MD @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeMD")
ME @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeME")
MF @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeMF")
MG @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeMG")
MH @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeMH")
MK @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeMK")
ML @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeML")
MM @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeMM")
MN @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeMN")
MO @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeMO")
MP @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeMP")
MQ @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeMQ")
MR @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeMR")
MS @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeMS")
MT @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeMT")
MU @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeMU")
MV @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeMV")
MW @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeMW")
MX @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeMX")
MY @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeMY")
MZ @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeMZ")
NA @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeNA")
NC @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeNC")
NE @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeNE")
NF @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeNF")
NG @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeNG")
NI @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeNI")
NL @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeNL")
NO @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeNO")
NP @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeNP")
NR @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeNR")
NU @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeNU")
NZ @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeNZ")
OM @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeOM")
PA @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodePA")
PE @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodePE")
PF @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodePF")
PG @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodePG")
PH @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodePH")
PK @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodePK")
PL @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodePL")
PM @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodePM")
PN @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodePN")
PR @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodePR")
PS @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodePS")
PT @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodePT")
PW @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodePW")
PY @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodePY")
QA @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeQA")
RE @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeRE")
RO @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeRO")
RS @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeRS")
RU @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeRU")
RW @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeRW")
SA @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeSA")
SB @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeSB")
SC @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeSC")
SD @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeSD")
SE @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeSE")
SG @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeSG")
SH @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeSH")
SI @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeSI")
SJ @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeSJ")
SK @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeSK")
SL @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeSL")
SM @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeSM")
SN @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeSN")
SO @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeSO")
SR @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeSR")
SS @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeSS")
ST @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeST")
SV @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeSV")
SX @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeSX")
SY @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeSY")
SZ @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeSZ")
TC @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeTC")
TD @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeTD")
TF @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeTF")
TG @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeTG")
TH @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeTH")
TJ @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeTJ")
TK @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeTK")
TL @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeTL")
TM @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeTM")
TN @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeTN")
TO @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeTO")
TR @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeTR")
TT @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeTT")
TV @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeTV")
TW @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeTW")
TZ @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeTZ")
UA @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeUA")
UG @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeUG")
UM @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeUM")
US @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeUS")
UY @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeUY")
UZ @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeUZ")
VA @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeVA")
VC @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeVC")
VE @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeVE")
VG @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeVG")
VI @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeVI")
VN @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeVN")
VU @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeVU")
WF @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeWF")
WS @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeWS")
YE @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeYE")
YT @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeYT")
ZA @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeZA")
ZM @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeZM")
ZW @goEnum(value: "github.com/getprobo/probo/pkg/coredata.CountryCodeZW")
@goModel(model: "go.probo.inc/probo/pkg/coredata.CountryCode") {
AD @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeAD")
AE @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeAE")
AF @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeAF")
AG @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeAG")
AI @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeAI")
AL @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeAL")
AM @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeAM")
AO @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeAO")
AQ @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeAQ")
AR @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeAR")
AS @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeAS")
AT @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeAT")
AU @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeAU")
AW @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeAW")
AX @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeAX")
AZ @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeAZ")
BA @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeBA")
BB @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeBB")
BD @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeBD")
BE @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeBE")
BF @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeBF")
BG @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeBG")
BH @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeBH")
BI @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeBI")
BJ @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeBJ")
BL @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeBL")
BM @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeBM")
BN @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeBN")
BO @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeBO")
BQ @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeBQ")
BR @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeBR")
BS @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeBS")
BT @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeBT")
BV @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeBV")
BW @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeBW")
BY @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeBY")
BZ @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeBZ")
CA @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeCA")
CC @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeCC")
CD @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeCD")
CF @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeCF")
CG @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeCG")
CH @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeCH")
CI @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeCI")
CK @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeCK")
CL @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeCL")
CM @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeCM")
CN @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeCN")
CO @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeCO")
CR @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeCR")
CU @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeCU")
CV @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeCV")
CW @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeCW")
CX @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeCX")
CY @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeCY")
CZ @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeCZ")
DE @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeDE")
DJ @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeDJ")
DK @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeDK")
DM @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeDM")
DO @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeDO")
DZ @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeDZ")
EC @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeEC")
EE @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeEE")
EG @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeEG")
EH @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeEH")
ER @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeER")
ES @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeES")
ET @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeET")
EU @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeEU")
FI @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeFI")
FJ @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeFJ")
FK @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeFK")
FM @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeFM")
FO @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeFO")
FR @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeFR")
GA @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeGA")
GB @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeGB")
GD @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeGD")
GE @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeGE")
GF @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeGF")
GG @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeGG")
GH @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeGH")
GI @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeGI")
GL @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeGL")
GM @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeGM")
GN @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeGN")
GP @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeGP")
GQ @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeGQ")
GR @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeGR")
GT @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeGT")
GU @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeGU")
GW @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeGW")
GY @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeGY")
HK @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeHK")
HM @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeHM")
HN @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeHN")
HR @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeHR")
HT @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeHT")
HU @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeHU")
ID @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeID")
IE @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeIE")
IL @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeIL")
IM @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeIM")
IN @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeIN")
IO @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeIO")
IQ @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeIQ")
IR @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeIR")
IS @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeIS")
IT @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeIT")
JE @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeJE")
JM @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeJM")
JO @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeJO")
JP @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeJP")
KE @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeKE")
KG @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeKG")
KH @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeKH")
KI @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeKI")
KM @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeKM")
KN @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeKN")
KP @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeKP")
KR @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeKR")
KW @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeKW")
KY @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeKY")
KZ @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeKZ")
LA @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeLA")
LB @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeLB")
LC @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeLC")
LI @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeLI")
LK @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeLK")
LR @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeLR")
LS @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeLS")
LT @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeLT")
LU @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeLU")
LV @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeLV")
LY @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeLY")
MA @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeMA")
MC @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeMC")
MD @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeMD")
ME @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeME")
MF @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeMF")
MG @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeMG")
MH @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeMH")
MK @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeMK")
ML @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeML")
MM @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeMM")
MN @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeMN")
MO @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeMO")
MP @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeMP")
MQ @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeMQ")
MR @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeMR")
MS @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeMS")
MT @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeMT")
MU @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeMU")
MV @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeMV")
MW @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeMW")
MX @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeMX")
MY @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeMY")
MZ @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeMZ")
NA @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeNA")
NC @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeNC")
NE @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeNE")
NF @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeNF")
NG @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeNG")
NI @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeNI")
NL @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeNL")
NO @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeNO")
NP @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeNP")
NR @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeNR")
NU @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeNU")
NZ @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeNZ")
OM @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeOM")
PA @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodePA")
PE @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodePE")
PF @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodePF")
PG @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodePG")
PH @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodePH")
PK @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodePK")
PL @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodePL")
PM @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodePM")
PN @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodePN")
PR @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodePR")
PS @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodePS")
PT @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodePT")
PW @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodePW")
PY @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodePY")
QA @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeQA")
RE @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeRE")
RO @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeRO")
RS @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeRS")
RU @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeRU")
RW @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeRW")
SA @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeSA")
SB @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeSB")
SC @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeSC")
SD @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeSD")
SE @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeSE")
SG @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeSG")
SH @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeSH")
SI @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeSI")
SJ @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeSJ")
SK @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeSK")
SL @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeSL")
SM @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeSM")
SN @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeSN")
SO @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeSO")
SR @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeSR")
SS @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeSS")
ST @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeST")
SV @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeSV")
SX @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeSX")
SY @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeSY")
SZ @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeSZ")
TC @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeTC")
TD @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeTD")
TF @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeTF")
TG @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeTG")
TH @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeTH")
TJ @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeTJ")
TK @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeTK")
TL @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeTL")
TM @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeTM")
TN @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeTN")
TO @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeTO")
TR @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeTR")
TT @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeTT")
TV @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeTV")
TW @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeTW")
TZ @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeTZ")
UA @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeUA")
UG @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeUG")
UM @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeUM")
US @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeUS")
UY @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeUY")
UZ @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeUZ")
VA @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeVA")
VC @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeVC")
VE @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeVE")
VG @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeVG")
VI @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeVI")
VN @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeVN")
VU @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeVU")
WF @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeWF")
WS @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeWS")
YE @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeYE")
YT @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeYT")
ZA @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeZA")
ZM @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeZM")
ZW @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeZW")
}
enum VendorCategory
@goModel(model: "github.com/getprobo/probo/pkg/coredata.VendorCategory") {
@goModel(model: "go.probo.inc/probo/pkg/coredata.VendorCategory") {
ANALYTICS
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.VendorCategoryAnalytics"
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryAnalytics"
)
CLOUD_MONITORING
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.VendorCategoryCloudMonitoring"
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryCloudMonitoring"
)
CLOUD_PROVIDER
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.VendorCategoryCloudProvider"
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryCloudProvider"
)
COLLABORATION
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.VendorCategoryCollaboration"
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryCollaboration"
)
CUSTOMER_SUPPORT
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.VendorCategoryCustomerSupport"
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryCustomerSupport"
)
DATA_STORAGE_AND_PROCESSING
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.VendorCategoryDataStorageAndProcessing"
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryDataStorageAndProcessing"
)
DOCUMENT_MANAGEMENT
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.VendorCategoryDocumentManagement"
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryDocumentManagement"
)
EMPLOYEE_MANAGEMENT
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.VendorCategoryEmployeeManagement"
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryEmployeeManagement"
)
ENGINEERING
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.VendorCategoryEngineering"
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryEngineering"
)
FINANCE
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.VendorCategoryFinance"
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryFinance"
)
IDENTITY_PROVIDER
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.VendorCategoryIdentityProvider"
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryIdentityProvider"
)
IT @goEnum(value: "github.com/getprobo/probo/pkg/coredata.VendorCategoryIT")
IT @goEnum(value: "go.probo.inc/probo/pkg/coredata.VendorCategoryIT")
MARKETING
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.VendorCategoryMarketing"
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryMarketing"
)
OFFICE_OPERATIONS
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.VendorCategoryOfficeOperations"
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryOfficeOperations"
)
OTHER
@goEnum(value: "github.com/getprobo/probo/pkg/coredata.VendorCategoryOther")
@goEnum(value: "go.probo.inc/probo/pkg/coredata.VendorCategoryOther")
PASSWORD_MANAGEMENT
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.VendorCategoryPasswordManagement"
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryPasswordManagement"
)
PRODUCT_AND_DESIGN
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.VendorCategoryProductAndDesign"
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryProductAndDesign"
)
PROFESSIONAL_SERVICES
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.VendorCategoryProfessionalServices"
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryProfessionalServices"
)
RECRUITING
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.VendorCategoryRecruiting"
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryRecruiting"
)
SALES
@goEnum(value: "github.com/getprobo/probo/pkg/coredata.VendorCategorySales")
@goEnum(value: "go.probo.inc/probo/pkg/coredata.VendorCategorySales")
SECURITY
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.VendorCategorySecurity"
value: "go.probo.inc/probo/pkg/coredata.VendorCategorySecurity"
)
VERSION_CONTROL
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.VendorCategoryVersionControl"
value: "go.probo.inc/probo/pkg/coredata.VendorCategoryVersionControl"
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -21,13 +21,13 @@ import (
"net/http"
"time"
"go.gearno.de/kit/httpserver"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/probo"
console_v1 "go.probo.inc/probo/pkg/server/api/console/v1"
"go.probo.inc/probo/pkg/server/session"
"go.probo.inc/probo/pkg/statelesstoken"
"go.probo.inc/probo/pkg/trust"
"go.gearno.de/kit/httpserver"
)
var (

View File

@@ -16,7 +16,7 @@ package types
import (
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/server/graphql/types/cursor"
"go.probo.inc/probo/pkg/server/gqlutils/types/cursor"
)
func NewCursor[O page.OrderField](

View File

@@ -16,7 +16,7 @@ package types
import (
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/server/graphql/types/pageinfo"
"go.probo.inc/probo/pkg/server/gqlutils/types/pageinfo"
)
func NewPageInfo[T page.Paginable[O], O page.OrderField](p *page.Page[T, O]) *PageInfo {

View File

@@ -10,13 +10,13 @@ import (
"fmt"
"time"
"github.com/vektah/gqlparser/v2/gqlerror"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/server/api/trust/v1/schema"
"go.probo.inc/probo/pkg/server/api/trust/v1/types"
"go.probo.inc/probo/pkg/trust"
"github.com/vektah/gqlparser/v2/gqlerror"
)
// Framework is the resolver for the framework field.

View File

@@ -18,11 +18,11 @@ import (
"net/http"
"time"
"github.com/go-chi/chi/v5"
"go.gearno.de/kit/log"
authsvc "go.probo.inc/probo/pkg/auth"
"go.probo.inc/probo/pkg/authz"
"go.probo.inc/probo/pkg/filemanager"
"github.com/go-chi/chi/v5"
"go.gearno.de/kit/log"
)
type Config struct {
@@ -56,6 +56,12 @@ func NewServer(cfg Config) (*Server, error) {
router.Get("/invitations", RequireAuth(cfg.Auth, cfg.Authz, cfg.CookieName, cfg.CookieSecret, ListInvitationsHandler(cfg.Authz)))
router.Post("/invitations/accept", AcceptInvitationHandler(cfg.Auth, cfg.Authz, cfg.CookieName, cfg.CookieSecret))
router.Get("/api-keys", RequireAuth(cfg.Auth, cfg.Authz, cfg.CookieName, cfg.CookieSecret, ListUserAPIKeysHandler(cfg.Auth, cfg.Authz)))
router.Post("/api-keys", RequireAuth(cfg.Auth, cfg.Authz, cfg.CookieName, cfg.CookieSecret, CreateUserAPIKeyHandler(cfg.Auth)))
router.Get("/api-keys/{id}", RequireAuth(cfg.Auth, cfg.Authz, cfg.CookieName, cfg.CookieSecret, GetUserAPIKeyHandler(cfg.Auth)))
router.Put("/api-keys", RequireAuth(cfg.Auth, cfg.Authz, cfg.CookieName, cfg.CookieSecret, UpdateUserAPIKeyHandler(cfg.Auth)))
router.Delete("/api-keys", RequireAuth(cfg.Auth, cfg.Authz, cfg.CookieName, cfg.CookieSecret, DeleteUserAPIKeyHandler(cfg.Auth)))
router.Get("/saml/login/{samlConfigID}", SAMLLoginHandler(cfg.SAML, cfg.Auth, cfg.Logger))
router.Post("/saml/consume", SAMLACSHandler(cfg.SAML, cfg.Auth, cfg.Authz, cfg.CookieName, cfg.CookieSecret, cfg.SessionDuration, cfg.Logger))
router.Get("/saml/metadata", SAMLMetadataHandler(cfg.SAML))

View File

@@ -0,0 +1,126 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package auth
import (
"encoding/json"
"fmt"
"net/http"
"time"
"go.gearno.de/kit/httpserver"
authsvc "go.probo.inc/probo/pkg/auth"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
)
type (
CreateUserAPIKeyRequest struct {
Name string `json:"name"`
ExpiresAt time.Time `json:"expiresAt"`
Organizations []UserAPIKeyOrganizationMembershipRequest `json:"organizations"`
}
UserAPIKeyOrganizationMembershipRequest struct {
OrganizationID string `json:"organizationId"`
Role string `json:"role"`
}
CreateUserAPIKeyResponse struct {
UserAPIKey UserAPIKeyResponse `json:"apiKey"`
Key string `json:"key"`
}
)
func CreateUserAPIKeyHandler(authSvc *authsvc.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
user := UserFromContext(ctx)
var req CreateUserAPIKeyRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
httpserver.RenderJSON(w, http.StatusBadRequest, map[string]string{
"error": "invalid request body",
})
return
}
if req.ExpiresAt.IsZero() {
httpserver.RenderJSON(w, http.StatusBadRequest, map[string]string{
"error": "expiresAt is required",
})
return
}
if req.ExpiresAt.Before(time.Now()) {
httpserver.RenderJSON(w, http.StatusBadRequest, map[string]string{
"error": "expiration date must be in the future",
})
return
}
if len(req.Organizations) == 0 {
httpserver.RenderJSON(w, http.StatusBadRequest, map[string]string{
"error": "at least one organization is required",
})
return
}
name := req.Name
if name == "" {
name = time.Now().Format("2006-01-02")
}
orgInputs := make([]authsvc.UserAPIKeyOrganizationRequest, len(req.Organizations))
for i, org := range req.Organizations {
orgID, err := gid.ParseGID(org.OrganizationID)
if err != nil {
httpserver.RenderJSON(w, http.StatusBadRequest, map[string]string{
"error": "invalid organization id",
})
return
}
orgInputs[i] = authsvc.UserAPIKeyOrganizationRequest{
OrganizationID: orgID,
Role: coredata.APIRole(org.Role),
}
}
memberships, err := authSvc.ValidateAndBuildUserAPIKeyMemberships(ctx, user.ID, orgInputs)
if err != nil {
httpserver.RenderJSON(w, http.StatusBadRequest, map[string]string{
"error": "invalid organizations",
})
return
}
userAPIKey, key, err := authSvc.CreateUserAPIKey(ctx, user.ID, name, req.ExpiresAt, memberships)
if err != nil {
panic(fmt.Errorf("cannot create user api key: %w", err))
}
response := CreateUserAPIKeyResponse{
UserAPIKey: UserAPIKeyResponse{
ID: userAPIKey.ID,
Name: userAPIKey.Name,
ExpiresAt: userAPIKey.ExpiresAt,
CreatedAt: userAPIKey.CreatedAt,
},
Key: key,
}
httpserver.RenderJSON(w, http.StatusCreated, response)
}
}

View File

@@ -0,0 +1,84 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package auth
import (
"encoding/json"
"errors"
"net/http"
"go.gearno.de/kit/httpserver"
authsvc "go.probo.inc/probo/pkg/auth"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
)
type DeleteUserAPIKeyRequest struct {
ID string `json:"id"`
}
type DeleteUserAPIKeyResponse struct {
ID string `json:"id"`
}
func DeleteUserAPIKeyHandler(authSvc *authsvc.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
user := UserFromContext(ctx)
var req DeleteUserAPIKeyRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
httpserver.RenderJSON(w, http.StatusBadRequest, map[string]string{
"error": "invalid request body",
})
return
}
if req.ID == "" {
httpserver.RenderJSON(w, http.StatusBadRequest, map[string]string{
"error": "user api key id is required",
})
return
}
userAPIKeyID, err := gid.ParseGID(req.ID)
if err != nil {
httpserver.RenderJSON(w, http.StatusBadRequest, map[string]string{
"error": "invalid user api key id",
})
return
}
if err := authSvc.DeleteUserAPIKey(ctx, userAPIKeyID, user.ID); err != nil {
var errNotFound *coredata.ErrUserAPIKeyNotFound
if errors.As(err, &errNotFound) {
httpserver.RenderJSON(w, http.StatusNotFound, map[string]string{
"error": "user api key not found",
})
return
}
httpserver.RenderJSON(w, http.StatusInternalServerError, map[string]string{
"error": "failed to delete user api key",
})
return
}
response := DeleteUserAPIKeyResponse{
ID: req.ID,
}
httpserver.RenderJSON(w, http.StatusOK, response)
}
}

View File

@@ -0,0 +1,65 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package auth
import (
"net/http"
"github.com/go-chi/chi/v5"
"go.gearno.de/kit/httpserver"
authsvc "go.probo.inc/probo/pkg/auth"
"go.probo.inc/probo/pkg/gid"
)
type GetUserAPIKeyResponse struct {
Key string `json:"key"`
}
func GetUserAPIKeyHandler(authSvc *authsvc.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
user := UserFromContext(ctx)
userAPIKeyIDStr := chi.URLParam(r, "id")
if userAPIKeyIDStr == "" {
httpserver.RenderJSON(w, http.StatusBadRequest, map[string]string{
"error": "user api key id is required",
})
return
}
userAPIKeyID, err := gid.ParseGID(userAPIKeyIDStr)
if err != nil {
httpserver.RenderJSON(w, http.StatusBadRequest, map[string]string{
"error": "invalid user api key id",
})
return
}
key, err := authSvc.GetUserAPIKey(ctx, userAPIKeyID, user.ID)
if err != nil {
httpserver.RenderJSON(w, http.StatusNotFound, map[string]string{
"error": "user api key not found",
})
return
}
response := GetUserAPIKeyResponse{
Key: key,
}
httpserver.RenderJSON(w, http.StatusOK, response)
}
}

View File

@@ -0,0 +1,93 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package auth
import (
"fmt"
"net/http"
"time"
"go.gearno.de/kit/httpserver"
authsvc "go.probo.inc/probo/pkg/auth"
"go.probo.inc/probo/pkg/authz"
"go.probo.inc/probo/pkg/gid"
)
type (
ListUserAPIKeysResponse struct {
UserAPIKeys []UserAPIKeyResponse `json:"apiKeys"`
}
UserAPIKeyResponse struct {
ID gid.GID `json:"id"`
Name string `json:"name"`
ExpiresAt time.Time `json:"expiresAt"`
CreatedAt time.Time `json:"createdAt"`
Organizations []UserAPIKeyOrganizationMembership `json:"organizations"`
}
UserAPIKeyOrganizationMembership struct {
OrganizationID gid.GID `json:"organizationId"`
OrganizationName string `json:"organizationName"`
Role string `json:"role"`
}
)
func ListUserAPIKeysHandler(authSvc *authsvc.Service, authzSvc *authz.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
user := UserFromContext(ctx)
organizations, err := authzSvc.GetAllUserOrganizations(ctx, user.ID)
if err != nil {
panic(fmt.Errorf("cannot list organizations for user: %w", err))
}
tenantIDs := make([]gid.TenantID, 0, len(organizations))
for _, org := range organizations {
tenantIDs = append(tenantIDs, org.ID.TenantID())
}
userAPIKeysWithMemberships, err := authSvc.ListUserAPIKeysWithMemberships(ctx, user.ID, tenantIDs)
if err != nil {
panic(fmt.Errorf("cannot list user api keys: %w", err))
}
response := ListUserAPIKeysResponse{
UserAPIKeys: make([]UserAPIKeyResponse, 0, len(userAPIKeysWithMemberships)),
}
for _, keyWithMemberships := range userAPIKeysWithMemberships {
organizations := make([]UserAPIKeyOrganizationMembership, 0, len(keyWithMemberships.Memberships))
for _, membership := range keyWithMemberships.Memberships {
organizations = append(organizations, UserAPIKeyOrganizationMembership{
OrganizationID: membership.OrganizationID,
OrganizationName: membership.OrganizationName,
Role: membership.Role.String(),
})
}
response.UserAPIKeys = append(response.UserAPIKeys, UserAPIKeyResponse{
ID: keyWithMemberships.UserAPIKey.ID,
Name: keyWithMemberships.UserAPIKey.Name,
ExpiresAt: keyWithMemberships.UserAPIKey.ExpiresAt,
CreatedAt: keyWithMemberships.UserAPIKey.CreatedAt,
Organizations: organizations,
})
}
httpserver.RenderJSON(w, http.StatusOK, response)
}
}

View File

@@ -0,0 +1,142 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package auth
import (
"encoding/json"
"errors"
"net/http"
"strings"
"go.gearno.de/kit/httpserver"
authsvc "go.probo.inc/probo/pkg/auth"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
)
type UpdateUserAPIKeyRequest struct {
ID string `json:"id"`
Name *string `json:"name,omitempty"`
Organizations []UserAPIKeyOrganizationMembershipRequest `json:"organizations"`
}
func UpdateUserAPIKeyHandler(authSvc *authsvc.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
user := UserFromContext(ctx)
var req UpdateUserAPIKeyRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
httpserver.RenderJSON(w, http.StatusBadRequest, map[string]string{
"error": "invalid request body",
})
return
}
if req.ID == "" {
httpserver.RenderJSON(w, http.StatusBadRequest, map[string]string{
"error": "user api key id is required",
})
return
}
userAPIKeyID, err := gid.ParseGID(req.ID)
if err != nil {
httpserver.RenderJSON(w, http.StatusBadRequest, map[string]string{
"error": "invalid user api key id",
})
return
}
if req.Name == nil && len(req.Organizations) == 0 {
httpserver.RenderJSON(w, http.StatusBadRequest, map[string]string{
"error": "at least one field must be provided for update",
})
return
}
if req.Name != nil && *req.Name != "" {
if err := authSvc.UpdateUserAPIKeyName(ctx, userAPIKeyID, user.ID, *req.Name); err != nil {
var errNotFound *coredata.ErrUserAPIKeyNotFound
if errors.As(err, &errNotFound) {
httpserver.RenderJSON(w, http.StatusNotFound, map[string]string{
"error": "user api key not found",
})
return
}
if strings.Contains(err.Error(), "does not belong to user") {
httpserver.RenderJSON(w, http.StatusForbidden, map[string]string{
"error": "access denied",
})
return
}
httpserver.RenderJSON(w, http.StatusInternalServerError, map[string]string{
"error": "failed to update user api key name",
})
return
}
}
if len(req.Organizations) > 0 {
orgInputs := make([]authsvc.UserAPIKeyOrganizationRequest, len(req.Organizations))
for i, org := range req.Organizations {
orgID, err := gid.ParseGID(org.OrganizationID)
if err != nil {
httpserver.RenderJSON(w, http.StatusBadRequest, map[string]string{
"error": "invalid organization id",
})
return
}
orgInputs[i] = authsvc.UserAPIKeyOrganizationRequest{
OrganizationID: orgID,
Role: coredata.APIRole(org.Role),
}
}
memberships, err := authSvc.ValidateAndBuildUserAPIKeyMemberships(ctx, user.ID, orgInputs)
if err != nil {
httpserver.RenderJSON(w, http.StatusBadRequest, map[string]string{
"error": "invalid organizations",
})
return
}
if err := authSvc.UpdateUserAPIKeyMemberships(ctx, userAPIKeyID, user.ID, memberships); err != nil {
var errNotFound *coredata.ErrUserAPIKeyNotFound
if errors.As(err, &errNotFound) {
httpserver.RenderJSON(w, http.StatusNotFound, map[string]string{
"error": "user api key not found",
})
return
}
if strings.Contains(err.Error(), "does not belong to user") {
httpserver.RenderJSON(w, http.StatusForbidden, map[string]string{
"error": "access denied",
})
return
}
httpserver.RenderJSON(w, http.StatusInternalServerError, map[string]string{
"error": "failed to update user api key",
})
return
}
}
httpserver.RenderJSON(w, http.StatusOK, map[string]string{
"message": "User API key updated successfully",
})
}
}

View File

@@ -12,7 +12,7 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package graphql
package gqlutils
import (
"maps"

View File

@@ -12,18 +12,18 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package graphql
package gqlutils
import (
"context"
"errors"
"runtime/debug"
"go.probo.inc/probo/pkg/auth"
"go.probo.inc/probo/pkg/authz"
"github.com/vektah/gqlparser/v2/gqlerror"
"go.gearno.de/kit/httpserver"
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/auth"
"go.probo.inc/probo/pkg/authz"
)
func RecoverFunc(ctx context.Context, err any) error {

View File

@@ -12,7 +12,7 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package graphql
package gqlutils
import (
"context"

View File

@@ -66,13 +66,15 @@ func (e ErrExpiredToken) Error() string {
return e.message
}
// NewToken creates a new token with the specified type, data, and expiration time
func NewToken[T any](secret string, tokenType string, expirationTime time.Duration, data T) (string, error) {
now := time.Now()
return NewDeterministicToken(secret, tokenType, now.Add(expirationTime), now, data)
}
func NewDeterministicToken[T any](secret string, tokenType string, expiresAt time.Time, issuedAt time.Time, data T) (string, error) {
payload := Payload[T]{
ExpiresAt: now.Add(expirationTime),
IssuedAt: now,
ExpiresAt: expiresAt,
IssuedAt: issuedAt,
Type: tokenType,
Data: data,
}

View File

@@ -25,14 +25,14 @@ import (
"text/template"
"time"
"go.gearno.de/kit/log"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/packages/emails"
"go.probo.inc/probo/pkg/auth"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/probo"
"go.probo.inc/probo/pkg/statelesstoken"
"go.gearno.de/kit/log"
"go.gearno.de/kit/pg"
)
var (