Disable activation on scim profiles

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-02-18 17:14:43 +04:00
parent 13aef29ddf
commit 9941a6a66a
12 changed files with 163 additions and 287 deletions

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<c5c251783bc35aea15dec0c06f6ef1fb>> * @generated SignedSource<<a32e2b4330ac1d54762817127289b51d>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -301,6 +301,13 @@ return {
"name": "fullName", "name": "fullName",
"storageKey": null "storageKey": null
}, },
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "emailAddress",
"storageKey": null
},
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -333,25 +340,6 @@ return {
], ],
"storageKey": null "storageKey": null
}, },
{
"alias": null,
"args": null,
"concreteType": "Identity",
"kind": "LinkedField",
"name": "identity",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "email",
"storageKey": null
},
(v12/*: any*/)
],
"storageKey": null
},
{ {
"alias": "lastInvitation", "alias": "lastInvitation",
"args": (v14/*: any*/), "args": (v14/*: any*/),
@@ -377,20 +365,6 @@ return {
"plural": false, "plural": false,
"selections": [ "selections": [
(v12/*: any*/), (v12/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "expiresAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "acceptedAt",
"storageKey": null
},
(v15/*: any*/), (v15/*: any*/),
(v11/*: any*/) (v11/*: any*/)
], ],
@@ -524,12 +498,12 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "f25ca8641478e79fb2ccf6fa825fe042", "cacheID": "15a5dd79df1b16aa1a66ccb8024f9627",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "PeopleListFragment_RefetchQuery", "name": "PeopleListFragment_RefetchQuery",
"operationKind": "query", "operationKind": "query",
"text": "query PeopleListFragment_RefetchQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 20\n $last: Int = null\n $order: ProfileOrder = {direction: ASC, field: FULL_NAME}\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...PeopleListFragment_16fISc\n id\n }\n}\n\nfragment PeopleListFragment_16fISc on Organization {\n profiles(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n totalCount\n edges {\n node {\n id\n ...PeopleListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment PeopleListItemFragment on Profile {\n id\n source\n state\n fullName\n membership {\n id\n role\n canUpdate: permission(action: \"iam:membership:update\")\n }\n identity {\n email\n id\n }\n lastInvitation: pendingInvitations(first: 1, orderBy: {field: CREATED_AT, direction: DESC}) {\n edges {\n node {\n id\n expiresAt\n acceptedAt\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n createdAt\n canUpdate: permission(action: \"iam:membership-profile:update\")\n canInvite: permission(action: \"iam:invitation:create\")\n canDelete: permission(action: \"iam:membership-profile:delete\")\n}\n" "text": "query PeopleListFragment_RefetchQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 20\n $last: Int = null\n $order: ProfileOrder = {direction: ASC, field: FULL_NAME}\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...PeopleListFragment_16fISc\n id\n }\n}\n\nfragment PeopleListFragment_16fISc on Organization {\n profiles(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n totalCount\n edges {\n node {\n id\n ...PeopleListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment PeopleListItemFragment on Profile {\n id\n source\n state\n fullName\n emailAddress\n membership {\n id\n role\n canUpdate: permission(action: \"iam:membership:update\")\n }\n lastInvitation: pendingInvitations(first: 1, orderBy: {field: CREATED_AT, direction: DESC}) {\n edges {\n node {\n id\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n createdAt\n canUpdate: permission(action: \"iam:membership-profile:update\")\n canInvite: permission(action: \"iam:invitation:create\")\n canDelete: permission(action: \"iam:membership-profile:delete\")\n}\n"
} }
}; };
})(); })();

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<91bec60a40a9170868272814635ea3fa>> * @generated SignedSource<<f8383da9cb37af6caca48c69a809346c>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -17,18 +17,14 @@ export type PeopleListItemFragment$data = {
readonly canInvite: boolean; readonly canInvite: boolean;
readonly canUpdate: boolean; readonly canUpdate: boolean;
readonly createdAt: string; readonly createdAt: string;
readonly emailAddress: string;
readonly fullName: string; readonly fullName: string;
readonly id: string; readonly id: string;
readonly identity: {
readonly email: string;
};
readonly lastInvitation: { readonly lastInvitation: {
readonly __id: string; readonly __id: string;
readonly edges: ReadonlyArray<{ readonly edges: ReadonlyArray<{
readonly node: { readonly node: {
readonly acceptedAt: string | null | undefined;
readonly createdAt: string; readonly createdAt: string;
readonly expiresAt: string;
readonly id: string; readonly id: string;
}; };
}>; }>;
@@ -101,6 +97,13 @@ return {
"name": "fullName", "name": "fullName",
"storageKey": null "storageKey": null
}, },
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "emailAddress",
"storageKey": null
},
{ {
"kind": "RequiredField", "kind": "RequiredField",
"field": { "field": {
@@ -137,28 +140,6 @@ return {
}, },
"action": "THROW" "action": "THROW"
}, },
{
"kind": "RequiredField",
"field": {
"alias": null,
"args": null,
"concreteType": "Identity",
"kind": "LinkedField",
"name": "identity",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "email",
"storageKey": null
}
],
"storageKey": null
},
"action": "THROW"
},
{ {
"kind": "RequiredField", "kind": "RequiredField",
"field": { "field": {
@@ -195,20 +176,6 @@ return {
"plural": false, "plural": false,
"selections": [ "selections": [
(v0/*: any*/), (v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "expiresAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "acceptedAt",
"storageKey": null
},
(v1/*: any*/), (v1/*: any*/),
{ {
"alias": null, "alias": null,
@@ -318,6 +285,6 @@ return {
}; };
})(); })();
(node as any).hash = "b276a06948260a9d89947b386c6392d1"; (node as any).hash = "f10aa3067eeb7fc95eb7c7a893e7dd6e";
export default node; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<e32759f25b173edfe9c63fb659d7e75e>> * @generated SignedSource<<86b86e1209fbe58b62c2be028fb0482c>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -266,6 +266,13 @@ return {
"name": "fullName", "name": "fullName",
"storageKey": null "storageKey": null
}, },
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "emailAddress",
"storageKey": null
},
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -298,25 +305,6 @@ return {
], ],
"storageKey": null "storageKey": null
}, },
{
"alias": null,
"args": null,
"concreteType": "Identity",
"kind": "LinkedField",
"name": "identity",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "email",
"storageKey": null
},
(v6/*: any*/)
],
"storageKey": null
},
{ {
"alias": "lastInvitation", "alias": "lastInvitation",
"args": (v8/*: any*/), "args": (v8/*: any*/),
@@ -342,20 +330,6 @@ return {
"plural": false, "plural": false,
"selections": [ "selections": [
(v6/*: any*/), (v6/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "expiresAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "acceptedAt",
"storageKey": null
},
(v9/*: any*/), (v9/*: any*/),
(v2/*: any*/) (v2/*: any*/)
], ],
@@ -489,12 +463,12 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "a5d551184b2abe39feea3dfb05bfe21d", "cacheID": "af366ea24d2c96763b104acc69d9b3ff",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "PeoplePageQuery", "name": "PeoplePageQuery",
"operationKind": "query", "operationKind": "query",
"text": "query PeoplePageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n canCreateUser: permission(action: \"iam:membership-profile:create\")\n ...PeopleListFragment_8lnpd\n }\n id\n }\n}\n\nfragment PeopleListFragment_8lnpd on Organization {\n profiles(first: 20, orderBy: {direction: ASC, field: FULL_NAME}) {\n totalCount\n edges {\n node {\n id\n ...PeopleListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment PeopleListItemFragment on Profile {\n id\n source\n state\n fullName\n membership {\n id\n role\n canUpdate: permission(action: \"iam:membership:update\")\n }\n identity {\n email\n id\n }\n lastInvitation: pendingInvitations(first: 1, orderBy: {field: CREATED_AT, direction: DESC}) {\n edges {\n node {\n id\n expiresAt\n acceptedAt\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n createdAt\n canUpdate: permission(action: \"iam:membership-profile:update\")\n canInvite: permission(action: \"iam:invitation:create\")\n canDelete: permission(action: \"iam:membership-profile:delete\")\n}\n" "text": "query PeoplePageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n canCreateUser: permission(action: \"iam:membership-profile:create\")\n ...PeopleListFragment_8lnpd\n }\n id\n }\n}\n\nfragment PeopleListFragment_8lnpd on Organization {\n profiles(first: 20, orderBy: {direction: ASC, field: FULL_NAME}) {\n totalCount\n edges {\n node {\n id\n ...PeopleListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment PeopleListItemFragment on Profile {\n id\n source\n state\n fullName\n emailAddress\n membership {\n id\n role\n canUpdate: permission(action: \"iam:membership:update\")\n }\n lastInvitation: pendingInvitations(first: 1, orderBy: {field: CREATED_AT, direction: DESC}) {\n edges {\n node {\n id\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n createdAt\n canUpdate: permission(action: \"iam:membership-profile:update\")\n canInvite: permission(action: \"iam:invitation:create\")\n canDelete: permission(action: \"iam:membership-profile:delete\")\n}\n"
} }
}; };
})(); })();

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<a8e224db1200fa5aaa6edc17c09e7a35>> * @generated SignedSource<<1eea90ca68f7381b881c68f0489837cb>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -25,6 +25,7 @@ export type PersonFormFragment$data = {
readonly role: MembershipRole; readonly role: MembershipRole;
}; };
readonly position: string | null | undefined; readonly position: string | null | undefined;
readonly source: string;
readonly " $fragmentType": "PersonFormFragment"; readonly " $fragmentType": "PersonFormFragment";
}; };
export type PersonFormFragment$key = { export type PersonFormFragment$key = {
@@ -59,6 +60,13 @@ const node: ReaderFragment = {
"name": "emailAddress", "name": "emailAddress",
"storageKey": null "storageKey": null
}, },
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "source",
"storageKey": null
},
{ {
"kind": "RequiredField", "kind": "RequiredField",
"field": { "field": {
@@ -134,6 +142,6 @@ const node: ReaderFragment = {
"abstractKey": null "abstractKey": null
}; };
(node as any).hash = "4f99d9b9ed708290c5a7d7ec258a0b1d"; (node as any).hash = "37e8a56333b217e5b6d3b94b455d7d40";
export default node; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<85bf3907699ac605e0603c8d1ae6ef8b>> * @generated SignedSource<<7889e38e185230a2c5b41c3b0fb73cca>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -195,6 +195,13 @@ return {
"name": "fullName", "name": "fullName",
"storageKey": null "storageKey": null
}, },
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "emailAddress",
"storageKey": null
},
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -227,25 +234,6 @@ return {
], ],
"storageKey": null "storageKey": null
}, },
{
"alias": null,
"args": null,
"concreteType": "Identity",
"kind": "LinkedField",
"name": "identity",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "email",
"storageKey": null
},
(v3/*: any*/)
],
"storageKey": null
},
{ {
"alias": "lastInvitation", "alias": "lastInvitation",
"args": (v4/*: any*/), "args": (v4/*: any*/),
@@ -271,20 +259,6 @@ return {
"plural": false, "plural": false,
"selections": [ "selections": [
(v3/*: any*/), (v3/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "expiresAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "acceptedAt",
"storageKey": null
},
(v5/*: any*/), (v5/*: any*/),
{ {
"alias": null, "alias": null,
@@ -425,12 +399,12 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "afbd8ffc5b5ae7156a108a2d1b25440b", "cacheID": "738254d707f8ed725c30ee87baa585fb",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "PersonForm_createMutation", "name": "PersonForm_createMutation",
"operationKind": "mutation", "operationKind": "mutation",
"text": "mutation PersonForm_createMutation(\n $input: CreateUserInput!\n) {\n createUser(input: $input) {\n profileEdge {\n node {\n ...PeopleListItemFragment\n id\n }\n }\n }\n}\n\nfragment PeopleListItemFragment on Profile {\n id\n source\n state\n fullName\n membership {\n id\n role\n canUpdate: permission(action: \"iam:membership:update\")\n }\n identity {\n email\n id\n }\n lastInvitation: pendingInvitations(first: 1, orderBy: {field: CREATED_AT, direction: DESC}) {\n edges {\n node {\n id\n expiresAt\n acceptedAt\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n createdAt\n canUpdate: permission(action: \"iam:membership-profile:update\")\n canInvite: permission(action: \"iam:invitation:create\")\n canDelete: permission(action: \"iam:membership-profile:delete\")\n}\n" "text": "mutation PersonForm_createMutation(\n $input: CreateUserInput!\n) {\n createUser(input: $input) {\n profileEdge {\n node {\n ...PeopleListItemFragment\n id\n }\n }\n }\n}\n\nfragment PeopleListItemFragment on Profile {\n id\n source\n state\n fullName\n emailAddress\n membership {\n id\n role\n canUpdate: permission(action: \"iam:membership:update\")\n }\n lastInvitation: pendingInvitations(first: 1, orderBy: {field: CREATED_AT, direction: DESC}) {\n edges {\n node {\n id\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n createdAt\n canUpdate: permission(action: \"iam:membership-profile:update\")\n canInvite: permission(action: \"iam:invitation:create\")\n canDelete: permission(action: \"iam:membership-profile:delete\")\n}\n"
} }
}; };
})(); })();

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<679cd9698980b9fdf8b330df8df3aac5>> * @generated SignedSource<<aa1188431214a8844e8ee1879bbada7e>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -17,11 +17,10 @@ export type PersonPageQuery$data = {
readonly person: { readonly person: {
readonly __typename: "Profile"; readonly __typename: "Profile";
readonly canDelete: boolean; readonly canDelete: boolean;
readonly emailAddress: string;
readonly fullName: string; readonly fullName: string;
readonly id: string; readonly id: string;
readonly identity: { readonly source: string;
readonly email: string;
};
readonly " $fragmentSpreads": FragmentRefs<"PersonFormFragment">; readonly " $fragmentSpreads": FragmentRefs<"PersonFormFragment">;
} | { } | {
// This will never be '%other', but we need some // This will never be '%other', but we need some
@@ -74,10 +73,17 @@ v5 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "email", "name": "emailAddress",
"storageKey": null "storageKey": null
}, },
v6 = { v6 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "source",
"storageKey": null
},
v7 = {
"alias": "canDelete", "alias": "canDelete",
"args": [ "args": [
{ {
@@ -113,23 +119,9 @@ return {
"selections": [ "selections": [
(v3/*: any*/), (v3/*: any*/),
(v4/*: any*/), (v4/*: any*/),
{ (v5/*: any*/),
"kind": "RequiredField",
"field": {
"alias": null,
"args": null,
"concreteType": "Identity",
"kind": "LinkedField",
"name": "identity",
"plural": false,
"selections": [
(v5/*: any*/)
],
"storageKey": null
},
"action": "THROW"
},
(v6/*: any*/), (v6/*: any*/),
(v7/*: any*/),
{ {
"args": null, "args": null,
"kind": "FragmentSpread", "kind": "FragmentSpread",
@@ -168,27 +160,9 @@ return {
"kind": "InlineFragment", "kind": "InlineFragment",
"selections": [ "selections": [
(v4/*: any*/), (v4/*: any*/),
{ (v5/*: any*/),
"alias": null,
"args": null,
"concreteType": "Identity",
"kind": "LinkedField",
"name": "identity",
"plural": false,
"selections": [
(v5/*: any*/),
(v3/*: any*/)
],
"storageKey": null
},
(v6/*: any*/), (v6/*: any*/),
{ (v7/*: any*/),
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "emailAddress",
"storageKey": null
},
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -266,16 +240,16 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "cb1cec2fd7c66b42f61d40e6a22b0d67", "cacheID": "f78728c7deb19de68b5b996c58ebd05e",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "PersonPageQuery", "name": "PersonPageQuery",
"operationKind": "query", "operationKind": "query",
"text": "query PersonPageQuery(\n $personId: ID!\n) {\n person: node(id: $personId) {\n __typename\n ... on Profile {\n id\n fullName\n identity {\n email\n id\n }\n canDelete: permission(action: \"iam:membership-profile:delete\")\n ...PersonFormFragment\n }\n id\n }\n}\n\nfragment PersonFormFragment on Profile {\n id\n fullName\n emailAddress\n membership {\n role\n id\n }\n kind\n position\n additionalEmailAddresses\n contractStartDate\n contractEndDate\n canUpdate: permission(action: \"iam:membership-profile:update\")\n}\n" "text": "query PersonPageQuery(\n $personId: ID!\n) {\n person: node(id: $personId) {\n __typename\n ... on Profile {\n id\n fullName\n emailAddress\n source\n canDelete: permission(action: \"iam:membership-profile:delete\")\n ...PersonFormFragment\n }\n id\n }\n}\n\nfragment PersonFormFragment on Profile {\n id\n fullName\n emailAddress\n source\n membership {\n role\n id\n }\n kind\n position\n additionalEmailAddresses\n contractStartDate\n contractEndDate\n canUpdate: permission(action: \"iam:membership-profile:update\")\n}\n"
} }
}; };
})(); })();
(node as any).hash = "c2954bc62ed4a3e3a9b1ca9596b68e1c"; (node as any).hash = "4aecfb7a9ff05a585ccc6b533fb75bec";
export default node; export default node;

View File

@@ -1,6 +1,6 @@
import { sprintf } from "@probo/helpers"; import { sprintf } from "@probo/helpers";
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
import { ActionDropdown, Avatar, Breadcrumb, Card, DropdownItem, IconTrashCan, useConfirm } from "@probo/ui"; import { ActionDropdown, Avatar, Badge, Breadcrumb, Card, DropdownItem, IconTrashCan, useConfirm } from "@probo/ui";
import { type PreloadedQuery, usePreloadedQuery } from "react-relay"; import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -18,9 +18,8 @@ export const personPageQuery = graphql`
... on Profile { ... on Profile {
id id
fullName fullName
identity @required(action: THROW) { emailAddress
email source
}
canDelete: permission(action: "iam:membership-profile:delete") canDelete: permission(action: "iam:membership-profile:delete")
...PersonFormFragment ...PersonFormFragment
} }
@@ -100,9 +99,11 @@ export function PersonPage(props: { queryRef: PreloadedQuery<PersonPageQuery> })
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
<Avatar name={person.fullName} size="xl" /> <Avatar name={person.fullName} size="xl" />
<div> <div>
<div className="flex items-center gap-2">
<div className="text-2xl">{person.fullName}</div> <span className="text-2xl">{person.fullName}</span>
<div className="text-lg text-txt-secondary">{person.identity.email}</div> <Badge variant="info">{person.source}</Badge>
</div>
<div className="text-lg text-txt-secondary">{person.emailAddress}</div>
</div> </div>
</div> </div>
{person.canDelete && ( {person.canDelete && (

View File

@@ -29,14 +29,12 @@ const fragment = graphql`
source source
state state
fullName fullName
emailAddress
membership @required(action: THROW) { membership @required(action: THROW) {
id id
role role
canUpdate: permission(action: "iam:membership:update") canUpdate: permission(action: "iam:membership:update")
} }
identity @required(action: THROW) {
email
}
lastInvitation: pendingInvitations(first: 1, orderBy: { field: CREATED_AT, direction: DESC }) lastInvitation: pendingInvitations(first: 1, orderBy: { field: CREATED_AT, direction: DESC })
@required(action: THROW) @required(action: THROW)
@connection(key: "PeopleListItem_lastInvitation") { @connection(key: "PeopleListItem_lastInvitation") {
@@ -44,8 +42,6 @@ const fragment = graphql`
edges { edges {
node { node {
id id
expiresAt
acceptedAt
createdAt createdAt
} }
} }
@@ -193,87 +189,85 @@ export function PeopleListItem(props: {
}; };
return ( return (
<> <Tr to={`/organizations/${organizationId}/people/${profile.id}`}>
<Tr to={`/organizations/${organizationId}/people/${profile.id}`}> <Td className={clsx(
<Td className={clsx( isRemoving && "opacity-60 pointer-events-none",
isInactive && "opacity-50",
)}
>
<span className="font-semibold">{profile.fullName}</span>
</Td>
<Td>
<Badge variant={profile.state === "INACTIVE" ? "neutral" : "success"}>{profile.state}</Badge>
</Td>
<Td className={clsx(
isRemoving && "opacity-60 pointer-events-none",
isInactive && "opacity-50",
)}
>
<div className="flex items-center gap-2">
{profile.emailAddress}
<Badge variant="info">{profile.source}</Badge>
</div>
</Td>
<Td
noLink
className={clsx(
"pr-4",
isRemoving && "opacity-60 pointer-events-none", isRemoving && "opacity-60 pointer-events-none",
isInactive && "opacity-50", isInactive && "opacity-50",
)} )}
>
<Select
disabled={!profile.membership.canUpdate || isUpdatingRole}
value={profile.membership.role}
onValueChange={role => void handleUpdateRole(role)}
> >
<span className="font-semibold">{profile.fullName}</span> {availableRoles.includes("OWNER") && (
</Td> <Option value="OWNER">{__("Owner")}</Option>
<Td>
<Badge variant={profile.state === "INACTIVE" ? "neutral" : "success"}>{profile.state}</Badge>
</Td>
<Td className={clsx(
isRemoving && "opacity-60 pointer-events-none",
isInactive && "opacity-50",
)}
>
<div className="flex items-center gap-2">
{profile.identity.email}
<Badge variant="info">{profile.source}</Badge>
</div>
</Td>
<Td
noLink
className={clsx(
"pr-4",
isRemoving && "opacity-60 pointer-events-none",
isInactive && "opacity-50",
)} )}
> {availableRoles.includes("ADMIN") && (
<Select <Option value="ADMIN">{__("Admin")}</Option>
disabled={!profile.membership.canUpdate || isUpdatingRole} )}
value={profile.membership.role} {availableRoles.includes("VIEWER") && (
onValueChange={role => void handleUpdateRole(role)} <Option value="VIEWER">{__("Viewer")}</Option>
> )}
{availableRoles.includes("OWNER") && ( {availableRoles.includes("AUDITOR") && (
<Option value="OWNER">{__("Owner")}</Option> <Option value="AUDITOR">{__("Auditor")}</Option>
)} )}
{availableRoles.includes("ADMIN") && ( {availableRoles.includes("EMPLOYEE") && (
<Option value="ADMIN">{__("Admin")}</Option> <Option value="EMPLOYEE">{__("Employee")}</Option>
)} )}
{availableRoles.includes("VIEWER") && ( </Select>
<Option value="VIEWER">{__("Viewer")}</Option> </Td>
)} <Td className={clsx(
{availableRoles.includes("AUDITOR") && ( isRemoving && "opacity-60 pointer-events-none",
<Option value="AUDITOR">{__("Auditor")}</Option> isInactive && "opacity-50",
)} )}
{availableRoles.includes("EMPLOYEE") && ( >
<Option value="EMPLOYEE">{__("Employee")}</Option> {new Date(profile.createdAt).toLocaleDateString()}
)} </Td>
</Select> <Td noLink width={160} className="text-end">
</Td> <ActionDropdown>
<Td className={clsx( {isInactive && profile.source !== "SCIM" && profile.canInvite && (
isRemoving && "opacity-60 pointer-events-none", <DropdownItem
isInactive && "opacity-50", onClick={handleInvite}
)} icon={IconMail}
> >
{new Date(profile.createdAt).toLocaleDateString()} {lastInvitation ? __("Resend activation mail") : __("Send activation mail")}
</Td> </DropdownItem>
<Td noLink width={160} className="text-end"> )}
<ActionDropdown> {profile.canDelete && profile.source !== "SCIM" && (
{isInactive && ( <DropdownItem
<DropdownItem onClick={handleRemove}
onClick={handleInvite} variant="danger"
icon={IconMail} icon={IconTrashCan}
> >
{lastInvitation ? __("Resend activation mail") : __("Send activation mail")} {__("Remove person")}
</DropdownItem> </DropdownItem>
)} )}
{profile.canDelete && ( </ActionDropdown>
<DropdownItem </Td>
onClick={handleRemove} </Tr>
variant="danger"
icon={IconTrashCan}
>
{__("Remove person")}
</DropdownItem>
)}
</ActionDropdown>
</Td>
</Tr>
</>
); );
} }

View File

@@ -23,6 +23,7 @@ const fragment = graphql`
id id
fullName fullName
emailAddress emailAddress
source
membership @required(action: THROW) { membership @required(action: THROW) {
role role
} }
@@ -268,7 +269,7 @@ export function PersonFormLoader(props: { fragmentRef: PersonFormFragment$key })
return ( return (
<PersonForm <PersonForm
id={person.id} id={person.id}
disabled={!person.canUpdate} disabled={!person.canUpdate || person.source === "SCIM"}
defaultValues={ defaultValues={
{ {
kind: person.kind, kind: person.kind,

View File

@@ -177,6 +177,10 @@ func (s *AuthService) ActivateAccount(
return fmt.Errorf("cannot load user: %w", err) return fmt.Errorf("cannot load user: %w", err)
} }
if profile.Source == coredata.ProfileSourceSCIM {
return NewUserManagedBySCIMError(profile.ID)
}
if profile.State == coredata.ProfileStateInactive { if profile.State == coredata.ProfileStateInactive {
profile.State = coredata.ProfileStateActive profile.State = coredata.ProfileStateActive
profile.UpdatedAt = now profile.UpdatedAt = now

View File

@@ -372,6 +372,10 @@ func (s *OrganizationService) InviteUser(
return fmt.Errorf("cannot load profile: %w", err) return fmt.Errorf("cannot load profile: %w", err)
} }
if profile.Source == coredata.ProfileSourceSCIM {
return NewUserManagedBySCIMError(profile.ID)
}
err = invitation.Insert(ctx, tx, scope) err = invitation.Insert(ctx, tx, scope)
if err != nil { if err != nil {
return fmt.Errorf("cannot insert invitation: %w", err) return fmt.Errorf("cannot insert invitation: %w", err)

View File

@@ -69,6 +69,7 @@ func NewProfile(profile *coredata.MembershipProfile) *Profile {
return &Profile{ return &Profile{
ID: profile.ID, ID: profile.ID,
FullName: profile.FullName, FullName: profile.FullName,
EmailAddress: profile.EmailAddress,
Source: profile.Source.String(), Source: profile.Source.String(),
State: profile.State, State: profile.State,
AdditionalEmailAddresses: profile.AdditionalEmailAddresses, AdditionalEmailAddresses: profile.AdditionalEmailAddresses,