Improve 'Leave Team' process with logging and confirmation step

Added logging to the 'Leave Team' functionality to track user actions, and implemented a confirmation input to further validate a user's intent to leave a team. Also revised the user-facing prompt for more clarity on the team leaving process. Corresponding changes were applied to the relevant services and front-end components.
This commit is contained in:
giancarlo
2024-03-29 16:40:44 +08:00
parent 2b0fbc445b
commit 9e06d420bd
5 changed files with 120 additions and 23 deletions

View File

@@ -49,7 +49,7 @@
"inviteMembersLoading": "Inviting members...",
"removeInviteButtonLabel": "Remove invite",
"addAnotherMemberButtonLabel": "Add another one",
"inviteMembersSubmitLabel": "Send Invites",
"inviteMembersButtonLabel": "Send Invites",
"removeMemberModalHeading": "You are removing this user",
"removeMemberModalDescription": "Remove this member from the team. They will no longer have access to the team.",
"removeMemberSuccessMessage": "Member removed successfully",
@@ -153,5 +153,7 @@
"acceptInvitationDescription": "You have been invited to join the team {{accountName}}. If you wish to accept the invitation, please click the button below.",
"joinTeam": "Join {{accountName}}",
"joinTeamAccount": "Join Team",
"joiningTeam": "Joining team..."
"joiningTeam": "Joining team...",
"leaveTeamInputLabel": "Please type LEAVE to confirm leaving the team.",
"leaveTeamInputDescription": "By leaving the team, you will no longer have access to it."
}

View File

@@ -56,7 +56,7 @@ export function TeamAccountDangerZone({
const userIsPrimaryOwner = user?.id === primaryOwnerUserId;
if (userIsPrimaryOwner) {
return <DeleteTeamContainer account={account} />;
return <LeaveTeamContainer account={account} />;
}
return <LeaveTeamContainer account={account} />;
@@ -240,6 +240,20 @@ function LeaveTeamContainer(props: {
id: string;
};
}) {
const form = useForm({
resolver: zodResolver(
z.object({
confirmation: z.string().refine((value) => value === 'LEAVE', {
message: 'Confirmation required to leave team',
path: ['confirmation'],
}),
}),
),
defaultValues: {
confirmation: '',
},
});
return (
<div className={'flex flex-col space-y-4'}>
<p className={'text-muted-foreground text-sm'}>
@@ -276,18 +290,58 @@ function LeaveTeamContainer(props: {
</AlertDialogHeader>
<ErrorBoundary fallback={<LeaveTeamErrorAlert />}>
<form action={leaveTeamAccountAction}>
<input type={'hidden'} value={props.account.id} name={'id'} />
</form>
<Form {...form}>
<form
className={'flex flex-col space-y-4'}
action={leaveTeamAccountAction}
>
<input
type={'hidden'}
value={props.account.id}
name={'accountId'}
/>
<FormField
name={'confirmation'}
render={({ field }) => {
return (
<FormItem>
<FormLabel>
<Trans i18nKey={'teams:leaveTeamInputLabel'} />
</FormLabel>
<FormControl>
<Input
data-test="leave-team-input-field"
type="text"
className="w-full"
placeholder=""
pattern="LEAVE"
required
{...field}
/>
</FormControl>
<FormDescription>
<Trans i18nKey={'teams:leaveTeamInputDescription'} />
</FormDescription>
<FormMessage />
</FormItem>
);
}}
/>
<AlertDialogFooter>
<AlertDialogCancel>
<Trans i18nKey={'common:cancel'} />
</AlertDialogCancel>
<LeaveTeamSubmitButton />
</AlertDialogFooter>
</form>
</Form>
</ErrorBoundary>
<AlertDialogFooter>
<AlertDialogCancel>
<Trans i18nKey={'common:cancel'} />
</AlertDialogCancel>
<LeaveTeamSubmitButton />
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
@@ -310,15 +364,23 @@ function LeaveTeamSubmitButton() {
function LeaveTeamErrorAlert() {
return (
<Alert variant={'destructive'}>
<AlertTitle>
<Trans i18nKey={'teams:leaveTeamErrorHeading'} />
</AlertTitle>
<>
<Alert variant={'destructive'}>
<AlertTitle>
<Trans i18nKey={'teams:leaveTeamErrorHeading'} />
</AlertTitle>
<AlertDescription>
<Trans i18nKey={'common:genericError'} />
</AlertDescription>
</Alert>
<AlertDescription>
<Trans i18nKey={'common:genericError'} />
</AlertDescription>
</Alert>
<AlertDialogFooter>
<AlertDialogCancel>
<Trans i18nKey={'common:cancel'} />
</AlertDialogCancel>
</AlertDialogFooter>
</>
);
}

View File

@@ -2,4 +2,5 @@ import { z } from 'zod';
export const LeaveTeamAccountSchema = z.object({
accountId: z.string(),
confirmation: z.custom((value) => value === 'LEAVE'),
});

View File

@@ -3,6 +3,7 @@ import { SupabaseClient } from '@supabase/supabase-js';
import 'server-only';
import { z } from 'zod';
import { Logger } from '@kit/shared/logger';
import { Database } from '@kit/supabase/database';
const Schema = z.object({
@@ -11,9 +12,18 @@ const Schema = z.object({
});
export class LeaveTeamAccountService {
private readonly namespace = 'leave-team-account';
constructor(private readonly adminClient: SupabaseClient<Database>) {}
async leaveTeamAccount(params: z.infer<typeof Schema>) {
const ctx = {
...params,
name: this.namespace,
};
Logger.info(ctx, 'Leaving team account');
const { accountId, userId } = Schema.parse(params);
const { error } = await this.adminClient
@@ -25,7 +35,11 @@ export class LeaveTeamAccountService {
});
if (error) {
throw error;
Logger.error({ ...ctx, error }, 'Failed to leave team account');
throw new Error('Failed to leave team account');
}
Logger.info(ctx, 'Successfully left team account');
}
}

View File

@@ -453,6 +453,22 @@ delete on table public.accounts_memberships to service_role;
-- Enable RLS on the accounts_memberships table
alter table public.accounts_memberships enable row level security;
-- Trigger to prevent a primary owner from being removed from an account
create
or replace function kit.prevent_account_owner_membership_delete () returns trigger as $$
begin
if exists (select 1 from public.accounts where id = old.account_id and primary_owner_user_id = old.user_id) then
raise exception 'The primary account owner cannot be removed from the account membership list';
end if;
return old;
end;
$$ language plpgsql;
create or replace trigger prevent_account_owner_membership_delete_check before delete
on public.accounts_memberships for each row
execute function kit.prevent_account_owner_membership_delete ();
create
or replace function public.has_role_on_account (
account_id uuid,
@@ -572,6 +588,8 @@ using (
)
);
/*
* -------------------------------------------------------
* Section: Account Roles