Allow super admin to create users and reset password (#238)
1. Add user creation and password reset dialog functionalities; added Junie guidelines Introduced new `AdminCreateUserDialog` and `AdminResetPasswordDialog` components for managing user accounts in the admin panel. Updated the `AdminAccountsTable` page with a button for user creation and implemented backend logic for password resets with robust error handling. 2. Added Jetbrains AI guidelines
This commit is contained in:
committed by
GitHub
parent
e193c94f06
commit
4f41304be4
27
.aiignore
Normal file
27
.aiignore
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# An .aiignore file follows the same syntax as a .gitignore file.
|
||||||
|
# .gitignore documentation: https://git-scm.com/docs/gitignore
|
||||||
|
|
||||||
|
# you can ignore files
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
|
||||||
|
# or folders
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
out/
|
||||||
|
|
||||||
|
.cursor
|
||||||
|
.cursorignore
|
||||||
|
database.types.ts
|
||||||
|
playwright-report
|
||||||
|
test-results
|
||||||
|
web/supabase/migrations
|
||||||
|
pnpm-lock.yaml
|
||||||
|
.env.local
|
||||||
|
.env.production.local
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
.zed
|
||||||
|
tsconfig.tsbuildinfo
|
||||||
|
.windsurfrules
|
||||||
1715
.junie/guidelines.md
Normal file
1715
.junie/guidelines.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,11 @@
|
|||||||
import { ServerDataLoader } from '@makerkit/data-loader-supabase-nextjs';
|
import { ServerDataLoader } from '@makerkit/data-loader-supabase-nextjs';
|
||||||
|
|
||||||
import { AdminAccountsTable } from '@kit/admin/components/admin-accounts-table';
|
import { AdminAccountsTable } from '@kit/admin/components/admin-accounts-table';
|
||||||
|
import { AdminCreateUserDialog } from '@kit/admin/components/admin-create-user-dialog';
|
||||||
import { AdminGuard } from '@kit/admin/components/admin-guard';
|
import { AdminGuard } from '@kit/admin/components/admin-guard';
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
|
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
|
||||||
|
import { Button } from '@kit/ui/button';
|
||||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||||
|
|
||||||
interface SearchParams {
|
interface SearchParams {
|
||||||
@@ -27,7 +29,13 @@ async function AccountsPage(props: AdminAccountsPageProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader description={<AppBreadcrumbs />} />
|
<PageHeader description={<AppBreadcrumbs />}>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<AdminCreateUserDialog>
|
||||||
|
<Button data-test="admin-create-user-button">Create User</Button>
|
||||||
|
</AdminCreateUserDialog>
|
||||||
|
</div>
|
||||||
|
</PageHeader>
|
||||||
|
|
||||||
<PageBody>
|
<PageBody>
|
||||||
<ServerDataLoader
|
<ServerDataLoader
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import {
|
|||||||
import { AdminDeleteAccountDialog } from './admin-delete-account-dialog';
|
import { AdminDeleteAccountDialog } from './admin-delete-account-dialog';
|
||||||
import { AdminDeleteUserDialog } from './admin-delete-user-dialog';
|
import { AdminDeleteUserDialog } from './admin-delete-user-dialog';
|
||||||
import { AdminImpersonateUserDialog } from './admin-impersonate-user-dialog';
|
import { AdminImpersonateUserDialog } from './admin-impersonate-user-dialog';
|
||||||
|
import { AdminResetPasswordDialog } from './admin-reset-password-dialog';
|
||||||
|
|
||||||
type Account = Database['public']['Tables']['accounts']['Row'];
|
type Account = Database['public']['Tables']['accounts']['Row'];
|
||||||
|
|
||||||
@@ -203,9 +204,10 @@ function getColumns(): ColumnDef<Account>[] {
|
|||||||
const userId = row.original.id;
|
const userId = row.original.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className={'flex justify-end'}>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant={'ghost'}>
|
<Button variant={'outline'} size={'icon'}>
|
||||||
<EllipsisVertical className={'h-4'} />
|
<EllipsisVertical className={'h-4'} />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@@ -224,6 +226,12 @@ function getColumns(): ColumnDef<Account>[] {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
<If condition={isPersonalAccount}>
|
<If condition={isPersonalAccount}>
|
||||||
|
<AdminResetPasswordDialog userId={userId}>
|
||||||
|
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||||
|
Send Reset Password link
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</AdminResetPasswordDialog>
|
||||||
|
|
||||||
<AdminImpersonateUserDialog userId={userId}>
|
<AdminImpersonateUserDialog userId={userId}>
|
||||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||||
Impersonate User
|
Impersonate User
|
||||||
@@ -247,6 +255,7 @@ function getColumns(): ColumnDef<Account>[] {
|
|||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useTransition } from 'react';
|
||||||
|
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from '@kit/ui/alert-dialog';
|
||||||
|
import { Button } from '@kit/ui/button';
|
||||||
|
import { Checkbox } from '@kit/ui/checkbox';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@kit/ui/form';
|
||||||
|
import { If } from '@kit/ui/if';
|
||||||
|
import { Input } from '@kit/ui/input';
|
||||||
|
import { toast } from '@kit/ui/sonner';
|
||||||
|
|
||||||
|
import { createUserAction } from '../lib/server/admin-server-actions';
|
||||||
|
import {
|
||||||
|
CreateUserSchema,
|
||||||
|
CreateUserSchemaType,
|
||||||
|
} 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 form = useForm<CreateUserSchemaType>({
|
||||||
|
resolver: zodResolver(CreateUserSchema),
|
||||||
|
defaultValues: {
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
emailConfirm: false,
|
||||||
|
},
|
||||||
|
mode: 'onChange',
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = (data: CreateUserSchemaType) => {
|
||||||
|
startTransition(async () => {
|
||||||
|
try {
|
||||||
|
const result = await createUserAction(data);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
toast.success('User creates successfully');
|
||||||
|
form.reset();
|
||||||
|
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(null);
|
||||||
|
} catch (e) {
|
||||||
|
setError(e instanceof Error ? e.message : 'Error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertDialog open={open} onOpenChange={setOpen}>
|
||||||
|
<AlertDialogTrigger asChild>{props.children}</AlertDialogTrigger>
|
||||||
|
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Create New User</AlertDialogTitle>
|
||||||
|
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Complete the form below to create a new user.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
data-test={'admin-create-user-form'}
|
||||||
|
className={'flex flex-col space-y-4'}
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
<If condition={!!error}>
|
||||||
|
<Alert variant={'destructive'}>
|
||||||
|
<AlertTitle>Error</AlertTitle>
|
||||||
|
|
||||||
|
<AlertDescription>{error}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</If>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
name={'email'}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Email</FormLabel>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
required
|
||||||
|
type="email"
|
||||||
|
placeholder={'user@example.com'}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
name={'password'}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Password</FormLabel>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
required
|
||||||
|
type="password"
|
||||||
|
placeholder={'Password'}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormDescription>
|
||||||
|
Password must be at least 8 characters long.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
name={'emailConfirm'}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<div className="flex flex-col space-y-1">
|
||||||
|
<FormLabel>Auto-confirm email</FormLabel>
|
||||||
|
|
||||||
|
<FormDescription>
|
||||||
|
If checked, the user's email will be automatically
|
||||||
|
confirmed.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
|
||||||
|
<Button disabled={pending} type={'submit'}>
|
||||||
|
{pending ? 'Creating...' : 'Create User'}
|
||||||
|
</Button>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -74,9 +74,16 @@ export function AdminImpersonateUserDialog(
|
|||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Impersonate User</AlertDialogTitle>
|
<AlertDialogTitle>Impersonate User</AlertDialogTitle>
|
||||||
|
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription className={'flex flex-col space-y-1'}>
|
||||||
|
<span>
|
||||||
Are you sure you want to impersonate this user? You will be logged
|
Are you sure you want to impersonate this user? You will be logged
|
||||||
in as this user. To stop impersonating, log out.
|
in as this user. To stop impersonating, log out.
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
<b>NB:</b> If the user has 2FA enabled, you will not be able to
|
||||||
|
impersonate them.
|
||||||
|
</span>
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useTransition } from 'react';
|
||||||
|
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from '@kit/ui/alert-dialog';
|
||||||
|
import { Button } from '@kit/ui/button';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@kit/ui/form';
|
||||||
|
import { If } from '@kit/ui/if';
|
||||||
|
import { Input } from '@kit/ui/input';
|
||||||
|
import { toast } from '@kit/ui/sonner';
|
||||||
|
|
||||||
|
import { resetPasswordAction } from '../lib/server/admin-server-actions';
|
||||||
|
|
||||||
|
const FormSchema = z.object({
|
||||||
|
userId: z.string().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.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>{props.children}</AlertDialogTrigger>
|
||||||
|
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Send a Reset Password Email</AlertDialogTitle>
|
||||||
|
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Do you want to send a reset password email to this user?
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -15,6 +15,8 @@ import {
|
|||||||
ImpersonateUserSchema,
|
ImpersonateUserSchema,
|
||||||
ReactivateUserSchema,
|
ReactivateUserSchema,
|
||||||
} from './schema/admin-actions.schema';
|
} from './schema/admin-actions.schema';
|
||||||
|
import { CreateUserSchema } from './schema/create-user.schema';
|
||||||
|
import { ResetPasswordSchema } from './schema/reset-password.schema';
|
||||||
import { createAdminAccountsService } from './services/admin-accounts.service';
|
import { createAdminAccountsService } from './services/admin-accounts.service';
|
||||||
import { createAdminAuthUserService } from './services/admin-auth-user.service';
|
import { createAdminAuthUserService } from './services/admin-auth-user.service';
|
||||||
import { adminAction } from './utils/admin-action';
|
import { adminAction } from './utils/admin-action';
|
||||||
@@ -150,6 +152,80 @@ export const deleteAccountAction = adminAction(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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();
|
||||||
|
|
||||||
|
logger.info({ email }, `Super Admin is creating a new user...`);
|
||||||
|
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
{ userId: data.user.id },
|
||||||
|
`Super Admin has successfully created a new user`,
|
||||||
|
);
|
||||||
|
|
||||||
|
revalidateAdmin();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
user: data.user,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
schema: CreateUserSchema,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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();
|
||||||
|
|
||||||
|
logger.info({ userId }, `Super Admin is resetting user password...`);
|
||||||
|
|
||||||
|
const result = await service.resetPassword(userId);
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
{ userId },
|
||||||
|
`Super Admin has successfully sent password reset email`,
|
||||||
|
);
|
||||||
|
|
||||||
|
revalidateAdmin();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
schema: ResetPasswordSchema,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
function revalidateAdmin() {
|
||||||
|
revalidatePath('/admin', 'layout');
|
||||||
|
}
|
||||||
|
|
||||||
function getAdminAuthService() {
|
function getAdminAuthService() {
|
||||||
const client = getSupabaseServerClient();
|
const client = getSupabaseServerClient();
|
||||||
const adminClient = getSupabaseServerAdminClient();
|
const adminClient = getSupabaseServerAdminClient();
|
||||||
@@ -162,7 +238,3 @@ function getAdminAccountsService() {
|
|||||||
|
|
||||||
return createAdminAccountsService(adminClient);
|
return createAdminAccountsService(adminClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
function revalidateAdmin() {
|
|
||||||
revalidatePath('/admin', 'layout');
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const CreateUserSchema = z.object({
|
||||||
|
email: z.string().email({ message: 'Please enter a valid email address' }),
|
||||||
|
password: z
|
||||||
|
.string()
|
||||||
|
.min(8, { message: 'Password must be at least 8 characters' }),
|
||||||
|
emailConfirm: z.boolean().default(false).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CreateUserSchemaType = z.infer<typeof CreateUserSchema>;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schema for resetting a user's password
|
||||||
|
*/
|
||||||
|
export const ResetPasswordSchema = z.object({
|
||||||
|
userId: z.string().uuid(),
|
||||||
|
confirmation: z.custom<string>((value) => value === 'CONFIRM'),
|
||||||
|
});
|
||||||
@@ -2,6 +2,8 @@ import 'server-only';
|
|||||||
|
|
||||||
import { SupabaseClient } from '@supabase/supabase-js';
|
import { SupabaseClient } from '@supabase/supabase-js';
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { Database } from '@kit/supabase/database';
|
import { Database } from '@kit/supabase/database';
|
||||||
|
|
||||||
export function createAdminAuthUserService(
|
export function createAdminAuthUserService(
|
||||||
@@ -155,4 +157,47 @@ class AdminAuthUserService {
|
|||||||
ban_duration: banDuration,
|
ban_duration: banDuration,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset a user's password by sending a password reset email.
|
||||||
|
* @param userId
|
||||||
|
*/
|
||||||
|
async resetPassword(userId: string) {
|
||||||
|
await this.assertUserIsNotCurrentSuperAdmin(userId);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: { user },
|
||||||
|
error,
|
||||||
|
} = await this.adminClient.auth.admin.getUserById(userId);
|
||||||
|
|
||||||
|
if (error ?? !user) {
|
||||||
|
throw new Error(`Error fetching user`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const email = user.email;
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
throw new Error(`User has no email. Cannot reset password`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the site URL from environment variable
|
||||||
|
const siteUrl = z.string().url().parse(process.env.NEXT_PUBLIC_SITE_URL);
|
||||||
|
|
||||||
|
const redirectTo = `${siteUrl}/update-password`;
|
||||||
|
|
||||||
|
const { error: resetError } =
|
||||||
|
await this.adminClient.auth.resetPasswordForEmail(email, {
|
||||||
|
redirectTo,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resetError) {
|
||||||
|
throw new Error(
|
||||||
|
`Error sending password reset email: ${resetError.message}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user