Next.js Supabase V3 (#463)
Version 3 of the kit: - Radix UI replaced with Base UI (using the Shadcn UI patterns) - next-intl replaces react-i18next - enhanceAction deprecated; usage moved to next-safe-action - main layout now wrapped with [locale] path segment - Teams only mode - Layout updates - Zod v4 - Next.js 16.2 - Typescript 6 - All other dependencies updated - Removed deprecated Edge CSRF - Dynamic Github Action runner
This commit is contained in:
committed by
GitHub
parent
4912e402a3
commit
7ebff31475
@@ -60,9 +60,8 @@ async function PersonalAccountPage(props: { account: Account }) {
|
||||
userResult.data.user.banned_until !== 'none';
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageBody className="gap-y-4">
|
||||
<PageHeader
|
||||
className="border-b"
|
||||
description={
|
||||
<AppBreadcrumbs
|
||||
values={{
|
||||
@@ -123,41 +122,39 @@ async function PersonalAccountPage(props: { account: Account }) {
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<PageBody className={'space-y-6 py-4'}>
|
||||
<div className={'flex items-center justify-between'}>
|
||||
<div className={'flex items-center gap-x-4'}>
|
||||
<div className={'flex items-center gap-x-2.5'}>
|
||||
<ProfileAvatar
|
||||
pictureUrl={props.account.picture_url}
|
||||
displayName={props.account.name}
|
||||
/>
|
||||
<div className={'flex items-center justify-between'}>
|
||||
<div className={'flex items-center gap-x-4'}>
|
||||
<div className={'flex items-center gap-x-2.5'}>
|
||||
<ProfileAvatar
|
||||
pictureUrl={props.account.picture_url}
|
||||
displayName={props.account.name}
|
||||
/>
|
||||
|
||||
<span className={'text-sm font-semibold capitalize'}>
|
||||
{props.account.name}
|
||||
</span>
|
||||
</div>
|
||||
<span className={'text-sm font-semibold capitalize'}>
|
||||
{props.account.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Badge variant={'outline'}>Personal Account</Badge>
|
||||
<Badge variant={'outline'}>Personal Account</Badge>
|
||||
|
||||
<If condition={isBanned}>
|
||||
<Badge variant={'destructive'}>Banned</Badge>
|
||||
</If>
|
||||
<If condition={isBanned}>
|
||||
<Badge variant={'destructive'}>Banned</Badge>
|
||||
</If>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={'flex flex-col gap-y-8'}>
|
||||
<SubscriptionsTable accountId={props.account.id} />
|
||||
|
||||
<div className={'divider-divider-x flex flex-col gap-y-2.5'}>
|
||||
<Heading level={6}>Teams</Heading>
|
||||
|
||||
<div className={'rounded-lg border p-2'}>
|
||||
<AdminMembershipsTable memberships={memberships} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={'flex flex-col gap-y-8'}>
|
||||
<SubscriptionsTable accountId={props.account.id} />
|
||||
|
||||
<div className={'divider-divider-x flex flex-col gap-y-2.5'}>
|
||||
<Heading level={6}>Teams</Heading>
|
||||
|
||||
<div className={'rounded-lg border p-2'}>
|
||||
<AdminMembershipsTable memberships={memberships} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageBody>
|
||||
</>
|
||||
</div>
|
||||
</PageBody>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -167,9 +164,8 @@ async function TeamAccountPage(props: {
|
||||
const members = await getMembers(props.account.slug ?? '');
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageBody className={'gap-y-6'}>
|
||||
<PageHeader
|
||||
className="border-b"
|
||||
description={
|
||||
<AppBreadcrumbs
|
||||
values={{
|
||||
@@ -191,39 +187,37 @@ async function TeamAccountPage(props: {
|
||||
</AdminDeleteAccountDialog>
|
||||
</PageHeader>
|
||||
|
||||
<PageBody className={'space-y-6 py-4'}>
|
||||
<div className={'flex justify-between'}>
|
||||
<div className={'flex items-center gap-x-4'}>
|
||||
<div className={'flex items-center gap-x-2.5'}>
|
||||
<ProfileAvatar
|
||||
pictureUrl={props.account.picture_url}
|
||||
displayName={props.account.name}
|
||||
/>
|
||||
<div className={'flex justify-between'}>
|
||||
<div className={'flex items-center gap-x-4'}>
|
||||
<div className={'flex items-center gap-x-2.5'}>
|
||||
<ProfileAvatar
|
||||
pictureUrl={props.account.picture_url}
|
||||
displayName={props.account.name}
|
||||
/>
|
||||
|
||||
<span className={'text-sm font-semibold capitalize'}>
|
||||
{props.account.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Badge variant={'outline'}>Team Account</Badge>
|
||||
<span className={'text-sm font-semibold capitalize'}>
|
||||
{props.account.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Badge variant={'outline'}>Team Account</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className={'flex flex-col gap-y-8'}>
|
||||
<SubscriptionsTable accountId={props.account.id} />
|
||||
<div>
|
||||
<div className={'flex flex-col gap-y-8'}>
|
||||
<SubscriptionsTable accountId={props.account.id} />
|
||||
|
||||
<div className={'flex flex-col gap-y-2.5'}>
|
||||
<Heading level={6}>Team Members</Heading>
|
||||
<div className={'flex flex-col gap-y-2.5'}>
|
||||
<Heading level={6}>Team Members</Heading>
|
||||
|
||||
<div className={'rounded-lg border p-2'}>
|
||||
<AdminMembersTable members={members} />
|
||||
</div>
|
||||
<div className={'rounded-lg border p-2'}>
|
||||
<AdminMembersTable members={members} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageBody>
|
||||
</>
|
||||
</div>
|
||||
</PageBody>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
|
||||
@@ -7,7 +9,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { EllipsisVertical } from 'lucide-react';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
import { Tables } from '@kit/supabase/database';
|
||||
import { Button } from '@kit/ui/button';
|
||||
@@ -21,7 +23,6 @@ import {
|
||||
} from '@kit/ui/dropdown-menu';
|
||||
import { DataTable } from '@kit/ui/enhanced-data-table';
|
||||
import { Form, FormControl, FormField, FormItem } from '@kit/ui/form';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import {
|
||||
Select,
|
||||
@@ -77,7 +78,7 @@ export function AdminAccountsTable(
|
||||
}
|
||||
|
||||
function AccountsTableFilters(props: {
|
||||
filters: z.infer<typeof FiltersSchema>;
|
||||
filters: z.output<typeof FiltersSchema>;
|
||||
}) {
|
||||
const form = useForm({
|
||||
resolver: zodResolver(FiltersSchema),
|
||||
@@ -92,7 +93,7 @@ function AccountsTableFilters(props: {
|
||||
const router = useRouter();
|
||||
const pathName = usePathname();
|
||||
|
||||
const onSubmit = ({ type, query }: z.infer<typeof FiltersSchema>) => {
|
||||
const onSubmit = ({ type, query }: z.output<typeof FiltersSchema>) => {
|
||||
const params = new URLSearchParams({
|
||||
account_type: type,
|
||||
query: query ?? '',
|
||||
@@ -105,6 +106,12 @@ function AccountsTableFilters(props: {
|
||||
|
||||
const type = useWatch({ control: form.control, name: 'type' });
|
||||
|
||||
const options = {
|
||||
all: 'All Accounts',
|
||||
team: 'Team',
|
||||
personal: 'Personal',
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
@@ -116,7 +123,7 @@ function AccountsTableFilters(props: {
|
||||
onValueChange={(value) => {
|
||||
form.setValue(
|
||||
'type',
|
||||
value as z.infer<typeof FiltersSchema>['type'],
|
||||
value as z.output<typeof FiltersSchema>['type'],
|
||||
{
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
@@ -128,16 +135,20 @@ function AccountsTableFilters(props: {
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={'Account Type'} />
|
||||
<SelectValue placeholder={'Account Type'}>
|
||||
{(value: keyof typeof options) => options[value]}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Account Type</SelectLabel>
|
||||
|
||||
<SelectItem value={'all'}>All accounts</SelectItem>
|
||||
<SelectItem value={'team'}>Team</SelectItem>
|
||||
<SelectItem value={'personal'}>Personal</SelectItem>
|
||||
{Object.entries(options).map(([key, value]) => (
|
||||
<SelectItem key={key} value={key}>
|
||||
{value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -157,6 +168,8 @@ function AccountsTableFilters(props: {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<button type="submit" hidden />
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
@@ -194,75 +207,143 @@ function getColumns(): ColumnDef<Account>[] {
|
||||
{
|
||||
id: 'created_at',
|
||||
header: 'Created At',
|
||||
accessorKey: 'created_at',
|
||||
cell: ({ row }) => {
|
||||
return new Date(row.original.created_at!).toLocaleDateString(
|
||||
undefined,
|
||||
{
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'updated_at',
|
||||
header: 'Updated At',
|
||||
accessorKey: 'updated_at',
|
||||
cell: ({ row }) => {
|
||||
return row.original.updated_at
|
||||
? new Date(row.original.updated_at).toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
: '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: '',
|
||||
cell: ({ row }) => {
|
||||
const isPersonalAccount = row.original.is_personal_account;
|
||||
const userId = row.original.id;
|
||||
|
||||
return (
|
||||
<div className={'flex justify-end'}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant={'outline'} size={'icon'}>
|
||||
<EllipsisVertical className={'h-4'} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align={'end'}>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
|
||||
<DropdownMenuItem>
|
||||
<Link
|
||||
className={'h-full w-full'}
|
||||
href={`/admin/accounts/${userId}`}
|
||||
>
|
||||
View
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<If condition={isPersonalAccount}>
|
||||
<AdminResetPasswordDialog userId={userId}>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
Send Reset Password link
|
||||
</DropdownMenuItem>
|
||||
</AdminResetPasswordDialog>
|
||||
|
||||
<AdminImpersonateUserDialog userId={userId}>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
Impersonate User
|
||||
</DropdownMenuItem>
|
||||
</AdminImpersonateUserDialog>
|
||||
|
||||
<AdminDeleteUserDialog userId={userId}>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
Delete Personal Account
|
||||
</DropdownMenuItem>
|
||||
</AdminDeleteUserDialog>
|
||||
</If>
|
||||
|
||||
<If condition={!isPersonalAccount}>
|
||||
<AdminDeleteAccountDialog accountId={row.original.id}>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
Delete Team Account
|
||||
</DropdownMenuItem>
|
||||
</AdminDeleteAccountDialog>
|
||||
</If>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => <ActionsCell account={row.original} />,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
type ActiveDialog =
|
||||
| 'reset-password'
|
||||
| 'impersonate'
|
||||
| 'delete-user'
|
||||
| 'delete-account'
|
||||
| null;
|
||||
|
||||
function ActionsCell({ account }: { account: Account }) {
|
||||
const [activeDialog, setActiveDialog] = useState<ActiveDialog>(null);
|
||||
const isPersonalAccount = account.is_personal_account;
|
||||
|
||||
return (
|
||||
<div className={'flex justify-end'}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button variant={'outline'} size={'icon'}>
|
||||
<EllipsisVertical className={'h-4'} />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<DropdownMenuContent className="min-w-52">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
|
||||
<DropdownMenuItem
|
||||
render={
|
||||
<Link
|
||||
className={'h-full w-full'}
|
||||
href={`/admin/accounts/${account.id}`}
|
||||
>
|
||||
View
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
|
||||
{isPersonalAccount && (
|
||||
<>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setActiveDialog('reset-password')}
|
||||
>
|
||||
Send Reset Password link
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={() => setActiveDialog('impersonate')}
|
||||
>
|
||||
Impersonate User
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
variant="destructive"
|
||||
onClick={() => setActiveDialog('delete-user')}
|
||||
>
|
||||
Delete Personal Account
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isPersonalAccount && (
|
||||
<DropdownMenuItem
|
||||
variant="destructive"
|
||||
onClick={() => setActiveDialog('delete-account')}
|
||||
>
|
||||
Delete Team Account
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{isPersonalAccount && (
|
||||
<>
|
||||
<AdminResetPasswordDialog
|
||||
userId={account.id}
|
||||
open={activeDialog === 'reset-password'}
|
||||
onOpenChange={(open) => !open && setActiveDialog(null)}
|
||||
/>
|
||||
|
||||
<AdminImpersonateUserDialog
|
||||
userId={account.id}
|
||||
open={activeDialog === 'impersonate'}
|
||||
onOpenChange={(open) => !open && setActiveDialog(null)}
|
||||
/>
|
||||
|
||||
<AdminDeleteUserDialog
|
||||
userId={account.id}
|
||||
open={activeDialog === 'delete-user'}
|
||||
onOpenChange={(open) => !open && setActiveDialog(null)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isPersonalAccount && (
|
||||
<AdminDeleteAccountDialog
|
||||
accountId={account.id}
|
||||
open={activeDialog === 'delete-account'}
|
||||
onOpenChange={(open) => !open && setActiveDialog(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useTransition } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
@@ -26,6 +25,7 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@kit/ui/form';
|
||||
import { useAsyncDialog } from '@kit/ui/hooks/use-async-dialog';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Input } from '@kit/ui/input';
|
||||
|
||||
@@ -37,11 +37,14 @@ export function AdminBanUserDialog(
|
||||
userId: string;
|
||||
}>,
|
||||
) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { dialogProps, isPending, setIsPending, setOpen } = useAsyncDialog();
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={setOpen}>
|
||||
<AlertDialogTrigger asChild>{props.children}</AlertDialogTrigger>
|
||||
<AlertDialog
|
||||
open={dialogProps.open}
|
||||
onOpenChange={dialogProps.onOpenChange}
|
||||
>
|
||||
<AlertDialogTrigger render={props.children as React.ReactElement} />
|
||||
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
@@ -53,15 +56,31 @@ export function AdminBanUserDialog(
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
<BanUserForm userId={props.userId} onSuccess={() => setOpen(false)} />
|
||||
<BanUserForm
|
||||
userId={props.userId}
|
||||
isPending={isPending}
|
||||
setIsPending={setIsPending}
|
||||
onSuccess={() => {
|
||||
setIsPending(false);
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
|
||||
function BanUserForm(props: { userId: string; onSuccess: () => void }) {
|
||||
const [pending, startTransition] = useTransition();
|
||||
const [error, setError] = useState<boolean>(false);
|
||||
function BanUserForm(props: {
|
||||
userId: string;
|
||||
isPending: boolean;
|
||||
setIsPending: (pending: boolean) => void;
|
||||
onSuccess: () => void;
|
||||
}) {
|
||||
const { execute, hasErrored } = useAction(banUserAction, {
|
||||
onExecute: () => props.setIsPending(true),
|
||||
onSuccess: () => props.onSuccess(),
|
||||
onSettled: () => props.setIsPending(false),
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(BanUserSchema),
|
||||
@@ -76,18 +95,9 @@ function BanUserForm(props: { userId: string; onSuccess: () => void }) {
|
||||
<form
|
||||
data-test={'admin-ban-user-form'}
|
||||
className={'flex flex-col space-y-8'}
|
||||
onSubmit={form.handleSubmit((data) => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await banUserAction(data);
|
||||
props.onSuccess();
|
||||
} catch {
|
||||
setError(true);
|
||||
}
|
||||
});
|
||||
})}
|
||||
onSubmit={form.handleSubmit((data) => execute(data))}
|
||||
>
|
||||
<If condition={error}>
|
||||
<If condition={hasErrored}>
|
||||
<Alert variant={'destructive'}>
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
|
||||
@@ -125,10 +135,16 @@ function BanUserForm(props: { userId: string; onSuccess: () => void }) {
|
||||
/>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={pending}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogCancel disabled={props.isPending}>
|
||||
Cancel
|
||||
</AlertDialogCancel>
|
||||
|
||||
<Button disabled={pending} type={'submit'} variant={'destructive'}>
|
||||
{pending ? 'Banning...' : 'Ban User'}
|
||||
<Button
|
||||
disabled={props.isPending}
|
||||
type={'submit'}
|
||||
variant={'destructive'}
|
||||
>
|
||||
{props.isPending ? 'Banning...' : 'Ban User'}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</form>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useTransition } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
@@ -27,6 +26,7 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@kit/ui/form';
|
||||
import { useAsyncDialog } from '@kit/ui/hooks/use-async-dialog';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
@@ -38,9 +38,7 @@ import {
|
||||
} from '../lib/server/schema/create-user.schema';
|
||||
|
||||
export function AdminCreateUserDialog(props: React.PropsWithChildren) {
|
||||
const [pending, startTransition] = useTransition();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const { dialogProps, isPending, setIsPending, setOpen } = useAsyncDialog();
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(CreateUserSchema),
|
||||
@@ -52,28 +50,25 @@ export function AdminCreateUserDialog(props: React.PropsWithChildren) {
|
||||
mode: 'onChange',
|
||||
});
|
||||
|
||||
const onSubmit = (data: CreateUserSchemaType) => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
const result = await createUserAction(data);
|
||||
const { execute, result } = useAction(createUserAction, {
|
||||
onExecute: () => setIsPending(true),
|
||||
onSuccess: () => {
|
||||
toast.success('User created successfully');
|
||||
form.reset();
|
||||
setIsPending(false);
|
||||
setOpen(false);
|
||||
},
|
||||
onSettled: () => setIsPending(false),
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
toast.success('User creates successfully');
|
||||
form.reset();
|
||||
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
setError(null);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Error');
|
||||
}
|
||||
});
|
||||
};
|
||||
const error = result.serverError;
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={setOpen}>
|
||||
<AlertDialogTrigger asChild>{props.children}</AlertDialogTrigger>
|
||||
<AlertDialog
|
||||
open={dialogProps.open}
|
||||
onOpenChange={dialogProps.onOpenChange}
|
||||
>
|
||||
<AlertDialogTrigger render={props.children as React.ReactElement} />
|
||||
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
@@ -88,7 +83,9 @@ export function AdminCreateUserDialog(props: React.PropsWithChildren) {
|
||||
<form
|
||||
data-test={'admin-create-user-form'}
|
||||
className={'flex flex-col space-y-4'}
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
onSubmit={form.handleSubmit((data: CreateUserSchemaType) =>
|
||||
execute(data),
|
||||
)}
|
||||
>
|
||||
<If condition={!!error}>
|
||||
<Alert variant={'destructive'}>
|
||||
@@ -164,10 +161,10 @@ export function AdminCreateUserDialog(props: React.PropsWithChildren) {
|
||||
/>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogCancel disabled={isPending}>Cancel</AlertDialogCancel>
|
||||
|
||||
<Button disabled={pending} type={'submit'}>
|
||||
{pending ? 'Creating...' : 'Create User'}
|
||||
<Button disabled={isPending} type={'submit'}>
|
||||
{isPending ? 'Creating...' : 'Create User'}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</form>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useTransition } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
@@ -35,22 +34,15 @@ import { DeleteAccountSchema } from '../lib/server/schema/admin-actions.schema';
|
||||
export function AdminDeleteAccountDialog(
|
||||
props: React.PropsWithChildren<{
|
||||
accountId: string;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}>,
|
||||
) {
|
||||
const [pending, startTransition] = useTransition();
|
||||
const [error, setError] = useState<boolean>(false);
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(DeleteAccountSchema),
|
||||
defaultValues: {
|
||||
accountId: props.accountId,
|
||||
confirmation: '',
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>{props.children}</AlertDialogTrigger>
|
||||
<AlertDialog open={props.open} onOpenChange={props.onOpenChange}>
|
||||
<If condition={props.children}>
|
||||
<AlertDialogTrigger render={props.children as React.ReactElement} />
|
||||
</If>
|
||||
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
@@ -63,73 +55,75 @@ export function AdminDeleteAccountDialog(
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
data-form={'admin-delete-account-form'}
|
||||
className={'flex flex-col space-y-8'}
|
||||
onSubmit={form.handleSubmit((data) => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await deleteAccountAction(data);
|
||||
setError(false);
|
||||
} catch {
|
||||
setError(true);
|
||||
}
|
||||
});
|
||||
})}
|
||||
>
|
||||
<If condition={error}>
|
||||
<Alert variant={'destructive'}>
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
There was an error deleting the account. Please check the
|
||||
server logs to see what went wrong.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</If>
|
||||
|
||||
<FormField
|
||||
name={'confirmation'}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Type <b>CONFIRM</b> to confirm
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input
|
||||
pattern={'CONFIRM'}
|
||||
required
|
||||
placeholder={'Type CONFIRM to confirm'}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
Are you sure you want to do this? This action cannot be
|
||||
undone.
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
|
||||
<Button
|
||||
disabled={pending}
|
||||
type={'submit'}
|
||||
variant={'destructive'}
|
||||
>
|
||||
{pending ? 'Deleting...' : 'Delete'}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
<DeleteAccountForm accountId={props.accountId} />
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
|
||||
function DeleteAccountForm(props: { accountId: string }) {
|
||||
const { execute, isPending, hasErrored } = useAction(deleteAccountAction);
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(DeleteAccountSchema),
|
||||
defaultValues: {
|
||||
accountId: props.accountId,
|
||||
confirmation: '',
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
data-test={'admin-delete-account-form'}
|
||||
className={'flex flex-col space-y-8'}
|
||||
onSubmit={form.handleSubmit((data) => execute(data))}
|
||||
>
|
||||
<If condition={hasErrored}>
|
||||
<Alert variant={'destructive'}>
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
There was an error deleting the account. Please check the server
|
||||
logs to see what went wrong.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</If>
|
||||
|
||||
<FormField
|
||||
name={'confirmation'}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Type <b>CONFIRM</b> to confirm
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input
|
||||
pattern={'CONFIRM'}
|
||||
required
|
||||
placeholder={'Type CONFIRM to confirm'}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
Are you sure you want to do this? This action cannot be undone.
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
|
||||
<Button disabled={isPending} type={'submit'} variant={'destructive'}>
|
||||
{isPending ? 'Deleting...' : 'Delete'}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useTransition } from 'react';
|
||||
|
||||
import { isRedirectError } from 'next/dist/client/components/redirect-error';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
@@ -37,22 +34,15 @@ import { DeleteUserSchema } from '../lib/server/schema/admin-actions.schema';
|
||||
export function AdminDeleteUserDialog(
|
||||
props: React.PropsWithChildren<{
|
||||
userId: string;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}>,
|
||||
) {
|
||||
const [pending, startTransition] = useTransition();
|
||||
const [error, setError] = useState<boolean>(false);
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(DeleteUserSchema),
|
||||
defaultValues: {
|
||||
userId: props.userId,
|
||||
confirmation: '',
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>{props.children}</AlertDialogTrigger>
|
||||
<AlertDialog open={props.open} onOpenChange={props.onOpenChange}>
|
||||
<If condition={props.children}>
|
||||
<AlertDialogTrigger render={props.children as React.ReactElement} />
|
||||
</If>
|
||||
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
@@ -65,78 +55,75 @@ export function AdminDeleteUserDialog(
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
data-test={'admin-delete-user-form'}
|
||||
className={'flex flex-col space-y-8'}
|
||||
onSubmit={form.handleSubmit((data) => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await deleteUserAction(data);
|
||||
|
||||
setError(false);
|
||||
} catch {
|
||||
if (isRedirectError(error)) {
|
||||
// Do nothing
|
||||
} else {
|
||||
setError(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
})}
|
||||
>
|
||||
<If condition={error}>
|
||||
<Alert variant={'destructive'}>
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
There was an error deleting the user. Please check the server
|
||||
logs to see what went wrong.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</If>
|
||||
|
||||
<FormField
|
||||
name={'confirmation'}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Type <b>CONFIRM</b> to confirm
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input
|
||||
required
|
||||
pattern={'CONFIRM'}
|
||||
placeholder={'Type CONFIRM to confirm'}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
Are you sure you want to do this? This action cannot be
|
||||
undone.
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
|
||||
<Button
|
||||
disabled={pending}
|
||||
type={'submit'}
|
||||
variant={'destructive'}
|
||||
>
|
||||
{pending ? 'Deleting...' : 'Delete'}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
<DeleteUserForm userId={props.userId} />
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
|
||||
function DeleteUserForm(props: { userId: string }) {
|
||||
const { execute, isPending, hasErrored } = useAction(deleteUserAction);
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(DeleteUserSchema),
|
||||
defaultValues: {
|
||||
userId: props.userId,
|
||||
confirmation: '',
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
data-test={'admin-delete-user-form'}
|
||||
className={'flex flex-col space-y-8'}
|
||||
onSubmit={form.handleSubmit((data) => execute(data))}
|
||||
>
|
||||
<If condition={hasErrored}>
|
||||
<Alert variant={'destructive'}>
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
There was an error deleting the user. Please check the server logs
|
||||
to see what went wrong.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</If>
|
||||
|
||||
<FormField
|
||||
name={'confirmation'}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Type <b>CONFIRM</b> to confirm
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input
|
||||
required
|
||||
pattern={'CONFIRM'}
|
||||
placeholder={'Type CONFIRM to confirm'}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
Are you sure you want to do this? This action cannot be undone.
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
|
||||
<Button disabled={isPending} type={'submit'} variant={'destructive'}>
|
||||
{isPending ? 'Deleting...' : 'Delete'}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useTransition } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
||||
@@ -35,40 +36,34 @@ import { LoadingOverlay } from '@kit/ui/loading-overlay';
|
||||
import { impersonateUserAction } from '../lib/server/admin-server-actions';
|
||||
import { ImpersonateUserSchema } from '../lib/server/schema/admin-actions.schema';
|
||||
|
||||
type Tokens = {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
};
|
||||
|
||||
export function AdminImpersonateUserDialog(
|
||||
props: React.PropsWithChildren<{
|
||||
userId: string;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}>,
|
||||
) {
|
||||
const form = useForm({
|
||||
resolver: zodResolver(ImpersonateUserSchema),
|
||||
defaultValues: {
|
||||
userId: props.userId,
|
||||
confirmation: '',
|
||||
},
|
||||
});
|
||||
|
||||
const [tokens, setTokens] = useState<{
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
}>();
|
||||
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [error, setError] = useState<boolean | null>(null);
|
||||
const [tokens, setTokens] = useState<Tokens>();
|
||||
|
||||
if (tokens) {
|
||||
return (
|
||||
<>
|
||||
<ImpersonateUserAuthSetter tokens={tokens} />
|
||||
|
||||
<LoadingOverlay>Setting up your session...</LoadingOverlay>
|
||||
</>
|
||||
);
|
||||
return <ImpersonateUserAuthSetter tokens={tokens} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>{props.children}</AlertDialogTrigger>
|
||||
<AlertDialog
|
||||
open={props.open}
|
||||
onOpenChange={(open) => {
|
||||
props.onOpenChange?.(open);
|
||||
}}
|
||||
>
|
||||
<If condition={props.children}>
|
||||
<AlertDialogTrigger render={props.children as React.ReactElement} />
|
||||
</If>
|
||||
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
@@ -87,73 +82,88 @@ export function AdminImpersonateUserDialog(
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
data-test={'admin-impersonate-user-form'}
|
||||
className={'flex flex-col space-y-8'}
|
||||
onSubmit={form.handleSubmit((data) => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
const result = await impersonateUserAction(data);
|
||||
|
||||
setTokens(result);
|
||||
} catch {
|
||||
setError(true);
|
||||
}
|
||||
});
|
||||
})}
|
||||
>
|
||||
<If condition={error}>
|
||||
<Alert variant={'destructive'}>
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
Failed to impersonate user. Please check the logs to
|
||||
understand what went wrong.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</If>
|
||||
|
||||
<FormField
|
||||
name={'confirmation'}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Type <b>CONFIRM</b> to confirm
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input
|
||||
required
|
||||
pattern={'CONFIRM'}
|
||||
placeholder={'Type CONFIRM to confirm'}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
Are you sure you want to impersonate this user?
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
|
||||
<Button disabled={isPending} type={'submit'}>
|
||||
{isPending ? 'Impersonating...' : 'Impersonate User'}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
<AdminImpersonateUserForm userId={props.userId} onSuccess={setTokens} />
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
|
||||
function AdminImpersonateUserForm(props: {
|
||||
userId: string;
|
||||
onSuccess: (data: Tokens) => void;
|
||||
}) {
|
||||
const form = useForm({
|
||||
resolver: zodResolver(ImpersonateUserSchema),
|
||||
defaultValues: {
|
||||
userId: props.userId,
|
||||
confirmation: '',
|
||||
},
|
||||
});
|
||||
|
||||
const { execute, isPending, hasErrored } = useAction(impersonateUserAction, {
|
||||
onSuccess: ({ data }) => {
|
||||
if (data) {
|
||||
props.onSuccess(data);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
data-test={'admin-impersonate-user-form'}
|
||||
className={'flex flex-col space-y-8'}
|
||||
onSubmit={form.handleSubmit((data) => execute(data))}
|
||||
>
|
||||
<If condition={hasErrored}>
|
||||
<Alert variant={'destructive'}>
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
Failed to impersonate user. Please check the logs to understand
|
||||
what went wrong.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</If>
|
||||
|
||||
<FormField
|
||||
name={'confirmation'}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Type <b>CONFIRM</b> to confirm
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input
|
||||
required
|
||||
pattern={'CONFIRM'}
|
||||
placeholder={'Type CONFIRM to confirm'}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
Are you sure you want to impersonate this user?
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
|
||||
<Button disabled={isPending} type={'submit'}>
|
||||
{isPending ? 'Impersonating...' : 'Impersonate User'}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
function ImpersonateUserAuthSetter({
|
||||
tokens,
|
||||
}: React.PropsWithChildren<{
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useTransition } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
@@ -26,6 +25,7 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@kit/ui/form';
|
||||
import { useAsyncDialog } from '@kit/ui/hooks/use-async-dialog';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Input } from '@kit/ui/input';
|
||||
|
||||
@@ -37,11 +37,14 @@ export function AdminReactivateUserDialog(
|
||||
userId: string;
|
||||
}>,
|
||||
) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { dialogProps, isPending, setIsPending, setOpen } = useAsyncDialog();
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={setOpen}>
|
||||
<AlertDialogTrigger asChild>{props.children}</AlertDialogTrigger>
|
||||
<AlertDialog
|
||||
open={dialogProps.open}
|
||||
onOpenChange={dialogProps.onOpenChange}
|
||||
>
|
||||
<AlertDialogTrigger render={props.children as React.ReactElement} />
|
||||
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
@@ -54,16 +57,29 @@ export function AdminReactivateUserDialog(
|
||||
|
||||
<ReactivateUserForm
|
||||
userId={props.userId}
|
||||
onSuccess={() => setOpen(false)}
|
||||
isPending={isPending}
|
||||
setIsPending={setIsPending}
|
||||
onSuccess={() => {
|
||||
setIsPending(false);
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
|
||||
function ReactivateUserForm(props: { userId: string; onSuccess: () => void }) {
|
||||
const [pending, startTransition] = useTransition();
|
||||
const [error, setError] = useState<boolean>(false);
|
||||
function ReactivateUserForm(props: {
|
||||
userId: string;
|
||||
isPending: boolean;
|
||||
setIsPending: (pending: boolean) => void;
|
||||
onSuccess: () => void;
|
||||
}) {
|
||||
const { execute, hasErrored } = useAction(reactivateUserAction, {
|
||||
onExecute: () => props.setIsPending(true),
|
||||
onSuccess: () => props.onSuccess(),
|
||||
onSettled: () => props.setIsPending(false),
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(ReactivateUserSchema),
|
||||
@@ -78,18 +94,9 @@ function ReactivateUserForm(props: { userId: string; onSuccess: () => void }) {
|
||||
<form
|
||||
data-test={'admin-reactivate-user-form'}
|
||||
className={'flex flex-col space-y-8'}
|
||||
onSubmit={form.handleSubmit((data) => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await reactivateUserAction(data);
|
||||
props.onSuccess();
|
||||
} catch {
|
||||
setError(true);
|
||||
}
|
||||
});
|
||||
})}
|
||||
onSubmit={form.handleSubmit((data) => execute(data))}
|
||||
>
|
||||
<If condition={error}>
|
||||
<If condition={hasErrored}>
|
||||
<Alert variant={'destructive'}>
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
|
||||
@@ -127,10 +134,12 @@ function ReactivateUserForm(props: { userId: string; onSuccess: () => void }) {
|
||||
/>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={pending}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogCancel disabled={props.isPending}>
|
||||
Cancel
|
||||
</AlertDialogCancel>
|
||||
|
||||
<Button disabled={pending} type={'submit'}>
|
||||
{pending ? 'Reactivating...' : 'Reactivate User'}
|
||||
<Button disabled={props.isPending} type={'submit'}>
|
||||
{props.isPending ? 'Reactivating...' : 'Reactivate User'}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</form>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useTransition } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
import {
|
||||
@@ -34,50 +33,21 @@ import { toast } from '@kit/ui/sonner';
|
||||
import { resetPasswordAction } from '../lib/server/admin-server-actions';
|
||||
|
||||
const FormSchema = z.object({
|
||||
userId: z.string().uuid(),
|
||||
userId: z.uuid(),
|
||||
confirmation: z.custom<string>((value) => value === 'CONFIRM'),
|
||||
});
|
||||
|
||||
export function AdminResetPasswordDialog(
|
||||
props: React.PropsWithChildren<{
|
||||
userId: string;
|
||||
}>,
|
||||
) {
|
||||
const form = useForm({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
userId: props.userId,
|
||||
confirmation: '',
|
||||
},
|
||||
});
|
||||
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => {
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await resetPasswordAction(data);
|
||||
|
||||
setSuccess(true);
|
||||
form.reset({ userId: props.userId, confirmation: '' });
|
||||
|
||||
toast.success('Password reset email successfully sent');
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e));
|
||||
|
||||
toast.error('We hit an error. Please read the logs.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export function AdminResetPasswordDialog(props: {
|
||||
userId: string;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>{props.children}</AlertDialogTrigger>
|
||||
<AlertDialog open={props.open} onOpenChange={props.onOpenChange}>
|
||||
{props.children && (
|
||||
<AlertDialogTrigger render={props.children as React.ReactElement} />
|
||||
)}
|
||||
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
@@ -89,75 +59,102 @@ export function AdminResetPasswordDialog(
|
||||
</AlertDialogHeader>
|
||||
|
||||
<div className="relative">
|
||||
<Form {...form}>
|
||||
<form onSubmit={onSubmit} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="confirmation"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Confirmation</FormLabel>
|
||||
|
||||
<FormDescription>
|
||||
Type CONFIRM to execute this request.
|
||||
</FormDescription>
|
||||
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="CONFIRM"
|
||||
{...field}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<If condition={!!error}>
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle>
|
||||
We encountered an error while sending the email
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
Please check the server logs for more details.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</If>
|
||||
|
||||
<If condition={success}>
|
||||
<Alert>
|
||||
<AlertTitle>
|
||||
Password reset email sent successfully
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
The password reset email has been sent to the user.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</If>
|
||||
|
||||
<input type="hidden" name="userId" value={props.userId} />
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isPending}>
|
||||
Cancel
|
||||
</AlertDialogCancel>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
variant="destructive"
|
||||
>
|
||||
{isPending ? 'Sending...' : 'Send Reset Email'}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
<AdminResetPasswordForm
|
||||
userId={props.userId}
|
||||
onSuccess={() => props.onOpenChange(false)}
|
||||
/>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
|
||||
function AdminResetPasswordForm({
|
||||
userId,
|
||||
onSuccess,
|
||||
}: {
|
||||
userId: string;
|
||||
onSuccess: () => void;
|
||||
}) {
|
||||
const form = useForm({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
userId,
|
||||
confirmation: '',
|
||||
},
|
||||
});
|
||||
|
||||
const { execute, isPending, hasErrored, hasSucceeded } = useAction(
|
||||
resetPasswordAction,
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success('Password reset email successfully sent');
|
||||
onSuccess();
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('We hit an error. Please read the logs.');
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit((data) => execute(data))}
|
||||
className="space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="confirmation"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Confirmation</FormLabel>
|
||||
|
||||
<FormDescription>
|
||||
Type CONFIRM to execute this request.
|
||||
</FormDescription>
|
||||
|
||||
<FormControl>
|
||||
<Input placeholder="CONFIRM" {...field} autoComplete="off" />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<If condition={hasErrored}>
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle>
|
||||
We encountered an error while sending the email
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
Please check the server logs for more details.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</If>
|
||||
|
||||
<If condition={hasSucceeded}>
|
||||
<Alert>
|
||||
<AlertTitle>Password reset email sent successfully</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
The password reset email has been sent to the user.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</If>
|
||||
|
||||
<input type="hidden" name="userId" value={userId} />
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isPending}>Cancel</AlertDialogCancel>
|
||||
|
||||
<Button type="submit" disabled={isPending} variant="destructive">
|
||||
{isPending ? 'Sending...' : 'Send Reset Email'}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
@@ -19,212 +18,168 @@ import { CreateUserSchema } from './schema/create-user.schema';
|
||||
import { ResetPasswordSchema } from './schema/reset-password.schema';
|
||||
import { createAdminAccountsService } from './services/admin-accounts.service';
|
||||
import { createAdminAuthUserService } from './services/admin-auth-user.service';
|
||||
import { adminAction } from './utils/admin-action';
|
||||
import { adminActionClient } from './utils/admin-action-client';
|
||||
|
||||
/**
|
||||
* @name banUserAction
|
||||
* @description Ban a user from the system.
|
||||
*/
|
||||
export const banUserAction = adminAction(
|
||||
enhanceAction(
|
||||
async ({ userId }) => {
|
||||
const service = getAdminAuthService();
|
||||
const logger = await getLogger();
|
||||
export const banUserAction = adminActionClient
|
||||
.inputSchema(BanUserSchema)
|
||||
.action(async ({ parsedInput: { userId } }) => {
|
||||
const service = getAdminAuthService();
|
||||
const logger = await getLogger();
|
||||
|
||||
logger.info({ userId }, `Super Admin is banning user...`);
|
||||
logger.info({ userId }, `Super Admin is banning user...`);
|
||||
|
||||
const { error } = await service.banUser(userId);
|
||||
const { error } = await service.banUser(userId);
|
||||
|
||||
if (error) {
|
||||
logger.error({ error }, `Error banning user`);
|
||||
if (error) {
|
||||
logger.error({ error }, `Error banning user`);
|
||||
throw new Error('Error banning user');
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
revalidateAdmin();
|
||||
|
||||
revalidateAdmin();
|
||||
|
||||
logger.info({ userId }, `Super Admin has successfully banned user`);
|
||||
},
|
||||
{
|
||||
schema: BanUserSchema,
|
||||
},
|
||||
),
|
||||
);
|
||||
logger.info({ userId }, `Super Admin has successfully banned user`);
|
||||
});
|
||||
|
||||
/**
|
||||
* @name reactivateUserAction
|
||||
* @description Reactivate a user in the system.
|
||||
*/
|
||||
export const reactivateUserAction = adminAction(
|
||||
enhanceAction(
|
||||
async ({ userId }) => {
|
||||
const service = getAdminAuthService();
|
||||
const logger = await getLogger();
|
||||
export const reactivateUserAction = adminActionClient
|
||||
.inputSchema(ReactivateUserSchema)
|
||||
.action(async ({ parsedInput: { userId } }) => {
|
||||
const service = getAdminAuthService();
|
||||
const logger = await getLogger();
|
||||
|
||||
logger.info({ userId }, `Super Admin is reactivating user...`);
|
||||
logger.info({ userId }, `Super Admin is reactivating user...`);
|
||||
|
||||
const { error } = await service.reactivateUser(userId);
|
||||
const { error } = await service.reactivateUser(userId);
|
||||
|
||||
if (error) {
|
||||
logger.error({ error }, `Error reactivating user`);
|
||||
if (error) {
|
||||
logger.error({ error }, `Error reactivating user`);
|
||||
throw new Error('Error reactivating user');
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
revalidateAdmin();
|
||||
|
||||
revalidateAdmin();
|
||||
|
||||
logger.info({ userId }, `Super Admin has successfully reactivated user`);
|
||||
},
|
||||
{
|
||||
schema: ReactivateUserSchema,
|
||||
},
|
||||
),
|
||||
);
|
||||
logger.info({ userId }, `Super Admin has successfully reactivated user`);
|
||||
});
|
||||
|
||||
/**
|
||||
* @name impersonateUserAction
|
||||
* @description Impersonate a user in the system.
|
||||
*/
|
||||
export const impersonateUserAction = adminAction(
|
||||
enhanceAction(
|
||||
async ({ userId }) => {
|
||||
const service = getAdminAuthService();
|
||||
const logger = await getLogger();
|
||||
export const impersonateUserAction = adminActionClient
|
||||
.inputSchema(ImpersonateUserSchema)
|
||||
.action(async ({ parsedInput: { userId } }) => {
|
||||
const service = getAdminAuthService();
|
||||
const logger = await getLogger();
|
||||
|
||||
logger.info({ userId }, `Super Admin is impersonating user...`);
|
||||
logger.info({ userId }, `Super Admin is impersonating user...`);
|
||||
|
||||
return await service.impersonateUser(userId);
|
||||
},
|
||||
{
|
||||
schema: ImpersonateUserSchema,
|
||||
},
|
||||
),
|
||||
);
|
||||
return await service.impersonateUser(userId);
|
||||
});
|
||||
|
||||
/**
|
||||
* @name deleteUserAction
|
||||
* @description Delete a user from the system.
|
||||
*/
|
||||
export const deleteUserAction = adminAction(
|
||||
enhanceAction(
|
||||
async ({ userId }) => {
|
||||
const service = getAdminAuthService();
|
||||
const logger = await getLogger();
|
||||
export const deleteUserAction = adminActionClient
|
||||
.inputSchema(DeleteUserSchema)
|
||||
.action(async ({ parsedInput: { userId } }) => {
|
||||
const service = getAdminAuthService();
|
||||
const logger = await getLogger();
|
||||
|
||||
logger.info({ userId }, `Super Admin is deleting user...`);
|
||||
logger.info({ userId }, `Super Admin is deleting user...`);
|
||||
|
||||
await service.deleteUser(userId);
|
||||
await service.deleteUser(userId);
|
||||
|
||||
logger.info({ userId }, `Super Admin has successfully deleted user`);
|
||||
logger.info({ userId }, `Super Admin has successfully deleted user`);
|
||||
|
||||
return redirect('/admin/accounts');
|
||||
},
|
||||
{
|
||||
schema: DeleteUserSchema,
|
||||
},
|
||||
),
|
||||
);
|
||||
redirect('/admin/accounts');
|
||||
});
|
||||
|
||||
/**
|
||||
* @name deleteAccountAction
|
||||
* @description Delete an account from the system.
|
||||
*/
|
||||
export const deleteAccountAction = adminAction(
|
||||
enhanceAction(
|
||||
async ({ accountId }) => {
|
||||
const service = getAdminAccountsService();
|
||||
const logger = await getLogger();
|
||||
export const deleteAccountAction = adminActionClient
|
||||
.inputSchema(DeleteAccountSchema)
|
||||
.action(async ({ parsedInput: { accountId } }) => {
|
||||
const service = getAdminAccountsService();
|
||||
const logger = await getLogger();
|
||||
|
||||
logger.info({ accountId }, `Super Admin is deleting account...`);
|
||||
logger.info({ accountId }, `Super Admin is deleting account...`);
|
||||
|
||||
await service.deleteAccount(accountId);
|
||||
await service.deleteAccount(accountId);
|
||||
|
||||
revalidateAdmin();
|
||||
revalidateAdmin();
|
||||
|
||||
logger.info(
|
||||
{ accountId },
|
||||
`Super Admin has successfully deleted account`,
|
||||
);
|
||||
logger.info({ accountId }, `Super Admin has successfully deleted account`);
|
||||
|
||||
return redirect('/admin/accounts');
|
||||
},
|
||||
{
|
||||
schema: DeleteAccountSchema,
|
||||
},
|
||||
),
|
||||
);
|
||||
redirect('/admin/accounts');
|
||||
});
|
||||
|
||||
/**
|
||||
* @name createUserAction
|
||||
* @description Create a new user in the system.
|
||||
*/
|
||||
export const createUserAction = adminAction(
|
||||
enhanceAction(
|
||||
async ({ email, password, emailConfirm }) => {
|
||||
const adminClient = getSupabaseServerAdminClient();
|
||||
const logger = await getLogger();
|
||||
export const createUserAction = adminActionClient
|
||||
.inputSchema(CreateUserSchema)
|
||||
.action(async ({ parsedInput: { email, password, emailConfirm } }) => {
|
||||
const adminClient = getSupabaseServerAdminClient();
|
||||
const logger = await getLogger();
|
||||
|
||||
logger.info({ email }, `Super Admin is creating a new user...`);
|
||||
logger.info({ email }, `Super Admin is creating a new user...`);
|
||||
|
||||
const { data, error } = await adminClient.auth.admin.createUser({
|
||||
email,
|
||||
password,
|
||||
email_confirm: emailConfirm,
|
||||
});
|
||||
const { data, error } = await adminClient.auth.admin.createUser({
|
||||
email,
|
||||
password,
|
||||
email_confirm: emailConfirm,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
logger.error({ error }, `Error creating user`);
|
||||
throw new Error(`Error creating user: ${error.message}`);
|
||||
}
|
||||
if (error) {
|
||||
logger.error({ error }, `Error creating user`);
|
||||
throw new Error(`Error creating user: ${error.message}`);
|
||||
}
|
||||
|
||||
logger.info(
|
||||
{ userId: data.user.id },
|
||||
`Super Admin has successfully created a new user`,
|
||||
);
|
||||
logger.info(
|
||||
{ userId: data.user.id },
|
||||
`Super Admin has successfully created a new user`,
|
||||
);
|
||||
|
||||
revalidatePath(`/admin/accounts`);
|
||||
revalidatePath(`/admin/accounts`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
user: data.user,
|
||||
};
|
||||
},
|
||||
{
|
||||
schema: CreateUserSchema,
|
||||
},
|
||||
),
|
||||
);
|
||||
return {
|
||||
success: true,
|
||||
user: data.user,
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* @name resetPasswordAction
|
||||
* @description Reset a user's password by sending a password reset email.
|
||||
*/
|
||||
export const resetPasswordAction = adminAction(
|
||||
enhanceAction(
|
||||
async ({ userId }) => {
|
||||
const service = getAdminAuthService();
|
||||
const logger = await getLogger();
|
||||
export const resetPasswordAction = adminActionClient
|
||||
.inputSchema(ResetPasswordSchema)
|
||||
.action(async ({ parsedInput: { userId } }) => {
|
||||
const service = getAdminAuthService();
|
||||
const logger = await getLogger();
|
||||
|
||||
logger.info({ userId }, `Super Admin is resetting user password...`);
|
||||
logger.info({ userId }, `Super Admin is resetting user password...`);
|
||||
|
||||
const result = await service.resetPassword(userId);
|
||||
const result = await service.resetPassword(userId);
|
||||
|
||||
logger.info(
|
||||
{ userId },
|
||||
`Super Admin has successfully sent password reset email`,
|
||||
);
|
||||
logger.info(
|
||||
{ userId },
|
||||
`Super Admin has successfully sent password reset email`,
|
||||
);
|
||||
|
||||
return result;
|
||||
},
|
||||
{
|
||||
schema: ResetPasswordSchema,
|
||||
},
|
||||
),
|
||||
);
|
||||
return result;
|
||||
});
|
||||
|
||||
function revalidateAdmin() {
|
||||
revalidatePath(`/admin/accounts/[id]`, 'page');
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'server-only';
|
||||
|
||||
import { cache } from 'react';
|
||||
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
const ConfirmationSchema = z.object({
|
||||
confirmation: z.custom<string>((value) => value === 'CONFIRM'),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
export const CreateUserSchema = z.object({
|
||||
email: z.string().email({ message: 'Please enter a valid email address' }),
|
||||
@@ -8,4 +8,4 @@ export const CreateUserSchema = z.object({
|
||||
emailConfirm: z.boolean().default(false).optional(),
|
||||
});
|
||||
|
||||
export type CreateUserSchemaType = z.infer<typeof CreateUserSchema>;
|
||||
export type CreateUserSchemaType = z.output<typeof CreateUserSchema>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
/**
|
||||
* Schema for resetting a user's password
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'server-only';
|
||||
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import { Database } from '@kit/supabase/database';
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import 'server-only';
|
||||
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
import { Database } from '@kit/supabase/database';
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import 'server-only';
|
||||
import { authActionClient } from '@kit/next/safe-action';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
import { isSuperAdmin } from './is-super-admin';
|
||||
|
||||
/**
|
||||
* @name adminActionClient
|
||||
* @description Safe action client for admin-only actions.
|
||||
* Extends authActionClient with super admin verification.
|
||||
*/
|
||||
export const adminActionClient = authActionClient.use(async ({ next, ctx }) => {
|
||||
const isAdmin = await isSuperAdmin(getSupabaseServerClient());
|
||||
|
||||
if (!isAdmin) {
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
|
||||
return next({ ctx });
|
||||
});
|
||||
Reference in New Issue
Block a user