index.tsx90 lines · main
1import dayjs from 'dayjs'
2import { AlertCircle } from 'lucide-react'
3import { useState } from 'react'
4import { Alert, AlertDescription, AlertTitle, Button } from 'ui'
5import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
6
7import { AddNewFactorModal } from './AddNewFactorModal'
8import DeleteFactorModal from './DeleteFactorModal'
9import AlertError from '@/components/ui/AlertError'
10import { useMfaListFactorsQuery } from '@/data/profile/mfa-list-factors-query'
11import { DATETIME_FORMAT } from '@/lib/constants'
12
13export const TOTPFactors = () => {
14 const [isAddNewFactorOpen, setIsAddNewFactorOpen] = useState(false)
15 const [factorToBeDeleted, setFactorToBeDeleted] = useState<string | null>(null)
16 const { data, isPending: isLoading, isError, isSuccess, error } = useMfaListFactorsQuery()
17
18 return (
19 <>
20 <section className="space-y-3">
21 <p className="text-sm text-foreground-light">
22 Generate one-time passwords via authenticator apps like 1Password, Authy, etc. as a second
23 factor to verify your identity during sign-in.
24 </p>
25 <div>
26 {isLoading && <GenericSkeletonLoader />}
27 {isError && (
28 <AlertError error={error} subject="Failed to retrieve account security information" />
29 )}
30 {isSuccess && (
31 <>
32 {data.totp.length === 1 && (
33 <Alert variant="default" className="mb-2">
34 <AlertCircle className="h-4 w-4" />
35 <AlertTitle>
36 We recommend configuring two authenticator apps across different devices
37 </AlertTitle>
38 <AlertDescription className="flex flex-col gap-3">
39 The two authenticator apps will serve as a backup for each other.
40 </AlertDescription>
41 </Alert>
42 )}
43 <div>
44 {data.totp.map((factor) => {
45 return (
46 <div key={factor.id} className="flex flex-row justify-between py-2">
47 <p className="text-sm text-foreground flex items-center space-x-2">
48 <span className="text-foreground-light">Name:</span>{' '}
49 <span>{factor.friendly_name ?? 'No name provided'}</span>
50 </p>
51 <div className="flex items-center gap-4">
52 <p className="text-sm text-foreground-light">
53 Added on {dayjs(factor.updated_at).format(DATETIME_FORMAT)}
54 </p>
55 <Button
56 size="tiny"
57 type="default"
58 onClick={() => setFactorToBeDeleted(factor.id)}
59 >
60 Remove
61 </Button>
62 </div>
63 </div>
64 )
65 })}
66 </div>
67 {data.totp.length < 2 ? (
68 <>
69 <div className="pt-2">
70 <Button onClick={() => setIsAddNewFactorOpen(true)}>Add new app</Button>
71 </div>
72 </>
73 ) : null}
74 </>
75 )}
76 </div>
77 </section>
78 <AddNewFactorModal
79 visible={isAddNewFactorOpen}
80 onClose={() => setIsAddNewFactorOpen(false)}
81 />
82 <DeleteFactorModal
83 visible={factorToBeDeleted !== null}
84 factorId={factorToBeDeleted}
85 lastFactorToBeDeleted={data?.totp.length === 1}
86 onClose={() => setFactorToBeDeleted(null)}
87 />
88 </>
89 )
90}