@@ -1,564 +0,0 @@
|
|||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
|
||||||
Avatar,
|
|
||||||
Badge,
|
|
||||||
Button,
|
|
||||||
Dropdown,
|
|
||||||
DropdownItem,
|
|
||||||
DropdownSeparator,
|
|
||||||
IconArrowBoxLeft,
|
|
||||||
IconBank,
|
|
||||||
IconBook,
|
|
||||||
IconBox,
|
|
||||||
IconCalendar2,
|
|
||||||
IconCheckmark1,
|
|
||||||
IconChevronGrabberVertical,
|
|
||||||
IconCircleProgress,
|
|
||||||
IconCircleQuestionmark,
|
|
||||||
IconClock,
|
|
||||||
IconCrossLargeX,
|
|
||||||
IconFire3,
|
|
||||||
IconGroup1,
|
|
||||||
IconInboxEmpty,
|
|
||||||
IconKey,
|
|
||||||
IconListStack,
|
|
||||||
IconPageCheck,
|
|
||||||
IconLock,
|
|
||||||
IconMagnifyingGlass,
|
|
||||||
IconMedal,
|
|
||||||
IconPageTextLine,
|
|
||||||
IconPeopleAdd,
|
|
||||||
IconPlusLarge,
|
|
||||||
IconRotateCw,
|
|
||||||
IconSettingsGear2,
|
|
||||||
IconShield,
|
|
||||||
IconStore,
|
|
||||||
IconTodo,
|
|
||||||
Input,
|
|
||||||
Layout,
|
|
||||||
SidebarItem,
|
|
||||||
Skeleton,
|
|
||||||
UserDropdownItem,
|
|
||||||
UserDropdown as UserDropdownRoot,
|
|
||||||
useToast,
|
|
||||||
} from "@probo/ui";
|
|
||||||
import { Suspense, use, useEffect, useMemo, useState } from "react";
|
|
||||||
import { ErrorBoundary } from "react-error-boundary";
|
|
||||||
import { useLazyLoadQuery } from "react-relay";
|
|
||||||
import { Link, Navigate, Outlet, useParams } from "react-router";
|
|
||||||
import { graphql } from "relay-runtime";
|
|
||||||
import type { MainLayoutQuery as MainLayoutQueryType } from "./__generated__/MainLayoutQuery.graphql";
|
|
||||||
import { PageError } from "/components/PageError";
|
|
||||||
import { PermissionsProvider } from "/providers/PermissionsProvider";
|
|
||||||
import { PermissionsContext } from "/providers/PermissionsContext";
|
|
||||||
|
|
||||||
const MainLayoutQuery = graphql`
|
|
||||||
query MainLayoutQuery($organizationId: ID!) {
|
|
||||||
viewer {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
organization: node(id: $organizationId) {
|
|
||||||
... on Organization {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
logoUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Site layout with a header and a sidebar
|
|
||||||
*/
|
|
||||||
export function MainLayout() {
|
|
||||||
const { organizationId } = useParams();
|
|
||||||
|
|
||||||
const prefix = `/organizations/${organizationId}`;
|
|
||||||
|
|
||||||
if (!organizationId) {
|
|
||||||
return <Navigate to="/" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Suspense fallback={<Skeleton className="w-full h-screen" />}>
|
|
||||||
<PermissionsProvider>
|
|
||||||
<MainLayoutContent organizationId={organizationId} prefix={prefix} />
|
|
||||||
</PermissionsProvider>
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MainLayoutContent({
|
|
||||||
organizationId,
|
|
||||||
prefix,
|
|
||||||
}: {
|
|
||||||
organizationId: string;
|
|
||||||
prefix: string;
|
|
||||||
}) {
|
|
||||||
const { __ } = useTranslate();
|
|
||||||
const { isAuthorized } = use(PermissionsContext);
|
|
||||||
const data = useLazyLoadQuery<MainLayoutQueryType>(MainLayoutQuery, {
|
|
||||||
organizationId,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Layout
|
|
||||||
header={
|
|
||||||
<>
|
|
||||||
<div className="mr-auto">
|
|
||||||
<OrganizationSelector currentOrganization={data.organization} />
|
|
||||||
</div>
|
|
||||||
<Suspense fallback={<Skeleton className="w-32 h-8" />}>
|
|
||||||
<UserDropdown organizationId={organizationId} />
|
|
||||||
</Suspense>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
sidebar={
|
|
||||||
<ul className="space-y-[2px]">
|
|
||||||
{isAuthorized("Organization", "listMeetings") && (
|
|
||||||
<SidebarItem
|
|
||||||
label={__("Meetings")}
|
|
||||||
icon={IconCalendar2}
|
|
||||||
to={`${prefix}/meetings`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isAuthorized("Organization", "listTasks") && (
|
|
||||||
<SidebarItem
|
|
||||||
label={__("Tasks")}
|
|
||||||
icon={IconInboxEmpty}
|
|
||||||
to={`${prefix}/tasks`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isAuthorized("Organization", "listMeasures") && (
|
|
||||||
<SidebarItem
|
|
||||||
label={__("Measures")}
|
|
||||||
icon={IconTodo}
|
|
||||||
to={`${prefix}/measures`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isAuthorized("Organization", "listRisks") && (
|
|
||||||
<SidebarItem
|
|
||||||
label={__("Risks")}
|
|
||||||
icon={IconFire3}
|
|
||||||
to={`${prefix}/risks`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isAuthorized("Organization", "listFrameworks") && (
|
|
||||||
<SidebarItem
|
|
||||||
label={__("Frameworks")}
|
|
||||||
icon={IconBank}
|
|
||||||
to={`${prefix}/frameworks`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isAuthorized("Organization", "listPeople") && (
|
|
||||||
<SidebarItem
|
|
||||||
label={__("People")}
|
|
||||||
icon={IconGroup1}
|
|
||||||
to={`${prefix}/people`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isAuthorized("Organization", "listVendors") && (
|
|
||||||
<SidebarItem
|
|
||||||
label={__("Vendors")}
|
|
||||||
icon={IconStore}
|
|
||||||
to={`${prefix}/vendors`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isAuthorized("Organization", "listDocuments") && (
|
|
||||||
<SidebarItem
|
|
||||||
label={__("Documents")}
|
|
||||||
icon={IconPageTextLine}
|
|
||||||
to={`${prefix}/documents`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isAuthorized("Organization", "listAssets") && (
|
|
||||||
<SidebarItem
|
|
||||||
label={__("Assets")}
|
|
||||||
icon={IconBox}
|
|
||||||
to={`${prefix}/assets`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isAuthorized("Organization", "listData") && (
|
|
||||||
<SidebarItem
|
|
||||||
label={__("Data")}
|
|
||||||
icon={IconListStack}
|
|
||||||
to={`${prefix}/data`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isAuthorized("Organization", "listAudits") && (
|
|
||||||
<SidebarItem
|
|
||||||
label={__("Audits")}
|
|
||||||
icon={IconMedal}
|
|
||||||
to={`${prefix}/audits`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isAuthorized("Organization", "listNonconformities") && (
|
|
||||||
<SidebarItem
|
|
||||||
label={__("Nonconformities")}
|
|
||||||
icon={IconCrossLargeX}
|
|
||||||
to={`${prefix}/nonconformities`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isAuthorized("Organization", "listObligations") && (
|
|
||||||
<SidebarItem
|
|
||||||
label={__("Obligations")}
|
|
||||||
icon={IconBook}
|
|
||||||
to={`${prefix}/obligations`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isAuthorized("Organization", "listContinualImprovements") && (
|
|
||||||
<SidebarItem
|
|
||||||
label={__("Continual Improvements")}
|
|
||||||
icon={IconRotateCw}
|
|
||||||
to={`${prefix}/continual-improvements`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isAuthorized("Organization", "listProcessingActivities") && (
|
|
||||||
<SidebarItem
|
|
||||||
label={__("Processing Activities")}
|
|
||||||
icon={IconCircleProgress}
|
|
||||||
to={`${prefix}/processing-activities`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isAuthorized("Organization", "listRightsRequests") && (
|
|
||||||
<SidebarItem
|
|
||||||
label={__("Rights Requests")}
|
|
||||||
icon={IconLock}
|
|
||||||
to={`${prefix}/rights-requests`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isAuthorized("Organization", "listStatesOfApplicability") && (
|
|
||||||
<SidebarItem
|
|
||||||
label={__("States of Applicability")}
|
|
||||||
icon={IconPageCheck}
|
|
||||||
to={`${prefix}/states-of-applicability`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isAuthorized("Organization", "listSnapshots") && (
|
|
||||||
<SidebarItem
|
|
||||||
label={__("Snapshots")}
|
|
||||||
icon={IconClock}
|
|
||||||
to={`${prefix}/snapshots`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isAuthorized("Organization", "getTrustCenter") && (
|
|
||||||
<SidebarItem
|
|
||||||
label={__("Trust Center")}
|
|
||||||
icon={IconShield}
|
|
||||||
to={`${prefix}/trust-center`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isAuthorized("Organization", "listMembers") && (
|
|
||||||
<SidebarItem
|
|
||||||
label={__("Settings")}
|
|
||||||
icon={IconSettingsGear2}
|
|
||||||
to={`${prefix}/settings`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ErrorBoundary FallbackComponent={PageError}>
|
|
||||||
<Outlet />
|
|
||||||
</ErrorBoundary>
|
|
||||||
</Layout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function UserDropdown({ organizationId }: { organizationId: string }) {
|
|
||||||
const { __ } = useTranslate();
|
|
||||||
const { toast } = useToast();
|
|
||||||
const { isAuthorized } = use(PermissionsContext);
|
|
||||||
// const user = useLazyLoadQuery<MainLayoutQueryType>(MainLayoutQuery, {
|
|
||||||
// organizationId,
|
|
||||||
// }).viewer.user;
|
|
||||||
const user = {
|
|
||||||
fullName: "",
|
|
||||||
email: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLogout: React.MouseEventHandler<HTMLAnchorElement> = async (
|
|
||||||
e
|
|
||||||
) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
fetch("/connect/logout", {
|
|
||||||
method: "DELETE",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({}),
|
|
||||||
})
|
|
||||||
.then(async (res) => {
|
|
||||||
if (!res.ok) {
|
|
||||||
const error = await res.json();
|
|
||||||
throw new Error(error.message || __("Failed to login"));
|
|
||||||
}
|
|
||||||
|
|
||||||
window.location.reload();
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
toast({
|
|
||||||
title: __("Error"),
|
|
||||||
description: e.message as string,
|
|
||||||
variant: "error",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<UserDropdownRoot fullName={user.fullName} email={user.email}>
|
|
||||||
{isAuthorized("Organization", "deleteOrganization") && (
|
|
||||||
<UserDropdownItem
|
|
||||||
to="/api-keys"
|
|
||||||
icon={IconKey}
|
|
||||||
label={__("API Keys")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isAuthorized("Organization", "listSignableDocuments") && (
|
|
||||||
<UserDropdownItem
|
|
||||||
to={`/organizations/${organizationId}/employee`}
|
|
||||||
icon={IconPageTextLine}
|
|
||||||
label={__("My Signatures")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<UserDropdownItem
|
|
||||||
to="mailto:support@getprobo.com"
|
|
||||||
icon={IconCircleQuestionmark}
|
|
||||||
label={__("Help")}
|
|
||||||
/>
|
|
||||||
<DropdownSeparator />
|
|
||||||
<UserDropdownItem
|
|
||||||
variant="danger"
|
|
||||||
to="/logout"
|
|
||||||
icon={IconArrowBoxLeft}
|
|
||||||
label="Logout"
|
|
||||||
onClick={handleLogout}
|
|
||||||
/>
|
|
||||||
</UserDropdownRoot>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Organization {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
logoUrl?: string | null;
|
|
||||||
authenticationMethod: string;
|
|
||||||
authStatus: "authenticated" | "unauthenticated" | "expired";
|
|
||||||
loginUrl: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OrganizationsResponse {
|
|
||||||
organizations: Organization[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Invitation {
|
|
||||||
id: string;
|
|
||||||
email: string;
|
|
||||||
fullName: string;
|
|
||||||
role: string;
|
|
||||||
expiresAt: string;
|
|
||||||
acceptedAt?: string | null;
|
|
||||||
createdAt: string;
|
|
||||||
organization: {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface InvitationsResponse {
|
|
||||||
invitations: Invitation[];
|
|
||||||
}
|
|
||||||
|
|
||||||
function OrganizationSelector({
|
|
||||||
currentOrganization,
|
|
||||||
}: {
|
|
||||||
currentOrganization: MainLayoutQueryType["response"]["organization"];
|
|
||||||
}) {
|
|
||||||
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
|
||||||
const [pendingInvitationsCount, setPendingInvitationsCount] = useState(0);
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [search, setSearch] = useState("");
|
|
||||||
const { __ } = useTranslate();
|
|
||||||
|
|
||||||
const filteredOrganizations = useMemo(() => {
|
|
||||||
if (!search.trim()) {
|
|
||||||
return organizations;
|
|
||||||
}
|
|
||||||
return organizations.filter((org) =>
|
|
||||||
org.name.toLowerCase().includes(search.toLowerCase())
|
|
||||||
);
|
|
||||||
}, [organizations, search]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
const [orgsResponse, invitationsResponse] = await Promise.all([
|
|
||||||
fetch("/connect/organizations", { credentials: "include" }),
|
|
||||||
fetch("/connect/invitations", { credentials: "include" }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!orgsResponse.ok) {
|
|
||||||
throw new Error("Failed to fetch organizations");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!invitationsResponse.ok) {
|
|
||||||
throw new Error("Failed to fetch invitations");
|
|
||||||
}
|
|
||||||
|
|
||||||
const orgsData: OrganizationsResponse = await orgsResponse.json();
|
|
||||||
const invitationsData: InvitationsResponse =
|
|
||||||
await invitationsResponse.json();
|
|
||||||
|
|
||||||
const pendingCount = invitationsData.invitations.filter(
|
|
||||||
(inv) => !inv.acceptedAt
|
|
||||||
).length;
|
|
||||||
|
|
||||||
setOrganizations(orgsData.organizations);
|
|
||||||
setPendingInvitationsCount(pendingCount);
|
|
||||||
setError(null);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : "Unknown error");
|
|
||||||
console.error("Failed to fetch data:", err);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Button className="-ml-3" variant="tertiary" disabled>
|
|
||||||
{__("Error loading organizations")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Dropdown
|
|
||||||
toggle={
|
|
||||||
<Button
|
|
||||||
className="-ml-3"
|
|
||||||
variant="tertiary"
|
|
||||||
iconAfter={IconChevronGrabberVertical}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
{isLoading ? __("Loading...") : currentOrganization?.name || ""}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="px-3 py-2">
|
|
||||||
<Input
|
|
||||||
icon={IconMagnifyingGlass}
|
|
||||||
placeholder={__("Search organizations...")}
|
|
||||||
value={search}
|
|
||||||
onValueChange={setSearch}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="max-h-150 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent hover:scrollbar-thumb-gray-400">
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="px-3 py-2 text-gray-500">
|
|
||||||
{__("Loading organizations...")}
|
|
||||||
</div>
|
|
||||||
) : filteredOrganizations.length === 0 ? (
|
|
||||||
<div className="px-3 py-2 text-txt-secondary">
|
|
||||||
{__("No organizations found")}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
filteredOrganizations.map((organization) => {
|
|
||||||
const isAuthenticated =
|
|
||||||
organization.authStatus === "authenticated";
|
|
||||||
const isExpired = organization.authStatus === "expired";
|
|
||||||
const needsAuth = organization.authStatus === "unauthenticated";
|
|
||||||
|
|
||||||
const targetUrl = isAuthenticated
|
|
||||||
? `/organizations/${organization.id}`
|
|
||||||
: organization.loginUrl;
|
|
||||||
|
|
||||||
const isSAMLUrl = targetUrl.includes("/connect/saml/");
|
|
||||||
|
|
||||||
// Use organization endpoint for all logos for consistency
|
|
||||||
const logoUrl = organization.logoUrl;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownItem asChild key={organization.id}>
|
|
||||||
{isSAMLUrl ? (
|
|
||||||
<a href={targetUrl} className="flex items-center gap-2">
|
|
||||||
<Avatar name={organization.name} src={logoUrl} />
|
|
||||||
<span className="flex-1">{organization.name}</span>
|
|
||||||
{isAuthenticated && (
|
|
||||||
<IconCheckmark1 size={16} className="text-green-600" />
|
|
||||||
)}
|
|
||||||
{isExpired && (
|
|
||||||
<IconClock size={16} className="text-orange-600" />
|
|
||||||
)}
|
|
||||||
{needsAuth && (
|
|
||||||
<IconLock size={16} className="text-gray-400" />
|
|
||||||
)}
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<Link to={targetUrl} className="flex items-center gap-2">
|
|
||||||
<Avatar name={organization.name} src={logoUrl} />
|
|
||||||
<span className="flex-1">{organization.name}</span>
|
|
||||||
{isAuthenticated && (
|
|
||||||
<IconCheckmark1 size={16} className="text-green-600" />
|
|
||||||
)}
|
|
||||||
{isExpired && (
|
|
||||||
<IconClock size={16} className="text-orange-600" />
|
|
||||||
)}
|
|
||||||
{needsAuth && (
|
|
||||||
<IconLock size={16} className="text-gray-400" />
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</DropdownItem>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<DropdownSeparator />
|
|
||||||
{pendingInvitationsCount > 0 && (
|
|
||||||
<DropdownItem asChild>
|
|
||||||
<Link to="/">
|
|
||||||
<IconPeopleAdd size={16} />
|
|
||||||
<span className="flex-1">{__("Invitations")}</span>
|
|
||||||
<Badge variant="info" size="sm">
|
|
||||||
{pendingInvitationsCount}
|
|
||||||
</Badge>
|
|
||||||
</Link>
|
|
||||||
</DropdownItem>
|
|
||||||
)}
|
|
||||||
<DropdownItem asChild>
|
|
||||||
<Link to="/organizations/new">
|
|
||||||
<IconPlusLarge size={16} />
|
|
||||||
{__("Add organization")}
|
|
||||||
</Link>
|
|
||||||
</DropdownItem>
|
|
||||||
</Dropdown>
|
|
||||||
{pendingInvitationsCount > 0 && (
|
|
||||||
<Link to="/" className="relative" title={__("Invitations")}>
|
|
||||||
<Button variant="tertiary" icon={IconPeopleAdd} />
|
|
||||||
<Badge
|
|
||||||
variant="info"
|
|
||||||
size="sm"
|
|
||||||
className="absolute -top-1 -right-1 min-w-[20px] h-5 flex items-center justify-center"
|
|
||||||
>
|
|
||||||
{pendingInvitationsCount}
|
|
||||||
</Badge>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
IconInboxEmpty,
|
IconInboxEmpty,
|
||||||
IconListStack,
|
IconListStack,
|
||||||
IconMedal,
|
IconMedal,
|
||||||
|
IconPageCheck,
|
||||||
IconPageTextLine,
|
IconPageTextLine,
|
||||||
IconRotateCw,
|
IconRotateCw,
|
||||||
IconSettingsGear2,
|
IconSettingsGear2,
|
||||||
@@ -139,6 +140,13 @@ export function Sidebar() {
|
|||||||
to={`${prefix}/processing-activities`}
|
to={`${prefix}/processing-activities`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{isAuthorized("Organization", "listStatesOfApplicability") && (
|
||||||
|
<SidebarItem
|
||||||
|
label={__("States of Applicability")}
|
||||||
|
icon={IconPageCheck}
|
||||||
|
to={`${prefix}/states-of-applicability`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{isAuthorized("Organization", "listSnapshots") && (
|
{isAuthorized("Organization", "listSnapshots") && (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Snapshots")}
|
label={__("Snapshots")}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { loadQuery } from "react-relay";
|
import { loadQuery } from "react-relay";
|
||||||
import { consoleEnvironment } from "/environments";
|
import { coreEnvironment } from "/environments";
|
||||||
import { PageSkeleton } from "/components/skeletons/PageSkeleton";
|
import { PageSkeleton } from "/components/skeletons/PageSkeleton";
|
||||||
import { lazy } from "@probo/react-lazy";
|
import { lazy } from "@probo/react-lazy";
|
||||||
import { rightsRequestsQuery, rightsRequestNodeQuery } from "/hooks/graph/RightsRequestGraph";
|
import { rightsRequestsQuery, rightsRequestNodeQuery } from "/hooks/graph/RightsRequestGraph";
|
||||||
@@ -12,7 +12,7 @@ export const rightsRequestRoutes = [
|
|||||||
path: "rights-requests",
|
path: "rights-requests",
|
||||||
Fallback: PageSkeleton,
|
Fallback: PageSkeleton,
|
||||||
loader: loaderFromQueryLoader(({ organizationId }) =>
|
loader: loaderFromQueryLoader(({ organizationId }) =>
|
||||||
loadQuery<RightsRequestGraphListQuery>(consoleEnvironment, rightsRequestsQuery, {
|
loadQuery<RightsRequestGraphListQuery>(coreEnvironment, rightsRequestsQuery, {
|
||||||
organizationId,
|
organizationId,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -24,7 +24,7 @@ export const rightsRequestRoutes = [
|
|||||||
path: "rights-requests/:requestId",
|
path: "rights-requests/:requestId",
|
||||||
Fallback: PageSkeleton,
|
Fallback: PageSkeleton,
|
||||||
loader: loaderFromQueryLoader(({ requestId }) =>
|
loader: loaderFromQueryLoader(({ requestId }) =>
|
||||||
loadQuery<RightsRequestGraphNodeQuery>(consoleEnvironment, rightsRequestNodeQuery, {
|
loadQuery<RightsRequestGraphNodeQuery>(coreEnvironment, rightsRequestNodeQuery, {
|
||||||
rightsRequestId: requestId!,
|
rightsRequestId: requestId!,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { lazy } from "@probo/react-lazy";
|
import { lazy } from "@probo/react-lazy";
|
||||||
import { loadQuery } from "react-relay";
|
import { loadQuery } from "react-relay";
|
||||||
import { consoleEnvironment } from "/environments";
|
import { coreEnvironment } from "/environments";
|
||||||
import { PageSkeleton } from "/components/skeletons/PageSkeleton.tsx";
|
import { PageSkeleton } from "/components/skeletons/PageSkeleton.tsx";
|
||||||
import {
|
import {
|
||||||
paginatedStateOfApplicabilityQuery,
|
paginatedStateOfApplicabilityQuery,
|
||||||
@@ -16,7 +16,7 @@ export const statesOfApplicabilityRoutes = [
|
|||||||
Fallback: PageSkeleton,
|
Fallback: PageSkeleton,
|
||||||
loader: loaderFromQueryLoader(({ organizationId }) =>
|
loader: loaderFromQueryLoader(({ organizationId }) =>
|
||||||
loadQuery<StateOfApplicabilityGraphPaginatedQuery>(
|
loadQuery<StateOfApplicabilityGraphPaginatedQuery>(
|
||||||
consoleEnvironment,
|
coreEnvironment,
|
||||||
paginatedStateOfApplicabilityQuery,
|
paginatedStateOfApplicabilityQuery,
|
||||||
{ organizationId: organizationId! },
|
{ organizationId: organizationId! },
|
||||||
),
|
),
|
||||||
@@ -30,7 +30,7 @@ export const statesOfApplicabilityRoutes = [
|
|||||||
Fallback: PageSkeleton,
|
Fallback: PageSkeleton,
|
||||||
loader: loaderFromQueryLoader(({ organizationId }) =>
|
loader: loaderFromQueryLoader(({ organizationId }) =>
|
||||||
loadQuery<StateOfApplicabilityGraphPaginatedQuery>(
|
loadQuery<StateOfApplicabilityGraphPaginatedQuery>(
|
||||||
consoleEnvironment,
|
coreEnvironment,
|
||||||
paginatedStateOfApplicabilityQuery,
|
paginatedStateOfApplicabilityQuery,
|
||||||
{ organizationId: organizationId! },
|
{ organizationId: organizationId! },
|
||||||
),
|
),
|
||||||
@@ -44,7 +44,7 @@ export const statesOfApplicabilityRoutes = [
|
|||||||
Fallback: PageSkeleton,
|
Fallback: PageSkeleton,
|
||||||
loader: loaderFromQueryLoader(({ stateOfApplicabilityId }) =>
|
loader: loaderFromQueryLoader(({ stateOfApplicabilityId }) =>
|
||||||
loadQuery<StateOfApplicabilityGraphNodeQuery>(
|
loadQuery<StateOfApplicabilityGraphNodeQuery>(
|
||||||
consoleEnvironment,
|
coreEnvironment,
|
||||||
stateOfApplicabilityNodeQuery,
|
stateOfApplicabilityNodeQuery,
|
||||||
{ stateOfApplicabilityId: stateOfApplicabilityId! },
|
{ stateOfApplicabilityId: stateOfApplicabilityId! },
|
||||||
),
|
),
|
||||||
@@ -58,7 +58,7 @@ export const statesOfApplicabilityRoutes = [
|
|||||||
Fallback: PageSkeleton,
|
Fallback: PageSkeleton,
|
||||||
loader: loaderFromQueryLoader(({ stateOfApplicabilityId }) =>
|
loader: loaderFromQueryLoader(({ stateOfApplicabilityId }) =>
|
||||||
loadQuery<StateOfApplicabilityGraphNodeQuery>(
|
loadQuery<StateOfApplicabilityGraphNodeQuery>(
|
||||||
consoleEnvironment,
|
coreEnvironment,
|
||||||
stateOfApplicabilityNodeQuery,
|
stateOfApplicabilityNodeQuery,
|
||||||
{ stateOfApplicabilityId: stateOfApplicabilityId! },
|
{ stateOfApplicabilityId: stateOfApplicabilityId! },
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ const (
|
|||||||
RightsRequestEntityType uint16 = 48
|
RightsRequestEntityType uint16 = 48
|
||||||
StateOfApplicabilityEntityType uint16 = 49
|
StateOfApplicabilityEntityType uint16 = 49
|
||||||
StateOfApplicabilityControlEntityType uint16 = 50
|
StateOfApplicabilityControlEntityType uint16 = 50
|
||||||
|
IdentityProfileEntityType uint16 = 51
|
||||||
)
|
)
|
||||||
|
|
||||||
type EntityInfo struct {
|
type EntityInfo struct {
|
||||||
@@ -284,6 +285,10 @@ var entityRegistry = map[uint16]EntityInfo{
|
|||||||
Model: "StateOfApplicabilityControl",
|
Model: "StateOfApplicabilityControl",
|
||||||
Table: "states_of_applicability_controls",
|
Table: "states_of_applicability_controls",
|
||||||
},
|
},
|
||||||
|
IdentityProfileEntityType: {
|
||||||
|
Model: "IdentityProfile",
|
||||||
|
Table: "iam_identity_profiles",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func EntityTable(entityType uint16) (string, bool) {
|
func EntityTable(entityType uint16) (string, bool) {
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ type (
|
|||||||
ID gid.GID `db:"id"`
|
ID gid.GID `db:"id"`
|
||||||
EmailAddress mail.Addr `db:"email_address"`
|
EmailAddress mail.Addr `db:"email_address"`
|
||||||
HashedPassword []byte `db:"hashed_password"`
|
HashedPassword []byte `db:"hashed_password"`
|
||||||
FullName string `db:"fullname"`
|
|
||||||
EmailAddressVerified bool `db:"email_address_verified"`
|
EmailAddressVerified bool `db:"email_address_verified"`
|
||||||
SAMLSubject *string `db:"saml_subject"`
|
SAMLSubject *string `db:"saml_subject"`
|
||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
@@ -66,7 +65,6 @@ SELECT
|
|||||||
email_address,
|
email_address,
|
||||||
hashed_password,
|
hashed_password,
|
||||||
email_address_verified,
|
email_address_verified,
|
||||||
fullname,
|
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -143,7 +141,6 @@ SELECT
|
|||||||
email_address,
|
email_address,
|
||||||
hashed_password,
|
hashed_password,
|
||||||
email_address_verified,
|
email_address_verified,
|
||||||
fullname,
|
|
||||||
saml_subject,
|
saml_subject,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
@@ -187,7 +184,6 @@ SELECT
|
|||||||
email_address,
|
email_address,
|
||||||
hashed_password,
|
hashed_password,
|
||||||
email_address_verified,
|
email_address_verified,
|
||||||
fullname,
|
|
||||||
saml_subject,
|
saml_subject,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
@@ -225,13 +221,12 @@ func (i *Identity) Insert(
|
|||||||
) error {
|
) error {
|
||||||
q := `
|
q := `
|
||||||
INSERT INTO
|
INSERT INTO
|
||||||
identities (id, email_address, hashed_password, email_address_verified, fullname, saml_subject, created_at, updated_at)
|
identities (id, email_address, hashed_password, email_address_verified, saml_subject, created_at, updated_at)
|
||||||
VALUES (
|
VALUES (
|
||||||
@identity_id,
|
@identity_id,
|
||||||
@email_address,
|
@email_address,
|
||||||
@hashed_password,
|
@hashed_password,
|
||||||
@email_address_verified,
|
@email_address_verified,
|
||||||
@fullname,
|
|
||||||
@saml_subject,
|
@saml_subject,
|
||||||
@created_at,
|
@created_at,
|
||||||
@updated_at
|
@updated_at
|
||||||
@@ -242,7 +237,6 @@ VALUES (
|
|||||||
"identity_id": i.ID,
|
"identity_id": i.ID,
|
||||||
"email_address": i.EmailAddress,
|
"email_address": i.EmailAddress,
|
||||||
"hashed_password": i.HashedPassword,
|
"hashed_password": i.HashedPassword,
|
||||||
"fullname": i.FullName,
|
|
||||||
"saml_subject": i.SAMLSubject,
|
"saml_subject": i.SAMLSubject,
|
||||||
"created_at": i.CreatedAt,
|
"created_at": i.CreatedAt,
|
||||||
"updated_at": i.UpdatedAt,
|
"updated_at": i.UpdatedAt,
|
||||||
@@ -273,7 +267,6 @@ SET
|
|||||||
email_address = @email_address,
|
email_address = @email_address,
|
||||||
email_address_verified = @email_address_verified,
|
email_address_verified = @email_address_verified,
|
||||||
saml_subject = @saml_subject,
|
saml_subject = @saml_subject,
|
||||||
fullname = @fullname,
|
|
||||||
hashed_password = @hashed_password,
|
hashed_password = @hashed_password,
|
||||||
updated_at = @updated_at
|
updated_at = @updated_at
|
||||||
WHERE
|
WHERE
|
||||||
@@ -286,7 +279,6 @@ WHERE
|
|||||||
"email_address_verified": i.EmailAddressVerified,
|
"email_address_verified": i.EmailAddressVerified,
|
||||||
"saml_subject": i.SAMLSubject,
|
"saml_subject": i.SAMLSubject,
|
||||||
"updated_at": i.UpdatedAt,
|
"updated_at": i.UpdatedAt,
|
||||||
"fullname": i.FullName,
|
|
||||||
"hashed_password": i.HashedPassword,
|
"hashed_password": i.HashedPassword,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,7 +306,6 @@ SELECT
|
|||||||
email_address,
|
email_address,
|
||||||
hashed_password,
|
hashed_password,
|
||||||
email_address_verified,
|
email_address_verified,
|
||||||
fullname,
|
|
||||||
saml_subject,
|
saml_subject,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
|
|||||||
293
pkg/coredata/identity_profile.go
Normal file
293
pkg/coredata/identity_profile.go
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
// 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"
|
||||||
|
"maps"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"go.gearno.de/kit/pg"
|
||||||
|
"go.probo.inc/probo/pkg/gid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
IdentityProfile struct {
|
||||||
|
ID gid.GID `db:"id"`
|
||||||
|
IdentityID gid.GID `db:"identity_id"`
|
||||||
|
MembershipID *gid.GID `db:"membership_id"`
|
||||||
|
FullName string `db:"full_name"`
|
||||||
|
CreatedAt time.Time `db:"created_at"`
|
||||||
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
IdentityProfiles []*IdentityProfile
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *IdentityProfile) IsDefault() bool {
|
||||||
|
return p.MembershipID == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadDefaultByIdentityID loads the default profile for an identity (where membership_id is NULL)
|
||||||
|
func (p *IdentityProfile) LoadDefaultByIdentityID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
identityID gid.GID,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
identity_id,
|
||||||
|
membership_id,
|
||||||
|
full_name,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM
|
||||||
|
iam_identity_profiles
|
||||||
|
WHERE
|
||||||
|
tenant_id IS NULL
|
||||||
|
AND identity_id = @identity_id
|
||||||
|
AND membership_id IS NULL
|
||||||
|
LIMIT 1;
|
||||||
|
`
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{"identity_id": identityID}
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query default identity profile: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
profile, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[IdentityProfile])
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return ErrResourceNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("cannot collect default identity profile: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = profile
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadByMembershipID loads the profile for a specific membership
|
||||||
|
func (p *IdentityProfile) LoadByMembershipID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
membershipID gid.GID,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
identity_id,
|
||||||
|
membership_id,
|
||||||
|
full_name,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM
|
||||||
|
iam_identity_profiles
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND membership_id = @membership_id
|
||||||
|
LIMIT 1;
|
||||||
|
`
|
||||||
|
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{"membership_id": membershipID}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query identity profile: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
profile, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[IdentityProfile])
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return ErrResourceNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("cannot collect identity profile: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = profile
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *IdentityProfile) LoadByID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
profileID gid.GID,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
identity_id,
|
||||||
|
membership_id,
|
||||||
|
full_name,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM
|
||||||
|
iam_identity_profiles
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND id = @profile_id
|
||||||
|
LIMIT 1;
|
||||||
|
`
|
||||||
|
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{"profile_id": profileID}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query identity profile: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
profile, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[IdentityProfile])
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return ErrResourceNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("cannot collect identity profile: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = profile
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *IdentityProfile) Insert(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
INSERT INTO
|
||||||
|
iam_identity_profiles (
|
||||||
|
tenant_id,
|
||||||
|
id,
|
||||||
|
identity_id,
|
||||||
|
membership_id,
|
||||||
|
full_name,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
@tenant_id,
|
||||||
|
@id,
|
||||||
|
@identity_id,
|
||||||
|
@membership_id,
|
||||||
|
@full_name,
|
||||||
|
@created_at,
|
||||||
|
@updated_at
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{
|
||||||
|
"tenant_id": p.ID.TenantID().String(),
|
||||||
|
"id": p.ID,
|
||||||
|
"identity_id": p.IdentityID,
|
||||||
|
"membership_id": p.MembershipID,
|
||||||
|
"full_name": p.FullName,
|
||||||
|
"created_at": p.CreatedAt,
|
||||||
|
"updated_at": p.UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := conn.Exec(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot insert identity profile: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *IdentityProfile) Update(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
UPDATE
|
||||||
|
iam_identity_profiles
|
||||||
|
SET
|
||||||
|
full_name = @full_name,
|
||||||
|
updated_at = @updated_at
|
||||||
|
WHERE
|
||||||
|
id = @id
|
||||||
|
AND %s
|
||||||
|
`
|
||||||
|
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{
|
||||||
|
"id": p.ID,
|
||||||
|
"full_name": p.FullName,
|
||||||
|
"updated_at": p.UpdatedAt,
|
||||||
|
}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
result, err := conn.Exec(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot update identity profile: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.RowsAffected() == 0 {
|
||||||
|
return ErrResourceNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *IdentityProfile) Delete(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
profileID gid.GID,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
DELETE FROM
|
||||||
|
iam_identity_profiles
|
||||||
|
WHERE
|
||||||
|
id = @profile_id
|
||||||
|
AND %s
|
||||||
|
`
|
||||||
|
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{"profile_id": profileID}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
result, err := conn.Exec(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot delete identity profile: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.RowsAffected() == 0 {
|
||||||
|
return ErrResourceNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -189,7 +189,7 @@ SELECT
|
|||||||
mbr.identity_id,
|
mbr.identity_id,
|
||||||
mbr.organization_id,
|
mbr.organization_id,
|
||||||
mbr.role,
|
mbr.role,
|
||||||
i.fullname as full_name,
|
COALESCE(mp.full_name, dp.full_name, '') as full_name,
|
||||||
i.email_address,
|
i.email_address,
|
||||||
mbr.created_at,
|
mbr.created_at,
|
||||||
mbr.updated_at
|
mbr.updated_at
|
||||||
@@ -197,6 +197,10 @@ FROM
|
|||||||
mbr
|
mbr
|
||||||
JOIN
|
JOIN
|
||||||
identities i ON mbr.identity_id = i.id
|
identities i ON mbr.identity_id = i.id
|
||||||
|
LEFT JOIN
|
||||||
|
iam_identity_profiles mp ON mp.membership_id = mbr.id
|
||||||
|
LEFT JOIN
|
||||||
|
iam_identity_profiles dp ON dp.identity_id = mbr.identity_id AND dp.membership_id IS NULL
|
||||||
`
|
`
|
||||||
|
|
||||||
query = fmt.Sprintf(query, scope.SQLFragment())
|
query = fmt.Sprintf(query, scope.SQLFragment())
|
||||||
@@ -329,7 +333,7 @@ SELECT
|
|||||||
mbr.identity_id,
|
mbr.identity_id,
|
||||||
mbr.organization_id,
|
mbr.organization_id,
|
||||||
mbr.role,
|
mbr.role,
|
||||||
i.fullname as full_name,
|
COALESCE(mp.full_name, dp.full_name, '') as full_name,
|
||||||
i.email_address,
|
i.email_address,
|
||||||
mbr.created_at,
|
mbr.created_at,
|
||||||
mbr.updated_at
|
mbr.updated_at
|
||||||
@@ -337,6 +341,10 @@ FROM
|
|||||||
mbr
|
mbr
|
||||||
JOIN
|
JOIN
|
||||||
identities i ON mbr.identity_id = i.id
|
identities i ON mbr.identity_id = i.id
|
||||||
|
LEFT JOIN
|
||||||
|
iam_identity_profiles mp ON mp.membership_id = mbr.id
|
||||||
|
LEFT JOIN
|
||||||
|
iam_identity_profiles dp ON dp.identity_id = mbr.identity_id AND dp.membership_id IS NULL
|
||||||
`
|
`
|
||||||
|
|
||||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
@@ -455,7 +463,7 @@ SELECT
|
|||||||
mbr.identity_id,
|
mbr.identity_id,
|
||||||
mbr.organization_id,
|
mbr.organization_id,
|
||||||
mbr.role,
|
mbr.role,
|
||||||
i.fullname as full_name,
|
COALESCE(mp.full_name, dp.full_name, '') as full_name,
|
||||||
i.email_address,
|
i.email_address,
|
||||||
mbr.created_at,
|
mbr.created_at,
|
||||||
mbr.updated_at
|
mbr.updated_at
|
||||||
@@ -463,6 +471,10 @@ FROM
|
|||||||
mbr
|
mbr
|
||||||
JOIN
|
JOIN
|
||||||
identities i ON mbr.identity_id = i.id
|
identities i ON mbr.identity_id = i.id
|
||||||
|
LEFT JOIN
|
||||||
|
iam_identity_profiles mp ON mp.membership_id = mbr.id
|
||||||
|
LEFT JOIN
|
||||||
|
iam_identity_profiles dp ON dp.identity_id = mbr.identity_id AND dp.membership_id IS NULL
|
||||||
ORDER BY
|
ORDER BY
|
||||||
mbr.created_at DESC
|
mbr.created_at DESC
|
||||||
`
|
`
|
||||||
@@ -525,7 +537,7 @@ FROM (
|
|||||||
mbr.identity_id,
|
mbr.identity_id,
|
||||||
mbr.organization_id,
|
mbr.organization_id,
|
||||||
mbr.role,
|
mbr.role,
|
||||||
i.fullname as full_name,
|
COALESCE(mp.full_name, dp.full_name, '') as full_name,
|
||||||
i.email_address,
|
i.email_address,
|
||||||
mbr.created_at,
|
mbr.created_at,
|
||||||
mbr.updated_at
|
mbr.updated_at
|
||||||
@@ -533,6 +545,10 @@ FROM (
|
|||||||
mbr
|
mbr
|
||||||
JOIN
|
JOIN
|
||||||
identities i ON mbr.identity_id = i.id
|
identities i ON mbr.identity_id = i.id
|
||||||
|
LEFT JOIN
|
||||||
|
iam_identity_profiles mp ON mp.membership_id = mbr.id
|
||||||
|
LEFT JOIN
|
||||||
|
iam_identity_profiles dp ON dp.identity_id = mbr.identity_id AND dp.membership_id IS NULL
|
||||||
) AS membership_with_identity
|
) AS membership_with_identity
|
||||||
WHERE %s
|
WHERE %s
|
||||||
`
|
`
|
||||||
|
|||||||
12
pkg/coredata/migrations/20251220T140915Z.sql
Normal file
12
pkg/coredata/migrations/20251220T140915Z.sql
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
CREATE TABLE iam_identity_profiles (
|
||||||
|
tenant_id TEXT NOT NULL,
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
identity_id TEXT NOT NULL REFERENCES identities(id) ON DELETE CASCADE,
|
||||||
|
membership_id TEXT REFERENCES iam_memberships(id) ON DELETE CASCADE,
|
||||||
|
full_name TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL,
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX idx_iam_identity_profiles_default ON iam_identity_profiles(identity_id) WHERE membership_id IS NULL;
|
||||||
|
CREATE UNIQUE INDEX idx_iam_identity_profiles_membership ON iam_identity_profiles(membership_id) WHERE membership_id IS NOT NULL;
|
||||||
32
pkg/coredata/migrations/20251221T222519Z.sql
Normal file
32
pkg/coredata/migrations/20251221T222519Z.sql
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
INSERT INTO iam_identity_profiles (tenant_id, id, identity_id, membership_id, full_name, created_at, updated_at)
|
||||||
|
SELECT
|
||||||
|
'',
|
||||||
|
generate_gid('\x0000000000000000'::bytea, 51),
|
||||||
|
i.id,
|
||||||
|
NULL,
|
||||||
|
COALESCE(i.fullname, ''),
|
||||||
|
i.created_at,
|
||||||
|
NOW()
|
||||||
|
FROM identities i
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM iam_identity_profiles p
|
||||||
|
WHERE p.identity_id = i.id AND p.membership_id IS NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO iam_identity_profiles (tenant_id, id, identity_id, membership_id, full_name, created_at, updated_at)
|
||||||
|
SELECT
|
||||||
|
m.tenant_id,
|
||||||
|
generate_gid(decode_base64_unpadded(m.tenant_id), 51),
|
||||||
|
m.identity_id,
|
||||||
|
m.id,
|
||||||
|
COALESCE(i.fullname, ''),
|
||||||
|
m.created_at,
|
||||||
|
NOW()
|
||||||
|
FROM iam_memberships m
|
||||||
|
JOIN identities i ON i.id = m.identity_id
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM iam_identity_profiles p
|
||||||
|
WHERE p.membership_id = m.id
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE identities DROP COLUMN fullname;
|
||||||
@@ -125,9 +125,14 @@ func (s AccountService) ChangeEmail(ctx context.Context, identityID gid.GID, req
|
|||||||
return fmt.Errorf("cannot update identity: %w", err)
|
return fmt.Errorf("cannot update identity: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
profile := &coredata.IdentityProfile{}
|
||||||
|
if err := profile.LoadDefaultByIdentityID(ctx, tx, identityID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load default profile: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
subject, textBody, htmlBody, err := emails.RenderConfirmEmail(
|
subject, textBody, htmlBody, err := emails.RenderConfirmEmail(
|
||||||
s.baseURL,
|
s.baseURL,
|
||||||
identity.FullName,
|
profile.FullName,
|
||||||
confirmationUrl,
|
confirmationUrl,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -135,7 +140,7 @@ func (s AccountService) ChangeEmail(ctx context.Context, identityID gid.GID, req
|
|||||||
}
|
}
|
||||||
|
|
||||||
confirmationEmail := coredata.NewEmail(
|
confirmationEmail := coredata.NewEmail(
|
||||||
identity.FullName,
|
profile.FullName,
|
||||||
identity.EmailAddress,
|
identity.EmailAddress,
|
||||||
subject,
|
subject,
|
||||||
textBody,
|
textBody,
|
||||||
@@ -714,3 +719,157 @@ func (s AccountService) ListOrganizations(ctx context.Context, identityID gid.GI
|
|||||||
|
|
||||||
return organizations, nil
|
return organizations, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s AccountService) GetProfileForMembership(ctx context.Context, membershipID gid.GID) (*coredata.IdentityProfile, error) {
|
||||||
|
var (
|
||||||
|
scope = coredata.NewScopeFromObjectID(membershipID)
|
||||||
|
profile = &coredata.IdentityProfile{}
|
||||||
|
)
|
||||||
|
|
||||||
|
err := s.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
membership := &coredata.Membership{}
|
||||||
|
err := membership.LoadByID(ctx, conn, scope, membershipID)
|
||||||
|
if err != nil {
|
||||||
|
if err == coredata.ErrResourceNotFound {
|
||||||
|
return NewMembershipNotFoundError(membershipID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("cannot load membership: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = profile.LoadByMembershipID(ctx, conn, scope, membershipID)
|
||||||
|
if err != nil {
|
||||||
|
if err == coredata.ErrResourceNotFound {
|
||||||
|
return NewProfileNotFoundError(membershipID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("cannot load identity profile: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return profile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s AccountService) GetDefaultProfile(ctx context.Context, identityID gid.GID) (*coredata.IdentityProfile, error) {
|
||||||
|
var profile = &coredata.IdentityProfile{}
|
||||||
|
|
||||||
|
err := s.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
err := profile.LoadDefaultByIdentityID(ctx, conn, identityID)
|
||||||
|
if err != nil {
|
||||||
|
if err == coredata.ErrResourceNotFound {
|
||||||
|
return NewProfileNotFoundError(identityID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("cannot load default profile: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return profile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateIdentityProfileRequest struct {
|
||||||
|
MembershipID gid.GID
|
||||||
|
FullName *string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s AccountService) UpdateIdentityProfile(
|
||||||
|
ctx context.Context,
|
||||||
|
identityID gid.GID,
|
||||||
|
req *UpdateIdentityProfileRequest,
|
||||||
|
) (*coredata.IdentityProfile, error) {
|
||||||
|
// var (
|
||||||
|
// scope = coredata.NewScopeFromObjectID(req.MembershipID)
|
||||||
|
// profile = &coredata.IdentityProfile{}
|
||||||
|
// )
|
||||||
|
|
||||||
|
// err := s.pg.WithTx(
|
||||||
|
// ctx,
|
||||||
|
// func(tx pg.Conn) error {
|
||||||
|
// // First verify the membership belongs to the identity
|
||||||
|
// membership := &coredata.Membership{}
|
||||||
|
// err := membership.LoadByID(ctx, tx, scope, req.MembershipID)
|
||||||
|
// if err != nil {
|
||||||
|
// if err == coredata.ErrResourceNotFound {
|
||||||
|
// return NewMembershipNotFoundError(req.MembershipID)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return fmt.Errorf("cannot load membership: %w", err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if membership.IdentityID != identityID {
|
||||||
|
// return NewMembershipNotFoundError(req.MembershipID)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Try to load existing membership profile
|
||||||
|
// err = profile.LoadByMembershipID(ctx, tx, scope, req.MembershipID)
|
||||||
|
// if err != nil && err != coredata.ErrResourceNotFound {
|
||||||
|
// return fmt.Errorf("cannot load identity profile: %w", err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// now := time.Now()
|
||||||
|
|
||||||
|
// if err == coredata.ErrResourceNotFound {
|
||||||
|
// // Create new membership profile, optionally inheriting from default
|
||||||
|
// tenantID := req.MembershipID.TenantID()
|
||||||
|
// membershipID := req.MembershipID
|
||||||
|
|
||||||
|
// // Try to get default profile to inherit FullName
|
||||||
|
// defaultProfile := &coredata.IdentityProfile{}
|
||||||
|
// defaultFullName := ""
|
||||||
|
// if loadErr := defaultProfile.LoadDefaultByIdentityID(ctx, tx, identityID); loadErr == nil {
|
||||||
|
// defaultFullName = defaultProfile.FullName
|
||||||
|
// }
|
||||||
|
|
||||||
|
// tenantIDStr := tenantID.String()
|
||||||
|
// profile = &coredata.IdentityProfile{
|
||||||
|
// ID: gid.New(tenantID, coredata.IdentityProfileEntityType),
|
||||||
|
// TenantID: &tenantIDStr,
|
||||||
|
// IdentityID: identityID,
|
||||||
|
// MembershipID: &membershipID,
|
||||||
|
// FullName: defaultFullName,
|
||||||
|
// CreatedAt: now,
|
||||||
|
// UpdatedAt: now,
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Apply updates
|
||||||
|
// if req.FullName != nil {
|
||||||
|
// profile.FullName = *req.FullName
|
||||||
|
// }
|
||||||
|
// profile.UpdatedAt = now
|
||||||
|
|
||||||
|
// // Upsert the membership profile
|
||||||
|
// err = profile.UpsertMembership(ctx, tx)
|
||||||
|
// if err != nil {
|
||||||
|
// return fmt.Errorf("cannot upsert identity profile: %w", err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return nil
|
||||||
|
// },
|
||||||
|
// )
|
||||||
|
|
||||||
|
// if err != nil {
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return profile, nil
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -158,7 +158,6 @@ func (s *AuthService) CreateIdentityFromInvitation(
|
|||||||
EmailAddress: invitation.Email,
|
EmailAddress: invitation.Email,
|
||||||
HashedPassword: hashedPassword,
|
HashedPassword: hashedPassword,
|
||||||
EmailAddressVerified: true,
|
EmailAddressVerified: true,
|
||||||
FullName: invitation.FullName,
|
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
@@ -172,6 +171,19 @@ func (s *AuthService) CreateIdentityFromInvitation(
|
|||||||
return fmt.Errorf("cannot insert identity: %w", err)
|
return fmt.Errorf("cannot insert identity: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defaultProfile := &coredata.IdentityProfile{
|
||||||
|
ID: gid.New(gid.NilTenant, coredata.IdentityProfileEntityType),
|
||||||
|
IdentityID: identity.ID,
|
||||||
|
FullName: invitation.FullName,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = defaultProfile.Insert(ctx, tx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot insert default profile: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
session = coredata.NewRootSession(identity.ID, coredata.AuthMethodPassword, s.sessionDuration)
|
session = coredata.NewRootSession(identity.ID, coredata.AuthMethodPassword, s.sessionDuration)
|
||||||
err = session.Insert(ctx, tx)
|
err = session.Insert(ctx, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -273,9 +285,14 @@ func (s AuthService) SendPasswordResetInstructionByEmail(
|
|||||||
return fmt.Errorf("cannot load identity: %w", err)
|
return fmt.Errorf("cannot load identity: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
profile := &coredata.IdentityProfile{}
|
||||||
|
if err := profile.LoadDefaultByIdentityID(ctx, tx, identity.ID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load default profile: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
subject, textBody, htmlBody, err := emails.RenderPasswordReset(
|
subject, textBody, htmlBody, err := emails.RenderPasswordReset(
|
||||||
s.baseURL,
|
s.baseURL,
|
||||||
identity.FullName,
|
profile.FullName,
|
||||||
resetPasswordUrl,
|
resetPasswordUrl,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -283,7 +300,7 @@ func (s AuthService) SendPasswordResetInstructionByEmail(
|
|||||||
}
|
}
|
||||||
|
|
||||||
passwordResetEmail := coredata.NewEmail(
|
passwordResetEmail := coredata.NewEmail(
|
||||||
identity.FullName,
|
profile.FullName,
|
||||||
identity.EmailAddress,
|
identity.EmailAddress,
|
||||||
subject,
|
subject,
|
||||||
textBody,
|
textBody,
|
||||||
@@ -325,6 +342,13 @@ func (s AuthService) CreateIdentityWithPassword(
|
|||||||
EmailAddress: req.Email,
|
EmailAddress: req.Email,
|
||||||
HashedPassword: hashedPassword,
|
HashedPassword: hashedPassword,
|
||||||
EmailAddressVerified: false,
|
EmailAddressVerified: false,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultProfile = &coredata.IdentityProfile{
|
||||||
|
ID: gid.New(gid.NilTenant, coredata.IdentityProfileEntityType),
|
||||||
|
IdentityID: identity.ID,
|
||||||
FullName: req.FullName,
|
FullName: req.FullName,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
@@ -358,7 +382,7 @@ func (s AuthService) CreateIdentityWithPassword(
|
|||||||
|
|
||||||
subject, textBody, htmlBody, err := emails.RenderConfirmEmail(
|
subject, textBody, htmlBody, err := emails.RenderConfirmEmail(
|
||||||
s.baseURL,
|
s.baseURL,
|
||||||
identity.FullName,
|
req.FullName,
|
||||||
confirmationUrl,
|
confirmationUrl,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -366,7 +390,7 @@ func (s AuthService) CreateIdentityWithPassword(
|
|||||||
}
|
}
|
||||||
|
|
||||||
confirmationEmail := coredata.NewEmail(
|
confirmationEmail := coredata.NewEmail(
|
||||||
identity.FullName,
|
req.FullName,
|
||||||
identity.EmailAddress,
|
identity.EmailAddress,
|
||||||
subject,
|
subject,
|
||||||
textBody,
|
textBody,
|
||||||
@@ -385,6 +409,11 @@ func (s AuthService) CreateIdentityWithPassword(
|
|||||||
return fmt.Errorf("cannot insert identity: %w", err)
|
return fmt.Errorf("cannot insert identity: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = defaultProfile.Insert(ctx, tx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot insert default profile: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := confirmationEmail.Insert(ctx, tx); err != nil {
|
if err := confirmationEmail.Insert(ctx, tx); err != nil {
|
||||||
return fmt.Errorf("cannot insert email: %w", err)
|
return fmt.Errorf("cannot insert email: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,6 +200,16 @@ func (e ErrPersonalAPIKeyNotFound) Error() string {
|
|||||||
return fmt.Sprintf("personal API key %q not found", e.PersonalAPIKeyID)
|
return fmt.Sprintf("personal API key %q not found", e.PersonalAPIKeyID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ErrProfileNotFound struct{ MembershipID gid.GID }
|
||||||
|
|
||||||
|
func NewProfileNotFoundError(membershipID gid.GID) error {
|
||||||
|
return &ErrProfileNotFound{MembershipID: membershipID}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrProfileNotFound) Error() string {
|
||||||
|
return fmt.Sprintf("profile for membership %q not found", e.MembershipID)
|
||||||
|
}
|
||||||
|
|
||||||
type ErrPersonalAPIKeyExpired struct{ PersonalAPIKeyID gid.GID }
|
type ErrPersonalAPIKeyExpired struct{ PersonalAPIKeyID gid.GID }
|
||||||
|
|
||||||
func NewPersonalAPIKeyExpiredError(personalAPIKeyID gid.GID) error {
|
func NewPersonalAPIKeyExpiredError(personalAPIKeyID gid.GID) error {
|
||||||
|
|||||||
@@ -260,7 +260,6 @@ func (s *Service) HandleAssertion(
|
|||||||
EmailAddress: email,
|
EmailAddress: email,
|
||||||
HashedPassword: nil,
|
HashedPassword: nil,
|
||||||
EmailAddressVerified: true,
|
EmailAddressVerified: true,
|
||||||
FullName: fullname,
|
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
@@ -269,11 +268,23 @@ func (s *Service) HandleAssertion(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot insert identity: %w", err)
|
return fmt.Errorf("cannot insert identity: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defaultProfile := &coredata.IdentityProfile{
|
||||||
|
ID: gid.New(gid.NilTenant, coredata.IdentityProfileEntityType),
|
||||||
|
IdentityID: identity.ID,
|
||||||
|
FullName: fullname,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = defaultProfile.Insert(ctx, tx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot insert default profile: %w", err)
|
||||||
|
}
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return fmt.Errorf("cannot load identity: %w", err)
|
return fmt.Errorf("cannot load identity: %w", err)
|
||||||
} else {
|
} else {
|
||||||
identity.SAMLSubject = &assertion.Subject.NameID.Value
|
identity.SAMLSubject = &assertion.Subject.NameID.Value
|
||||||
identity.FullName = fullname
|
|
||||||
identity.EmailAddress = email
|
identity.EmailAddress = email
|
||||||
identity.EmailAddressVerified = true
|
identity.EmailAddressVerified = true
|
||||||
identity.UpdatedAt = now
|
identity.UpdatedAt = now
|
||||||
@@ -304,6 +315,20 @@ func (s *Service) HandleAssertion(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot insert membership: %w", err)
|
return fmt.Errorf("cannot insert membership: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
membershipProfile := &coredata.IdentityProfile{
|
||||||
|
ID: gid.New(membership.ID.TenantID(), coredata.IdentityProfileEntityType),
|
||||||
|
IdentityID: identity.ID,
|
||||||
|
MembershipID: &membership.ID,
|
||||||
|
FullName: fullname,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = membershipProfile.Insert(ctx, tx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot insert membership profile: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if role != nil {
|
if role != nil {
|
||||||
@@ -316,6 +341,19 @@ func (s *Service) HandleAssertion(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
memberProfile := &coredata.IdentityProfile{}
|
||||||
|
err = memberProfile.LoadByMembershipID(ctx, tx, coredata.NewNoScope(), membership.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot load membership profile: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
memberProfile.FullName = fullname
|
||||||
|
memberProfile.UpdatedAt = now
|
||||||
|
err = memberProfile.Update(ctx, tx, coredata.NewNoScope())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot update membership profile: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -138,6 +138,8 @@ type Identity implements Node {
|
|||||||
createdAt: Datetime!
|
createdAt: Datetime!
|
||||||
updatedAt: Datetime!
|
updatedAt: Datetime!
|
||||||
|
|
||||||
|
defaultProfile: IdentityProfile @goField(forceResolver: true) @isViewer
|
||||||
|
|
||||||
memberships(
|
memberships(
|
||||||
first: Int
|
first: Int
|
||||||
after: CursorKey
|
after: CursorKey
|
||||||
@@ -168,27 +170,12 @@ type Identity implements Node {
|
|||||||
last: Int
|
last: Int
|
||||||
before: CursorKey
|
before: CursorKey
|
||||||
): PersonalAPIKeyConnection @goField(forceResolver: true) @isViewer
|
): PersonalAPIKeyConnection @goField(forceResolver: true) @isViewer
|
||||||
|
|
||||||
profileFor(organizationId: ID!): IdentityProfile @isViewer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type IdentityProfile implements Node {
|
type IdentityProfile implements Node {
|
||||||
id: ID!
|
id: ID!
|
||||||
displayName: String!
|
fullName: String!
|
||||||
firstName: String
|
|
||||||
lastName: String
|
|
||||||
jobTitle: String
|
|
||||||
department: String
|
|
||||||
phoneNumber: String
|
|
||||||
avatarUrl: String
|
|
||||||
manager: IdentityProfile
|
|
||||||
timezone: String
|
|
||||||
locale: String
|
|
||||||
customAttributes: [CustomAttribute!]!
|
|
||||||
provisionedBy: ProvisioningSource!
|
|
||||||
externalId: String
|
|
||||||
identity: Identity!
|
identity: Identity!
|
||||||
organization: Organization!
|
|
||||||
createdAt: Datetime!
|
createdAt: Datetime!
|
||||||
updatedAt: Datetime!
|
updatedAt: Datetime!
|
||||||
}
|
}
|
||||||
@@ -244,8 +231,8 @@ enum MembershipRole
|
|||||||
type Membership implements Node {
|
type Membership implements Node {
|
||||||
id: ID!
|
id: ID!
|
||||||
createdAt: Datetime!
|
createdAt: Datetime!
|
||||||
profile: IdentityProfile!
|
|
||||||
identity: Identity @goField(forceResolver: true)
|
identity: Identity @goField(forceResolver: true)
|
||||||
|
profile: IdentityProfile @goField(forceResolver: true) @isViewer
|
||||||
organization: Organization @goField(forceResolver: true)
|
organization: Organization @goField(forceResolver: true)
|
||||||
role: MembershipRole!
|
role: MembershipRole!
|
||||||
permissions: [Permission!]!
|
permissions: [Permission!]!
|
||||||
@@ -382,12 +369,6 @@ enum SAMLEnforcementPolicy
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AuthMethod {
|
|
||||||
PASSWORD
|
|
||||||
SAML
|
|
||||||
RECOVERY_CODE
|
|
||||||
}
|
|
||||||
|
|
||||||
enum TokenScope {
|
enum TokenScope {
|
||||||
READ_ORGANIZATION
|
READ_ORGANIZATION
|
||||||
WRITE_ORGANIZATION
|
WRITE_ORGANIZATION
|
||||||
@@ -578,14 +559,7 @@ input AssumeOrganizationSessionInput {
|
|||||||
|
|
||||||
input UpdateIdentityProfileInput {
|
input UpdateIdentityProfileInput {
|
||||||
membershipId: ID!
|
membershipId: ID!
|
||||||
displayName: String
|
fullName: String
|
||||||
firstName: String
|
|
||||||
lastName: String
|
|
||||||
jobTitle: String
|
|
||||||
department: String
|
|
||||||
phoneNumber: String
|
|
||||||
timezone: String
|
|
||||||
locale: String
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input RevokeSessionInput {
|
input RevokeSessionInput {
|
||||||
@@ -750,10 +724,6 @@ type DeactivateAccountPayload {
|
|||||||
success: Boolean!
|
success: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
type DeleteAccountPayload {
|
|
||||||
success: Boolean!
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdateIdentityProfilePayload {
|
type UpdateIdentityProfilePayload {
|
||||||
profile: IdentityProfile
|
profile: IdentityProfile
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
26
pkg/server/api/connect/v1/types/identity_profile.go
Normal file
26
pkg/server/api/connect/v1/types/identity_profile.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// 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 types
|
||||||
|
|
||||||
|
import "go.probo.inc/probo/pkg/coredata"
|
||||||
|
|
||||||
|
func NewIdentityProfile(profile *coredata.IdentityProfile) *IdentityProfile {
|
||||||
|
return &IdentityProfile{
|
||||||
|
ID: profile.ID,
|
||||||
|
FullName: profile.FullName,
|
||||||
|
CreatedAt: profile.CreatedAt,
|
||||||
|
UpdatedAt: profile.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -71,5 +71,8 @@ func NewInvitation(invitation *coredata.Invitation) *Invitation {
|
|||||||
AcceptedAt: invitation.AcceptedAt,
|
AcceptedAt: invitation.AcceptedAt,
|
||||||
CreatedAt: invitation.CreatedAt,
|
CreatedAt: invitation.CreatedAt,
|
||||||
Status: invitation.Status,
|
Status: invitation.Status,
|
||||||
|
Organization: &Organization{
|
||||||
|
ID: invitation.OrganizationID,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,8 +62,13 @@ func NewMembershipEdge(membership *coredata.Membership, orderField coredata.Memb
|
|||||||
func NewMembership(membership *coredata.Membership) *Membership {
|
func NewMembership(membership *coredata.Membership) *Membership {
|
||||||
return &Membership{
|
return &Membership{
|
||||||
ID: membership.ID,
|
ID: membership.ID,
|
||||||
IdentityID: membership.IdentityID,
|
|
||||||
CreatedAt: membership.CreatedAt,
|
CreatedAt: membership.CreatedAt,
|
||||||
|
Identity: &Identity{
|
||||||
|
ID: membership.IdentityID,
|
||||||
|
},
|
||||||
|
Organization: &Organization{
|
||||||
|
ID: membership.OrganizationID,
|
||||||
|
},
|
||||||
// Permissions: membership.Permissions,
|
// Permissions: membership.Permissions,
|
||||||
// ProvisionedBy: membership.ProvisionedBy,
|
// ProvisionedBy: membership.ProvisionedBy,
|
||||||
// Active: membership.Active,
|
// Active: membership.Active,
|
||||||
|
|||||||
@@ -62,8 +62,10 @@ func NewSessionEdge(session *coredata.Session, orderField coredata.SessionOrderF
|
|||||||
func NewSession(session *coredata.Session) *Session {
|
func NewSession(session *coredata.Session) *Session {
|
||||||
return &Session{
|
return &Session{
|
||||||
ID: session.ID,
|
ID: session.ID,
|
||||||
|
Identity: &Identity{
|
||||||
|
ID: session.IdentityID,
|
||||||
|
},
|
||||||
IPAddress: session.IPAddress.String(),
|
IPAddress: session.IPAddress.String(),
|
||||||
IdentityID: session.IdentityID,
|
|
||||||
UserAgent: session.UserAgent,
|
UserAgent: session.UserAgent,
|
||||||
UpdatedAt: session.UpdatedAt,
|
UpdatedAt: session.UpdatedAt,
|
||||||
CreatedAt: session.CreatedAt,
|
CreatedAt: session.CreatedAt,
|
||||||
|
|||||||
@@ -33,12 +33,6 @@ type AcceptInvitationPayload struct {
|
|||||||
MembershipEdge *MembershipEdge `json:"membershipEdge"`
|
MembershipEdge *MembershipEdge `json:"membershipEdge"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AddIPAllowlistEntryInput struct {
|
|
||||||
OrganizationID gid.GID `json:"organizationId"`
|
|
||||||
Cidr string `json:"cidr"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Application struct {
|
type Application struct {
|
||||||
ID ApplicationID `json:"id"`
|
ID ApplicationID `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -108,28 +102,10 @@ type CreateSAMLConfigurationPayload struct {
|
|||||||
SamlConfigurationEdge *SAMLConfigurationEdge `json:"samlConfigurationEdge"`
|
SamlConfigurationEdge *SAMLConfigurationEdge `json:"samlConfigurationEdge"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CustomAttribute struct {
|
|
||||||
Key string `json:"key"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DeactivateAccountInput struct {
|
|
||||||
Password string `json:"password"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DeactivateAccountPayload struct {
|
type DeactivateAccountPayload struct {
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DeleteAccountInput struct {
|
|
||||||
Password string `json:"password"`
|
|
||||||
Confirmation string `json:"confirmation"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DeleteAccountPayload struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DeleteInvitationInput struct {
|
type DeleteInvitationInput struct {
|
||||||
OrganizationID gid.GID `json:"organizationId"`
|
OrganizationID gid.GID `json:"organizationId"`
|
||||||
InvitationID gid.GID `json:"invitationId"`
|
InvitationID gid.GID `json:"invitationId"`
|
||||||
@@ -178,11 +154,11 @@ type Identity struct {
|
|||||||
EmailVerified bool `json:"emailVerified"`
|
EmailVerified bool `json:"emailVerified"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
DefaultProfile *IdentityProfile `json:"defaultProfile,omitempty"`
|
||||||
Memberships *MembershipConnection `json:"memberships,omitempty"`
|
Memberships *MembershipConnection `json:"memberships,omitempty"`
|
||||||
PendingInvitations *InvitationConnection `json:"pendingInvitations,omitempty"`
|
PendingInvitations *InvitationConnection `json:"pendingInvitations,omitempty"`
|
||||||
Sessions *SessionConnection `json:"sessions,omitempty"`
|
Sessions *SessionConnection `json:"sessions,omitempty"`
|
||||||
PersonalAPIKeys *PersonalAPIKeyConnection `json:"personalAPIKeys,omitempty"`
|
PersonalAPIKeys *PersonalAPIKeyConnection `json:"personalAPIKeys,omitempty"`
|
||||||
ProfileFor *IdentityProfile `json:"profileFor,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Identity) IsNode() {}
|
func (Identity) IsNode() {}
|
||||||
@@ -190,21 +166,8 @@ func (this Identity) GetID() gid.GID { return this.ID }
|
|||||||
|
|
||||||
type IdentityProfile struct {
|
type IdentityProfile struct {
|
||||||
ID gid.GID `json:"id"`
|
ID gid.GID `json:"id"`
|
||||||
DisplayName string `json:"displayName"`
|
FullName string `json:"fullName"`
|
||||||
FirstName *string `json:"firstName,omitempty"`
|
|
||||||
LastName *string `json:"lastName,omitempty"`
|
|
||||||
JobTitle *string `json:"jobTitle,omitempty"`
|
|
||||||
Department *string `json:"department,omitempty"`
|
|
||||||
PhoneNumber *string `json:"phoneNumber,omitempty"`
|
|
||||||
AvatarURL *string `json:"avatarUrl,omitempty"`
|
|
||||||
Manager *IdentityProfile `json:"manager,omitempty"`
|
|
||||||
Timezone *string `json:"timezone,omitempty"`
|
|
||||||
Locale *string `json:"locale,omitempty"`
|
|
||||||
CustomAttributes []*CustomAttribute `json:"customAttributes"`
|
|
||||||
ProvisionedBy ProvisioningSource `json:"provisionedBy"`
|
|
||||||
ExternalID *string `json:"externalId,omitempty"`
|
|
||||||
Identity *Identity `json:"identity"`
|
Identity *Identity `json:"identity"`
|
||||||
Organization *Organization `json:"organization"`
|
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
}
|
}
|
||||||
@@ -243,16 +206,12 @@ type InviteMemberPayload struct {
|
|||||||
|
|
||||||
type Membership struct {
|
type Membership struct {
|
||||||
ID gid.GID `json:"id"`
|
ID gid.GID `json:"id"`
|
||||||
IdentityID gid.GID `json:"identityId"`
|
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
Profile *IdentityProfile `json:"profile"`
|
|
||||||
Identity *Identity `json:"identity,omitempty"`
|
Identity *Identity `json:"identity,omitempty"`
|
||||||
|
Profile *IdentityProfile `json:"profile,omitempty"`
|
||||||
Organization *Organization `json:"organization,omitempty"`
|
Organization *Organization `json:"organization,omitempty"`
|
||||||
Role coredata.MembershipRole `json:"role"`
|
Role coredata.MembershipRole `json:"role"`
|
||||||
Permissions []*Permission `json:"permissions"`
|
Permissions []*Permission `json:"permissions"`
|
||||||
ProvisionedBy ProvisioningSource `json:"provisionedBy"`
|
|
||||||
Active bool `json:"active"`
|
|
||||||
LastSyncedAt *time.Time `json:"lastSyncedAt,omitempty"`
|
|
||||||
LastSession *Session `json:"lastSession,omitempty"`
|
LastSession *Session `json:"lastSession,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,10 +305,6 @@ type PersonalAPIKeyEdge struct {
|
|||||||
type Query struct {
|
type Query struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type RemoveIPAllowlistEntryInput struct {
|
|
||||||
EntryID gid.GID `json:"entryId"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RemoveMemberInput struct {
|
type RemoveMemberInput struct {
|
||||||
OrganizationID gid.GID `json:"organizationId"`
|
OrganizationID gid.GID `json:"organizationId"`
|
||||||
MembershipID gid.GID `json:"membershipId"`
|
MembershipID gid.GID `json:"membershipId"`
|
||||||
@@ -443,7 +398,7 @@ type SSOAvailability struct {
|
|||||||
|
|
||||||
type Session struct {
|
type Session struct {
|
||||||
ID gid.GID `json:"id"`
|
ID gid.GID `json:"id"`
|
||||||
IdentityID gid.GID `json:"identityId"`
|
Identity *Identity `json:"identity,omitempty"`
|
||||||
IPAddress string `json:"ipAddress"`
|
IPAddress string `json:"ipAddress"`
|
||||||
UserAgent string `json:"userAgent"`
|
UserAgent string `json:"userAgent"`
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
@@ -464,20 +419,6 @@ type SessionOrder struct {
|
|||||||
Field coredata.SessionOrderField `json:"field"`
|
Field coredata.SessionOrderField `json:"field"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SessionPolicy struct {
|
|
||||||
MaxSessionDurationHours int `json:"maxSessionDurationHours"`
|
|
||||||
IdleTimeoutMinutes int `json:"idleTimeoutMinutes"`
|
|
||||||
MaxConcurrentSessions *int `json:"maxConcurrentSessions,omitempty"`
|
|
||||||
RequireReauthForSensitiveActions bool `json:"requireReauthForSensitiveActions"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SessionPolicyInput struct {
|
|
||||||
MaxSessionDurationHours *int `json:"maxSessionDurationHours,omitempty"`
|
|
||||||
IdleTimeoutMinutes *int `json:"idleTimeoutMinutes,omitempty"`
|
|
||||||
MaxConcurrentSessions *int `json:"maxConcurrentSessions,omitempty"`
|
|
||||||
RequireReauthForSensitiveActions *bool `json:"requireReauthForSensitiveActions,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SignInInput struct {
|
type SignInInput struct {
|
||||||
Email mail.Addr `json:"email"`
|
Email mail.Addr `json:"email"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
@@ -513,14 +454,7 @@ type SignUpPayload struct {
|
|||||||
|
|
||||||
type UpdateIdentityProfileInput struct {
|
type UpdateIdentityProfileInput struct {
|
||||||
MembershipID gid.GID `json:"membershipId"`
|
MembershipID gid.GID `json:"membershipId"`
|
||||||
DisplayName *string `json:"displayName,omitempty"`
|
FullName *string `json:"fullName,omitempty"`
|
||||||
FirstName *string `json:"firstName,omitempty"`
|
|
||||||
LastName *string `json:"lastName,omitempty"`
|
|
||||||
JobTitle *string `json:"jobTitle,omitempty"`
|
|
||||||
Department *string `json:"department,omitempty"`
|
|
||||||
PhoneNumber *string `json:"phoneNumber,omitempty"`
|
|
||||||
Timezone *string `json:"timezone,omitempty"`
|
|
||||||
Locale *string `json:"locale,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateIdentityProfilePayload struct {
|
type UpdateIdentityProfilePayload struct {
|
||||||
@@ -699,63 +633,6 @@ func (e ApplicationID) MarshalJSON() ([]byte, error) {
|
|||||||
return buf.Bytes(), nil
|
return buf.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthMethod string
|
|
||||||
|
|
||||||
const (
|
|
||||||
AuthMethodPassword AuthMethod = "PASSWORD"
|
|
||||||
AuthMethodSaml AuthMethod = "SAML"
|
|
||||||
AuthMethodRecoveryCode AuthMethod = "RECOVERY_CODE"
|
|
||||||
)
|
|
||||||
|
|
||||||
var AllAuthMethod = []AuthMethod{
|
|
||||||
AuthMethodPassword,
|
|
||||||
AuthMethodSaml,
|
|
||||||
AuthMethodRecoveryCode,
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e AuthMethod) IsValid() bool {
|
|
||||||
switch e {
|
|
||||||
case AuthMethodPassword, AuthMethodSaml, AuthMethodRecoveryCode:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e AuthMethod) String() string {
|
|
||||||
return string(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *AuthMethod) UnmarshalGQL(v any) error {
|
|
||||||
str, ok := v.(string)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("enums must be strings")
|
|
||||||
}
|
|
||||||
|
|
||||||
*e = AuthMethod(str)
|
|
||||||
if !e.IsValid() {
|
|
||||||
return fmt.Errorf("%s is not a valid AuthMethod", str)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e AuthMethod) MarshalGQL(w io.Writer) {
|
|
||||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *AuthMethod) UnmarshalJSON(b []byte) error {
|
|
||||||
s, err := strconv.Unquote(string(b))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return e.UnmarshalGQL(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e AuthMethod) MarshalJSON() ([]byte, error) {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
e.MarshalGQL(&buf)
|
|
||||||
return buf.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type PrincipalType string
|
type PrincipalType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -811,63 +688,6 @@ func (e PrincipalType) MarshalJSON() ([]byte, error) {
|
|||||||
return buf.Bytes(), nil
|
return buf.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProvisioningSource string
|
|
||||||
|
|
||||||
const (
|
|
||||||
ProvisioningSourceManual ProvisioningSource = "MANUAL"
|
|
||||||
ProvisioningSourceInvitation ProvisioningSource = "INVITATION"
|
|
||||||
ProvisioningSourceSaml ProvisioningSource = "SAML"
|
|
||||||
)
|
|
||||||
|
|
||||||
var AllProvisioningSource = []ProvisioningSource{
|
|
||||||
ProvisioningSourceManual,
|
|
||||||
ProvisioningSourceInvitation,
|
|
||||||
ProvisioningSourceSaml,
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e ProvisioningSource) IsValid() bool {
|
|
||||||
switch e {
|
|
||||||
case ProvisioningSourceManual, ProvisioningSourceInvitation, ProvisioningSourceSaml:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e ProvisioningSource) String() string {
|
|
||||||
return string(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ProvisioningSource) UnmarshalGQL(v any) error {
|
|
||||||
str, ok := v.(string)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("enums must be strings")
|
|
||||||
}
|
|
||||||
|
|
||||||
*e = ProvisioningSource(str)
|
|
||||||
if !e.IsValid() {
|
|
||||||
return fmt.Errorf("%s is not a valid ProvisioningSource", str)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e ProvisioningSource) MarshalGQL(w io.Writer) {
|
|
||||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ProvisioningSource) UnmarshalJSON(b []byte) error {
|
|
||||||
s, err := strconv.Unquote(string(b))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return e.UnmarshalGQL(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e ProvisioningSource) MarshalJSON() ([]byte, error) {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
e.MarshalGQL(&buf)
|
|
||||||
return buf.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReauthenticationReason string
|
type ReauthenticationReason string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -24,6 +24,17 @@ import (
|
|||||||
"go.probo.inc/probo/pkg/server/gqlutils/types/cursor"
|
"go.probo.inc/probo/pkg/server/gqlutils/types/cursor"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DefaultProfile is the resolver for the defaultProfile field.
|
||||||
|
func (r *identityResolver) DefaultProfile(ctx context.Context, obj *types.Identity) (*types.IdentityProfile, error) {
|
||||||
|
profile, err := r.iam.AccountService.GetDefaultProfile(ctx, obj.ID)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.ErrorCtx(ctx, "cannot get default profile", log.Error(err))
|
||||||
|
return nil, gqlutils.InternalServerError(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.NewIdentityProfile(profile), nil
|
||||||
|
}
|
||||||
|
|
||||||
// Memberships is the resolver for the memberships field.
|
// Memberships is the resolver for the memberships field.
|
||||||
func (r *identityResolver) Memberships(ctx context.Context, obj *types.Identity, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.MembershipOrderBy) (*types.MembershipConnection, error) {
|
func (r *identityResolver) Memberships(ctx context.Context, obj *types.Identity, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.MembershipOrderBy) (*types.MembershipConnection, error) {
|
||||||
pageOrderBy := page.OrderBy[coredata.MembershipOrderField]{
|
pageOrderBy := page.OrderBy[coredata.MembershipOrderField]{
|
||||||
@@ -41,7 +52,8 @@ func (r *identityResolver) Memberships(ctx context.Context, obj *types.Identity,
|
|||||||
|
|
||||||
page, err := r.iam.AccountService.ListMemberships(ctx, obj.ID, cursor)
|
page, err := r.iam.AccountService.ListMemberships(ctx, obj.ID, cursor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Errorf("cannot list memberships: %w", err))
|
r.logger.ErrorCtx(ctx, "cannot list memberships", log.Error(err))
|
||||||
|
return nil, gqlutils.InternalServerError(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
return types.NewMembershipConnection(page, r, obj.ID), nil
|
return types.NewMembershipConnection(page, r, obj.ID), nil
|
||||||
@@ -58,7 +70,8 @@ func (r *identityResolver) PendingInvitations(ctx context.Context, obj *types.Id
|
|||||||
|
|
||||||
page, err := r.iam.AccountService.ListPendingInvitations(ctx, obj.ID, cursor)
|
page, err := r.iam.AccountService.ListPendingInvitations(ctx, obj.ID, cursor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Errorf("cannot list pending invitations: %w", err))
|
r.logger.ErrorCtx(ctx, "cannot list pending invitations", log.Error(err))
|
||||||
|
return nil, gqlutils.InternalServerError(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
return types.NewInvitationConnection(page, r, obj.ID, nil), nil
|
return types.NewInvitationConnection(page, r, obj.ID, nil), nil
|
||||||
@@ -81,7 +94,8 @@ func (r *identityResolver) Sessions(ctx context.Context, obj *types.Identity, fi
|
|||||||
|
|
||||||
page, err := r.iam.AccountService.ListSessions(ctx, obj.ID, cursor)
|
page, err := r.iam.AccountService.ListSessions(ctx, obj.ID, cursor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Errorf("cannot list sessions: %w", err))
|
r.logger.ErrorCtx(ctx, "cannot list sessions", log.Error(err))
|
||||||
|
return nil, gqlutils.InternalServerError(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
return types.NewSessionConnection(page, r, obj.ID), nil
|
return types.NewSessionConnection(page, r, obj.ID), nil
|
||||||
@@ -98,7 +112,8 @@ func (r *identityResolver) PersonalAPIKeys(ctx context.Context, obj *types.Ident
|
|||||||
|
|
||||||
page, err := r.iam.AccountService.ListPersonalAPIKeys(ctx, obj.ID, cursor)
|
page, err := r.iam.AccountService.ListPersonalAPIKeys(ctx, obj.ID, cursor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Errorf("cannot list personal api keys: %w", err))
|
r.logger.ErrorCtx(ctx, "cannot list personal api keys", log.Error(err))
|
||||||
|
return nil, gqlutils.InternalServerError(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
return types.NewPersonalAPIKeyConnection(page, r, obj.ID), nil
|
return types.NewPersonalAPIKeyConnection(page, r, obj.ID), nil
|
||||||
@@ -108,7 +123,8 @@ func (r *identityResolver) PersonalAPIKeys(ctx context.Context, obj *types.Ident
|
|||||||
func (r *invitationResolver) Organization(ctx context.Context, obj *types.Invitation) (*types.Organization, error) {
|
func (r *invitationResolver) Organization(ctx context.Context, obj *types.Invitation) (*types.Organization, error) {
|
||||||
organization, err := r.iam.OrganizationService.GetOrganizationForInvitation(ctx, obj.ID)
|
organization, err := r.iam.OrganizationService.GetOrganizationForInvitation(ctx, obj.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Errorf("cannot get organization for invitation: %w", err))
|
r.logger.ErrorCtx(ctx, "cannot get organization for invitation", log.Error(err))
|
||||||
|
return nil, gqlutils.InternalServerError(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
return types.NewOrganization(organization), nil
|
return types.NewOrganization(organization), nil
|
||||||
@@ -120,31 +136,51 @@ func (r *invitationConnectionResolver) TotalCount(ctx context.Context, obj *type
|
|||||||
case *organizationResolver:
|
case *organizationResolver:
|
||||||
count, err := r.iam.OrganizationService.CountInvitations(ctx, obj.ParentID, obj.Filters)
|
count, err := r.iam.OrganizationService.CountInvitations(ctx, obj.ParentID, obj.Filters)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Errorf("cannot count invitations: %w", err))
|
r.logger.ErrorCtx(ctx, "cannot count invitations", log.Error(err))
|
||||||
|
return nil, gqlutils.InternalServerError(ctx)
|
||||||
}
|
}
|
||||||
return &count, nil
|
return &count, nil
|
||||||
case *identityResolver:
|
case *identityResolver:
|
||||||
count, err := r.iam.AccountService.CountPendingInvitations(ctx, obj.ParentID)
|
count, err := r.iam.AccountService.CountPendingInvitations(ctx, obj.ParentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Errorf("cannot count invitations: %w", err))
|
r.logger.ErrorCtx(ctx, "cannot count invitations", log.Error(err))
|
||||||
|
return nil, gqlutils.InternalServerError(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &count, nil
|
return &count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver))
|
r.logger.ErrorCtx(ctx, "unsupported resolver", log.Any("resolver", obj.Resolver))
|
||||||
|
return nil, gqlutils.InternalServerError(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Identity is the resolver for the identity field.
|
// Identity is the resolver for the identity field.
|
||||||
func (r *membershipResolver) Identity(ctx context.Context, obj *types.Membership) (*types.Identity, error) {
|
func (r *membershipResolver) Identity(ctx context.Context, obj *types.Membership) (*types.Identity, error) {
|
||||||
identity, err := r.iam.AccountService.GetIdentityForMembership(ctx, obj.ID)
|
identity, err := r.iam.AccountService.GetIdentityForMembership(ctx, obj.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Errorf("cannot get identity: %w", err))
|
r.logger.ErrorCtx(ctx, "cannot get identity for membership", log.Error(err))
|
||||||
|
return nil, gqlutils.InternalServerError(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
return types.NewIdentity(identity), nil
|
return types.NewIdentity(identity), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Profile is the resolver for the profile field.
|
||||||
|
func (r *membershipResolver) Profile(ctx context.Context, obj *types.Membership) (*types.IdentityProfile, error) {
|
||||||
|
profile, err := r.iam.AccountService.GetProfileForMembership(ctx, obj.ID)
|
||||||
|
if err != nil {
|
||||||
|
var errProfileNotFound *iam.ErrProfileNotFound
|
||||||
|
if errors.As(err, &errProfileNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.ErrorCtx(ctx, "cannot get profile for membership", log.Error(err))
|
||||||
|
return nil, gqlutils.InternalServerError(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.NewIdentityProfile(profile), nil
|
||||||
|
}
|
||||||
|
|
||||||
// Organization is the resolver for the organization field.
|
// Organization is the resolver for the organization field.
|
||||||
func (r *membershipResolver) Organization(ctx context.Context, obj *types.Membership) (*types.Organization, error) {
|
func (r *membershipResolver) Organization(ctx context.Context, obj *types.Membership) (*types.Organization, error) {
|
||||||
organization, err := r.iam.OrganizationService.GetOrganizationForMembership(ctx, obj.ID)
|
organization, err := r.iam.OrganizationService.GetOrganizationForMembership(ctx, obj.ID)
|
||||||
@@ -517,7 +553,29 @@ func (r *mutationResolver) AssumeOrganizationSession(ctx context.Context, input
|
|||||||
|
|
||||||
// UpdateIdentityProfile is the resolver for the updateIdentityProfile field.
|
// UpdateIdentityProfile is the resolver for the updateIdentityProfile field.
|
||||||
func (r *mutationResolver) UpdateIdentityProfile(ctx context.Context, input types.UpdateIdentityProfileInput) (*types.UpdateIdentityProfilePayload, error) {
|
func (r *mutationResolver) UpdateIdentityProfile(ctx context.Context, input types.UpdateIdentityProfileInput) (*types.UpdateIdentityProfilePayload, error) {
|
||||||
panic(fmt.Errorf("not implemented: UpdateIdentityProfile - updateIdentityProfile"))
|
identity := IdentityFromContext(ctx)
|
||||||
|
|
||||||
|
profile, err := r.iam.AccountService.UpdateIdentityProfile(
|
||||||
|
ctx,
|
||||||
|
identity.ID,
|
||||||
|
&iam.UpdateIdentityProfileRequest{
|
||||||
|
MembershipID: input.MembershipID,
|
||||||
|
FullName: input.FullName,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
var errMembershipNotFound *iam.ErrMembershipNotFound
|
||||||
|
if errors.As(err, &errMembershipNotFound) {
|
||||||
|
return nil, gqlutils.NotFound(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.ErrorCtx(ctx, "cannot update identity profile", log.Error(err))
|
||||||
|
return nil, gqlutils.InternalServerError(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.UpdateIdentityProfilePayload{
|
||||||
|
Profile: types.NewIdentityProfile(profile),
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RevokeSession is the resolver for the revokeSession field.
|
// RevokeSession is the resolver for the revokeSession field.
|
||||||
@@ -801,7 +859,8 @@ func (r *mutationResolver) CreateSAMLConfiguration(ctx context.Context, input ty
|
|||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Errorf("cannot create saml configuration: %w", err))
|
r.logger.ErrorCtx(ctx, "cannot create saml configuration", log.Error(err))
|
||||||
|
return nil, gqlutils.InternalServerError(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &types.CreateSAMLConfigurationPayload{
|
return &types.CreateSAMLConfigurationPayload{
|
||||||
@@ -1095,6 +1154,17 @@ func (r *sAMLConfigurationConnectionResolver) TotalCount(ctx context.Context, ob
|
|||||||
return nil, gqlutils.InternalServerError(ctx)
|
return nil, gqlutils.InternalServerError(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Identity is the resolver for the identity field.
|
||||||
|
func (r *sessionResolver) Identity(ctx context.Context, obj *types.Session) (*types.Identity, error) {
|
||||||
|
identity, err := r.iam.AccountService.GetIdentity(ctx, obj.Identity.ID)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.ErrorCtx(ctx, "cannot get identity for session", log.Error(err))
|
||||||
|
return nil, gqlutils.InternalServerError(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.NewIdentity(identity), nil
|
||||||
|
}
|
||||||
|
|
||||||
// TotalCount is the resolver for the totalCount field.
|
// TotalCount is the resolver for the totalCount field.
|
||||||
func (r *sessionConnectionResolver) TotalCount(ctx context.Context, obj *types.SessionConnection) (*int, error) {
|
func (r *sessionConnectionResolver) TotalCount(ctx context.Context, obj *types.SessionConnection) (*int, error) {
|
||||||
switch obj.Resolver.(type) {
|
switch obj.Resolver.(type) {
|
||||||
@@ -1150,6 +1220,9 @@ func (r *Resolver) SAMLConfigurationConnection() schema.SAMLConfigurationConnect
|
|||||||
return &sAMLConfigurationConnectionResolver{r}
|
return &sAMLConfigurationConnectionResolver{r}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Session returns schema.SessionResolver implementation.
|
||||||
|
func (r *Resolver) Session() schema.SessionResolver { return &sessionResolver{r} }
|
||||||
|
|
||||||
// SessionConnection returns schema.SessionConnectionResolver implementation.
|
// SessionConnection returns schema.SessionConnectionResolver implementation.
|
||||||
func (r *Resolver) SessionConnection() schema.SessionConnectionResolver {
|
func (r *Resolver) SessionConnection() schema.SessionConnectionResolver {
|
||||||
return &sessionConnectionResolver{r}
|
return &sessionConnectionResolver{r}
|
||||||
@@ -1165,4 +1238,5 @@ type organizationResolver struct{ *Resolver }
|
|||||||
type personalAPIKeyConnectionResolver struct{ *Resolver }
|
type personalAPIKeyConnectionResolver struct{ *Resolver }
|
||||||
type queryResolver struct{ *Resolver }
|
type queryResolver struct{ *Resolver }
|
||||||
type sAMLConfigurationConnectionResolver struct{ *Resolver }
|
type sAMLConfigurationConnectionResolver struct{ *Resolver }
|
||||||
|
type sessionResolver struct{ *Resolver }
|
||||||
type sessionConnectionResolver struct{ *Resolver }
|
type sessionConnectionResolver struct{ *Resolver }
|
||||||
|
|||||||
@@ -2167,14 +2167,21 @@ func (r *mutationResolver) ExportFramework(ctx context.Context, input types.Expo
|
|||||||
prb := r.ProboService(ctx, input.FrameworkID.TenantID())
|
prb := r.ProboService(ctx, input.FrameworkID.TenantID())
|
||||||
identity := connect_v1.IdentityFromContext(ctx)
|
identity := connect_v1.IdentityFromContext(ctx)
|
||||||
|
|
||||||
err, exportJobID := prb.Frameworks.RequestExport(
|
// Load default profile to get the full name
|
||||||
|
recipientName := ""
|
||||||
|
profile, err := r.iam.AccountService.GetDefaultProfile(ctx, identity.ID)
|
||||||
|
if err == nil {
|
||||||
|
recipientName = profile.FullName
|
||||||
|
}
|
||||||
|
|
||||||
|
exportErr, exportJobID := prb.Frameworks.RequestExport(
|
||||||
ctx,
|
ctx,
|
||||||
input.FrameworkID,
|
input.FrameworkID,
|
||||||
identity.EmailAddress,
|
identity.EmailAddress,
|
||||||
identity.FullName,
|
recipientName,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if exportErr != nil {
|
||||||
panic(fmt.Errorf("cannot export framework: %w", err))
|
panic(fmt.Errorf("cannot export framework: %w", exportErr))
|
||||||
}
|
}
|
||||||
|
|
||||||
return &types.ExportFrameworkPayload{
|
return &types.ExportFrameworkPayload{
|
||||||
@@ -3345,15 +3352,22 @@ func (r *mutationResolver) BulkExportDocuments(ctx context.Context, input types.
|
|||||||
|
|
||||||
identity := connect_v1.IdentityFromContext(ctx)
|
identity := connect_v1.IdentityFromContext(ctx)
|
||||||
|
|
||||||
|
// Load default profile to get the full name
|
||||||
|
recipientName := ""
|
||||||
|
profile, err := r.iam.AccountService.GetDefaultProfile(ctx, identity.ID)
|
||||||
|
if err == nil {
|
||||||
|
recipientName = profile.FullName
|
||||||
|
}
|
||||||
|
|
||||||
options := probo.ExportPDFOptions{
|
options := probo.ExportPDFOptions{
|
||||||
WithWatermark: input.WithWatermark,
|
WithWatermark: input.WithWatermark,
|
||||||
WithSignatures: input.WithSignatures,
|
WithSignatures: input.WithSignatures,
|
||||||
WatermarkEmail: input.WatermarkEmail,
|
WatermarkEmail: input.WatermarkEmail,
|
||||||
}
|
}
|
||||||
|
|
||||||
documentExport, err := prb.Documents.RequestExport(ctx, input.DocumentIds, identity.EmailAddress, identity.FullName, options)
|
documentExport, exportErr := prb.Documents.RequestExport(ctx, input.DocumentIds, identity.EmailAddress, recipientName, options)
|
||||||
if err != nil {
|
if exportErr != nil {
|
||||||
panic(fmt.Errorf("cannot request document export: %w", err))
|
panic(fmt.Errorf("cannot request document export: %w", exportErr))
|
||||||
}
|
}
|
||||||
|
|
||||||
return &types.BulkExportDocumentsPayload{
|
return &types.BulkExportDocumentsPayload{
|
||||||
|
|||||||
Reference in New Issue
Block a user