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...", "inviteMembersLoading": "Inviting members...",
"removeInviteButtonLabel": "Remove invite", "removeInviteButtonLabel": "Remove invite",
"addAnotherMemberButtonLabel": "Add another one", "addAnotherMemberButtonLabel": "Add another one",
"inviteMembersSubmitLabel": "Send Invites", "inviteMembersButtonLabel": "Send Invites",
"removeMemberModalHeading": "You are removing this user", "removeMemberModalHeading": "You are removing this user",
"removeMemberModalDescription": "Remove this member from the team. They will no longer have access to the team.", "removeMemberModalDescription": "Remove this member from the team. They will no longer have access to the team.",
"removeMemberSuccessMessage": "Member removed successfully", "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.", "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}}", "joinTeam": "Join {{accountName}}",
"joinTeamAccount": "Join Team", "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; const userIsPrimaryOwner = user?.id === primaryOwnerUserId;
if (userIsPrimaryOwner) { if (userIsPrimaryOwner) {
return <DeleteTeamContainer account={account} />; return <LeaveTeamContainer account={account} />;
} }
return <LeaveTeamContainer account={account} />; return <LeaveTeamContainer account={account} />;
@@ -240,6 +240,20 @@ function LeaveTeamContainer(props: {
id: string; 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 ( return (
<div className={'flex flex-col space-y-4'}> <div className={'flex flex-col space-y-4'}>
<p className={'text-muted-foreground text-sm'}> <p className={'text-muted-foreground text-sm'}>
@@ -276,18 +290,58 @@ function LeaveTeamContainer(props: {
</AlertDialogHeader> </AlertDialogHeader>
<ErrorBoundary fallback={<LeaveTeamErrorAlert />}> <ErrorBoundary fallback={<LeaveTeamErrorAlert />}>
<form action={leaveTeamAccountAction}> <Form {...form}>
<input type={'hidden'} value={props.account.id} name={'id'} /> <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> </ErrorBoundary>
<AlertDialogFooter>
<AlertDialogCancel>
<Trans i18nKey={'common:cancel'} />
</AlertDialogCancel>
<LeaveTeamSubmitButton />
</AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
</div> </div>
@@ -310,15 +364,23 @@ function LeaveTeamSubmitButton() {
function LeaveTeamErrorAlert() { function LeaveTeamErrorAlert() {
return ( return (
<Alert variant={'destructive'}> <>
<AlertTitle> <Alert variant={'destructive'}>
<Trans i18nKey={'teams:leaveTeamErrorHeading'} /> <AlertTitle>
</AlertTitle> <Trans i18nKey={'teams:leaveTeamErrorHeading'} />
</AlertTitle>
<AlertDescription> <AlertDescription>
<Trans i18nKey={'common:genericError'} /> <Trans i18nKey={'common:genericError'} />
</AlertDescription> </AlertDescription>
</Alert> </Alert>
<AlertDialogFooter>
<AlertDialogCancel>
<Trans i18nKey={'common:cancel'} />
</AlertDialogCancel>
</AlertDialogFooter>
</>
); );
} }

View File

@@ -2,4 +2,5 @@ import { z } from 'zod';
export const LeaveTeamAccountSchema = z.object({ export const LeaveTeamAccountSchema = z.object({
accountId: z.string(), 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 'server-only';
import { z } from 'zod'; import { z } from 'zod';
import { Logger } from '@kit/shared/logger';
import { Database } from '@kit/supabase/database'; import { Database } from '@kit/supabase/database';
const Schema = z.object({ const Schema = z.object({
@@ -11,9 +12,18 @@ const Schema = z.object({
}); });
export class LeaveTeamAccountService { export class LeaveTeamAccountService {
private readonly namespace = 'leave-team-account';
constructor(private readonly adminClient: SupabaseClient<Database>) {} constructor(private readonly adminClient: SupabaseClient<Database>) {}
async leaveTeamAccount(params: z.infer<typeof Schema>) { 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 { accountId, userId } = Schema.parse(params);
const { error } = await this.adminClient const { error } = await this.adminClient
@@ -25,7 +35,11 @@ export class LeaveTeamAccountService {
}); });
if (error) { 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 -- Enable RLS on the accounts_memberships table
alter table public.accounts_memberships enable row level security; 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 create
or replace function public.has_role_on_account ( or replace function public.has_role_on_account (
account_id uuid, account_id uuid,
@@ -572,6 +588,8 @@ using (
) )
); );
/* /*
* ------------------------------------------------------- * -------------------------------------------------------
* Section: Account Roles * Section: Account Roles