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:
@@ -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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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'),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user