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 { RouterProvider } from "react-router";
import { router } from "./routes"; import { router } from "./routes";
import { Toasts } from "@probo/ui"; import { Toasts, ConfirmDialog } from "@probo/ui";
export function App() { export function App() {
return ( return (
<> <>
<RouterProvider router={router} /> <RouterProvider router={router} />
<Toasts /> <Toasts />
<ConfirmDialog />
</> </>
); );
} }

View File

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

View File

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

View File

@@ -24,6 +24,8 @@ import (
"net/mail" "net/mail"
"time" "time"
"github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/packages/emails" "go.probo.inc/probo/packages/emails"
"go.probo.inc/probo/pkg/baseurl" "go.probo.inc/probo/pkg/baseurl"
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
@@ -31,8 +33,6 @@ import (
"go.probo.inc/probo/pkg/crypto/passwdhash" "go.probo.inc/probo/pkg/crypto/passwdhash"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/statelesstoken" "go.probo.inc/probo/pkg/statelesstoken"
"github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg"
) )
type ( type (
@@ -124,11 +124,32 @@ type (
OrganizationID gid.GID OrganizationID gid.GID
RedirectURL string 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 ( const (
TokenTypeEmailConfirmation = "email_confirmation" TokenTypeEmailConfirmation = "email_confirmation"
TokenTypePasswordReset = "password_reset" TokenTypePasswordReset = "password_reset"
TokenTypeAPIKey = "api_key"
) )
func (e ErrInvalidCredentials) Error() string { func (e ErrInvalidCredentials) Error() string {
@@ -1235,3 +1256,388 @@ func verifyDomainOwnership(ctx context.Context, domain, expectedToken string) (b
return false, nil 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" "net/url"
"time" "time"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/packages/emails" "go.probo.inc/probo/packages/emails"
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/statelesstoken" "go.probo.inc/probo/pkg/statelesstoken"
"go.gearno.de/kit/pg"
) )
type TenantAccessError struct { type TenantAccessError struct {
@@ -103,6 +103,26 @@ func (s *Service) GetAllUserOrganizations(
return organizations, err 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( func (s *Service) GetUserOrganizations(
ctx context.Context, ctx context.Context,
userID gid.GID, 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 SlackMessageEntityType
TrustCenterFileEntityType TrustCenterFileEntityType
SAMLConfigurationEntityType 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" "maps"
"time" "time"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg" "go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
) )
type ( type (
@@ -161,7 +161,7 @@ FROM
INNER JOIN INNER JOIN
user_org ON organizations.id = user_org.organization_id user_org ON organizations.id = user_org.organization_id
WHERE WHERE
%S %s
AND %s AND %s
` `
@@ -237,6 +237,60 @@ ORDER BY
return nil 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( func (o *Organization) Insert(
ctx context.Context, ctx context.Context,
conn pg.Conn, 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" "unicode"
"github.com/aws/aws-sdk-go-v2/service/s3" "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/packages/emails"
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/docgen" "go.probo.inc/probo/pkg/docgen"
@@ -23,10 +27,6 @@ import (
"go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/statelesstoken" "go.probo.inc/probo/pkg/statelesstoken"
"go.probo.inc/probo/pkg/watermarkpdf" "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 ( type (

View File

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

View File

@@ -20,16 +20,16 @@ call_argument_directives_with_null: true
models: models:
ID: ID:
model: model:
- "github.com/getprobo/probo/pkg/server/graphql/types/gid.GIDScalar" - "go.probo.inc/probo/pkg/server/gqlutils/types/gid.GIDScalar"
Datetime: Datetime:
model: model:
- "github.com/99designs/gqlgen/graphql.Time" - "github.com/99designs/gqlgen/graphql.Time"
CursorKey: CursorKey:
model: model:
- "github.com/getprobo/probo/pkg/server/graphql/types/cursor.CursorKeyScalar" - "go.probo.inc/probo/pkg/server/gqlutils/types/cursor.CursorKeyScalar"
Duration: Duration:
model: model:
- "github.com/99designs/gqlgen/graphql.Duration" - "github.com/99designs/gqlgen/graphql.Duration"
BigInt: BigInt:
model: 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/extension"
"github.com/99designs/gqlgen/graphql/handler/transport" "github.com/99designs/gqlgen/graphql/handler/transport"
"github.com/99designs/gqlgen/graphql/playground" "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/auth"
"go.probo.inc/probo/pkg/authz" "go.probo.inc/probo/pkg/authz"
"go.probo.inc/probo/pkg/connector" "go.probo.inc/probo/pkg/connector"
@@ -38,14 +43,9 @@ import (
"go.probo.inc/probo/pkg/probo" "go.probo.inc/probo/pkg/probo"
"go.probo.inc/probo/pkg/saferedirect" "go.probo.inc/probo/pkg/saferedirect"
"go.probo.inc/probo/pkg/server/api/console/v1/schema" "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/server/session"
"go.probo.inc/probo/pkg/statelesstoken" "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 ( type (
@@ -77,6 +77,7 @@ var (
sessionContextKey = &ctxKey{name: "session"} sessionContextKey = &ctxKey{name: "session"}
userContextKey = &ctxKey{name: "user"} userContextKey = &ctxKey{name: "user"}
userTenantContextKey = &ctxKey{name: "user_tenants"} userTenantContextKey = &ctxKey{name: "user_tenants"}
userAPIKeyContextKey = &ctxKey{name: "user_api_key"}
) )
func SessionFromContext(ctx context.Context) *coredata.Session { func SessionFromContext(ctx context.Context) *coredata.Session {
@@ -89,6 +90,11 @@ func UserFromContext(ctx context.Context) *coredata.User {
return user return user
} }
func UserAPIKeyFromContext(ctx context.Context) *coredata.UserAPIKey {
userAPIKey, _ := ctx.Value(userAPIKeyContextKey).(*coredata.UserAPIKey)
return userAPIKey
}
func NewMux( func NewMux(
logger *log.Logger, logger *log.Logger,
proboSvc *probo.Service, 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) { return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
if authCtx := tryAPIKeyAuth(ctx, r, authSvc, authzSvc); authCtx != nil {
next(w, r.WithContext(authCtx))
return
}
sessionAuthCfg := session.AuthConfig{ sessionAuthCfg := session.AuthConfig{
CookieName: authCfg.CookieName, CookieName: authCfg.CookieName,
CookieSecret: authCfg.CookieSecret, 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 { func (r *Resolver) ProboService(ctx context.Context, tenantID gid.TenantID) *probo.TenantService {
return GetTenantService(ctx, r.proboSvc, tenantID) 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 ( import (
"go.probo.inc/probo/pkg/page" "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]( func NewCursor[O page.OrderField](

View File

@@ -16,7 +16,7 @@ package types
import ( import (
"go.probo.inc/probo/pkg/page" "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 { func NewPageInfo[T page.Paginable[O], O page.OrderField](p *page.Page[T, O]) *PageInfo {

View File

@@ -13,6 +13,8 @@ import (
"strings" "strings"
"time" "time"
pgx "github.com/jackc/pgx/v5"
"github.com/vektah/gqlparser/v2/gqlerror"
"go.probo.inc/probo/pkg/auth" "go.probo.inc/probo/pkg/auth"
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
@@ -20,9 +22,7 @@ import (
"go.probo.inc/probo/pkg/probo" "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/schema"
"go.probo.inc/probo/pkg/server/api/console/v1/types" "go.probo.inc/probo/pkg/server/api/console/v1/types"
"go.probo.inc/probo/pkg/server/graphql" "go.probo.inc/probo/pkg/server/gqlutils"
pgx "github.com/jackc/pgx/v5"
"github.com/vektah/gqlparser/v2/gqlerror"
) )
// Owner is the resolver for the owner field. // 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 { if err != nil {
var errNotFound *coredata.ErrAssetNotFound var errNotFound *coredata.ErrAssetNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get asset: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrPeopleNotFound var errNotFound *coredata.ErrPeopleNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get owner: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrOrganizationNotFound var errNotFound *coredata.ErrOrganizationNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get organization: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrAuditNotFound var errNotFound *coredata.ErrAuditNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot load audit: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrOrganizationNotFound var errNotFound *coredata.ErrOrganizationNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot load organization: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrAuditNotFound var errNotFound *coredata.ErrAuditNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot load audit: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrFrameworkNotFound var errNotFound *coredata.ErrFrameworkNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot load framework: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrAuditNotFound var errNotFound *coredata.ErrAuditNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot load audit: %w", err)) panic(fmt.Errorf("cannot load audit: %w", err))
} }
@@ -277,7 +277,7 @@ func (r *continualImprovementResolver) Organization(ctx context.Context, obj *ty
if err != nil { if err != nil {
var errNotFound *coredata.ErrOrganizationNotFound var errNotFound *coredata.ErrOrganizationNotFound
if errors.As(err, &errNotFound) { 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)) 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 { if err != nil {
var errNotFound *coredata.ErrPeopleNotFound var errNotFound *coredata.ErrPeopleNotFound
if errors.As(err, &errNotFound) { 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)) 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 { if err != nil {
var errNotFound *coredata.ErrControlNotFound var errNotFound *coredata.ErrControlNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get control: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrFrameworkNotFound var errNotFound *coredata.ErrFrameworkNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get framework: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrPeopleNotFound var errNotFound *coredata.ErrPeopleNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
return nil, fmt.Errorf("cannot get owner: %w", err) 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 { if err != nil {
var errNotFound *coredata.ErrOrganizationNotFound var errNotFound *coredata.ErrOrganizationNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get organization: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrDocumentNotFound var errNotFound *coredata.ErrDocumentNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get document: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrPeopleNotFound var errNotFound *coredata.ErrPeopleNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get owner: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrDocumentNotFound var errNotFound *coredata.ErrDocumentNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get document: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrOrganizationNotFound var errNotFound *coredata.ErrOrganizationNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get organization: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrDocumentNotFound var errNotFound *coredata.ErrDocumentNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get document: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrPeopleNotFound var errNotFound *coredata.ErrPeopleNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get owner: %w", err)) panic(fmt.Errorf("cannot get owner: %w", err))
} }
@@ -823,7 +823,7 @@ func (r *documentVersionSignatureResolver) SignedBy(ctx context.Context, obj *ty
if err != nil { if err != nil {
var errNotFound *coredata.ErrPeopleNotFound var errNotFound *coredata.ErrPeopleNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get people: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrFileNotFound var errNotFound *coredata.ErrFileNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot load evidence file: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrTaskNotFound var errNotFound *coredata.ErrTaskNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot load task: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrMeasureNotFound var errNotFound *coredata.ErrMeasureNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot load measure: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrFrameworkNotFound var errNotFound *coredata.ErrFrameworkNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot load framework: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrOrganizationNotFound var errNotFound *coredata.ErrOrganizationNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot load organization: %w", err)) 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 { if err != nil {
var errAlreadyExists *coredata.ErrOrganizationAlreadyExists var errAlreadyExists *coredata.ErrOrganizationAlreadyExists
if errors.As(err, &errAlreadyExists) { if errors.As(err, &errAlreadyExists) {
return nil, graphql.Conflict(errAlreadyExists) return nil, gqlutils.Conflict(errAlreadyExists)
} }
var errTrustCenterAlreadyExists *coredata.ErrTrustCenterAlreadyExists var errTrustCenterAlreadyExists *coredata.ErrTrustCenterAlreadyExists
if errors.As(err, &errTrustCenterAlreadyExists) { if errors.As(err, &errTrustCenterAlreadyExists) {
return nil, graphql.Conflict(errTrustCenterAlreadyExists) return nil, gqlutils.Conflict(errTrustCenterAlreadyExists)
} }
panic(fmt.Errorf("cannot create organization: %w", err)) 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 { if err != nil {
var errAlreadyExists *coredata.ErrPeopleAlreadyExists var errAlreadyExists *coredata.ErrPeopleAlreadyExists
if errors.As(err, &errAlreadyExists) { if errors.As(err, &errAlreadyExists) {
return nil, graphql.Conflict(errAlreadyExists) return nil, gqlutils.Conflict(errAlreadyExists)
} }
panic(fmt.Errorf("cannot create people: %w", err)) panic(fmt.Errorf("cannot create people: %w", err))
} }
@@ -1408,7 +1408,7 @@ func (r *mutationResolver) CreateTrustCenterAccess(ctx context.Context, input ty
if err != nil { if err != nil {
var errAlreadyExists *coredata.ErrTrustCenterAccessAlreadyExists var errAlreadyExists *coredata.ErrTrustCenterAccessAlreadyExists
if errors.As(err, &errAlreadyExists) { 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)) 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 { if err != nil {
var errAlreadyExists *coredata.ErrPeopleAlreadyExists var errAlreadyExists *coredata.ErrPeopleAlreadyExists
if errors.As(err, &errAlreadyExists) { 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) 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 { if err != nil {
var errAlreadyExists *coredata.ErrPeopleAlreadyExists var errAlreadyExists *coredata.ErrPeopleAlreadyExists
if errors.As(err, &errAlreadyExists) { if errors.As(err, &errAlreadyExists) {
return nil, graphql.Conflict(errAlreadyExists) return nil, gqlutils.Conflict(errAlreadyExists)
} }
panic(fmt.Errorf("cannot create people: %w", err)) 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 { if err != nil {
var errAlreadyExists *coredata.ErrVendorAlreadyExists var errAlreadyExists *coredata.ErrVendorAlreadyExists
if errors.As(err, &errAlreadyExists) { if errors.As(err, &errAlreadyExists) {
return nil, graphql.Conflict(errAlreadyExists) return nil, gqlutils.Conflict(errAlreadyExists)
} }
return nil, fmt.Errorf("cannot create vendor: %w", err) 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 { if err != nil {
var errAlreadyExists *coredata.ErrControlAlreadyExists var errAlreadyExists *coredata.ErrControlAlreadyExists
if errors.As(err, &errAlreadyExists) { if errors.As(err, &errAlreadyExists) {
return nil, graphql.Conflict(errAlreadyExists) return nil, gqlutils.Conflict(errAlreadyExists)
} }
panic(fmt.Errorf("cannot create control: %w", err)) 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 { if err != nil {
var errAlreadyExists *coredata.ErrControlAlreadyExists var errAlreadyExists *coredata.ErrControlAlreadyExists
if errors.As(err, &errAlreadyExists) { if errors.As(err, &errAlreadyExists) {
return nil, graphql.Conflict(errAlreadyExists) return nil, gqlutils.Conflict(errAlreadyExists)
} }
panic(fmt.Errorf("cannot update control: %w", err)) 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 { if err != nil {
var errAlreadyExists *coredata.ErrMeasureAlreadyExists var errAlreadyExists *coredata.ErrMeasureAlreadyExists
if errors.As(err, &errAlreadyExists) { if errors.As(err, &errAlreadyExists) {
return nil, graphql.Conflict(errAlreadyExists) return nil, gqlutils.Conflict(errAlreadyExists)
} }
panic(fmt.Errorf("cannot create measure: %w", err)) panic(fmt.Errorf("cannot create measure: %w", err))
} }
@@ -2233,7 +2233,7 @@ func (r *mutationResolver) CreateControlDocumentMapping(ctx context.Context, inp
if err != nil { if err != nil {
var errMappingExists *coredata.ErrControlDocumentMappingAlreadyExists var errMappingExists *coredata.ErrControlDocumentMappingAlreadyExists
if errors.As(err, &errMappingExists) { 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)) 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 { if err != nil {
var errAlreadyExists *coredata.ErrTaskAlreadyExists var errAlreadyExists *coredata.ErrTaskAlreadyExists
if errors.As(err, &errAlreadyExists) { if errors.As(err, &errAlreadyExists) {
return nil, graphql.Conflict(errAlreadyExists) return nil, gqlutils.Conflict(errAlreadyExists)
} }
panic(fmt.Errorf("cannot create task: %w", err)) 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 { if err != nil {
var errAlreadyExists *coredata.ErrRiskAlreadyExists var errAlreadyExists *coredata.ErrRiskAlreadyExists
if errors.As(err, &errAlreadyExists) { if errors.As(err, &errAlreadyExists) {
return nil, graphql.Conflict(errAlreadyExists) return nil, gqlutils.Conflict(errAlreadyExists)
} }
panic(fmt.Errorf("cannot create risk: %w", err)) 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 { if err != nil {
var errAlreadyExists *coredata.ErrDocumentAlreadyExists var errAlreadyExists *coredata.ErrDocumentAlreadyExists
if errors.As(err, &errAlreadyExists) { if errors.As(err, &errAlreadyExists) {
return nil, graphql.Conflict(errAlreadyExists) return nil, gqlutils.Conflict(errAlreadyExists)
} }
panic(fmt.Errorf("cannot create document: %w", err)) panic(fmt.Errorf("cannot create document: %w", err))
} }
@@ -2858,7 +2858,7 @@ func (r *mutationResolver) PublishDocumentVersion(ctx context.Context, input typ
if err != nil { if err != nil {
var errNoChanges *coredata.ErrDocumentVersionNoChanges var errNoChanges *coredata.ErrDocumentVersionNoChanges
if errors.As(err, &errNoChanges) { if errors.As(err, &errNoChanges) {
return nil, graphql.Invalid(errNoChanges) return nil, gqlutils.Invalid(errNoChanges)
} }
panic(fmt.Errorf("cannot publish document version: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrOrganizationNotFound var errNotFound *coredata.ErrOrganizationNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get nonconformity organization: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrAuditNotFound var errNotFound *coredata.ErrAuditNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get nonconformity audit: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrPeopleNotFound var errNotFound *coredata.ErrPeopleNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get nonconformity owner: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrOrganizationNotFound var errNotFound *coredata.ErrOrganizationNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get obligation organization: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrPeopleNotFound var errNotFound *coredata.ErrPeopleNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get obligation owner: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrOrganizationNotFound var errNotFound *coredata.ErrOrganizationNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get organization: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrOrganizationNotFound var errNotFound *coredata.ErrOrganizationNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get organization: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrPeopleNotFound var errNotFound *coredata.ErrPeopleNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get people: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrVendorNotFound var errNotFound *coredata.ErrVendorNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get vendor: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrFrameworkNotFound var errNotFound *coredata.ErrFrameworkNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get framework: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrMeasureNotFound var errNotFound *coredata.ErrMeasureNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get measure: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrTaskNotFound var errNotFound *coredata.ErrTaskNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get task: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrDocumentNotFound var errNotFound *coredata.ErrDocumentNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get document: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrControlNotFound var errNotFound *coredata.ErrControlNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get control: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrRiskNotFound var errNotFound *coredata.ErrRiskNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get risk: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrAssetNotFound var errNotFound *coredata.ErrAssetNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get asset: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrAuditNotFound var errNotFound *coredata.ErrAuditNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get audit: %w", err)) 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) { func (r *queryResolver) Viewer(ctx context.Context) (*types.Viewer, error) {
user := UserFromContext(ctx) user := UserFromContext(ctx)
session := SessionFromContext(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{ return &types.Viewer{
ID: session.ID, ID: viewerID,
User: types.NewUser(user), User: types.NewUser(user),
}, nil }, nil
} }
@@ -5060,7 +5070,7 @@ func (r *riskResolver) Owner(ctx context.Context, obj *types.Risk) (*types.Peopl
if err != nil { if err != nil {
var errNotFound *coredata.ErrRiskNotFound var errNotFound *coredata.ErrRiskNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get risk: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrPeopleNotFound var errNotFound *coredata.ErrPeopleNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get owner: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrRiskNotFound var errNotFound *coredata.ErrRiskNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get risk: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrOrganizationNotFound var errNotFound *coredata.ErrOrganizationNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get organization: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrOrganizationNotFound var errNotFound *coredata.ErrOrganizationNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get organization: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrTaskNotFound var errNotFound *coredata.ErrTaskNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get task: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrPeopleNotFound var errNotFound *coredata.ErrPeopleNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get assigned to: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrTaskNotFound var errNotFound *coredata.ErrTaskNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get task: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrOrganizationNotFound var errNotFound *coredata.ErrOrganizationNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get organization: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrTaskNotFound var errNotFound *coredata.ErrTaskNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get task: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrMeasureNotFound var errNotFound *coredata.ErrMeasureNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get measure: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrOrganizationNotFound var errNotFound *coredata.ErrOrganizationNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get organization: %w", err)) panic(fmt.Errorf("cannot get organization: %w", err))
} }
@@ -5614,7 +5624,7 @@ func (r *trustCenterDocumentAccessResolver) Document(ctx context.Context, obj *t
if err != nil { if err != nil {
var errNotFound *coredata.ErrDocumentNotFound var errNotFound *coredata.ErrDocumentNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
return nil, fmt.Errorf("cannot load document: %w", err) 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 { if err != nil {
var errNotFound *coredata.ErrOrganizationNotFound var errNotFound *coredata.ErrOrganizationNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get organization: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrVendorNotFound var errNotFound *coredata.ErrVendorNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get vendor: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrOrganizationNotFound var errNotFound *coredata.ErrOrganizationNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get organization: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrVendorNotFound var errNotFound *coredata.ErrVendorNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get vendor: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrPeopleNotFound var errNotFound *coredata.ErrPeopleNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get business owner: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrVendorNotFound var errNotFound *coredata.ErrVendorNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get vendor: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrPeopleNotFound var errNotFound *coredata.ErrPeopleNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get security owner: %w", err)) panic(fmt.Errorf("cannot get security owner: %w", err))
} }
@@ -5970,7 +5980,7 @@ func (r *vendorBusinessAssociateAgreementResolver) Vendor(ctx context.Context, o
if err != nil { if err != nil {
var errNotFound *coredata.ErrVendorNotFound var errNotFound *coredata.ErrVendorNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
return nil, fmt.Errorf("cannot get vendor: %w", err) 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 { if err != nil {
var errNotFound *coredata.ErrVendorNotFound var errNotFound *coredata.ErrVendorNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get vendor: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrFileNotFound var errNotFound *coredata.ErrFileNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot load evidence file: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrVendorNotFound var errNotFound *coredata.ErrVendorNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get vendor: %w", err)) panic(fmt.Errorf("cannot get vendor: %w", err))
} }
@@ -6089,7 +6099,7 @@ func (r *vendorDataPrivacyAgreementResolver) Vendor(ctx context.Context, obj *ty
if err != nil { if err != nil {
var errNotFound *coredata.ErrVendorNotFound var errNotFound *coredata.ErrVendorNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get vendor: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrVendorNotFound var errNotFound *coredata.ErrVendorNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get vendor: %w", err)) 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 { if err != nil {
var errNotFound *coredata.ErrVendorNotFound var errNotFound *coredata.ErrVendorNotFound
if errors.As(err, &errNotFound) { if errors.As(err, &errNotFound) {
return nil, graphql.NotFound(errNotFound) return nil, gqlutils.NotFound(errNotFound)
} }
panic(fmt.Errorf("cannot get vendor: %w", err)) panic(fmt.Errorf("cannot get vendor: %w", err))
} }

View File

@@ -20,10 +20,10 @@ call_argument_directives_with_null: true
models: models:
ID: ID:
model: model:
- "github.com/getprobo/probo/pkg/server/graphql/types/gid.GIDScalar" - "go.probo.inc/probo/pkg/server/gqlutils/types/gid.GIDScalar"
Datetime: Datetime:
model: model:
- "github.com/99designs/gqlgen/graphql.Time" - "github.com/99designs/gqlgen/graphql.Time"
CursorKey: CursorKey:
model: 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"
"github.com/99designs/gqlgen/graphql/handler/extension" "github.com/99designs/gqlgen/graphql/handler/extension"
"github.com/99designs/gqlgen/graphql/handler/transport" "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/auth"
"go.probo.inc/probo/pkg/authz" "go.probo.inc/probo/pkg/authz"
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
@@ -34,12 +36,10 @@ import (
console_v1 "go.probo.inc/probo/pkg/server/api/console/v1" 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/schema"
"go.probo.inc/probo/pkg/server/api/trust/v1/trustauth" "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/server/session"
"go.probo.inc/probo/pkg/statelesstoken" "go.probo.inc/probo/pkg/statelesstoken"
"go.probo.inc/probo/pkg/trust" "go.probo.inc/probo/pkg/trust"
"github.com/go-chi/chi/v5"
"go.gearno.de/kit/log"
) )
type ( type (

View File

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

View File

@@ -16,7 +16,7 @@ package types
import ( import (
"go.probo.inc/probo/pkg/page" "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]( func NewCursor[O page.OrderField](

View File

@@ -16,7 +16,7 @@ package types
import ( import (
"go.probo.inc/probo/pkg/page" "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 { func NewPageInfo[T page.Paginable[O], O page.OrderField](p *page.Page[T, O]) *PageInfo {

View File

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

View File

@@ -18,11 +18,11 @@ import (
"net/http" "net/http"
"time" "time"
"github.com/go-chi/chi/v5"
"go.gearno.de/kit/log"
authsvc "go.probo.inc/probo/pkg/auth" authsvc "go.probo.inc/probo/pkg/auth"
"go.probo.inc/probo/pkg/authz" "go.probo.inc/probo/pkg/authz"
"go.probo.inc/probo/pkg/filemanager" "go.probo.inc/probo/pkg/filemanager"
"github.com/go-chi/chi/v5"
"go.gearno.de/kit/log"
) )
type Config struct { 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.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.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.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.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)) 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 // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE. // PERFORMANCE OF THIS SOFTWARE.
package graphql package gqlutils
import ( import (
"maps" "maps"

View File

@@ -12,18 +12,18 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE. // PERFORMANCE OF THIS SOFTWARE.
package graphql package gqlutils
import ( import (
"context" "context"
"errors" "errors"
"runtime/debug" "runtime/debug"
"go.probo.inc/probo/pkg/auth"
"go.probo.inc/probo/pkg/authz"
"github.com/vektah/gqlparser/v2/gqlerror" "github.com/vektah/gqlparser/v2/gqlerror"
"go.gearno.de/kit/httpserver" "go.gearno.de/kit/httpserver"
"go.gearno.de/kit/log" "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 { 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 // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE. // PERFORMANCE OF THIS SOFTWARE.
package graphql package gqlutils
import ( import (
"context" "context"

View File

@@ -66,13 +66,15 @@ func (e ErrExpiredToken) Error() string {
return e.message 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) { func NewToken[T any](secret string, tokenType string, expirationTime time.Duration, data T) (string, error) {
now := time.Now() 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]{ payload := Payload[T]{
ExpiresAt: now.Add(expirationTime), ExpiresAt: expiresAt,
IssuedAt: now, IssuedAt: issuedAt,
Type: tokenType, Type: tokenType,
Data: data, Data: data,
} }

View File

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