BanUserModal.tsx154 lines · main
1import { zodResolver } from '@hookform/resolvers/zod'
2import { useParams } from 'common'
3import dayjs from 'dayjs'
4import { useEffect } from 'react'
5import { useForm } from 'react-hook-form'
6import { toast } from 'sonner'
7import {
8 Button,
9 cn,
10 Form,
11 FormControl,
12 FormField,
13 Input,
14 Modal,
15 Select,
16 SelectContent,
17 SelectItem,
18 SelectTrigger,
19 Separator,
20} from 'ui'
21import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
22import * as z from 'zod'
23
24import { useUserUpdateMutation } from '@/data/auth/user-update-mutation'
25import { User } from '@/data/auth/users-infinite-query'
26
27interface BanUserModalProps {
28 visible: boolean
29 user: User
30 onClose: () => void
31}
32
33export const BanUserModal = ({ visible, user, onClose }: BanUserModalProps) => {
34 const { ref: projectRef } = useParams()
35
36 const { mutate: updateUser, isPending: isBanningUser } = useUserUpdateMutation({
37 onSuccess: (_, vars) => {
38 const bannedUntil = dayjs()
39 .add(Number(vars.banDuration), 'hours')
40 .format('DD MMM YYYY HH:mm (ZZ)')
41 toast.success(`User banned successfully until ${bannedUntil}`)
42 onClose()
43 },
44 })
45
46 const FormSchema = z.object({
47 value: z.string().min(1, { message: 'Please provide a duration' }),
48 unit: z.enum(['hours', 'days']),
49 })
50 type FormType = z.infer<typeof FormSchema>
51 const defaultValues: FormType = { value: '24', unit: 'hours' }
52 const form = useForm<FormType>({
53 mode: 'onBlur',
54 reValidateMode: 'onChange',
55 resolver: zodResolver(FormSchema as any),
56 defaultValues,
57 })
58
59 const { value, unit } = form.watch()
60 const bannedUntil = dayjs().add(Number(value), unit).format('DD MMM YYYY HH:mm (ZZ)')
61
62 const onSubmit = (data: FormType) => {
63 if (projectRef === undefined) return console.error('Project ref is required')
64 if (user.id === undefined) {
65 return toast.error(`Failed to ban user: User ID not found`)
66 }
67
68 const durationHours = data.unit === 'hours' ? Number(data.value) : Number(data.value) * 24
69
70 updateUser({
71 projectRef,
72 userId: user.id,
73 banDuration: durationHours,
74 })
75 }
76
77 useEffect(() => {
78 if (visible) form.reset(defaultValues)
79 // eslint-disable-next-line react-hooks/exhaustive-deps
80 }, [visible])
81
82 return (
83 <Modal
84 hideFooter
85 visible={visible}
86 size="small"
87 header="Confirm to ban user"
88 onCancel={() => onClose()}
89 >
90 <Form {...form}>
91 <form onSubmit={form.handleSubmit(onSubmit)}>
92 <Modal.Content className="flex flex-col gap-y-3">
93 <p className="text-sm">
94 This will revoke the user's access to your project and prevent them from logging in
95 for a specified duration.
96 </p>
97 <div className="flex items-start gap-x-2 [&>div:first-child]:grow">
98 <FormField
99 control={form.control}
100 name="value"
101 render={({ field }) => (
102 <FormItemLayout className="[&>div>div]:mt-0" label="Set a ban duration">
103 <FormControl>
104 <Input {...field} />
105 </FormControl>
106 </FormItemLayout>
107 )}
108 />
109 <FormField
110 control={form.control}
111 name="unit"
112 render={({ field }) => (
113 <FormItemLayout className="[&>div>div]:mt-0 mt-[33px]">
114 <FormControl>
115 <Select
116 {...field}
117 value={field.value}
118 onValueChange={(value) => form.setValue('unit', value as 'hours' | 'days')}
119 >
120 <SelectTrigger className="capitalize w-24">{field.value}</SelectTrigger>
121 <SelectContent>
122 <SelectItem value="hours">Hours</SelectItem>
123 <SelectItem value="days">Days</SelectItem>
124 </SelectContent>
125 </Select>
126 </FormControl>
127 </FormItemLayout>
128 )}
129 />
130 </div>
131
132 <div>
133 <p className="text-sm text-foreground-lighter">
134 This user will not be able to log in until:
135 </p>
136 <p className={cn('text-sm', !value && 'text-foreground-light')}>
137 {!!value ? bannedUntil : 'Invalid duration set'}
138 </p>
139 </div>
140 </Modal.Content>
141 <Separator />
142 <Modal.Content className="flex justify-end gap-2">
143 <Button type="default" disabled={isBanningUser} onClick={() => onClose()}>
144 Cancel
145 </Button>
146 <Button type="warning" htmlType="submit" loading={isBanningUser}>
147 Confirm ban
148 </Button>
149 </Modal.Content>
150 </form>
151 </Form>
152 </Modal>
153 )
154}