@actuate-media/cms-admin 0.28.0 → 0.30.0
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/dist/AdminRoot.d.ts +13 -0
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +22 -4
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/views/account-security.render.test.d.ts +2 -0
- package/dist/__tests__/views/account-security.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/account-security.render.test.js +98 -0
- package/dist/__tests__/views/account-security.render.test.js.map +1 -0
- package/dist/__tests__/views/login-mfa.render.test.d.ts +2 -0
- package/dist/__tests__/views/login-mfa.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/login-mfa.render.test.js +73 -0
- package/dist/__tests__/views/login-mfa.render.test.js.map +1 -0
- package/dist/__tests__/views/settings-save-footer.render.test.d.ts +2 -0
- package/dist/__tests__/views/settings-save-footer.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/settings-save-footer.render.test.js +53 -0
- package/dist/__tests__/views/settings-save-footer.render.test.js.map +1 -0
- package/dist/actuate-admin.css +1 -1
- package/dist/components/Breadcrumbs.d.ts.map +1 -1
- package/dist/components/Breadcrumbs.js +1 -0
- package/dist/components/Breadcrumbs.js.map +1 -1
- package/dist/layout/Header.d.ts +3 -1
- package/dist/layout/Header.d.ts.map +1 -1
- package/dist/layout/Header.js +2 -2
- package/dist/layout/Header.js.map +1 -1
- package/dist/layout/Layout.d.ts +2 -1
- package/dist/layout/Layout.d.ts.map +1 -1
- package/dist/layout/Layout.js +2 -2
- package/dist/layout/Layout.js.map +1 -1
- package/dist/views/Login.d.ts +16 -1
- package/dist/views/Login.d.ts.map +1 -1
- package/dist/views/Login.js +55 -3
- package/dist/views/Login.js.map +1 -1
- package/dist/views/Profile.d.ts +12 -0
- package/dist/views/Profile.d.ts.map +1 -0
- package/dist/views/Profile.js +16 -0
- package/dist/views/Profile.js.map +1 -0
- package/dist/views/Settings.d.ts.map +1 -1
- package/dist/views/Settings.js +54 -51
- package/dist/views/Settings.js.map +1 -1
- package/dist/views/Users.d.ts.map +1 -1
- package/dist/views/Users.js +50 -7
- package/dist/views/Users.js.map +1 -1
- package/dist/views/profile/AccountSecurity.d.ts +2 -0
- package/dist/views/profile/AccountSecurity.d.ts.map +1 -0
- package/dist/views/profile/AccountSecurity.js +117 -0
- package/dist/views/profile/AccountSecurity.js.map +1 -0
- package/dist/views/profile/useMfaEnrollment.d.ts +35 -0
- package/dist/views/profile/useMfaEnrollment.d.ts.map +1 -0
- package/dist/views/profile/useMfaEnrollment.js +103 -0
- package/dist/views/profile/useMfaEnrollment.js.map +1 -0
- package/dist/views/settings/AuthMfaCard.d.ts +2 -1
- package/dist/views/settings/AuthMfaCard.d.ts.map +1 -1
- package/dist/views/settings/AuthMfaCard.js +3 -2
- package/dist/views/settings/AuthMfaCard.js.map +1 -1
- package/dist/views/settings/SecurityTab.d.ts.map +1 -1
- package/dist/views/settings/SecurityTab.js +1 -1
- package/dist/views/settings/SecurityTab.js.map +1 -1
- package/package.json +3 -2
- package/src/AdminRoot.tsx +46 -2
- package/src/__tests__/views/account-security.render.test.tsx +140 -0
- package/src/__tests__/views/login-mfa.render.test.tsx +88 -0
- package/src/__tests__/views/settings-save-footer.render.test.tsx +63 -0
- package/src/components/Breadcrumbs.tsx +1 -0
- package/src/layout/Header.tsx +10 -2
- package/src/layout/Layout.tsx +10 -1
- package/src/views/Login.tsx +247 -94
- package/src/views/Profile.tsx +54 -0
- package/src/views/Settings.tsx +142 -263
- package/src/views/Users.tsx +94 -12
- package/src/views/profile/AccountSecurity.tsx +406 -0
- package/src/views/profile/useMfaEnrollment.ts +120 -0
- package/src/views/settings/AuthMfaCard.tsx +19 -4
- package/src/views/settings/SecurityTab.tsx +1 -0
package/src/views/Login.tsx
CHANGED
|
@@ -25,6 +25,24 @@ export interface LoginProps {
|
|
|
25
25
|
email: string,
|
|
26
26
|
password: string,
|
|
27
27
|
captchaToken?: string,
|
|
28
|
+
) => Promise<{
|
|
29
|
+
success: boolean
|
|
30
|
+
error?: string
|
|
31
|
+
/** Set when the account has TOTP enabled — the password step succeeded but
|
|
32
|
+
* a second factor is still required. */
|
|
33
|
+
requiresTOTP?: boolean
|
|
34
|
+
/** Short-lived token tying the second-factor step to this password step. */
|
|
35
|
+
mfaPendingToken?: string
|
|
36
|
+
}>
|
|
37
|
+
/**
|
|
38
|
+
* Completes the second-factor step. Receives the `mfaPendingToken` from
|
|
39
|
+
* `onLogin` plus the user's authenticator code (or a one-time backup code).
|
|
40
|
+
* Required for accounts with MFA enabled; when omitted, the challenge screen
|
|
41
|
+
* explains that MFA sign-in isn't wired up.
|
|
42
|
+
*/
|
|
43
|
+
onVerifyTotp?: (
|
|
44
|
+
mfaPendingToken: string,
|
|
45
|
+
code: string,
|
|
28
46
|
) => Promise<{ success: boolean; error?: string }>
|
|
29
47
|
onNavigate?: (path: string) => void
|
|
30
48
|
oauthProviders?: string[]
|
|
@@ -189,6 +207,7 @@ async function getCaptchaToken(provider: string, siteKey: string, action: string
|
|
|
189
207
|
|
|
190
208
|
export function Login({
|
|
191
209
|
onLogin,
|
|
210
|
+
onVerifyTotp,
|
|
192
211
|
onNavigate,
|
|
193
212
|
oauthProviders,
|
|
194
213
|
captchaConfig,
|
|
@@ -200,6 +219,14 @@ export function Login({
|
|
|
200
219
|
const [submitting, setSubmitting] = useState(false)
|
|
201
220
|
const [error, setError] = useState('')
|
|
202
221
|
|
|
222
|
+
// Second-factor challenge state. When `mfaPendingToken` is set we swap the
|
|
223
|
+
// email/password form for the TOTP code entry.
|
|
224
|
+
const [mfaPendingToken, setMfaPendingToken] = useState<string | null>(null)
|
|
225
|
+
const [totpCode, setTotpCode] = useState('')
|
|
226
|
+
const [useBackupCode, setUseBackupCode] = useState(false)
|
|
227
|
+
const [totpError, setTotpError] = useState('')
|
|
228
|
+
const [verifying, setVerifying] = useState(false)
|
|
229
|
+
|
|
203
230
|
useCaptchaScript(captchaConfig?.provider, captchaConfig?.siteKey)
|
|
204
231
|
|
|
205
232
|
const canSubmit = email.trim() && password && !submitting
|
|
@@ -224,6 +251,13 @@ export function Login({
|
|
|
224
251
|
|
|
225
252
|
const result = await onLogin(email.trim(), password, captchaToken)
|
|
226
253
|
|
|
254
|
+
if (result.requiresTOTP && result.mfaPendingToken) {
|
|
255
|
+
// Password verified — advance to the second-factor step.
|
|
256
|
+
setMfaPendingToken(result.mfaPendingToken)
|
|
257
|
+
setPassword('')
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
|
|
227
261
|
if (!result.success) {
|
|
228
262
|
setError(result.error ?? 'Invalid email or password')
|
|
229
263
|
}
|
|
@@ -234,6 +268,39 @@ export function Login({
|
|
|
234
268
|
}
|
|
235
269
|
}
|
|
236
270
|
|
|
271
|
+
const handleVerifyTotp = async (e: FormEvent) => {
|
|
272
|
+
e.preventDefault()
|
|
273
|
+
const code = totpCode.trim()
|
|
274
|
+
if (!code || !mfaPendingToken) return
|
|
275
|
+
|
|
276
|
+
setTotpError('')
|
|
277
|
+
|
|
278
|
+
if (!onVerifyTotp) {
|
|
279
|
+
setTotpError('Two-factor sign-in is not available. Contact your administrator.')
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
setVerifying(true)
|
|
284
|
+
try {
|
|
285
|
+
const result = await onVerifyTotp(mfaPendingToken, code)
|
|
286
|
+
if (!result.success) {
|
|
287
|
+
setTotpError(result.error ?? 'That code was not accepted. Try again.')
|
|
288
|
+
}
|
|
289
|
+
} catch (err) {
|
|
290
|
+
setTotpError(err instanceof Error ? err.message : 'An unexpected error occurred')
|
|
291
|
+
} finally {
|
|
292
|
+
setVerifying(false)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const resetToPasswordStep = () => {
|
|
297
|
+
setMfaPendingToken(null)
|
|
298
|
+
setTotpCode('')
|
|
299
|
+
setUseBackupCode(false)
|
|
300
|
+
setTotpError('')
|
|
301
|
+
setError('')
|
|
302
|
+
}
|
|
303
|
+
|
|
237
304
|
const handleOAuthClick = (provider: string) => {
|
|
238
305
|
window.location.href = `/api/cms/auth/oauth/${provider}`
|
|
239
306
|
}
|
|
@@ -269,116 +336,202 @@ export function Login({
|
|
|
269
336
|
<p className="mt-2 text-gray-600">{brandTagline}</p>
|
|
270
337
|
</div>
|
|
271
338
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
<div
|
|
278
|
-
<
|
|
279
|
-
<p className="text-sm text-
|
|
339
|
+
{mfaPendingToken ? (
|
|
340
|
+
<form
|
|
341
|
+
onSubmit={handleVerifyTotp}
|
|
342
|
+
className="space-y-5 rounded-xl border border-gray-200 bg-white p-6 shadow-sm"
|
|
343
|
+
>
|
|
344
|
+
<div>
|
|
345
|
+
<h2 className="text-lg font-semibold text-gray-900">Two-factor authentication</h2>
|
|
346
|
+
<p className="mt-1 text-sm text-gray-600">
|
|
347
|
+
{useBackupCode
|
|
348
|
+
? 'Enter one of your one-time backup codes.'
|
|
349
|
+
: 'Enter the 6-digit code from your authenticator app.'}
|
|
350
|
+
</p>
|
|
280
351
|
</div>
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
placeholder="admin@example.com"
|
|
293
|
-
className="w-full rounded-lg border border-gray-300 px-3 py-2.5 text-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
|
294
|
-
required
|
|
295
|
-
autoFocus
|
|
296
|
-
autoComplete="email"
|
|
297
|
-
/>
|
|
298
|
-
</div>
|
|
299
|
-
|
|
300
|
-
<div>
|
|
301
|
-
<div className="mb-1.5 flex items-center justify-between">
|
|
302
|
-
<label htmlFor="login-password" className="block text-sm font-medium text-gray-700">
|
|
303
|
-
Password
|
|
352
|
+
|
|
353
|
+
{totpError && (
|
|
354
|
+
<div className="flex items-start gap-3 rounded-lg border border-red-200 bg-red-50 p-3">
|
|
355
|
+
<AlertTriangle className="mt-0.5 h-5 w-5 shrink-0 text-red-600" />
|
|
356
|
+
<p className="text-sm text-red-800">{totpError}</p>
|
|
357
|
+
</div>
|
|
358
|
+
)}
|
|
359
|
+
|
|
360
|
+
<div>
|
|
361
|
+
<label htmlFor="totp-code" className="mb-1.5 block text-sm font-medium text-gray-700">
|
|
362
|
+
{useBackupCode ? 'Backup code' : 'Authentication code'}
|
|
304
363
|
</label>
|
|
305
|
-
{onNavigate && (
|
|
306
|
-
<button
|
|
307
|
-
type="button"
|
|
308
|
-
onClick={() => onNavigate('/forgot-password')}
|
|
309
|
-
className="text-xs font-medium text-blue-600 hover:text-blue-700"
|
|
310
|
-
>
|
|
311
|
-
Forgot Password?
|
|
312
|
-
</button>
|
|
313
|
-
)}
|
|
314
|
-
</div>
|
|
315
|
-
<div className="relative">
|
|
316
364
|
<input
|
|
317
|
-
id="
|
|
318
|
-
type=
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
365
|
+
id="totp-code"
|
|
366
|
+
type="text"
|
|
367
|
+
inputMode={useBackupCode ? 'text' : 'numeric'}
|
|
368
|
+
autoComplete="one-time-code"
|
|
369
|
+
value={totpCode}
|
|
370
|
+
onChange={(e) =>
|
|
371
|
+
setTotpCode(
|
|
372
|
+
useBackupCode
|
|
373
|
+
? e.target.value.trim()
|
|
374
|
+
: e.target.value.replace(/\D/g, '').slice(0, 6),
|
|
375
|
+
)
|
|
376
|
+
}
|
|
377
|
+
placeholder={useBackupCode ? 'abcd1234' : '123456'}
|
|
378
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2.5 text-center font-mono text-lg tracking-[0.3em] focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
|
379
|
+
autoFocus
|
|
323
380
|
required
|
|
324
|
-
autoComplete="current-password"
|
|
325
381
|
/>
|
|
382
|
+
</div>
|
|
383
|
+
|
|
384
|
+
<button
|
|
385
|
+
type="submit"
|
|
386
|
+
disabled={!totpCode.trim() || verifying}
|
|
387
|
+
className="flex w-full items-center justify-center gap-2 rounded-lg bg-blue-600 py-2.5 font-medium text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50"
|
|
388
|
+
>
|
|
389
|
+
{verifying ? (
|
|
390
|
+
<>
|
|
391
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
392
|
+
Verifying...
|
|
393
|
+
</>
|
|
394
|
+
) : (
|
|
395
|
+
'Verify'
|
|
396
|
+
)}
|
|
397
|
+
</button>
|
|
398
|
+
|
|
399
|
+
<div className="flex items-center justify-between text-xs">
|
|
326
400
|
<button
|
|
327
401
|
type="button"
|
|
328
|
-
onClick={() =>
|
|
329
|
-
|
|
330
|
-
|
|
402
|
+
onClick={() => {
|
|
403
|
+
setUseBackupCode((v) => !v)
|
|
404
|
+
setTotpCode('')
|
|
405
|
+
setTotpError('')
|
|
406
|
+
}}
|
|
407
|
+
className="font-medium text-blue-600 hover:text-blue-700"
|
|
331
408
|
>
|
|
332
|
-
{
|
|
409
|
+
{useBackupCode ? 'Use authenticator app instead' : 'Use a backup code'}
|
|
410
|
+
</button>
|
|
411
|
+
<button
|
|
412
|
+
type="button"
|
|
413
|
+
onClick={resetToPasswordStep}
|
|
414
|
+
className="font-medium text-gray-500 hover:text-gray-700"
|
|
415
|
+
>
|
|
416
|
+
Back to sign in
|
|
333
417
|
</button>
|
|
334
418
|
</div>
|
|
335
|
-
</
|
|
336
|
-
|
|
337
|
-
<
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
className="flex w-full items-center justify-center gap-2 rounded-lg bg-blue-600 py-2.5 font-medium text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50"
|
|
419
|
+
</form>
|
|
420
|
+
) : (
|
|
421
|
+
<form
|
|
422
|
+
onSubmit={handleSubmit}
|
|
423
|
+
className="space-y-5 rounded-xl border border-gray-200 bg-white p-6 shadow-sm"
|
|
341
424
|
>
|
|
342
|
-
{
|
|
343
|
-
|
|
344
|
-
<
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
) : (
|
|
348
|
-
'Sign In'
|
|
425
|
+
{error && (
|
|
426
|
+
<div className="flex items-start gap-3 rounded-lg border border-red-200 bg-red-50 p-3">
|
|
427
|
+
<AlertTriangle className="mt-0.5 h-5 w-5 shrink-0 text-red-600" />
|
|
428
|
+
<p className="text-sm text-red-800">{error}</p>
|
|
429
|
+
</div>
|
|
349
430
|
)}
|
|
350
|
-
</button>
|
|
351
431
|
|
|
352
|
-
|
|
353
|
-
|
|
432
|
+
<div>
|
|
433
|
+
<label
|
|
434
|
+
htmlFor="login-email"
|
|
435
|
+
className="mb-1.5 block text-sm font-medium text-gray-700"
|
|
436
|
+
>
|
|
437
|
+
Email Address
|
|
438
|
+
</label>
|
|
439
|
+
<input
|
|
440
|
+
id="login-email"
|
|
441
|
+
type="email"
|
|
442
|
+
value={email}
|
|
443
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
444
|
+
placeholder="admin@example.com"
|
|
445
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2.5 text-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
|
446
|
+
required
|
|
447
|
+
autoFocus
|
|
448
|
+
autoComplete="email"
|
|
449
|
+
/>
|
|
450
|
+
</div>
|
|
451
|
+
|
|
452
|
+
<div>
|
|
453
|
+
<div className="mb-1.5 flex items-center justify-between">
|
|
454
|
+
<label htmlFor="login-password" className="block text-sm font-medium text-gray-700">
|
|
455
|
+
Password
|
|
456
|
+
</label>
|
|
457
|
+
{onNavigate && (
|
|
458
|
+
<button
|
|
459
|
+
type="button"
|
|
460
|
+
onClick={() => onNavigate('/forgot-password')}
|
|
461
|
+
className="text-xs font-medium text-blue-600 hover:text-blue-700"
|
|
462
|
+
>
|
|
463
|
+
Forgot Password?
|
|
464
|
+
</button>
|
|
465
|
+
)}
|
|
466
|
+
</div>
|
|
354
467
|
<div className="relative">
|
|
355
|
-
<
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
468
|
+
<input
|
|
469
|
+
id="login-password"
|
|
470
|
+
type={showPassword ? 'text' : 'password'}
|
|
471
|
+
value={password}
|
|
472
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
473
|
+
placeholder="Enter your password"
|
|
474
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2.5 pr-10 text-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
|
475
|
+
required
|
|
476
|
+
autoComplete="current-password"
|
|
477
|
+
/>
|
|
478
|
+
<button
|
|
479
|
+
type="button"
|
|
480
|
+
onClick={() => setShowPassword(!showPassword)}
|
|
481
|
+
className="absolute top-1/2 right-3 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
|
482
|
+
tabIndex={-1}
|
|
483
|
+
>
|
|
484
|
+
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
|
485
|
+
</button>
|
|
361
486
|
</div>
|
|
487
|
+
</div>
|
|
362
488
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
489
|
+
<button
|
|
490
|
+
type="submit"
|
|
491
|
+
disabled={!canSubmit}
|
|
492
|
+
className="flex w-full items-center justify-center gap-2 rounded-lg bg-blue-600 py-2.5 font-medium text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50"
|
|
493
|
+
>
|
|
494
|
+
{submitting ? (
|
|
495
|
+
<>
|
|
496
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
497
|
+
Signing in...
|
|
498
|
+
</>
|
|
499
|
+
) : (
|
|
500
|
+
'Sign In'
|
|
501
|
+
)}
|
|
502
|
+
</button>
|
|
503
|
+
|
|
504
|
+
{enabledProviders.length > 0 && (
|
|
505
|
+
<>
|
|
506
|
+
<div className="relative">
|
|
507
|
+
<div className="absolute inset-0 flex items-center">
|
|
508
|
+
<div className="w-full border-t border-gray-200" />
|
|
509
|
+
</div>
|
|
510
|
+
<div className="relative flex justify-center text-sm">
|
|
511
|
+
<span className="bg-white px-3 text-gray-500">Or continue with</span>
|
|
512
|
+
</div>
|
|
513
|
+
</div>
|
|
514
|
+
|
|
515
|
+
<div className="grid gap-3">
|
|
516
|
+
{enabledProviders.map((provider) => {
|
|
517
|
+
const colors = OAUTH_COLORS[provider] ?? OAUTH_COLORS.google
|
|
518
|
+
return (
|
|
519
|
+
<button
|
|
520
|
+
key={provider}
|
|
521
|
+
type="button"
|
|
522
|
+
onClick={() => handleOAuthClick(provider)}
|
|
523
|
+
className={`flex w-full items-center justify-center gap-3 border px-4 py-2.5 ${colors?.border ?? ''} ${colors?.bg ?? ''} ${colors?.text ?? ''} rounded-lg text-sm font-medium ${colors?.hover ?? ''} transition-colors`}
|
|
524
|
+
>
|
|
525
|
+
<OAuthIcon provider={provider} />
|
|
526
|
+
{OAUTH_LABELS[provider] ?? provider}
|
|
527
|
+
</button>
|
|
528
|
+
)
|
|
529
|
+
})}
|
|
530
|
+
</div>
|
|
531
|
+
</>
|
|
532
|
+
)}
|
|
533
|
+
</form>
|
|
534
|
+
)}
|
|
382
535
|
</div>
|
|
383
536
|
</div>
|
|
384
537
|
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useId } from 'react'
|
|
4
|
+
import { SecurityCard } from './settings/securityPrimitives.js'
|
|
5
|
+
import { AccountSecurity } from './profile/AccountSecurity.js'
|
|
6
|
+
|
|
7
|
+
export interface ProfileProps {
|
|
8
|
+
onNavigate?: (path: string) => void
|
|
9
|
+
session?: { user?: { name?: string; email?: string; role?: string } }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function roleLabel(role?: string): string {
|
|
13
|
+
if (!role) return 'Member'
|
|
14
|
+
return role.charAt(0).toUpperCase() + role.slice(1).toLowerCase()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function Profile({ session }: ProfileProps = {}) {
|
|
18
|
+
const accountHeadingId = useId()
|
|
19
|
+
const user = session?.user
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className="p-3 pr-6 sm:p-4 sm:pr-8">
|
|
23
|
+
<div className="mb-4">
|
|
24
|
+
<h1 className="mb-1 text-2xl font-semibold text-gray-900">Your profile</h1>
|
|
25
|
+
<p className="text-sm text-gray-600">Manage your account details and sign-in security.</p>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div className="grid max-w-3xl gap-4">
|
|
29
|
+
<SecurityCard
|
|
30
|
+
headingId={accountHeadingId}
|
|
31
|
+
title="Account"
|
|
32
|
+
description="Your identity within this CMS. Contact an administrator to change your role."
|
|
33
|
+
>
|
|
34
|
+
<dl className="divide-border divide-y text-sm">
|
|
35
|
+
<div className="flex items-center justify-between gap-3 py-2">
|
|
36
|
+
<dt className="text-muted-foreground">Name</dt>
|
|
37
|
+
<dd className="text-foreground">{user?.name ?? '—'}</dd>
|
|
38
|
+
</div>
|
|
39
|
+
<div className="flex items-center justify-between gap-3 py-2">
|
|
40
|
+
<dt className="text-muted-foreground">Email</dt>
|
|
41
|
+
<dd className="text-foreground">{user?.email ?? '—'}</dd>
|
|
42
|
+
</div>
|
|
43
|
+
<div className="flex items-center justify-between gap-3 py-2">
|
|
44
|
+
<dt className="text-muted-foreground">Role</dt>
|
|
45
|
+
<dd className="text-foreground">{roleLabel(user?.role)}</dd>
|
|
46
|
+
</div>
|
|
47
|
+
</dl>
|
|
48
|
+
</SecurityCard>
|
|
49
|
+
|
|
50
|
+
<AccountSecurity />
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
}
|