Refactor invitation system
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -45,14 +45,15 @@ const schema = z.object({
|
|||||||
|
|
||||||
type Props = PropsWithChildren & {
|
type Props = PropsWithChildren & {
|
||||||
connectionId?: string;
|
connectionId?: string;
|
||||||
|
onRefetch: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function InviteUserDialog({ children, connectionId }: Props) {
|
export function InviteUserDialog({ children, connectionId, onRefetch }: Props) {
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
const [inviteUser, isInviting] = useMutationWithToasts(inviteMutation, {
|
const [inviteUser, isInviting] = useMutationWithToasts(inviteMutation, {
|
||||||
successMessage: __("User invited successfully"),
|
successMessage: __("Invitation sent successfully"),
|
||||||
errorMessage: __("Failed to invite user"),
|
errorMessage: __("Failed to send invitation"),
|
||||||
});
|
});
|
||||||
const { register, handleSubmit, formState, reset, control } = useFormWithSchema(
|
const { register, handleSubmit, formState, reset, control } = useFormWithSchema(
|
||||||
schema,
|
schema,
|
||||||
@@ -72,9 +73,10 @@ export function InviteUserDialog({ children, connectionId }: Props) {
|
|||||||
},
|
},
|
||||||
connections: connectionId ? [connectionId] : ["SettingsPageInvitations_invitations"],
|
connections: connectionId ? [connectionId] : ["SettingsPageInvitations_invitations"],
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onCompleted: () => {
|
||||||
reset();
|
reset();
|
||||||
dialogRef.current?.close();
|
dialogRef.current?.close();
|
||||||
|
onRefetch();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ export const paginatedVendorsFragment = graphql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const vendorNodeQuery = graphql`
|
export const vendorNodeQuery = graphql`
|
||||||
query VendorGraphNodeQuery($vendorId: ID!, $organizationId: ID!) {
|
query VendorGraphNodeQuery($vendorId: ID!) {
|
||||||
node(id: $vendorId) {
|
node(id: $vendorId) {
|
||||||
... on Vendor {
|
... on Vendor {
|
||||||
id
|
id
|
||||||
@@ -166,9 +166,6 @@ export const vendorNodeQuery = graphql`
|
|||||||
viewer {
|
viewer {
|
||||||
user {
|
user {
|
||||||
id
|
id
|
||||||
people(organizationId: $organizationId) {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<fdfadfbc4f5fdecf5f70a8c92d3ded88>>
|
* @generated SignedSource<<cd9f55de7564e6b9e142c91a79bd9d9d>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -11,7 +11,6 @@
|
|||||||
import { ConcreteRequest } from 'relay-runtime';
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
import { FragmentRefs } from "relay-runtime";
|
import { FragmentRefs } from "relay-runtime";
|
||||||
export type VendorGraphNodeQuery$variables = {
|
export type VendorGraphNodeQuery$variables = {
|
||||||
organizationId: string;
|
|
||||||
vendorId: string;
|
vendorId: string;
|
||||||
};
|
};
|
||||||
export type VendorGraphNodeQuery$data = {
|
export type VendorGraphNodeQuery$data = {
|
||||||
@@ -25,9 +24,6 @@ export type VendorGraphNodeQuery$data = {
|
|||||||
readonly viewer: {
|
readonly viewer: {
|
||||||
readonly user: {
|
readonly user: {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly people: {
|
|
||||||
readonly id: string;
|
|
||||||
} | null | undefined;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -37,153 +33,132 @@ export type VendorGraphNodeQuery = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
var v0 = {
|
var v0 = [
|
||||||
"defaultValue": null,
|
{
|
||||||
"kind": "LocalArgument",
|
|
||||||
"name": "organizationId"
|
|
||||||
},
|
|
||||||
v1 = {
|
|
||||||
"defaultValue": null,
|
"defaultValue": null,
|
||||||
"kind": "LocalArgument",
|
"kind": "LocalArgument",
|
||||||
"name": "vendorId"
|
"name": "vendorId"
|
||||||
},
|
}
|
||||||
v2 = [
|
],
|
||||||
|
v1 = [
|
||||||
{
|
{
|
||||||
"kind": "Variable",
|
"kind": "Variable",
|
||||||
"name": "id",
|
"name": "id",
|
||||||
"variableName": "vendorId"
|
"variableName": "vendorId"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
v3 = {
|
v2 = {
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"kind": "ScalarField",
|
"kind": "ScalarField",
|
||||||
"name": "id",
|
"name": "id",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
v4 = {
|
v3 = {
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"kind": "ScalarField",
|
"kind": "ScalarField",
|
||||||
"name": "snapshotId",
|
"name": "snapshotId",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
v5 = {
|
v4 = {
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"kind": "ScalarField",
|
"kind": "ScalarField",
|
||||||
"name": "name",
|
"name": "name",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
v6 = {
|
v5 = {
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"kind": "ScalarField",
|
"kind": "ScalarField",
|
||||||
"name": "websiteUrl",
|
"name": "websiteUrl",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
v7 = [
|
v6 = [
|
||||||
(v3/*: any*/)
|
(v2/*: any*/)
|
||||||
],
|
],
|
||||||
v8 = {
|
v7 = {
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"concreteType": "User",
|
"concreteType": "User",
|
||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "user",
|
"name": "user",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
"selections": [
|
"selections": (v6/*: any*/),
|
||||||
(v3/*: any*/),
|
|
||||||
{
|
|
||||||
"alias": null,
|
|
||||||
"args": [
|
|
||||||
{
|
|
||||||
"kind": "Variable",
|
|
||||||
"name": "organizationId",
|
|
||||||
"variableName": "organizationId"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"concreteType": "People",
|
|
||||||
"kind": "LinkedField",
|
|
||||||
"name": "people",
|
|
||||||
"plural": false,
|
|
||||||
"selections": (v7/*: any*/),
|
|
||||||
"storageKey": null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
v9 = {
|
v8 = {
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"kind": "ScalarField",
|
"kind": "ScalarField",
|
||||||
"name": "__typename",
|
"name": "__typename",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
v10 = {
|
v9 = {
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"kind": "ScalarField",
|
"kind": "ScalarField",
|
||||||
"name": "description",
|
"name": "description",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
v11 = [
|
v10 = [
|
||||||
{
|
{
|
||||||
"kind": "Literal",
|
"kind": "Literal",
|
||||||
"name": "first",
|
"name": "first",
|
||||||
"value": 50
|
"value": 50
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
v12 = {
|
v11 = {
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"kind": "ScalarField",
|
"kind": "ScalarField",
|
||||||
"name": "validUntil",
|
"name": "validUntil",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
v13 = {
|
v12 = {
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"kind": "ScalarField",
|
"kind": "ScalarField",
|
||||||
"name": "fileName",
|
"name": "fileName",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
v14 = {
|
v13 = {
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"kind": "ScalarField",
|
"kind": "ScalarField",
|
||||||
"name": "cursor",
|
"name": "cursor",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
v15 = {
|
v14 = {
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"kind": "ScalarField",
|
"kind": "ScalarField",
|
||||||
"name": "endCursor",
|
"name": "endCursor",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
v16 = {
|
v15 = {
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"kind": "ScalarField",
|
"kind": "ScalarField",
|
||||||
"name": "hasNextPage",
|
"name": "hasNextPage",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
v17 = {
|
v16 = {
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"kind": "ScalarField",
|
"kind": "ScalarField",
|
||||||
"name": "hasPreviousPage",
|
"name": "hasPreviousPage",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
v18 = {
|
v17 = {
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"kind": "ScalarField",
|
"kind": "ScalarField",
|
||||||
"name": "startCursor",
|
"name": "startCursor",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
v19 = {
|
v18 = {
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"concreteType": "PageInfo",
|
"concreteType": "PageInfo",
|
||||||
@@ -191,14 +166,14 @@ v19 = {
|
|||||||
"name": "pageInfo",
|
"name": "pageInfo",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
"selections": [
|
"selections": [
|
||||||
|
(v14/*: any*/),
|
||||||
(v15/*: any*/),
|
(v15/*: any*/),
|
||||||
(v16/*: any*/),
|
(v16/*: any*/),
|
||||||
(v17/*: any*/),
|
(v17/*: any*/)
|
||||||
(v18/*: any*/)
|
|
||||||
],
|
],
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
v20 = {
|
v19 = {
|
||||||
"kind": "ClientExtension",
|
"kind": "ClientExtension",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
@@ -210,26 +185,26 @@ v20 = {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
v21 = [
|
v20 = [
|
||||||
"orderBy"
|
"orderBy"
|
||||||
],
|
],
|
||||||
v22 = {
|
v21 = {
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"kind": "ScalarField",
|
"kind": "ScalarField",
|
||||||
"name": "createdAt",
|
"name": "createdAt",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
v23 = {
|
v22 = {
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"kind": "ScalarField",
|
"kind": "ScalarField",
|
||||||
"name": "updatedAt",
|
"name": "updatedAt",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
v24 = [
|
v23 = [
|
||||||
(v3/*: any*/),
|
(v2/*: any*/),
|
||||||
(v13/*: any*/),
|
(v12/*: any*/),
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
@@ -244,22 +219,19 @@ v24 = [
|
|||||||
"name": "validFrom",
|
"name": "validFrom",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
(v12/*: any*/),
|
(v11/*: any*/),
|
||||||
(v22/*: any*/)
|
(v21/*: any*/)
|
||||||
];
|
];
|
||||||
return {
|
return {
|
||||||
"fragment": {
|
"fragment": {
|
||||||
"argumentDefinitions": [
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
(v0/*: any*/),
|
|
||||||
(v1/*: any*/)
|
|
||||||
],
|
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "VendorGraphNodeQuery",
|
"name": "VendorGraphNodeQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": (v2/*: any*/),
|
"args": (v1/*: any*/),
|
||||||
"concreteType": null,
|
"concreteType": null,
|
||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "node",
|
"name": "node",
|
||||||
@@ -268,10 +240,10 @@ return {
|
|||||||
{
|
{
|
||||||
"kind": "InlineFragment",
|
"kind": "InlineFragment",
|
||||||
"selections": [
|
"selections": [
|
||||||
|
(v2/*: any*/),
|
||||||
(v3/*: any*/),
|
(v3/*: any*/),
|
||||||
(v4/*: any*/),
|
(v4/*: any*/),
|
||||||
(v5/*: any*/),
|
(v5/*: any*/),
|
||||||
(v6/*: any*/),
|
|
||||||
{
|
{
|
||||||
"args": null,
|
"args": null,
|
||||||
"kind": "FragmentSpread",
|
"kind": "FragmentSpread",
|
||||||
@@ -322,7 +294,7 @@ return {
|
|||||||
"name": "viewer",
|
"name": "viewer",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
"selections": [
|
"selections": [
|
||||||
(v8/*: any*/)
|
(v7/*: any*/)
|
||||||
],
|
],
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
}
|
}
|
||||||
@@ -332,30 +304,27 @@ return {
|
|||||||
},
|
},
|
||||||
"kind": "Request",
|
"kind": "Request",
|
||||||
"operation": {
|
"operation": {
|
||||||
"argumentDefinitions": [
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
(v1/*: any*/),
|
|
||||||
(v0/*: any*/)
|
|
||||||
],
|
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "VendorGraphNodeQuery",
|
"name": "VendorGraphNodeQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": (v2/*: any*/),
|
"args": (v1/*: any*/),
|
||||||
"concreteType": null,
|
"concreteType": null,
|
||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "node",
|
"name": "node",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
"selections": [
|
"selections": [
|
||||||
(v9/*: any*/),
|
(v8/*: any*/),
|
||||||
(v3/*: any*/),
|
(v2/*: any*/),
|
||||||
{
|
{
|
||||||
"kind": "InlineFragment",
|
"kind": "InlineFragment",
|
||||||
"selections": [
|
"selections": [
|
||||||
|
(v3/*: any*/),
|
||||||
(v4/*: any*/),
|
(v4/*: any*/),
|
||||||
(v5/*: any*/),
|
(v5/*: any*/),
|
||||||
(v6/*: any*/),
|
(v9/*: any*/),
|
||||||
(v10/*: any*/),
|
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
@@ -447,7 +416,7 @@ return {
|
|||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "businessOwner",
|
"name": "businessOwner",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
"selections": (v7/*: any*/),
|
"selections": (v6/*: any*/),
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -457,12 +426,12 @@ return {
|
|||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "securityOwner",
|
"name": "securityOwner",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
"selections": (v7/*: any*/),
|
"selections": (v6/*: any*/),
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": (v11/*: any*/),
|
"args": (v10/*: any*/),
|
||||||
"concreteType": "VendorComplianceReportConnection",
|
"concreteType": "VendorComplianceReportConnection",
|
||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "complianceReports",
|
"name": "complianceReports",
|
||||||
@@ -484,7 +453,7 @@ return {
|
|||||||
"name": "node",
|
"name": "node",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
"selections": [
|
"selections": [
|
||||||
(v3/*: any*/),
|
(v2/*: any*/),
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
@@ -492,7 +461,7 @@ return {
|
|||||||
"name": "reportDate",
|
"name": "reportDate",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
(v12/*: any*/),
|
(v11/*: any*/),
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
@@ -508,7 +477,7 @@ return {
|
|||||||
"name": "file",
|
"name": "file",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
"selections": [
|
"selections": [
|
||||||
(v13/*: any*/),
|
(v12/*: any*/),
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
@@ -523,27 +492,27 @@ return {
|
|||||||
"name": "size",
|
"name": "size",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
(v3/*: any*/)
|
(v2/*: any*/)
|
||||||
],
|
],
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
(v9/*: any*/)
|
(v8/*: any*/)
|
||||||
],
|
],
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
(v14/*: any*/)
|
(v13/*: any*/)
|
||||||
],
|
],
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
(v19/*: any*/),
|
(v18/*: any*/),
|
||||||
(v20/*: any*/)
|
(v19/*: any*/)
|
||||||
],
|
],
|
||||||
"storageKey": "complianceReports(first:50)"
|
"storageKey": "complianceReports(first:50)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": (v11/*: any*/),
|
"args": (v10/*: any*/),
|
||||||
"filters": (v21/*: any*/),
|
"filters": (v20/*: any*/),
|
||||||
"handle": "connection",
|
"handle": "connection",
|
||||||
"key": "VendorComplianceTabFragment_complianceReports",
|
"key": "VendorComplianceTabFragment_complianceReports",
|
||||||
"kind": "LinkedHandle",
|
"kind": "LinkedHandle",
|
||||||
@@ -551,7 +520,7 @@ return {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": (v11/*: any*/),
|
"args": (v10/*: any*/),
|
||||||
"concreteType": "VendorContactConnection",
|
"concreteType": "VendorContactConnection",
|
||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "contacts",
|
"name": "contacts",
|
||||||
@@ -573,7 +542,7 @@ return {
|
|||||||
"name": "node",
|
"name": "node",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
"selections": [
|
"selections": [
|
||||||
(v3/*: any*/),
|
(v2/*: any*/),
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
@@ -602,25 +571,25 @@ return {
|
|||||||
"name": "role",
|
"name": "role",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
|
(v21/*: any*/),
|
||||||
(v22/*: any*/),
|
(v22/*: any*/),
|
||||||
(v23/*: any*/),
|
(v8/*: any*/)
|
||||||
(v9/*: any*/)
|
|
||||||
],
|
],
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
(v14/*: any*/)
|
(v13/*: any*/)
|
||||||
],
|
],
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
(v19/*: any*/),
|
(v18/*: any*/),
|
||||||
(v20/*: any*/)
|
(v19/*: any*/)
|
||||||
],
|
],
|
||||||
"storageKey": "contacts(first:50)"
|
"storageKey": "contacts(first:50)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": (v11/*: any*/),
|
"args": (v10/*: any*/),
|
||||||
"filters": (v21/*: any*/),
|
"filters": (v20/*: any*/),
|
||||||
"handle": "connection",
|
"handle": "connection",
|
||||||
"key": "VendorContactsTabFragment_contacts",
|
"key": "VendorContactsTabFragment_contacts",
|
||||||
"kind": "LinkedHandle",
|
"kind": "LinkedHandle",
|
||||||
@@ -628,7 +597,7 @@ return {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": (v11/*: any*/),
|
"args": (v10/*: any*/),
|
||||||
"concreteType": "VendorServiceConnection",
|
"concreteType": "VendorServiceConnection",
|
||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "services",
|
"name": "services",
|
||||||
@@ -650,28 +619,28 @@ return {
|
|||||||
"name": "node",
|
"name": "node",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
"selections": [
|
"selections": [
|
||||||
(v3/*: any*/),
|
(v2/*: any*/),
|
||||||
(v5/*: any*/),
|
(v4/*: any*/),
|
||||||
(v10/*: any*/),
|
(v9/*: any*/),
|
||||||
|
(v21/*: any*/),
|
||||||
(v22/*: any*/),
|
(v22/*: any*/),
|
||||||
(v23/*: any*/),
|
(v8/*: any*/)
|
||||||
(v9/*: any*/)
|
|
||||||
],
|
],
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
(v14/*: any*/)
|
(v13/*: any*/)
|
||||||
],
|
],
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
(v19/*: any*/),
|
(v18/*: any*/),
|
||||||
(v20/*: any*/)
|
(v19/*: any*/)
|
||||||
],
|
],
|
||||||
"storageKey": "services(first:50)"
|
"storageKey": "services(first:50)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": (v11/*: any*/),
|
"args": (v10/*: any*/),
|
||||||
"filters": (v21/*: any*/),
|
"filters": (v20/*: any*/),
|
||||||
"handle": "connection",
|
"handle": "connection",
|
||||||
"key": "VendorServicesTabFragment_services",
|
"key": "VendorServicesTabFragment_services",
|
||||||
"kind": "LinkedHandle",
|
"kind": "LinkedHandle",
|
||||||
@@ -679,7 +648,7 @@ return {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": (v11/*: any*/),
|
"args": (v10/*: any*/),
|
||||||
"concreteType": "VendorRiskAssessmentConnection",
|
"concreteType": "VendorRiskAssessmentConnection",
|
||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "riskAssessments",
|
"name": "riskAssessments",
|
||||||
@@ -701,8 +670,8 @@ return {
|
|||||||
"name": "node",
|
"name": "node",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
"selections": [
|
"selections": [
|
||||||
(v3/*: any*/),
|
(v2/*: any*/),
|
||||||
(v22/*: any*/),
|
(v21/*: any*/),
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
@@ -731,11 +700,11 @@ return {
|
|||||||
"name": "notes",
|
"name": "notes",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
(v9/*: any*/)
|
(v8/*: any*/)
|
||||||
],
|
],
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
(v14/*: any*/)
|
(v13/*: any*/)
|
||||||
],
|
],
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
@@ -747,21 +716,21 @@ return {
|
|||||||
"name": "pageInfo",
|
"name": "pageInfo",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
"selections": [
|
"selections": [
|
||||||
(v16/*: any*/),
|
|
||||||
(v15/*: any*/),
|
(v15/*: any*/),
|
||||||
(v17/*: any*/),
|
(v14/*: any*/),
|
||||||
(v18/*: any*/)
|
(v16/*: any*/),
|
||||||
|
(v17/*: any*/)
|
||||||
],
|
],
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
(v20/*: any*/)
|
(v19/*: any*/)
|
||||||
],
|
],
|
||||||
"storageKey": "riskAssessments(first:50)"
|
"storageKey": "riskAssessments(first:50)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": (v11/*: any*/),
|
"args": (v10/*: any*/),
|
||||||
"filters": (v21/*: any*/),
|
"filters": (v20/*: any*/),
|
||||||
"handle": "connection",
|
"handle": "connection",
|
||||||
"key": "VendorRiskAssessmentTabFragment_riskAssessments",
|
"key": "VendorRiskAssessmentTabFragment_riskAssessments",
|
||||||
"kind": "LinkedHandle",
|
"kind": "LinkedHandle",
|
||||||
@@ -774,7 +743,7 @@ return {
|
|||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "businessAssociateAgreement",
|
"name": "businessAssociateAgreement",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
"selections": (v24/*: any*/),
|
"selections": (v23/*: any*/),
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -784,7 +753,7 @@ return {
|
|||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "dataPrivacyAgreement",
|
"name": "dataPrivacyAgreement",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
"selections": (v24/*: any*/),
|
"selections": (v23/*: any*/),
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -802,24 +771,24 @@ return {
|
|||||||
"name": "viewer",
|
"name": "viewer",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
"selections": [
|
"selections": [
|
||||||
(v8/*: any*/),
|
(v7/*: any*/),
|
||||||
(v3/*: any*/)
|
(v2/*: any*/)
|
||||||
],
|
],
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "faee4f059ef9baab46d5f2420e36bb60",
|
"cacheID": "b4e4f090951f025be6f359df3d2dc41a",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "VendorGraphNodeQuery",
|
"name": "VendorGraphNodeQuery",
|
||||||
"operationKind": "query",
|
"operationKind": "query",
|
||||||
"text": "query VendorGraphNodeQuery(\n $vendorId: ID!\n $organizationId: ID!\n) {\n node(id: $vendorId) {\n __typename\n ... on Vendor {\n id\n snapshotId\n name\n websiteUrl\n ...useVendorFormFragment\n ...VendorComplianceTabFragment\n ...VendorContactsTabFragment\n ...VendorServicesTabFragment\n ...VendorRiskAssessmentTabFragment\n ...VendorOverviewTabBusinessAssociateAgreementFragment\n ...VendorOverviewTabDataPrivacyAgreementFragment\n }\n id\n }\n viewer {\n user {\n id\n people(organizationId: $organizationId) {\n id\n }\n }\n id\n }\n}\n\nfragment VendorComplianceTabFragment on Vendor {\n complianceReports(first: 50) {\n edges {\n node {\n id\n ...VendorComplianceTabFragment_report\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment VendorComplianceTabFragment_report on VendorComplianceReport {\n id\n reportDate\n validUntil\n reportName\n file {\n fileName\n mimeType\n size\n id\n }\n}\n\nfragment VendorContactsTabFragment on Vendor {\n contacts(first: 50) {\n edges {\n node {\n id\n ...VendorContactsTabFragment_contact\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment VendorContactsTabFragment_contact on VendorContact {\n id\n fullName\n email\n phone\n role\n createdAt\n updatedAt\n}\n\nfragment VendorOverviewTabBusinessAssociateAgreementFragment on Vendor {\n businessAssociateAgreement {\n id\n fileName\n fileUrl\n validFrom\n validUntil\n createdAt\n }\n}\n\nfragment VendorOverviewTabDataPrivacyAgreementFragment on Vendor {\n dataPrivacyAgreement {\n id\n fileName\n fileUrl\n validFrom\n validUntil\n createdAt\n }\n}\n\nfragment VendorRiskAssessmentTabFragment on Vendor {\n id\n riskAssessments(first: 50) {\n edges {\n node {\n id\n ...VendorRiskAssessmentTabFragment_assessment\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment VendorRiskAssessmentTabFragment_assessment on VendorRiskAssessment {\n id\n createdAt\n expiresAt\n dataSensitivity\n businessImpact\n notes\n}\n\nfragment VendorServicesTabFragment on Vendor {\n services(first: 50) {\n edges {\n node {\n id\n ...VendorServicesTabFragment_service\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment VendorServicesTabFragment_service on VendorService {\n id\n name\n description\n createdAt\n updatedAt\n}\n\nfragment useVendorFormFragment on Vendor {\n id\n name\n description\n category\n statusPageUrl\n termsOfServiceUrl\n privacyPolicyUrl\n serviceLevelAgreementUrl\n dataProcessingAgreementUrl\n websiteUrl\n legalName\n headquarterAddress\n certifications\n countries\n securityPageUrl\n trustPageUrl\n businessOwner {\n id\n }\n securityOwner {\n id\n }\n}\n"
|
"text": "query VendorGraphNodeQuery(\n $vendorId: ID!\n) {\n node(id: $vendorId) {\n __typename\n ... on Vendor {\n id\n snapshotId\n name\n websiteUrl\n ...useVendorFormFragment\n ...VendorComplianceTabFragment\n ...VendorContactsTabFragment\n ...VendorServicesTabFragment\n ...VendorRiskAssessmentTabFragment\n ...VendorOverviewTabBusinessAssociateAgreementFragment\n ...VendorOverviewTabDataPrivacyAgreementFragment\n }\n id\n }\n viewer {\n user {\n id\n }\n id\n }\n}\n\nfragment VendorComplianceTabFragment on Vendor {\n complianceReports(first: 50) {\n edges {\n node {\n id\n ...VendorComplianceTabFragment_report\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment VendorComplianceTabFragment_report on VendorComplianceReport {\n id\n reportDate\n validUntil\n reportName\n file {\n fileName\n mimeType\n size\n id\n }\n}\n\nfragment VendorContactsTabFragment on Vendor {\n contacts(first: 50) {\n edges {\n node {\n id\n ...VendorContactsTabFragment_contact\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment VendorContactsTabFragment_contact on VendorContact {\n id\n fullName\n email\n phone\n role\n createdAt\n updatedAt\n}\n\nfragment VendorOverviewTabBusinessAssociateAgreementFragment on Vendor {\n businessAssociateAgreement {\n id\n fileName\n fileUrl\n validFrom\n validUntil\n createdAt\n }\n}\n\nfragment VendorOverviewTabDataPrivacyAgreementFragment on Vendor {\n dataPrivacyAgreement {\n id\n fileName\n fileUrl\n validFrom\n validUntil\n createdAt\n }\n}\n\nfragment VendorRiskAssessmentTabFragment on Vendor {\n id\n riskAssessments(first: 50) {\n edges {\n node {\n id\n ...VendorRiskAssessmentTabFragment_assessment\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment VendorRiskAssessmentTabFragment_assessment on VendorRiskAssessment {\n id\n createdAt\n expiresAt\n dataSensitivity\n businessImpact\n notes\n}\n\nfragment VendorServicesTabFragment on Vendor {\n services(first: 50) {\n edges {\n node {\n id\n ...VendorServicesTabFragment_service\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment VendorServicesTabFragment_service on VendorService {\n id\n name\n description\n createdAt\n updatedAt\n}\n\nfragment useVendorFormFragment on Vendor {\n id\n name\n description\n category\n statusPageUrl\n termsOfServiceUrl\n privacyPolicyUrl\n serviceLevelAgreementUrl\n dataProcessingAgreementUrl\n websiteUrl\n legalName\n headquarterAddress\n certifications\n countries\n securityPageUrl\n trustPageUrl\n businessOwner {\n id\n }\n securityOwner {\n id\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "5b907e5cc3446745ff2414ee06839aed";
|
(node as any).hash = "0a13dd059626880c4a51f8a47b9c2423";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ import {
|
|||||||
IconPlusLarge,
|
IconPlusLarge,
|
||||||
IconChevronDown,
|
IconChevronDown,
|
||||||
Avatar,
|
Avatar,
|
||||||
|
IconPeopleAdd,
|
||||||
|
Badge,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import { useTranslate } from "@probo/i18n";
|
import { useTranslate } from "@probo/i18n";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
@@ -85,6 +87,9 @@ const OrganizationSelectorFragment = graphql`
|
|||||||
endCursor
|
endCursor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
invitations(first: 1, filter: {onlyPending: true}) {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -287,6 +292,7 @@ function OrganizationSelector({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const organizations = data.organizations.edges.map((edge) => edge.node);
|
const organizations = data.organizations.edges.map((edge) => edge.node);
|
||||||
|
const pendingInvitationsCount = data.invitations.totalCount;
|
||||||
|
|
||||||
const handleLoadMore = (e?: React.MouseEvent) => {
|
const handleLoadMore = (e?: React.MouseEvent) => {
|
||||||
e?.preventDefault();
|
e?.preventDefault();
|
||||||
@@ -301,6 +307,7 @@ function OrganizationSelector({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
<Dropdown
|
<Dropdown
|
||||||
toggle={
|
toggle={
|
||||||
<Button
|
<Button
|
||||||
@@ -343,12 +350,36 @@ function OrganizationSelector({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<DropdownSeparator />
|
<DropdownSeparator />
|
||||||
<DropdownItem asChild icon={IconPlusLarge}>
|
{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">
|
<Link to="/organizations/new">
|
||||||
<IconPlusLarge size={16} />
|
<IconPlusLarge size={16} />
|
||||||
{__("Add organization")}
|
{__("Add organization")}
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</Dropdown>
|
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<92c96c7b8a58554e9fb99b86489e13d4>>
|
* @generated SignedSource<<7288534d3b5e852c463af820a9001298>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -208,6 +208,37 @@ return {
|
|||||||
"kind": "LinkedHandle",
|
"kind": "LinkedHandle",
|
||||||
"name": "organizations"
|
"name": "organizations"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "Literal",
|
||||||
|
"name": "filter",
|
||||||
|
"value": {
|
||||||
|
"onlyPending": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Literal",
|
||||||
|
"name": "first",
|
||||||
|
"value": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"concreteType": "InvitationConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "invitations",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "totalCount",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": "invitations(filter:{\"onlyPending\":true},first:1)"
|
||||||
|
},
|
||||||
(v4/*: any*/)
|
(v4/*: any*/)
|
||||||
],
|
],
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
@@ -215,16 +246,16 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "57fa65c9d5f23210b606fbf0b2005fb8",
|
"cacheID": "77f3fbbca18dda24151bff9901c54626",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "MainLayoutOrganizationSelectorPaginationQuery",
|
"name": "MainLayoutOrganizationSelectorPaginationQuery",
|
||||||
"operationKind": "query",
|
"operationKind": "query",
|
||||||
"text": "query MainLayoutOrganizationSelectorPaginationQuery(\n $after: CursorKey\n $first: Int = 25\n) {\n viewer {\n ...MainLayout_OrganizationSelector_viewer_2HEEH6\n id\n }\n}\n\nfragment MainLayout_OrganizationSelector_viewer_2HEEH6 on Viewer {\n organizations(first: $first, after: $after, orderBy: {field: NAME, direction: ASC}) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n}\n"
|
"text": "query MainLayoutOrganizationSelectorPaginationQuery(\n $after: CursorKey\n $first: Int = 25\n) {\n viewer {\n ...MainLayout_OrganizationSelector_viewer_2HEEH6\n id\n }\n}\n\nfragment MainLayout_OrganizationSelector_viewer_2HEEH6 on Viewer {\n organizations(first: $first, after: $after, orderBy: {field: NAME, direction: ASC}) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n invitations(first: 1, filter: {onlyPending: true}) {\n totalCount\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "0dc2841aeeb6ba5291cb45d8644ec4af";
|
(node as any).hash = "99a5cdcce2542bc7fba01d32bbd4648b";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<0719379cc0124f36eb1810ad24b2ae5a>>
|
* @generated SignedSource<<cb335e1fd93543371a82adf23002f600>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -275,6 +275,37 @@ return {
|
|||||||
"key": "MainLayout_OrganizationSelector_organizations",
|
"key": "MainLayout_OrganizationSelector_organizations",
|
||||||
"kind": "LinkedHandle",
|
"kind": "LinkedHandle",
|
||||||
"name": "organizations"
|
"name": "organizations"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "Literal",
|
||||||
|
"name": "filter",
|
||||||
|
"value": {
|
||||||
|
"status": "PENDING"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Literal",
|
||||||
|
"name": "first",
|
||||||
|
"value": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"concreteType": "InvitationConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "invitations",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "totalCount",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": "invitations(filter:{\"status\":\"PENDING\"},first:1)"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
@@ -304,12 +335,12 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "6287440397ef427d84f8734b0953784c",
|
"cacheID": "117a78fef8e7a8fe200baf1ebf2d2487",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "MainLayoutQuery",
|
"name": "MainLayoutQuery",
|
||||||
"operationKind": "query",
|
"operationKind": "query",
|
||||||
"text": "query MainLayoutQuery(\n $organizationId: ID!\n) {\n viewer {\n id\n user {\n fullName\n email\n id\n }\n ...MainLayout_OrganizationSelector_viewer\n }\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n logoUrl\n }\n id\n }\n}\n\nfragment MainLayout_OrganizationSelector_viewer on Viewer {\n organizations(first: 25, orderBy: {field: NAME, direction: ASC}) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n}\n"
|
"text": "query MainLayoutQuery(\n $organizationId: ID!\n) {\n viewer {\n id\n user {\n fullName\n email\n id\n }\n ...MainLayout_OrganizationSelector_viewer\n }\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n logoUrl\n }\n id\n }\n}\n\nfragment MainLayout_OrganizationSelector_viewer on Viewer {\n organizations(first: 25, orderBy: {field: NAME, direction: ASC}) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n invitations(first: 1, filter: {status: PENDING}) {\n totalCount\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<2eda1054dbabd2bb5e45d8db5c7a01fc>>
|
* @generated SignedSource<<bd521d62fb0f29ace7af511b527ef750>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -11,6 +11,9 @@
|
|||||||
import { ReaderFragment } from 'relay-runtime';
|
import { ReaderFragment } from 'relay-runtime';
|
||||||
import { FragmentRefs } from "relay-runtime";
|
import { FragmentRefs } from "relay-runtime";
|
||||||
export type MainLayout_OrganizationSelector_viewer$data = {
|
export type MainLayout_OrganizationSelector_viewer$data = {
|
||||||
|
readonly invitations: {
|
||||||
|
readonly totalCount: number;
|
||||||
|
};
|
||||||
readonly organizations: {
|
readonly organizations: {
|
||||||
readonly edges: ReadonlyArray<{
|
readonly edges: ReadonlyArray<{
|
||||||
readonly node: {
|
readonly node: {
|
||||||
@@ -178,6 +181,37 @@ return {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"storageKey": "__MainLayout_OrganizationSelector_organizations_connection(orderBy:{\"direction\":\"ASC\",\"field\":\"NAME\"})"
|
"storageKey": "__MainLayout_OrganizationSelector_organizations_connection(orderBy:{\"direction\":\"ASC\",\"field\":\"NAME\"})"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "Literal",
|
||||||
|
"name": "filter",
|
||||||
|
"value": {
|
||||||
|
"status": "PENDING"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Literal",
|
||||||
|
"name": "first",
|
||||||
|
"value": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"concreteType": "InvitationConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "invitations",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "totalCount",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": "invitations(filter:{\"status\":\"PENDING\"},first:1)"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"type": "Viewer",
|
"type": "Viewer",
|
||||||
@@ -185,6 +219,6 @@ return {
|
|||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "0dc2841aeeb6ba5291cb45d8644ec4af";
|
(node as any).hash = "d1491a8228d455e2491724c03c7c30c1";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
IconPlusLarge,
|
IconPlusLarge,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import { usePageTitle } from "@probo/hooks";
|
import { usePageTitle } from "@probo/hooks";
|
||||||
|
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
|
||||||
|
import { formatDate } from "@probo/helpers";
|
||||||
|
|
||||||
const OrganizationsPageQuery = graphql`
|
const OrganizationsPageQuery = graphql`
|
||||||
query OrganizationsPageQuery {
|
query OrganizationsPageQuery {
|
||||||
@@ -25,6 +27,34 @@ const OrganizationsPageQuery = graphql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
invitations(first: 1000, orderBy: {field: CREATED_AT, direction: DESC}, filter: {onlyPending: true}) @connection(key: "OrganizationsPage_invitations") {
|
||||||
|
__id
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
fullName
|
||||||
|
role
|
||||||
|
expiresAt
|
||||||
|
acceptedAt
|
||||||
|
createdAt
|
||||||
|
organization {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const acceptInvitationMutation = graphql`
|
||||||
|
mutation OrganizationsPage_AcceptInvitationMutation($input: AcceptInvitationInput!) {
|
||||||
|
acceptInvitation(input: $input) {
|
||||||
|
invitation {
|
||||||
|
id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -41,15 +71,40 @@ export default function OrganizationsPage() {
|
|||||||
(edge) => edge.node
|
(edge) => edge.node
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const pendingInvitations = data.viewer.invitations.edges.map(
|
||||||
|
(edge) => edge.node
|
||||||
|
);
|
||||||
|
|
||||||
|
const [acceptInvitation, isAccepting] = useMutationWithToasts(
|
||||||
|
acceptInvitationMutation,
|
||||||
|
{
|
||||||
|
successMessage: __("Invitation accepted successfully"),
|
||||||
|
errorMessage: __("Failed to accept invitation"),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleAcceptInvitation = (invitationId: string, organizationId: string) => {
|
||||||
|
acceptInvitation({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
invitationId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
navigate(`/organizations/${organizationId}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
usePageTitle(__("Select an organization"));
|
usePageTitle(__("Select an organization"));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (organizations.length === 1) {
|
if (organizations.length === 1 && pendingInvitations.length === 0) {
|
||||||
navigate(`/organizations/${organizations[0].id}`);
|
navigate(`/organizations/${organizations[0].id}`);
|
||||||
} else if (organizations.length === 0) {
|
} else if (organizations.length === 0 && pendingInvitations.length === 0) {
|
||||||
navigate("/organizations/new");
|
navigate("/organizations/new");
|
||||||
}
|
}
|
||||||
}, [organizations]);
|
}, [organizations, pendingInvitations]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -58,12 +113,36 @@ export default function OrganizationsPage() {
|
|||||||
{__("Select an organization")}
|
{__("Select an organization")}
|
||||||
</h1>
|
</h1>
|
||||||
<div className="space-y-4 w-full">
|
<div className="space-y-4 w-full">
|
||||||
|
{pendingInvitations.length > 0 && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h2 className="text-xl font-semibold">
|
||||||
|
{__("Pending invitations")}
|
||||||
|
</h2>
|
||||||
|
{pendingInvitations.map((invitation) => (
|
||||||
|
<InvitationCard
|
||||||
|
key={invitation.id}
|
||||||
|
invitation={invitation}
|
||||||
|
onAccept={handleAcceptInvitation}
|
||||||
|
isAccepting={isAccepting}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{organizations.length > 0 && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{pendingInvitations.length > 0 && (
|
||||||
|
<h2 className="text-xl font-semibold">
|
||||||
|
{__("Your organizations")}
|
||||||
|
</h2>
|
||||||
|
)}
|
||||||
{organizations.map((organization) => (
|
{organizations.map((organization) => (
|
||||||
<OrganizationCard
|
<OrganizationCard
|
||||||
key={organization.id}
|
key={organization.id}
|
||||||
organization={organization}
|
organization={organization}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Card padded>
|
<Card padded>
|
||||||
<h2 className="text-xl font-semibold mb-1">
|
<h2 className="text-xl font-semibold mb-1">
|
||||||
{__("Create an organization")}
|
{__("Create an organization")}
|
||||||
@@ -86,6 +165,51 @@ export default function OrganizationsPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InvitationCardProps = {
|
||||||
|
invitation: {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
fullName: string;
|
||||||
|
role: string;
|
||||||
|
expiresAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
organization: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
onAccept: (invitationId: string, organizationId: string) => void;
|
||||||
|
isAccepting: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function InvitationCard({ invitation, onAccept, isAccepting }: InvitationCardProps) {
|
||||||
|
const { __ } = useTranslate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card padded className="w-full">
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div className="flex-1 space-y-1">
|
||||||
|
<h3 className="text-lg font-semibold">
|
||||||
|
{invitation.organization.name}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-txt-secondary">
|
||||||
|
{__("Role")}: <span className="font-medium">{invitation.role}</span>
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-txt-tertiary">
|
||||||
|
{__("Invited on")} {formatDate(invitation.createdAt)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={() => onAccept(invitation.id, invitation.organization.id)}
|
||||||
|
disabled={isAccepting}
|
||||||
|
>
|
||||||
|
{isAccepting ? __("Accepting...") : __("Accept invitation")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type OrganizationCardProps = {
|
type OrganizationCardProps = {
|
||||||
organization: {
|
organization: {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<84d0e43eced78862d1c82603e9c332e8>>
|
* @generated SignedSource<<c25588b7506704dd23d9c8be8672f9ef>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -12,6 +12,24 @@ import { ConcreteRequest } from 'relay-runtime';
|
|||||||
export type OrganizationsPageQuery$variables = Record<PropertyKey, never>;
|
export type OrganizationsPageQuery$variables = Record<PropertyKey, never>;
|
||||||
export type OrganizationsPageQuery$data = {
|
export type OrganizationsPageQuery$data = {
|
||||||
readonly viewer: {
|
readonly viewer: {
|
||||||
|
readonly invitations: {
|
||||||
|
readonly __id: string;
|
||||||
|
readonly edges: ReadonlyArray<{
|
||||||
|
readonly node: {
|
||||||
|
readonly acceptedAt: any | null | undefined;
|
||||||
|
readonly createdAt: any;
|
||||||
|
readonly email: string;
|
||||||
|
readonly expiresAt: any;
|
||||||
|
readonly fullName: string;
|
||||||
|
readonly id: string;
|
||||||
|
readonly organization: {
|
||||||
|
readonly id: string;
|
||||||
|
readonly name: string;
|
||||||
|
};
|
||||||
|
readonly role: string;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
};
|
||||||
readonly organizations: {
|
readonly organizations: {
|
||||||
readonly __id: string;
|
readonly __id: string;
|
||||||
readonly edges: ReadonlyArray<{
|
readonly edges: ReadonlyArray<{
|
||||||
@@ -45,59 +63,28 @@ v1 = {
|
|||||||
"name": "id",
|
"name": "id",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
v2 = [
|
v2 = {
|
||||||
{
|
|
||||||
"alias": null,
|
|
||||||
"args": null,
|
|
||||||
"concreteType": "OrganizationEdge",
|
|
||||||
"kind": "LinkedField",
|
|
||||||
"name": "edges",
|
|
||||||
"plural": true,
|
|
||||||
"selections": [
|
|
||||||
{
|
|
||||||
"alias": null,
|
|
||||||
"args": null,
|
|
||||||
"concreteType": "Organization",
|
|
||||||
"kind": "LinkedField",
|
|
||||||
"name": "node",
|
|
||||||
"plural": false,
|
|
||||||
"selections": [
|
|
||||||
(v1/*: any*/),
|
|
||||||
{
|
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"kind": "ScalarField",
|
"kind": "ScalarField",
|
||||||
"name": "name",
|
"name": "name",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
{
|
v3 = {
|
||||||
"alias": null,
|
|
||||||
"args": null,
|
|
||||||
"kind": "ScalarField",
|
|
||||||
"name": "logoUrl",
|
|
||||||
"storageKey": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"kind": "ScalarField",
|
"kind": "ScalarField",
|
||||||
"name": "__typename",
|
"name": "__typename",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
}
|
},
|
||||||
],
|
v4 = {
|
||||||
"storageKey": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"kind": "ScalarField",
|
"kind": "ScalarField",
|
||||||
"name": "cursor",
|
"name": "cursor",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
}
|
},
|
||||||
],
|
v5 = {
|
||||||
"storageKey": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"concreteType": "PageInfo",
|
"concreteType": "PageInfo",
|
||||||
@@ -121,8 +108,8 @@ v2 = [
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
{
|
v6 = {
|
||||||
"kind": "ClientExtension",
|
"kind": "ClientExtension",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
@@ -133,15 +120,156 @@ v2 = [
|
|||||||
"storageKey": null
|
"storageKey": null
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
],
|
v7 = [
|
||||||
v3 = [
|
|
||||||
{
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "OrganizationEdge",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "edges",
|
||||||
|
"plural": true,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Organization",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v1/*: any*/),
|
||||||
|
(v2/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "logoUrl",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v3/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v4/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v5/*: any*/),
|
||||||
|
(v6/*: any*/)
|
||||||
|
],
|
||||||
|
v8 = {
|
||||||
|
"kind": "Literal",
|
||||||
|
"name": "filter",
|
||||||
|
"value": {
|
||||||
|
"onlyPending": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
v9 = {
|
||||||
|
"kind": "Literal",
|
||||||
|
"name": "orderBy",
|
||||||
|
"value": {
|
||||||
|
"direction": "DESC",
|
||||||
|
"field": "CREATED_AT"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
v10 = [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "InvitationEdge",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "edges",
|
||||||
|
"plural": true,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Invitation",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v1/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "email",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "fullName",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "role",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "expiresAt",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "acceptedAt",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "createdAt",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Organization",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "organization",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v1/*: any*/),
|
||||||
|
(v2/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v3/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v4/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v5/*: any*/),
|
||||||
|
(v6/*: any*/)
|
||||||
|
],
|
||||||
|
v11 = {
|
||||||
"kind": "Literal",
|
"kind": "Literal",
|
||||||
"name": "first",
|
"name": "first",
|
||||||
"value": 1000
|
"value": 1000
|
||||||
},
|
},
|
||||||
|
v12 = [
|
||||||
|
(v11/*: any*/),
|
||||||
(v0/*: any*/)
|
(v0/*: any*/)
|
||||||
|
],
|
||||||
|
v13 = [
|
||||||
|
(v8/*: any*/),
|
||||||
|
(v11/*: any*/),
|
||||||
|
(v9/*: any*/)
|
||||||
];
|
];
|
||||||
return {
|
return {
|
||||||
"fragment": {
|
"fragment": {
|
||||||
@@ -167,8 +295,21 @@ return {
|
|||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "__OrganizationsPage_organizations_connection",
|
"name": "__OrganizationsPage_organizations_connection",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
"selections": (v2/*: any*/),
|
"selections": (v7/*: any*/),
|
||||||
"storageKey": "__OrganizationsPage_organizations_connection(orderBy:{\"direction\":\"ASC\",\"field\":\"NAME\"})"
|
"storageKey": "__OrganizationsPage_organizations_connection(orderBy:{\"direction\":\"ASC\",\"field\":\"NAME\"})"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "invitations",
|
||||||
|
"args": [
|
||||||
|
(v8/*: any*/),
|
||||||
|
(v9/*: any*/)
|
||||||
|
],
|
||||||
|
"concreteType": "InvitationConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "__OrganizationsPage_invitations_connection",
|
||||||
|
"plural": false,
|
||||||
|
"selections": (v10/*: any*/),
|
||||||
|
"storageKey": "__OrganizationsPage_invitations_connection(filter:{\"onlyPending\":true},orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
@@ -193,17 +334,17 @@ return {
|
|||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": (v3/*: any*/),
|
"args": (v12/*: any*/),
|
||||||
"concreteType": "OrganizationConnection",
|
"concreteType": "OrganizationConnection",
|
||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "organizations",
|
"name": "organizations",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
"selections": (v2/*: any*/),
|
"selections": (v7/*: any*/),
|
||||||
"storageKey": "organizations(first:1000,orderBy:{\"direction\":\"ASC\",\"field\":\"NAME\"})"
|
"storageKey": "organizations(first:1000,orderBy:{\"direction\":\"ASC\",\"field\":\"NAME\"})"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": (v3/*: any*/),
|
"args": (v12/*: any*/),
|
||||||
"filters": [
|
"filters": [
|
||||||
"orderBy"
|
"orderBy"
|
||||||
],
|
],
|
||||||
@@ -212,6 +353,28 @@ return {
|
|||||||
"kind": "LinkedHandle",
|
"kind": "LinkedHandle",
|
||||||
"name": "organizations"
|
"name": "organizations"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v13/*: any*/),
|
||||||
|
"concreteType": "InvitationConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "invitations",
|
||||||
|
"plural": false,
|
||||||
|
"selections": (v10/*: any*/),
|
||||||
|
"storageKey": "invitations(filter:{\"onlyPending\":true},first:1000,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v13/*: any*/),
|
||||||
|
"filters": [
|
||||||
|
"orderBy",
|
||||||
|
"filter"
|
||||||
|
],
|
||||||
|
"handle": "connection",
|
||||||
|
"key": "OrganizationsPage_invitations",
|
||||||
|
"kind": "LinkedHandle",
|
||||||
|
"name": "invitations"
|
||||||
|
},
|
||||||
(v1/*: any*/)
|
(v1/*: any*/)
|
||||||
],
|
],
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
@@ -219,7 +382,7 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "1735764e6816660969c5f96922320ac5",
|
"cacheID": "5675b3eb7810ef04bf531d7bf988c86c",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"connection": [
|
"connection": [
|
||||||
@@ -231,16 +394,25 @@ return {
|
|||||||
"viewer",
|
"viewer",
|
||||||
"organizations"
|
"organizations"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": null,
|
||||||
|
"cursor": null,
|
||||||
|
"direction": "forward",
|
||||||
|
"path": [
|
||||||
|
"viewer",
|
||||||
|
"invitations"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"name": "OrganizationsPageQuery",
|
"name": "OrganizationsPageQuery",
|
||||||
"operationKind": "query",
|
"operationKind": "query",
|
||||||
"text": "query OrganizationsPageQuery {\n viewer {\n organizations(first: 1000, orderBy: {field: NAME, direction: ASC}) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n id\n }\n}\n"
|
"text": "query OrganizationsPageQuery {\n viewer {\n organizations(first: 1000, orderBy: {field: NAME, direction: ASC}) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n invitations(first: 1000, orderBy: {field: CREATED_AT, direction: DESC}, filter: {onlyPending: true}) {\n edges {\n node {\n id\n email\n fullName\n role\n expiresAt\n acceptedAt\n createdAt\n organization {\n id\n name\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n id\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "6fde39384e4678f88f17c34bbd30e684";
|
(node as any).hash = "a548f9c3a434e12c079fb2ff651822d2";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
|
|||||||
105
apps/console/src/pages/__generated__/OrganizationsPage_AcceptInvitationMutation.graphql.ts
generated
Normal file
105
apps/console/src/pages/__generated__/OrganizationsPage_AcceptInvitationMutation.graphql.ts
generated
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<b883cd0523e827b2c71b89e1d249d6dd>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type AcceptInvitationInput = {
|
||||||
|
invitationId: string;
|
||||||
|
};
|
||||||
|
export type OrganizationsPage_AcceptInvitationMutation$variables = {
|
||||||
|
input: AcceptInvitationInput;
|
||||||
|
};
|
||||||
|
export type OrganizationsPage_AcceptInvitationMutation$data = {
|
||||||
|
readonly acceptInvitation: {
|
||||||
|
readonly invitation: {
|
||||||
|
readonly id: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type OrganizationsPage_AcceptInvitationMutation = {
|
||||||
|
response: OrganizationsPage_AcceptInvitationMutation$data;
|
||||||
|
variables: OrganizationsPage_AcceptInvitationMutation$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = [
|
||||||
|
{
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v1 = [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "input",
|
||||||
|
"variableName": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"concreteType": "AcceptInvitationPayload",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "acceptInvitation",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Invitation",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "invitation",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "id",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "OrganizationsPage_AcceptInvitationMutation",
|
||||||
|
"selections": (v1/*: any*/),
|
||||||
|
"type": "Mutation",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "OrganizationsPage_AcceptInvitationMutation",
|
||||||
|
"selections": (v1/*: any*/)
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "cc4a442037edaa624948b5be0c009823",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "OrganizationsPage_AcceptInvitationMutation",
|
||||||
|
"operationKind": "mutation",
|
||||||
|
"text": "mutation OrganizationsPage_AcceptInvitationMutation(\n $input: AcceptInvitationInput!\n) {\n acceptInvitation(input: $input) {\n invitation {\n id\n }\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "190213ab6fdc068343a270b4fa94e160";
|
||||||
|
|
||||||
|
export default node;
|
||||||
@@ -1,35 +1,49 @@
|
|||||||
import { Link, useNavigate } from "react-router";
|
import { Link, useNavigate, useSearchParams } from "react-router";
|
||||||
import { Button, Field, useToast } from "@probo/ui";
|
import { Button, Field, useToast } from "@probo/ui";
|
||||||
import { useTranslate } from "@probo/i18n";
|
import { useTranslate } from "@probo/i18n";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { useFormWithSchema } from "/hooks/useFormWithSchema";
|
import { useFormWithSchema } from "/hooks/useFormWithSchema";
|
||||||
import { usePageTitle } from "@probo/hooks";
|
import { usePageTitle } from "@probo/hooks";
|
||||||
import { buildEndpoint } from "/providers/RelayProviders";
|
import { buildEndpoint } from "/providers/RelayProviders";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
|
fullName: z.string().min(2),
|
||||||
password: z.string().min(8),
|
password: z.string().min(8),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function ConfirmInvitationPage() {
|
export default function SignupFromInvitationPage() {
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { register, handleSubmit, formState } = useFormWithSchema(
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const { register, handleSubmit, formState, reset } = useFormWithSchema(
|
||||||
schema,
|
schema,
|
||||||
{
|
{
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
|
fullName: "",
|
||||||
password: "",
|
password: "",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fullNameFromParams = searchParams.get("fullName") || "";
|
||||||
|
if (fullNameFromParams) {
|
||||||
|
reset({
|
||||||
|
fullName: fullNameFromParams,
|
||||||
|
password: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [searchParams, reset]);
|
||||||
|
|
||||||
const onSubmit = handleSubmit(async (data) => {
|
const onSubmit = handleSubmit(async (data) => {
|
||||||
const searchParams = new URLSearchParams(location.search);
|
|
||||||
const token = searchParams.get("token");
|
const token = searchParams.get("token");
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
toast({
|
toast({
|
||||||
title: __("Confirmation failed"),
|
title: __("Signup failed"),
|
||||||
description: __("Invalid or missing invitation token"),
|
description: __("Invalid or missing invitation token"),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
@@ -37,7 +51,7 @@ export default function ConfirmInvitationPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
buildEndpoint("/api/console/v1/auth/invitation"),
|
buildEndpoint("/api/console/v1/auth/signup-from-invitation"),
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -47,16 +61,16 @@ export default function ConfirmInvitationPage() {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
token: token,
|
token: token,
|
||||||
password: data.password,
|
password: data.password,
|
||||||
|
fullName: data.fullName,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Registration failed
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json().catch(() => ({}));
|
const errorData = await response.json().catch(() => ({}));
|
||||||
toast({
|
toast({
|
||||||
title: __("Confirmation failed"),
|
title: __("Signup failed"),
|
||||||
description: errorData.message || __("Confirmation failed"),
|
description: errorData.message || __("Signup failed"),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -64,24 +78,33 @@ export default function ConfirmInvitationPage() {
|
|||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: __("Success"),
|
title: __("Success"),
|
||||||
description: __("Invitation confirmed successfully"),
|
description: __("Account created successfully. Please accept your invitation to join the organization."),
|
||||||
variant: "success",
|
variant: "success",
|
||||||
});
|
});
|
||||||
navigate("/", { replace: true });
|
navigate("/", { replace: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
usePageTitle(__("Confirm invitation"));
|
usePageTitle(__("Create your account"));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 w-full max-w-md mx-auto">
|
<div className="space-y-6 w-full max-w-md mx-auto">
|
||||||
<div className="space-y-2 text-center">
|
<div className="space-y-2 text-center">
|
||||||
<h1 className="text-3xl font-bold">{__("Confirm invitation")}</h1>
|
<h1 className="text-3xl font-bold">{__("Create your account")}</h1>
|
||||||
<p className="text-txt-tertiary">
|
<p className="text-txt-tertiary">
|
||||||
{__("Enter your information to confirm your invitation")}
|
{__("Set your password to join the organization")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={onSubmit} className="space-y-4">
|
<form onSubmit={onSubmit} className="space-y-4">
|
||||||
|
<Field
|
||||||
|
label={__("Full Name")}
|
||||||
|
type="text"
|
||||||
|
placeholder={__("John Doe")}
|
||||||
|
{...register("fullName")}
|
||||||
|
required
|
||||||
|
error={formState.errors.fullName?.message}
|
||||||
|
/>
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
label={__("Password")}
|
label={__("Password")}
|
||||||
type="password"
|
type="password"
|
||||||
@@ -93,8 +116,8 @@ export default function ConfirmInvitationPage() {
|
|||||||
|
|
||||||
<Button type="submit" className="w-full" disabled={formState.isLoading}>
|
<Button type="submit" className="w-full" disabled={formState.isLoading}>
|
||||||
{formState.isLoading
|
{formState.isLoading
|
||||||
? __("Confirming invitation...")
|
? __("Creating account...")
|
||||||
: __("Confirm invitation")}
|
: __("Create account")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -50,13 +50,13 @@ import { sprintf } from "@probo/helpers";
|
|||||||
import { useFormWithSchema } from "/hooks/useFormWithSchema";
|
import { useFormWithSchema } from "/hooks/useFormWithSchema";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import type { NodeOf } from "/types";
|
import type { NodeOf } from "/types";
|
||||||
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
|
|
||||||
import { useOrganizationId } from "/hooks/useOrganizationId";
|
import { useOrganizationId } from "/hooks/useOrganizationId";
|
||||||
import { InviteUserDialog } from "/components/organizations/InviteUserDialog";
|
import { InviteUserDialog } from "/components/organizations/InviteUserDialog";
|
||||||
import { useDeleteOrganizationMutation } from "/hooks/graph/OrganizationGraph";
|
import { useDeleteOrganizationMutation } from "/hooks/graph/OrganizationGraph";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { DeleteOrganizationDialog } from "/components/organizations/DeleteOrganizationDialog";
|
import { DeleteOrganizationDialog } from "/components/organizations/DeleteOrganizationDialog";
|
||||||
import { CustomDomainManager } from "/components/customDomains/CustomDomainManager";
|
import { CustomDomainManager } from "/components/customDomains/CustomDomainManager";
|
||||||
|
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
|
||||||
|
|
||||||
const organizationSchema = z.object({
|
const organizationSchema = z.object({
|
||||||
name: z.string().min(1, "Organization name is required"),
|
name: z.string().min(1, "Organization name is required"),
|
||||||
@@ -220,6 +220,7 @@ const deleteHorizontalLogoMutation = graphql`
|
|||||||
export default function SettingsPage({ queryRef }: Props) {
|
export default function SettingsPage({ queryRef }: Props) {
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const organizationId = useOrganizationId();
|
||||||
const organizationKey = usePreloadedQuery(
|
const organizationKey = usePreloadedQuery(
|
||||||
organizationViewQuery,
|
organizationViewQuery,
|
||||||
queryRef
|
queryRef
|
||||||
@@ -240,6 +241,14 @@ export default function SettingsPage({ queryRef }: Props) {
|
|||||||
organizationKey as SettingsPageInvitationsFragment$key
|
organizationKey as SettingsPageInvitationsFragment$key
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const refetchMemberships = () => {
|
||||||
|
membershipsPagination.refetch({}, { fetchPolicy: 'network-only' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const refetchInvitations = () => {
|
||||||
|
invitationsPagination.refetch({}, { fetchPolicy: 'network-only' });
|
||||||
|
};
|
||||||
|
|
||||||
const [updateOrganization] = useMutation(updateOrganizationMutation);
|
const [updateOrganization] = useMutation(updateOrganizationMutation);
|
||||||
const [deleteHorizontalLogo, isDeletingHorizontalLogo] = useMutationWithToasts(
|
const [deleteHorizontalLogo, isDeletingHorizontalLogo] = useMutationWithToasts(
|
||||||
deleteHorizontalLogoMutation,
|
deleteHorizontalLogoMutation,
|
||||||
@@ -253,24 +262,6 @@ export default function SettingsPage({ queryRef }: Props) {
|
|||||||
const invitations = invitationsPagination.data.invitations?.edges.map((edge) => edge.node) || [];
|
const invitations = invitationsPagination.data.invitations?.edges.map((edge) => edge.node) || [];
|
||||||
const [activeTab, setActiveTab] = useState<"memberships" | "invitations">("memberships");
|
const [activeTab, setActiveTab] = useState<"memberships" | "invitations">("memberships");
|
||||||
|
|
||||||
const refetchMemberships = ({ order }: { order: { direction: string; field: string } }) => {
|
|
||||||
membershipsPagination.refetch({
|
|
||||||
order: {
|
|
||||||
direction: order.direction as "ASC" | "DESC",
|
|
||||||
field: order.field as "CREATED_AT" | "FULL_NAME" | "EMAIL_ADDRESS" | "ROLE"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const refetchInvitations = ({ order }: { order: { direction: string; field: string } }) => {
|
|
||||||
invitationsPagination.refetch({
|
|
||||||
order: {
|
|
||||||
direction: order.direction as "ASC" | "DESC",
|
|
||||||
field: order.field as "CREATED_AT" | "EXPIRES_AT" | "FULL_NAME" | "EMAIL" | "ROLE" | "STATUS" | "ACCEPTED_AT"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const { formState, handleSubmit, register, reset } = useFormWithSchema(
|
const { formState, handleSubmit, register, reset } = useFormWithSchema(
|
||||||
organizationSchema,
|
organizationSchema,
|
||||||
{
|
{
|
||||||
@@ -572,7 +563,10 @@ export default function SettingsPage({ queryRef }: Props) {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-base font-medium">{__("Workspace members")}</h2>
|
<h2 className="text-base font-medium">{__("Workspace members")}</h2>
|
||||||
<InviteUserDialog connectionId={invitationsPagination.data.invitations?.__id}>
|
<InviteUserDialog
|
||||||
|
connectionId={invitationsPagination.data.invitations?.__id}
|
||||||
|
onRefetch={refetchInvitations}
|
||||||
|
>
|
||||||
<Button variant="secondary">{__("Invite member")}</Button>
|
<Button variant="secondary">{__("Invite member")}</Button>
|
||||||
</InviteUserDialog>
|
</InviteUserDialog>
|
||||||
</div>
|
</div>
|
||||||
@@ -603,7 +597,14 @@ export default function SettingsPage({ queryRef }: Props) {
|
|||||||
{activeTab === "memberships" && (
|
{activeTab === "memberships" && (
|
||||||
<SortableTable
|
<SortableTable
|
||||||
{...membershipsPagination}
|
{...membershipsPagination}
|
||||||
refetch={refetchMemberships}
|
refetch={({ order }: { order: { direction: string; field: string } }) => {
|
||||||
|
membershipsPagination.refetch({
|
||||||
|
order: {
|
||||||
|
direction: order.direction as "ASC" | "DESC",
|
||||||
|
field: order.field as "CREATED_AT" | "FULL_NAME" | "EMAIL_ADDRESS" | "ROLE"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Thead>
|
<Thead>
|
||||||
<Tr>
|
<Tr>
|
||||||
@@ -623,7 +624,13 @@ export default function SettingsPage({ queryRef }: Props) {
|
|||||||
</Tr>
|
</Tr>
|
||||||
) : (
|
) : (
|
||||||
memberships.map((membership) => (
|
memberships.map((membership) => (
|
||||||
<MembershipRow key={membership.id} membership={membership} />
|
<MembershipRow
|
||||||
|
key={membership.id}
|
||||||
|
membership={membership}
|
||||||
|
connectionId={membershipsPagination.data.memberships?.__id}
|
||||||
|
organizationId={organizationId}
|
||||||
|
onRefetch={refetchMemberships}
|
||||||
|
/>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</Tbody>
|
</Tbody>
|
||||||
@@ -633,7 +640,14 @@ export default function SettingsPage({ queryRef }: Props) {
|
|||||||
{activeTab === "invitations" && (
|
{activeTab === "invitations" && (
|
||||||
<SortableTable
|
<SortableTable
|
||||||
{...invitationsPagination}
|
{...invitationsPagination}
|
||||||
refetch={refetchInvitations}
|
refetch={({ order }: { order: { direction: string; field: string } }) => {
|
||||||
|
invitationsPagination.refetch({
|
||||||
|
order: {
|
||||||
|
direction: order.direction as "ASC" | "DESC",
|
||||||
|
field: order.field as "CREATED_AT" | "EXPIRES_AT" | "FULL_NAME" | "EMAIL" | "ROLE" | "STATUS" | "ACCEPTED_AT"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Thead>
|
<Thead>
|
||||||
<Tr>
|
<Tr>
|
||||||
@@ -659,6 +673,8 @@ export default function SettingsPage({ queryRef }: Props) {
|
|||||||
key={invitation.id}
|
key={invitation.id}
|
||||||
invitation={invitation}
|
invitation={invitation}
|
||||||
connectionId={invitationsPagination.data.invitations?.__id}
|
connectionId={invitationsPagination.data.invitations?.__id}
|
||||||
|
organizationId={organizationId}
|
||||||
|
onRefetch={refetchInvitations}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
@@ -794,9 +810,12 @@ function Connectors(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const removeMemberMutation = graphql`
|
const removeMemberMutation = graphql`
|
||||||
mutation SettingsPage_RemoveMemberMutation($input: RemoveMemberInput!) {
|
mutation SettingsPage_RemoveMemberMutation(
|
||||||
|
$input: RemoveMemberInput!
|
||||||
|
$connections: [ID!]!
|
||||||
|
) {
|
||||||
removeMember(input: $input) {
|
removeMember(input: $input) {
|
||||||
success
|
deletedMemberId @deleteEdge(connections: $connections)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -804,16 +823,16 @@ const removeMemberMutation = graphql`
|
|||||||
function InvitationRow(props: {
|
function InvitationRow(props: {
|
||||||
invitation: NodeOf<SettingsPageInvitationsFragment$data["invitations"]>;
|
invitation: NodeOf<SettingsPageInvitationsFragment$data["invitations"]>;
|
||||||
connectionId?: string;
|
connectionId?: string;
|
||||||
|
organizationId: string;
|
||||||
|
onRefetch: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const [deleteInvitation, isDeleting] = useMutationWithToasts(
|
const [deleteInvitation, isDeleting] = useMutationWithToasts(
|
||||||
deleteInvitationMutation,
|
deleteInvitationMutation,
|
||||||
{
|
{
|
||||||
successMessage: sprintf(
|
successMessage: __("Invitation deleted successfully"),
|
||||||
__("Invitation for %s deleted successfully"),
|
errorMessage: __("Failed to delete invitation"),
|
||||||
props.invitation.fullName
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -830,6 +849,9 @@ function InvitationRow(props: {
|
|||||||
},
|
},
|
||||||
connections: props.connectionId ? [props.connectionId] : [],
|
connections: props.connectionId ? [props.connectionId] : [],
|
||||||
},
|
},
|
||||||
|
onCompleted: () => {
|
||||||
|
props.onRefetch();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -885,15 +907,16 @@ function InvitationRow(props: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MembershipRow(props: { membership: NodeOf<SettingsPageMembershipsFragment$data["memberships"]> }) {
|
function MembershipRow(props: {
|
||||||
|
membership: NodeOf<SettingsPageMembershipsFragment$data["memberships"]>;
|
||||||
|
connectionId?: string;
|
||||||
|
organizationId: string;
|
||||||
|
onRefetch: () => void;
|
||||||
|
}) {
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const organizationId = useOrganizationId();
|
|
||||||
const [removeMember, isRemoving] = useMutationWithToasts(removeMemberMutation, {
|
const [removeMember, isRemoving] = useMutationWithToasts(removeMemberMutation, {
|
||||||
successMessage: sprintf(
|
successMessage: __("Member removed successfully"),
|
||||||
__("Member %s removed successfully"),
|
errorMessage: __("Failed to remove member"),
|
||||||
props.membership.fullName
|
|
||||||
),
|
|
||||||
errorMessage: sprintf(__("Failed to remove member %s"), props.membership.fullName),
|
|
||||||
});
|
});
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const [isRemoved, setIsRemoved] = useState(false);
|
const [isRemoved, setIsRemoved] = useState(false);
|
||||||
@@ -909,11 +932,13 @@ function MembershipRow(props: { membership: NodeOf<SettingsPageMembershipsFragme
|
|||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
memberId: props.membership.id,
|
memberId: props.membership.id,
|
||||||
organizationId: organizationId,
|
organizationId: props.organizationId,
|
||||||
},
|
},
|
||||||
|
connections: props.connectionId ? [props.connectionId] : [],
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onCompleted: () => {
|
||||||
setIsRemoved(true);
|
setIsRemoved(true);
|
||||||
|
props.onRefetch();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<4f099d1ee6b4635ca8129eca77d3f7e8>>
|
* @generated SignedSource<<09561c2c29459fc840773b414af6083a>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
|
|
||||||
import { ReaderFragment } from 'relay-runtime';
|
import { ReaderFragment } from 'relay-runtime';
|
||||||
|
export type InvitationStatus = "ACCEPTED" | "EXPIRED" | "PENDING";
|
||||||
import { FragmentRefs } from "relay-runtime";
|
import { FragmentRefs } from "relay-runtime";
|
||||||
export type SettingsPageInvitationsFragment$data = {
|
export type SettingsPageInvitationsFragment$data = {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
@@ -23,6 +24,7 @@ export type SettingsPageInvitationsFragment$data = {
|
|||||||
readonly fullName: string;
|
readonly fullName: string;
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly role: string;
|
readonly role: string;
|
||||||
|
readonly status: InvitationStatus;
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
readonly totalCount: number;
|
readonly totalCount: number;
|
||||||
@@ -171,6 +173,13 @@ return {
|
|||||||
"name": "role",
|
"name": "role",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "status",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
@@ -273,6 +282,6 @@ return {
|
|||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "d971d93653991efde284ed2b87068698";
|
(node as any).hash = "f9a1ec38579cea21312ba0a20bb7394a";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<994eb713978ecef3329fd9873e4c744c>>
|
* @generated SignedSource<<d210161405099a17aa25d06ca4563634>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -14,11 +14,12 @@ export type RemoveMemberInput = {
|
|||||||
organizationId: string;
|
organizationId: string;
|
||||||
};
|
};
|
||||||
export type SettingsPage_RemoveMemberMutation$variables = {
|
export type SettingsPage_RemoveMemberMutation$variables = {
|
||||||
|
connections: ReadonlyArray<string>;
|
||||||
input: RemoveMemberInput;
|
input: RemoveMemberInput;
|
||||||
};
|
};
|
||||||
export type SettingsPage_RemoveMemberMutation$data = {
|
export type SettingsPage_RemoveMemberMutation$data = {
|
||||||
readonly removeMember: {
|
readonly removeMember: {
|
||||||
readonly success: boolean;
|
readonly deletedMemberId: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type SettingsPage_RemoveMemberMutation = {
|
export type SettingsPage_RemoveMemberMutation = {
|
||||||
@@ -27,67 +28,106 @@ export type SettingsPage_RemoveMemberMutation = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
var v0 = [
|
var v0 = {
|
||||||
{
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "connections"
|
||||||
|
},
|
||||||
|
v1 = {
|
||||||
"defaultValue": null,
|
"defaultValue": null,
|
||||||
"kind": "LocalArgument",
|
"kind": "LocalArgument",
|
||||||
"name": "input"
|
"name": "input"
|
||||||
}
|
},
|
||||||
],
|
v2 = [
|
||||||
v1 = [
|
|
||||||
{
|
|
||||||
"alias": null,
|
|
||||||
"args": [
|
|
||||||
{
|
{
|
||||||
"kind": "Variable",
|
"kind": "Variable",
|
||||||
"name": "input",
|
"name": "input",
|
||||||
"variableName": "input"
|
"variableName": "input"
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
v3 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "deletedMemberId",
|
||||||
|
"storageKey": null
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": [
|
||||||
|
(v0/*: any*/),
|
||||||
|
(v1/*: any*/)
|
||||||
],
|
],
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "SettingsPage_RemoveMemberMutation",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v2/*: any*/),
|
||||||
"concreteType": "RemoveMemberPayload",
|
"concreteType": "RemoveMemberPayload",
|
||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "removeMember",
|
"name": "removeMember",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
(v3/*: any*/)
|
||||||
"alias": null,
|
|
||||||
"args": null,
|
|
||||||
"kind": "ScalarField",
|
|
||||||
"name": "success",
|
|
||||||
"storageKey": null
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
}
|
}
|
||||||
];
|
],
|
||||||
return {
|
|
||||||
"fragment": {
|
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
|
||||||
"kind": "Fragment",
|
|
||||||
"metadata": null,
|
|
||||||
"name": "SettingsPage_RemoveMemberMutation",
|
|
||||||
"selections": (v1/*: any*/),
|
|
||||||
"type": "Mutation",
|
"type": "Mutation",
|
||||||
"abstractKey": null
|
"abstractKey": null
|
||||||
},
|
},
|
||||||
"kind": "Request",
|
"kind": "Request",
|
||||||
"operation": {
|
"operation": {
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": [
|
||||||
|
(v1/*: any*/),
|
||||||
|
(v0/*: any*/)
|
||||||
|
],
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "SettingsPage_RemoveMemberMutation",
|
"name": "SettingsPage_RemoveMemberMutation",
|
||||||
"selections": (v1/*: any*/)
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v2/*: any*/),
|
||||||
|
"concreteType": "RemoveMemberPayload",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "removeMember",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v3/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"filters": null,
|
||||||
|
"handle": "deleteEdge",
|
||||||
|
"key": "",
|
||||||
|
"kind": "ScalarHandle",
|
||||||
|
"name": "deletedMemberId",
|
||||||
|
"handleArgs": [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "connections",
|
||||||
|
"variableName": "connections"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "97e29046871ce8aab01abf98a62236fc",
|
"cacheID": "e2dd0f4d7327ce3bc97754c85d3f700d",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "SettingsPage_RemoveMemberMutation",
|
"name": "SettingsPage_RemoveMemberMutation",
|
||||||
"operationKind": "mutation",
|
"operationKind": "mutation",
|
||||||
"text": "mutation SettingsPage_RemoveMemberMutation(\n $input: RemoveMemberInput!\n) {\n removeMember(input: $input) {\n success\n }\n}\n"
|
"text": "mutation SettingsPage_RemoveMemberMutation(\n $input: RemoveMemberInput!\n) {\n removeMember(input: $input) {\n deletedMemberId\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "f61071a0fb6f6554e56e79b6b04bc135";
|
(node as any).hash = "9909a8b95f8d8621ffdf02da34ec8da2";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
|
|||||||
@@ -74,8 +74,8 @@ const routes = [
|
|||||||
Component: lazy(() => import("./pages/auth/ConfirmEmailPage")),
|
Component: lazy(() => import("./pages/auth/ConfirmEmailPage")),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "confirm-invitation",
|
path: "signup-from-invitation",
|
||||||
Component: lazy(() => import("./pages/auth/ConfirmInvitationPage")),
|
Component: lazy(() => import("./pages/auth/SignupFromInvitationPage")),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "forgot-password",
|
path: "forgot-password",
|
||||||
|
|||||||
@@ -30,10 +30,9 @@ export const vendorRoutes = [
|
|||||||
{
|
{
|
||||||
path: "vendors/:vendorId",
|
path: "vendors/:vendorId",
|
||||||
fallback: PageSkeleton,
|
fallback: PageSkeleton,
|
||||||
queryLoader: ({ vendorId, organizationId }) =>
|
queryLoader: ({ vendorId }) =>
|
||||||
loadQuery(relayEnvironment, vendorNodeQuery, {
|
loadQuery(relayEnvironment, vendorNodeQuery, {
|
||||||
vendorId,
|
vendorId,
|
||||||
organizationId,
|
|
||||||
}),
|
}),
|
||||||
Component: lazy(
|
Component: lazy(
|
||||||
() => import("../pages/organizations/vendors/VendorDetailPage")
|
() => import("../pages/organizations/vendors/VendorDetailPage")
|
||||||
@@ -95,10 +94,9 @@ export const vendorRoutes = [
|
|||||||
{
|
{
|
||||||
path: "snapshots/:snapshotId/vendors/:vendorId",
|
path: "snapshots/:snapshotId/vendors/:vendorId",
|
||||||
fallback: PageSkeleton,
|
fallback: PageSkeleton,
|
||||||
queryLoader: ({ vendorId, organizationId }) =>
|
queryLoader: ({ vendorId }) =>
|
||||||
loadQuery(relayEnvironment, vendorNodeQuery, {
|
loadQuery(relayEnvironment, vendorNodeQuery, {
|
||||||
vendorId,
|
vendorId,
|
||||||
organizationId,
|
|
||||||
}),
|
}),
|
||||||
Component: lazy(
|
Component: lazy(
|
||||||
() => import("../pages/organizations/vendors/VendorDetailPage")
|
() => import("../pages/organizations/vendors/VendorDetailPage")
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import logo from "../assets/android-chrome-512x512.png";
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { Outlet } from "react-router";
|
import { Outlet } from "react-router";
|
||||||
|
import { Logo } from "../Atoms/Logo/Logo";
|
||||||
|
|
||||||
export function AuthLayout() {
|
export function AuthLayout() {
|
||||||
const { __ } = useTranslate();
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 min-h-screen text-txt-primary">
|
<div className="grid grid-cols-1 lg:grid-cols-2 min-h-screen text-txt-primary">
|
||||||
<div className="bg-level-0 flex flex-col items-center justify-center">
|
<div className="bg-level-0 flex flex-col items-center justify-center">
|
||||||
@@ -11,13 +9,9 @@ export function AuthLayout() {
|
|||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden lg:flex bg-dialog text-invert text-5xl font-bold flex flex-col items-center justify-center p-8 text-txt-primary lg:p-10">
|
<div className="hidden lg:flex bg-dialog font-bold flex flex-col items-center justify-center p-8 text-txt-primary lg:p-10">
|
||||||
<div className="flex flex-col 2xl:flex-row-reverse items-center justify-center gap-4">
|
<div className="flex flex-col items-center justify-center gap-4">
|
||||||
<img src={logo} alt="Probo logo" className="h-auto w-96" />
|
<Logo withPicto className="w-[440px]" />
|
||||||
<span>
|
|
||||||
{__("Navigate compliance with confidence thanks to")}
|
|
||||||
<span className="text-txt-accent"> probo</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -600,3 +600,107 @@ func (s Service) ResetPassword(ctx context.Context, tokenString string, newPassw
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s Service) SignupFromInvitation(
|
||||||
|
ctx context.Context,
|
||||||
|
token string,
|
||||||
|
password string,
|
||||||
|
fullName string,
|
||||||
|
) (*coredata.User, *coredata.Session, error) {
|
||||||
|
payload, err := statelesstoken.ValidateToken[coredata.InvitationData](
|
||||||
|
s.tokenSecret,
|
||||||
|
"organization_invitation",
|
||||||
|
token,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, &ErrInvalidTokenType{"invalid invitation token"}
|
||||||
|
}
|
||||||
|
invitationData := payload.Data
|
||||||
|
|
||||||
|
if len(password) < 8 || len(password) > 128 {
|
||||||
|
return nil, nil, &ErrInvalidPassword{minLength: 8, maxLength: 128}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := mail.ParseAddress(invitationData.Email); err != nil {
|
||||||
|
return nil, nil, &ErrInvalidEmail{invitationData.Email}
|
||||||
|
}
|
||||||
|
|
||||||
|
if fullName == "" {
|
||||||
|
fullName = invitationData.FullName
|
||||||
|
}
|
||||||
|
|
||||||
|
if fullName == "" {
|
||||||
|
return nil, nil, &ErrInvalidFullName{fullName}
|
||||||
|
}
|
||||||
|
|
||||||
|
hashedPassword, err := s.hp.HashPassword([]byte(password))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("cannot hash password: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var user *coredata.User
|
||||||
|
var session *coredata.Session
|
||||||
|
|
||||||
|
scope := coredata.NewScope(invitationData.InvitationID.TenantID())
|
||||||
|
err = s.pg.WithTx(
|
||||||
|
ctx,
|
||||||
|
func(tx pg.Conn) error {
|
||||||
|
invitation := &coredata.Invitation{}
|
||||||
|
if err := invitation.LoadByID(ctx, tx, scope, invitationData.InvitationID); err != nil {
|
||||||
|
var errInvitationNotFound *coredata.ErrInvitationNotFound
|
||||||
|
if errors.As(err, &errInvitationNotFound) {
|
||||||
|
return fmt.Errorf("invitation was deleted or no longer exists")
|
||||||
|
}
|
||||||
|
return fmt.Errorf("cannot load invitation: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if invitation.AcceptedAt != nil {
|
||||||
|
return fmt.Errorf("invitation already accepted")
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Now().After(invitation.ExpiresAt) {
|
||||||
|
return fmt.Errorf("invitation expired")
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
user = &coredata.User{
|
||||||
|
ID: gid.New(gid.NilTenant, coredata.UserEntityType),
|
||||||
|
EmailAddress: invitationData.Email,
|
||||||
|
HashedPassword: hashedPassword,
|
||||||
|
EmailAddressVerified: true,
|
||||||
|
FullName: fullName,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.Insert(ctx, tx); err != nil {
|
||||||
|
var errUserAlreadyExists *coredata.ErrUserAlreadyExists
|
||||||
|
if errors.As(err, &errUserAlreadyExists) {
|
||||||
|
return &ErrUserAlreadyExists{errUserAlreadyExists.Error()}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("cannot insert user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
session = &coredata.Session{
|
||||||
|
ID: gid.New(gid.NilTenant, coredata.SessionEntityType),
|
||||||
|
UserID: user.ID,
|
||||||
|
Data: coredata.SessionData{},
|
||||||
|
ExpiredAt: now.Add(24 * time.Hour * 7),
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := session.Insert(ctx, tx); err != nil {
|
||||||
|
return fmt.Errorf("cannot insert session: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, session, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ import (
|
|||||||
_ "embed"
|
_ "embed"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"net/url"
|
||||||
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/getprobo/probo/pkg/coredata"
|
"github.com/getprobo/probo/pkg/coredata"
|
||||||
@@ -41,6 +42,14 @@ type (
|
|||||||
invitationTokenValidity time.Duration
|
invitationTokenValidity time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TenantAuthzService struct {
|
||||||
|
pg *pg.Client
|
||||||
|
hostname string
|
||||||
|
tokenSecret string
|
||||||
|
invitationTokenValidity time.Duration
|
||||||
|
scope coredata.Scoper
|
||||||
|
}
|
||||||
|
|
||||||
Role string
|
Role string
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -78,6 +87,18 @@ func NewService(
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) WithTenant(tenantID gid.TenantID) *TenantAuthzService {
|
||||||
|
return &TenantAuthzService{
|
||||||
|
pg: s.pg,
|
||||||
|
hostname: s.hostname,
|
||||||
|
tokenSecret: s.tokenSecret,
|
||||||
|
invitationTokenValidity: s.invitationTokenValidity,
|
||||||
|
scope: coredata.NewScope(tenantID),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This method is on Service (not TenantAuthzService) because it operates across tenants
|
||||||
|
// and doesn't require tenant-scoped access.
|
||||||
func (s *Service) GetAllUserOrganizations(
|
func (s *Service) GetAllUserOrganizations(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
userID gid.GID,
|
userID gid.GID,
|
||||||
@@ -98,6 +119,8 @@ func (s *Service) GetAllUserOrganizations(
|
|||||||
return organizations, err
|
return organizations, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This method is on Service (not TenantAuthzService) because it operates across tenants
|
||||||
|
// and doesn't require tenant-scoped access.
|
||||||
func (s *Service) GetUserOrganizations(
|
func (s *Service) GetUserOrganizations(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
userID gid.GID,
|
userID gid.GID,
|
||||||
@@ -115,7 +138,253 @@ func (s *Service) GetUserOrganizations(
|
|||||||
return organizations, err
|
return organizations, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetAllOrganizationInvitations(
|
// This method is on Service (not TenantAuthzService) because the user accepting
|
||||||
|
// the invitation doesn't have tenant access yet.
|
||||||
|
func (s *Service) AcceptInvitation(
|
||||||
|
ctx context.Context,
|
||||||
|
token string,
|
||||||
|
userID gid.GID,
|
||||||
|
) error {
|
||||||
|
payload, err := statelesstoken.ValidateToken[coredata.InvitationData](
|
||||||
|
s.tokenSecret,
|
||||||
|
TokenTypeOrganizationInvitation,
|
||||||
|
token,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid invitation token: %w", err)
|
||||||
|
}
|
||||||
|
invitationData := payload.Data
|
||||||
|
scope := coredata.NewScope(invitationData.InvitationID.TenantID())
|
||||||
|
|
||||||
|
return s.pg.WithTx(
|
||||||
|
ctx,
|
||||||
|
func(tx pg.Conn) error {
|
||||||
|
invitation := &coredata.Invitation{}
|
||||||
|
if err := invitation.LoadByID(ctx, tx, scope, invitationData.InvitationID); err != nil {
|
||||||
|
var errInvitationNotFound *coredata.ErrInvitationNotFound
|
||||||
|
if errors.As(err, &errInvitationNotFound) {
|
||||||
|
return fmt.Errorf("invitation was deleted or no longer exists")
|
||||||
|
}
|
||||||
|
return fmt.Errorf("cannot load invitation: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if invitation.AcceptedAt != nil {
|
||||||
|
return fmt.Errorf("invitation already accepted")
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Now().After(invitation.ExpiresAt) {
|
||||||
|
return fmt.Errorf("invitation expired")
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
membershipID := gid.New(scope.GetTenantID(), coredata.MembershipEntityType)
|
||||||
|
|
||||||
|
membership := &coredata.Membership{
|
||||||
|
ID: membershipID,
|
||||||
|
UserID: userID,
|
||||||
|
OrganizationID: invitation.OrganizationID,
|
||||||
|
Role: invitation.Role,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := membership.Create(ctx, tx, scope); err != nil {
|
||||||
|
return fmt.Errorf("failed to add user to organization: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
invitation.AcceptedAt = &now
|
||||||
|
if err := invitation.Update(ctx, tx, scope); err != nil {
|
||||||
|
return fmt.Errorf("failed to mark invitation as accepted: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This method is on Service (not TenantAuthzService) because the user accepting
|
||||||
|
// the invitation doesn't have tenant access yet.
|
||||||
|
func (s *Service) AcceptInvitationByID(
|
||||||
|
ctx context.Context,
|
||||||
|
invitationID gid.GID,
|
||||||
|
userID gid.GID,
|
||||||
|
) (*coredata.Invitation, error) {
|
||||||
|
var acceptedInvitation *coredata.Invitation
|
||||||
|
scope := coredata.NewScope(invitationID.TenantID())
|
||||||
|
err := s.pg.WithTx(
|
||||||
|
ctx,
|
||||||
|
func(tx pg.Conn) error {
|
||||||
|
invitation := &coredata.Invitation{}
|
||||||
|
if err := invitation.LoadByID(ctx, tx, scope, invitationID); err != nil {
|
||||||
|
var errInvitationNotFound *coredata.ErrInvitationNotFound
|
||||||
|
if errors.As(err, &errInvitationNotFound) {
|
||||||
|
return fmt.Errorf("invitation was deleted or no longer exists")
|
||||||
|
}
|
||||||
|
return fmt.Errorf("cannot load invitation: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if invitation.AcceptedAt != nil {
|
||||||
|
return fmt.Errorf("invitation already accepted")
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Now().After(invitation.ExpiresAt) {
|
||||||
|
return fmt.Errorf("invitation expired")
|
||||||
|
}
|
||||||
|
|
||||||
|
user := &coredata.User{}
|
||||||
|
if err := user.LoadByID(ctx, tx, userID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if invitation.Email != user.EmailAddress {
|
||||||
|
return fmt.Errorf("invitation email does not match user email")
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
membershipID := gid.New(scope.GetTenantID(), coredata.MembershipEntityType)
|
||||||
|
|
||||||
|
membership := &coredata.Membership{
|
||||||
|
ID: membershipID,
|
||||||
|
UserID: userID,
|
||||||
|
OrganizationID: invitation.OrganizationID,
|
||||||
|
Role: invitation.Role,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := membership.Create(ctx, tx, scope); err != nil {
|
||||||
|
return fmt.Errorf("failed to add user to organization: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
invitation.AcceptedAt = &now
|
||||||
|
if err := invitation.Update(ctx, tx, scope); err != nil {
|
||||||
|
return fmt.Errorf("failed to mark invitation as accepted: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
acceptedInvitation = invitation
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return acceptedInvitation, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This method is on Service (not TenantAuthzService) because the user viewing
|
||||||
|
// their invitations doesn't have tenant access yet, and it operates across multiple tenants.
|
||||||
|
func (s *Service) GetUserInvitations(
|
||||||
|
ctx context.Context,
|
||||||
|
email string,
|
||||||
|
cursor *page.Cursor[coredata.InvitationOrderField],
|
||||||
|
filter *coredata.InvitationFilter,
|
||||||
|
) (*page.Page[*coredata.Invitation, coredata.InvitationOrderField], error) {
|
||||||
|
var invitations coredata.Invitations
|
||||||
|
|
||||||
|
err := s.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
if err := invitations.LoadByEmail(ctx, conn, email, cursor, filter); err != nil {
|
||||||
|
return fmt.Errorf("failed to load invitations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return page.NewPage(invitations, cursor), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This method is on Service (not TenantAuthzService) because the user viewing
|
||||||
|
// their invitations doesn't have tenant access yet, and it operates across multiple tenants.
|
||||||
|
func (s *Service) CountUserInvitations(
|
||||||
|
ctx context.Context,
|
||||||
|
email string,
|
||||||
|
filter *coredata.InvitationFilter,
|
||||||
|
) (int, error) {
|
||||||
|
var count int
|
||||||
|
|
||||||
|
err := s.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
var invitations coredata.Invitations
|
||||||
|
var err error
|
||||||
|
count, err = invitations.CountByEmail(ctx, conn, email, filter)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// This method is on Service (not TenantAuthzService) because the user viewing
|
||||||
|
// the invitation organization doesn't have tenant access yet.
|
||||||
|
func (s *Service) GetOrganizationByInvitationID(
|
||||||
|
ctx context.Context,
|
||||||
|
invitationID gid.GID,
|
||||||
|
) (*coredata.Organization, error) {
|
||||||
|
scope := coredata.NewScope(invitationID.TenantID())
|
||||||
|
|
||||||
|
var organization coredata.Organization
|
||||||
|
err := s.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
var invitation coredata.Invitation
|
||||||
|
if err := invitation.LoadByID(ctx, conn, scope, invitationID); err != nil {
|
||||||
|
return fmt.Errorf("failed to load invitation: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := organization.LoadByID(ctx, conn, scope, invitation.OrganizationID); err != nil {
|
||||||
|
return fmt.Errorf("failed to load organization: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &organization, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This method is on Service (not TenantAuthzService) because the user added to the organization
|
||||||
|
// doesn't have tenant access yet
|
||||||
|
func (s *Service) AddUserToOrganization(
|
||||||
|
ctx context.Context,
|
||||||
|
userID gid.GID,
|
||||||
|
orgID gid.GID,
|
||||||
|
role string,
|
||||||
|
) error {
|
||||||
|
now := time.Now()
|
||||||
|
tenantID := orgID.TenantID()
|
||||||
|
membershipID := gid.New(tenantID, coredata.MembershipEntityType)
|
||||||
|
scope := coredata.NewScope(tenantID)
|
||||||
|
|
||||||
|
membership := &coredata.Membership{
|
||||||
|
ID: membershipID,
|
||||||
|
UserID: userID,
|
||||||
|
OrganizationID: orgID,
|
||||||
|
Role: role,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
if err := membership.Create(ctx, conn, scope); err != nil {
|
||||||
|
return fmt.Errorf("failed to add user to organization: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TenantAuthzService) GetInvitationsByOrganizationID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
orgID gid.GID,
|
orgID gid.GID,
|
||||||
cursor *page.Cursor[coredata.InvitationOrderField],
|
cursor *page.Cursor[coredata.InvitationOrderField],
|
||||||
@@ -125,7 +394,7 @@ func (s *Service) GetAllOrganizationInvitations(
|
|||||||
err := s.pg.WithConn(
|
err := s.pg.WithConn(
|
||||||
ctx,
|
ctx,
|
||||||
func(conn pg.Conn) error {
|
func(conn pg.Conn) error {
|
||||||
if err := invitations.LoadByOrganizationID(ctx, conn, orgID, cursor); err != nil {
|
if err := invitations.LoadByOrganizationID(ctx, conn, s.scope, orgID, cursor); err != nil {
|
||||||
return fmt.Errorf("failed to load organization invitations: %w", err)
|
return fmt.Errorf("failed to load organization invitations: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +408,7 @@ func (s *Service) GetAllOrganizationInvitations(
|
|||||||
return page.NewPage(invitations, cursor), nil
|
return page.NewPage(invitations, cursor), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) CountOrganizationInvitations(
|
func (s *TenantAuthzService) CountOrganizationInvitations(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
orgID gid.GID,
|
orgID gid.GID,
|
||||||
) (int, error) {
|
) (int, error) {
|
||||||
@@ -149,7 +418,7 @@ func (s *Service) CountOrganizationInvitations(
|
|||||||
func(conn pg.Conn) error {
|
func(conn pg.Conn) error {
|
||||||
var invitations coredata.Invitations
|
var invitations coredata.Invitations
|
||||||
var err error
|
var err error
|
||||||
count, err = invitations.CountByOrganizationID(ctx, conn, orgID)
|
count, err = invitations.CountByOrganizationID(ctx, conn, s.scope, orgID)
|
||||||
return err
|
return err
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -160,7 +429,27 @@ func (s *Service) CountOrganizationInvitations(
|
|||||||
return count, nil
|
return count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) DeleteInvitation(
|
func (s *TenantAuthzService) GetInvitationByID(
|
||||||
|
ctx context.Context,
|
||||||
|
invitationID gid.GID,
|
||||||
|
) (*coredata.Invitation, error) {
|
||||||
|
invitation := &coredata.Invitation{}
|
||||||
|
err := s.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
if err := invitation.LoadByID(ctx, conn, s.scope, invitationID); err != nil {
|
||||||
|
return fmt.Errorf("failed to load invitation: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return invitation, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TenantAuthzService) DeleteInvitation(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
invitationID gid.GID,
|
invitationID gid.GID,
|
||||||
) error {
|
) error {
|
||||||
@@ -168,11 +457,11 @@ func (s *Service) DeleteInvitation(
|
|||||||
ctx,
|
ctx,
|
||||||
func(conn pg.Conn) error {
|
func(conn pg.Conn) error {
|
||||||
invitation := &coredata.Invitation{}
|
invitation := &coredata.Invitation{}
|
||||||
if err := invitation.LoadByID(ctx, conn, invitationID); err != nil {
|
if err := invitation.LoadByID(ctx, conn, s.scope, invitationID); err != nil {
|
||||||
return fmt.Errorf("failed to load invitation: %w", err)
|
return fmt.Errorf("failed to load invitation: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := invitation.Delete(ctx, conn); err != nil {
|
if err := invitation.Delete(ctx, conn, s.scope); err != nil {
|
||||||
return fmt.Errorf("failed to delete invitation: %w", err)
|
return fmt.Errorf("failed to delete invitation: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +470,7 @@ func (s *Service) DeleteInvitation(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetAllOrganizationMemberships(
|
func (s *TenantAuthzService) GetMembershipsByOrganizationID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
orgID gid.GID,
|
orgID gid.GID,
|
||||||
cursor *page.Cursor[coredata.MembershipOrderField],
|
cursor *page.Cursor[coredata.MembershipOrderField],
|
||||||
@@ -191,7 +480,7 @@ func (s *Service) GetAllOrganizationMemberships(
|
|||||||
err := s.pg.WithConn(
|
err := s.pg.WithConn(
|
||||||
ctx,
|
ctx,
|
||||||
func(conn pg.Conn) error {
|
func(conn pg.Conn) error {
|
||||||
if err := memberships.LoadByOrganizationID(ctx, conn, orgID, cursor); err != nil {
|
if err := memberships.LoadByOrganizationID(ctx, conn, s.scope, orgID, cursor); err != nil {
|
||||||
return fmt.Errorf("failed to load organization memberships: %w", err)
|
return fmt.Errorf("failed to load organization memberships: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,7 +494,7 @@ func (s *Service) GetAllOrganizationMemberships(
|
|||||||
return page.NewPage(memberships, cursor), nil
|
return page.NewPage(memberships, cursor), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) CountOrganizationMemberships(
|
func (s *TenantAuthzService) CountOrganizationMemberships(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
orgID gid.GID,
|
orgID gid.GID,
|
||||||
) (int, error) {
|
) (int, error) {
|
||||||
@@ -215,7 +504,7 @@ func (s *Service) CountOrganizationMemberships(
|
|||||||
func(conn pg.Conn) error {
|
func(conn pg.Conn) error {
|
||||||
var memberships coredata.Memberships
|
var memberships coredata.Memberships
|
||||||
var err error
|
var err error
|
||||||
count, err = memberships.CountByOrganizationID(ctx, conn, orgID)
|
count, err = memberships.CountByOrganizationID(ctx, conn, s.scope, orgID)
|
||||||
return err
|
return err
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -226,7 +515,28 @@ func (s *Service) CountOrganizationMemberships(
|
|||||||
return count, nil
|
return count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) CanUserAccessOrganization(
|
func (s *TenantAuthzService) CountOrganizationUsers(
|
||||||
|
ctx context.Context,
|
||||||
|
orgID gid.GID,
|
||||||
|
) (int, error) {
|
||||||
|
var count int
|
||||||
|
err := s.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
var users coredata.Users
|
||||||
|
var err error
|
||||||
|
count, err = users.CountByOrganizationID(ctx, conn, s.scope, orgID)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to count users: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TenantAuthzService) CanUserAccessOrganization(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
userID gid.GID,
|
userID gid.GID,
|
||||||
orgID gid.GID,
|
orgID gid.GID,
|
||||||
@@ -238,7 +548,7 @@ func (s *Service) CanUserAccessOrganization(
|
|||||||
err := s.pg.WithConn(
|
err := s.pg.WithConn(
|
||||||
ctx,
|
ctx,
|
||||||
func(conn pg.Conn) error {
|
func(conn pg.Conn) error {
|
||||||
if err := membership.LoadByUserAndOrg(ctx, conn, userID, orgID); err != nil {
|
if err := membership.LoadByUserAndOrg(ctx, conn, s.scope, userID, orgID); err != nil {
|
||||||
if _, ok := err.(coredata.ErrMembershipNotFound); ok {
|
if _, ok := err.(coredata.ErrMembershipNotFound); ok {
|
||||||
return nil // Not an error, just no access
|
return nil // Not an error, just no access
|
||||||
}
|
}
|
||||||
@@ -256,7 +566,7 @@ func (s *Service) CanUserAccessOrganization(
|
|||||||
return haveAccess, nil
|
return haveAccess, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetUserRoleInOrganization(
|
func (s *TenantAuthzService) GetUserRoleInOrganization(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
userID gid.GID,
|
userID gid.GID,
|
||||||
orgID gid.GID,
|
orgID gid.GID,
|
||||||
@@ -266,7 +576,7 @@ func (s *Service) GetUserRoleInOrganization(
|
|||||||
err := s.pg.WithConn(
|
err := s.pg.WithConn(
|
||||||
ctx,
|
ctx,
|
||||||
func(conn pg.Conn) error {
|
func(conn pg.Conn) error {
|
||||||
if err := membership.LoadByUserAndOrg(ctx, conn, userID, orgID); err != nil {
|
if err := membership.LoadByUserAndOrg(ctx, conn, s.scope, userID, orgID); err != nil {
|
||||||
return fmt.Errorf("failed to get user role: %w", err)
|
return fmt.Errorf("failed to get user role: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -280,7 +590,7 @@ func (s *Service) GetUserRoleInOrganization(
|
|||||||
return membership.Role, nil
|
return membership.Role, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) RemoveMemberFromOrganization(
|
func (s *TenantAuthzService) RemoveMemberFromOrganization(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
orgID gid.GID,
|
orgID gid.GID,
|
||||||
memberID gid.GID,
|
memberID gid.GID,
|
||||||
@@ -290,7 +600,7 @@ func (s *Service) RemoveMemberFromOrganization(
|
|||||||
return s.pg.WithTx(
|
return s.pg.WithTx(
|
||||||
ctx,
|
ctx,
|
||||||
func(tx pg.Conn) error {
|
func(tx pg.Conn) error {
|
||||||
if err := membership.LoadByID(ctx, tx, memberID); err != nil {
|
if err := membership.LoadByID(ctx, tx, s.scope, memberID); err != nil {
|
||||||
return fmt.Errorf("failed to load membership: %w", err)
|
return fmt.Errorf("failed to load membership: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,7 +608,7 @@ func (s *Service) RemoveMemberFromOrganization(
|
|||||||
return fmt.Errorf("membership does not belong to organization")
|
return fmt.Errorf("membership does not belong to organization")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := membership.Delete(ctx, tx); err != nil {
|
if err := membership.Delete(ctx, tx, s.scope); err != nil {
|
||||||
return fmt.Errorf("failed to delete membership: %w", err)
|
return fmt.Errorf("failed to delete membership: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,32 +617,7 @@ func (s *Service) RemoveMemberFromOrganization(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) AddUserToOrganization(
|
func (s *TenantAuthzService) UpdateUserRole(
|
||||||
ctx context.Context,
|
|
||||||
userID gid.GID,
|
|
||||||
orgID gid.GID,
|
|
||||||
role string,
|
|
||||||
) error {
|
|
||||||
membership := &coredata.Membership{
|
|
||||||
UserID: userID,
|
|
||||||
OrganizationID: orgID,
|
|
||||||
Role: role,
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
UpdatedAt: time.Now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.pg.WithConn(
|
|
||||||
ctx,
|
|
||||||
func(conn pg.Conn) error {
|
|
||||||
if err := membership.Create(ctx, conn); err != nil {
|
|
||||||
return fmt.Errorf("failed to add user to organization: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) UpdateUserRole(
|
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
userID gid.GID,
|
userID gid.GID,
|
||||||
orgID gid.GID,
|
orgID gid.GID,
|
||||||
@@ -342,14 +627,14 @@ func (s *Service) UpdateUserRole(
|
|||||||
ctx,
|
ctx,
|
||||||
func(tx pg.Conn) error {
|
func(tx pg.Conn) error {
|
||||||
membership := &coredata.Membership{}
|
membership := &coredata.Membership{}
|
||||||
if err := membership.LoadByUserAndOrg(ctx, tx, userID, orgID); err != nil {
|
if err := membership.LoadByUserAndOrg(ctx, tx, s.scope, userID, orgID); err != nil {
|
||||||
return fmt.Errorf("failed to find membership: %w", err)
|
return fmt.Errorf("failed to find membership: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
membership.Role = newRole
|
membership.Role = newRole
|
||||||
membership.UpdatedAt = time.Now()
|
membership.UpdatedAt = time.Now()
|
||||||
|
|
||||||
if err := membership.Update(ctx, tx); err != nil {
|
if err := membership.Update(ctx, tx, s.scope); err != nil {
|
||||||
return fmt.Errorf("failed to update user role: %w", err)
|
return fmt.Errorf("failed to update user role: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,7 +643,7 @@ func (s *Service) UpdateUserRole(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) InviteUserToOrganization(
|
func (s *TenantAuthzService) InviteUserToOrganization(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
organizationID gid.GID,
|
organizationID gid.GID,
|
||||||
emailAddress string,
|
emailAddress string,
|
||||||
@@ -380,12 +665,11 @@ func (s *Service) InviteUserToOrganization(
|
|||||||
}
|
}
|
||||||
|
|
||||||
organization := &coredata.Organization{}
|
organization := &coredata.Organization{}
|
||||||
scope := coredata.NewScope(organizationID.TenantID())
|
if err := organization.LoadByID(ctx, tx, s.scope, organizationID); err != nil {
|
||||||
if err := organization.LoadByID(ctx, tx, scope, organizationID); err != nil {
|
|
||||||
return fmt.Errorf("failed to load organization: %w", err)
|
return fmt.Errorf("failed to load organization: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
invitationID := gid.New(organizationID.TenantID(), coredata.InvitationEntityType)
|
invitationID := gid.New(s.scope.GetTenantID(), coredata.InvitationEntityType)
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
invitation = &coredata.Invitation{
|
invitation = &coredata.Invitation{
|
||||||
ID: invitationID,
|
ID: invitationID,
|
||||||
@@ -397,19 +681,20 @@ func (s *Service) InviteUserToOrganization(
|
|||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body := bytes.NewBuffer(nil)
|
||||||
|
var err error
|
||||||
if userExists {
|
if userExists {
|
||||||
membership := &coredata.Membership{
|
err = invitationEmailBodyTemplate.Execute(
|
||||||
UserID: user.ID,
|
body,
|
||||||
OrganizationID: organizationID,
|
map[string]string{
|
||||||
Role: role,
|
"FullName": user.FullName,
|
||||||
CreatedAt: now,
|
"OrganizationName": organization.Name,
|
||||||
UpdatedAt: now,
|
"InvitationURL": fmt.Sprintf("https://%s/", s.hostname),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to execute template: %w", err)
|
||||||
}
|
}
|
||||||
if err := membership.Create(ctx, tx); err != nil {
|
|
||||||
return fmt.Errorf("failed to add user to organization: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
invitation.AcceptedAt = &now
|
|
||||||
} else {
|
} else {
|
||||||
invitationData := coredata.InvitationData{
|
invitationData := coredata.InvitationData{
|
||||||
InvitationID: invitationID,
|
InvitationID: invitationID,
|
||||||
@@ -429,18 +714,18 @@ func (s *Service) InviteUserToOrganization(
|
|||||||
return fmt.Errorf("failed to generate invitation token: %w", err)
|
return fmt.Errorf("failed to generate invitation token: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
body := bytes.NewBuffer(nil)
|
|
||||||
err = invitationEmailBodyTemplate.Execute(
|
err = invitationEmailBodyTemplate.Execute(
|
||||||
body,
|
body,
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"FullName": fullName,
|
"FullName": fullName,
|
||||||
"OrganizationName": organization.Name,
|
"OrganizationName": organization.Name,
|
||||||
"InvitationURL": fmt.Sprintf("https://%s/auth/confirm-invitation?token=%s", s.hostname, invitationToken),
|
"InvitationURL": fmt.Sprintf("https://%s/auth/signup-from-invitation?token=%s&fullName=%s", s.hostname, invitationToken, url.QueryEscape(fullName)),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to execute template: %w", err)
|
return fmt.Errorf("failed to execute template: %w", err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
email := coredata.NewEmail(
|
email := coredata.NewEmail(
|
||||||
fullName,
|
fullName,
|
||||||
@@ -452,9 +737,8 @@ func (s *Service) InviteUserToOrganization(
|
|||||||
if err := email.Insert(ctx, tx); err != nil {
|
if err := email.Insert(ctx, tx); err != nil {
|
||||||
return fmt.Errorf("cannot insert email: %w", err)
|
return fmt.Errorf("cannot insert email: %w", err)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if err := invitation.Create(ctx, tx); err != nil {
|
if err := invitation.Create(ctx, tx, s.scope); err != nil {
|
||||||
return fmt.Errorf("cannot create invitation: %w", err)
|
return fmt.Errorf("cannot create invitation: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -468,66 +752,8 @@ func (s *Service) InviteUserToOrganization(
|
|||||||
return invitation, nil
|
return invitation, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) AcceptInvitation(
|
|
||||||
ctx context.Context,
|
|
||||||
token string,
|
|
||||||
userID gid.GID,
|
|
||||||
) error {
|
|
||||||
payload, err := statelesstoken.ValidateToken[coredata.InvitationData](
|
|
||||||
s.tokenSecret,
|
|
||||||
TokenTypeOrganizationInvitation,
|
|
||||||
token,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid invitation token: %w", err)
|
|
||||||
}
|
|
||||||
invitationData := payload.Data
|
|
||||||
|
|
||||||
return s.pg.WithTx(
|
|
||||||
ctx,
|
|
||||||
func(tx pg.Conn) error {
|
|
||||||
invitation := &coredata.Invitation{}
|
|
||||||
if err := invitation.LoadByID(ctx, tx, invitationData.InvitationID); err != nil {
|
|
||||||
var errInvitationNotFound *coredata.ErrInvitationNotFound
|
|
||||||
if errors.As(err, &errInvitationNotFound) {
|
|
||||||
return fmt.Errorf("invitation was deleted or no longer exists")
|
|
||||||
}
|
|
||||||
return fmt.Errorf("cannot load invitation: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if invitation.AcceptedAt != nil {
|
|
||||||
return fmt.Errorf("invitation already accepted")
|
|
||||||
}
|
|
||||||
|
|
||||||
if time.Now().After(invitation.ExpiresAt) {
|
|
||||||
return fmt.Errorf("invitation expired")
|
|
||||||
}
|
|
||||||
|
|
||||||
membership := &coredata.Membership{
|
|
||||||
UserID: userID,
|
|
||||||
OrganizationID: invitation.OrganizationID,
|
|
||||||
Role: invitation.Role,
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
UpdatedAt: time.Now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := membership.Create(ctx, tx); err != nil {
|
|
||||||
return fmt.Errorf("failed to add user to organization: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
invitation.AcceptedAt = &now
|
|
||||||
if err := invitation.Update(ctx, tx); err != nil {
|
|
||||||
return fmt.Errorf("failed to mark invitation as accepted: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is a placeholder for future permission system
|
// This is a placeholder for future permission system
|
||||||
func (s *Service) HasPermission(
|
func (s *TenantAuthzService) HasPermission(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
userID gid.GID,
|
userID gid.GID,
|
||||||
orgID gid.GID,
|
orgID gid.GID,
|
||||||
@@ -538,22 +764,3 @@ func (s *Service) HasPermission(
|
|||||||
// In the future, this will check specific permissions based on role
|
// In the future, this will check specific permissions based on role
|
||||||
return s.CanUserAccessOrganization(ctx, userID, orgID)
|
return s.CanUserAccessOrganization(ctx, userID, orgID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) ListUserInvitations(
|
|
||||||
ctx context.Context,
|
|
||||||
email string,
|
|
||||||
) ([]*coredata.Invitation, error) {
|
|
||||||
var invitations coredata.Invitations
|
|
||||||
|
|
||||||
err := s.pg.WithConn(
|
|
||||||
ctx,
|
|
||||||
func(conn pg.Conn) error {
|
|
||||||
if err := invitations.LoadByEmail(ctx, conn, email); err != nil {
|
|
||||||
return fmt.Errorf("failed to load invitations: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return invitations, err
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -163,7 +163,6 @@ JOIN snapshot_vendors sv ON sv.source_id = av.vendor_id
|
|||||||
query = fmt.Sprintf(query, scope.SQLFragment())
|
query = fmt.Sprintf(query, scope.SQLFragment())
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"tenant_id": scope.GetTenantID(),
|
|
||||||
"snapshot_id": snapshotID,
|
"snapshot_id": snapshotID,
|
||||||
"organization_id": organizationID,
|
"organization_id": organizationID,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,7 +158,6 @@ JOIN snapshot_vendors sv ON sv.source_id = dv.vendor_id
|
|||||||
query = fmt.Sprintf(query, scope.SQLFragment())
|
query = fmt.Sprintf(query, scope.SQLFragment())
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"tenant_id": scope.GetTenantID(),
|
|
||||||
"snapshot_id": snapshotID,
|
"snapshot_id": snapshotID,
|
||||||
"organization_id": organizationID,
|
"organization_id": organizationID,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,17 +81,17 @@ func (i Invitation) CursorKey(orderBy InvitationOrderField) page.CursorKey {
|
|||||||
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
|
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tenant id scope is not applied because invitations are managed at the organization level and don't require tenant isolation.
|
func (i *Invitation) Create(ctx context.Context, conn pg.Conn, scope Scoper) error {
|
||||||
func (i *Invitation) Create(ctx context.Context, conn pg.Conn) error {
|
|
||||||
query := `
|
query := `
|
||||||
INSERT INTO authz_invitations (
|
INSERT INTO authz_invitations (
|
||||||
id, organization_id, email, full_name, role, expires_at, created_at
|
tenant_id, id, organization_id, email, full_name, role, expires_at, created_at
|
||||||
) VALUES (
|
) VALUES (
|
||||||
@id, @organization_id, @email, @full_name, @role, @expires_at, @created_at
|
@tenant_id, @id, @organization_id, @email, @full_name, @role, @expires_at, @created_at
|
||||||
)
|
)
|
||||||
`
|
`
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
|
"tenant_id": scope.GetTenantID(),
|
||||||
"id": i.ID,
|
"id": i.ID,
|
||||||
"organization_id": i.OrganizationID,
|
"organization_id": i.OrganizationID,
|
||||||
"email": i.Email,
|
"email": i.Email,
|
||||||
@@ -109,21 +109,24 @@ func (i *Invitation) Create(ctx context.Context, conn pg.Conn) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tenant id scope is not applied because we want to access invitations across all tenants for authentication purposes.
|
|
||||||
func (i *Invitation) LoadByID(
|
func (i *Invitation) LoadByID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
id gid.GID,
|
id gid.GID,
|
||||||
) error {
|
) error {
|
||||||
query := `
|
query := `
|
||||||
SELECT id, organization_id, email, full_name, role, expires_at, accepted_at, created_at
|
SELECT id, organization_id, email, full_name, role, expires_at, accepted_at, created_at
|
||||||
FROM authz_invitations
|
FROM authz_invitations
|
||||||
WHERE id = @id
|
WHERE id = @id AND %s
|
||||||
`
|
`
|
||||||
|
|
||||||
|
query = fmt.Sprintf(query, scope.SQLFragment())
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"id": id,
|
"id": id,
|
||||||
}
|
}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
rows, err := conn.Query(ctx, query, args)
|
rows, err := conn.Query(ctx, query, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -142,18 +145,20 @@ func (i *Invitation) LoadByID(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tenant id scope is not applied because invitations are managed at the organization level and don't require tenant isolation.
|
func (i *Invitation) Update(ctx context.Context, conn pg.Conn, scope Scoper) error {
|
||||||
func (i *Invitation) Update(ctx context.Context, conn pg.Conn) error {
|
|
||||||
query := `
|
query := `
|
||||||
UPDATE authz_invitations
|
UPDATE authz_invitations
|
||||||
SET accepted_at = @accepted_at
|
SET accepted_at = @accepted_at
|
||||||
WHERE id = @id
|
WHERE id = @id AND %s
|
||||||
`
|
`
|
||||||
|
|
||||||
|
query = fmt.Sprintf(query, scope.SQLFragment())
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"id": i.ID,
|
"id": i.ID,
|
||||||
"accepted_at": i.AcceptedAt,
|
"accepted_at": i.AcceptedAt,
|
||||||
}
|
}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
result, err := conn.Exec(ctx, query, args)
|
result, err := conn.Exec(ctx, query, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -167,16 +172,18 @@ func (i *Invitation) Update(ctx context.Context, conn pg.Conn) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tenant id scope is not applied because invitations are managed at the organization level and don't require tenant isolation.
|
func (i *Invitation) Delete(ctx context.Context, conn pg.Conn, scope Scoper) error {
|
||||||
func (i *Invitation) Delete(ctx context.Context, conn pg.Conn) error {
|
|
||||||
query := `
|
query := `
|
||||||
DELETE FROM authz_invitations
|
DELETE FROM authz_invitations
|
||||||
WHERE id = @id
|
WHERE id = @id AND %s
|
||||||
`
|
`
|
||||||
|
|
||||||
|
query = fmt.Sprintf(query, scope.SQLFragment())
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"id": i.ID,
|
"id": i.ID,
|
||||||
}
|
}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
result, err := conn.Exec(ctx, query, args)
|
result, err := conn.Exec(ctx, query, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -190,21 +197,30 @@ func (i *Invitation) Delete(ctx context.Context, conn pg.Conn) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tenant scope is not applied because this is used to query invitations across all tenants
|
||||||
|
// for a user who doesn't have tenant access yet (before accepting an invitation).
|
||||||
func (i *Invitations) LoadByEmail(
|
func (i *Invitations) LoadByEmail(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
email string,
|
email string,
|
||||||
|
cursor *page.Cursor[InvitationOrderField],
|
||||||
|
filter *InvitationFilter,
|
||||||
) error {
|
) error {
|
||||||
query := `
|
query := `
|
||||||
SELECT id, organization_id, email, full_name, role, expires_at, accepted_at, created_at
|
SELECT id, organization_id, email, full_name, role, expires_at, accepted_at, created_at
|
||||||
FROM authz_invitations
|
FROM authz_invitations
|
||||||
WHERE email = @email AND accepted_at IS NULL
|
WHERE email = @email
|
||||||
ORDER BY created_at DESC
|
AND %s
|
||||||
|
AND %s
|
||||||
`
|
`
|
||||||
|
|
||||||
|
query = fmt.Sprintf(query, filter.SQLFragment(), cursor.SQLFragment())
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"email": email,
|
"email": email,
|
||||||
}
|
}
|
||||||
|
maps.Copy(args, filter.SQLArguments())
|
||||||
|
maps.Copy(args, cursor.SQLArguments())
|
||||||
|
|
||||||
rows, err := conn.Query(ctx, query, args)
|
rows, err := conn.Query(ctx, query, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -223,19 +239,23 @@ func (i *Invitations) LoadByEmail(
|
|||||||
func (i *Invitations) LoadByOrganizationID(
|
func (i *Invitations) LoadByOrganizationID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
orgID gid.GID,
|
orgID gid.GID,
|
||||||
cursor *page.Cursor[InvitationOrderField],
|
cursor *page.Cursor[InvitationOrderField],
|
||||||
) error {
|
) error {
|
||||||
query := `
|
query := `
|
||||||
SELECT id, organization_id, email, full_name, role, expires_at, accepted_at, created_at
|
SELECT id, organization_id, email, full_name, role, expires_at, accepted_at, created_at
|
||||||
FROM authz_invitations
|
FROM authz_invitations
|
||||||
WHERE organization_id = @organization_id
|
WHERE organization_id = @organization_id AND %s
|
||||||
AND %s
|
AND %s
|
||||||
`
|
`
|
||||||
|
|
||||||
query = fmt.Sprintf(query, cursor.SQLFragment())
|
query = fmt.Sprintf(query, scope.SQLFragment(), cursor.SQLFragment())
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{"organization_id": orgID}
|
args := pgx.StrictNamedArgs{
|
||||||
|
"organization_id": orgID,
|
||||||
|
}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
maps.Copy(args, cursor.SQLArguments())
|
maps.Copy(args, cursor.SQLArguments())
|
||||||
|
|
||||||
rows, err := conn.Query(ctx, query, args)
|
rows, err := conn.Query(ctx, query, args)
|
||||||
@@ -255,6 +275,7 @@ func (i *Invitations) LoadByOrganizationID(
|
|||||||
func (i *Invitations) CountByOrganizationID(
|
func (i *Invitations) CountByOrganizationID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
orgID gid.GID,
|
orgID gid.GID,
|
||||||
) (int, error) {
|
) (int, error) {
|
||||||
q := `
|
q := `
|
||||||
@@ -263,10 +284,51 @@ SELECT
|
|||||||
FROM
|
FROM
|
||||||
authz_invitations
|
authz_invitations
|
||||||
WHERE
|
WHERE
|
||||||
organization_id = @organization_id
|
organization_id = @organization_id AND %s
|
||||||
`
|
`
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{"organization_id": orgID}
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{
|
||||||
|
"organization_id": orgID,
|
||||||
|
}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
row := conn.QueryRow(ctx, q, args)
|
||||||
|
|
||||||
|
var count int
|
||||||
|
err := row.Scan(&count)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("cannot count invitations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tenant scope is not applied because this is used to count invitations across all tenants
|
||||||
|
// for a user who doesn't have tenant access yet (before accepting an invitation).
|
||||||
|
func (i *Invitations) CountByEmail(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
email string,
|
||||||
|
filter *InvitationFilter,
|
||||||
|
) (int, error) {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
COUNT(*)
|
||||||
|
FROM
|
||||||
|
authz_invitations
|
||||||
|
WHERE
|
||||||
|
email = @email
|
||||||
|
AND %s
|
||||||
|
`
|
||||||
|
|
||||||
|
q = fmt.Sprintf(q, filter.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{
|
||||||
|
"email": email,
|
||||||
|
}
|
||||||
|
maps.Copy(args, filter.SQLArguments())
|
||||||
|
|
||||||
row := conn.QueryRow(ctx, q, args)
|
row := conn.QueryRow(ctx, q, args)
|
||||||
|
|
||||||
|
|||||||
49
pkg/coredata/invitation_filter.go
Normal file
49
pkg/coredata/invitation_filter.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// 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 (
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
InvitationFilter struct {
|
||||||
|
onlyPending *bool
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewInvitationFilter(onlyPending *bool) *InvitationFilter {
|
||||||
|
return &InvitationFilter{
|
||||||
|
onlyPending: onlyPending,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *InvitationFilter) SQLArguments() pgx.NamedArgs {
|
||||||
|
return pgx.NamedArgs{
|
||||||
|
"only_pending": f.onlyPending,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *InvitationFilter) SQLFragment() string {
|
||||||
|
return `
|
||||||
|
(
|
||||||
|
CASE
|
||||||
|
WHEN @only_pending::boolean IS NOT NULL AND @only_pending::boolean = true THEN
|
||||||
|
(accepted_at IS NULL AND expires_at > NOW())
|
||||||
|
ELSE TRUE
|
||||||
|
END
|
||||||
|
)`
|
||||||
|
}
|
||||||
|
|
||||||
@@ -76,28 +76,20 @@ func (m Membership) CursorKey(orderBy MembershipOrderField) page.CursorKey {
|
|||||||
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
|
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tenant id scope is not applied because memberships are managed at the organization level and don't require tenant isolation.
|
func (m *Membership) Create(ctx context.Context, conn pg.Conn, scope Scoper) error {
|
||||||
func (m *Membership) Create(ctx context.Context, conn pg.Conn) error {
|
|
||||||
query := `
|
query := `
|
||||||
INSERT INTO authz_memberships (id, user_id, organization_id, role, created_at, updated_at)
|
INSERT INTO authz_memberships (tenant_id, id, user_id, organization_id, role, created_at, updated_at)
|
||||||
SELECT
|
VALUES (@tenant_id, @id, @user_id, @organization_id, @role, @created_at, @updated_at)
|
||||||
generate_gid(decode_base64_unpadded(o.tenant_id), @entity_type),
|
|
||||||
@user_id,
|
|
||||||
@organization_id,
|
|
||||||
@role,
|
|
||||||
@created_at,
|
|
||||||
@updated_at
|
|
||||||
FROM organizations o
|
|
||||||
WHERE o.id = @organization_id
|
|
||||||
`
|
`
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
|
"tenant_id": scope.GetTenantID(),
|
||||||
|
"id": m.ID,
|
||||||
"user_id": m.UserID,
|
"user_id": m.UserID,
|
||||||
"organization_id": m.OrganizationID,
|
"organization_id": m.OrganizationID,
|
||||||
"role": m.Role,
|
"role": m.Role,
|
||||||
"created_at": m.CreatedAt,
|
"created_at": m.CreatedAt,
|
||||||
"updated_at": m.UpdatedAt,
|
"updated_at": m.UpdatedAt,
|
||||||
"entity_type": MembershipEntityType,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := conn.Exec(ctx, query, args)
|
result, err := conn.Exec(ctx, query, args)
|
||||||
@@ -116,10 +108,10 @@ func (m *Membership) Create(ctx context.Context, conn pg.Conn) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tenant id scope is not applied because we want to access memberships across all tenants for authentication purposes.
|
|
||||||
func (m *Membership) LoadByID(
|
func (m *Membership) LoadByID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
membershipID gid.GID,
|
membershipID gid.GID,
|
||||||
) error {
|
) error {
|
||||||
query := `
|
query := `
|
||||||
@@ -134,12 +126,15 @@ func (m *Membership) LoadByID(
|
|||||||
m.updated_at
|
m.updated_at
|
||||||
FROM authz_memberships m
|
FROM authz_memberships m
|
||||||
JOIN users u ON m.user_id = u.id
|
JOIN users u ON m.user_id = u.id
|
||||||
WHERE m.id = @membership_id
|
WHERE m.id = @membership_id AND %s
|
||||||
`
|
`
|
||||||
|
|
||||||
|
query = fmt.Sprintf(query, scope.SQLFragment())
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"membership_id": membershipID,
|
"membership_id": membershipID,
|
||||||
}
|
}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
rows, err := conn.Query(ctx, query, args)
|
rows, err := conn.Query(ctx, query, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -158,10 +153,10 @@ func (m *Membership) LoadByID(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tenant id scope is not applied because we want to access memberships across all tenants for authentication purposes.
|
|
||||||
func (m *Membership) LoadByUserAndOrg(
|
func (m *Membership) LoadByUserAndOrg(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
userID gid.GID,
|
userID gid.GID,
|
||||||
orgID gid.GID,
|
orgID gid.GID,
|
||||||
) error {
|
) error {
|
||||||
@@ -177,13 +172,16 @@ func (m *Membership) LoadByUserAndOrg(
|
|||||||
m.updated_at
|
m.updated_at
|
||||||
FROM authz_memberships m
|
FROM authz_memberships m
|
||||||
JOIN users u ON m.user_id = u.id
|
JOIN users u ON m.user_id = u.id
|
||||||
WHERE m.user_id = @user_id AND m.organization_id = @organization_id
|
WHERE m.user_id = @user_id AND m.organization_id = @organization_id AND %s
|
||||||
`
|
`
|
||||||
|
|
||||||
|
query = fmt.Sprintf(query, scope.SQLFragment())
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"user_id": userID,
|
"user_id": userID,
|
||||||
"organization_id": orgID,
|
"organization_id": orgID,
|
||||||
}
|
}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
rows, err := conn.Query(ctx, query, args)
|
rows, err := conn.Query(ctx, query, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -202,19 +200,21 @@ func (m *Membership) LoadByUserAndOrg(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tenant id scope is not applied because memberships are managed at the organization level and don't require tenant isolation.
|
func (m *Membership) Update(ctx context.Context, conn pg.Conn, scope Scoper) error {
|
||||||
func (m *Membership) Update(ctx context.Context, conn pg.Conn) error {
|
|
||||||
query := `
|
query := `
|
||||||
UPDATE authz_memberships
|
UPDATE authz_memberships
|
||||||
SET role = @role, updated_at = @updated_at
|
SET role = @role, updated_at = @updated_at
|
||||||
WHERE id = @id
|
WHERE id = @id AND %s
|
||||||
`
|
`
|
||||||
|
|
||||||
|
query = fmt.Sprintf(query, scope.SQLFragment())
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"id": m.ID,
|
"id": m.ID,
|
||||||
"role": m.Role,
|
"role": m.Role,
|
||||||
"updated_at": m.UpdatedAt,
|
"updated_at": m.UpdatedAt,
|
||||||
}
|
}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
result, err := conn.Exec(ctx, query, args)
|
result, err := conn.Exec(ctx, query, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -228,16 +228,18 @@ func (m *Membership) Update(ctx context.Context, conn pg.Conn) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tenant id scope is not applied because memberships are managed at the organization level and don't require tenant isolation.
|
func (m *Membership) Delete(ctx context.Context, conn pg.Conn, scope Scoper) error {
|
||||||
func (m *Membership) Delete(ctx context.Context, conn pg.Conn) error {
|
|
||||||
query := `
|
query := `
|
||||||
DELETE FROM authz_memberships
|
DELETE FROM authz_memberships
|
||||||
WHERE id = @id
|
WHERE id = @id AND %s
|
||||||
`
|
`
|
||||||
|
|
||||||
|
query = fmt.Sprintf(query, scope.SQLFragment())
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"id": m.ID,
|
"id": m.ID,
|
||||||
}
|
}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
result, err := conn.Exec(ctx, query, args)
|
result, err := conn.Exec(ctx, query, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -251,10 +253,10 @@ func (m *Membership) Delete(ctx context.Context, conn pg.Conn) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tenant id scope is not applied because we want to access all user's memberships across tenants for authentication purposes.
|
|
||||||
func (m *Memberships) LoadByUserID(
|
func (m *Memberships) LoadByUserID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
userID gid.GID,
|
userID gid.GID,
|
||||||
) error {
|
) error {
|
||||||
query := `
|
query := `
|
||||||
@@ -272,11 +274,17 @@ FROM
|
|||||||
JOIN users u ON m.user_id = u.id
|
JOIN users u ON m.user_id = u.id
|
||||||
WHERE
|
WHERE
|
||||||
m.user_id = @user_id
|
m.user_id = @user_id
|
||||||
|
AND %s
|
||||||
ORDER BY
|
ORDER BY
|
||||||
m.created_at DESC
|
m.created_at DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{"user_id": userID}
|
query = fmt.Sprintf(query, scope.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{
|
||||||
|
"user_id": userID,
|
||||||
|
}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
rows, err := conn.Query(ctx, query, args)
|
rows, err := conn.Query(ctx, query, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -292,10 +300,10 @@ ORDER BY
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tenant id scope is not applied because we want to access memberships across all tenants for authentication purposes.
|
|
||||||
func (m *Memberships) LoadByOrganizationID(
|
func (m *Memberships) LoadByOrganizationID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
organizationID gid.GID,
|
organizationID gid.GID,
|
||||||
cursor *page.Cursor[MembershipOrderField],
|
cursor *page.Cursor[MembershipOrderField],
|
||||||
) error {
|
) error {
|
||||||
@@ -315,11 +323,15 @@ JOIN users u ON m.user_id = u.id
|
|||||||
WHERE
|
WHERE
|
||||||
m.organization_id = @organization_id
|
m.organization_id = @organization_id
|
||||||
AND %s
|
AND %s
|
||||||
|
AND %s
|
||||||
`
|
`
|
||||||
|
|
||||||
query = fmt.Sprintf(query, cursor.SQLFragment())
|
query = fmt.Sprintf(query, scope.SQLFragment(), cursor.SQLFragment())
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
args := pgx.StrictNamedArgs{
|
||||||
|
"organization_id": organizationID,
|
||||||
|
}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
maps.Copy(args, cursor.SQLArguments())
|
maps.Copy(args, cursor.SQLArguments())
|
||||||
|
|
||||||
rows, err := conn.Query(ctx, query, args)
|
rows, err := conn.Query(ctx, query, args)
|
||||||
@@ -339,14 +351,19 @@ WHERE
|
|||||||
func (m *Memberships) CountByOrganizationID(
|
func (m *Memberships) CountByOrganizationID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
organizationID gid.GID,
|
organizationID gid.GID,
|
||||||
) (int, error) {
|
) (int, error) {
|
||||||
query := `
|
query := `
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM authz_memberships
|
FROM authz_memberships
|
||||||
WHERE organization_id = @organization_id
|
WHERE organization_id = @organization_id AND %s
|
||||||
`
|
`
|
||||||
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
query = fmt.Sprintf(query, scope.SQLFragment())
|
||||||
|
args := pgx.StrictNamedArgs{
|
||||||
|
"organization_id": organizationID,
|
||||||
|
}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
row := conn.QueryRow(ctx, query, args)
|
row := conn.QueryRow(ctx, query, args)
|
||||||
var count int
|
var count int
|
||||||
if err := row.Scan(&count); err != nil {
|
if err := row.Scan(&count); err != nil {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ CREATE TYPE authz_role AS ENUM ('OWNER', 'ADMIN', 'MEMBER', 'VIEWER');
|
|||||||
-- Create authz_memberships table with id as primary key
|
-- Create authz_memberships table with id as primary key
|
||||||
CREATE TABLE authz_memberships (
|
CREATE TABLE authz_memberships (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
|
tenant_id TEXT NOT NULL,
|
||||||
user_id TEXT NOT NULL,
|
user_id TEXT NOT NULL,
|
||||||
organization_id TEXT NOT NULL,
|
organization_id TEXT NOT NULL,
|
||||||
role authz_role NOT NULL,
|
role authz_role NOT NULL,
|
||||||
@@ -19,6 +20,7 @@ CREATE TABLE authz_memberships (
|
|||||||
-- Create authz_invitations table
|
-- Create authz_invitations table
|
||||||
CREATE TABLE authz_invitations (
|
CREATE TABLE authz_invitations (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
|
tenant_id TEXT NOT NULL,
|
||||||
organization_id TEXT NOT NULL,
|
organization_id TEXT NOT NULL,
|
||||||
email TEXT NOT NULL,
|
email TEXT NOT NULL,
|
||||||
full_name TEXT NOT NULL,
|
full_name TEXT NOT NULL,
|
||||||
@@ -29,8 +31,9 @@ CREATE TABLE authz_invitations (
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- Copy data from users_organizations to authz_memberships
|
-- Copy data from users_organizations to authz_memberships
|
||||||
INSERT INTO authz_memberships (id, user_id, organization_id, role, created_at, updated_at)
|
INSERT INTO authz_memberships (tenant_id, id, user_id, organization_id, role, created_at, updated_at)
|
||||||
SELECT
|
SELECT
|
||||||
|
organizations.tenant_id,
|
||||||
generate_gid(decode_base64_unpadded(organizations.tenant_id), 38) as id,
|
generate_gid(decode_base64_unpadded(organizations.tenant_id), 38) as id,
|
||||||
users_organizations.user_id,
|
users_organizations.user_id,
|
||||||
users_organizations.organization_id,
|
users_organizations.organization_id,
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ type (
|
|||||||
ID gid.GID `db:"id"`
|
ID gid.GID `db:"id"`
|
||||||
OrganizationID gid.GID `db:"organization_id"`
|
OrganizationID gid.GID `db:"organization_id"`
|
||||||
Kind PeopleKind `db:"kind"`
|
Kind PeopleKind `db:"kind"`
|
||||||
UserID *gid.GID `db:"user_id"`
|
|
||||||
FullName string `db:"full_name"`
|
FullName string `db:"full_name"`
|
||||||
PrimaryEmailAddress string `db:"primary_email_address"`
|
PrimaryEmailAddress string `db:"primary_email_address"`
|
||||||
AdditionalEmailAddresses []string `db:"additional_email_addresses"`
|
AdditionalEmailAddresses []string `db:"additional_email_addresses"`
|
||||||
@@ -78,7 +77,6 @@ SELECT
|
|||||||
id,
|
id,
|
||||||
organization_id,
|
organization_id,
|
||||||
kind,
|
kind,
|
||||||
user_id,
|
|
||||||
full_name,
|
full_name,
|
||||||
primary_email_address,
|
primary_email_address,
|
||||||
additional_email_addresses,
|
additional_email_addresses,
|
||||||
@@ -126,7 +124,6 @@ func (p *People) LoadByEmail(
|
|||||||
id,
|
id,
|
||||||
organization_id,
|
organization_id,
|
||||||
kind,
|
kind,
|
||||||
user_id,
|
|
||||||
full_name,
|
full_name,
|
||||||
primary_email_address,
|
primary_email_address,
|
||||||
additional_email_addresses,
|
additional_email_addresses,
|
||||||
@@ -167,58 +164,6 @@ func (p *People) LoadByEmail(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *People) LoadByUserID(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Conn,
|
|
||||||
scope Scoper,
|
|
||||||
userID gid.GID,
|
|
||||||
) error {
|
|
||||||
q := `
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
organization_id,
|
|
||||||
kind,
|
|
||||||
user_id,
|
|
||||||
full_name,
|
|
||||||
primary_email_address,
|
|
||||||
additional_email_addresses,
|
|
||||||
position,
|
|
||||||
contract_start_date,
|
|
||||||
contract_end_date,
|
|
||||||
created_at,
|
|
||||||
updated_at
|
|
||||||
FROM
|
|
||||||
peoples
|
|
||||||
WHERE
|
|
||||||
%s
|
|
||||||
AND user_id = @user_id
|
|
||||||
LIMIT 1;
|
|
||||||
`
|
|
||||||
|
|
||||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{"user_id": userID}
|
|
||||||
maps.Copy(args, scope.SQLArguments())
|
|
||||||
|
|
||||||
rows, err := conn.Query(ctx, q, args)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot query people: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
people, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[People])
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return &ErrPeopleNotFound{Identifier: userID.String()}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("cannot collect people: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
*p = people
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p People) Insert(
|
func (p People) Insert(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
@@ -230,7 +175,6 @@ INSERT INTO
|
|||||||
tenant_id,
|
tenant_id,
|
||||||
id,
|
id,
|
||||||
organization_id,
|
organization_id,
|
||||||
user_id,
|
|
||||||
kind,
|
kind,
|
||||||
full_name,
|
full_name,
|
||||||
primary_email_address,
|
primary_email_address,
|
||||||
@@ -245,7 +189,6 @@ VALUES (
|
|||||||
@tenant_id,
|
@tenant_id,
|
||||||
@people_id,
|
@people_id,
|
||||||
@organization_id,
|
@organization_id,
|
||||||
@user_id,
|
|
||||||
@kind,
|
@kind,
|
||||||
@full_name,
|
@full_name,
|
||||||
@primary_email_address,
|
@primary_email_address,
|
||||||
@@ -262,7 +205,6 @@ VALUES (
|
|||||||
"tenant_id": scope.GetTenantID(),
|
"tenant_id": scope.GetTenantID(),
|
||||||
"people_id": p.ID,
|
"people_id": p.ID,
|
||||||
"organization_id": p.OrganizationID,
|
"organization_id": p.OrganizationID,
|
||||||
"user_id": p.UserID,
|
|
||||||
"kind": p.Kind,
|
"kind": p.Kind,
|
||||||
"full_name": p.FullName,
|
"full_name": p.FullName,
|
||||||
"primary_email_address": p.PrimaryEmailAddress,
|
"primary_email_address": p.PrimaryEmailAddress,
|
||||||
@@ -343,7 +285,6 @@ SELECT
|
|||||||
id,
|
id,
|
||||||
organization_id,
|
organization_id,
|
||||||
kind,
|
kind,
|
||||||
user_id,
|
|
||||||
full_name,
|
full_name,
|
||||||
primary_email_address,
|
primary_email_address,
|
||||||
additional_email_addresses,
|
additional_email_addresses,
|
||||||
@@ -390,7 +331,6 @@ func (p *People) Update(
|
|||||||
) error {
|
) error {
|
||||||
q := `
|
q := `
|
||||||
UPDATE peoples SET
|
UPDATE peoples SET
|
||||||
user_id = @user_id,
|
|
||||||
full_name = @full_name,
|
full_name = @full_name,
|
||||||
primary_email_address = @primary_email_address,
|
primary_email_address = @primary_email_address,
|
||||||
additional_email_addresses = @additional_email_addresses,
|
additional_email_addresses = @additional_email_addresses,
|
||||||
@@ -406,7 +346,6 @@ WHERE %s
|
|||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"people_id": p.ID,
|
"people_id": p.ID,
|
||||||
"user_id": p.UserID,
|
|
||||||
"full_name": p.FullName,
|
"full_name": p.FullName,
|
||||||
"primary_email_address": p.PrimaryEmailAddress,
|
"primary_email_address": p.PrimaryEmailAddress,
|
||||||
"additional_email_addresses": p.AdditionalEmailAddresses,
|
"additional_email_addresses": p.AdditionalEmailAddresses,
|
||||||
@@ -447,7 +386,6 @@ SELECT
|
|||||||
id,
|
id,
|
||||||
organization_id,
|
organization_id,
|
||||||
kind,
|
kind,
|
||||||
user_id,
|
|
||||||
full_name,
|
full_name,
|
||||||
primary_email_address,
|
primary_email_address,
|
||||||
additional_email_addresses,
|
additional_email_addresses,
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ WHERE
|
|||||||
func (u *Users) CountByOrganizationID(
|
func (u *Users) CountByOrganizationID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
organizationID gid.GID,
|
organizationID gid.GID,
|
||||||
) (int, error) {
|
) (int, error) {
|
||||||
q := `
|
q := `
|
||||||
@@ -124,11 +125,14 @@ FROM
|
|||||||
users
|
users
|
||||||
WHERE
|
WHERE
|
||||||
id IN (
|
id IN (
|
||||||
SELECT user_id FROM authz_memberships WHERE organization_id = @organization_id
|
SELECT user_id FROM authz_memberships WHERE organization_id = @organization_id AND %s
|
||||||
)
|
)
|
||||||
`
|
`
|
||||||
|
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
row := conn.QueryRow(ctx, q, args)
|
row := conn.QueryRow(ctx, q, args)
|
||||||
|
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
// 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"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/getprobo/probo/pkg/gid"
|
|
||||||
"github.com/jackc/pgx/v5"
|
|
||||||
"go.gearno.de/kit/pg"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
UserOrganization struct {
|
|
||||||
UserID gid.GID `db:"user_id"`
|
|
||||||
OrganizationID gid.GID `db:"organization_id"`
|
|
||||||
CreatedAt time.Time `db:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
UserOrganizations []*UserOrganization
|
|
||||||
)
|
|
||||||
|
|
||||||
func (uo UserOrganization) Insert(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Conn,
|
|
||||||
) error {
|
|
||||||
q := `
|
|
||||||
INSERT INTO users_organizations (user_id, organization_id, created_at)
|
|
||||||
VALUES (@user_id, @organization_id, @created_at)
|
|
||||||
`
|
|
||||||
|
|
||||||
_, err := conn.Exec(ctx, q, pgx.StrictNamedArgs{"user_id": uo.UserID, "organization_id": uo.OrganizationID, "created_at": uo.CreatedAt})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tenant id scope is not applied because user organizations are managed at the organization level and don't require tenant isolation.
|
|
||||||
func (uo UserOrganization) Delete(ctx context.Context, conn pg.Conn) error {
|
|
||||||
q := `
|
|
||||||
DELETE FROM users_organizations WHERE user_id = @user_id AND organization_id = @organization_id
|
|
||||||
`
|
|
||||||
|
|
||||||
_, err := conn.Exec(ctx, q, pgx.StrictNamedArgs{"user_id": uo.UserID, "organization_id": uo.OrganizationID})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (uo *UserOrganizations) ForUserID(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Conn,
|
|
||||||
userID gid.GID,
|
|
||||||
) error {
|
|
||||||
q := `
|
|
||||||
SELECT user_id, organization_id, created_at FROM users_organizations WHERE user_id = @user_id
|
|
||||||
`
|
|
||||||
|
|
||||||
rows, err := conn.Query(ctx, q, pgx.StrictNamedArgs{"user_id": userID})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
userOrganizations, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[UserOrganization])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
*uo = userOrganizations
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -32,7 +32,6 @@ type (
|
|||||||
|
|
||||||
UpdatePeopleRequest struct {
|
UpdatePeopleRequest struct {
|
||||||
ID gid.GID
|
ID gid.GID
|
||||||
UserID *gid.GID
|
|
||||||
Kind *coredata.PeopleKind
|
Kind *coredata.PeopleKind
|
||||||
FullName *string
|
FullName *string
|
||||||
PrimaryEmailAddress *string
|
PrimaryEmailAddress *string
|
||||||
@@ -44,7 +43,6 @@ type (
|
|||||||
|
|
||||||
CreatePeopleRequest struct {
|
CreatePeopleRequest struct {
|
||||||
OrganizationID gid.GID
|
OrganizationID gid.GID
|
||||||
UserID *gid.GID
|
|
||||||
FullName string
|
FullName string
|
||||||
PrimaryEmailAddress string
|
PrimaryEmailAddress string
|
||||||
AdditionalEmailAddresses []string
|
AdditionalEmailAddresses []string
|
||||||
@@ -75,26 +73,6 @@ func (s PeopleService) Get(
|
|||||||
return people, nil
|
return people, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s PeopleService) GetByUserID(
|
|
||||||
ctx context.Context,
|
|
||||||
userID gid.GID,
|
|
||||||
) (*coredata.People, error) {
|
|
||||||
people := &coredata.People{}
|
|
||||||
|
|
||||||
err := s.svc.pg.WithConn(
|
|
||||||
ctx,
|
|
||||||
func(conn pg.Conn) error {
|
|
||||||
return people.LoadByUserID(ctx, conn, s.svc.scope, userID)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return people, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s PeopleService) CountForOrganizationID(
|
func (s PeopleService) CountForOrganizationID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
organizationID gid.GID,
|
organizationID gid.GID,
|
||||||
@@ -164,10 +142,6 @@ func (s PeopleService) Update(
|
|||||||
return fmt.Errorf("cannot load people: %w", err)
|
return fmt.Errorf("cannot load people: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.UserID != nil {
|
|
||||||
people.UserID = req.UserID
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.Kind != nil {
|
if req.Kind != nil {
|
||||||
people.Kind = *req.Kind
|
people.Kind = *req.Kind
|
||||||
}
|
}
|
||||||
@@ -234,7 +208,6 @@ func (s PeopleService) Create(
|
|||||||
FullName: req.FullName,
|
FullName: req.FullName,
|
||||||
PrimaryEmailAddress: req.PrimaryEmailAddress,
|
PrimaryEmailAddress: req.PrimaryEmailAddress,
|
||||||
AdditionalEmailAddresses: req.AdditionalEmailAddresses,
|
AdditionalEmailAddresses: req.AdditionalEmailAddresses,
|
||||||
UserID: req.UserID,
|
|
||||||
Position: req.Position,
|
Position: req.Position,
|
||||||
ContractStartDate: req.ContractStartDate,
|
ContractStartDate: req.ContractStartDate,
|
||||||
ContractEndDate: req.ContractEndDate,
|
ContractEndDate: req.ContractEndDate,
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
// 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 console_v1
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/getprobo/probo/pkg/auth"
|
|
||||||
"github.com/getprobo/probo/pkg/authz"
|
|
||||||
"github.com/getprobo/probo/pkg/coredata"
|
|
||||||
"github.com/getprobo/probo/pkg/statelesstoken"
|
|
||||||
"go.gearno.de/kit/httpserver"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
InvitationConfirmationRequest struct {
|
|
||||||
Token string `json:"token"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
}
|
|
||||||
|
|
||||||
InvitationConfirmationResponse struct {
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func InvitationConfirmationHandler(authSvc *auth.Service, authzSvc *authz.Service, authCfg AuthConfig) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var req InvitationConfirmationRequest
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
||||||
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("cannot decode body: %w", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
payload, err := statelesstoken.ValidateToken[coredata.InvitationData](
|
|
||||||
authCfg.CookieSecret,
|
|
||||||
authz.TokenTypeOrganizationInvitation,
|
|
||||||
req.Token,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("invalid invitation token: %w", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user, _, err := authSvc.SignUp(r.Context(), payload.Data.Email, req.Password, payload.Data.FullName)
|
|
||||||
if err != nil {
|
|
||||||
var errUserAlreadyExists *auth.ErrUserAlreadyExists
|
|
||||||
if errors.As(err, &errUserAlreadyExists) {
|
|
||||||
user, err = authSvc.GetUserByEmail(r.Context(), payload.Data.Email)
|
|
||||||
if err != nil {
|
|
||||||
httpserver.RenderError(w, http.StatusInternalServerError, fmt.Errorf("failed to load existing user: %w", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
httpserver.RenderError(w, http.StatusInternalServerError, fmt.Errorf("failed to create user: %w", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = authzSvc.AcceptInvitation(r.Context(), req.Token, user.ID)
|
|
||||||
if err != nil {
|
|
||||||
httpserver.RenderError(w, http.StatusInternalServerError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
httpserver.RenderJSON(w, http.StatusOK, InvitationConfirmationResponse{})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -157,7 +158,7 @@ func NewMux(
|
|||||||
r.Post("/auth/register", SignUpHandler(authSvc, authCfg))
|
r.Post("/auth/register", SignUpHandler(authSvc, authCfg))
|
||||||
r.Post("/auth/login", SignInHandler(authSvc, authCfg))
|
r.Post("/auth/login", SignInHandler(authSvc, authCfg))
|
||||||
r.Delete("/auth/logout", SignOutHandler(authSvc, authCfg))
|
r.Delete("/auth/logout", SignOutHandler(authSvc, authCfg))
|
||||||
r.Post("/auth/invitation", InvitationConfirmationHandler(authSvc, authzSvc, authCfg))
|
r.Post("/auth/signup-from-invitation", SignupFromInvitationHandler(authSvc, authCfg))
|
||||||
r.Post("/auth/forget-password", ForgetPasswordHandler(authSvc, authCfg))
|
r.Post("/auth/forget-password", ForgetPasswordHandler(authSvc, authCfg))
|
||||||
r.Post("/auth/reset-password", ResetPasswordHandler(authSvc, authCfg))
|
r.Post("/auth/reset-password", ResetPasswordHandler(authSvc, authCfg))
|
||||||
|
|
||||||
@@ -316,18 +317,28 @@ func (r *Resolver) ProboService(ctx context.Context, tenantID gid.TenantID) *pro
|
|||||||
return GetTenantService(ctx, r.proboSvc, tenantID)
|
return GetTenantService(ctx, r.proboSvc, tenantID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Resolver) AuthzService(ctx context.Context, tenantID gid.TenantID) *authz.TenantAuthzService {
|
||||||
|
return GetTenantAuthzService(ctx, r.authzSvc, tenantID)
|
||||||
|
}
|
||||||
|
|
||||||
func GetTenantService(ctx context.Context, proboSvc *probo.Service, tenantID gid.TenantID) *probo.TenantService {
|
func GetTenantService(ctx context.Context, proboSvc *probo.Service, tenantID gid.TenantID) *probo.TenantService {
|
||||||
|
validateTenantAccess(ctx, tenantID)
|
||||||
|
return proboSvc.WithTenant(tenantID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTenantAuthzService(ctx context.Context, authzSvc *authz.Service, tenantID gid.TenantID) *authz.TenantAuthzService {
|
||||||
|
validateTenantAccess(ctx, tenantID)
|
||||||
|
return authzSvc.WithTenant(tenantID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateTenantAccess(ctx context.Context, tenantID gid.TenantID) {
|
||||||
tenantIDs, _ := ctx.Value(userTenantContextKey).(*[]gid.TenantID)
|
tenantIDs, _ := ctx.Value(userTenantContextKey).(*[]gid.TenantID)
|
||||||
|
|
||||||
if tenantIDs == nil {
|
if tenantIDs == nil {
|
||||||
panic(fmt.Errorf("tenant not found"))
|
panic(fmt.Errorf("tenant not found"))
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, id := range *tenantIDs {
|
if !slices.Contains(*tenantIDs, tenantID) {
|
||||||
if id == tenantID {
|
|
||||||
return proboSvc.WithTenant(tenantID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
panic(fmt.Errorf("tenant not found"))
|
panic(fmt.Errorf("tenant not found"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1465,6 +1465,10 @@ input InvitationOrder {
|
|||||||
field: InvitationOrderField!
|
field: InvitationOrderField!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input InvitationFilter {
|
||||||
|
onlyPending: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
input DocumentVersionFilter {
|
input DocumentVersionFilter {
|
||||||
status: DocumentStatus
|
status: DocumentStatus
|
||||||
}
|
}
|
||||||
@@ -1572,6 +1576,7 @@ type Organization implements Node {
|
|||||||
last: Int
|
last: Int
|
||||||
before: CursorKey
|
before: CursorKey
|
||||||
orderBy: InvitationOrder
|
orderBy: InvitationOrder
|
||||||
|
filter: InvitationFilter
|
||||||
): InvitationConnection! @goField(forceResolver: true)
|
): InvitationConnection! @goField(forceResolver: true)
|
||||||
|
|
||||||
connectors(
|
connectors(
|
||||||
@@ -1736,8 +1741,6 @@ type User implements Node {
|
|||||||
email: String!
|
email: String!
|
||||||
createdAt: Datetime!
|
createdAt: Datetime!
|
||||||
updatedAt: Datetime!
|
updatedAt: Datetime!
|
||||||
|
|
||||||
people(organizationId: ID!): People @goField(forceResolver: true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Membership implements Node {
|
type Membership implements Node {
|
||||||
@@ -1759,6 +1762,7 @@ type Invitation implements Node {
|
|||||||
expiresAt: Datetime!
|
expiresAt: Datetime!
|
||||||
acceptedAt: Datetime
|
acceptedAt: Datetime
|
||||||
createdAt: Datetime!
|
createdAt: Datetime!
|
||||||
|
organization: Organization! @goField(forceResolver: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Connector implements Node {
|
type Connector implements Node {
|
||||||
@@ -2295,6 +2299,15 @@ type Viewer {
|
|||||||
before: CursorKey
|
before: CursorKey
|
||||||
orderBy: OrganizationOrder
|
orderBy: OrganizationOrder
|
||||||
): OrganizationConnection! @goField(forceResolver: true)
|
): OrganizationConnection! @goField(forceResolver: true)
|
||||||
|
|
||||||
|
invitations(
|
||||||
|
first: Int
|
||||||
|
after: CursorKey
|
||||||
|
last: Int
|
||||||
|
before: CursorKey
|
||||||
|
orderBy: InvitationOrder
|
||||||
|
filter: InvitationFilter
|
||||||
|
): InvitationConnection! @goField(forceResolver: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
# Connection Types
|
# Connection Types
|
||||||
@@ -2394,13 +2407,19 @@ type TrustCenterReferenceEdge {
|
|||||||
node: TrustCenterReference!
|
node: TrustCenterReference!
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserConnection {
|
type UserConnection
|
||||||
|
@goModel(
|
||||||
|
model: "github.com/getprobo/probo/pkg/server/api/console/v1/types.UserConnection"
|
||||||
|
) {
|
||||||
totalCount: Int! @goField(forceResolver: true)
|
totalCount: Int! @goField(forceResolver: true)
|
||||||
edges: [UserEdge!]!
|
edges: [UserEdge!]!
|
||||||
pageInfo: PageInfo!
|
pageInfo: PageInfo!
|
||||||
}
|
}
|
||||||
|
|
||||||
type MembershipConnection {
|
type MembershipConnection
|
||||||
|
@goModel(
|
||||||
|
model: "github.com/getprobo/probo/pkg/server/api/console/v1/types.MembershipConnection"
|
||||||
|
) {
|
||||||
totalCount: Int! @goField(forceResolver: true)
|
totalCount: Int! @goField(forceResolver: true)
|
||||||
edges: [MembershipEdge!]!
|
edges: [MembershipEdge!]!
|
||||||
pageInfo: PageInfo!
|
pageInfo: PageInfo!
|
||||||
@@ -2709,7 +2728,10 @@ type File {
|
|||||||
updatedAt: Datetime!
|
updatedAt: Datetime!
|
||||||
}
|
}
|
||||||
|
|
||||||
type InvitationConnection {
|
type InvitationConnection
|
||||||
|
@goModel(
|
||||||
|
model: "github.com/getprobo/probo/pkg/server/api/console/v1/types.InvitationConnection"
|
||||||
|
) {
|
||||||
totalCount: Int! @goField(forceResolver: true)
|
totalCount: Int! @goField(forceResolver: true)
|
||||||
edges: [InvitationEdge!]!
|
edges: [InvitationEdge!]!
|
||||||
pageInfo: PageInfo!
|
pageInfo: PageInfo!
|
||||||
@@ -2780,6 +2802,7 @@ type Mutation {
|
|||||||
# User mutations
|
# User mutations
|
||||||
confirmEmail(input: ConfirmEmailInput!): ConfirmEmailPayload!
|
confirmEmail(input: ConfirmEmailInput!): ConfirmEmailPayload!
|
||||||
inviteUser(input: InviteUserInput!): InviteUserPayload!
|
inviteUser(input: InviteUserInput!): InviteUserPayload!
|
||||||
|
acceptInvitation(input: AcceptInvitationInput!): AcceptInvitationPayload!
|
||||||
deleteInvitation(input: DeleteInvitationInput!): DeleteInvitationPayload!
|
deleteInvitation(input: DeleteInvitationInput!): DeleteInvitationPayload!
|
||||||
removeMember(input: RemoveMemberInput!): RemoveMemberPayload!
|
removeMember(input: RemoveMemberInput!): RemoveMemberPayload!
|
||||||
|
|
||||||
@@ -3540,6 +3563,10 @@ input InviteUserInput {
|
|||||||
createPeople: Boolean!
|
createPeople: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input AcceptInvitationInput {
|
||||||
|
invitationId: ID!
|
||||||
|
}
|
||||||
|
|
||||||
input DeleteInvitationInput {
|
input DeleteInvitationInput {
|
||||||
invitationId: ID!
|
invitationId: ID!
|
||||||
}
|
}
|
||||||
@@ -4066,12 +4093,16 @@ type InviteUserPayload {
|
|||||||
invitationEdge: InvitationEdge!
|
invitationEdge: InvitationEdge!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AcceptInvitationPayload {
|
||||||
|
invitation: Invitation!
|
||||||
|
}
|
||||||
|
|
||||||
type DeleteInvitationPayload {
|
type DeleteInvitationPayload {
|
||||||
deletedInvitationId: ID!
|
deletedInvitationId: ID!
|
||||||
}
|
}
|
||||||
|
|
||||||
type RemoveMemberPayload {
|
type RemoveMemberPayload {
|
||||||
success: Boolean!
|
deletedMemberId: ID!
|
||||||
}
|
}
|
||||||
|
|
||||||
input VendorRiskAssessmentOrder {
|
input VendorRiskAssessmentOrder {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
75
pkg/server/api/console/v1/signup_from_invitation_handler.go
Normal file
75
pkg/server/api/console/v1/signup_from_invitation_handler.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
// 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 console_v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/getprobo/probo/pkg/auth"
|
||||||
|
"github.com/getprobo/probo/pkg/securecookie"
|
||||||
|
"go.gearno.de/kit/httpserver"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
SignupFromInvitationRequest struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
FullName string `json:"fullName"`
|
||||||
|
}
|
||||||
|
|
||||||
|
SignupFromInvitationResponse struct {
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func SignupFromInvitationHandler(authSvc *auth.Service, authCfg AuthConfig) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req SignupFromInvitationRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("cannot decode body: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, session, err := authSvc.SignupFromInvitation(r.Context(), req.Token, req.Password, req.FullName)
|
||||||
|
if err != nil {
|
||||||
|
httpserver.RenderError(w, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
securecookie.Set(
|
||||||
|
w,
|
||||||
|
securecookie.DefaultConfig(
|
||||||
|
authCfg.CookieName,
|
||||||
|
authCfg.CookieSecret,
|
||||||
|
),
|
||||||
|
session.ID.String(),
|
||||||
|
)
|
||||||
|
|
||||||
|
httpserver.RenderJSON(
|
||||||
|
w,
|
||||||
|
http.StatusOK,
|
||||||
|
SignUpResponse{
|
||||||
|
User: UserResponse{
|
||||||
|
ID: user.ID,
|
||||||
|
Email: user.EmailAddress,
|
||||||
|
FullName: user.FullName,
|
||||||
|
CreatedAt: user.CreatedAt,
|
||||||
|
UpdatedAt: user.UpdatedAt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,10 +16,28 @@ package types
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/getprobo/probo/pkg/coredata"
|
"github.com/getprobo/probo/pkg/coredata"
|
||||||
|
"github.com/getprobo/probo/pkg/gid"
|
||||||
"github.com/getprobo/probo/pkg/page"
|
"github.com/getprobo/probo/pkg/page"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewInvitationConnection(p *page.Page[*coredata.Invitation, coredata.InvitationOrderField]) *InvitationConnection {
|
type (
|
||||||
|
InvitationConnection struct {
|
||||||
|
TotalCount int `json:"totalCount"`
|
||||||
|
Edges []*InvitationEdge `json:"edges"`
|
||||||
|
PageInfo *PageInfo `json:"pageInfo"`
|
||||||
|
|
||||||
|
Resolver any
|
||||||
|
ParentID gid.GID
|
||||||
|
Filter *InvitationFilter
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewInvitationConnection(
|
||||||
|
p *page.Page[*coredata.Invitation, coredata.InvitationOrderField],
|
||||||
|
resolver any,
|
||||||
|
parentID gid.GID,
|
||||||
|
filter *InvitationFilter,
|
||||||
|
) *InvitationConnection {
|
||||||
var edges = make([]*InvitationEdge, len(p.Data))
|
var edges = make([]*InvitationEdge, len(p.Data))
|
||||||
|
|
||||||
for i := range edges {
|
for i := range edges {
|
||||||
@@ -29,6 +47,9 @@ func NewInvitationConnection(p *page.Page[*coredata.Invitation, coredata.Invitat
|
|||||||
return &InvitationConnection{
|
return &InvitationConnection{
|
||||||
Edges: edges,
|
Edges: edges,
|
||||||
PageInfo: NewPageInfo(p),
|
PageInfo: NewPageInfo(p),
|
||||||
|
Resolver: resolver,
|
||||||
|
ParentID: parentID,
|
||||||
|
Filter: filter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,14 +16,28 @@ package types
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/getprobo/probo/pkg/coredata"
|
"github.com/getprobo/probo/pkg/coredata"
|
||||||
|
"github.com/getprobo/probo/pkg/gid"
|
||||||
"github.com/getprobo/probo/pkg/page"
|
"github.com/getprobo/probo/pkg/page"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
MembershipConnection struct {
|
||||||
|
TotalCount int `json:"totalCount"`
|
||||||
|
Edges []*MembershipEdge `json:"edges"`
|
||||||
|
PageInfo *PageInfo `json:"pageInfo"`
|
||||||
|
|
||||||
|
Resolver any
|
||||||
|
ParentID gid.GID
|
||||||
|
}
|
||||||
|
|
||||||
MembershipOrderBy OrderBy[coredata.MembershipOrderField]
|
MembershipOrderBy OrderBy[coredata.MembershipOrderField]
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewMembershipConnection(p *page.Page[*coredata.Membership, coredata.MembershipOrderField]) *MembershipConnection {
|
func NewMembershipConnection(
|
||||||
|
p *page.Page[*coredata.Membership, coredata.MembershipOrderField],
|
||||||
|
resolver any,
|
||||||
|
parentID gid.GID,
|
||||||
|
) *MembershipConnection {
|
||||||
var edges = make([]*MembershipEdge, len(p.Data))
|
var edges = make([]*MembershipEdge, len(p.Data))
|
||||||
|
|
||||||
for i := range edges {
|
for i := range edges {
|
||||||
@@ -33,6 +47,8 @@ func NewMembershipConnection(p *page.Page[*coredata.Membership, coredata.Members
|
|||||||
return &MembershipConnection{
|
return &MembershipConnection{
|
||||||
Edges: edges,
|
Edges: edges,
|
||||||
PageInfo: NewPageInfo(p),
|
PageInfo: NewPageInfo(p),
|
||||||
|
Resolver: resolver,
|
||||||
|
ParentID: parentID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,14 @@ type Node interface {
|
|||||||
GetID() gid.GID
|
GetID() gid.GID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AcceptInvitationInput struct {
|
||||||
|
InvitationID gid.GID `json:"invitationId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AcceptInvitationPayload struct {
|
||||||
|
Invitation *Invitation `json:"invitation"`
|
||||||
|
}
|
||||||
|
|
||||||
type AssessVendorInput struct {
|
type AssessVendorInput struct {
|
||||||
ID gid.GID `json:"id"`
|
ID gid.GID `json:"id"`
|
||||||
WebsiteURL string `json:"websiteUrl"`
|
WebsiteURL string `json:"websiteUrl"`
|
||||||
@@ -1207,22 +1215,21 @@ type Invitation struct {
|
|||||||
ExpiresAt time.Time `json:"expiresAt"`
|
ExpiresAt time.Time `json:"expiresAt"`
|
||||||
AcceptedAt *time.Time `json:"acceptedAt,omitempty"`
|
AcceptedAt *time.Time `json:"acceptedAt,omitempty"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
Organization *Organization `json:"organization"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Invitation) IsNode() {}
|
func (Invitation) IsNode() {}
|
||||||
func (this Invitation) GetID() gid.GID { return this.ID }
|
func (this Invitation) GetID() gid.GID { return this.ID }
|
||||||
|
|
||||||
type InvitationConnection struct {
|
|
||||||
TotalCount int `json:"totalCount"`
|
|
||||||
Edges []*InvitationEdge `json:"edges"`
|
|
||||||
PageInfo *PageInfo `json:"pageInfo"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type InvitationEdge struct {
|
type InvitationEdge struct {
|
||||||
Cursor page.CursorKey `json:"cursor"`
|
Cursor page.CursorKey `json:"cursor"`
|
||||||
Node *Invitation `json:"node"`
|
Node *Invitation `json:"node"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InvitationFilter struct {
|
||||||
|
OnlyPending *bool `json:"onlyPending,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type InvitationOrder struct {
|
type InvitationOrder struct {
|
||||||
Direction page.OrderDirection `json:"direction"`
|
Direction page.OrderDirection `json:"direction"`
|
||||||
Field coredata.InvitationOrderField `json:"field"`
|
Field coredata.InvitationOrderField `json:"field"`
|
||||||
@@ -1280,12 +1287,6 @@ type Membership struct {
|
|||||||
func (Membership) IsNode() {}
|
func (Membership) IsNode() {}
|
||||||
func (this Membership) GetID() gid.GID { return this.ID }
|
func (this Membership) GetID() gid.GID { return this.ID }
|
||||||
|
|
||||||
type MembershipConnection struct {
|
|
||||||
TotalCount int `json:"totalCount"`
|
|
||||||
Edges []*MembershipEdge `json:"edges"`
|
|
||||||
PageInfo *PageInfo `json:"pageInfo"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MembershipEdge struct {
|
type MembershipEdge struct {
|
||||||
Cursor page.CursorKey `json:"cursor"`
|
Cursor page.CursorKey `json:"cursor"`
|
||||||
Node *Membership `json:"node"`
|
Node *Membership `json:"node"`
|
||||||
@@ -1493,7 +1494,7 @@ type RemoveMemberInput struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RemoveMemberPayload struct {
|
type RemoveMemberPayload struct {
|
||||||
Success bool `json:"success"`
|
DeletedMemberID gid.GID `json:"deletedMemberId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Report struct {
|
type Report struct {
|
||||||
@@ -2120,18 +2121,11 @@ type User struct {
|
|||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
People *People `json:"people,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (User) IsNode() {}
|
func (User) IsNode() {}
|
||||||
func (this User) GetID() gid.GID { return this.ID }
|
func (this User) GetID() gid.GID { return this.ID }
|
||||||
|
|
||||||
type UserConnection struct {
|
|
||||||
TotalCount int `json:"totalCount"`
|
|
||||||
Edges []*UserEdge `json:"edges"`
|
|
||||||
PageInfo *PageInfo `json:"pageInfo"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserEdge struct {
|
type UserEdge struct {
|
||||||
Cursor page.CursorKey `json:"cursor"`
|
Cursor page.CursorKey `json:"cursor"`
|
||||||
Node *User `json:"node"`
|
Node *User `json:"node"`
|
||||||
@@ -2316,4 +2310,5 @@ type Viewer struct {
|
|||||||
ID gid.GID `json:"id"`
|
ID gid.GID `json:"id"`
|
||||||
User *User `json:"user"`
|
User *User `json:"user"`
|
||||||
Organizations *OrganizationConnection `json:"organizations"`
|
Organizations *OrganizationConnection `json:"organizations"`
|
||||||
|
Invitations *InvitationConnection `json:"invitations"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,14 +16,28 @@ package types
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/getprobo/probo/pkg/coredata"
|
"github.com/getprobo/probo/pkg/coredata"
|
||||||
|
"github.com/getprobo/probo/pkg/gid"
|
||||||
"github.com/getprobo/probo/pkg/page"
|
"github.com/getprobo/probo/pkg/page"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
UserConnection struct {
|
||||||
|
TotalCount int `json:"totalCount"`
|
||||||
|
Edges []*UserEdge `json:"edges"`
|
||||||
|
PageInfo *PageInfo `json:"pageInfo"`
|
||||||
|
|
||||||
|
Resolver any
|
||||||
|
ParentID gid.GID
|
||||||
|
}
|
||||||
|
|
||||||
UserOrderBy OrderBy[coredata.UserOrderField]
|
UserOrderBy OrderBy[coredata.UserOrderField]
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewUserConnection(p *page.Page[*coredata.User, coredata.UserOrderField]) *UserConnection {
|
func NewUserConnection(
|
||||||
|
p *page.Page[*coredata.User, coredata.UserOrderField],
|
||||||
|
resolver any,
|
||||||
|
parentID gid.GID,
|
||||||
|
) *UserConnection {
|
||||||
var edges = make([]*UserEdge, len(p.Data))
|
var edges = make([]*UserEdge, len(p.Data))
|
||||||
|
|
||||||
for i := range edges {
|
for i := range edges {
|
||||||
@@ -33,6 +47,8 @@ func NewUserConnection(p *page.Page[*coredata.User, coredata.UserOrderField]) *U
|
|||||||
return &UserConnection{
|
return &UserConnection{
|
||||||
Edges: edges,
|
Edges: edges,
|
||||||
PageInfo: NewPageInfo(p),
|
PageInfo: NewPageInfo(p),
|
||||||
|
Resolver: resolver,
|
||||||
|
ParentID: parentID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -891,25 +891,45 @@ func (r *frameworkConnectionResolver) TotalCount(ctx context.Context, obj *types
|
|||||||
panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver))
|
panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Organization is the resolver for the organization field.
|
||||||
|
func (r *invitationResolver) Organization(ctx context.Context, obj *types.Invitation) (*types.Organization, error) {
|
||||||
|
organization, err := r.authzSvc.GetOrganizationByInvitationID(ctx, obj.ID)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("cannot load organization: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.NewOrganization(organization), nil
|
||||||
|
}
|
||||||
|
|
||||||
// TotalCount is the resolver for the totalCount field.
|
// TotalCount is the resolver for the totalCount field.
|
||||||
func (r *invitationConnectionResolver) TotalCount(ctx context.Context, obj *types.InvitationConnection) (int, error) {
|
func (r *invitationConnectionResolver) TotalCount(ctx context.Context, obj *types.InvitationConnection) (int, error) {
|
||||||
currentUser := UserFromContext(ctx)
|
switch obj.Resolver.(type) {
|
||||||
if currentUser == nil {
|
case *organizationResolver:
|
||||||
return 0, fmt.Errorf("no authenticated user")
|
authzSvc := r.AuthzService(ctx, obj.ParentID.TenantID())
|
||||||
}
|
count, err := authzSvc.CountOrganizationInvitations(ctx, obj.ParentID)
|
||||||
|
|
||||||
memberships, err := r.authzSvc.GetAllUserOrganizations(ctx, currentUser.ID)
|
|
||||||
if err != nil || len(memberships) == 0 {
|
|
||||||
return 0, fmt.Errorf("user has no organization memberships")
|
|
||||||
}
|
|
||||||
|
|
||||||
orgID := memberships[0].ID
|
|
||||||
count, err := r.authzSvc.CountOrganizationInvitations(ctx, orgID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("failed to count invitations: %w", err)
|
panic(fmt.Errorf("failed to count organization invitations: %w", err))
|
||||||
|
}
|
||||||
|
return count, nil
|
||||||
|
case *viewerResolver:
|
||||||
|
user := UserFromContext(ctx)
|
||||||
|
if user == nil {
|
||||||
|
panic(fmt.Errorf("no authenticated user"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
invitationFilter := coredata.NewInvitationFilter(nil)
|
||||||
|
if obj.Filter != nil {
|
||||||
|
invitationFilter = coredata.NewInvitationFilter(obj.Filter.OnlyPending)
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err := r.authzSvc.CountUserInvitations(ctx, user.EmailAddress, invitationFilter)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("failed to count user invitations: %w", err))
|
||||||
|
}
|
||||||
return count, nil
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evidences is the resolver for the evidences field.
|
// Evidences is the resolver for the evidences field.
|
||||||
@@ -1052,23 +1072,17 @@ func (r *measureConnectionResolver) TotalCount(ctx context.Context, obj *types.M
|
|||||||
|
|
||||||
// TotalCount is the resolver for the totalCount field.
|
// TotalCount is the resolver for the totalCount field.
|
||||||
func (r *membershipConnectionResolver) TotalCount(ctx context.Context, obj *types.MembershipConnection) (int, error) {
|
func (r *membershipConnectionResolver) TotalCount(ctx context.Context, obj *types.MembershipConnection) (int, error) {
|
||||||
currentUser := UserFromContext(ctx)
|
switch obj.Resolver.(type) {
|
||||||
if currentUser == nil {
|
case *organizationResolver:
|
||||||
return 0, fmt.Errorf("no authenticated user")
|
authzSvc := r.AuthzService(ctx, obj.ParentID.TenantID())
|
||||||
}
|
count, err := authzSvc.CountOrganizationMemberships(ctx, obj.ParentID)
|
||||||
|
|
||||||
memberships, err := r.authzSvc.GetAllUserOrganizations(ctx, currentUser.ID)
|
|
||||||
if err != nil || len(memberships) == 0 {
|
|
||||||
return 0, fmt.Errorf("user has no organization memberships")
|
|
||||||
}
|
|
||||||
|
|
||||||
orgID := memberships[0].ID
|
|
||||||
count, err := r.authzSvc.CountOrganizationMemberships(ctx, orgID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("failed to count memberships: %w", err)
|
panic(fmt.Errorf("failed to count organization memberships: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
return count, nil
|
return count, nil
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("unknown resolver type for membership connection"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateOrganization is the resolver for the createOrganization field.
|
// CreateOrganization is the resolver for the createOrganization field.
|
||||||
@@ -1104,7 +1118,6 @@ func (r *mutationResolver) CreateOrganization(ctx context.Context, input types.C
|
|||||||
ctx,
|
ctx,
|
||||||
probo.CreatePeopleRequest{
|
probo.CreatePeopleRequest{
|
||||||
OrganizationID: organization.ID,
|
OrganizationID: organization.ID,
|
||||||
UserID: &UserFromContext(ctx).ID,
|
|
||||||
FullName: UserFromContext(ctx).FullName,
|
FullName: UserFromContext(ctx).FullName,
|
||||||
PrimaryEmailAddress: UserFromContext(ctx).EmailAddress,
|
PrimaryEmailAddress: UserFromContext(ctx).EmailAddress,
|
||||||
AdditionalEmailAddresses: []string{},
|
AdditionalEmailAddresses: []string{},
|
||||||
@@ -1379,18 +1392,10 @@ func (r *mutationResolver) ConfirmEmail(ctx context.Context, input types.Confirm
|
|||||||
|
|
||||||
// InviteUser is the resolver for the inviteUser field.
|
// InviteUser is the resolver for the inviteUser field.
|
||||||
func (r *mutationResolver) InviteUser(ctx context.Context, input types.InviteUserInput) (*types.InviteUserPayload, error) {
|
func (r *mutationResolver) InviteUser(ctx context.Context, input types.InviteUserInput) (*types.InviteUserPayload, error) {
|
||||||
user := UserFromContext(ctx)
|
authzSvc := r.AuthzService(ctx, input.OrganizationID.TenantID())
|
||||||
|
invitation, err := authzSvc.InviteUserToOrganization(ctx, input.OrganizationID, input.Email, input.FullName, string(authz.RoleMember))
|
||||||
organizations, err := r.authzSvc.GetAllUserOrganizations(ctx, user.ID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Errorf("failed to list organizations for user: %w", err))
|
panic(fmt.Errorf("failed to invite user to organization: %w", err))
|
||||||
}
|
|
||||||
|
|
||||||
for _, organization := range organizations {
|
|
||||||
if organization.ID == input.OrganizationID {
|
|
||||||
invitation, err := r.authzSvc.InviteUserToOrganization(ctx, input.OrganizationID, input.Email, input.FullName, string(authz.RoleMember))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if input.CreatePeople {
|
if input.CreatePeople {
|
||||||
@@ -1410,17 +1415,26 @@ func (r *mutationResolver) InviteUser(ctx context.Context, input types.InviteUse
|
|||||||
return &types.InviteUserPayload{
|
return &types.InviteUserPayload{
|
||||||
InvitationEdge: types.NewInvitationEdge(invitation, coredata.InvitationOrderFieldCreatedAt),
|
InvitationEdge: types.NewInvitationEdge(invitation, coredata.InvitationOrderFieldCreatedAt),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AcceptInvitation is the resolver for the acceptInvitation field.
|
||||||
|
func (r *mutationResolver) AcceptInvitation(ctx context.Context, input types.AcceptInvitationInput) (*types.AcceptInvitationPayload, error) {
|
||||||
|
user := UserFromContext(ctx)
|
||||||
|
|
||||||
|
invitation, err := r.authzSvc.AcceptInvitationByID(ctx, input.InvitationID, user.ID)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("failed to accept invitation: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("organization not found")
|
return &types.AcceptInvitationPayload{Invitation: types.NewInvitation(invitation)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteInvitation is the resolver for the deleteInvitation field.
|
// DeleteInvitation is the resolver for the deleteInvitation field.
|
||||||
func (r *mutationResolver) DeleteInvitation(ctx context.Context, input types.DeleteInvitationInput) (*types.DeleteInvitationPayload, error) {
|
func (r *mutationResolver) DeleteInvitation(ctx context.Context, input types.DeleteInvitationInput) (*types.DeleteInvitationPayload, error) {
|
||||||
err := r.authzSvc.DeleteInvitation(ctx, input.InvitationID)
|
authzSvc := r.AuthzService(ctx, input.InvitationID.TenantID())
|
||||||
|
err := authzSvc.DeleteInvitation(ctx, input.InvitationID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
panic(fmt.Errorf("failed to delete invitation: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
return &types.DeleteInvitationPayload{
|
return &types.DeleteInvitationPayload{
|
||||||
@@ -1430,25 +1444,13 @@ func (r *mutationResolver) DeleteInvitation(ctx context.Context, input types.Del
|
|||||||
|
|
||||||
// RemoveMember is the resolver for the removeMember field.
|
// RemoveMember is the resolver for the removeMember field.
|
||||||
func (r *mutationResolver) RemoveMember(ctx context.Context, input types.RemoveMemberInput) (*types.RemoveMemberPayload, error) {
|
func (r *mutationResolver) RemoveMember(ctx context.Context, input types.RemoveMemberInput) (*types.RemoveMemberPayload, error) {
|
||||||
user := UserFromContext(ctx)
|
authzSvc := r.AuthzService(ctx, input.OrganizationID.TenantID())
|
||||||
|
err := authzSvc.RemoveMemberFromOrganization(ctx, input.OrganizationID, input.MemberID)
|
||||||
organizations, err := r.authzSvc.GetAllUserOrganizations(ctx, user.ID)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("failed to list organizations for user: %w", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, organization := range organizations {
|
|
||||||
if organization.ID == input.OrganizationID {
|
|
||||||
err := r.authzSvc.RemoveMemberFromOrganization(ctx, input.OrganizationID, input.MemberID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &types.RemoveMemberPayload{Success: true}, nil
|
return &types.RemoveMemberPayload{DeletedMemberID: input.MemberID}, nil
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("organization not found")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreatePeople is the resolver for the createPeople field.
|
// CreatePeople is the resolver for the createPeople field.
|
||||||
@@ -3611,16 +3613,17 @@ func (r *organizationResolver) Memberships(ctx context.Context, obj *types.Organ
|
|||||||
|
|
||||||
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
|
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
|
||||||
|
|
||||||
page, err := r.authzSvc.GetAllOrganizationMemberships(ctx, obj.ID, cursor)
|
authzSvc := r.AuthzService(ctx, obj.ID.TenantID())
|
||||||
|
page, err := authzSvc.GetMembershipsByOrganizationID(ctx, obj.ID, cursor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Errorf("cannot list memberships: %w", err))
|
panic(fmt.Errorf("cannot list memberships: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
return types.NewMembershipConnection(page), nil
|
return types.NewMembershipConnection(page, r, obj.ID), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invitations is the resolver for the invitations field.
|
// Invitations is the resolver for the invitations field.
|
||||||
func (r *organizationResolver) Invitations(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.InvitationOrder) (*types.InvitationConnection, error) {
|
func (r *organizationResolver) Invitations(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.InvitationOrder, filter *types.InvitationFilter) (*types.InvitationConnection, error) {
|
||||||
pageOrderBy := page.OrderBy[coredata.InvitationOrderField]{
|
pageOrderBy := page.OrderBy[coredata.InvitationOrderField]{
|
||||||
Field: coredata.InvitationOrderFieldCreatedAt,
|
Field: coredata.InvitationOrderFieldCreatedAt,
|
||||||
Direction: page.OrderDirectionDesc,
|
Direction: page.OrderDirectionDesc,
|
||||||
@@ -3634,12 +3637,13 @@ func (r *organizationResolver) Invitations(ctx context.Context, obj *types.Organ
|
|||||||
|
|
||||||
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
|
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
|
||||||
|
|
||||||
page, err := r.authzSvc.GetAllOrganizationInvitations(ctx, obj.ID, cursor)
|
authzSvc := r.AuthzService(ctx, obj.ID.TenantID())
|
||||||
|
page, err := authzSvc.GetInvitationsByOrganizationID(ctx, obj.ID, cursor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Errorf("cannot list invitations: %w", err))
|
panic(fmt.Errorf("cannot list invitations: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
return types.NewInvitationConnection(page), nil
|
return types.NewInvitationConnection(page, r, obj.ID, filter), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connectors is the resolver for the connectors field.
|
// Connectors is the resolver for the connectors field.
|
||||||
@@ -4943,41 +4947,19 @@ func (r *trustCenterReferenceConnectionResolver) TotalCount(ctx context.Context,
|
|||||||
return count, nil
|
return count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// People is the resolver for the people field.
|
|
||||||
func (r *userResolver) People(ctx context.Context, obj *types.User, organizationID gid.GID) (*types.People, error) {
|
|
||||||
prb := r.ProboService(ctx, organizationID.TenantID())
|
|
||||||
|
|
||||||
people, err := prb.Peoples.GetByUserID(ctx, obj.ID)
|
|
||||||
if err != nil {
|
|
||||||
var errPeopleNotFound *coredata.ErrPeopleNotFound
|
|
||||||
if errors.As(err, &errPeopleNotFound) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
panic(fmt.Errorf("failed to get people: %w", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return types.NewPeople(people), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TotalCount is the resolver for the totalCount field.
|
// TotalCount is the resolver for the totalCount field.
|
||||||
func (r *userConnectionResolver) TotalCount(ctx context.Context, obj *types.UserConnection) (int, error) {
|
func (r *userConnectionResolver) TotalCount(ctx context.Context, obj *types.UserConnection) (int, error) {
|
||||||
currentUser := UserFromContext(ctx)
|
switch obj.Resolver.(type) {
|
||||||
if currentUser == nil {
|
case *organizationResolver:
|
||||||
return 0, fmt.Errorf("no authenticated user")
|
authzSvc := r.AuthzService(ctx, obj.ParentID.TenantID())
|
||||||
}
|
count, err := authzSvc.CountOrganizationUsers(ctx, obj.ParentID)
|
||||||
|
|
||||||
memberships, err := r.authzSvc.GetAllUserOrganizations(ctx, currentUser.ID)
|
|
||||||
if err != nil || len(memberships) == 0 {
|
|
||||||
return 0, fmt.Errorf("user has no organization memberships")
|
|
||||||
}
|
|
||||||
|
|
||||||
orgID := memberships[0].ID
|
|
||||||
count, err := r.authzSvc.CountOrganizationMemberships(ctx, orgID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("failed to count memberships: %w", err)
|
panic(fmt.Errorf("failed to count organization users: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
return count, nil
|
return count, nil
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("unknown resolver type for user connection"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Organization is the resolver for the organization field.
|
// Organization is the resolver for the organization field.
|
||||||
@@ -5353,6 +5335,35 @@ func (r *viewerResolver) Organizations(ctx context.Context, obj *types.Viewer, f
|
|||||||
return types.NewOrganizationConnection(page), nil
|
return types.NewOrganizationConnection(page), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Invitations is the resolver for the invitations field.
|
||||||
|
func (r *viewerResolver) Invitations(ctx context.Context, obj *types.Viewer, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.InvitationOrder, filter *types.InvitationFilter) (*types.InvitationConnection, error) {
|
||||||
|
user := UserFromContext(ctx)
|
||||||
|
|
||||||
|
pageOrderBy := page.OrderBy[coredata.InvitationOrderField]{
|
||||||
|
Field: coredata.InvitationOrderFieldCreatedAt,
|
||||||
|
Direction: page.OrderDirectionDesc,
|
||||||
|
}
|
||||||
|
if orderBy != nil {
|
||||||
|
pageOrderBy = page.OrderBy[coredata.InvitationOrderField]{
|
||||||
|
Field: orderBy.Field,
|
||||||
|
Direction: orderBy.Direction,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
|
||||||
|
|
||||||
|
invitationFilter := coredata.NewInvitationFilter(nil)
|
||||||
|
if filter != nil {
|
||||||
|
invitationFilter = coredata.NewInvitationFilter(filter.OnlyPending)
|
||||||
|
}
|
||||||
|
|
||||||
|
invitations, err := r.authzSvc.GetUserInvitations(ctx, user.EmailAddress, cursor, invitationFilter)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("failed to list invitations for user: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.NewInvitationConnection(invitations, r, gid.GID{}, filter), nil
|
||||||
|
}
|
||||||
|
|
||||||
// Asset returns schema.AssetResolver implementation.
|
// Asset returns schema.AssetResolver implementation.
|
||||||
func (r *Resolver) Asset() schema.AssetResolver { return &assetResolver{r} }
|
func (r *Resolver) Asset() schema.AssetResolver { return &assetResolver{r} }
|
||||||
|
|
||||||
@@ -5432,6 +5443,9 @@ func (r *Resolver) FrameworkConnection() schema.FrameworkConnectionResolver {
|
|||||||
return &frameworkConnectionResolver{r}
|
return &frameworkConnectionResolver{r}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Invitation returns schema.InvitationResolver implementation.
|
||||||
|
func (r *Resolver) Invitation() schema.InvitationResolver { return &invitationResolver{r} }
|
||||||
|
|
||||||
// InvitationConnection returns schema.InvitationConnectionResolver implementation.
|
// InvitationConnection returns schema.InvitationConnectionResolver implementation.
|
||||||
func (r *Resolver) InvitationConnection() schema.InvitationConnectionResolver {
|
func (r *Resolver) InvitationConnection() schema.InvitationConnectionResolver {
|
||||||
return &invitationConnectionResolver{r}
|
return &invitationConnectionResolver{r}
|
||||||
@@ -5541,9 +5555,6 @@ func (r *Resolver) TrustCenterReferenceConnection() schema.TrustCenterReferenceC
|
|||||||
return &trustCenterReferenceConnectionResolver{r}
|
return &trustCenterReferenceConnectionResolver{r}
|
||||||
}
|
}
|
||||||
|
|
||||||
// User returns schema.UserResolver implementation.
|
|
||||||
func (r *Resolver) User() schema.UserResolver { return &userResolver{r} }
|
|
||||||
|
|
||||||
// UserConnection returns schema.UserConnectionResolver implementation.
|
// UserConnection returns schema.UserConnectionResolver implementation.
|
||||||
func (r *Resolver) UserConnection() schema.UserConnectionResolver { return &userConnectionResolver{r} }
|
func (r *Resolver) UserConnection() schema.UserConnectionResolver { return &userConnectionResolver{r} }
|
||||||
|
|
||||||
@@ -5603,6 +5614,7 @@ type evidenceConnectionResolver struct{ *Resolver }
|
|||||||
type fileResolver struct{ *Resolver }
|
type fileResolver struct{ *Resolver }
|
||||||
type frameworkResolver struct{ *Resolver }
|
type frameworkResolver struct{ *Resolver }
|
||||||
type frameworkConnectionResolver struct{ *Resolver }
|
type frameworkConnectionResolver struct{ *Resolver }
|
||||||
|
type invitationResolver struct{ *Resolver }
|
||||||
type invitationConnectionResolver struct{ *Resolver }
|
type invitationConnectionResolver struct{ *Resolver }
|
||||||
type measureResolver struct{ *Resolver }
|
type measureResolver struct{ *Resolver }
|
||||||
type measureConnectionResolver struct{ *Resolver }
|
type measureConnectionResolver struct{ *Resolver }
|
||||||
@@ -5630,7 +5642,6 @@ type trustCenterDocumentAccessResolver struct{ *Resolver }
|
|||||||
type trustCenterDocumentAccessConnectionResolver struct{ *Resolver }
|
type trustCenterDocumentAccessConnectionResolver struct{ *Resolver }
|
||||||
type trustCenterReferenceResolver struct{ *Resolver }
|
type trustCenterReferenceResolver struct{ *Resolver }
|
||||||
type trustCenterReferenceConnectionResolver struct{ *Resolver }
|
type trustCenterReferenceConnectionResolver struct{ *Resolver }
|
||||||
type userResolver struct{ *Resolver }
|
|
||||||
type userConnectionResolver struct{ *Resolver }
|
type userConnectionResolver struct{ *Resolver }
|
||||||
type vendorResolver struct{ *Resolver }
|
type vendorResolver struct{ *Resolver }
|
||||||
type vendorBusinessAssociateAgreementResolver struct{ *Resolver }
|
type vendorBusinessAssociateAgreementResolver struct{ *Resolver }
|
||||||
|
|||||||
Reference in New Issue
Block a user