@hed-hog/core 0.0.185 → 0.0.190
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/hedhog/frontend/app/account/2fa/page.tsx.ejs +5 -0
- package/hedhog/frontend/app/account/accounts/page.tsx.ejs +5 -0
- package/hedhog/frontend/app/account/components/active-sessions.tsx.ejs +356 -0
- package/hedhog/frontend/app/account/components/change-email-form.tsx.ejs +379 -0
- package/hedhog/frontend/app/account/components/change-password-form.tsx.ejs +184 -0
- package/hedhog/frontend/app/account/components/connected-accounts.tsx.ejs +144 -0
- package/hedhog/frontend/app/account/components/email-request-dialog.tsx.ejs +96 -0
- package/hedhog/frontend/app/account/components/mfa-add-buttons.tsx.ejs +43 -0
- package/hedhog/frontend/app/account/components/mfa-method-card.tsx.ejs +115 -0
- package/hedhog/frontend/app/account/components/mfa-setup-dialog.tsx.ejs +236 -0
- package/hedhog/frontend/app/account/components/profile-form.tsx.ejs +209 -0
- package/hedhog/frontend/app/account/components/recovery-codes-dialog.tsx.ejs +192 -0
- package/hedhog/frontend/app/account/components/regenerate-codes-dialog.tsx.ejs +372 -0
- package/hedhog/frontend/app/account/components/remove-mfa-dialog.tsx.ejs +337 -0
- package/hedhog/frontend/app/account/components/two-factor-auth.tsx.ejs +393 -0
- package/hedhog/frontend/app/account/components/verify-before-add-dialog.tsx.ejs +332 -0
- package/hedhog/frontend/app/account/email/page.tsx.ejs +5 -0
- package/hedhog/frontend/app/account/hooks/use-mfa-methods.ts.ejs +27 -0
- package/hedhog/frontend/app/account/hooks/use-mfa-setup.ts.ejs +461 -0
- package/hedhog/frontend/app/account/layout.tsx.ejs +105 -0
- package/hedhog/frontend/app/account/lib/mfa-utils.tsx.ejs +37 -0
- package/hedhog/frontend/app/account/page.tsx.ejs +5 -0
- package/hedhog/frontend/app/account/password/page.tsx.ejs +5 -0
- package/hedhog/frontend/app/account/profile/page.tsx.ejs +5 -0
- package/hedhog/frontend/app/account/sessions/page.tsx.ejs +5 -0
- package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +490 -0
- package/hedhog/frontend/app/configurations/[slug]/page.tsx.ejs +62 -0
- package/hedhog/frontend/app/configurations/layout.tsx.ejs +316 -0
- package/hedhog/frontend/app/configurations/page.tsx.ejs +35 -0
- package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +351 -0
- package/hedhog/frontend/app/dashboard/[slug]/page.tsx.ejs +11 -0
- package/hedhog/frontend/app/dashboard/[slug]/types.ts.ejs +62 -0
- package/hedhog/frontend/app/dashboard/[slug]/widget-renderer.tsx.ejs +45 -0
- package/hedhog/frontend/app/dashboard/dashboard.css.ejs +196 -0
- package/hedhog/frontend/app/dashboard/management/page.tsx.ejs +63 -0
- package/hedhog/frontend/app/dashboard/management/tabs/component-roles-tab.tsx.ejs +516 -0
- package/hedhog/frontend/app/dashboard/management/tabs/components-tab.tsx.ejs +753 -0
- package/hedhog/frontend/app/dashboard/management/tabs/dashboard-roles-tab.tsx.ejs +516 -0
- package/hedhog/frontend/app/dashboard/management/tabs/dashboards-tab.tsx.ejs +489 -0
- package/hedhog/frontend/app/dashboard/management/tabs/items-tab.tsx.ejs +621 -0
- package/hedhog/frontend/app/dashboard/page.tsx.ejs +14 -0
- package/hedhog/frontend/app/mail/log/page.tsx.ejs +312 -0
- package/hedhog/frontend/app/mail/template/page.tsx.ejs +1177 -0
- package/hedhog/frontend/app/preferences/page.tsx.ejs +448 -0
- package/hedhog/frontend/app/roles/menus.tsx.ejs +504 -0
- package/hedhog/frontend/app/roles/page.tsx.ejs +814 -0
- package/hedhog/frontend/app/roles/routes.tsx.ejs +397 -0
- package/hedhog/frontend/app/roles/users.tsx.ejs +306 -0
- package/hedhog/frontend/app/users/active-session.tsx.ejs +159 -0
- package/hedhog/frontend/app/users/identifiers.tsx.ejs +279 -0
- package/hedhog/frontend/app/users/page.tsx.ejs +1257 -0
- package/hedhog/frontend/app/users/permissions.tsx.ejs +155 -0
- package/hedhog/frontend/messages/en.json +1080 -0
- package/hedhog/frontend/messages/pt.json +1135 -0
- package/package.json +4 -4
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { Button } from '@/components/ui/button';
|
|
2
|
+
import { formatDateTime } from '@/lib/format-date';
|
|
3
|
+
import { User, UserSession } from '@hed-hog/api-types';
|
|
4
|
+
import { useApp } from '@hed-hog/next-app-provider';
|
|
5
|
+
import { Monitor } from 'lucide-react';
|
|
6
|
+
import { useTranslations } from 'next-intl';
|
|
7
|
+
import { toast } from 'sonner';
|
|
8
|
+
|
|
9
|
+
function parseUserAgent(ua: string) {
|
|
10
|
+
if (!ua) return { os: 'Unknown OS', browser: 'Unknown Browser' };
|
|
11
|
+
|
|
12
|
+
let os = 'Unknown OS';
|
|
13
|
+
let browser = 'Unknown Browser';
|
|
14
|
+
|
|
15
|
+
if (ua.includes('Windows')) os = 'Windows';
|
|
16
|
+
else if (ua.includes('Mac')) os = 'macOS';
|
|
17
|
+
else if (ua.includes('Linux')) os = 'Linux';
|
|
18
|
+
|
|
19
|
+
if (ua.includes('Chrome')) browser = 'Chrome';
|
|
20
|
+
else if (ua.includes('Firefox')) browser = 'Firefox';
|
|
21
|
+
else if (ua.includes('Safari')) browser = 'Safari';
|
|
22
|
+
|
|
23
|
+
return { os, browser };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface IProps {
|
|
27
|
+
editingUser: User;
|
|
28
|
+
refetch: () => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function ActiveSessions({ editingUser, refetch }: IProps) {
|
|
32
|
+
const { request, currentLocaleCode, getSettingValue } = useApp();
|
|
33
|
+
const t = useTranslations('core.UserActiveSessions');
|
|
34
|
+
const sessions = editingUser?.user_session ?? [];
|
|
35
|
+
const currentSession = sessions[0];
|
|
36
|
+
const otherSessions = sessions.slice(1);
|
|
37
|
+
|
|
38
|
+
const handleRevokeSession = async (sessionId: number) => {
|
|
39
|
+
try {
|
|
40
|
+
await request({
|
|
41
|
+
url: `/sessions/${sessionId}/revoke`,
|
|
42
|
+
method: 'DELETE',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
refetch();
|
|
46
|
+
toast.success(t('revokeSuccess'));
|
|
47
|
+
} catch (error) {
|
|
48
|
+
toast.error(t('revokeFailure'));
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const handleRevokeAllSessions = async () => {
|
|
53
|
+
try {
|
|
54
|
+
await request({
|
|
55
|
+
url: '/sessions/revoke-all',
|
|
56
|
+
method: 'DELETE',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
refetch();
|
|
60
|
+
toast.success(t('revokeAllSuccess'));
|
|
61
|
+
} catch (error) {
|
|
62
|
+
toast.error(t('revokeAllFailure'));
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div className="space-y-3">
|
|
68
|
+
<div className="flex items-center justify-between">
|
|
69
|
+
<h4 className="text-sm font-medium">{t('title')}</h4>
|
|
70
|
+
<Button variant="outline" size="sm" onClick={handleRevokeAllSessions}>
|
|
71
|
+
{t('revokeAll')}
|
|
72
|
+
</Button>
|
|
73
|
+
</div>
|
|
74
|
+
<div className="space-y-2">
|
|
75
|
+
{currentSession ? (
|
|
76
|
+
<div className="rounded-lg border p-3">
|
|
77
|
+
<div className="flex items-center gap-3">
|
|
78
|
+
<div className="rounded-md bg-blue-50 p-2">
|
|
79
|
+
<Monitor className="h-5 w-5 text-blue-600" />
|
|
80
|
+
</div>
|
|
81
|
+
<div className="flex-1">
|
|
82
|
+
<p className="text-sm font-medium">{t('currentSession')}</p>
|
|
83
|
+
<p className="text-xs text-muted-foreground">
|
|
84
|
+
{parseUserAgent(currentSession.user_agent).os} ·{' '}
|
|
85
|
+
{parseUserAgent(currentSession.user_agent).browser} ·{' '}
|
|
86
|
+
{t('lastActive')}{' '}
|
|
87
|
+
{formatDateTime(
|
|
88
|
+
String(currentSession.updated_at),
|
|
89
|
+
getSettingValue,
|
|
90
|
+
currentLocaleCode
|
|
91
|
+
)}
|
|
92
|
+
</p>
|
|
93
|
+
<p className="text-[11px] text-muted-foreground">
|
|
94
|
+
{t('ip')} {currentSession.ip_address} · {t('expires')}{' '}
|
|
95
|
+
{formatDateTime(
|
|
96
|
+
currentSession.expires_at,
|
|
97
|
+
getSettingValue,
|
|
98
|
+
currentLocaleCode
|
|
99
|
+
)}
|
|
100
|
+
</p>
|
|
101
|
+
</div>
|
|
102
|
+
<span className="rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700">
|
|
103
|
+
{t('active')}
|
|
104
|
+
</span>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
) : (
|
|
108
|
+
<div className="rounded-lg border p-3 text-center">
|
|
109
|
+
<p className="text-sm text-muted-foreground">
|
|
110
|
+
{t('noActiveSession')}
|
|
111
|
+
</p>
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
|
|
115
|
+
{otherSessions.length
|
|
116
|
+
? otherSessions.map((session: UserSession) => {
|
|
117
|
+
const { os, browser } = parseUserAgent(session.user_agent);
|
|
118
|
+
return (
|
|
119
|
+
<div key={session.id} className="rounded-lg border p-3">
|
|
120
|
+
<div className="flex items-center gap-3">
|
|
121
|
+
<div className="rounded-md bg-slate-50 p-2">
|
|
122
|
+
<Monitor className="h-5 w-5 text-slate-600" />
|
|
123
|
+
</div>
|
|
124
|
+
<div className="flex-1">
|
|
125
|
+
<p className="text-sm font-medium">{t('otherSession')}</p>
|
|
126
|
+
<p className="text-xs text-muted-foreground">
|
|
127
|
+
{os} · {browser} · {t('lastActive')}{' '}
|
|
128
|
+
{formatDateTime(
|
|
129
|
+
String(session.updated_at),
|
|
130
|
+
getSettingValue,
|
|
131
|
+
currentLocaleCode
|
|
132
|
+
)}
|
|
133
|
+
</p>
|
|
134
|
+
<p className="text-[11px] text-muted-foreground">
|
|
135
|
+
{t('ip')} {session.ip_address} · {t('expires')}{' '}
|
|
136
|
+
{formatDateTime(
|
|
137
|
+
session.expires_at,
|
|
138
|
+
getSettingValue,
|
|
139
|
+
currentLocaleCode
|
|
140
|
+
)}
|
|
141
|
+
</p>
|
|
142
|
+
</div>
|
|
143
|
+
<button
|
|
144
|
+
className="text-xs text-red-400 cursor-pointer hover:text-red-700"
|
|
145
|
+
onClick={() => {
|
|
146
|
+
handleRevokeSession(Number(session.id));
|
|
147
|
+
}}
|
|
148
|
+
>
|
|
149
|
+
{t('disconnect')}
|
|
150
|
+
</button>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
})
|
|
155
|
+
: null}
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { Button } from '@/components/ui/button';
|
|
2
|
+
import {
|
|
3
|
+
Dialog,
|
|
4
|
+
DialogContent,
|
|
5
|
+
DialogDescription,
|
|
6
|
+
DialogFooter,
|
|
7
|
+
DialogHeader,
|
|
8
|
+
DialogTitle,
|
|
9
|
+
} from '@/components/ui/dialog';
|
|
10
|
+
import {
|
|
11
|
+
InputOTP,
|
|
12
|
+
InputOTPGroup,
|
|
13
|
+
InputOTPSeparator,
|
|
14
|
+
InputOTPSlot,
|
|
15
|
+
} from '@/components/ui/input-otp';
|
|
16
|
+
import { Label } from '@/components/ui/label';
|
|
17
|
+
import { formatDate } from '@/lib/format-date';
|
|
18
|
+
import { User, UserIdentifier } from '@hed-hog/api-types';
|
|
19
|
+
import { useApp } from '@hed-hog/next-app-provider';
|
|
20
|
+
import {
|
|
21
|
+
CheckCircle2,
|
|
22
|
+
Fingerprint,
|
|
23
|
+
HelpCircle,
|
|
24
|
+
Mail,
|
|
25
|
+
Phone,
|
|
26
|
+
ShieldCheck,
|
|
27
|
+
User as UserIcon,
|
|
28
|
+
} from 'lucide-react';
|
|
29
|
+
import { useTranslations } from 'next-intl';
|
|
30
|
+
import { useState } from 'react';
|
|
31
|
+
import { toast } from 'sonner';
|
|
32
|
+
|
|
33
|
+
function getIdentifierIcon(type?: string) {
|
|
34
|
+
const t = (type || '').toLowerCase();
|
|
35
|
+
switch (t) {
|
|
36
|
+
case 'email':
|
|
37
|
+
return Mail;
|
|
38
|
+
case 'phone':
|
|
39
|
+
return Phone;
|
|
40
|
+
case 'username':
|
|
41
|
+
return UserIcon;
|
|
42
|
+
default:
|
|
43
|
+
return Fingerprint;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getIdentifierLabel(type?: string, t?: any) {
|
|
48
|
+
const typeKey = (type || '').toLowerCase();
|
|
49
|
+
|
|
50
|
+
switch (typeKey) {
|
|
51
|
+
case 'email':
|
|
52
|
+
return t ? t('email') : 'Email';
|
|
53
|
+
case 'phone':
|
|
54
|
+
return t ? t('phone') : 'Phone';
|
|
55
|
+
case 'username':
|
|
56
|
+
return t ? t('username') : 'Username';
|
|
57
|
+
default:
|
|
58
|
+
return type
|
|
59
|
+
? type.charAt(0).toUpperCase() + type.slice(1)
|
|
60
|
+
: t
|
|
61
|
+
? t('identifier')
|
|
62
|
+
: 'Identifier';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function UserIdentifiersSection({ editingUser }: { editingUser: User }) {
|
|
67
|
+
const { currentLocaleCode, getSettingValue, request } = useApp();
|
|
68
|
+
const t = useTranslations('core.UserIdentifiers');
|
|
69
|
+
const identifiers: UserIdentifier[] = editingUser?.user_identifier ?? [];
|
|
70
|
+
const sortedIdentifiers = [...identifiers].sort((a, b) => {
|
|
71
|
+
const aDate = new Date(a.created_at || 0).getTime();
|
|
72
|
+
const bDate = new Date(b.created_at || 0).getTime();
|
|
73
|
+
return bDate - aDate;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const [showPinDialog, setShowPinDialog] = useState(false);
|
|
77
|
+
const [verificationCode, setVerificationCode] = useState('');
|
|
78
|
+
const [currentIdentifierId, setCurrentIdentifierId] = useState<number | null>(
|
|
79
|
+
null
|
|
80
|
+
);
|
|
81
|
+
const pinCodeLength = getSettingValue('mfa-email-code-length') || 6;
|
|
82
|
+
|
|
83
|
+
const handleSendVerificationCode = async (identifierId: number) => {
|
|
84
|
+
try {
|
|
85
|
+
await request({
|
|
86
|
+
url: `/user-identifier/${identifierId}/verify`,
|
|
87
|
+
method: 'POST',
|
|
88
|
+
});
|
|
89
|
+
toast.success(t('verificationCodeSent'));
|
|
90
|
+
setCurrentIdentifierId(identifierId);
|
|
91
|
+
setShowPinDialog(true);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
toast.error(t('verifyError'));
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const handleConfirmPin = async () => {
|
|
98
|
+
if (!currentIdentifierId) return;
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
await request({
|
|
102
|
+
url: `/user-identifier/${currentIdentifierId}/verify/confirm`,
|
|
103
|
+
method: 'POST',
|
|
104
|
+
data: { pin: verificationCode },
|
|
105
|
+
});
|
|
106
|
+
toast.success(t('verifySuccess'));
|
|
107
|
+
setShowPinDialog(false);
|
|
108
|
+
setVerificationCode('');
|
|
109
|
+
setCurrentIdentifierId(null);
|
|
110
|
+
window.location.reload();
|
|
111
|
+
} catch (error) {
|
|
112
|
+
toast.error(t('invalidPin'));
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<div className="space-y-3">
|
|
118
|
+
<h4 className="text-sm font-medium">{t('title')}</h4>
|
|
119
|
+
|
|
120
|
+
<div className="space-y-2">
|
|
121
|
+
{sortedIdentifiers.length ? (
|
|
122
|
+
sortedIdentifiers.map((identifier) => {
|
|
123
|
+
const Icon = getIdentifierIcon(identifier.type);
|
|
124
|
+
const label = getIdentifierLabel(identifier.type, t);
|
|
125
|
+
const isVerified = Boolean(identifier.verified_at);
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<div key={identifier.id} className="rounded-lg border p-3">
|
|
129
|
+
<div className="flex items-center justify-between">
|
|
130
|
+
<div className="flex items-center gap-3">
|
|
131
|
+
<div className="rounded-md bg-slate-50 p-2">
|
|
132
|
+
<Icon className="h-5 w-5 text-muted-foreground" />
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<div>
|
|
136
|
+
<p className="text-sm font-medium">{label}</p>
|
|
137
|
+
<p className="text-xs text-muted-foreground">
|
|
138
|
+
{identifier.value}
|
|
139
|
+
</p>
|
|
140
|
+
|
|
141
|
+
{(identifier.created_at || identifier.updated_at) && (
|
|
142
|
+
<p className="mt-1 text-[11px] text-muted-foreground">
|
|
143
|
+
{identifier.created_at && (
|
|
144
|
+
<>
|
|
145
|
+
{t('created')}{' '}
|
|
146
|
+
{formatDate(
|
|
147
|
+
identifier.created_at,
|
|
148
|
+
getSettingValue,
|
|
149
|
+
currentLocaleCode
|
|
150
|
+
)}{' '}
|
|
151
|
+
</>
|
|
152
|
+
)}
|
|
153
|
+
{identifier.updated_at && (
|
|
154
|
+
<>
|
|
155
|
+
• {t('updated')}{' '}
|
|
156
|
+
{formatDate(
|
|
157
|
+
identifier.updated_at,
|
|
158
|
+
getSettingValue,
|
|
159
|
+
currentLocaleCode
|
|
160
|
+
)}
|
|
161
|
+
</>
|
|
162
|
+
)}
|
|
163
|
+
</p>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<div className="flex items-center gap-2">
|
|
169
|
+
{isVerified ? (
|
|
170
|
+
<span className="inline-flex items-center gap-1 rounded-full bg-emerald-50 px-2 py-1 text-xs font-medium text-emerald-700">
|
|
171
|
+
<CheckCircle2 className="h-3.5 w-3.5" />
|
|
172
|
+
{t('verified')}
|
|
173
|
+
</span>
|
|
174
|
+
) : (
|
|
175
|
+
<span className="inline-flex items-center gap-1 rounded-full bg-amber-50 px-2 py-1 text-xs font-medium text-amber-700">
|
|
176
|
+
<HelpCircle className="h-3.5 w-3.5" />
|
|
177
|
+
{t('unverified')}
|
|
178
|
+
</span>
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
})
|
|
185
|
+
) : (
|
|
186
|
+
<div className="rounded-lg border border-dashed p-3 text-center">
|
|
187
|
+
<p className="text-sm text-muted-foreground">
|
|
188
|
+
{t('noIdentifiers')}
|
|
189
|
+
</p>
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<Dialog open={showPinDialog} onOpenChange={setShowPinDialog}>
|
|
195
|
+
<DialogContent className="sm:max-w-lg">
|
|
196
|
+
<DialogHeader>
|
|
197
|
+
<DialogTitle>{t('verifyDialogTitle')}</DialogTitle>
|
|
198
|
+
<DialogDescription>
|
|
199
|
+
{t('verifyDialogDescription')}
|
|
200
|
+
</DialogDescription>
|
|
201
|
+
</DialogHeader>
|
|
202
|
+
<form
|
|
203
|
+
onSubmit={(e) => {
|
|
204
|
+
e.preventDefault();
|
|
205
|
+
handleConfirmPin();
|
|
206
|
+
}}
|
|
207
|
+
className="space-y-4"
|
|
208
|
+
>
|
|
209
|
+
<div className="space-y-2 flex flex-col gap-2 items-center justify-center">
|
|
210
|
+
<Label className="text-sm font-medium">{t('pinCodeLabel')}</Label>
|
|
211
|
+
<InputOTP
|
|
212
|
+
maxLength={pinCodeLength}
|
|
213
|
+
value={verificationCode}
|
|
214
|
+
onChange={setVerificationCode}
|
|
215
|
+
>
|
|
216
|
+
{(() => {
|
|
217
|
+
const groupSize = Math.ceil(pinCodeLength / 2);
|
|
218
|
+
const groups = [
|
|
219
|
+
Array.from({ length: groupSize }, (_, i) => (
|
|
220
|
+
<InputOTPSlot key={i} index={i} />
|
|
221
|
+
)),
|
|
222
|
+
Array.from(
|
|
223
|
+
{ length: pinCodeLength - groupSize },
|
|
224
|
+
(_, i) => (
|
|
225
|
+
<InputOTPSlot
|
|
226
|
+
key={i + groupSize}
|
|
227
|
+
index={i + groupSize}
|
|
228
|
+
/>
|
|
229
|
+
)
|
|
230
|
+
),
|
|
231
|
+
];
|
|
232
|
+
return (
|
|
233
|
+
<>
|
|
234
|
+
<InputOTPGroup>{groups[0]}</InputOTPGroup>
|
|
235
|
+
<InputOTPSeparator />
|
|
236
|
+
<InputOTPGroup>{groups[1]}</InputOTPGroup>
|
|
237
|
+
</>
|
|
238
|
+
);
|
|
239
|
+
})()}
|
|
240
|
+
</InputOTP>
|
|
241
|
+
{currentIdentifierId && (
|
|
242
|
+
<Button
|
|
243
|
+
type="button"
|
|
244
|
+
variant="ghost"
|
|
245
|
+
className="mt-2"
|
|
246
|
+
onClick={() =>
|
|
247
|
+
handleSendVerificationCode(currentIdentifierId)
|
|
248
|
+
}
|
|
249
|
+
>
|
|
250
|
+
{t('resendCode')}
|
|
251
|
+
</Button>
|
|
252
|
+
)}
|
|
253
|
+
</div>
|
|
254
|
+
<DialogFooter>
|
|
255
|
+
<Button
|
|
256
|
+
type="button"
|
|
257
|
+
variant="outline"
|
|
258
|
+
onClick={() => {
|
|
259
|
+
setShowPinDialog(false);
|
|
260
|
+
setVerificationCode('');
|
|
261
|
+
setCurrentIdentifierId(null);
|
|
262
|
+
}}
|
|
263
|
+
>
|
|
264
|
+
{t('cancel')}
|
|
265
|
+
</Button>
|
|
266
|
+
<Button
|
|
267
|
+
type="submit"
|
|
268
|
+
disabled={verificationCode.length !== pinCodeLength}
|
|
269
|
+
>
|
|
270
|
+
<ShieldCheck className="mr-2 size-4" />
|
|
271
|
+
{t('confirmVerification')}
|
|
272
|
+
</Button>
|
|
273
|
+
</DialogFooter>
|
|
274
|
+
</form>
|
|
275
|
+
</DialogContent>
|
|
276
|
+
</Dialog>
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
}
|