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