diff --git a/apps/console/src/pages/iam/organizations/people/PersonPage.tsx b/apps/console/src/pages/iam/organizations/people/PersonPage.tsx index 5b74ef9e1..fd73eed68 100644 --- a/apps/console/src/pages/iam/organizations/people/PersonPage.tsx +++ b/apps/console/src/pages/iam/organizations/people/PersonPage.tsx @@ -67,8 +67,8 @@ export function PersonPage(props: { queryRef: PreloadedQuery }) const [removeUser, isRemoving] = useMutationWithToasts( removeUserMutation, { - successMessage: __("Person removed successfully"), - errorMessage: __("Failed to remove person"), + successMessage: __("Person archived successfully"), + errorMessage: __("Failed to archive person"), }, ); @@ -89,13 +89,15 @@ export function PersonPage(props: { queryRef: PreloadedQuery }) }, { message: sprintf( - __("Are you sure you want to remove %s?"), + __("Are you sure you want to archive %s?"), person.fullName, ), }, ); }; + const canArchive = person.canDelete && person.source !== "SCIM"; + return (
})
{person.emailAddress}
- {person.canDelete && person.source !== "SCIM" && ( + {canArchive && ( }) onClick={handleRemove} disabled={isRemoving} > - {__("Delete")} + {__("Archive")} )} diff --git a/apps/console/src/pages/iam/organizations/people/_components/PeopleListItem.tsx b/apps/console/src/pages/iam/organizations/people/_components/PeopleListItem.tsx index 9459d824b..e6ec90de9 100644 --- a/apps/console/src/pages/iam/organizations/people/_components/PeopleListItem.tsx +++ b/apps/console/src/pages/iam/organizations/people/_components/PeopleListItem.tsx @@ -112,7 +112,7 @@ export function PeopleListItem(props: { fKey: PeopleListItemFragment$key; onRefetch: () => void; }) { - const { fKey, connectionId } = props; + const { fKey, connectionId, onRefetch } = props; const organizationId = useOrganizationId(); const { __ } = useTranslate(); @@ -127,7 +127,7 @@ export function PeopleListItem(props: { const isInactive = profile.state === "INACTIVE"; const canSendActivationMail = isInactive && profile.source !== "SCIM" && profile.canInvite; - const canDelete = profile.canDelete && profile.source !== "SCIM"; + const canArchive = profile.canDelete && profile.source !== "SCIM" && profile.state !== "INACTIVE"; const [inviteUser] = useMutationWithToasts(inviteUserMutation, { @@ -144,8 +144,8 @@ export function PeopleListItem(props: { const [removeUser, isRemoving] = useMutationWithToasts( removeUserMutation, { - successMessage: __("Person removed successfully"), - errorMessage: __("Failed to remove person"), + successMessage: __("Person archived successfully"), + errorMessage: __("Failed to archive person"), }, ); @@ -194,11 +194,14 @@ export function PeopleListItem(props: { }, connections: [connectionId], }, + onCompleted: () => { + onRefetch(); + }, }); }, { message: sprintf( - __("Are you sure you want to remove %s?"), + __("Are you sure you want to archive %s?"), profile.fullName, ), }, @@ -267,7 +270,7 @@ export function PeopleListItem(props: { {new Date(profile.createdAt).toLocaleDateString()} - {(canSendActivationMail || canDelete) && ( + {(canSendActivationMail || canArchive) && ( {canSendActivationMail && ( )} - {canDelete && ( + {canArchive && ( - {__("Remove person")} + {__("Archive person")} )} diff --git a/packages/n8n-node/nodes/Probo/actions/user/index.ts b/packages/n8n-node/nodes/Probo/actions/user/index.ts index 7100cb303..e5bd1b998 100644 --- a/packages/n8n-node/nodes/Probo/actions/user/index.ts +++ b/packages/n8n-node/nodes/Probo/actions/user/index.ts @@ -58,10 +58,10 @@ export const description: INodeProperties[] = [ action: 'List users', }, { - name: 'Remove', + name: 'Archive', value: 'removeUser', - description: 'Remove a user from the organization', - action: 'Remove a user', + description: 'Archive a user in the organization', + action: 'Archive a user', }, { name: 'Update', diff --git a/packages/n8n-node/nodes/Probo/actions/user/removeUser.operation.ts b/packages/n8n-node/nodes/Probo/actions/user/removeUser.operation.ts index 5ce261d9b..d8d756ecb 100644 --- a/packages/n8n-node/nodes/Probo/actions/user/removeUser.operation.ts +++ b/packages/n8n-node/nodes/Probo/actions/user/removeUser.operation.ts @@ -41,7 +41,7 @@ export const description: INodeProperties[] = [ }, }, default: '', - description: 'The ID of the user (profile) to remove from the organization', + description: 'The ID of the user (profile) to archive in the organization', required: true, }, ]; @@ -56,7 +56,7 @@ export async function execute( const query = ` mutation RemoveUser($input: RemoveUserInput!) { removeUser(input: $input) { - deletedProfileId + archivedProfileId: deletedProfileId } } `; diff --git a/pkg/cmd/user/archive/archive.go b/pkg/cmd/user/archive/archive.go new file mode 100644 index 000000000..346a8fe23 --- /dev/null +++ b/pkg/cmd/user/archive/archive.go @@ -0,0 +1,114 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 archive + +import ( + "fmt" + + "github.com/charmbracelet/huh" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const archiveMutation = ` +mutation($input: RemoveUserInput!) { + removeUser(input: $input) { + deletedProfileId + } +} +` + +func NewCmdArchive(f *cmdutil.Factory) *cobra.Command { + var ( + flagOrg string + flagYes bool + ) + + cmd := &cobra.Command{ + Use: "archive ", + Short: "Archive a user", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if !flagYes { + if !f.IOStreams.IsInteractive() { + return fmt.Errorf("cannot archive user: confirmation required, use --yes to confirm") + } + + var confirmed bool + + err := huh.NewConfirm(). + Title(fmt.Sprintf("Archive user %s?", args[0])). + Value(&confirmed). + Run() + if err != nil { + return err + } + + if !confirmed { + return nil + } + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + if flagOrg == "" { + flagOrg = hc.Organization + } + + if flagOrg == "" { + return fmt.Errorf("organization is required; pass --org or set a default with 'prb auth login'") + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + _, err = client.Do( + archiveMutation, + map[string]any{ + "input": map[string]any{ + "organizationId": flagOrg, + "profileId": args[0], + }, + }, + ) + if err != nil { + return err + } + + _, _ = fmt.Fprintf(f.IOStreams.Out, "Archived user %s\n", args[0]) + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt") + + return cmd +} diff --git a/pkg/cmd/user/user.go b/pkg/cmd/user/user.go index 456acc1de..6d23fc312 100644 --- a/pkg/cmd/user/user.go +++ b/pkg/cmd/user/user.go @@ -17,6 +17,7 @@ package user import ( "github.com/spf13/cobra" "go.probo.inc/probo/pkg/cmd/cmdutil" + "go.probo.inc/probo/pkg/cmd/user/archive" "go.probo.inc/probo/pkg/cmd/user/list" "go.probo.inc/probo/pkg/cmd/user/view" ) @@ -29,6 +30,7 @@ func NewCmdUser(f *cmdutil.Factory) *cobra.Command { cmd.AddCommand(list.NewCmdList(f)) cmd.AddCommand(view.NewCmdView(f)) + cmd.AddCommand(archive.NewCmdArchive(f)) return cmd } diff --git a/pkg/server/api/connect/v1/profile_resolvers.go b/pkg/server/api/connect/v1/profile_resolvers.go index 26fe2caa7..89861421c 100644 --- a/pkg/server/api/connect/v1/profile_resolvers.go +++ b/pkg/server/api/connect/v1/profile_resolvers.go @@ -113,15 +113,15 @@ func (r *mutationResolver) RemoveUser(ctx context.Context, input types.RemoveUse err := r.iam.OrganizationService.RemoveUser(ctx, input.OrganizationID, input.ProfileID) if err != nil { if _, ok := errors.AsType[*iam.ErrUserManagedBySCIM](err); ok { - return nil, gqlutils.Conflict(ctx, err) + return nil, gqlutils.Conflictf(ctx, "user is managed by SCIM and cannot be archived") } if _, ok := errors.AsType[*iam.ErrLastActiveOwner](err); ok { - return nil, gqlutils.Conflict(ctx, err) + return nil, gqlutils.Conflictf(ctx, "cannot archive last active owner") } if errors.Is(err, coredata.ErrResourceInUse) { - return nil, gqlutils.Conflict(ctx, err) + return nil, gqlutils.Conflictf(ctx, "cannot archive user") } r.logger.ErrorCtx(ctx, "cannot remove user from organization", log.Error(err)) diff --git a/pkg/server/api/mcp/v1/schema.resolvers.go b/pkg/server/api/mcp/v1/schema.resolvers.go index 1063e8fef..f8242a83d 100644 --- a/pkg/server/api/mcp/v1/schema.resolvers.go +++ b/pkg/server/api/mcp/v1/schema.resolvers.go @@ -2923,21 +2923,21 @@ func (r *Resolver) RemoveUserTool(ctx context.Context, req *mcp.CallToolRequest, err := r.iamSvc.OrganizationService.RemoveUser(ctx, input.OrganizationID, input.ProfileID) if err != nil { if _, ok := errors.AsType[*iam.ErrUserManagedBySCIM](err); ok { - return nil, types.RemoveUserOutput{}, fmt.Errorf("user is managed by SCIM and cannot be removed: %w", err) + return nil, types.RemoveUserOutput{}, fmt.Errorf("user is managed by SCIM and cannot be archived: %w", err) } if _, ok := errors.AsType[*iam.ErrLastActiveOwner](err); ok { - return nil, types.RemoveUserOutput{}, fmt.Errorf("cannot remove last active owner: %w", err) + return nil, types.RemoveUserOutput{}, fmt.Errorf("cannot archive last active owner: %w", err) } if errors.Is(err, coredata.ErrResourceInUse) { - return nil, types.RemoveUserOutput{}, fmt.Errorf("cannot remove user: %w", err) + return nil, types.RemoveUserOutput{}, fmt.Errorf("cannot archive user: %w", err) } - return nil, types.RemoveUserOutput{}, fmt.Errorf("remove user: %w", err) + return nil, types.RemoveUserOutput{}, fmt.Errorf("archive user: %w", err) } - return nil, types.RemoveUserOutput{DeletedUserID: input.ProfileID}, nil + return nil, types.RemoveUserOutput{ArchivedUserID: input.ProfileID}, nil } func (r *Resolver) DeleteDataProtectionImpactAssessmentTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteDataProtectionImpactAssessmentInput) (*mcp.CallToolResult, types.DeleteDataProtectionImpactAssessmentOutput, error) { diff --git a/pkg/server/api/mcp/v1/specification.yaml b/pkg/server/api/mcp/v1/specification.yaml index 6cda911d7..6ae919cda 100644 --- a/pkg/server/api/mcp/v1/specification.yaml +++ b/pkg/server/api/mcp/v1/specification.yaml @@ -1709,16 +1709,16 @@ components: description: Organization ID profile_id: $ref: "#/components/schemas/GID" - description: User (profile) ID to remove + description: User (profile) ID to archive RemoveUserOutput: type: object required: - - deleted_user_id + - archived_user_id properties: - deleted_user_id: + archived_user_id: $ref: "#/components/schemas/GID" - description: Deleted user (profile) ID + description: Archived user (profile) ID GetProfileInput: type: object @@ -11934,7 +11934,7 @@ tools: outputSchema: $ref: "#/components/schemas/UpdateMembershipOutput" - name: removeUser - description: Remove a user from the organization + description: Archive a user in the organization hints: readonly: false inputSchema: