@atzentis/auth-react 0.0.15 → 0.0.16

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/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { AuthClient, AuthError, AuthErrorCode } from '@atzentis/auth-sdk';
2
2
  import { createContext, useState, useCallback, useMemo, useContext, useEffect, useRef } from 'react';
3
3
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
- import { DropdownMenu, DropdownMenuTrigger, Button, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, Card, CardHeader, CardTitle, CardDescription, CardContent, DropdownMenuLabel } from '@atzentis/ui-shadcn';
5
- import { OTPInput, FormInput, FormSelect, FormPasswordInput } from '@atzentis/ui-shared';
4
+ import { Card, Button, CardHeader, CardTitle, CardDescription, CardContent, Input, Textarea, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuLabel } from '@atzentis/ui-shadcn';
5
+ import { FormInput, FormSelect, FormPasswordInput, OTPInput } from '@atzentis/ui-shared';
6
6
  import { zodResolver } from '@hookform/resolvers/zod';
7
7
  import { useForm } from 'react-hook-form';
8
8
  import { z } from 'zod';
@@ -266,6 +266,8 @@ var authLocalization = {
266
266
  CHANGE_PASSWORD_DESCRIPTION: "Enter your current and new password",
267
267
  /** @default "Password has been changed" */
268
268
  CHANGE_PASSWORD_SUCCESS: "Password has been changed",
269
+ /** @default "Changing password..." */
270
+ CHANGING_PASSWORD: "Changing password...",
269
271
  /** @default "Forgot Password" */
270
272
  FORGOT_PASSWORD: "Forgot Password",
271
273
  /** @default "Send reset link" */
@@ -286,6 +288,18 @@ var authLocalization = {
286
288
  SET_PASSWORD: "Set Password",
287
289
  /** @default "Click the button to receive an email to set your password" */
288
290
  SET_PASSWORD_DESCRIPTION: "Click the button to receive an email to set your password",
291
+ /** @default "Change Username" */
292
+ CHANGE_USERNAME: "Change Username",
293
+ /** @default "Your unique username" */
294
+ CHANGE_USERNAME_DESCRIPTION: "Your unique username",
295
+ /** @default "Username available" */
296
+ USERNAME_AVAILABLE_STATUS: "Username available",
297
+ /** @default "Username is taken" */
298
+ USERNAME_TAKEN_STATUS: "Username is taken",
299
+ /** @default "Checking..." */
300
+ CHECKING_AVAILABILITY: "Checking...",
301
+ /** @default "Username updated" */
302
+ USERNAME_UPDATED: "Username updated",
289
303
  // --- Forgot Password (P11) ---
290
304
  /** @default "Check your email" */
291
305
  CHECK_YOUR_EMAIL: "Check your email",
@@ -337,6 +351,18 @@ var authLocalization = {
337
351
  SESSIONS: "Sessions",
338
352
  /** @default "Manage your active sessions" */
339
353
  SESSIONS_DESCRIPTION: "Manage your active sessions",
354
+ /** @default "Revoke" */
355
+ REVOKE_SESSION: "Revoke",
356
+ /** @default "Revoke all other sessions" */
357
+ REVOKE_ALL_OTHER_SESSIONS: "Revoke all other sessions",
358
+ /** @default "Session revoked" */
359
+ SESSION_REVOKED: "Session revoked",
360
+ /** @default "All other sessions revoked" */
361
+ ALL_SESSIONS_REVOKED: "All other sessions revoked",
362
+ /** @default "No other sessions" */
363
+ NO_OTHER_SESSIONS: "No other sessions",
364
+ /** @default "Loading sessions..." */
365
+ LOADING_SESSIONS: "Loading sessions...",
340
366
  /** @default "Trust" */
341
367
  TRUST_DEVICE: "Trust",
342
368
  /** @default "Untrust" */
@@ -433,10 +459,26 @@ var authLocalization = {
433
459
  DELETE_AVATAR: "Delete Avatar",
434
460
  /** @default "Upload Avatar" */
435
461
  UPLOAD_AVATAR: "Upload Avatar",
462
+ /** @default "Avatar updated" */
463
+ AVATAR_UPDATED: "Avatar updated",
464
+ /** @default "Avatar deleted" */
465
+ AVATAR_DELETED: "Avatar deleted",
466
+ /** @default "Click to upload" */
467
+ CLICK_TO_UPLOAD: "Click to upload",
468
+ /** @default "File too large" */
469
+ FILE_TOO_LARGE: "File too large",
470
+ /** @default "Max file size:" */
471
+ MAX_FILE_SIZE: "Max file size:",
436
472
  /** @default "Email" */
437
473
  EMAIL: "Email",
438
474
  /** @default "Name" */
439
475
  NAME: "Name",
476
+ /** @default "Update Name" */
477
+ UPDATE_NAME: "Update Name",
478
+ /** @default "Your display name" */
479
+ UPDATE_NAME_DESCRIPTION: "Your display name",
480
+ /** @default "Name updated" */
481
+ NAME_UPDATED: "Name updated",
440
482
  // --- Two-Factor ---
441
483
  /** @default "Two-Factor" */
442
484
  TWO_FACTOR: "Two-Factor",
@@ -467,6 +509,148 @@ var authLocalization = {
467
509
  USE_AUTHENTICATOR: "Use authenticator",
468
510
  /** @default "Enter backup code" */
469
511
  BACKUP_CODE_PLACEHOLDER: "Enter backup code",
512
+ /** @default "Scan this QR code with your authenticator app" */
513
+ SCAN_QR_CODE: "Scan this QR code with your authenticator app",
514
+ /** @default "Or enter this code manually" */
515
+ MANUAL_ENTRY_CODE: "Or enter this code manually",
516
+ /** @default "Enter the code from your authenticator" */
517
+ ENTER_CODE_TO_VERIFY: "Enter the code from your authenticator",
518
+ /** @default "Copy all codes" */
519
+ COPY_BACKUP_CODES: "Copy all codes",
520
+ /** @default "Codes copied" */
521
+ BACKUP_CODES_COPIED: "Codes copied",
522
+ /** @default "Enter your password to disable two-factor" */
523
+ CONFIRM_DISABLE_2FA: "Enter your password to disable two-factor",
524
+ /** @default "Two-factor enabled" */
525
+ TWO_FACTOR_SETUP_SUCCESS: "Two-factor enabled",
526
+ /** @default "Status" */
527
+ STATUS: "Status",
528
+ /** @default "Enabled" */
529
+ ENABLED: "Enabled",
530
+ /** @default "Disabled" */
531
+ DISABLED: "Disabled",
532
+ /** @default "Enable" */
533
+ ENABLE: "Enable",
534
+ /** @default "Disable" */
535
+ DISABLE: "Disable",
536
+ /** @default "Back" */
537
+ BACK: "Back",
538
+ /** @default "Next" */
539
+ NEXT: "Next",
540
+ /** @default "Finish" */
541
+ FINISH: "Finish",
542
+ // --- Email Management (P12) ---
543
+ /** @default "Change Email" */
544
+ CHANGE_EMAIL: "Change Email",
545
+ /** @default "Enter a new email address" */
546
+ CHANGE_EMAIL_DESCRIPTION: "Enter a new email address",
547
+ /** @default "Current email" */
548
+ CURRENT_EMAIL: "Current email",
549
+ /** @default "New email" */
550
+ NEW_EMAIL_PLACEHOLDER: "New email",
551
+ /** @default "Verification email sent to your new address" */
552
+ CHANGE_EMAIL_SENT: "Verification email sent to your new address",
553
+ // --- OAuth Provider Management (P12) ---
554
+ /** @default "Connected Accounts" */
555
+ CONNECTED_PROVIDERS: "Connected Accounts",
556
+ /** @default "Manage linked sign-in methods" */
557
+ CONNECTED_PROVIDERS_DESCRIPTION: "Manage linked sign-in methods",
558
+ /** @default "Connect" */
559
+ CONNECT_PROVIDER: "Connect",
560
+ /** @default "Disconnect" */
561
+ DISCONNECT_PROVIDER: "Disconnect",
562
+ /** @default "No accounts connected" */
563
+ NO_PROVIDERS_CONNECTED: "No accounts connected",
564
+ /** @default "Provider disconnected" */
565
+ PROVIDER_DISCONNECTED: "Provider disconnected",
566
+ /** @default "Connected on" */
567
+ CONNECTED_ON: "Connected on",
568
+ // --- Passkeys (P12) ---
569
+ /** @default "Passkeys" */
570
+ PASSKEYS: "Passkeys",
571
+ /** @default "Manage your passkeys for passwordless sign-in" */
572
+ PASSKEYS_DESCRIPTION: "Manage your passkeys for passwordless sign-in",
573
+ /** @default "Register passkey" */
574
+ REGISTER_PASSKEY: "Register passkey",
575
+ /** @default "Remove" */
576
+ REMOVE_PASSKEY: "Remove",
577
+ /** @default "Passkey name" */
578
+ PASSKEY_NAME: "Passkey name",
579
+ /** @default "No passkeys registered" */
580
+ NO_PASSKEYS: "No passkeys registered",
581
+ /** @default "Your browser doesn't support passkeys" */
582
+ WEBAUTHN_NOT_SUPPORTED: "Your browser doesn't support passkeys",
583
+ /** @default "Passkey registered" */
584
+ PASSKEY_REGISTERED: "Passkey registered",
585
+ /** @default "Passkey removed" */
586
+ PASSKEY_REMOVED: "Passkey removed",
587
+ /** @default "Loading passkeys..." */
588
+ LOADING_PASSKEYS: "Loading passkeys...",
589
+ /** @default "Created" */
590
+ CREATED: "Created",
591
+ /** @default "Last used" */
592
+ LAST_USED_DATE: "Last used",
593
+ /** @default "Never" */
594
+ NEVER: "Never",
595
+ /** @default "Are you sure you want to remove this passkey?" */
596
+ CONFIRM_REMOVE_PASSKEY: "Are you sure you want to remove this passkey?",
597
+ /** @default "Registering..." */
598
+ REGISTERING_PASSKEY: "Registering...",
599
+ /** @default "Removing..." */
600
+ REMOVING_PASSKEY: "Removing...",
601
+ // --- Account Deletion (P12) ---
602
+ /** @default "Delete Account" */
603
+ DELETE_ACCOUNT: "Delete Account",
604
+ /** @default "Permanently delete your account and all data" */
605
+ DELETE_ACCOUNT_WARNING: "Permanently delete your account and all data",
606
+ /** @default "Type \"{{phrase}}\" to confirm" */
607
+ TYPE_TO_CONFIRM: 'Type "{{phrase}}" to confirm',
608
+ /** @default "delete my account" */
609
+ DEFAULT_CONFIRM_PHRASE: "delete my account",
610
+ /** @default "Confirm deletion" */
611
+ CONFIRM_DELETION: "Confirm deletion",
612
+ /** @default "Deleting account..." */
613
+ DELETING_ACCOUNT: "Deleting account...",
614
+ /** @default "Reason for leaving (optional)" */
615
+ DELETE_REASON: "Reason for leaving (optional)",
616
+ // --- API Keys (P12) ---
617
+ /** @default "API Keys" */
618
+ API_KEYS: "API Keys",
619
+ /** @default "Manage your API keys" */
620
+ API_KEYS_DESCRIPTION: "Manage your API keys",
621
+ /** @default "Create API key" */
622
+ CREATE_API_KEY: "Create API key",
623
+ /** @default "Revoke" */
624
+ REVOKE_API_KEY: "Revoke",
625
+ /** @default "Key name" */
626
+ KEY_NAME: "Key name",
627
+ /** @default "Scopes" */
628
+ KEY_SCOPES: "Scopes",
629
+ /** @default "Expires" */
630
+ KEY_EXPIRES: "Expires",
631
+ /** @default "Copy this key now — it won't be shown again" */
632
+ KEY_CREATED_WARNING: "Copy this key now \u2014 it won't be shown again",
633
+ /** @default "Copy key" */
634
+ COPY_KEY: "Copy key",
635
+ /** @default "Key copied" */
636
+ KEY_COPIED: "Key copied",
637
+ /** @default "No API keys" */
638
+ NO_API_KEYS: "No API keys",
639
+ /** @default "30 days" */
640
+ EXPIRES_30_DAYS: "30 days",
641
+ /** @default "90 days" */
642
+ EXPIRES_90_DAYS: "90 days",
643
+ /** @default "1 year" */
644
+ EXPIRES_1_YEAR: "1 year",
645
+ /** @default "Loading API keys..." */
646
+ LOADING_API_KEYS: "Loading API keys...",
647
+ /** @default "my-api-key" */
648
+ KEY_NAME_PLACEHOLDER: "my-api-key",
649
+ // --- Account View (P12) ---
650
+ /** @default "Account Settings" */
651
+ ACCOUNT_SETTINGS: "Account Settings",
652
+ /** @default "Danger Zone" */
653
+ DANGER_ZONE: "Danger Zone",
470
654
  // --- Validation ---
471
655
  /** @default "Passwords do not match" */
472
656
  PASSWORDS_DO_NOT_MATCH: "Passwords do not match",
@@ -971,6 +1155,74 @@ function useOrganizations() {
971
1155
  leave
972
1156
  };
973
1157
  }
1158
+ function usePasskeys() {
1159
+ const { client } = useAuthContext();
1160
+ const [passkeys, setPasskeys] = useState([]);
1161
+ const [isLoading, setIsLoading] = useState(false);
1162
+ const [error, setError] = useState(null);
1163
+ const refresh = useCallback(async () => {
1164
+ setIsLoading(true);
1165
+ setError(null);
1166
+ try {
1167
+ const response = await client.passkeys.list();
1168
+ setPasskeys(response.data);
1169
+ } catch (err) {
1170
+ setError(err);
1171
+ } finally {
1172
+ setIsLoading(false);
1173
+ }
1174
+ }, [client]);
1175
+ const register = useCallback(
1176
+ async (name) => {
1177
+ const challenge = await client.passkeys.getRegistrationChallenge();
1178
+ const credential = await navigator.credentials.create({
1179
+ publicKey: {
1180
+ challenge: Uint8Array.from(challenge.challenge, (c) => c.charCodeAt(0)),
1181
+ rp: {
1182
+ id: challenge.rpId,
1183
+ name: challenge.rpName
1184
+ },
1185
+ user: {
1186
+ id: Uint8Array.from(challenge.userId, (c) => c.charCodeAt(0)),
1187
+ name: challenge.userName,
1188
+ displayName: challenge.userName
1189
+ },
1190
+ pubKeyCredParams: [
1191
+ { alg: -7, type: "public-key" },
1192
+ // ES256
1193
+ { alg: -257, type: "public-key" }
1194
+ // RS256
1195
+ ],
1196
+ timeout: challenge.timeout,
1197
+ attestation: "none"
1198
+ }
1199
+ });
1200
+ const request = {
1201
+ name,
1202
+ credential
1203
+ };
1204
+ const response = await client.passkeys.register(request);
1205
+ await refresh();
1206
+ return response;
1207
+ },
1208
+ [client, refresh]
1209
+ );
1210
+ const remove = useCallback(
1211
+ async (passkeyId) => {
1212
+ await client.passkeys.remove(passkeyId);
1213
+ await refresh();
1214
+ },
1215
+ [client, refresh]
1216
+ );
1217
+ return {
1218
+ passkeys,
1219
+ isLoading,
1220
+ error,
1221
+ refresh,
1222
+ register,
1223
+ remove
1224
+ };
1225
+ }
974
1226
  function usePhone() {
975
1227
  const { client, setUser } = useAuthContext();
976
1228
  const [isSending, setIsSending] = useState(false);
@@ -1216,97 +1468,14 @@ function useUser() {
1216
1468
  };
1217
1469
  }
1218
1470
 
1219
- // src/helpers/initials.ts
1220
- function getInitials(name) {
1221
- return name.split(" ").map((w) => w[0]).join("").toUpperCase().slice(0, 2);
1222
- }
1223
- function AccountSwitcher({
1224
- showAddAccount = false,
1225
- onSwitch,
1226
- onAddAccount,
1227
- maxAccounts = 5,
1228
- className
1229
- }) {
1230
- const { deviceSessions, switchAccount, isLoading, refresh } = useSessions();
1231
- const { localization } = useAuthLocalization();
1232
- useEffect(() => {
1233
- refresh();
1234
- }, [refresh]);
1235
- const handleSwitch = useCallback(
1236
- async (sessionToken, user) => {
1237
- await switchAccount(sessionToken);
1238
- onSwitch?.(user);
1239
- },
1240
- [switchAccount, onSwitch]
1241
- );
1242
- const visibleSessions = deviceSessions.slice(0, maxAccounts);
1243
- const activeSession = deviceSessions.find((s) => s.isActive);
1244
- return /* @__PURE__ */ jsx("div", { className, children: /* @__PURE__ */ jsxs(DropdownMenu, { children: [
1245
- /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "outline", disabled: isLoading, children: activeSession ? activeSession.user.name : localization.SELECT_ACCOUNT }) }),
1246
- /* @__PURE__ */ jsxs(DropdownMenuContent, { align: "end", children: [
1247
- visibleSessions.map((session) => /* @__PURE__ */ jsx(
1248
- DropdownMenuItem,
1249
- {
1250
- onClick: () => handleSwitch(session.sessionToken, session.user),
1251
- children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1252
- /* @__PURE__ */ jsx("span", { className: "flex h-8 w-8 items-center justify-center rounded-full bg-gray-200 text-xs font-medium", children: session.user.avatar ? /* @__PURE__ */ jsx("img", { src: session.user.avatar, alt: "", className: "h-8 w-8 rounded-full" }) : getInitials(session.user.name) }),
1253
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
1254
- /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: session.user.name }),
1255
- /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-500", children: session.user.email })
1256
- ] }),
1257
- session.isActive && /* @__PURE__ */ jsx("span", { className: "ml-auto text-sm", children: "\u2713" })
1258
- ] })
1259
- },
1260
- session.sessionToken
1261
- )),
1262
- showAddAccount && /* @__PURE__ */ jsxs(Fragment, { children: [
1263
- /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
1264
- /* @__PURE__ */ jsx(DropdownMenuItem, { onClick: onAddAccount, children: localization.ADD_ACCOUNT })
1265
- ] })
1266
- ] })
1267
- ] }) });
1268
- }
1269
- function SessionGuard({
1270
- children,
1271
- fallback,
1272
- loadingFallback = null,
1273
- freshRequired = false,
1274
- freshThreshold = 300,
1275
- requiredRole,
1276
- className
1277
- }) {
1278
- const { user, session, isAuthenticated, isLoading } = useSession();
1279
- if (isLoading) {
1280
- return className ? /* @__PURE__ */ jsx("div", { className, children: loadingFallback }) : /* @__PURE__ */ jsx(Fragment, { children: loadingFallback });
1281
- }
1282
- if (!isAuthenticated) {
1283
- return className ? /* @__PURE__ */ jsx("div", { className, children: fallback }) : /* @__PURE__ */ jsx(Fragment, { children: fallback });
1284
- }
1285
- if (freshRequired && session) {
1286
- const sessionAge = (Date.now() - new Date(session.createdAt).getTime()) / 1e3;
1287
- if (sessionAge > freshThreshold) {
1288
- return className ? /* @__PURE__ */ jsx("div", { className, children: fallback }) : /* @__PURE__ */ jsx(Fragment, { children: fallback });
1289
- }
1290
- }
1291
- if (requiredRole && user) {
1292
- const userRole = user.role;
1293
- if (userRole !== requiredRole) {
1294
- return className ? /* @__PURE__ */ jsx("div", { className, children: fallback }) : /* @__PURE__ */ jsx(Fragment, { children: fallback });
1295
- }
1296
- }
1297
- return className ? /* @__PURE__ */ jsx("div", { className, children }) : /* @__PURE__ */ jsx(Fragment, { children });
1298
- }
1299
-
1300
- // src/components/auth/auth-view-paths.ts
1301
- var authViewPaths = {
1302
- "/sign-in": "/sign-in",
1303
- "/sign-up": "/sign-up",
1304
- "/forgot-password": "/forgot-password",
1305
- "/reset-password": "/reset-password",
1306
- "/verify-email": "/verify-email",
1307
- "/email-otp": "/email-otp",
1308
- "/recover-account": "/recover-account",
1309
- "/two-factor": "/two-factor"
1471
+ // src/components/settings/account-view-sections.ts
1472
+ var accountViewSections = {
1473
+ "/account": "/account",
1474
+ "/security": "/security",
1475
+ "/sessions": "/sessions",
1476
+ "/providers": "/providers",
1477
+ "/api-keys": "/api-keys",
1478
+ "/danger": "/danger"
1310
1479
  };
1311
1480
  function toAuthError(error) {
1312
1481
  if (error instanceof AuthError) return error;
@@ -1317,14 +1486,17 @@ function toAuthError(error) {
1317
1486
  statusCode: 0
1318
1487
  });
1319
1488
  }
1320
- var emailOtpEmailSchema = z.object({
1489
+ var changeEmailSchema = z.object({
1321
1490
  email: z.string().min(1).email()
1322
1491
  });
1323
- var emailOtpCodeSchema = z.object({
1324
- code: z.string().length(6).regex(/^\d{6}$/)
1325
- });
1326
- function SubmitButton({ loading, loadingLabel, actionLabel, className }) {
1327
- return /* @__PURE__ */ jsx(Button, { type: "submit", disabled: loading, className: className ?? "w-full", children: loading ? loadingLabel : actionLabel });
1492
+ function SubmitButton({
1493
+ loading,
1494
+ loadingLabel,
1495
+ actionLabel,
1496
+ className,
1497
+ disabled
1498
+ }) {
1499
+ return /* @__PURE__ */ jsx(Button, { type: "submit", disabled: loading || disabled, className: className ?? "w-full", children: loading ? loadingLabel : actionLabel });
1328
1500
  }
1329
1501
 
1330
1502
  // src/components/shared/utils.ts
@@ -1337,6 +1509,1798 @@ function maskEmail(email) {
1337
1509
  const masked = local.length <= 1 ? local : `${local[0]}${"*".repeat(Math.min(local.length - 1, 3))}`;
1338
1510
  return `${masked}@${domain}`;
1339
1511
  }
1512
+ function ChangeEmailCard({
1513
+ redirectUrl,
1514
+ onSuccess,
1515
+ onError,
1516
+ resendCooldown = 60,
1517
+ className
1518
+ }) {
1519
+ const { sendVerificationEmail } = useAuth();
1520
+ const { user } = useUser();
1521
+ const { localization, localizeErrors } = useAuthLocalization();
1522
+ const [sent, setSent] = useState(false);
1523
+ const [sentEmail, setSentEmail] = useState("");
1524
+ const [isSubmitting, setIsSubmitting] = useState(false);
1525
+ const [formError, setFormError] = useState(null);
1526
+ const [cooldownRemaining, setCooldownRemaining] = useState(0);
1527
+ const {
1528
+ register,
1529
+ handleSubmit,
1530
+ formState: { errors },
1531
+ getValues
1532
+ } = useForm({
1533
+ resolver: zodResolver(changeEmailSchema),
1534
+ defaultValues: { email: "" }
1535
+ });
1536
+ useEffect(() => {
1537
+ if (cooldownRemaining <= 0) return;
1538
+ const timer = setInterval(() => {
1539
+ setCooldownRemaining((prev) => Math.max(0, prev - 1));
1540
+ }, 1e3);
1541
+ return () => clearInterval(timer);
1542
+ }, [cooldownRemaining]);
1543
+ const onSubmit = useCallback(
1544
+ async (data) => {
1545
+ setFormError(null);
1546
+ setIsSubmitting(true);
1547
+ try {
1548
+ await sendVerificationEmail({ email: data.email, redirectUrl });
1549
+ setSent(true);
1550
+ setSentEmail(data.email);
1551
+ setCooldownRemaining(resendCooldown);
1552
+ onSuccess?.();
1553
+ } catch (err) {
1554
+ setFormError(getLocalizedError(err, localization, localizeErrors));
1555
+ onError?.(toAuthError(err));
1556
+ } finally {
1557
+ setIsSubmitting(false);
1558
+ }
1559
+ },
1560
+ [
1561
+ sendVerificationEmail,
1562
+ redirectUrl,
1563
+ resendCooldown,
1564
+ onSuccess,
1565
+ onError,
1566
+ localization,
1567
+ localizeErrors
1568
+ ]
1569
+ );
1570
+ const handleResend = useCallback(async () => {
1571
+ const email = getValues("email");
1572
+ setFormError(null);
1573
+ setIsSubmitting(true);
1574
+ try {
1575
+ await sendVerificationEmail({ email, redirectUrl });
1576
+ setCooldownRemaining(resendCooldown);
1577
+ } catch (err) {
1578
+ setFormError(getLocalizedError(err, localization, localizeErrors));
1579
+ } finally {
1580
+ setIsSubmitting(false);
1581
+ }
1582
+ }, [sendVerificationEmail, redirectUrl, resendCooldown, getValues, localization, localizeErrors]);
1583
+ if (sent) {
1584
+ return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs("div", { className: "p-6 space-y-4", children: [
1585
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1586
+ /* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold", children: localization.CHANGE_EMAIL }),
1587
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-gray-500", children: localization.CHANGE_EMAIL_SENT }),
1588
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: maskEmail(sentEmail) })
1589
+ ] }),
1590
+ /* @__PURE__ */ jsx("div", { className: "space-y-2", children: /* @__PURE__ */ jsx(
1591
+ Button,
1592
+ {
1593
+ type: "button",
1594
+ variant: "outline",
1595
+ disabled: isSubmitting || cooldownRemaining > 0,
1596
+ onClick: handleResend,
1597
+ className: "w-full",
1598
+ children: cooldownRemaining > 0 ? localization.RESEND_IN.replace("{{seconds}}", String(cooldownRemaining)) : isSubmitting ? localization.RESENDING : localization.RESEND
1599
+ }
1600
+ ) }),
1601
+ formError && /* @__PURE__ */ jsx("div", { "aria-live": "polite", className: "text-sm text-red-600", children: formError })
1602
+ ] }) });
1603
+ }
1604
+ const currentEmail = user?.email ?? "";
1605
+ return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs("div", { className: "p-6 space-y-4", children: [
1606
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1607
+ /* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold", children: localization.CHANGE_EMAIL }),
1608
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-gray-500", children: localization.CHANGE_EMAIL_DESCRIPTION })
1609
+ ] }),
1610
+ /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit(onSubmit), className: "space-y-4", children: [
1611
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1612
+ /* @__PURE__ */ jsx("label", { className: "text-sm font-medium", htmlFor: "current-email", children: localization.CURRENT_EMAIL }),
1613
+ /* @__PURE__ */ jsx("div", { className: "text-sm text-gray-700 bg-gray-50 rounded-md p-3 border border-gray-200", children: currentEmail })
1614
+ ] }),
1615
+ /* @__PURE__ */ jsx(
1616
+ FormInput,
1617
+ {
1618
+ label: localization.EMAIL,
1619
+ id: "new-email",
1620
+ type: "email",
1621
+ autoComplete: "email",
1622
+ placeholder: localization.NEW_EMAIL_PLACEHOLDER,
1623
+ error: errors.email?.message,
1624
+ disabled: isSubmitting,
1625
+ ...register("email")
1626
+ }
1627
+ ),
1628
+ formError && /* @__PURE__ */ jsx("div", { "aria-live": "polite", className: "text-sm text-red-600", children: formError }),
1629
+ /* @__PURE__ */ jsx(
1630
+ SubmitButton,
1631
+ {
1632
+ loading: isSubmitting,
1633
+ loadingLabel: localization.SENDING,
1634
+ actionLabel: localization.CONTINUE
1635
+ }
1636
+ )
1637
+ ] })
1638
+ ] }) });
1639
+ }
1640
+
1641
+ // src/helpers/initials.ts
1642
+ function getInitials(name) {
1643
+ return name.split(" ").map((w) => w[0]).join("").toUpperCase().slice(0, 2);
1644
+ }
1645
+ function UpdateAvatarCard({
1646
+ onUpload,
1647
+ onSuccess,
1648
+ onError,
1649
+ maxSizeMB = 5,
1650
+ className
1651
+ }) {
1652
+ const { user, updateProfile } = useUser();
1653
+ const { localization, localizeErrors } = useAuthLocalization();
1654
+ const fileInputRef = useRef(null);
1655
+ const [isUploading, setIsUploading] = useState(false);
1656
+ const [previewUrl, setPreviewUrl] = useState(null);
1657
+ const [selectedFile, setSelectedFile] = useState(null);
1658
+ const [formError, setFormError] = useState(null);
1659
+ const [successMessage, setSuccessMessage] = useState(null);
1660
+ useEffect(() => {
1661
+ if (!successMessage) return;
1662
+ const timer = setTimeout(() => {
1663
+ setSuccessMessage(null);
1664
+ }, 3e3);
1665
+ return () => clearTimeout(timer);
1666
+ }, [successMessage]);
1667
+ const handleAvatarClick = useCallback(() => {
1668
+ const input = fileInputRef.current;
1669
+ if (input) {
1670
+ input.click();
1671
+ }
1672
+ }, []);
1673
+ const handleFileChange = useCallback(
1674
+ (event) => {
1675
+ const file = event.target.files?.[0];
1676
+ if (!file) return;
1677
+ setFormError(null);
1678
+ const fileSizeMB = file.size / 1024 / 1024;
1679
+ if (fileSizeMB > maxSizeMB) {
1680
+ setFormError(
1681
+ `${localization.FILE_TOO_LARGE} (${localization.MAX_FILE_SIZE} ${maxSizeMB}MB)`
1682
+ );
1683
+ return;
1684
+ }
1685
+ const reader = new FileReader();
1686
+ reader.onload = (e) => {
1687
+ const result = e.target?.result;
1688
+ if (typeof result === "string") {
1689
+ setPreviewUrl(result);
1690
+ }
1691
+ };
1692
+ reader.readAsDataURL(file);
1693
+ setSelectedFile(file);
1694
+ },
1695
+ [maxSizeMB, localization]
1696
+ );
1697
+ const handleSave = useCallback(async () => {
1698
+ if (!selectedFile) return;
1699
+ setFormError(null);
1700
+ setSuccessMessage(null);
1701
+ setIsUploading(true);
1702
+ try {
1703
+ const avatarUrl = await onUpload(selectedFile);
1704
+ const updatedUser = await updateProfile({ avatar: avatarUrl });
1705
+ setSuccessMessage(localization.AVATAR_UPDATED);
1706
+ setPreviewUrl(null);
1707
+ setSelectedFile(null);
1708
+ onSuccess?.(updatedUser);
1709
+ } catch (err) {
1710
+ setFormError(getLocalizedError(err, localization, localizeErrors));
1711
+ onError?.(toAuthError(err));
1712
+ } finally {
1713
+ setIsUploading(false);
1714
+ }
1715
+ }, [selectedFile, onUpload, updateProfile, localization, localizeErrors, onSuccess, onError]);
1716
+ const handleDelete = useCallback(async () => {
1717
+ setFormError(null);
1718
+ setSuccessMessage(null);
1719
+ setIsUploading(true);
1720
+ try {
1721
+ const updatedUser = await updateProfile({ avatar: "" });
1722
+ setSuccessMessage(localization.AVATAR_DELETED);
1723
+ setPreviewUrl(null);
1724
+ setSelectedFile(null);
1725
+ onSuccess?.(updatedUser);
1726
+ } catch (err) {
1727
+ setFormError(getLocalizedError(err, localization, localizeErrors));
1728
+ onError?.(toAuthError(err));
1729
+ } finally {
1730
+ setIsUploading(false);
1731
+ }
1732
+ }, [updateProfile, localization, localizeErrors, onSuccess, onError]);
1733
+ const handleCancel = useCallback(() => {
1734
+ setPreviewUrl(null);
1735
+ setSelectedFile(null);
1736
+ setFormError(null);
1737
+ if (fileInputRef.current) {
1738
+ fileInputRef.current.value = "";
1739
+ }
1740
+ }, []);
1741
+ const displayUrl = previewUrl ?? user?.avatar;
1742
+ const showPreview = !!selectedFile;
1743
+ const hasAvatar = !!user?.avatar;
1744
+ return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs("div", { className: "p-6 space-y-4", children: [
1745
+ /* @__PURE__ */ jsxs("div", { children: [
1746
+ /* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold", children: localization.AVATAR }),
1747
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-gray-500", children: localization.AVATAR_DESCRIPTION })
1748
+ ] }),
1749
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-4", children: [
1750
+ /* @__PURE__ */ jsxs(
1751
+ "button",
1752
+ {
1753
+ type: "button",
1754
+ onClick: handleAvatarClick,
1755
+ disabled: isUploading,
1756
+ className: "relative h-20 w-20 rounded-full overflow-hidden border-2 border-gray-200 hover:border-gray-300 transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed",
1757
+ children: [
1758
+ displayUrl ? /* @__PURE__ */ jsx(
1759
+ "img",
1760
+ {
1761
+ src: displayUrl,
1762
+ alt: localization.AVATAR,
1763
+ className: "h-full w-full object-cover"
1764
+ }
1765
+ ) : /* @__PURE__ */ jsx("div", { className: "h-full w-full bg-gray-200 flex items-center justify-center text-gray-600 text-lg font-medium", children: getInitials(user?.name ?? "") }),
1766
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-black bg-opacity-0 hover:bg-opacity-10 transition-opacity flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "text-white text-xs opacity-0 hover:opacity-100 transition-opacity", children: localization.CLICK_TO_UPLOAD }) })
1767
+ ]
1768
+ }
1769
+ ),
1770
+ /* @__PURE__ */ jsx(
1771
+ "input",
1772
+ {
1773
+ ref: fileInputRef,
1774
+ type: "file",
1775
+ accept: "image/*",
1776
+ onChange: handleFileChange,
1777
+ disabled: isUploading,
1778
+ className: "hidden"
1779
+ }
1780
+ ),
1781
+ /* @__PURE__ */ jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxs("p", { className: "text-sm text-gray-600", children: [
1782
+ localization.MAX_FILE_SIZE,
1783
+ " ",
1784
+ maxSizeMB,
1785
+ "MB"
1786
+ ] }) })
1787
+ ] }),
1788
+ formError && /* @__PURE__ */ jsx("div", { "aria-live": "polite", className: "text-sm text-red-600", children: formError }),
1789
+ successMessage && /* @__PURE__ */ jsx("div", { "aria-live": "polite", className: "text-sm text-green-600", children: successMessage }),
1790
+ showPreview ? /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
1791
+ /* @__PURE__ */ jsx(Button, { type: "button", onClick: handleSave, disabled: isUploading, children: isUploading ? localization.LOADING : localization.SAVE }),
1792
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", onClick: handleCancel, disabled: isUploading, children: localization.CANCEL })
1793
+ ] }) : hasAvatar && /* @__PURE__ */ jsx(
1794
+ Button,
1795
+ {
1796
+ type: "button",
1797
+ variant: "destructive",
1798
+ onClick: handleDelete,
1799
+ disabled: isUploading,
1800
+ className: "w-full sm:w-auto",
1801
+ children: isUploading ? localization.LOADING : localization.DELETE_AVATAR
1802
+ }
1803
+ )
1804
+ ] }) });
1805
+ }
1806
+ var updateNameSchema = z.object({
1807
+ name: z.string().min(1)
1808
+ });
1809
+ function UpdateNameCard({ onSuccess, onError, className }) {
1810
+ const { user, updateProfile } = useUser();
1811
+ const { localization, localizeErrors } = useAuthLocalization();
1812
+ const [isSubmitting, setIsSubmitting] = useState(false);
1813
+ const [formError, setFormError] = useState(null);
1814
+ const [showSuccess, setShowSuccess] = useState(false);
1815
+ const {
1816
+ register,
1817
+ handleSubmit,
1818
+ formState: { errors }
1819
+ } = useForm({
1820
+ resolver: zodResolver(updateNameSchema),
1821
+ defaultValues: { name: user?.name ?? "" }
1822
+ });
1823
+ useEffect(() => {
1824
+ if (!showSuccess) return;
1825
+ const timer = setTimeout(() => {
1826
+ setShowSuccess(false);
1827
+ }, 3e3);
1828
+ return () => clearTimeout(timer);
1829
+ }, [showSuccess]);
1830
+ const onSubmit = useCallback(
1831
+ async (data) => {
1832
+ setFormError(null);
1833
+ setIsSubmitting(true);
1834
+ try {
1835
+ const updatedUser = await updateProfile({ name: data.name });
1836
+ setShowSuccess(true);
1837
+ onSuccess?.(updatedUser);
1838
+ } catch (err) {
1839
+ setFormError(getLocalizedError(err, localization, localizeErrors));
1840
+ onError?.(toAuthError(err));
1841
+ } finally {
1842
+ setIsSubmitting(false);
1843
+ }
1844
+ },
1845
+ [updateProfile, onSuccess, onError, localization, localizeErrors]
1846
+ );
1847
+ return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs("div", { className: "p-6 space-y-4", children: [
1848
+ /* @__PURE__ */ jsxs("div", { children: [
1849
+ /* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold", children: localization.UPDATE_NAME }),
1850
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-gray-500", children: localization.UPDATE_NAME_DESCRIPTION })
1851
+ ] }),
1852
+ /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit(onSubmit), className: "space-y-4", children: [
1853
+ /* @__PURE__ */ jsx(
1854
+ FormInput,
1855
+ {
1856
+ label: localization.NAME,
1857
+ id: "update-name-input",
1858
+ type: "text",
1859
+ autoComplete: "name",
1860
+ error: errors.name?.message,
1861
+ disabled: isSubmitting,
1862
+ ...register("name")
1863
+ }
1864
+ ),
1865
+ formError && /* @__PURE__ */ jsx("div", { "aria-live": "polite", className: "text-sm text-red-600", children: formError }),
1866
+ showSuccess && /* @__PURE__ */ jsx("div", { "aria-live": "polite", className: "text-sm text-green-600", children: localization.NAME_UPDATED }),
1867
+ /* @__PURE__ */ jsx(
1868
+ SubmitButton,
1869
+ {
1870
+ loading: isSubmitting,
1871
+ loadingLabel: localization.LOADING,
1872
+ actionLabel: localization.SAVE
1873
+ }
1874
+ )
1875
+ ] })
1876
+ ] }) });
1877
+ }
1878
+ var updateUsernameSchema = z.object({
1879
+ username: z.string().min(3, "Username must be at least 3 characters").max(30, "Username must not exceed 30 characters").regex(
1880
+ /^[a-zA-Z0-9_-]+$/,
1881
+ "Username can only contain letters, numbers, underscores, and hyphens"
1882
+ )
1883
+ });
1884
+ function UpdateUsernameCard({ onSuccess, onError, className }) {
1885
+ const { user, updateProfile } = useUser();
1886
+ const { isUsernameAvailable } = useAuth();
1887
+ const { localization, localizeErrors } = useAuthLocalization();
1888
+ const [isSubmitting, setIsSubmitting] = useState(false);
1889
+ const [formError, setFormError] = useState(null);
1890
+ const [showSuccess, setShowSuccess] = useState(false);
1891
+ const [availabilityState, setAvailabilityState] = useState("idle");
1892
+ const {
1893
+ register,
1894
+ handleSubmit,
1895
+ watch,
1896
+ formState: { errors }
1897
+ } = useForm({
1898
+ resolver: zodResolver(updateUsernameSchema),
1899
+ defaultValues: { username: user?.username ?? "" }
1900
+ });
1901
+ const usernameValue = watch("username");
1902
+ useEffect(() => {
1903
+ if (!showSuccess) return;
1904
+ const timer = setTimeout(() => {
1905
+ setShowSuccess(false);
1906
+ }, 3e3);
1907
+ return () => clearTimeout(timer);
1908
+ }, [showSuccess]);
1909
+ useEffect(() => {
1910
+ const currentUsername = usernameValue?.trim();
1911
+ const originalUsername = user?.username;
1912
+ if (!currentUsername || currentUsername === originalUsername) {
1913
+ setAvailabilityState("idle");
1914
+ return;
1915
+ }
1916
+ const isValidFormat = /^[a-zA-Z0-9_-]+$/.test(currentUsername);
1917
+ if (!isValidFormat || currentUsername.length < 3 || currentUsername.length > 30) {
1918
+ setAvailabilityState("idle");
1919
+ return;
1920
+ }
1921
+ setAvailabilityState("checking");
1922
+ const timer = setTimeout(() => {
1923
+ isUsernameAvailable(currentUsername).then((available) => {
1924
+ setAvailabilityState(available ? "available" : "taken");
1925
+ }).catch(() => {
1926
+ setAvailabilityState("idle");
1927
+ });
1928
+ }, 300);
1929
+ return () => clearTimeout(timer);
1930
+ }, [usernameValue, user?.username]);
1931
+ const onSubmit = useCallback(
1932
+ async (data) => {
1933
+ setFormError(null);
1934
+ setIsSubmitting(true);
1935
+ try {
1936
+ const updatedUser = await updateProfile({ username: data.username });
1937
+ setShowSuccess(true);
1938
+ setAvailabilityState("idle");
1939
+ onSuccess?.(updatedUser);
1940
+ } catch (err) {
1941
+ setFormError(getLocalizedError(err, localization, localizeErrors));
1942
+ onError?.(toAuthError(err));
1943
+ } finally {
1944
+ setIsSubmitting(false);
1945
+ }
1946
+ },
1947
+ [updateProfile, onSuccess, onError, localization, localizeErrors]
1948
+ );
1949
+ const isSubmitDisabled = isSubmitting || availabilityState === "checking" || availabilityState === "taken";
1950
+ return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs("div", { className: "p-6 space-y-4", children: [
1951
+ /* @__PURE__ */ jsxs("div", { children: [
1952
+ /* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold", children: localization.CHANGE_USERNAME }),
1953
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-gray-500", children: localization.CHANGE_USERNAME_DESCRIPTION })
1954
+ ] }),
1955
+ /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit(onSubmit), className: "space-y-4", children: [
1956
+ /* @__PURE__ */ jsxs("div", { children: [
1957
+ /* @__PURE__ */ jsx(
1958
+ FormInput,
1959
+ {
1960
+ label: localization.USERNAME,
1961
+ id: "update-username-input",
1962
+ type: "text",
1963
+ autoComplete: "username",
1964
+ error: errors.username?.message,
1965
+ disabled: isSubmitting,
1966
+ ...register("username")
1967
+ }
1968
+ ),
1969
+ availabilityState === "checking" && /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 mt-1", children: localization.CHECKING_AVAILABILITY }),
1970
+ availabilityState === "available" && /* @__PURE__ */ jsx("p", { className: "text-xs text-green-600 mt-1", children: localization.USERNAME_AVAILABLE_STATUS }),
1971
+ availabilityState === "taken" && /* @__PURE__ */ jsx("p", { className: "text-xs text-red-600 mt-1", children: localization.USERNAME_TAKEN_STATUS })
1972
+ ] }),
1973
+ formError && /* @__PURE__ */ jsx("div", { "aria-live": "polite", className: "text-sm text-red-600", children: formError }),
1974
+ showSuccess && /* @__PURE__ */ jsx("div", { "aria-live": "polite", className: "text-sm text-green-600", children: localization.USERNAME_UPDATED }),
1975
+ /* @__PURE__ */ jsx(Button, { type: "submit", disabled: isSubmitDisabled, className: "w-full", children: isSubmitting ? localization.LOADING : localization.SAVE })
1976
+ ] })
1977
+ ] }) });
1978
+ }
1979
+ function AccountSettingsCards({
1980
+ redirectUrl,
1981
+ onUpload,
1982
+ onSuccess,
1983
+ onError,
1984
+ className
1985
+ }) {
1986
+ return /* @__PURE__ */ jsxs("div", { className: `space-y-6 ${className ?? ""}`, children: [
1987
+ /* @__PURE__ */ jsx(UpdateNameCard, { onSuccess, onError }),
1988
+ /* @__PURE__ */ jsx(UpdateUsernameCard, { onSuccess, onError }),
1989
+ onUpload && /* @__PURE__ */ jsx(UpdateAvatarCard, { onUpload, onSuccess, onError }),
1990
+ redirectUrl && /* @__PURE__ */ jsx(ChangeEmailCard, { redirectUrl, onError })
1991
+ ] });
1992
+ }
1993
+ var MILLISECONDS_PER_DAY = 1e3 * 60 * 60 * 24;
1994
+ var createApiKeySchema = z.object({
1995
+ name: z.string().min(1, "Name is required"),
1996
+ expiresIn: z.string()
1997
+ });
1998
+ function ApiKeysCard({
1999
+ availableScopes,
2000
+ onCreated,
2001
+ onRevoked,
2002
+ onError,
2003
+ className
2004
+ }) {
2005
+ const { apiKeys, isLoading, refresh, create, revoke } = useApiKeys();
2006
+ const { localization, localizeErrors } = useAuthLocalization();
2007
+ const [showCreateForm, setShowCreateForm] = useState(false);
2008
+ const [isCreating, setIsCreating] = useState(false);
2009
+ const [isRevoking, setIsRevoking] = useState(null);
2010
+ const [createdKey, setCreatedKey] = useState(null);
2011
+ const [successMessage, setSuccessMessage] = useState(null);
2012
+ const [formError, setFormError] = useState(null);
2013
+ const [copiedKey, setCopiedKey] = useState(false);
2014
+ const expirationOptions = [
2015
+ { value: String(MILLISECONDS_PER_DAY * 30), label: localization.EXPIRES_30_DAYS },
2016
+ { value: String(MILLISECONDS_PER_DAY * 90), label: localization.EXPIRES_90_DAYS },
2017
+ { value: String(MILLISECONDS_PER_DAY * 365), label: localization.EXPIRES_1_YEAR },
2018
+ { value: "0", label: localization.NEVER }
2019
+ ];
2020
+ const {
2021
+ register,
2022
+ handleSubmit,
2023
+ watch,
2024
+ setValue,
2025
+ reset: resetForm,
2026
+ formState: { errors }
2027
+ } = useForm({
2028
+ resolver: zodResolver(createApiKeySchema),
2029
+ defaultValues: { name: "", expiresIn: String(MILLISECONDS_PER_DAY * 30) }
2030
+ });
2031
+ useEffect(() => {
2032
+ refresh();
2033
+ }, [refresh]);
2034
+ useEffect(() => {
2035
+ if (!successMessage) return;
2036
+ const timer = setTimeout(() => {
2037
+ setSuccessMessage(null);
2038
+ }, 3e3);
2039
+ return () => clearTimeout(timer);
2040
+ }, [successMessage]);
2041
+ const handleCreateSubmit = useCallback(
2042
+ async (data) => {
2043
+ setFormError(null);
2044
+ setSuccessMessage(null);
2045
+ setIsCreating(true);
2046
+ try {
2047
+ const expiresInMs = Number.parseInt(data.expiresIn, 10);
2048
+ const response = await create({
2049
+ name: data.name,
2050
+ scopes: availableScopes,
2051
+ expiresIn: expiresInMs > 0 ? expiresInMs : void 0
2052
+ });
2053
+ setCreatedKey(response);
2054
+ resetForm();
2055
+ setShowCreateForm(false);
2056
+ onCreated?.(response.apiKey);
2057
+ } catch (err) {
2058
+ setFormError(getLocalizedError(err, localization, localizeErrors));
2059
+ onError?.(toAuthError(err));
2060
+ } finally {
2061
+ setIsCreating(false);
2062
+ }
2063
+ },
2064
+ [create, availableScopes, localization, localizeErrors, onCreated, onError, resetForm]
2065
+ );
2066
+ const handleRevoke = useCallback(
2067
+ async (keyId) => {
2068
+ setIsRevoking(keyId);
2069
+ setSuccessMessage(null);
2070
+ try {
2071
+ await revoke(keyId);
2072
+ setSuccessMessage(localization.KEY_REVOKED);
2073
+ onRevoked?.(keyId);
2074
+ } catch (err) {
2075
+ onError?.(toAuthError(err));
2076
+ } finally {
2077
+ setIsRevoking(null);
2078
+ }
2079
+ },
2080
+ [revoke, localization, onRevoked, onError]
2081
+ );
2082
+ const handleCopyKey = useCallback(
2083
+ async (key) => {
2084
+ try {
2085
+ await navigator.clipboard.writeText(key);
2086
+ setCopiedKey(true);
2087
+ setTimeout(() => setCopiedKey(false), 2e3);
2088
+ } catch (err) {
2089
+ onError?.(toAuthError(err));
2090
+ }
2091
+ },
2092
+ [onError]
2093
+ );
2094
+ const handleDismissCreatedKey = useCallback(() => {
2095
+ setCreatedKey(null);
2096
+ }, []);
2097
+ const formatDate = (dateString) => {
2098
+ if (!dateString) return localization.NEVER;
2099
+ try {
2100
+ return new Date(dateString).toLocaleString();
2101
+ } catch {
2102
+ return dateString;
2103
+ }
2104
+ };
2105
+ return /* @__PURE__ */ jsxs(Card, { className, children: [
2106
+ /* @__PURE__ */ jsxs(CardHeader, { children: [
2107
+ /* @__PURE__ */ jsx(CardTitle, { children: localization.API_KEYS }),
2108
+ /* @__PURE__ */ jsx(CardDescription, { children: localization.API_KEYS_DESCRIPTION })
2109
+ ] }),
2110
+ /* @__PURE__ */ jsx(CardContent, { children: isLoading && apiKeys.length === 0 ? /* @__PURE__ */ jsx("div", { className: "text-center text-sm text-gray-500", children: localization.LOADING_API_KEYS }) : /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
2111
+ successMessage && /* @__PURE__ */ jsx("div", { "aria-live": "polite", className: "text-sm text-green-600", children: successMessage }),
2112
+ createdKey && /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-yellow-300 bg-yellow-50 p-4 space-y-3", children: [
2113
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-semibold text-yellow-900", children: localization.KEY_CREATED_WARNING }),
2114
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2115
+ /* @__PURE__ */ jsx(
2116
+ Input,
2117
+ {
2118
+ type: "text",
2119
+ value: createdKey.key,
2120
+ readOnly: true,
2121
+ className: "font-mono text-xs bg-white"
2122
+ }
2123
+ ),
2124
+ /* @__PURE__ */ jsx(
2125
+ Button,
2126
+ {
2127
+ type: "button",
2128
+ size: "sm",
2129
+ onClick: () => handleCopyKey(createdKey.key),
2130
+ variant: "outline",
2131
+ children: copiedKey ? localization.KEY_COPIED : localization.COPY_KEY
2132
+ }
2133
+ )
2134
+ ] }),
2135
+ /* @__PURE__ */ jsx(Button, { type: "button", size: "sm", onClick: handleDismissCreatedKey, variant: "ghost", children: localization.DONE })
2136
+ ] }),
2137
+ showCreateForm ? /* @__PURE__ */ jsxs(
2138
+ "form",
2139
+ {
2140
+ onSubmit: handleSubmit(handleCreateSubmit),
2141
+ className: "rounded border p-4 space-y-4",
2142
+ children: [
2143
+ /* @__PURE__ */ jsx(
2144
+ FormInput,
2145
+ {
2146
+ label: localization.KEY_NAME,
2147
+ required: true,
2148
+ placeholder: localization.KEY_NAME_PLACEHOLDER,
2149
+ error: errors.name?.message,
2150
+ disabled: isCreating,
2151
+ ...register("name")
2152
+ }
2153
+ ),
2154
+ /* @__PURE__ */ jsx(
2155
+ FormSelect,
2156
+ {
2157
+ label: localization.KEY_EXPIRES,
2158
+ required: true,
2159
+ options: expirationOptions,
2160
+ value: watch("expiresIn"),
2161
+ onValueChange: (value) => setValue("expiresIn", value),
2162
+ disabled: isCreating
2163
+ }
2164
+ ),
2165
+ formError && /* @__PURE__ */ jsx("div", { "aria-live": "polite", className: "text-sm text-red-600", children: formError }),
2166
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2167
+ /* @__PURE__ */ jsx(Button, { type: "submit", disabled: isCreating, size: "sm", children: isCreating ? localization.LOADING : localization.CREATE_API_KEY }),
2168
+ /* @__PURE__ */ jsx(
2169
+ Button,
2170
+ {
2171
+ type: "button",
2172
+ variant: "outline",
2173
+ size: "sm",
2174
+ onClick: () => {
2175
+ setShowCreateForm(false);
2176
+ resetForm();
2177
+ setFormError(null);
2178
+ },
2179
+ disabled: isCreating,
2180
+ children: localization.CANCEL
2181
+ }
2182
+ )
2183
+ ] })
2184
+ ]
2185
+ }
2186
+ ) : /* @__PURE__ */ jsx(Button, { type: "button", onClick: () => setShowCreateForm(true), size: "sm", children: localization.CREATE_API_KEY }),
2187
+ apiKeys.length === 0 ? /* @__PURE__ */ jsx("div", { className: "text-center text-sm text-gray-500", children: localization.NO_API_KEYS }) : /* @__PURE__ */ jsx("div", { className: "space-y-3", children: apiKeys.map((apiKey) => {
2188
+ const isRevokingThis = isRevoking === apiKey.id;
2189
+ return /* @__PURE__ */ jsxs(
2190
+ "div",
2191
+ {
2192
+ className: "flex items-start justify-between gap-4 rounded border p-4",
2193
+ children: [
2194
+ /* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
2195
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2196
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: apiKey.name }),
2197
+ /* @__PURE__ */ jsxs("span", { className: "rounded bg-gray-100 px-2 py-0.5 text-xs font-mono text-gray-700", children: [
2198
+ apiKey.prefix,
2199
+ "..."
2200
+ ] })
2201
+ ] }),
2202
+ apiKey.scopes && apiKey.scopes.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 mt-2 flex-wrap", children: [
2203
+ /* @__PURE__ */ jsxs("span", { className: "text-xs text-gray-500", children: [
2204
+ localization.KEY_SCOPES,
2205
+ ":"
2206
+ ] }),
2207
+ apiKey.scopes.map((scope) => /* @__PURE__ */ jsx(
2208
+ "span",
2209
+ {
2210
+ className: "rounded bg-blue-100 px-2 py-0.5 text-xs text-blue-700",
2211
+ children: scope
2212
+ },
2213
+ scope
2214
+ ))
2215
+ ] }),
2216
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-gray-500 mt-2", children: [
2217
+ localization.CREATED,
2218
+ ": ",
2219
+ formatDate(apiKey.createdAt)
2220
+ ] }),
2221
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-gray-500", children: [
2222
+ localization.LAST_USED_DATE,
2223
+ ": ",
2224
+ formatDate(apiKey.lastUsedAt)
2225
+ ] }),
2226
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-gray-500", children: [
2227
+ localization.KEY_EXPIRES,
2228
+ ": ",
2229
+ formatDate(apiKey.expiresAt)
2230
+ ] })
2231
+ ] }),
2232
+ /* @__PURE__ */ jsx(
2233
+ Button,
2234
+ {
2235
+ type: "button",
2236
+ variant: "destructive",
2237
+ size: "sm",
2238
+ disabled: isRevokingThis,
2239
+ onClick: () => handleRevoke(apiKey.id),
2240
+ children: isRevokingThis ? localization.LOADING : localization.REVOKE_API_KEY
2241
+ }
2242
+ )
2243
+ ]
2244
+ },
2245
+ apiKey.id
2246
+ );
2247
+ }) })
2248
+ ] }) })
2249
+ ] });
2250
+ }
2251
+ function DeleteAccountCard({
2252
+ onDeleted,
2253
+ onError,
2254
+ confirmationPhrase,
2255
+ className
2256
+ }) {
2257
+ const { deleteAccount } = useUser();
2258
+ const { localization, localizeErrors } = useAuthLocalization();
2259
+ const [step, setStep] = useState("warning");
2260
+ const [confirmationText, setConfirmationText] = useState("");
2261
+ const [password, setPassword] = useState("");
2262
+ const [reason, setReason] = useState("");
2263
+ const [isDeleting, setIsDeleting] = useState(false);
2264
+ const [formError, setFormError] = useState(null);
2265
+ const requiredPhrase = confirmationPhrase ?? localization.DEFAULT_CONFIRM_PHRASE;
2266
+ const isConfirmationValid = confirmationText.trim().toLowerCase() === requiredPhrase.toLowerCase();
2267
+ const handleDeleteClick = useCallback(() => {
2268
+ setStep("confirmation");
2269
+ setFormError(null);
2270
+ }, []);
2271
+ const handleConfirmationNext = useCallback(() => {
2272
+ if (!isConfirmationValid) return;
2273
+ setStep("password");
2274
+ setFormError(null);
2275
+ }, [isConfirmationValid]);
2276
+ const handleSubmit = useCallback(
2277
+ async (e) => {
2278
+ e.preventDefault();
2279
+ if (!password) return;
2280
+ setFormError(null);
2281
+ setIsDeleting(true);
2282
+ try {
2283
+ await deleteAccount(password, reason || void 0);
2284
+ onDeleted?.();
2285
+ } catch (err) {
2286
+ setFormError(getLocalizedError(err, localization, localizeErrors));
2287
+ onError?.(toAuthError(err));
2288
+ } finally {
2289
+ setIsDeleting(false);
2290
+ }
2291
+ },
2292
+ [password, reason, deleteAccount, onDeleted, onError, localization, localizeErrors]
2293
+ );
2294
+ const handleBack = useCallback(() => {
2295
+ if (step === "password") {
2296
+ setStep("confirmation");
2297
+ setPassword("");
2298
+ setReason("");
2299
+ } else if (step === "confirmation") {
2300
+ setStep("warning");
2301
+ setConfirmationText("");
2302
+ }
2303
+ setFormError(null);
2304
+ }, [step]);
2305
+ return /* @__PURE__ */ jsx(Card, { className: `border-red-600 ${className ?? ""}`, children: /* @__PURE__ */ jsxs("div", { className: "p-6 space-y-4", children: [
2306
+ /* @__PURE__ */ jsxs("div", { children: [
2307
+ /* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold text-red-600", children: localization.DELETE_ACCOUNT }),
2308
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-gray-500", children: localization.DELETE_ACCOUNT_WARNING })
2309
+ ] }),
2310
+ step === "warning" && /* @__PURE__ */ jsx("div", { className: "space-y-4", children: /* @__PURE__ */ jsx(Button, { variant: "destructive", onClick: handleDeleteClick, children: localization.DELETE_ACCOUNT }) }),
2311
+ step === "confirmation" && /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
2312
+ /* @__PURE__ */ jsxs("div", { children: [
2313
+ /* @__PURE__ */ jsx("label", { htmlFor: "delete-confirmation", className: "block text-sm font-medium mb-2", children: localization.TYPE_TO_CONFIRM.replace("{{phrase}}", requiredPhrase) }),
2314
+ /* @__PURE__ */ jsx(
2315
+ Input,
2316
+ {
2317
+ id: "delete-confirmation",
2318
+ type: "text",
2319
+ value: confirmationText,
2320
+ onChange: (e) => setConfirmationText(e.target.value),
2321
+ placeholder: requiredPhrase,
2322
+ className: "w-full"
2323
+ }
2324
+ )
2325
+ ] }),
2326
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
2327
+ /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: handleBack, children: localization.CANCEL }),
2328
+ /* @__PURE__ */ jsx(
2329
+ Button,
2330
+ {
2331
+ variant: "destructive",
2332
+ onClick: handleConfirmationNext,
2333
+ disabled: !isConfirmationValid,
2334
+ children: localization.CONTINUE
2335
+ }
2336
+ )
2337
+ ] })
2338
+ ] }),
2339
+ step === "password" && /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [
2340
+ /* @__PURE__ */ jsx(
2341
+ FormPasswordInput,
2342
+ {
2343
+ label: localization.PASSWORD,
2344
+ id: "delete-account-password",
2345
+ value: password,
2346
+ onChange: (e) => setPassword(e.target.value),
2347
+ disabled: isDeleting,
2348
+ required: true
2349
+ }
2350
+ ),
2351
+ /* @__PURE__ */ jsxs("div", { children: [
2352
+ /* @__PURE__ */ jsx("label", { htmlFor: "delete-account-reason", className: "block text-sm font-medium mb-2", children: localization.DELETE_REASON }),
2353
+ /* @__PURE__ */ jsx(
2354
+ Textarea,
2355
+ {
2356
+ id: "delete-account-reason",
2357
+ value: reason,
2358
+ onChange: (e) => setReason(e.target.value),
2359
+ disabled: isDeleting,
2360
+ placeholder: "",
2361
+ rows: 3,
2362
+ className: "w-full"
2363
+ }
2364
+ )
2365
+ ] }),
2366
+ formError && /* @__PURE__ */ jsx("div", { "aria-live": "polite", className: "text-sm text-red-600", children: formError }),
2367
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
2368
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", onClick: handleBack, disabled: isDeleting, children: localization.GO_BACK }),
2369
+ /* @__PURE__ */ jsx(Button, { type: "submit", variant: "destructive", disabled: !password || isDeleting, children: isDeleting ? localization.DELETING_ACCOUNT : localization.CONFIRM_DELETION })
2370
+ ] })
2371
+ ] })
2372
+ ] }) });
2373
+ }
2374
+ function ProvidersCard({
2375
+ availableProviders,
2376
+ oauthRedirectUri: _oauthRedirectUri,
2377
+ onConnect,
2378
+ onDisconnect,
2379
+ onError,
2380
+ className
2381
+ }) {
2382
+ const { listConnectedProviders, disconnectOAuthProvider } = useUser();
2383
+ const { localization, localizeErrors } = useAuthLocalization();
2384
+ const [connectedProviders, setConnectedProviders] = useState([]);
2385
+ const [isLoading, setIsLoading] = useState(true);
2386
+ const [disconnectingProvider, setDisconnectingProvider] = useState(null);
2387
+ const [error, setError] = useState(null);
2388
+ const [successMessage, setSuccessMessage] = useState(null);
2389
+ const loadProviders = useCallback(async () => {
2390
+ setIsLoading(true);
2391
+ setError(null);
2392
+ try {
2393
+ const providers = await listConnectedProviders();
2394
+ setConnectedProviders(providers);
2395
+ } catch (err) {
2396
+ const errorMessage = getLocalizedError(err, localization, localizeErrors);
2397
+ setError(errorMessage);
2398
+ onError?.(toAuthError(err));
2399
+ } finally {
2400
+ setIsLoading(false);
2401
+ }
2402
+ }, [listConnectedProviders, localization, localizeErrors, onError]);
2403
+ useEffect(() => {
2404
+ loadProviders();
2405
+ }, [loadProviders]);
2406
+ useEffect(() => {
2407
+ if (!successMessage) return;
2408
+ const timer = setTimeout(() => {
2409
+ setSuccessMessage(null);
2410
+ }, 3e3);
2411
+ return () => clearTimeout(timer);
2412
+ }, [successMessage]);
2413
+ const handleDisconnect = useCallback(
2414
+ async (provider) => {
2415
+ setDisconnectingProvider(provider);
2416
+ setError(null);
2417
+ setSuccessMessage(null);
2418
+ try {
2419
+ await disconnectOAuthProvider(provider);
2420
+ setSuccessMessage(localization.PROVIDER_DISCONNECTED);
2421
+ onDisconnect?.(provider);
2422
+ await loadProviders();
2423
+ } catch (err) {
2424
+ const errorMessage = getLocalizedError(err, localization, localizeErrors);
2425
+ setError(errorMessage);
2426
+ onError?.(toAuthError(err));
2427
+ } finally {
2428
+ setDisconnectingProvider(null);
2429
+ }
2430
+ },
2431
+ [disconnectOAuthProvider, localization, localizeErrors, onDisconnect, onError, loadProviders]
2432
+ );
2433
+ const handleConnect = useCallback(
2434
+ (provider) => {
2435
+ onConnect?.(provider);
2436
+ },
2437
+ [onConnect]
2438
+ );
2439
+ const getProviderDisplayName = (provider) => {
2440
+ const providerNames = {
2441
+ google: "Google",
2442
+ github: "GitHub",
2443
+ apple: "Apple",
2444
+ microsoft: "Microsoft",
2445
+ atzentis: "Atzentis"
2446
+ };
2447
+ return providerNames[provider] ?? provider;
2448
+ };
2449
+ const connectedProviderIds = new Set(connectedProviders.map((p) => p.provider));
2450
+ const unconnectedProviders = availableProviders.filter((p) => !connectedProviderIds.has(p));
2451
+ const formatDate = (dateString) => {
2452
+ try {
2453
+ const date = new Date(dateString);
2454
+ return date.toLocaleDateString(void 0, { year: "numeric", month: "long", day: "numeric" });
2455
+ } catch {
2456
+ return dateString;
2457
+ }
2458
+ };
2459
+ return /* @__PURE__ */ jsxs(Card, { className, children: [
2460
+ /* @__PURE__ */ jsxs(CardHeader, { children: [
2461
+ /* @__PURE__ */ jsx(CardTitle, { children: localization.CONNECTED_PROVIDERS }),
2462
+ /* @__PURE__ */ jsx(CardDescription, { children: localization.CONNECTED_PROVIDERS_DESCRIPTION })
2463
+ ] }),
2464
+ /* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
2465
+ error && /* @__PURE__ */ jsx("div", { "aria-live": "polite", className: "text-sm text-red-600", children: error }),
2466
+ successMessage && /* @__PURE__ */ jsx("div", { "aria-live": "polite", className: "text-sm text-green-600", children: successMessage }),
2467
+ isLoading ? /* @__PURE__ */ jsx("div", { className: "text-sm text-gray-500", children: localization.LOADING }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2468
+ connectedProviders.length === 0 && unconnectedProviders.length === 0 && /* @__PURE__ */ jsx("div", { className: "text-sm text-gray-500", children: localization.NO_PROVIDERS_CONNECTED }),
2469
+ connectedProviders.length > 0 && /* @__PURE__ */ jsx("div", { className: "space-y-3", children: connectedProviders.map((provider) => /* @__PURE__ */ jsxs(
2470
+ "div",
2471
+ {
2472
+ className: "flex items-center justify-between border rounded-lg p-4",
2473
+ children: [
2474
+ /* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
2475
+ /* @__PURE__ */ jsx("div", { className: "font-medium", children: getProviderDisplayName(provider.provider) }),
2476
+ provider.email && /* @__PURE__ */ jsx("div", { className: "text-sm text-gray-500", children: provider.email }),
2477
+ /* @__PURE__ */ jsxs("div", { className: "text-xs text-gray-400", children: [
2478
+ localization.CONNECTED_ON,
2479
+ " ",
2480
+ formatDate(provider.connectedAt)
2481
+ ] })
2482
+ ] }),
2483
+ /* @__PURE__ */ jsx(
2484
+ Button,
2485
+ {
2486
+ variant: "outline",
2487
+ size: "sm",
2488
+ onClick: () => handleDisconnect(provider.provider),
2489
+ disabled: disconnectingProvider === provider.provider,
2490
+ children: disconnectingProvider === provider.provider ? localization.LOADING : localization.DISCONNECT_PROVIDER
2491
+ }
2492
+ )
2493
+ ]
2494
+ },
2495
+ provider.provider
2496
+ )) }),
2497
+ unconnectedProviders.length > 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
2498
+ connectedProviders.length > 0 && /* @__PURE__ */ jsx("div", { className: "text-sm font-medium text-gray-700 mt-4", children: localization.CONNECT_PROVIDER }),
2499
+ unconnectedProviders.map((provider) => /* @__PURE__ */ jsxs(
2500
+ "div",
2501
+ {
2502
+ className: "flex items-center justify-between border rounded-lg p-4",
2503
+ children: [
2504
+ /* @__PURE__ */ jsx("div", { className: "font-medium", children: getProviderDisplayName(provider) }),
2505
+ /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: () => handleConnect(provider), children: localization.CONNECT_PROVIDER })
2506
+ ]
2507
+ },
2508
+ provider
2509
+ ))
2510
+ ] })
2511
+ ] })
2512
+ ] }) })
2513
+ ] });
2514
+ }
2515
+ var changePasswordSchema = z.object({
2516
+ confirmPassword: z.string().min(1),
2517
+ currentPassword: z.string().min(1),
2518
+ newPassword: z.string().min(8)
2519
+ }).refine((data) => data.newPassword === data.confirmPassword, {
2520
+ message: "PASSWORDS_DO_NOT_MATCH",
2521
+ path: ["confirmPassword"]
2522
+ });
2523
+ function ChangePasswordCard({ onSuccess, onError, className }) {
2524
+ const { changePassword } = useUser();
2525
+ const { localization, localizeErrors } = useAuthLocalization();
2526
+ const [isSubmitting, setIsSubmitting] = useState(false);
2527
+ const [formError, setFormError] = useState(null);
2528
+ const [successMessage, setSuccessMessage] = useState(null);
2529
+ const {
2530
+ register,
2531
+ handleSubmit,
2532
+ formState: { errors },
2533
+ reset
2534
+ } = useForm({
2535
+ resolver: zodResolver(changePasswordSchema),
2536
+ defaultValues: { confirmPassword: "", currentPassword: "", newPassword: "" }
2537
+ });
2538
+ useEffect(() => {
2539
+ if (!successMessage) return;
2540
+ const timer = setTimeout(() => {
2541
+ setSuccessMessage(null);
2542
+ }, 3e3);
2543
+ return () => clearTimeout(timer);
2544
+ }, [successMessage]);
2545
+ const onSubmit = useCallback(
2546
+ async (data) => {
2547
+ setFormError(null);
2548
+ setSuccessMessage(null);
2549
+ setIsSubmitting(true);
2550
+ try {
2551
+ await changePassword(data.currentPassword, data.newPassword);
2552
+ setSuccessMessage(localization.CHANGE_PASSWORD_SUCCESS);
2553
+ reset();
2554
+ onSuccess?.();
2555
+ } catch (err) {
2556
+ setFormError(getLocalizedError(err, localization, localizeErrors));
2557
+ onError?.(toAuthError(err));
2558
+ } finally {
2559
+ setIsSubmitting(false);
2560
+ }
2561
+ },
2562
+ [changePassword, localization, localizeErrors, onSuccess, onError, reset]
2563
+ );
2564
+ return /* @__PURE__ */ jsxs(Card, { className, children: [
2565
+ /* @__PURE__ */ jsxs(CardHeader, { children: [
2566
+ /* @__PURE__ */ jsx(CardTitle, { children: localization.CHANGE_PASSWORD }),
2567
+ /* @__PURE__ */ jsx(CardDescription, { children: localization.CHANGE_PASSWORD_DESCRIPTION })
2568
+ ] }),
2569
+ /* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit(onSubmit), className: "space-y-4", children: [
2570
+ /* @__PURE__ */ jsx(
2571
+ FormPasswordInput,
2572
+ {
2573
+ label: localization.CURRENT_PASSWORD,
2574
+ id: "change-password-current",
2575
+ autoComplete: "current-password",
2576
+ error: errors.currentPassword?.message,
2577
+ disabled: isSubmitting,
2578
+ ...register("currentPassword")
2579
+ }
2580
+ ),
2581
+ /* @__PURE__ */ jsx(
2582
+ FormPasswordInput,
2583
+ {
2584
+ label: localization.NEW_PASSWORD,
2585
+ id: "change-password-new",
2586
+ autoComplete: "new-password",
2587
+ error: errors.newPassword?.message,
2588
+ disabled: isSubmitting,
2589
+ ...register("newPassword")
2590
+ }
2591
+ ),
2592
+ /* @__PURE__ */ jsx(
2593
+ FormPasswordInput,
2594
+ {
2595
+ label: localization.CONFIRM_PASSWORD_PLACEHOLDER,
2596
+ id: "change-password-confirm",
2597
+ autoComplete: "new-password",
2598
+ error: errors.confirmPassword?.message,
2599
+ disabled: isSubmitting,
2600
+ ...register("confirmPassword")
2601
+ }
2602
+ ),
2603
+ formError && /* @__PURE__ */ jsx("div", { "aria-live": "polite", className: "text-sm text-red-600", children: formError }),
2604
+ successMessage && /* @__PURE__ */ jsx("div", { "aria-live": "polite", className: "text-sm text-green-600", children: successMessage }),
2605
+ /* @__PURE__ */ jsx(
2606
+ SubmitButton,
2607
+ {
2608
+ loading: isSubmitting,
2609
+ loadingLabel: localization.CHANGING_PASSWORD,
2610
+ actionLabel: localization.CHANGE_PASSWORD
2611
+ }
2612
+ )
2613
+ ] }) })
2614
+ ] });
2615
+ }
2616
+ function PasskeysCard({ onRegistered, onRemoved, onError, className }) {
2617
+ const { passkeys, isLoading, error, refresh, register, remove } = usePasskeys();
2618
+ const { localization, localizeErrors } = useAuthLocalization();
2619
+ const [isWebAuthnSupported, setIsWebAuthnSupported] = useState(false);
2620
+ const [isRegistering, setIsRegistering] = useState(false);
2621
+ const [showRegisterForm, setShowRegisterForm] = useState(false);
2622
+ const [passkeyName, setPasskeyName] = useState("");
2623
+ const [removingId, setRemovingId] = useState(null);
2624
+ const [confirmRemoveId, setConfirmRemoveId] = useState(null);
2625
+ const [formError, setFormError] = useState(null);
2626
+ const [successMessage, setSuccessMessage] = useState(null);
2627
+ useEffect(() => {
2628
+ const supported = typeof window !== "undefined" && !!window.PublicKeyCredential;
2629
+ setIsWebAuthnSupported(supported);
2630
+ }, []);
2631
+ useEffect(() => {
2632
+ if (isWebAuthnSupported) {
2633
+ refresh();
2634
+ }
2635
+ }, [isWebAuthnSupported, refresh]);
2636
+ useEffect(() => {
2637
+ if (!successMessage) return;
2638
+ const timer = setTimeout(() => {
2639
+ setSuccessMessage(null);
2640
+ }, 3e3);
2641
+ return () => clearTimeout(timer);
2642
+ }, [successMessage]);
2643
+ const handleRegister = useCallback(async () => {
2644
+ if (!passkeyName.trim()) {
2645
+ setFormError(localization.IS_REQUIRED);
2646
+ return;
2647
+ }
2648
+ setFormError(null);
2649
+ setSuccessMessage(null);
2650
+ setIsRegistering(true);
2651
+ try {
2652
+ const response = await register(passkeyName.trim());
2653
+ setSuccessMessage(localization.PASSKEY_REGISTERED);
2654
+ setPasskeyName("");
2655
+ setShowRegisterForm(false);
2656
+ onRegistered?.(response.passkey);
2657
+ } catch (err) {
2658
+ const errorMessage = getLocalizedError(err, localization, localizeErrors);
2659
+ setFormError(errorMessage);
2660
+ onError?.(toAuthError(err));
2661
+ } finally {
2662
+ setIsRegistering(false);
2663
+ }
2664
+ }, [passkeyName, register, localization, localizeErrors, onRegistered, onError]);
2665
+ const handleRemove = useCallback(
2666
+ async (passkeyId) => {
2667
+ setFormError(null);
2668
+ setSuccessMessage(null);
2669
+ setRemovingId(passkeyId);
2670
+ try {
2671
+ await remove(passkeyId);
2672
+ setSuccessMessage(localization.PASSKEY_REMOVED);
2673
+ setConfirmRemoveId(null);
2674
+ onRemoved?.(passkeyId);
2675
+ } catch (err) {
2676
+ const errorMessage = getLocalizedError(err, localization, localizeErrors);
2677
+ setFormError(errorMessage);
2678
+ onError?.(toAuthError(err));
2679
+ } finally {
2680
+ setRemovingId(null);
2681
+ }
2682
+ },
2683
+ [remove, localization, localizeErrors, onRemoved, onError]
2684
+ );
2685
+ const formatDate = (dateString) => {
2686
+ if (!dateString) return localization.NEVER;
2687
+ try {
2688
+ return new Date(dateString).toLocaleDateString();
2689
+ } catch {
2690
+ return localization.UNKNOWN;
2691
+ }
2692
+ };
2693
+ return /* @__PURE__ */ jsxs(Card, { className, children: [
2694
+ /* @__PURE__ */ jsxs(CardHeader, { children: [
2695
+ /* @__PURE__ */ jsx(CardTitle, { children: localization.PASSKEYS }),
2696
+ /* @__PURE__ */ jsx(CardDescription, { children: localization.PASSKEYS_DESCRIPTION })
2697
+ ] }),
2698
+ /* @__PURE__ */ jsxs(CardContent, { className: "space-y-4", children: [
2699
+ !isWebAuthnSupported && /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: localization.WEBAUTHN_NOT_SUPPORTED }),
2700
+ isWebAuthnSupported && /* @__PURE__ */ jsxs(Fragment, { children: [
2701
+ isLoading && /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: localization.LOADING_PASSKEYS }),
2702
+ error && /* @__PURE__ */ jsx("div", { "aria-live": "polite", className: "text-sm text-red-600", children: getLocalizedError(error, localization, localizeErrors) }),
2703
+ formError && /* @__PURE__ */ jsx("div", { "aria-live": "polite", className: "text-sm text-red-600", children: formError }),
2704
+ successMessage && /* @__PURE__ */ jsx("div", { "aria-live": "polite", className: "text-sm text-green-600", children: successMessage }),
2705
+ !isLoading && passkeys.length === 0 && /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: localization.NO_PASSKEYS }),
2706
+ !isLoading && passkeys.length > 0 && /* @__PURE__ */ jsx("div", { className: "space-y-3", children: passkeys.map((passkey) => /* @__PURE__ */ jsxs(
2707
+ "div",
2708
+ {
2709
+ className: "flex items-start justify-between rounded-lg border p-3 space-y-1",
2710
+ children: [
2711
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
2712
+ /* @__PURE__ */ jsx("div", { className: "font-medium", children: passkey.name }),
2713
+ /* @__PURE__ */ jsxs("div", { className: "text-xs text-muted-foreground", children: [
2714
+ localization.CREATED,
2715
+ ": ",
2716
+ formatDate(passkey.createdAt)
2717
+ ] }),
2718
+ /* @__PURE__ */ jsxs("div", { className: "text-xs text-muted-foreground", children: [
2719
+ localization.LAST_USED_DATE,
2720
+ ": ",
2721
+ formatDate(passkey.lastUsedAt)
2722
+ ] })
2723
+ ] }),
2724
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: confirmRemoveId === passkey.id ? /* @__PURE__ */ jsxs(Fragment, { children: [
2725
+ /* @__PURE__ */ jsx(
2726
+ Button,
2727
+ {
2728
+ size: "sm",
2729
+ variant: "destructive",
2730
+ onClick: () => handleRemove(passkey.id),
2731
+ disabled: removingId === passkey.id,
2732
+ children: removingId === passkey.id ? localization.REMOVING_PASSKEY : localization.REMOVE
2733
+ }
2734
+ ),
2735
+ /* @__PURE__ */ jsx(
2736
+ Button,
2737
+ {
2738
+ size: "sm",
2739
+ variant: "outline",
2740
+ onClick: () => setConfirmRemoveId(null),
2741
+ disabled: removingId === passkey.id,
2742
+ children: localization.CANCEL
2743
+ }
2744
+ )
2745
+ ] }) : /* @__PURE__ */ jsx(
2746
+ Button,
2747
+ {
2748
+ size: "sm",
2749
+ variant: "outline",
2750
+ onClick: () => setConfirmRemoveId(passkey.id),
2751
+ disabled: removingId !== null,
2752
+ children: localization.REMOVE_PASSKEY
2753
+ }
2754
+ ) })
2755
+ ]
2756
+ },
2757
+ passkey.id
2758
+ )) }),
2759
+ showRegisterForm ? /* @__PURE__ */ jsxs("div", { className: "space-y-3 rounded-lg border p-3", children: [
2760
+ /* @__PURE__ */ jsx(
2761
+ FormInput,
2762
+ {
2763
+ label: localization.PASSKEY_NAME,
2764
+ id: "passkey-name",
2765
+ value: passkeyName,
2766
+ onChange: (e) => {
2767
+ setPasskeyName(e.target.value);
2768
+ setFormError(null);
2769
+ },
2770
+ placeholder: localization.PASSKEY_NAME,
2771
+ disabled: isRegistering,
2772
+ autoFocus: true
2773
+ }
2774
+ ),
2775
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2776
+ /* @__PURE__ */ jsx(
2777
+ Button,
2778
+ {
2779
+ onClick: handleRegister,
2780
+ disabled: isRegistering || !passkeyName.trim(),
2781
+ size: "sm",
2782
+ children: isRegistering ? localization.REGISTERING_PASSKEY : localization.REGISTER_PASSKEY
2783
+ }
2784
+ ),
2785
+ /* @__PURE__ */ jsx(
2786
+ Button,
2787
+ {
2788
+ variant: "outline",
2789
+ onClick: () => {
2790
+ setShowRegisterForm(false);
2791
+ setPasskeyName("");
2792
+ setFormError(null);
2793
+ },
2794
+ disabled: isRegistering,
2795
+ size: "sm",
2796
+ children: localization.CANCEL
2797
+ }
2798
+ )
2799
+ ] })
2800
+ ] }) : /* @__PURE__ */ jsx(
2801
+ Button,
2802
+ {
2803
+ onClick: () => setShowRegisterForm(true),
2804
+ disabled: isLoading,
2805
+ variant: "outline",
2806
+ size: "sm",
2807
+ children: localization.REGISTER_PASSKEY
2808
+ }
2809
+ )
2810
+ ] })
2811
+ ] })
2812
+ ] });
2813
+ }
2814
+ function TwoFactorCard({ onEnabled, onDisabled, onError, className }) {
2815
+ const { user, setupTwoFactor, verifyTwoFactorSetup, disableTwoFactor } = useUser();
2816
+ const { localization, localizeErrors } = useAuthLocalization();
2817
+ const [step, setStep] = useState("status");
2818
+ const [isSubmitting, setIsSubmitting] = useState(false);
2819
+ const [formError, setFormError] = useState(null);
2820
+ const [twoFactorData, setTwoFactorData] = useState(null);
2821
+ const [disablePassword, setDisablePassword] = useState("");
2822
+ const codeRef = useRef("");
2823
+ const isEnabled = user?.twoFactorEnabled ?? false;
2824
+ const handleEnable = useCallback(async () => {
2825
+ setFormError(null);
2826
+ setIsSubmitting(true);
2827
+ try {
2828
+ const setup = await setupTwoFactor();
2829
+ setTwoFactorData(setup);
2830
+ setStep("qr");
2831
+ } catch (err) {
2832
+ setFormError(getLocalizedError(err, localization, localizeErrors));
2833
+ onError?.(toAuthError(err));
2834
+ } finally {
2835
+ setIsSubmitting(false);
2836
+ }
2837
+ }, [setupTwoFactor, localization, localizeErrors, onError]);
2838
+ const handleOtpComplete = useCallback((code) => {
2839
+ codeRef.current = code;
2840
+ }, []);
2841
+ const handleVerify = useCallback(async () => {
2842
+ const code = codeRef.current;
2843
+ if (!code || code.length !== 6) {
2844
+ setFormError(localization.REQUEST_FAILED);
2845
+ return;
2846
+ }
2847
+ setFormError(null);
2848
+ setIsSubmitting(true);
2849
+ try {
2850
+ await verifyTwoFactorSetup(code);
2851
+ setStep("backup-codes");
2852
+ } catch (err) {
2853
+ setFormError(getLocalizedError(err, localization, localizeErrors));
2854
+ onError?.(toAuthError(err));
2855
+ } finally {
2856
+ setIsSubmitting(false);
2857
+ }
2858
+ }, [verifyTwoFactorSetup, localization, localizeErrors, onError]);
2859
+ const handleCopyBackupCodes = useCallback(async () => {
2860
+ if (!twoFactorData?.backupCodes) return;
2861
+ try {
2862
+ await navigator.clipboard.writeText(twoFactorData.backupCodes.join("\n"));
2863
+ setFormError(null);
2864
+ const successMsg = localization.BACKUP_CODES_COPIED;
2865
+ setFormError(successMsg);
2866
+ setTimeout(() => {
2867
+ setFormError(null);
2868
+ }, 2e3);
2869
+ } catch (err) {
2870
+ setFormError(getLocalizedError(err, localization, localizeErrors));
2871
+ }
2872
+ }, [twoFactorData, localization, localizeErrors]);
2873
+ const handleFinishSetup = useCallback(() => {
2874
+ setStep("status");
2875
+ setTwoFactorData(null);
2876
+ codeRef.current = "";
2877
+ onEnabled?.();
2878
+ }, [onEnabled]);
2879
+ const handleDisableSubmit = useCallback(
2880
+ async (e) => {
2881
+ e.preventDefault();
2882
+ if (!disablePassword.trim()) {
2883
+ setFormError(localization.PASSWORD_REQUIRED);
2884
+ return;
2885
+ }
2886
+ setFormError(null);
2887
+ setIsSubmitting(true);
2888
+ try {
2889
+ await disableTwoFactor(disablePassword);
2890
+ setStep("status");
2891
+ setDisablePassword("");
2892
+ onDisabled?.();
2893
+ } catch (err) {
2894
+ setFormError(getLocalizedError(err, localization, localizeErrors));
2895
+ onError?.(toAuthError(err));
2896
+ } finally {
2897
+ setIsSubmitting(false);
2898
+ }
2899
+ },
2900
+ [disablePassword, disableTwoFactor, localization, localizeErrors, onDisabled, onError]
2901
+ );
2902
+ const handleBack = useCallback(() => {
2903
+ if (step === "qr") {
2904
+ setStep("status");
2905
+ setTwoFactorData(null);
2906
+ codeRef.current = "";
2907
+ } else if (step === "verify") {
2908
+ setStep("qr");
2909
+ codeRef.current = "";
2910
+ } else if (step === "disable") {
2911
+ setStep("status");
2912
+ setDisablePassword("");
2913
+ }
2914
+ setFormError(null);
2915
+ }, [step]);
2916
+ return /* @__PURE__ */ jsxs(Card, { className, children: [
2917
+ /* @__PURE__ */ jsxs(CardHeader, { children: [
2918
+ /* @__PURE__ */ jsxs(CardTitle, { className: "flex items-center justify-between", children: [
2919
+ /* @__PURE__ */ jsx("span", { children: localization.TWO_FACTOR }),
2920
+ step === "status" && /* @__PURE__ */ jsx(
2921
+ "span",
2922
+ {
2923
+ className: `rounded px-2 py-0.5 text-xs ${isEnabled ? "bg-blue-100 text-blue-700" : "bg-gray-100 text-gray-700"}`,
2924
+ children: isEnabled ? localization.ENABLED : localization.DISABLED
2925
+ }
2926
+ )
2927
+ ] }),
2928
+ /* @__PURE__ */ jsxs(CardDescription, { children: [
2929
+ step === "status" && localization.TWO_FACTOR_DESCRIPTION,
2930
+ step === "qr" && localization.SCAN_QR_CODE,
2931
+ step === "verify" && localization.ENTER_CODE_TO_VERIFY,
2932
+ step === "backup-codes" && localization.BACKUP_CODES_DESCRIPTION,
2933
+ step === "disable" && localization.CONFIRM_DISABLE_2FA
2934
+ ] })
2935
+ ] }),
2936
+ /* @__PURE__ */ jsxs(CardContent, { children: [
2937
+ step === "status" && /* @__PURE__ */ jsx("div", { className: "space-y-4", children: isEnabled ? /* @__PURE__ */ jsx(
2938
+ Button,
2939
+ {
2940
+ variant: "destructive",
2941
+ onClick: () => setStep("disable"),
2942
+ disabled: isSubmitting,
2943
+ children: localization.DISABLE_TWO_FACTOR
2944
+ }
2945
+ ) : /* @__PURE__ */ jsx(Button, { onClick: handleEnable, disabled: isSubmitting, children: isSubmitting ? localization.LOADING : localization.ENABLE_TWO_FACTOR }) }),
2946
+ step === "qr" && twoFactorData && /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
2947
+ /* @__PURE__ */ jsx("div", { className: "flex justify-center", children: /* @__PURE__ */ jsx("img", { src: twoFactorData.qrCode, alt: "QR Code", className: "max-w-xs" }) }),
2948
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
2949
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: localization.MANUAL_ENTRY_CODE }),
2950
+ /* @__PURE__ */ jsx("code", { className: "block rounded bg-muted p-3 text-sm font-mono break-all", children: twoFactorData.secret })
2951
+ ] }),
2952
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
2953
+ /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: handleBack, disabled: isSubmitting, children: localization.BACK }),
2954
+ /* @__PURE__ */ jsx(Button, { onClick: () => setStep("verify"), disabled: isSubmitting, children: localization.NEXT })
2955
+ ] })
2956
+ ] }),
2957
+ step === "verify" && /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
2958
+ /* @__PURE__ */ jsxs("label", { className: "block text-sm font-medium", children: [
2959
+ localization.VERIFICATION_CODE_LABEL,
2960
+ /* @__PURE__ */ jsx(
2961
+ OTPInput,
2962
+ {
2963
+ length: 6,
2964
+ onComplete: handleOtpComplete,
2965
+ disabled: isSubmitting,
2966
+ className: "mt-2"
2967
+ }
2968
+ )
2969
+ ] }),
2970
+ formError && /* @__PURE__ */ jsx("div", { "aria-live": "polite", className: "text-sm text-red-600", children: formError }),
2971
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
2972
+ /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: handleBack, disabled: isSubmitting, children: localization.BACK }),
2973
+ /* @__PURE__ */ jsx(Button, { type: "button", onClick: handleVerify, disabled: isSubmitting, children: isSubmitting ? localization.VERIFYING : localization.VERIFY })
2974
+ ] })
2975
+ ] }),
2976
+ step === "backup-codes" && twoFactorData && /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
2977
+ /* @__PURE__ */ jsx("div", { className: "rounded-md border p-4", children: /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-2 font-mono text-sm", children: twoFactorData.backupCodes.map((code) => /* @__PURE__ */ jsx("div", { className: "p-2 bg-muted rounded", children: code }, code)) }) }),
2978
+ /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: handleCopyBackupCodes, className: "w-full", children: localization.COPY_BACKUP_CODES }),
2979
+ formError && /* @__PURE__ */ jsx("div", { "aria-live": "polite", className: "text-sm text-green-600", children: formError }),
2980
+ /* @__PURE__ */ jsx(Button, { onClick: handleFinishSetup, className: "w-full", children: localization.FINISH })
2981
+ ] }),
2982
+ step === "disable" && /* @__PURE__ */ jsxs("form", { onSubmit: handleDisableSubmit, className: "space-y-4", children: [
2983
+ /* @__PURE__ */ jsx(
2984
+ FormPasswordInput,
2985
+ {
2986
+ label: localization.PASSWORD,
2987
+ id: "disable-2fa-password",
2988
+ autoComplete: "current-password",
2989
+ value: disablePassword,
2990
+ onChange: (e) => setDisablePassword(e.target.value),
2991
+ disabled: isSubmitting
2992
+ }
2993
+ ),
2994
+ formError && /* @__PURE__ */ jsx("div", { "aria-live": "polite", className: "text-sm text-red-600", children: formError }),
2995
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
2996
+ /* @__PURE__ */ jsx(Button, { variant: "outline", type: "button", onClick: handleBack, disabled: isSubmitting, children: localization.CANCEL }),
2997
+ /* @__PURE__ */ jsx(Button, { type: "submit", variant: "destructive", disabled: isSubmitting, children: isSubmitting ? localization.LOADING : localization.DISABLE_TWO_FACTOR })
2998
+ ] })
2999
+ ] })
3000
+ ] })
3001
+ ] });
3002
+ }
3003
+ function SecuritySettingsCards({ onError, className }) {
3004
+ return /* @__PURE__ */ jsxs("div", { className: `space-y-6 ${className ?? ""}`, children: [
3005
+ /* @__PURE__ */ jsx(ChangePasswordCard, { onError }),
3006
+ /* @__PURE__ */ jsx(TwoFactorCard, { onError }),
3007
+ /* @__PURE__ */ jsx(PasskeysCard, { onError })
3008
+ ] });
3009
+ }
3010
+ function SessionsCard({ onSessionRevoked, className }) {
3011
+ const {
3012
+ sessions,
3013
+ session: currentSession,
3014
+ isLoading,
3015
+ refresh,
3016
+ revokeSession,
3017
+ revokeAllSessions
3018
+ } = useSessions();
3019
+ const { localization } = useAuthLocalization();
3020
+ const [isRevoking, setIsRevoking] = useState(null);
3021
+ const [isRevokingAll, setIsRevokingAll] = useState(false);
3022
+ const [successMessage, setSuccessMessage] = useState(null);
3023
+ useEffect(() => {
3024
+ refresh();
3025
+ }, [refresh]);
3026
+ useEffect(() => {
3027
+ if (!successMessage) return;
3028
+ const timer = setTimeout(() => {
3029
+ setSuccessMessage(null);
3030
+ }, 3e3);
3031
+ return () => clearTimeout(timer);
3032
+ }, [successMessage]);
3033
+ const handleRevokeSession = useCallback(
3034
+ async (sessionId) => {
3035
+ setIsRevoking(sessionId);
3036
+ setSuccessMessage(null);
3037
+ try {
3038
+ await revokeSession(sessionId);
3039
+ setSuccessMessage(localization.SESSION_REVOKED);
3040
+ onSessionRevoked?.(sessionId);
3041
+ } catch {
3042
+ } finally {
3043
+ setIsRevoking(null);
3044
+ }
3045
+ },
3046
+ [revokeSession, localization, onSessionRevoked]
3047
+ );
3048
+ const handleRevokeAllOtherSessions = useCallback(async () => {
3049
+ setIsRevokingAll(true);
3050
+ setSuccessMessage(null);
3051
+ try {
3052
+ await revokeAllSessions({ exceptCurrent: true });
3053
+ setSuccessMessage(localization.ALL_SESSIONS_REVOKED);
3054
+ } catch {
3055
+ } finally {
3056
+ setIsRevokingAll(false);
3057
+ }
3058
+ }, [revokeAllSessions, localization]);
3059
+ const formatDate = (dateString) => {
3060
+ if (!dateString) return "";
3061
+ try {
3062
+ return new Date(dateString).toLocaleString();
3063
+ } catch {
3064
+ return dateString;
3065
+ }
3066
+ };
3067
+ const getDeviceName = (session) => {
3068
+ return session.device?.name || session.userAgent || localization.UNKNOWN;
3069
+ };
3070
+ const isCurrentSession = (session) => {
3071
+ return currentSession?.id === session.id;
3072
+ };
3073
+ const otherSessions = sessions.filter((s) => !isCurrentSession(s));
3074
+ return /* @__PURE__ */ jsxs(Card, { className, children: [
3075
+ /* @__PURE__ */ jsxs(CardHeader, { children: [
3076
+ /* @__PURE__ */ jsx(CardTitle, { children: localization.SESSIONS }),
3077
+ /* @__PURE__ */ jsx(CardDescription, { children: localization.SESSIONS_DESCRIPTION })
3078
+ ] }),
3079
+ /* @__PURE__ */ jsx(CardContent, { children: isLoading && sessions.length === 0 ? /* @__PURE__ */ jsx("div", { className: "text-center text-sm text-gray-500", children: localization.LOADING_SESSIONS }) : /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
3080
+ successMessage && /* @__PURE__ */ jsx("div", { "aria-live": "polite", className: "text-sm text-green-600", children: successMessage }),
3081
+ sessions.length === 0 ? /* @__PURE__ */ jsx("div", { className: "text-center text-sm text-gray-500", children: localization.NO_OTHER_SESSIONS }) : /* @__PURE__ */ jsx("div", { className: "space-y-3", children: sessions.map((session) => {
3082
+ const isCurrent = isCurrentSession(session);
3083
+ const isRevokingThis = isRevoking === session.id;
3084
+ return /* @__PURE__ */ jsxs(
3085
+ "div",
3086
+ {
3087
+ className: "flex items-start justify-between gap-4 rounded border p-4",
3088
+ children: [
3089
+ /* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
3090
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
3091
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: getDeviceName(session) }),
3092
+ isCurrent && /* @__PURE__ */ jsx("span", { className: "rounded bg-blue-100 px-2 py-0.5 text-xs text-blue-700", children: localization.CURRENT_SESSION })
3093
+ ] }),
3094
+ /* @__PURE__ */ jsxs("p", { className: "text-sm text-gray-500", children: [
3095
+ localization.IP_LABEL,
3096
+ " ",
3097
+ session.ip
3098
+ ] }),
3099
+ session.lastActivityAt && /* @__PURE__ */ jsxs("p", { className: "text-xs text-gray-400", children: [
3100
+ localization.LAST_USED,
3101
+ " ",
3102
+ formatDate(session.lastActivityAt)
3103
+ ] })
3104
+ ] }),
3105
+ !isCurrent && /* @__PURE__ */ jsx(
3106
+ Button,
3107
+ {
3108
+ type: "button",
3109
+ variant: "destructive",
3110
+ size: "sm",
3111
+ disabled: isRevokingThis || isRevokingAll,
3112
+ onClick: () => handleRevokeSession(session.id),
3113
+ children: isRevokingThis ? localization.LOADING : localization.REVOKE_SESSION
3114
+ }
3115
+ )
3116
+ ]
3117
+ },
3118
+ session.id
3119
+ );
3120
+ }) }),
3121
+ otherSessions.length > 0 && /* @__PURE__ */ jsx("div", { className: "pt-4 border-t", children: /* @__PURE__ */ jsx(
3122
+ Button,
3123
+ {
3124
+ type: "button",
3125
+ variant: "outline",
3126
+ className: "w-full",
3127
+ disabled: isRevokingAll || isRevoking !== null,
3128
+ onClick: handleRevokeAllOtherSessions,
3129
+ children: isRevokingAll ? localization.LOADING : localization.REVOKE_ALL_OTHER_SESSIONS
3130
+ }
3131
+ ) })
3132
+ ] }) })
3133
+ ] });
3134
+ }
3135
+ function AccountView({
3136
+ section,
3137
+ sections,
3138
+ fallback = null,
3139
+ onSectionChange,
3140
+ oauthProviders,
3141
+ oauthRedirectUri,
3142
+ redirectUrl,
3143
+ onUpload,
3144
+ availableScopes,
3145
+ className
3146
+ }) {
3147
+ const { localization } = useAuthLocalization();
3148
+ const resolvedSections = useMemo(() => {
3149
+ return { ...accountViewSections, ...sections };
3150
+ }, [sections]);
3151
+ const sectionLabels = useMemo(
3152
+ () => ({
3153
+ "/account": localization.ACCOUNT,
3154
+ "/security": localization.SECURITY,
3155
+ "/sessions": localization.SESSIONS,
3156
+ "/providers": localization.CONNECTED_PROVIDERS,
3157
+ "/api-keys": localization.API_KEYS,
3158
+ "/danger": localization.DANGER_ZONE
3159
+ }),
3160
+ [localization]
3161
+ );
3162
+ const matchedSection = Object.entries(resolvedSections).find(
3163
+ ([, mappedPath]) => mappedPath === section
3164
+ )?.[0];
3165
+ const renderContent = () => {
3166
+ switch (matchedSection) {
3167
+ case "/account":
3168
+ return /* @__PURE__ */ jsx(AccountSettingsCards, { redirectUrl, onUpload });
3169
+ case "/security":
3170
+ return /* @__PURE__ */ jsx(SecuritySettingsCards, {});
3171
+ case "/sessions":
3172
+ return /* @__PURE__ */ jsx(SessionsCard, {});
3173
+ case "/providers":
3174
+ if (oauthProviders && oauthRedirectUri) {
3175
+ return /* @__PURE__ */ jsx(
3176
+ ProvidersCard,
3177
+ {
3178
+ availableProviders: oauthProviders,
3179
+ oauthRedirectUri
3180
+ }
3181
+ );
3182
+ }
3183
+ return /* @__PURE__ */ jsx(ProvidersCard, { availableProviders: [], oauthRedirectUri: "" });
3184
+ case "/api-keys":
3185
+ return /* @__PURE__ */ jsx(ApiKeysCard, { availableScopes });
3186
+ case "/danger":
3187
+ return /* @__PURE__ */ jsx(DeleteAccountCard, {});
3188
+ default:
3189
+ return /* @__PURE__ */ jsx(Fragment, { children: fallback });
3190
+ }
3191
+ };
3192
+ return /* @__PURE__ */ jsxs("div", { className: `flex flex-col md:flex-row gap-8 ${className ?? ""}`, children: [
3193
+ /* @__PURE__ */ jsx("nav", { className: "w-full md:w-48 shrink-0", children: /* @__PURE__ */ jsx("ul", { className: "flex md:flex-col gap-1 overflow-x-auto md:overflow-visible", children: Object.entries(resolvedSections).map(
3194
+ ([sectionKey, sectionPath]) => {
3195
+ const isActive = sectionPath === section;
3196
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
3197
+ "button",
3198
+ {
3199
+ type: "button",
3200
+ onClick: () => onSectionChange?.(sectionPath),
3201
+ className: `w-full text-left px-3 py-2 rounded-md text-sm whitespace-nowrap transition-colors ${isActive ? "bg-gray-100 font-medium text-gray-900" : "text-gray-600 hover:bg-gray-50 hover:text-gray-900"}`,
3202
+ children: sectionLabels[sectionKey]
3203
+ }
3204
+ ) }, sectionKey);
3205
+ }
3206
+ ) }) }),
3207
+ /* @__PURE__ */ jsx("div", { className: "flex-1 min-w-0", children: renderContent() })
3208
+ ] });
3209
+ }
3210
+ function AccountSwitcher({
3211
+ showAddAccount = false,
3212
+ onSwitch,
3213
+ onAddAccount,
3214
+ maxAccounts = 5,
3215
+ className
3216
+ }) {
3217
+ const { deviceSessions, switchAccount, isLoading, refresh } = useSessions();
3218
+ const { localization } = useAuthLocalization();
3219
+ useEffect(() => {
3220
+ refresh();
3221
+ }, [refresh]);
3222
+ const handleSwitch = useCallback(
3223
+ async (sessionToken, user) => {
3224
+ await switchAccount(sessionToken);
3225
+ onSwitch?.(user);
3226
+ },
3227
+ [switchAccount, onSwitch]
3228
+ );
3229
+ const visibleSessions = deviceSessions.slice(0, maxAccounts);
3230
+ const activeSession = deviceSessions.find((s) => s.isActive);
3231
+ return /* @__PURE__ */ jsx("div", { className, children: /* @__PURE__ */ jsxs(DropdownMenu, { children: [
3232
+ /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "outline", disabled: isLoading, children: activeSession ? activeSession.user.name : localization.SELECT_ACCOUNT }) }),
3233
+ /* @__PURE__ */ jsxs(DropdownMenuContent, { align: "end", children: [
3234
+ visibleSessions.map((session) => /* @__PURE__ */ jsx(
3235
+ DropdownMenuItem,
3236
+ {
3237
+ onClick: () => handleSwitch(session.sessionToken, session.user),
3238
+ children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
3239
+ /* @__PURE__ */ jsx("span", { className: "flex h-8 w-8 items-center justify-center rounded-full bg-gray-200 text-xs font-medium", children: session.user.avatar ? /* @__PURE__ */ jsx("img", { src: session.user.avatar, alt: "", className: "h-8 w-8 rounded-full" }) : getInitials(session.user.name) }),
3240
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
3241
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: session.user.name }),
3242
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-500", children: session.user.email })
3243
+ ] }),
3244
+ session.isActive && /* @__PURE__ */ jsx("span", { className: "ml-auto text-sm", children: "\u2713" })
3245
+ ] })
3246
+ },
3247
+ session.sessionToken
3248
+ )),
3249
+ showAddAccount && /* @__PURE__ */ jsxs(Fragment, { children: [
3250
+ /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
3251
+ /* @__PURE__ */ jsx(DropdownMenuItem, { onClick: onAddAccount, children: localization.ADD_ACCOUNT })
3252
+ ] })
3253
+ ] })
3254
+ ] }) });
3255
+ }
3256
+ function SessionGuard({
3257
+ children,
3258
+ fallback,
3259
+ loadingFallback = null,
3260
+ freshRequired = false,
3261
+ freshThreshold = 300,
3262
+ requiredRole,
3263
+ className
3264
+ }) {
3265
+ const { user, session, isAuthenticated, isLoading } = useSession();
3266
+ if (isLoading) {
3267
+ return className ? /* @__PURE__ */ jsx("div", { className, children: loadingFallback }) : /* @__PURE__ */ jsx(Fragment, { children: loadingFallback });
3268
+ }
3269
+ if (!isAuthenticated) {
3270
+ return className ? /* @__PURE__ */ jsx("div", { className, children: fallback }) : /* @__PURE__ */ jsx(Fragment, { children: fallback });
3271
+ }
3272
+ if (freshRequired && session) {
3273
+ const sessionAge = (Date.now() - new Date(session.createdAt).getTime()) / 1e3;
3274
+ if (sessionAge > freshThreshold) {
3275
+ return className ? /* @__PURE__ */ jsx("div", { className, children: fallback }) : /* @__PURE__ */ jsx(Fragment, { children: fallback });
3276
+ }
3277
+ }
3278
+ if (requiredRole && user) {
3279
+ const userRole = user.role;
3280
+ if (userRole !== requiredRole) {
3281
+ return className ? /* @__PURE__ */ jsx("div", { className, children: fallback }) : /* @__PURE__ */ jsx(Fragment, { children: fallback });
3282
+ }
3283
+ }
3284
+ return className ? /* @__PURE__ */ jsx("div", { className, children }) : /* @__PURE__ */ jsx(Fragment, { children });
3285
+ }
3286
+
3287
+ // src/components/auth/auth-view-paths.ts
3288
+ var authViewPaths = {
3289
+ "/sign-in": "/sign-in",
3290
+ "/sign-up": "/sign-up",
3291
+ "/forgot-password": "/forgot-password",
3292
+ "/reset-password": "/reset-password",
3293
+ "/verify-email": "/verify-email",
3294
+ "/email-otp": "/email-otp",
3295
+ "/recover-account": "/recover-account",
3296
+ "/two-factor": "/two-factor"
3297
+ };
3298
+ var emailOtpEmailSchema = z.object({
3299
+ email: z.string().min(1).email()
3300
+ });
3301
+ var emailOtpCodeSchema = z.object({
3302
+ code: z.string().length(6).regex(/^\d{6}$/)
3303
+ });
1340
3304
  function EmailOTPForm({
1341
3305
  redirectUrl,
1342
3306
  onSuccess,
@@ -3659,6 +5623,6 @@ function UserMenu({ showOrganization = false, menuItems, className }) {
3659
5623
  ] }) });
3660
5624
  }
3661
5625
 
3662
- export { AUTH_ERROR_CODES, AccountSwitcher, AuthContext, AuthProvider, AuthView, DeviceList, EmailOTPForm, EmailVerificationForm, ForgotPasswordForm, LoadingBoundary, LocalizationContext, LoginActivityFeed, LoginForm, MagicLinkForm, OAuthButton, OrganizationMenu, PhoneLoginForm, RISK_COLORS, RecoverAccountForm, ResetPasswordForm, SessionGuard, SignupForm, TwoFactorForm, UserMenu, authLocalization, authViewPaths, backupCodeSchema, credentialLoginSchema, emailOtpCodeSchema, emailOtpEmailSchema, forgotPasswordSchema, getInitials, getLocalizedError, getRiskLevel, isEmail, magicLinkSchema, maskEmail, otpVerifySchema, phoneLoginSchema, phoneNumberSchema, otpVerifySchema2 as phoneOtpVerifySchema, phonePasswordSchema, recoverCodeSchema, recoverEmailSchema, recoverNewPasswordSchema, recoverPhoneSchema, resetPasswordSchema, signupSchema, toAuthError, totpSchema, useApiKeys, useAuth, useAuthContext, useAuthLocalization, useDevices, useLoginActivity, useOrganizations, usePhone, useSession, useSessions, useUser };
5626
+ export { AUTH_ERROR_CODES, AccountSettingsCards, AccountSwitcher, AccountView, ApiKeysCard, AuthContext, AuthProvider, AuthView, ChangeEmailCard, ChangePasswordCard, DeleteAccountCard, DeviceList, EmailOTPForm, EmailVerificationForm, ForgotPasswordForm, LoadingBoundary, LocalizationContext, LoginActivityFeed, LoginForm, MagicLinkForm, OAuthButton, OrganizationMenu, PasskeysCard, PhoneLoginForm, ProvidersCard, RISK_COLORS, RecoverAccountForm, ResetPasswordForm, SecuritySettingsCards, SessionGuard, SessionsCard, SignupForm, TwoFactorCard, TwoFactorForm, UpdateAvatarCard, UpdateNameCard, UpdateUsernameCard, UserMenu, accountViewSections, authLocalization, authViewPaths, backupCodeSchema, changeEmailSchema, changePasswordSchema, credentialLoginSchema, emailOtpCodeSchema, emailOtpEmailSchema, forgotPasswordSchema, getInitials, getLocalizedError, getRiskLevel, isEmail, magicLinkSchema, maskEmail, otpVerifySchema, phoneLoginSchema, phoneNumberSchema, otpVerifySchema2 as phoneOtpVerifySchema, phonePasswordSchema, recoverCodeSchema, recoverEmailSchema, recoverNewPasswordSchema, recoverPhoneSchema, resetPasswordSchema, signupSchema, toAuthError, totpSchema, updateNameSchema, updateUsernameSchema, useApiKeys, useAuth, useAuthContext, useAuthLocalization, useDevices, useLoginActivity, useOrganizations, usePasskeys, usePhone, useSession, useSessions, useUser };
3663
5627
  //# sourceMappingURL=index.js.map
3664
5628
  //# sourceMappingURL=index.js.map