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
@@ -36,6 +36,7 @@ import {
|
||||
import { AdminDeleteAccountDialog } from './admin-delete-account-dialog';
|
||||
import { AdminDeleteUserDialog } from './admin-delete-user-dialog';
|
||||
import { AdminImpersonateUserDialog } from './admin-impersonate-user-dialog';
|
||||
import { AdminResetPasswordDialog } from './admin-reset-password-dialog';
|
||||
|
||||
type Account = Database['public']['Tables']['accounts']['Row'];
|
||||
|
||||
@@ -203,50 +204,58 @@ function getColumns(): ColumnDef<Account>[] {
|
||||
const userId = row.original.id;
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant={'ghost'}>
|
||||
<EllipsisVertical className={'h-4'} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<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>
|
||||
<DropdownMenuContent align={'end'}>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
|
||||
<DropdownMenuItem>
|
||||
<Link
|
||||
className={'h-full w-full'}
|
||||
href={`/admin/accounts/${userId}`}
|
||||
>
|
||||
View
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Link
|
||||
className={'h-full w-full'}
|
||||
href={`/admin/accounts/${userId}`}
|
||||
>
|
||||
View
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<If condition={isPersonalAccount}>
|
||||
<AdminImpersonateUserDialog userId={userId}>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
Impersonate User
|
||||
</DropdownMenuItem>
|
||||
</AdminImpersonateUserDialog>
|
||||
<If condition={isPersonalAccount}>
|
||||
<AdminResetPasswordDialog userId={userId}>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
Send Reset Password link
|
||||
</DropdownMenuItem>
|
||||
</AdminResetPasswordDialog>
|
||||
|
||||
<AdminDeleteUserDialog userId={userId}>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
Delete Personal Account
|
||||
</DropdownMenuItem>
|
||||
</AdminDeleteUserDialog>
|
||||
</If>
|
||||
<AdminImpersonateUserDialog userId={userId}>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
Impersonate User
|
||||
</DropdownMenuItem>
|
||||
</AdminImpersonateUserDialog>
|
||||
|
||||
<If condition={!isPersonalAccount}>
|
||||
<AdminDeleteAccountDialog accountId={row.original.id}>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
Delete Team Account
|
||||
</DropdownMenuItem>
|
||||
</AdminDeleteAccountDialog>
|
||||
</If>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<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>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
<AlertDialogTitle>Impersonate User</AlertDialogTitle>
|
||||
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to impersonate this user? You will be logged
|
||||
in as this user. To stop impersonating, log out.
|
||||
<AlertDialogDescription className={'flex flex-col space-y-1'}>
|
||||
<span>
|
||||
Are you sure you want to impersonate this user? You will be logged
|
||||
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>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user