Update account management features and improve test configurations
Multiple updates are made to refine the account management features, including updating the 'Your Teams' text to show the number of teams, and modifying the form data validation process in the 'deletePersonalAccountAction' service. Additionally, improvements have been made in test configurations including updating the test timeout settings, taking screenshots when a test fails, and adjusting the location for saving Playwright reports.
This commit is contained in:
2
.github/workflows/workflow.yml
vendored
2
.github/workflows/workflow.yml
vendored
@@ -95,5 +95,5 @@ jobs:
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
path: apps/e2e/playwright-report/
|
||||
retention-days: 7
|
||||
@@ -25,6 +25,9 @@ export default defineConfig({
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: 'http://localhost:3000',
|
||||
|
||||
// take a screenshot when a test fails
|
||||
screenshot: "on",
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
|
||||
@@ -55,7 +55,9 @@ test.describe('Account Deletion', () => {
|
||||
await account.setup();
|
||||
await account.deleteAccount();
|
||||
|
||||
await page.waitForURL('http://localhost:3000');
|
||||
await page.waitForURL('http://localhost:3000', {
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
expect(page.url()).toEqual('http://localhost:3000/');
|
||||
});
|
||||
|
||||
@@ -38,7 +38,9 @@ export class TeamAccountsPageObject {
|
||||
await this.page.fill('[data-test="create-team-form"] input', teamName);
|
||||
await this.page.click('[data-test="create-team-form"] button:last-child');
|
||||
|
||||
await this.page.waitForURL(`http://localhost:3000/home/${slug}`);
|
||||
await this.page.waitForURL(`http://localhost:3000/home/${slug}`, {
|
||||
timeout: 5000,
|
||||
});
|
||||
}
|
||||
|
||||
async updateName(name: string) {
|
||||
@@ -56,7 +58,7 @@ export class TeamAccountsPageObject {
|
||||
}
|
||||
|
||||
createTeamName() {
|
||||
const random = (Math.random() * 1000000000).toFixed(0);
|
||||
const random = (Math.random() * 10).toFixed(0);
|
||||
|
||||
const teamName = `Team-Name-${random}`;
|
||||
const slug = `team-name-${random}`;
|
||||
|
||||
@@ -35,7 +35,9 @@ test.describe('Account Deletion', () => {
|
||||
|
||||
await teamAccounts.deleteAccount(params.teamName);
|
||||
|
||||
await page.waitForURL('http://localhost:3000/home');
|
||||
await page.waitForURL('http://localhost:3000/home', {
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
expect(page.url()).toEqual('http://localhost:3000/home');
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"billing": {
|
||||
"pageTitle": "Billing"
|
||||
},
|
||||
"yourTeams": "Your Teams",
|
||||
"yourTeams": "Your Teams ({{teamsCount}})",
|
||||
"createTeam": "Create a Team",
|
||||
"personalAccount": "Personal Account",
|
||||
"searchAccount": "Search Account...",
|
||||
|
||||
@@ -16,8 +16,6 @@ create extension if not exists "unaccent";
|
||||
-- Create a private Makerkit schema
|
||||
create schema if not exists kit;
|
||||
|
||||
grant USAGE on schema kit to authenticated, authenticated;
|
||||
|
||||
-- We remove all default privileges from public schema on functions to
|
||||
-- prevent public access to them
|
||||
alter default privileges revoke execute on functions from public;
|
||||
@@ -1595,6 +1593,8 @@ grant execute on function kit.slugify(text) to service_role, authenticated;
|
||||
create or replace function kit.set_slug_from_account_name()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
declare
|
||||
sql_string varchar;
|
||||
@@ -1616,7 +1616,7 @@ begin
|
||||
|
||||
end if;
|
||||
|
||||
sql_string = format('select count(1) cnt from accounts where slug = ''' || tmp_slug ||
|
||||
sql_string = format('select count(1) cnt from public.accounts where slug = ''' || tmp_slug ||
|
||||
'''; ');
|
||||
|
||||
for tmp_row in execute (sql_string)
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"supabase:web:start": "pnpm --filter web supabase:start",
|
||||
"supabase:web:stop": "pnpm --filter web supabase:stop",
|
||||
"supabase:web:typegen": "pnpm --filter web supabase:typegen",
|
||||
"supabase:web:reset": "pnpm --filter web supabase:reset",
|
||||
"stripe:listen": "pnpm --filter '@kit/stripe' start"
|
||||
},
|
||||
"prettier": "@kit/prettier-config",
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from '@kit/ui/command';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@kit/ui/popover';
|
||||
import { Separator } from '@kit/ui/separator';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
@@ -177,7 +178,14 @@ export function AccountSelector({
|
||||
|
||||
<If condition={features.enableTeamAccounts}>
|
||||
<If condition={accounts.length > 0}>
|
||||
<CommandGroup heading={<Trans i18nKey={'teams:yourTeams'} />}>
|
||||
<CommandGroup
|
||||
heading={
|
||||
<Trans
|
||||
i18nKey={'teams:yourTeams'}
|
||||
values={{ teamsCount: accounts.length }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{(accounts ?? []).map((account) => (
|
||||
<CommandItem
|
||||
data-test={'account-selector-team-' + account.value}
|
||||
@@ -218,13 +226,14 @@ export function AccountSelector({
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</If>
|
||||
</If>
|
||||
</CommandList>
|
||||
</Command>
|
||||
|
||||
<CommandSeparator />
|
||||
</If>
|
||||
</If>
|
||||
<Separator />
|
||||
|
||||
<If condition={features.enableTeamCreation}>
|
||||
<CommandGroup>
|
||||
<Button
|
||||
data-test={'create-team-account-trigger'}
|
||||
variant="ghost"
|
||||
@@ -240,10 +249,7 @@ export function AccountSelector({
|
||||
<Trans i18nKey={'teams:createTeam'} />
|
||||
</span>
|
||||
</Button>
|
||||
</CommandGroup>
|
||||
</If>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useFormStatus } from 'react-dom';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
import {
|
||||
@@ -23,6 +22,7 @@ import { Form, FormControl, FormItem, FormLabel } from '@kit/ui/form';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { DeletePersonalAccountSchema } from '../../schema/delete-personal-account.schema';
|
||||
import { deletePersonalAccountAction } from '../../server/personal-accounts-server-actions';
|
||||
|
||||
export function AccountDangerZone() {
|
||||
@@ -71,11 +71,7 @@ function DeleteAccountModal() {
|
||||
|
||||
function DeleteAccountForm() {
|
||||
const form = useForm({
|
||||
resolver: zodResolver(
|
||||
z.object({
|
||||
confirmation: z.string().refine((value) => value === 'DELETE'),
|
||||
}),
|
||||
),
|
||||
resolver: zodResolver(DeletePersonalAccountSchema),
|
||||
defaultValues: {
|
||||
confirmation: '',
|
||||
},
|
||||
@@ -140,11 +136,10 @@ function DeleteAccountSubmitButton() {
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-test={'confirm-delete-account-button'}
|
||||
type={'submit'}
|
||||
disabled={pending}
|
||||
data-test={'confirm-delete-account-button'}
|
||||
name={'action'}
|
||||
value={'delete'}
|
||||
variant={'destructive'}
|
||||
>
|
||||
<Trans i18nKey={'account:deleteAccount'} />
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const DeletePersonalAccountSchema = z.object({
|
||||
confirmation: z.string().refine((value) => value === 'DELETE'),
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
'use server';
|
||||
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { RedirectType, redirect } from 'next/navigation';
|
||||
|
||||
import { z } from 'zod';
|
||||
@@ -8,6 +9,7 @@ import { getLogger } from '@kit/shared/logger';
|
||||
import { requireUser } from '@kit/supabase/require-user';
|
||||
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
|
||||
|
||||
import { DeletePersonalAccountSchema } from '../schema/delete-personal-account.schema';
|
||||
import { DeletePersonalAccountService } from './services/delete-personal-account.service';
|
||||
|
||||
const emailSettings = getEmailSettingsFromEnvironment();
|
||||
@@ -21,10 +23,13 @@ export async function refreshAuthSession() {
|
||||
}
|
||||
|
||||
export async function deletePersonalAccountAction(formData: FormData) {
|
||||
const confirmation = formData.get('confirmation');
|
||||
// validate the form data
|
||||
const { success } = DeletePersonalAccountSchema.safeParse(
|
||||
Object.fromEntries(formData.entries()),
|
||||
);
|
||||
|
||||
if (confirmation !== 'DELETE') {
|
||||
throw new Error('Confirmation required to delete account');
|
||||
if (!success) {
|
||||
throw new Error('Invalid form data');
|
||||
}
|
||||
|
||||
const client = getSupabaseServerActionClient();
|
||||
@@ -32,7 +37,13 @@ export async function deletePersonalAccountAction(formData: FormData) {
|
||||
|
||||
if (auth.error) {
|
||||
const logger = await getLogger();
|
||||
logger.error(`User is not authenticated. Redirecting to login page`);
|
||||
|
||||
logger.error(
|
||||
{
|
||||
error: auth.error,
|
||||
},
|
||||
`User is not authenticated. Redirecting to login page.`,
|
||||
);
|
||||
|
||||
redirect(auth.redirectTo);
|
||||
}
|
||||
@@ -55,6 +66,8 @@ export async function deletePersonalAccountAction(formData: FormData) {
|
||||
// sign out the user after deleting their account
|
||||
await client.auth.signOut();
|
||||
|
||||
revalidatePath('/', 'layout');
|
||||
|
||||
// redirect to the home page
|
||||
redirect('/', RedirectType.replace);
|
||||
}
|
||||
|
||||
@@ -35,11 +35,15 @@ export class DeletePersonalAccountService {
|
||||
productName: string;
|
||||
};
|
||||
}) {
|
||||
const userId = params.userId;
|
||||
const logger = await getLogger();
|
||||
|
||||
const userId = params.userId;
|
||||
const ctx = { userId, name: this.namespace };
|
||||
|
||||
logger.info(ctx, 'User requested deletion. Processing...');
|
||||
logger.info(
|
||||
ctx,
|
||||
'User requested to delete their personal account. Processing...',
|
||||
);
|
||||
|
||||
// execute the deletion of the user
|
||||
try {
|
||||
@@ -50,12 +54,12 @@ export class DeletePersonalAccountService {
|
||||
...ctx,
|
||||
error,
|
||||
},
|
||||
'Error deleting user',
|
||||
'Encountered an error deleting user',
|
||||
);
|
||||
|
||||
throw new Error('Error deleting user');
|
||||
}
|
||||
|
||||
logger.info(ctx, 'User deleted successfully');
|
||||
logger.info(ctx, 'User successfully deleted!');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { useTransition } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
@@ -28,6 +30,7 @@ export const UpdateTeamAccountNameForm = (props: {
|
||||
path: string;
|
||||
}) => {
|
||||
const [pending, startTransition] = useTransition();
|
||||
const { t } = useTranslation('teams');
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(TeamNameFormSchema),
|
||||
@@ -43,12 +46,18 @@ export const UpdateTeamAccountNameForm = (props: {
|
||||
data-test={'update-team-account-name-form'}
|
||||
className={'flex flex-col space-y-4'}
|
||||
onSubmit={form.handleSubmit((data) => {
|
||||
startTransition(async () => {
|
||||
await updateTeamAccountName({
|
||||
startTransition(() => {
|
||||
const promise = updateTeamAccountName({
|
||||
slug: props.account.slug,
|
||||
name: data.name,
|
||||
path: props.path,
|
||||
});
|
||||
|
||||
toast.promise(promise, {
|
||||
loading: t('updateTeamLoadingMessage'),
|
||||
success: t('updateTeamSuccessMessage'),
|
||||
error: t('updateTeamErrorMessage'),
|
||||
});
|
||||
});
|
||||
})}
|
||||
>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { getLogger } from '@kit/shared/logger';
|
||||
import { Database } from '@kit/supabase/database';
|
||||
|
||||
export class DeleteTeamAccountService {
|
||||
private readonly namespace = 'accounts.delete';
|
||||
private readonly namespace = 'accounts.delete-team-account';
|
||||
|
||||
/**
|
||||
* Deletes a team account. Permissions are not checked here, as they are
|
||||
|
||||
Reference in New Issue
Block a user