@tinyrack/issuary-server 0.24.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/public/assets/2fa-BlpDkf-r.js +2 -0
- package/public/assets/{2fa-6btYq1AL.js.map → 2fa-BlpDkf-r.js.map} +1 -1
- package/public/assets/{2fa-Dxh4Kxy-.js → 2fa-DIpGlpoI.js} +2 -2
- package/public/assets/{2fa-Dxh4Kxy-.js.map → 2fa-DIpGlpoI.js.map} +1 -1
- package/public/assets/2fa-tzTSqiOS.js +2 -0
- package/public/assets/{2fa-UcpXAI9J.js.map → 2fa-tzTSqiOS.js.map} +1 -1
- package/public/assets/{2fa-D3ieXkuh.js → 2fa-waFAdIa0.js} +2 -2
- package/public/assets/{2fa-D3ieXkuh.js.map → 2fa-waFAdIa0.js.map} +1 -1
- package/public/assets/DialogHandle-ChG5Sj7T.js +2 -0
- package/public/assets/{DialogHandle-tiMUnjZQ.js.map → DialogHandle-ChG5Sj7T.js.map} +1 -1
- package/public/assets/{admin-B8RQbf4_.js → admin-Ii29X08B.js} +2 -2
- package/public/assets/{admin-B8RQbf4_.js.map → admin-Ii29X08B.js.map} +1 -1
- package/public/assets/{admin-users-Dz1QNdy3.js → admin-users-d7tT-cjc.js} +2 -2
- package/public/assets/{admin-users-Dz1QNdy3.js.map → admin-users-d7tT-cjc.js.map} +1 -1
- package/public/assets/alert-leqyvHOp.js.map +1 -1
- package/public/assets/{auth-choice-row-DWzfcZQY.js → auth-choice-row-DUvFyo7b.js} +2 -2
- package/public/assets/{auth-choice-row-DWzfcZQY.js.map → auth-choice-row-DUvFyo7b.js.map} +1 -1
- package/public/assets/{auth-field-DsLGh1U-.js → auth-field-BSlAWkbx.js} +2 -2
- package/public/assets/{auth-field-DsLGh1U-.js.map → auth-field-BSlAWkbx.js.map} +1 -1
- package/public/assets/auth-layout-CcJy6dVY.js +2 -0
- package/public/assets/auth-layout-CcJy6dVY.js.map +1 -0
- package/public/assets/{auth-page-header-COvqWINf.js → auth-page-header-ZLhtQ7Et.js} +2 -2
- package/public/assets/{auth-page-header-COvqWINf.js.map → auth-page-header-ZLhtQ7Et.js.map} +1 -1
- package/public/assets/{authorization-context-banner-DVx1TsPY.js → authorization-context-banner-DfkjwE93.js} +2 -2
- package/public/assets/{authorization-context-banner-DVx1TsPY.js.map → authorization-context-banner-DfkjwE93.js.map} +1 -1
- package/public/assets/{avatar-BCLugfaZ.js → avatar-D-70MKp8.js} +2 -2
- package/public/assets/{avatar-BCLugfaZ.js.map → avatar-D-70MKp8.js.map} +1 -1
- package/public/assets/badge-CsOfUji0.js.map +1 -1
- package/public/assets/checkbox-C3UmXoKz.js +2 -0
- package/public/assets/{checkbox-CFVDS3zJ.js.map → checkbox-C3UmXoKz.js.map} +1 -1
- package/public/assets/config-DTQCUlfZ.js.map +1 -1
- package/public/assets/{consent-ChiWc7Ql.js → consent-BJgO9pEV.js} +2 -2
- package/public/assets/{consent-ChiWc7Ql.js.map → consent-BJgO9pEV.js.map} +1 -1
- package/public/assets/{consent-CKOohdYI.js → consent-D_yGE8TR.js} +2 -2
- package/public/assets/{consent-CKOohdYI.js.map → consent-D_yGE8TR.js.map} +1 -1
- package/public/assets/createLucideIcon-BPpAIXR_.js.map +1 -1
- package/public/assets/{email-Crjg1dcr.js → email-DAa8JGoC.js} +2 -2
- package/public/assets/{email-Crjg1dcr.js.map → email-DAa8JGoC.js.map} +1 -1
- package/public/assets/email-tzTSqiOS.js +2 -0
- package/public/assets/{email-UcpXAI9J.js.map → email-tzTSqiOS.js.map} +1 -1
- package/public/assets/{error-VfMv226c.js → error-Dj4QQ8vW.js} +2 -2
- package/public/assets/{error-VfMv226c.js.map → error-Dj4QQ8vW.js.map} +1 -1
- package/public/assets/field-CEs4ugVJ.js +3 -0
- package/public/assets/{field-89zKYDVL.js.map → field-CEs4ugVJ.js.map} +1 -1
- package/public/assets/{forgot-Dr88fVMq.js → forgot-CYMAWPVu.js} +2 -2
- package/public/assets/{forgot-Dr88fVMq.js.map → forgot-CYMAWPVu.js.map} +1 -1
- package/public/assets/forgot-tzTSqiOS.js +2 -0
- package/public/assets/{forgot-UcpXAI9J.js.map → forgot-tzTSqiOS.js.map} +1 -1
- package/public/assets/{index-CZ0rWM7l.js → index-H6CJKjiN.js} +3 -3
- package/public/assets/{index-CZ0rWM7l.js.map → index-H6CJKjiN.js.map} +1 -1
- package/public/assets/{index-CDD8owGK.css → index-kD9gQA-w.css} +1 -1
- package/public/assets/{initial-avatar-ocwNWL38.js → initial-avatar-DjSqgENY.js} +2 -2
- package/public/assets/{initial-avatar-ocwNWL38.js.map → initial-avatar-DjSqgENY.js.map} +1 -1
- package/public/assets/{input-GPigtjgi.js → input-BHPUSlfp.js} +2 -2
- package/public/assets/{input-GPigtjgi.js.map → input-BHPUSlfp.js.map} +1 -1
- package/public/assets/link-C-WdTj3F.js.map +1 -1
- package/public/assets/{login-CQeTLmIu.js → login-CBinfk4L.js} +2 -2
- package/public/assets/{login-CQeTLmIu.js.map → login-CBinfk4L.js.map} +1 -1
- package/public/assets/login-tzTSqiOS.js +2 -0
- package/public/assets/{login-UcpXAI9J.js.map → login-tzTSqiOS.js.map} +1 -1
- package/public/assets/{modal-BV3gpI-p.js → modal-CwoBUXp3.js} +2 -2
- package/public/assets/{modal-BV3gpI-p.js.map → modal-CwoBUXp3.js.map} +1 -1
- package/public/assets/{passkey-Bf7n49Z7.js → passkey-Jw2CRzJp.js} +2 -2
- package/public/assets/{passkey-Bf7n49Z7.js.map → passkey-Jw2CRzJp.js.map} +1 -1
- package/public/assets/{passkey-CT3G7pCL.js → passkey-V_tav53p.js} +2 -2
- package/public/assets/{passkey-CT3G7pCL.js.map → passkey-V_tav53p.js.map} +1 -1
- package/public/assets/passkey-tzTSqiOS.js +2 -0
- package/public/assets/{passkey-UcpXAI9J.js.map → passkey-tzTSqiOS.js.map} +1 -1
- package/public/assets/{password-BKI5GTRs.js → password-6QOqTV_s.js} +2 -2
- package/public/assets/{password-BKI5GTRs.js.map → password-6QOqTV_s.js.map} +1 -1
- package/public/assets/{password-strength-DTWF07Px.js → password-strength-DtGMHOD-.js} +2 -2
- package/public/assets/{password-strength-DTWF07Px.js.map → password-strength-DtGMHOD-.js.map} +1 -1
- package/public/assets/password-tzTSqiOS.js +2 -0
- package/public/assets/{password-UcpXAI9J.js.map → password-tzTSqiOS.js.map} +1 -1
- package/public/assets/pin-input-DdhzDNy9.js +2 -0
- package/public/assets/{pin-input-BGOLfkwL.js.map → pin-input-DdhzDNy9.js.map} +1 -1
- package/public/assets/{profile-DSTMtoUf.js → profile-DVjIWt8X.js} +2 -2
- package/public/assets/{profile-DSTMtoUf.js.map → profile-DVjIWt8X.js.map} +1 -1
- package/public/assets/profile-SHr6F-7b.js +2 -0
- package/public/assets/profile-SHr6F-7b.js.map +1 -0
- package/public/assets/{recovery-DbgpqG6f.js → recovery-CvNl-yt9.js} +2 -2
- package/public/assets/{recovery-DbgpqG6f.js.map → recovery-CvNl-yt9.js.map} +1 -1
- package/public/assets/{register-B2Lixapy.js → register-SFXa5EeL.js} +2 -2
- package/public/assets/{register-B2Lixapy.js.map → register-SFXa5EeL.js.map} +1 -1
- package/public/assets/register-tzTSqiOS.js +2 -0
- package/public/assets/{register-UcpXAI9J.js.map → register-tzTSqiOS.js.map} +1 -1
- package/public/assets/{reset-C-Qjl2Qy.js → reset-l-pLzjwe.js} +2 -2
- package/public/assets/{reset-C-Qjl2Qy.js.map → reset-l-pLzjwe.js.map} +1 -1
- package/public/assets/reset-tzTSqiOS.js +2 -0
- package/public/assets/{reset-UcpXAI9J.js.map → reset-tzTSqiOS.js.map} +1 -1
- package/public/assets/{select-BKx4bF4A.js → select-D2H87qXJ.js} +2 -2
- package/public/assets/{select-BKx4bF4A.js.map → select-D2H87qXJ.js.map} +1 -1
- package/public/assets/select-tzTSqiOS.js +2 -0
- package/public/assets/{select-UcpXAI9J.js.map → select-tzTSqiOS.js.map} +1 -1
- package/public/assets/separator-B0Bvh1he.js.map +1 -1
- package/public/assets/shell-header-bar-BPgCjFPv.js +2 -0
- package/public/assets/shell-header-bar-BPgCjFPv.js.map +1 -0
- package/public/assets/{terms-Vgpg5IZK.js → terms-D5rfQhog.js} +2 -2
- package/public/assets/{terms-Vgpg5IZK.js.map → terms-D5rfQhog.js.map} +1 -1
- package/public/assets/{terms-DSTMtoUf.js → terms-DVjIWt8X.js} +2 -2
- package/public/assets/{terms-DSTMtoUf.js.map → terms-DVjIWt8X.js.map} +1 -1
- package/public/assets/{terms-checkbox-list-DqbXU4qw.js → terms-checkbox-list-BKJcS2pJ.js} +2 -2
- package/public/assets/{terms-checkbox-list-DqbXU4qw.js.map → terms-checkbox-list-BKJcS2pJ.js.map} +1 -1
- package/public/assets/{totp-g0-s50HZ.js → totp-BbQi-gsF.js} +2 -2
- package/public/assets/{totp-g0-s50HZ.js.map → totp-BbQi-gsF.js.map} +1 -1
- package/public/assets/{totp-Cjns-PoB.js → totp-wY0Zji8E.js} +2 -2
- package/public/assets/{totp-Cjns-PoB.js.map → totp-wY0Zji8E.js.map} +1 -1
- package/public/assets/{use-totp-setup-CtTaklH-.js → use-totp-setup-OH_GTMxA.js} +3 -3
- package/public/assets/{use-totp-setup-CtTaklH-.js.map → use-totp-setup-OH_GTMxA.js.map} +1 -1
- package/public/assets/{useAriaLabelledBy-7uIPXK-0.js → useAriaLabelledBy-9o83pv9X.js} +2 -2
- package/public/assets/{useAriaLabelledBy-7uIPXK-0.js.map → useAriaLabelledBy-9o83pv9X.js.map} +1 -1
- package/public/assets/useQuery-CI5Ukuae.js +2 -0
- package/public/assets/{useQuery-pOdkMV4B.js.map → useQuery-CI5Ukuae.js.map} +1 -1
- package/public/assets/{users-B1hvVo3B.js → users-DqCnblEo.js} +2 -2
- package/public/assets/{users-B1hvVo3B.js.map → users-DqCnblEo.js.map} +1 -1
- package/public/index.html +2 -2
- package/public/assets/2fa-6btYq1AL.js +0 -2
- package/public/assets/2fa-UcpXAI9J.js +0 -2
- package/public/assets/DialogHandle-tiMUnjZQ.js +0 -2
- package/public/assets/auth-layout-DGTylGtX.js +0 -2
- package/public/assets/auth-layout-DGTylGtX.js.map +0 -1
- package/public/assets/checkbox-CFVDS3zJ.js +0 -2
- package/public/assets/email-UcpXAI9J.js +0 -2
- package/public/assets/field-89zKYDVL.js +0 -3
- package/public/assets/forgot-UcpXAI9J.js +0 -2
- package/public/assets/login-UcpXAI9J.js +0 -2
- package/public/assets/passkey-UcpXAI9J.js +0 -2
- package/public/assets/password-UcpXAI9J.js +0 -2
- package/public/assets/pin-input-BGOLfkwL.js +0 -2
- package/public/assets/profile-RpDmbWNA.js +0 -2
- package/public/assets/profile-RpDmbWNA.js.map +0 -1
- package/public/assets/register-UcpXAI9J.js +0 -2
- package/public/assets/reset-UcpXAI9J.js +0 -2
- package/public/assets/select-UcpXAI9J.js +0 -2
- package/public/assets/shell-header-bar-CgyNXbPS.js +0 -2
- package/public/assets/shell-header-bar-CgyNXbPS.js.map +0 -1
- package/public/assets/useQuery-pOdkMV4B.js +0 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profile-SHr6F-7b.js","names":["__iconNode","__iconNode","__iconNode","__iconNode","useMutation","useQueryClient","useSuspenseQuery","useRouter","TRButton","TRCard","TRText","CircleAlertIcon","LogOutIcon","useState","useTranslation","AuthPageHeader","DangerZoneSection","LinkedAccountsSection","PasskeySection","PasswordSection","TotpSection","UnlinkOAuthModal","UserInfoSection","Alert","InitialAvatar","AppLayout","ChangePasswordModal","DeleteAccountModal","DisableTotpModal","ManagePasskeysModal","RegenerateTotpRecoveryCodesModal","RemovePasswordModal","SetPasswordModal","SetupPasskeyModal","SetupTotpModal","tick","appConfigQueryOptions","logoutMutationOptions","getOAuthAuthorizeUrl","oauthAccountsQueryOptions","unlinkOAuthMutationOptions","getSessionQueryOptions","PasswordModalType","TotpModalType","PasskeyModalType","OAUTH_ERROR_I18N_MAP","Record","access_denied","temporarily_unavailable","server_error","Route","Profile","t","router","queryClient","search","useSearch","oauthError","oauth_error","oauthErrorMessage","undefined","unlinkingProvider","setUnlinkingProvider","unlinkModal","setUnlinkModal","id","name","passwordModal","setPasswordModal","totpModal","setTotpModal","passkeyModal","setPasskeyModal","showDeleteModal","setShowDeleteModal","data","session","oauthAccountsData","appConfig","logoutMutation","onSuccess","setQueryData","queryKey","user","navigate","to","onSettled","invalidateQueries","unlinkMutation","handleUnlinkConfirm","mutateAsync","availableProviders","available_providers","hasLinkedOAuth","some","p","linked","isConfigManaged","managed_by","passwordAuthMethod","auth","password","passkeyAuthMethod","passkey","totpEnabled","totp","enabled","passkeyEnabled","accountDeletionEnabled","account_deletion","retentionPeriod","retention","retentionDays","match","value","parseInt","unit","showPasswordSection","showTotpSection","showPasskeySection","showLinkedAccounts","length","hasSecurityOptions","isPending","mutate","email","totp_recovery_codes_missing","has_password","totp_registered","passkey_count","provider","display_name","component"],"sources":["../../../../node_modules/.pnpm/lucide-react@1.25.0_react@19.2.7/node_modules/lucide-react/dist/esm/icons/cloud.mjs","../../../../node_modules/.pnpm/lucide-react@1.25.0_react@19.2.7/node_modules/lucide-react/dist/esm/icons/link-2-off.mjs","../../../../node_modules/.pnpm/lucide-react@1.25.0_react@19.2.7/node_modules/lucide-react/dist/esm/icons/pencil.mjs","../../../../node_modules/.pnpm/lucide-react@1.25.0_react@19.2.7/node_modules/lucide-react/dist/esm/icons/settings.mjs","../../../../node_modules/.pnpm/lucide-react@1.25.0_react@19.2.7/node_modules/lucide-react/dist/esm/icons/smartphone.mjs","../../../frontend/src/components/profile/danger-zone-section.tsx","../../../frontend/src/components/profile/security-row.tsx","../../../frontend/src/components/profile/linked-accounts-section.tsx","../../../frontend/src/components/profile/passkey-section.tsx","../../../frontend/src/components/profile/password-section.tsx","../../../frontend/src/components/profile/totp-section.tsx","../../../frontend/src/components/ui/alert-banner.tsx","../../../frontend/src/components/profile/unlink-oauth-modal.tsx","../../../frontend/src/components/profile/user-info-section.tsx","../../../frontend/src/features/layout/app-layout.tsx","../../../frontend/src/queries/password.ts","../../../frontend/src/features/profile/change-password-modal.tsx","../../../frontend/src/queries/account.ts","../../../frontend/src/features/profile/delete-account-modal.tsx","../../../frontend/src/features/profile/disable-totp-modal.tsx","../../../frontend/src/features/profile/manage-passkeys-modal.tsx","../../../frontend/src/features/profile/regenerate-totp-recovery-codes-modal.tsx","../../../frontend/src/features/profile/remove-password-modal.tsx","../../../frontend/src/features/profile/set-password-modal.tsx","../../../frontend/src/features/profile/setup-passkey-modal.tsx","../../../frontend/src/features/profile/setup-totp-modal.tsx","../../../frontend/src/routes/profile/index.tsx?tsr-split=component"],"sourcesContent":["/**\n * @license lucide-react v1.25.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createLucideIcon from '../createLucideIcon.mjs';\n\nconst __iconNode = [\n [\"path\", { d: \"M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z\", key: \"p7xjir\" }]\n];\nconst Cloud = createLucideIcon(\"cloud\", __iconNode);\n\nexport { __iconNode, Cloud as default };\n//# sourceMappingURL=cloud.mjs.map\n","/**\n * @license lucide-react v1.25.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createLucideIcon from '../createLucideIcon.mjs';\n\nconst __iconNode = [\n [\"path\", { d: \"M9 17H7A5 5 0 0 1 7 7\", key: \"10o201\" }],\n [\"path\", { d: \"M15 7h2a5 5 0 0 1 4 8\", key: \"1d3206\" }],\n [\"line\", { x1: \"8\", x2: \"12\", y1: \"12\", y2: \"12\", key: \"rvw6j4\" }],\n [\"line\", { x1: \"2\", x2: \"22\", y1: \"2\", y2: \"22\", key: \"a6p6uj\" }]\n];\nconst Link2Off = createLucideIcon(\"link-2-off\", __iconNode);\n\nexport { __iconNode, Link2Off as default };\n//# sourceMappingURL=link-2-off.mjs.map\n","/**\n * @license lucide-react v1.25.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createLucideIcon from '../createLucideIcon.mjs';\n\nconst __iconNode = [\n [\n \"path\",\n {\n d: \"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z\",\n key: \"1a8usu\"\n }\n ],\n [\"path\", { d: \"m15 5 4 4\", key: \"1mk7zo\" }]\n];\nconst Pencil = createLucideIcon(\"pencil\", __iconNode);\n\nexport { __iconNode, Pencil as default };\n//# sourceMappingURL=pencil.mjs.map\n","/**\n * @license lucide-react v1.25.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createLucideIcon from '../createLucideIcon.mjs';\n\nconst __iconNode = [\n [\n \"path\",\n {\n d: \"M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915\",\n key: \"1i5ecw\"\n }\n ],\n [\"circle\", { cx: \"12\", cy: \"12\", r: \"3\", key: \"1v7zrd\" }]\n];\nconst Settings = createLucideIcon(\"settings\", __iconNode);\n\nexport { __iconNode, Settings as default };\n//# sourceMappingURL=settings.mjs.map\n","/**\n * @license lucide-react v1.25.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createLucideIcon from '../createLucideIcon.mjs';\n\nconst __iconNode = [\n [\"rect\", { width: \"14\", height: \"20\", x: \"5\", y: \"2\", rx: \"2\", ry: \"2\", key: \"1yt0o3\" }],\n [\"path\", { d: \"M12 18h.01\", key: \"mhygvu\" }]\n];\nconst Smartphone = createLucideIcon(\"smartphone\", __iconNode);\n\nexport { __iconNode, Smartphone as default };\n//# sourceMappingURL=smartphone.mjs.map\n","import { TRButton } from '@tinyrack/ui/components/button';\nimport { TRCard } from '@tinyrack/ui/components/card';\nimport { TRText } from '@tinyrack/ui/components/text';\nimport { Trash2Icon, TriangleAlertIcon } from 'lucide-react';\nimport { useTranslation } from 'react-i18next';\n\ninterface DangerZoneSectionProps {\n isConfigManaged: boolean;\n isDeletionEnabled: boolean;\n onDeleteClick: () => void;\n}\n\nexport function DangerZoneSection({\n isConfigManaged,\n isDeletionEnabled,\n onDeleteClick,\n}: DangerZoneSectionProps) {\n const { t } = useTranslation();\n\n if (!isDeletionEnabled) {\n return null;\n }\n\n return (\n <TRCard.Root\n className=\"border-tinyrack-danger-border bg-tinyrack-danger-surface\"\n variant=\"outlined\"\n >\n <TRCard.Header className=\"border-tinyrack-danger-border border-b-tinyrack-default px-tinyrack-lg py-tinyrack-md\">\n <div className=\"flex items-center gap-tinyrack-sm\">\n <TriangleAlertIcon\n aria-hidden\n className=\"size-tinyrack-lg text-tinyrack-danger-foreground\"\n />\n <TRCard.Title className=\"text-tinyrack-danger-foreground\">\n {t('profile.dangerZone.title')}\n </TRCard.Title>\n </div>\n <TRCard.Description>\n {t('profile.dangerZone.description')}\n </TRCard.Description>\n </TRCard.Header>\n <TRCard.Content className=\"px-tinyrack-lg py-tinyrack-md\">\n <div className=\"flex items-center justify-between gap-tinyrack-lg\">\n <div className=\"flex min-w-0 flex-1 flex-col gap-tinyrack-3xs\">\n <TRText as=\"p\" variant=\"bodySm\" weight=\"medium\">\n {t('profile.deleteAccount.title')}\n </TRText>\n <TRText as=\"p\" color=\"muted\" variant=\"caption\">\n {isConfigManaged\n ? t('profile.deleteAccount.configManaged')\n : t('profile.deleteAccount.description')}\n </TRText>\n </div>\n <TRButton\n appearance=\"outline\"\n data-testid=\"profile-delete-account\"\n disabled={isConfigManaged}\n intent=\"danger\"\n onClick={onDeleteClick}\n type=\"button\"\n uiSize=\"sm\"\n >\n <Trash2Icon aria-hidden className=\"size-tinyrack-lg\" />\n {t('profile.deleteAccount.button')}\n </TRButton>\n </div>\n </TRCard.Content>\n </TRCard.Root>\n );\n}\n","import { TRText } from '@tinyrack/ui/components/text';\nimport type { LucideIcon } from 'lucide-react';\nimport { isValidElement, type ReactNode } from 'react';\n\ntype SecurityRowProps = {\n /**\n * A lucide glyph, which gets tinted with the row's state, or a ready-made\n * node for marks that carry their own colour — a provider's brand logo.\n */\n icon: LucideIcon | ReactNode;\n /** Tints the icon tile. \"Configured\" rather than \"enabled\" — a linked OAuth\n * account and a set password are both active states. */\n active: boolean;\n title: string;\n status: string;\n /** Secondary line under the status, e.g. a config-managed caveat. */\n note?: ReactNode;\n /** Trailing controls. */\n actions?: ReactNode;\n};\n\n/**\n * One credential the account has, or does not.\n *\n * Password, TOTP, passkeys, and each linked provider were four copies of the\n * same row — a tinted icon tile, a title, a status line, a trailing action\n * cluster — which is how they drifted into three different paddings.\n */\nexport function SecurityRow({\n icon,\n active,\n title,\n status,\n note,\n actions,\n}: SecurityRowProps) {\n /*\n Discriminated by `isValidElement`, not `typeof … === 'function'`: lucide\n icons are `forwardRef` objects, so a typeof check reads them as data and\n React then throws on rendering `{$$typeof, render}` as a child. An\n already-rendered mark is an element; a component is not.\n */\n let mark: ReactNode;\n if (isValidElement(icon)) {\n mark = icon;\n } else {\n const Icon = icon as LucideIcon;\n mark = (\n <Icon\n aria-hidden\n className={`size-tinyrack-lg ${\n active\n ? 'text-tinyrack-success-foreground'\n : 'text-tinyrack-text-muted'\n }`}\n />\n );\n }\n\n return (\n <div className=\"flex items-center justify-between gap-tinyrack-lg p-tinyrack-lg\">\n <div className=\"flex min-w-0 flex-1 items-center gap-tinyrack-md\">\n <div\n className={`flex size-9 shrink-0 items-center justify-center rounded-tinyrack-md ${\n active ? 'bg-tinyrack-success-surface' : 'bg-tinyrack-surface-muted'\n }`}\n >\n {mark}\n </div>\n <div className=\"flex min-w-0 flex-1 flex-col gap-tinyrack-3xs\">\n <TRText as=\"div\" variant=\"bodySm\" weight=\"medium\">\n {title}\n </TRText>\n <TRText as=\"div\" color=\"muted\" variant=\"caption\">\n {status}\n </TRText>\n {note}\n </div>\n </div>\n {actions && (\n <div className=\"flex shrink-0 flex-wrap justify-end gap-tinyrack-xs\">\n {actions}\n </div>\n )}\n </div>\n );\n}\n","import { TRButton } from '@tinyrack/ui/components/button';\nimport { TRCard } from '@tinyrack/ui/components/card';\nimport { TRLinkButton } from '@tinyrack/ui/components/link-button';\nimport { useTranslation } from 'react-i18next';\nimport { ProviderMark } from '#frontend/components/auth/provider-logos.tsx';\nimport { SecurityRow } from '#frontend/components/profile/security-row.tsx';\n\ninterface OAuthProvider {\n id: string;\n display_name: string;\n icon_url?: string;\n linked: boolean;\n}\n\ninterface LinkedAccountsSectionProps {\n providers: OAuthProvider[];\n unlinkingProvider: string | null;\n getAuthorizeUrl: (\n providerId: string,\n mode?: 'login' | 'register' | 'link',\n returnUrl?: string,\n ) => string;\n onUnlinkRequest: (provider: OAuthProvider) => void;\n}\n\nexport function LinkedAccountsSection({\n providers,\n unlinkingProvider,\n getAuthorizeUrl,\n onUnlinkRequest,\n}: LinkedAccountsSectionProps) {\n const { t } = useTranslation();\n\n if (providers.length === 0) {\n return null;\n }\n\n return (\n <TRCard.Root variant=\"outlined\">\n <TRCard.Header className=\"border-tinyrack-border border-b-tinyrack-default px-tinyrack-lg py-tinyrack-md\">\n <TRCard.Title>{t('profile.linkedAccounts.title')}</TRCard.Title>\n <TRCard.Description>\n {t('profile.linkedAccounts.description')}\n </TRCard.Description>\n </TRCard.Header>\n <TRCard.Content className=\"divide-y divide-tinyrack-border p-0\">\n {providers.map((provider) => (\n <SecurityRow\n actions={\n provider.linked ? (\n <TRButton\n appearance=\"ghost\"\n intent=\"danger\"\n loading={unlinkingProvider === provider.id}\n loadingLabel={t('profile.linkedAccounts.unlinking')}\n onClick={() => onUnlinkRequest(provider)}\n type=\"button\"\n uiSize=\"sm\"\n >\n {unlinkingProvider !== provider.id\n ? t('profile.linkedAccounts.unlink')\n : undefined}\n </TRButton>\n ) : (\n <TRLinkButton\n appearance=\"outline\"\n intent=\"neutral\"\n render={\n <a\n href={getAuthorizeUrl(provider.id, 'link', '/profile')}\n />\n }\n uiSize=\"sm\"\n >\n {t('profile.linkedAccounts.link')}\n </TRLinkButton>\n )\n }\n active={provider.linked}\n /*\n The real brand mark, not a generic link glyph. The sign-in screen\n shows the Google logo; showing a chain link for the same provider\n on a screen the user reaches minutes later reads as a different\n product.\n */\n icon={\n <ProviderMark\n className=\"size-tinyrack-lg\"\n iconUrl={provider.icon_url}\n providerId={provider.id}\n />\n }\n key={provider.id}\n status={\n provider.linked\n ? t('profile.linkedAccounts.connected')\n : t('profile.linkedAccounts.notConnected')\n }\n title={provider.display_name}\n />\n ))}\n </TRCard.Content>\n </TRCard.Root>\n );\n}\n","import { TRButton } from '@tinyrack/ui/components/button';\nimport { FingerprintIcon } from 'lucide-react';\nimport { useTranslation } from 'react-i18next';\nimport { SecurityRow } from '#frontend/components/profile/security-row.tsx';\n\ntype PasskeyModalType = 'setup' | 'manage' | null;\n\ninterface PasskeySectionProps {\n passkeyCount: number;\n onOpenModal: (type: PasskeyModalType) => void;\n}\n\nexport function PasskeySection({\n passkeyCount,\n onOpenModal,\n}: PasskeySectionProps) {\n const { t } = useTranslation();\n const hasPasskeys = passkeyCount > 0;\n\n return (\n <SecurityRow\n actions={\n <TRButton\n appearance=\"ghost\"\n intent=\"primary\"\n onClick={() => onOpenModal(hasPasskeys ? 'manage' : 'setup')}\n type=\"button\"\n uiSize=\"sm\"\n >\n {hasPasskeys ? t('profile.passkey.manage') : t('profile.passkey.add')}\n </TRButton>\n }\n active={hasPasskeys}\n icon={FingerprintIcon}\n status={\n hasPasskeys\n ? t('profile.passkey.status.enabled', { count: passkeyCount })\n : t('profile.passkey.status.disabled')\n }\n title={t('profile.passkey.title')}\n />\n );\n}\n","import { TRButton } from '@tinyrack/ui/components/button';\nimport { TRText } from '@tinyrack/ui/components/text';\nimport { TRTooltip } from '@tinyrack/ui/components/tooltip';\nimport { InfoIcon, KeyRoundIcon, SettingsIcon } from 'lucide-react';\nimport { useTranslation } from 'react-i18next';\nimport { SecurityRow } from '#frontend/components/profile/security-row.tsx';\n\ntype PasswordModalType = 'set' | 'change' | 'remove' | null;\n\ninterface PasswordSectionProps {\n hasPassword: boolean;\n hasLinkedOAuth: boolean;\n isConfigManaged: boolean;\n hasSecondFactorOnly: boolean;\n onOpenModal: (type: PasswordModalType) => void;\n}\n\nexport function PasswordSection({\n hasPassword,\n hasLinkedOAuth,\n isConfigManaged,\n hasSecondFactorOnly,\n onOpenModal,\n}: PasswordSectionProps) {\n const { t } = useTranslation();\n\n return (\n <SecurityRow\n actions={\n isConfigManaged ? undefined : hasPassword ? (\n <>\n <TRButton\n appearance=\"ghost\"\n data-testid=\"profile-password-change\"\n intent=\"primary\"\n onClick={() => onOpenModal('change')}\n type=\"button\"\n uiSize=\"sm\"\n >\n {t('profile.password.change')}\n </TRButton>\n {hasLinkedOAuth ? (\n <TRButton\n appearance=\"ghost\"\n data-testid=\"profile-password-remove\"\n intent=\"danger\"\n onClick={() => onOpenModal('remove')}\n type=\"button\"\n uiSize=\"sm\"\n >\n {t('profile.password.remove')}\n </TRButton>\n ) : null}\n {!hasLinkedOAuth && hasSecondFactorOnly && (\n <TRTooltip.Root>\n <TRTooltip.Trigger\n render={\n <TRButton\n appearance=\"ghost\"\n disabled\n type=\"button\"\n uiSize=\"sm\"\n >\n {t('profile.password.remove')}\n </TRButton>\n }\n />\n <TRTooltip.Portal>\n <TRTooltip.Positioner>\n <TRTooltip.Popup className=\"max-w-tinyrack-measure-lg rounded-tinyrack-md border border-tinyrack-border bg-tinyrack-surface p-tinyrack-md text-tinyrack-text text-tinyrack-xs shadow-tinyrack-overlay\">\n <div className=\"flex items-start gap-tinyrack-sm\">\n <InfoIcon\n aria-hidden\n className=\"size-tinyrack-lg shrink-0 text-tinyrack-warning-foreground\"\n />\n <div>{t('profile.password.removeDisabledReason')}</div>\n </div>\n </TRTooltip.Popup>\n </TRTooltip.Positioner>\n </TRTooltip.Portal>\n </TRTooltip.Root>\n )}\n </>\n ) : (\n <TRButton\n appearance=\"ghost\"\n data-testid=\"profile-password-set\"\n intent=\"primary\"\n onClick={() => onOpenModal('set')}\n type=\"button\"\n uiSize=\"sm\"\n >\n {t('profile.password.set')}\n </TRButton>\n )\n }\n active={hasPassword}\n icon={KeyRoundIcon}\n note={\n isConfigManaged ? (\n <TRText\n as=\"div\"\n className=\"flex items-center gap-tinyrack-xs\"\n color=\"muted\"\n variant=\"caption\"\n >\n <SettingsIcon aria-hidden className=\"size-tinyrack-md shrink-0\" />\n <span>{t('profile.password.configManaged')}</span>\n </TRText>\n ) : undefined\n }\n status={\n hasPassword\n ? t('profile.password.status.set')\n : t('profile.password.status.notSet')\n }\n title={t('profile.password.title')}\n />\n );\n}\n","import { TRButton } from '@tinyrack/ui/components/button';\nimport { ShieldCheckIcon } from 'lucide-react';\nimport { useTranslation } from 'react-i18next';\nimport { SecurityRow } from '#frontend/components/profile/security-row.tsx';\n\ntype TotpModalType = 'setup' | 'disable' | 'regenerate' | null;\n\ninterface TotpSectionProps {\n totpEnabled: boolean;\n recoveryCodesMissing: boolean;\n onOpenModal: (type: TotpModalType) => void;\n}\n\nexport function TotpSection({\n totpEnabled,\n recoveryCodesMissing,\n onOpenModal,\n}: TotpSectionProps) {\n const { t } = useTranslation();\n\n return (\n <SecurityRow\n actions={\n totpEnabled ? (\n <>\n <TRButton\n appearance=\"ghost\"\n data-testid=\"profile-totp-regenerate\"\n intent=\"primary\"\n onClick={() => onOpenModal('regenerate')}\n type=\"button\"\n uiSize=\"sm\"\n >\n {t('profile.totp.regenerate')}\n </TRButton>\n <TRButton\n appearance=\"ghost\"\n data-testid=\"profile-totp-disable\"\n intent=\"danger\"\n onClick={() => onOpenModal('disable')}\n type=\"button\"\n uiSize=\"sm\"\n >\n {t('profile.totp.disable')}\n </TRButton>\n </>\n ) : (\n <TRButton\n appearance=\"ghost\"\n data-testid=\"profile-totp-enable\"\n intent=\"primary\"\n onClick={() => onOpenModal('setup')}\n type=\"button\"\n uiSize=\"sm\"\n >\n {t('profile.totp.enable')}\n </TRButton>\n )\n }\n active={totpEnabled}\n icon={ShieldCheckIcon}\n status={\n totpEnabled\n ? recoveryCodesMissing\n ? t('profile.totp.status.recoveryCodesMissing')\n : t('profile.totp.status.enabled')\n : t('profile.totp.status.disabled')\n }\n title={t('profile.totp.title')}\n />\n );\n}\n","import { CircleAlertIcon, InfoIcon, TriangleAlertIcon } from 'lucide-react';\nimport type { ReactNode } from 'react';\nimport { Alert } from '#frontend/components/ui/alert.tsx';\n\ntype AlertBannerVariant = 'error' | 'warning' | 'info';\n\ninterface AlertBannerProps {\n variant: AlertBannerVariant;\n children: ReactNode;\n}\n\nconst variantIcons: Record<AlertBannerVariant, typeof TriangleAlertIcon> = {\n error: CircleAlertIcon,\n warning: TriangleAlertIcon,\n info: InfoIcon,\n};\n\n/**\n * Status banner built on the design system's {@link Alert} (which wraps\n * `TRAlert`). Kept as a thin alias so existing call sites and the\n * `alert-banner-*` test ids stay stable while the visuals come from\n * `@tinyrack/ui`.\n */\nexport function AlertBanner({ variant, children }: AlertBannerProps) {\n return (\n <Alert\n data-testid={`alert-banner-${variant}`}\n icon={variantIcons[variant]}\n type={variant}\n >\n {children}\n </Alert>\n );\n}\n","import { TRButton } from '@tinyrack/ui/components/button';\nimport { Link2OffIcon } from 'lucide-react';\nimport { useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { AlertBanner } from '#frontend/components/ui/alert-banner.tsx';\nimport { Modal, ModalActions } from '#frontend/components/ui/modal.tsx';\n\ninterface UnlinkOAuthModalProps {\n isOpen: boolean;\n onClose: () => void;\n providerName: string;\n isPending: boolean;\n onConfirm: () => Promise<void>;\n}\n\nexport function UnlinkOAuthModal({\n isOpen,\n onClose,\n providerName,\n isPending,\n onConfirm,\n}: UnlinkOAuthModalProps) {\n const { t } = useTranslation();\n const [error, setError] = useState<string | null>(null);\n\n const handleClose = () => {\n if (isPending) return;\n setError(null);\n onClose();\n };\n\n const handleConfirm = async () => {\n setError(null);\n try {\n await onConfirm();\n handleClose();\n } catch {\n setError(t('profile.linkedAccounts.unlinkError'));\n }\n };\n\n return (\n <Modal\n description={t('profile.linkedAccounts.unlinkModal.description', {\n provider: providerName,\n })}\n icon={Link2OffIcon}\n isOpen={isOpen}\n onClose={handleClose}\n preventClose={isPending}\n title={t('profile.linkedAccounts.unlinkModal.title')}\n variant=\"destructive\"\n >\n <div className=\"mt-tinyrack-lg flex flex-col gap-tinyrack-md\">\n <AlertBanner variant=\"warning\">\n {t('profile.linkedAccounts.unlinkModal.warning', {\n provider: providerName,\n })}\n </AlertBanner>\n\n {error && <AlertBanner variant=\"error\">{error}</AlertBanner>}\n\n <ModalActions>\n <TRButton\n appearance=\"outline\"\n data-testid=\"unlink-oauth-cancel\"\n disabled={isPending}\n intent=\"neutral\"\n onClick={handleClose}\n type=\"button\"\n uiSize=\"sm\"\n >\n {t('profile.linkedAccounts.unlinkModal.cancel')}\n </TRButton>\n <TRButton\n data-testid=\"unlink-oauth-unlink\"\n disabled={isPending}\n intent=\"danger\"\n loading={isPending}\n loadingLabel={t('profile.linkedAccounts.unlinking')}\n onClick={handleConfirm}\n type=\"button\"\n uiSize=\"sm\"\n >\n {t('profile.linkedAccounts.unlink')}\n </TRButton>\n </ModalActions>\n </div>\n </Modal>\n );\n}\n","import { TRCard } from '@tinyrack/ui/components/card';\nimport { TRText } from '@tinyrack/ui/components/text';\nimport { useTranslation } from 'react-i18next';\n\nfunction InfoRow({ label, value }: { label: string; value: string }) {\n return (\n <div className=\"flex items-center justify-between gap-tinyrack-lg px-tinyrack-lg py-tinyrack-md\">\n <TRText className=\"shrink-0\" color=\"muted\" variant=\"caption\">\n {label}\n </TRText>\n <TRText truncate variant=\"bodySm\" weight=\"medium\">\n {value}\n </TRText>\n </div>\n );\n}\n\ninterface UserInfoSectionProps {\n user: {\n sub: string;\n email: string;\n };\n}\n\nexport function UserInfoSection({ user }: UserInfoSectionProps) {\n const { t } = useTranslation();\n\n return (\n <TRCard.Root variant=\"outlined\">\n <TRCard.Header className=\"border-tinyrack-border border-b-tinyrack-default px-tinyrack-lg py-tinyrack-md\">\n <TRCard.Title>{t('profile.account.title')}</TRCard.Title>\n <TRCard.Description>\n {t('profile.account.description')}\n </TRCard.Description>\n </TRCard.Header>\n <TRCard.Content className=\"divide-y divide-tinyrack-border p-0\">\n <InfoRow label={t('profile.id.label')} value={user.sub} />\n <InfoRow label={t('profile.email.label')} value={user.email} />\n </TRCard.Content>\n </TRCard.Root>\n );\n}\n","import { Toaster } from '#frontend/components/ui/toaster.tsx';\nimport { ShellFooter } from '#frontend/features/layout/shell-footer.tsx';\nimport { ShellHeaderBar } from '#frontend/features/layout/shell-header-bar.tsx';\n\ntype AppLayoutProps = {\n children: React.ReactNode;\n /** Trailing header action — the profile's sign-out. */\n headerActions?: React.ReactNode;\n};\n\n/**\n * The shell for signed-in screens that are not the admin console.\n *\n * Shares the auth surface's vocabulary — the canvas, the header bar, the\n * entrance, the token-owned rhythm — but drops the brand panel and runs full\n * width. A settings page is a list of cards to work through, not a single task\n * to focus on, so surrendering the leading columns to a permanent brand column\n * would be paying prime space for decoration.\n *\n * Deliberately not extracted into a wrapper shared with `AuthLayout`: that one\n * is a twelve-column grid because it has a panel to place, and its content\n * stack carries a mobile-only card that would be a box inside a box here.\n * The genuinely shared piece is `ShellHeaderBar`.\n */\nexport function AppLayout({ children, headerActions }: AppLayoutProps) {\n return (\n <div className=\"flex min-h-dvh flex-col bg-tinyrack-surface\">\n {/*\n Profile hosts the TOTP recovery-codes step, which confirms a copy with\n a toast. Without a viewport here that confirmation would go nowhere on\n the one screen where losing the codes is unrecoverable.\n */}\n <Toaster />\n <ShellHeaderBar actions={headerActions} brand />\n <main className=\"flex flex-1 justify-center px-tinyrack-lg py-tinyrack-xl\">\n {/*\n The stack owns vertical rhythm so screens compose as a flat list of\n blocks instead of each one hand-rolling margins that drift apart.\n */}\n <div className=\"auth-enter flex w-full max-w-tinyrack-overlay-md flex-col gap-tinyrack-xl\">\n {children}\n </div>\n </main>\n <ShellFooter />\n </div>\n );\n}\n","import { mutationOptions } from '@tanstack/react-query';\nimport type { InferRequestType } from 'hono/client';\nimport { client, jsonOk } from '#frontend/libs/api.ts';\n\n/**\n * Set password for OAuth-only users\n */\nexport type SetPasswordParams = InferRequestType<\n (typeof client.api.user.password)['$post']\n>['json'];\n\nexport const setPasswordMutationOptions = mutationOptions({\n mutationFn: async (values: SetPasswordParams) => {\n const res = await client.api.user.password.$post({\n json: values,\n });\n return jsonOk(res);\n },\n});\n\n/**\n * Change password for users who already have a password\n */\nexport type ChangePasswordParams = InferRequestType<\n (typeof client.api.user.password)['$put']\n>['json'];\n\nexport const changePasswordMutationOptions = mutationOptions({\n mutationFn: async (values: ChangePasswordParams) => {\n const res = await client.api.user.password.$put({\n json: values,\n });\n return jsonOk(res);\n },\n});\n\n/**\n * Remove password (only if user has at least one\n * OAuth account linked)\n */\nexport type RemovePasswordParams = InferRequestType<\n (typeof client.api.user.password)['$delete']\n>['json'];\n\nexport const removePasswordMutationOptions = mutationOptions({\n mutationFn: async (values: RemovePasswordParams) => {\n const res = await client.api.user.password.$delete({\n json: values,\n });\n return jsonOk(res);\n },\n});\n","import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';\nimport {\n useMutation,\n useQueryClient,\n useSuspenseQuery,\n} from '@tanstack/react-query';\nimport { TRButton } from '@tinyrack/ui/components/button';\nimport { KeyRoundIcon } from 'lucide-react';\nimport { useMemo } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport z from 'zod';\nimport { AuthField } from '#frontend/components/auth/auth-field.tsx';\nimport { AlertBanner } from '#frontend/components/ui/alert-banner.tsx';\nimport { Modal, ModalActions } from '#frontend/components/ui/modal.tsx';\nimport { IssuaryError } from '#frontend/libs/error.ts';\nimport { appConfigQueryOptions } from '#frontend/queries/config.ts';\nimport { changePasswordMutationOptions } from '#frontend/queries/password.ts';\nimport { getSessionQueryOptions } from '#frontend/queries/session.ts';\n\ninterface ChangePasswordModalProps {\n isOpen: boolean;\n onClose: () => void;\n}\n\nexport function ChangePasswordModal({\n isOpen,\n onClose,\n}: ChangePasswordModalProps) {\n const { t } = useTranslation();\n const queryClient = useQueryClient();\n const { data: configData } = useSuspenseQuery(appConfigQueryOptions);\n const passwordPolicy = configData.auth.password.policy;\n\n const schema = useMemo(\n () =>\n z\n .object({\n currentPassword: z.string().min(1, t('validation.password.required')),\n newPassword: z\n .string()\n .min(\n passwordPolicy.min_length,\n t('validation.password.min', {\n count: passwordPolicy.min_length,\n }),\n )\n .max(\n passwordPolicy.max_length,\n t('validation.password.max', {\n count: passwordPolicy.max_length,\n }),\n ),\n confirmPassword: z.string(),\n })\n .refine((data) => data.newPassword === data.confirmPassword, {\n message: t('validation.confirmPassword.mismatch'),\n path: ['confirmPassword'],\n }),\n [passwordPolicy.max_length, passwordPolicy.min_length, t],\n );\n\n const form = useForm({\n resolver: standardSchemaResolver(schema),\n defaultValues: {\n currentPassword: '',\n newPassword: '',\n confirmPassword: '',\n },\n });\n\n const mutation = useMutation({\n ...changePasswordMutationOptions,\n onSuccess: () => {\n queryClient.invalidateQueries({\n queryKey: getSessionQueryOptions.queryKey,\n });\n handleClose();\n },\n });\n\n const handleClose = () => {\n form.reset();\n onClose();\n };\n\n const handleSubmit = form.handleSubmit(async (data) => {\n try {\n await mutation.mutateAsync({\n current_password: data.currentPassword,\n new_password: data.newPassword,\n });\n } catch (err) {\n if (\n err instanceof IssuaryError &&\n err.code === 'INVALID_CURRENT_PASSWORD'\n ) {\n form.setError('currentPassword', {\n message: t('profile.password.changeModal.invalidCurrent'),\n });\n } else {\n form.setError('root', {\n message: t('profile.password.changeModal.error'),\n });\n }\n }\n });\n\n return (\n <Modal\n description={t('profile.password.changeModal.description')}\n icon={KeyRoundIcon}\n isOpen={isOpen}\n onClose={handleClose}\n title={t('profile.password.changeModal.title')}\n >\n <form\n className=\"mt-tinyrack-lg flex flex-col gap-tinyrack-md\"\n onSubmit={handleSubmit}\n >\n <AuthField\n error={form.formState.errors.currentPassword}\n errorTestId=\"change-password-error-currentPassword\"\n id=\"current-password\"\n label={t('profile.password.changeModal.currentPassword')}\n placeholder={t(\n 'profile.password.changeModal.currentPasswordPlaceholder',\n )}\n {...form.register('currentPassword')}\n type=\"password\"\n />\n <AuthField\n error={form.formState.errors.newPassword}\n errorTestId=\"change-password-error-newPassword\"\n id=\"new-password-change\"\n label={t('profile.password.changeModal.newPassword')}\n placeholder={t('profile.password.changeModal.newPasswordPlaceholder')}\n {...form.register('newPassword')}\n type=\"password\"\n />\n <AuthField\n error={form.formState.errors.confirmPassword}\n errorTestId=\"change-password-error-confirmPassword\"\n id=\"confirm-password-change\"\n label={t('profile.password.changeModal.confirmPassword')}\n placeholder={t(\n 'profile.password.changeModal.confirmPasswordPlaceholder',\n )}\n {...form.register('confirmPassword')}\n type=\"password\"\n />\n {form.formState.errors.root && (\n <AlertBanner variant=\"error\">\n {form.formState.errors.root.message}\n </AlertBanner>\n )}\n <ModalActions>\n <TRButton\n appearance=\"outline\"\n data-testid=\"change-password-cancel\"\n disabled={mutation.isPending}\n intent=\"neutral\"\n onClick={handleClose}\n type=\"button\"\n uiSize=\"sm\"\n >\n {t('profile.password.changeModal.cancel')}\n </TRButton>\n <TRButton\n data-testid=\"change-password-submit\"\n disabled={mutation.isPending}\n intent=\"primary\"\n loading={mutation.isPending}\n loadingLabel={t('profile.password.changeModal.submitting')}\n type=\"submit\"\n uiSize=\"sm\"\n >\n {mutation.isPending\n ? undefined\n : t('profile.password.changeModal.submit')}\n </TRButton>\n </ModalActions>\n </form>\n </Modal>\n );\n}\n","import { mutationOptions } from '@tanstack/react-query';\nimport type { InferResponseType } from 'hono/client';\nimport { client, jsonOk } from '#frontend/libs/api.ts';\n\nexport type AccountDeletionResponse = InferResponseType<\n (typeof client.api.user)['$delete'],\n 200\n>;\n\nexport const deleteAccountMutationOptions = mutationOptions({\n mutationFn: async () => {\n const res = await client.api.user.$delete();\n return jsonOk(res);\n },\n});\n","import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';\nimport { useMutation, useQueryClient } from '@tanstack/react-query';\nimport { useRouter } from '@tanstack/react-router';\nimport { TRButton } from '@tinyrack/ui/components/button';\nimport { Trash2Icon } from 'lucide-react';\nimport { useMemo } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport z from 'zod';\nimport { AuthField } from '#frontend/components/auth/auth-field.tsx';\nimport { AlertBanner } from '#frontend/components/ui/alert-banner.tsx';\nimport { Modal, ModalActions } from '#frontend/components/ui/modal.tsx';\nimport { tick } from '#frontend/libs/promise.ts';\nimport { deleteAccountMutationOptions } from '#frontend/queries/account.ts';\nimport { getSessionQueryOptions } from '#frontend/queries/session.ts';\n\ninterface DeleteAccountModalProps {\n isOpen: boolean;\n onClose: () => void;\n retentionDays: number;\n}\n\nexport function DeleteAccountModal({\n isOpen,\n onClose,\n retentionDays,\n}: DeleteAccountModalProps) {\n const { t } = useTranslation();\n const router = useRouter();\n const queryClient = useQueryClient();\n\n const schema = useMemo(\n () =>\n z.object({\n confirmation: z\n .string()\n .refine(\n (val) =>\n val.toLowerCase() ===\n t('profile.deleteAccount.modal.confirmPlaceholder').toLowerCase(),\n { message: t('validation.confirmText.mismatch') },\n ),\n }),\n [t],\n );\n\n const form = useForm({\n resolver: standardSchemaResolver(schema),\n defaultValues: { confirmation: '' },\n });\n\n const mutation = useMutation({\n ...deleteAccountMutationOptions,\n onSuccess: async () => {\n queryClient.setQueryData(getSessionQueryOptions.queryKey, {\n user: null,\n });\n await tick();\n router.navigate({ to: '/login' });\n },\n });\n\n const handleClose = () => {\n if (mutation.isPending) return;\n form.reset();\n onClose();\n };\n\n const handleSubmit = form.handleSubmit(async () => {\n try {\n await mutation.mutateAsync();\n } catch {\n form.setError('root', {\n message: t('profile.deleteAccount.error'),\n });\n }\n });\n\n return (\n <Modal\n icon={Trash2Icon}\n isOpen={isOpen}\n onClose={handleClose}\n preventClose={mutation.isPending}\n title={t('profile.deleteAccount.modal.title')}\n variant=\"destructive\"\n >\n <form\n className=\"mt-tinyrack-lg flex flex-col gap-tinyrack-md\"\n onSubmit={handleSubmit}\n >\n <AlertBanner variant=\"error\">\n <div className=\"flex flex-col gap-tinyrack-xs\">\n <p>\n {t('profile.deleteAccount.modal.description', {\n days: retentionDays,\n })}\n </p>\n <p className=\"text-tinyrack-xs opacity-tinyrack-hover\">\n {t('profile.deleteAccount.modal.warning')}\n </p>\n </div>\n </AlertBanner>\n\n <AuthField\n autoComplete=\"off\"\n error={form.formState.errors.confirmation}\n errorTestId=\"delete-account-error\"\n id=\"delete-confirmation\"\n label={t('profile.deleteAccount.modal.confirmLabel')}\n placeholder={t('profile.deleteAccount.modal.confirmPlaceholder')}\n {...form.register('confirmation')}\n type=\"text\"\n />\n\n {form.formState.errors.root && (\n <AlertBanner variant=\"error\">\n {form.formState.errors.root.message}\n </AlertBanner>\n )}\n\n <ModalActions>\n <TRButton\n appearance=\"outline\"\n data-testid=\"delete-account-cancel\"\n disabled={mutation.isPending}\n intent=\"neutral\"\n onClick={handleClose}\n type=\"button\"\n uiSize=\"sm\"\n >\n {t('profile.deleteAccount.modal.cancel')}\n </TRButton>\n <TRButton\n data-testid=\"delete-account-submit\"\n disabled={mutation.isPending}\n intent=\"danger\"\n loading={mutation.isPending}\n loadingLabel={t('profile.deleteAccount.modal.confirming')}\n type=\"submit\"\n uiSize=\"sm\"\n >\n {mutation.isPending\n ? undefined\n : t('profile.deleteAccount.modal.confirm')}\n </TRButton>\n </ModalActions>\n </form>\n </Modal>\n );\n}\n","import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';\nimport { useMutation, useQueryClient } from '@tanstack/react-query';\nimport { TRButton } from '@tinyrack/ui/components/button';\nimport { ShieldCheckIcon } from 'lucide-react';\nimport { useMemo } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport z from 'zod';\nimport { AuthField } from '#frontend/components/auth/auth-field.tsx';\nimport { AlertBanner } from '#frontend/components/ui/alert-banner.tsx';\nimport { Modal, ModalActions } from '#frontend/components/ui/modal.tsx';\nimport { IssuaryError } from '#frontend/libs/error.ts';\nimport { getSessionQueryOptions } from '#frontend/queries/session.ts';\nimport { disableTotpMutationOptions } from '#frontend/queries/totp.ts';\n\ninterface DisableTotpModalProps {\n isOpen: boolean;\n onClose: () => void;\n}\n\nexport function DisableTotpModal({ isOpen, onClose }: DisableTotpModalProps) {\n const { t } = useTranslation();\n const queryClient = useQueryClient();\n\n const schema = useMemo(\n () =>\n z.object({\n code: z\n .string()\n .length(6, t('validation.totp.length'))\n .regex(/^\\d{6}$/, t('validation.totp.digits')),\n }),\n [t],\n );\n\n const form = useForm({\n resolver: standardSchemaResolver(schema),\n defaultValues: { code: '' },\n });\n\n const mutation = useMutation({\n ...disableTotpMutationOptions,\n onSuccess: () => {\n queryClient.invalidateQueries({\n queryKey: getSessionQueryOptions.queryKey,\n });\n handleClose();\n },\n });\n\n const handleClose = () => {\n form.reset();\n onClose();\n };\n\n const handleSubmit = form.handleSubmit(async (data) => {\n try {\n await mutation.mutateAsync({ code: data.code });\n } catch (error) {\n if (error instanceof IssuaryError) {\n if (error.code === 'CANNOT_REMOVE_LAST_SECOND_FACTOR') {\n form.setError('code', {\n message: t(\n 'profile.totp.disableModal.cannotRemoveLastSecondFactor',\n ),\n });\n return;\n }\n }\n form.setError('code', {\n message: t('profile.totp.disableModal.error'),\n });\n }\n });\n\n return (\n <Modal\n description={t('profile.totp.disableModal.description')}\n icon={ShieldCheckIcon}\n isOpen={isOpen}\n onClose={handleClose}\n title={t('profile.totp.disableModal.title')}\n variant=\"destructive\"\n >\n <form\n className=\"mt-tinyrack-lg flex flex-col gap-tinyrack-md\"\n onSubmit={handleSubmit}\n >\n <AlertBanner variant=\"warning\">\n {t('profile.totp.disableModal.warning')}\n </AlertBanner>\n\n <AuthField\n autoComplete=\"one-time-code\"\n error={form.formState.errors.code}\n errorTestId=\"disable-totp-error\"\n id=\"disable-totp-code\"\n inputMode=\"numeric\"\n label={t('profile.totp.disableModal.codeLabel')}\n maxLength={6}\n pattern=\"[0-9]*\"\n placeholder=\"000000\"\n {...form.register('code')}\n type=\"text\"\n />\n\n <ModalActions>\n <TRButton\n appearance=\"outline\"\n data-testid=\"disable-totp-cancel\"\n disabled={mutation.isPending}\n intent=\"neutral\"\n onClick={handleClose}\n type=\"button\"\n uiSize=\"sm\"\n >\n {t('profile.totp.disableModal.cancel')}\n </TRButton>\n <TRButton\n data-testid=\"disable-totp-submit\"\n disabled={mutation.isPending}\n intent=\"danger\"\n loading={mutation.isPending}\n loadingLabel={t('profile.totp.disableModal.disabling')}\n type=\"submit\"\n uiSize=\"sm\"\n >\n {mutation.isPending\n ? undefined\n : t('profile.totp.disableModal.disable')}\n </TRButton>\n </ModalActions>\n </form>\n </Modal>\n );\n}\n","import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\nimport { TRBadge } from '@tinyrack/ui/components/badge';\nimport { TRButton } from '@tinyrack/ui/components/button';\nimport { TRSpinner } from '@tinyrack/ui/components/spinner';\nimport {\n CloudIcon,\n FingerprintIcon,\n PencilIcon,\n SmartphoneIcon,\n Trash2Icon,\n} from 'lucide-react';\nimport { useCallback, useMemo, useState } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport z from 'zod';\nimport { AuthField } from '#frontend/components/auth/auth-field.tsx';\nimport { AlertBanner } from '#frontend/components/ui/alert-banner.tsx';\nimport { Modal, ModalActions } from '#frontend/components/ui/modal.tsx';\nimport { IssuaryError } from '#frontend/libs/error.ts';\nimport { queryKeys } from '#frontend/queries/keys.ts';\nimport {\n deletePasskeyMutationOptions,\n getPasskeysQueryOptions,\n type PasskeyInfo,\n renamePasskeyMutationOptions,\n} from '#frontend/queries/passkey.ts';\nimport { getSessionQueryOptions } from '#frontend/queries/session.ts';\n\ninterface ManagePasskeysModalProps {\n isOpen: boolean;\n onClose: () => void;\n onAddNew: () => void;\n}\n\nexport function ManagePasskeysModal({\n isOpen,\n onClose,\n onAddNew,\n}: ManagePasskeysModalProps) {\n const { t } = useTranslation();\n const queryClient = useQueryClient();\n const [editingPasskey, setEditingPasskey] = useState<PasskeyInfo | null>(\n null,\n );\n const [deletingId, setDeletingId] = useState<string | null>(null);\n const [confirmingDeleteId, setConfirmingDeleteId] = useState<string | null>(\n null,\n );\n const [deleteError, setDeleteError] = useState<string | null>(null);\n\n const { data: passkeysData, isLoading } = useQuery({\n ...getPasskeysQueryOptions,\n enabled: isOpen,\n });\n\n const deleteMutation = useMutation({\n ...deletePasskeyMutationOptions,\n onSuccess: () => {\n queryClient.invalidateQueries({ queryKey: queryKeys.passkeys() });\n queryClient.invalidateQueries({\n queryKey: getSessionQueryOptions.queryKey,\n });\n },\n onSettled: () => {\n setDeletingId(null);\n },\n });\n\n const handleClose = useCallback(() => {\n setEditingPasskey(null);\n setDeletingId(null);\n setConfirmingDeleteId(null);\n setDeleteError(null);\n onClose();\n }, [onClose]);\n\n const handleRequestDelete = (passkey: PasskeyInfo) => {\n setConfirmingDeleteId(passkey.id);\n setDeleteError(null);\n };\n\n const handleCancelDelete = () => {\n setConfirmingDeleteId(null);\n setDeleteError(null);\n };\n\n const handleConfirmDelete = async (passkey: PasskeyInfo) => {\n setDeletingId(passkey.id);\n setConfirmingDeleteId(null);\n try {\n await deleteMutation.mutateAsync({ id: passkey.id });\n } catch (error) {\n if (error instanceof IssuaryError) {\n if (error.code === 'CANNOT_REMOVE_LAST_SECOND_FACTOR') {\n setDeleteError(\n t('profile.passkey.manageModal.cannotRemoveLastSecondFactor'),\n );\n return;\n }\n }\n setDeleteError(t('profile.passkey.manageModal.deleteError'));\n }\n };\n\n const formatDate = (dateString: string) => {\n return new Date(dateString).toLocaleDateString(undefined, {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n });\n };\n\n const passkeys = passkeysData?.passkeys || [];\n\n return (\n <Modal\n icon={FingerprintIcon}\n isOpen={isOpen}\n onClose={handleClose}\n size=\"lg\"\n title={t('profile.passkey.manageModal.title')}\n >\n <div className=\"mt-tinyrack-lg flex flex-col gap-tinyrack-sm\">\n {deleteError && (\n <AlertBanner variant=\"error\">{deleteError}</AlertBanner>\n )}\n\n {isLoading && (\n <div className=\"flex justify-center py-tinyrack-xl\">\n <TRSpinner uiSize=\"md\" />\n </div>\n )}\n\n {!isLoading && passkeys.length === 0 && (\n <div\n className=\"py-tinyrack-xl text-center text-tinyrack-sm text-tinyrack-text-muted\"\n data-testid=\"passkeys-empty\"\n >\n <p>{t('profile.passkey.manageModal.noPasskeys')}</p>\n </div>\n )}\n\n {!isLoading && passkeys.length > 0 && (\n <div className=\"flex flex-col gap-tinyrack-sm\">\n {passkeys.map((passkey) => (\n <PasskeyItem\n formatDate={formatDate}\n isConfirmingDelete={confirmingDeleteId === passkey.id}\n isDeleting={deletingId === passkey.id}\n isEditing={editingPasskey?.id === passkey.id}\n key={passkey.id}\n onCancelDelete={handleCancelDelete}\n onCancelEdit={() => setEditingPasskey(null)}\n onConfirmDelete={() => handleConfirmDelete(passkey)}\n onEdit={() => setEditingPasskey(passkey)}\n onRequestDelete={() => handleRequestDelete(passkey)}\n passkey={passkey}\n />\n ))}\n </div>\n )}\n </div>\n\n <ModalActions>\n <TRButton\n appearance=\"outline\"\n data-testid=\"manage-passkeys-close\"\n intent=\"neutral\"\n onClick={handleClose}\n type=\"button\"\n uiSize=\"sm\"\n >\n {t('profile.passkey.manageModal.close')}\n </TRButton>\n <TRButton\n data-testid=\"manage-passkeys-add-new\"\n intent=\"primary\"\n onClick={() => {\n handleClose();\n onAddNew();\n }}\n type=\"button\"\n uiSize=\"sm\"\n >\n {t('profile.passkey.manageModal.addNew')}\n </TRButton>\n </ModalActions>\n </Modal>\n );\n}\n\ninterface PasskeyItemProps {\n passkey: PasskeyInfo;\n isDeleting: boolean;\n isEditing: boolean;\n isConfirmingDelete: boolean;\n onEdit: () => void;\n onCancelEdit: () => void;\n onRequestDelete: () => void;\n onConfirmDelete: () => void;\n onCancelDelete: () => void;\n formatDate: (dateString: string) => string;\n}\n\nfunction PasskeyItem({\n passkey,\n isDeleting,\n isEditing,\n isConfirmingDelete,\n onEdit,\n onCancelEdit,\n onRequestDelete,\n onConfirmDelete,\n onCancelDelete,\n formatDate,\n}: PasskeyItemProps) {\n const { t } = useTranslation();\n const queryClient = useQueryClient();\n\n const schema = useMemo(\n () =>\n z.object({\n name: z\n .string()\n .min(1, t('validation.passkey.name.required'))\n .max(100, t('validation.passkey.name.max')),\n }),\n [t],\n );\n\n const form = useForm({\n resolver: standardSchemaResolver(schema),\n defaultValues: { name: passkey.name || '' },\n });\n\n const renameMutation = useMutation({\n ...renamePasskeyMutationOptions,\n onSuccess: () => {\n queryClient.invalidateQueries({ queryKey: queryKeys.passkeys() });\n onCancelEdit();\n },\n });\n\n const handleRename = form.handleSubmit(async (data) => {\n try {\n await renameMutation.mutateAsync({ id: passkey.id, name: data.name });\n } catch {\n form.setError('name', {\n message: t('profile.passkey.manageModal.renameError'),\n });\n }\n });\n\n if (isEditing) {\n return (\n <form\n className=\"flex flex-col gap-tinyrack-xs rounded-tinyrack-md bg-tinyrack-surface-muted p-tinyrack-sm\"\n onSubmit={handleRename}\n >\n <AuthField\n data-testid=\"passkey-rename-input\"\n error={form.formState.errors.name}\n errorTestId=\"passkey-rename-error\"\n hideLabel\n label={t('profile.passkey.manageModal.nameLabel')}\n placeholder={t('profile.passkey.manageModal.namePlaceholder')}\n {...form.register('name')}\n type=\"text\"\n />\n <div className=\"flex justify-end gap-tinyrack-xs\">\n <TRButton\n appearance=\"ghost\"\n disabled={renameMutation.isPending}\n onClick={() => {\n form.reset();\n onCancelEdit();\n }}\n type=\"button\"\n uiSize=\"sm\"\n >\n {t('profile.passkey.manageModal.cancelEdit')}\n </TRButton>\n <TRButton\n disabled={renameMutation.isPending}\n intent=\"primary\"\n type=\"submit\"\n uiSize=\"sm\"\n >\n {renameMutation.isPending\n ? t('profile.passkey.manageModal.saving')\n : t('profile.passkey.manageModal.save')}\n </TRButton>\n </div>\n </form>\n );\n }\n\n if (isConfirmingDelete) {\n return (\n <div className=\"flex items-center justify-between rounded-tinyrack-md border border-tinyrack-danger-border bg-tinyrack-danger-surface p-tinyrack-sm text-tinyrack-danger-foreground\">\n <div className=\"flex items-center gap-tinyrack-sm\">\n <Trash2Icon className=\"size-tinyrack-lg\" />\n <span className=\"text-tinyrack-xs\">\n {t('profile.passkey.manageModal.deleteConfirmInline')}\n </span>\n </div>\n <div className=\"flex gap-tinyrack-xs\">\n <TRButton\n appearance=\"ghost\"\n onClick={onCancelDelete}\n type=\"button\"\n uiSize=\"sm\"\n >\n {t('profile.passkey.manageModal.cancelEdit')}\n </TRButton>\n <TRButton\n intent=\"danger\"\n onClick={onConfirmDelete}\n type=\"button\"\n uiSize=\"sm\"\n >\n {t('profile.passkey.manageModal.delete')}\n </TRButton>\n </div>\n </div>\n );\n }\n\n return (\n <div\n className=\"flex items-center justify-between rounded-tinyrack-md bg-tinyrack-surface-muted p-tinyrack-sm\"\n data-testid=\"passkey-item\"\n >\n <div className=\"flex items-center gap-tinyrack-sm\">\n {passkey.device_type === 'multiDevice' ? (\n <CloudIcon className=\"size-tinyrack-lg text-tinyrack-primary-foreground\" />\n ) : (\n <SmartphoneIcon className=\"size-tinyrack-lg text-tinyrack-primary-foreground\" />\n )}\n <div>\n <div className=\"font-tinyrack-medium text-tinyrack-xs\">\n {passkey.name || t('profile.passkey.manageModal.unnamedPasskey')}\n </div>\n <div className=\"flex items-center gap-tinyrack-xs text-tinyrack-text-muted text-tinyrack-xs\">\n <span>\n {t('profile.passkey.manageModal.createdAt', {\n date: formatDate(passkey.created_at),\n })}\n </span>\n {passkey.backed_up && (\n <TRBadge uiSize=\"md\" variant=\"success\">\n {t('profile.passkey.manageModal.backedUp')}\n </TRBadge>\n )}\n </div>\n </div>\n </div>\n <div className=\"flex gap-tinyrack-xs\">\n <TRButton\n appearance=\"ghost\"\n aria-label={t('profile.passkey.manageModal.rename')}\n onClick={onEdit}\n type=\"button\"\n uiSize=\"sm\"\n >\n <PencilIcon className=\"size-tinyrack-lg\" />\n </TRButton>\n <TRButton\n appearance=\"ghost\"\n aria-label={t('profile.passkey.manageModal.delete')}\n disabled={isDeleting}\n intent=\"danger\"\n loading={isDeleting}\n onClick={onRequestDelete}\n type=\"button\"\n uiSize=\"sm\"\n >\n {isDeleting ? undefined : <Trash2Icon className=\"size-tinyrack-lg\" />}\n </TRButton>\n </div>\n </div>\n );\n}\n","import {\n useMutation,\n useQueryClient,\n useSuspenseQuery,\n} from '@tanstack/react-query';\nimport { useRouter } from '@tanstack/react-router';\nimport { ShieldCheckIcon } from 'lucide-react';\nimport { useCallback, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { RecoveryCodesStep } from '#frontend/components/totp/recovery-codes-step.tsx';\nimport { VerifyStep } from '#frontend/components/totp/verify-step.tsx';\nimport { AlertBanner } from '#frontend/components/ui/alert-banner.tsx';\nimport { Modal } from '#frontend/components/ui/modal.tsx';\nimport { IssuaryError } from '#frontend/libs/error.ts';\nimport { tick } from '#frontend/libs/promise.ts';\nimport { appConfigQueryOptions } from '#frontend/queries/config.ts';\nimport { getSessionQueryOptions } from '#frontend/queries/session.ts';\nimport {\n type RegenerateTotpRecoveryCodesResponse,\n regenerateTotpRecoveryCodesMutationOptions,\n} from '#frontend/queries/totp.ts';\n\ninterface RegenerateTotpRecoveryCodesModalProps {\n isOpen: boolean;\n onClose: () => void;\n}\n\ntype RegenerateStep = 'verify' | 'recovery';\n\nexport function RegenerateTotpRecoveryCodesModal({\n isOpen,\n onClose,\n}: RegenerateTotpRecoveryCodesModalProps) {\n const { t } = useTranslation();\n const router = useRouter();\n const queryClient = useQueryClient();\n const { data: appConfig } = useSuspenseQuery(appConfigQueryOptions);\n const [step, setStep] = useState<RegenerateStep>('verify');\n const [response, setResponse] =\n useState<RegenerateTotpRecoveryCodesResponse | null>(null);\n const [errorMessage, setErrorMessage] = useState<string | null>(null);\n\n const mutation = useMutation({\n ...regenerateTotpRecoveryCodesMutationOptions,\n onSuccess: async (data) => {\n setResponse(data);\n setStep('recovery');\n await queryClient.invalidateQueries({\n queryKey: getSessionQueryOptions.queryKey,\n });\n },\n });\n\n const reset = useCallback(() => {\n setStep('verify');\n setResponse(null);\n setErrorMessage(null);\n mutation.reset();\n }, [mutation]);\n\n const handleClose = useCallback(() => {\n reset();\n onClose();\n }, [onClose, reset]);\n\n const handleVerify = useCallback(\n async (code: string) => {\n setErrorMessage(null);\n\n try {\n await mutation.mutateAsync({ code });\n } catch (error) {\n if (error instanceof IssuaryError) {\n if (error.code === 'INVALID_TOTP_CODE') {\n throw error;\n }\n\n if (error.code === 'UNAUTHORIZED') {\n queryClient.setQueryData(getSessionQueryOptions.queryKey, {\n user: null,\n });\n await tick();\n router.navigate({ to: '/login' });\n return;\n }\n\n if (\n error.code === 'TOTP_NOT_ENABLED' ||\n error.code === 'VALIDATION_ERROR'\n ) {\n await queryClient.invalidateQueries({\n queryKey: getSessionQueryOptions.queryKey,\n });\n handleClose();\n return;\n }\n }\n\n setErrorMessage(t('profile.totp.regenerateModal.unexpectedError'));\n }\n },\n [handleClose, mutation, queryClient, router, t],\n );\n\n if (!appConfig.auth.password.totp.enabled) {\n return null;\n }\n\n return (\n <Modal\n description={\n step === 'verify'\n ? t('profile.totp.regenerateModal.description')\n : t('profile.totp.regenerateModal.recoveryDescription')\n }\n icon={ShieldCheckIcon}\n isOpen={isOpen}\n onClose={handleClose}\n preventClose={step === 'recovery'}\n size=\"sm\"\n title={\n step === 'verify'\n ? t('profile.totp.regenerateModal.title')\n : t('profile.totp.regenerateModal.recoveryTitle')\n }\n >\n {step === 'verify' ? (\n <div className=\"mt-tinyrack-lg\">\n {errorMessage && (\n <div className=\"mb-tinyrack-md\">\n <AlertBanner variant=\"error\">{errorMessage}</AlertBanner>\n </div>\n )}\n <VerifyStep\n invalidMessage={t('profile.totp.regenerateModal.error')}\n isPending={mutation.isPending}\n onSubmit={handleVerify}\n pendingText={t('profile.totp.regenerateModal.submitting')}\n submitLabel={t('profile.totp.regenerateModal.submit')}\n />\n </div>\n ) : (\n <div className=\"mt-tinyrack-lg\">\n <RecoveryCodesStep\n onConfirm={handleClose}\n recoveryCodes={response?.recovery_codes ?? []}\n />\n </div>\n )}\n </Modal>\n );\n}\n","import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';\nimport { useMutation, useQueryClient } from '@tanstack/react-query';\nimport { TRButton } from '@tinyrack/ui/components/button';\nimport { KeyRoundIcon } from 'lucide-react';\nimport { useMemo } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport z from 'zod';\nimport { AuthField } from '#frontend/components/auth/auth-field.tsx';\nimport { AlertBanner } from '#frontend/components/ui/alert-banner.tsx';\nimport { Modal, ModalActions } from '#frontend/components/ui/modal.tsx';\nimport { IssuaryError } from '#frontend/libs/error.ts';\nimport { removePasswordMutationOptions } from '#frontend/queries/password.ts';\nimport { getSessionQueryOptions } from '#frontend/queries/session.ts';\n\ninterface RemovePasswordModalProps {\n isOpen: boolean;\n onClose: () => void;\n}\n\nexport function RemovePasswordModal({\n isOpen,\n onClose,\n}: RemovePasswordModalProps) {\n const { t } = useTranslation();\n const queryClient = useQueryClient();\n\n const schema = useMemo(\n () =>\n z.object({\n currentPassword: z.string().min(1, t('validation.password.required')),\n }),\n [t],\n );\n\n const form = useForm({\n resolver: standardSchemaResolver(schema),\n defaultValues: { currentPassword: '' },\n });\n\n const mutation = useMutation({\n ...removePasswordMutationOptions,\n onSuccess: () => {\n queryClient.invalidateQueries({\n queryKey: getSessionQueryOptions.queryKey,\n });\n handleClose();\n },\n });\n\n const handleClose = () => {\n form.reset();\n onClose();\n };\n\n const handleSubmit = form.handleSubmit(async (data) => {\n try {\n await mutation.mutateAsync({\n current_password: data.currentPassword,\n });\n } catch (err) {\n if (err instanceof IssuaryError) {\n if (err.code === 'INVALID_CURRENT_PASSWORD') {\n form.setError('currentPassword', {\n message: t('profile.password.removeModal.invalidCurrent'),\n });\n return;\n }\n if (err.code === 'CANNOT_REMOVE_LAST_AUTH_METHOD') {\n form.setError('root', {\n message: t('profile.password.removeModal.noOAuth'),\n });\n return;\n }\n if (err.code === 'CANNOT_REMOVE_PASSWORD_WITH_SECOND_FACTOR_ONLY') {\n form.setError('root', {\n message: t('profile.password.removeModal.noOAuthWith2FA'),\n });\n return;\n }\n }\n form.setError('root', {\n message: t('profile.password.removeModal.error'),\n });\n }\n });\n\n return (\n <Modal\n description={t('profile.password.removeModal.description')}\n icon={KeyRoundIcon}\n isOpen={isOpen}\n onClose={handleClose}\n title={t('profile.password.removeModal.title')}\n variant=\"destructive\"\n >\n <form\n className=\"mt-tinyrack-lg flex flex-col gap-tinyrack-md\"\n onSubmit={handleSubmit}\n >\n <AuthField\n error={form.formState.errors.currentPassword}\n errorTestId=\"remove-password-error\"\n id=\"current-password-remove\"\n label={t('profile.password.removeModal.currentPassword')}\n placeholder={t(\n 'profile.password.removeModal.currentPasswordPlaceholder',\n )}\n {...form.register('currentPassword')}\n type=\"password\"\n />\n {form.formState.errors.root && (\n <AlertBanner variant=\"error\">\n {form.formState.errors.root.message}\n </AlertBanner>\n )}\n <ModalActions>\n <TRButton\n appearance=\"outline\"\n data-testid=\"remove-password-cancel\"\n disabled={mutation.isPending}\n intent=\"neutral\"\n onClick={handleClose}\n type=\"button\"\n uiSize=\"sm\"\n >\n {t('profile.password.removeModal.cancel')}\n </TRButton>\n <TRButton\n data-testid=\"remove-password-submit\"\n disabled={mutation.isPending}\n intent=\"danger\"\n loading={mutation.isPending}\n loadingLabel={t('profile.password.removeModal.submitting')}\n type=\"submit\"\n uiSize=\"sm\"\n >\n {mutation.isPending\n ? undefined\n : t('profile.password.removeModal.submit')}\n </TRButton>\n </ModalActions>\n </form>\n </Modal>\n );\n}\n","import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';\nimport {\n useMutation,\n useQueryClient,\n useSuspenseQuery,\n} from '@tanstack/react-query';\nimport { TRButton } from '@tinyrack/ui/components/button';\nimport { KeyRoundIcon } from 'lucide-react';\nimport { useMemo } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport z from 'zod';\nimport { AuthField } from '#frontend/components/auth/auth-field.tsx';\nimport { AlertBanner } from '#frontend/components/ui/alert-banner.tsx';\nimport { Modal, ModalActions } from '#frontend/components/ui/modal.tsx';\nimport { appConfigQueryOptions } from '#frontend/queries/config.ts';\nimport { setPasswordMutationOptions } from '#frontend/queries/password.ts';\nimport { getSessionQueryOptions } from '#frontend/queries/session.ts';\n\ninterface SetPasswordModalProps {\n isOpen: boolean;\n onClose: () => void;\n}\n\nexport function SetPasswordModal({ isOpen, onClose }: SetPasswordModalProps) {\n const { t } = useTranslation();\n const queryClient = useQueryClient();\n const { data: configData } = useSuspenseQuery(appConfigQueryOptions);\n const passwordPolicy = configData.auth.password.policy;\n\n const schema = useMemo(\n () =>\n z\n .object({\n password: z\n .string()\n .min(\n passwordPolicy.min_length,\n t('validation.password.min', {\n count: passwordPolicy.min_length,\n }),\n )\n .max(\n passwordPolicy.max_length,\n t('validation.password.max', {\n count: passwordPolicy.max_length,\n }),\n ),\n confirmPassword: z.string(),\n })\n .refine((data) => data.password === data.confirmPassword, {\n message: t('validation.confirmPassword.mismatch'),\n path: ['confirmPassword'],\n }),\n [passwordPolicy.max_length, passwordPolicy.min_length, t],\n );\n\n const form = useForm({\n resolver: standardSchemaResolver(schema),\n defaultValues: { password: '', confirmPassword: '' },\n });\n\n const mutation = useMutation({\n ...setPasswordMutationOptions,\n onSuccess: () => {\n queryClient.invalidateQueries({\n queryKey: getSessionQueryOptions.queryKey,\n });\n handleClose();\n },\n });\n\n const handleClose = () => {\n form.reset();\n onClose();\n };\n\n const handleSubmit = form.handleSubmit(async (data) => {\n try {\n await mutation.mutateAsync({ password: data.password });\n } catch (error) {\n void error;\n form.setError('root', {\n message: t('profile.password.setModal.error'),\n });\n }\n });\n\n return (\n <Modal\n description={t('profile.password.setModal.description')}\n icon={KeyRoundIcon}\n isOpen={isOpen}\n onClose={handleClose}\n title={t('profile.password.setModal.title')}\n >\n <form\n className=\"mt-tinyrack-lg flex flex-col gap-tinyrack-md\"\n onSubmit={handleSubmit}\n >\n <AuthField\n error={form.formState.errors.password}\n errorTestId=\"set-password-error-password\"\n id=\"new-password\"\n label={t('profile.password.setModal.newPassword')}\n placeholder={t('profile.password.setModal.newPasswordPlaceholder')}\n {...form.register('password')}\n type=\"password\"\n />\n <AuthField\n error={form.formState.errors.confirmPassword}\n errorTestId=\"set-password-error-confirmPassword\"\n id=\"confirm-password\"\n label={t('profile.password.setModal.confirmPassword')}\n placeholder={t(\n 'profile.password.setModal.confirmPasswordPlaceholder',\n )}\n {...form.register('confirmPassword')}\n type=\"password\"\n />\n {form.formState.errors.root && (\n <AlertBanner variant=\"error\">\n {form.formState.errors.root.message}\n </AlertBanner>\n )}\n <ModalActions>\n <TRButton\n appearance=\"outline\"\n data-testid=\"set-password-cancel\"\n disabled={mutation.isPending}\n intent=\"neutral\"\n onClick={handleClose}\n type=\"button\"\n uiSize=\"sm\"\n >\n {t('profile.password.setModal.cancel')}\n </TRButton>\n <TRButton\n data-testid=\"set-password-submit\"\n disabled={mutation.isPending}\n intent=\"primary\"\n loading={mutation.isPending}\n loadingLabel={t('profile.password.setModal.submitting')}\n type=\"submit\"\n uiSize=\"sm\"\n >\n {mutation.isPending\n ? undefined\n : t('profile.password.setModal.submit')}\n </TRButton>\n </ModalActions>\n </form>\n </Modal>\n );\n}\n","import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';\nimport { useMutation, useQueryClient } from '@tanstack/react-query';\nimport { TRButton } from '@tinyrack/ui/components/button';\nimport { TRSpinner } from '@tinyrack/ui/components/spinner';\nimport { FingerprintIcon } from 'lucide-react';\nimport { useCallback, useMemo, useState } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport z from 'zod';\nimport { AuthField } from '#frontend/components/auth/auth-field.tsx';\nimport { AlertBanner } from '#frontend/components/ui/alert-banner.tsx';\nimport { Modal, ModalActions } from '#frontend/components/ui/modal.tsx';\nimport { queryKeys } from '#frontend/queries/keys.ts';\nimport { registerPasskeyMutationOptions } from '#frontend/queries/passkey.ts';\nimport { getSessionQueryOptions } from '#frontend/queries/session.ts';\n\ninterface SetupPasskeyModalProps {\n isOpen: boolean;\n onClose: () => void;\n isRequired?: boolean;\n canSwitchToTotp?: boolean;\n onSwitchToTotp?: () => void;\n}\n\ntype SetupStep = 'name' | 'register';\n\nexport function SetupPasskeyModal({\n isOpen,\n onClose,\n isRequired = false,\n canSwitchToTotp = false,\n onSwitchToTotp,\n}: SetupPasskeyModalProps) {\n const { t } = useTranslation();\n const queryClient = useQueryClient();\n const [step, setStep] = useState<SetupStep>('name');\n const [errorMessage, setErrorMessage] = useState<string | null>(null);\n\n const schema = useMemo(\n () =>\n z.object({\n name: z\n .string()\n .max(100, t('validation.passkey.name.max'))\n .optional()\n .transform((val) => val?.trim() || undefined),\n }),\n [t],\n );\n\n const form = useForm({\n resolver: standardSchemaResolver(schema),\n defaultValues: { name: '' },\n });\n\n const registerMutation = useMutation({\n ...registerPasskeyMutationOptions,\n onSuccess: () => {\n queryClient.invalidateQueries({ queryKey: queryKeys.passkeys() });\n queryClient.invalidateQueries({\n queryKey: getSessionQueryOptions.queryKey,\n });\n handleClose();\n },\n onError: (error) => {\n setStep('name');\n if (error.name === 'NotAllowedError') {\n setErrorMessage(t('profile.passkey.setupModal.cancelled'));\n } else {\n setErrorMessage(t('profile.passkey.setupModal.error'));\n }\n },\n });\n\n const handleClose = useCallback(() => {\n form.reset();\n setStep('name');\n setErrorMessage(null);\n onClose();\n }, [form, onClose]);\n\n const handleRegister = form.handleSubmit(async (data) => {\n setStep('register');\n setErrorMessage(null);\n try {\n await registerMutation.mutateAsync({ name: data.name });\n } catch {\n // Error handling is done via mutation callbacks\n }\n });\n\n const isPending = registerMutation.isPending;\n\n return (\n <Modal\n description={\n isRequired\n ? t('profile.passkey.setupModal.requiredDescription')\n : undefined\n }\n icon={FingerprintIcon}\n isOpen={isOpen}\n onClose={handleClose}\n preventClose={isRequired}\n size=\"sm\"\n title={t('profile.passkey.setupModal.title')}\n >\n {step === 'name' && (\n <form\n className=\"mt-tinyrack-lg flex flex-col gap-tinyrack-md\"\n onSubmit={handleRegister}\n >\n <p className=\"text-tinyrack-text-muted text-tinyrack-xs\">\n {t('profile.passkey.setupModal.description')}\n </p>\n\n {errorMessage && (\n <AlertBanner variant=\"error\">{errorMessage}</AlertBanner>\n )}\n\n <AuthField\n error={form.formState.errors.name}\n errorTestId=\"setup-passkey-error\"\n hint={t('profile.passkey.setupModal.nameHint')}\n id=\"passkey-name\"\n label={t('profile.passkey.setupModal.nameLabel')}\n placeholder={t('profile.passkey.setupModal.namePlaceholder')}\n {...form.register('name')}\n type=\"text\"\n />\n\n <ModalActions>\n <TRButton\n appearance=\"outline\"\n data-testid=\"setup-passkey-cancel\"\n intent=\"neutral\"\n onClick={handleClose}\n type=\"button\"\n uiSize=\"sm\"\n >\n {t('profile.passkey.setupModal.cancel')}\n </TRButton>\n <TRButton\n data-testid=\"setup-passkey-continue\"\n intent=\"primary\"\n type=\"submit\"\n uiSize=\"sm\"\n >\n {t('profile.passkey.setupModal.continue')}\n </TRButton>\n </ModalActions>\n {canSwitchToTotp && onSwitchToTotp && (\n <p className=\"mt-tinyrack-sm text-center text-tinyrack-xs\">\n <TRButton\n appearance=\"ghost\"\n intent=\"primary\"\n onClick={onSwitchToTotp}\n type=\"button\"\n uiSize=\"sm\"\n >\n {t('profile.passkey.setupModal.switchToTotp')}\n </TRButton>\n </p>\n )}\n </form>\n )}\n\n {step === 'register' && (\n <div className=\"mt-tinyrack-lg\">\n <div\n className=\"flex flex-col items-center gap-tinyrack-md py-tinyrack-xl\"\n data-testid=\"setup-passkey-loading\"\n >\n <TRSpinner uiSize=\"md\" />\n <p className=\"text-center text-tinyrack-text-muted text-tinyrack-xs\">\n {t('profile.passkey.setupModal.waitingForDevice')}\n </p>\n </div>\n\n <ModalActions>\n <TRButton\n appearance=\"outline\"\n data-testid=\"setup-passkey-cancel\"\n disabled={isPending}\n intent=\"neutral\"\n onClick={handleClose}\n type=\"button\"\n uiSize=\"sm\"\n >\n {t('profile.passkey.setupModal.cancel')}\n </TRButton>\n </ModalActions>\n </div>\n )}\n </Modal>\n );\n}\n","import { TRButton } from '@tinyrack/ui/components/button';\nimport { TRSpinner } from '@tinyrack/ui/components/spinner';\nimport { ShieldCheckIcon } from 'lucide-react';\nimport { useCallback } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { QrStep } from '#frontend/components/totp/qr-step.tsx';\nimport { RecoveryCodesStep } from '#frontend/components/totp/recovery-codes-step.tsx';\nimport { VerifyStep } from '#frontend/components/totp/verify-step.tsx';\nimport { AlertBanner } from '#frontend/components/ui/alert-banner.tsx';\nimport { Modal, ModalActions } from '#frontend/components/ui/modal.tsx';\nimport { useTotpSetup } from '#frontend/features/totp/use-totp-setup.ts';\n\ninterface SetupTotpModalProps {\n isOpen: boolean;\n onClose: () => void;\n isRequired?: boolean;\n canSwitchToPasskey?: boolean;\n onSwitchToPasskey?: () => void;\n}\n\nexport function SetupTotpModal({\n isOpen,\n onClose,\n isRequired = false,\n canSwitchToPasskey = false,\n onSwitchToPasskey,\n}: SetupTotpModalProps) {\n const { t } = useTranslation();\n\n const {\n step,\n setupData,\n recoveryCodes,\n isSetupPending,\n isVerifyPending,\n isConfirmPending,\n startSetup,\n verify,\n goToVerify,\n goToQr,\n confirmRecoveryCodes,\n reset,\n } = useTotpSetup({\n autoStart: false,\n onConfirmSuccess: () => {\n reset();\n onClose();\n },\n });\n\n const handleClose = useCallback(() => {\n reset();\n onClose();\n }, [onClose, reset]);\n\n const handleModalOpen = useCallback(() => {\n if (!setupData && !isSetupPending) {\n startSetup();\n }\n }, [setupData, isSetupPending, startSetup]);\n\n if (isOpen && !setupData && !isSetupPending && step !== 'error') {\n handleModalOpen();\n }\n\n const handleVerify = useCallback(\n async (code: string) => {\n await verify(code);\n },\n [verify],\n );\n\n const modalTitle =\n step === 'recovery'\n ? t('setupTotp.recoveryCodes.title')\n : t('profile.totp.setupModal.title');\n\n return (\n <Modal\n description={\n isRequired\n ? t('profile.totp.setupModal.requiredDescription')\n : undefined\n }\n icon={ShieldCheckIcon}\n isOpen={isOpen}\n onClose={handleClose}\n preventClose={isRequired || step === 'recovery'}\n size=\"sm\"\n title={modalTitle}\n >\n {isSetupPending && (\n <div\n className=\"flex justify-center py-tinyrack-xl\"\n data-testid=\"setup-totp-loading\"\n >\n <TRSpinner uiSize=\"md\" />\n </div>\n )}\n\n {step === 'error' && (\n <div className=\"mt-tinyrack-lg flex flex-col gap-tinyrack-md\">\n <AlertBanner variant=\"error\">\n {t('profile.totp.setupModal.setupError')}\n </AlertBanner>\n <ModalActions>\n <TRButton\n appearance=\"outline\"\n intent=\"neutral\"\n onClick={handleClose}\n type=\"button\"\n uiSize=\"sm\"\n >\n {t('profile.totp.setupModal.cancel')}\n </TRButton>\n <TRButton\n intent=\"primary\"\n onClick={startSetup}\n type=\"button\"\n uiSize=\"sm\"\n >\n {t('profile.totp.setupModal.retry')}\n </TRButton>\n </ModalActions>\n </div>\n )}\n\n {setupData && step === 'qr' && (\n <div className=\"mt-tinyrack-lg\">\n <QrStep\n additionalActions={\n <>\n <ModalActions>\n <TRButton\n appearance=\"outline\"\n intent=\"neutral\"\n onClick={handleClose}\n type=\"button\"\n uiSize=\"sm\"\n >\n {t('profile.totp.setupModal.cancel')}\n </TRButton>\n </ModalActions>\n {canSwitchToPasskey && onSwitchToPasskey && (\n <p className=\"mt-tinyrack-sm text-center text-tinyrack-xs\">\n <TRButton\n appearance=\"ghost\"\n intent=\"primary\"\n onClick={onSwitchToPasskey}\n type=\"button\"\n uiSize=\"sm\"\n >\n {t('profile.totp.setupModal.switchToPasskey')}\n </TRButton>\n </p>\n )}\n </>\n }\n onNext={goToVerify}\n setupData={setupData}\n />\n </div>\n )}\n\n {setupData && step === 'verify' && (\n <div className=\"mt-tinyrack-lg\">\n <VerifyStep\n isPending={isVerifyPending}\n onBack={goToQr}\n onSubmit={handleVerify}\n />\n </div>\n )}\n\n {step === 'recovery' && recoveryCodes.length > 0 && (\n <div className=\"mt-tinyrack-lg\">\n <RecoveryCodesStep\n isLoading={isConfirmPending}\n onConfirm={confirmRecoveryCodes}\n recoveryCodes={recoveryCodes}\n />\n </div>\n )}\n </Modal>\n );\n}\n","import {\n useMutation,\n useQueryClient,\n useSuspenseQuery,\n} from '@tanstack/react-query';\nimport {\n createFileRoute,\n type ErrorComponentProps,\n redirect,\n useRouter,\n} from '@tanstack/react-router';\nimport { TRButton } from '@tinyrack/ui/components/button';\nimport { TRCard } from '@tinyrack/ui/components/card';\nimport { TRText } from '@tinyrack/ui/components/text';\nimport { CircleAlertIcon, LogOutIcon } from 'lucide-react';\nimport { useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport { AuthPageHeader } from '#frontend/components/auth/auth-page-header.tsx';\nimport { DangerZoneSection } from '#frontend/components/profile/danger-zone-section.tsx';\nimport { LinkedAccountsSection } from '#frontend/components/profile/linked-accounts-section.tsx';\nimport { PasskeySection } from '#frontend/components/profile/passkey-section.tsx';\nimport { PasswordSection } from '#frontend/components/profile/password-section.tsx';\nimport { TotpSection } from '#frontend/components/profile/totp-section.tsx';\nimport { UnlinkOAuthModal } from '#frontend/components/profile/unlink-oauth-modal.tsx';\nimport { UserInfoSection } from '#frontend/components/profile/user-info-section.tsx';\nimport { Alert } from '#frontend/components/ui/alert.tsx';\nimport { InitialAvatar } from '#frontend/components/ui/initial-avatar.tsx';\nimport { RouteErrorFallback } from '#frontend/components/ui/route-error-fallback.tsx';\nimport { AppLayout } from '#frontend/features/layout/app-layout.tsx';\nimport { ChangePasswordModal } from '#frontend/features/profile/change-password-modal.tsx';\nimport { DeleteAccountModal } from '#frontend/features/profile/delete-account-modal.tsx';\nimport { DisableTotpModal } from '#frontend/features/profile/disable-totp-modal.tsx';\nimport { ManagePasskeysModal } from '#frontend/features/profile/manage-passkeys-modal.tsx';\nimport { RegenerateTotpRecoveryCodesModal } from '#frontend/features/profile/regenerate-totp-recovery-codes-modal.tsx';\nimport { RemovePasswordModal } from '#frontend/features/profile/remove-password-modal.tsx';\nimport { SetPasswordModal } from '#frontend/features/profile/set-password-modal.tsx';\nimport { SetupPasskeyModal } from '#frontend/features/profile/setup-passkey-modal.tsx';\nimport { SetupTotpModal } from '#frontend/features/profile/setup-totp-modal.tsx';\nimport { tick } from '#frontend/libs/promise.ts';\nimport { appConfigQueryOptions } from '#frontend/queries/config.ts';\nimport { logoutMutationOptions } from '#frontend/queries/logout.ts';\nimport {\n getOAuthAuthorizeUrl,\n oauthAccountsQueryOptions,\n unlinkOAuthMutationOptions,\n} from '#frontend/queries/oauth.ts';\nimport { getSessionQueryOptions } from '#frontend/queries/session.ts';\n\ntype PasswordModalType = 'set' | 'change' | 'remove' | null;\ntype TotpModalType = 'setup' | 'disable' | 'regenerate' | null;\ntype PasskeyModalType = 'setup' | 'manage' | null;\n\nconst SearchSchema = z.object({\n oauth_error: z.string().optional(),\n oauth_error_description: z.string().optional(),\n});\n\nconst OAUTH_ERROR_I18N_MAP: Record<string, string> = {\n access_denied: 'oauth.error.accessDenied',\n temporarily_unavailable: 'oauth.error.temporarilyUnavailable',\n server_error: 'oauth.error.serverError',\n};\n\nfunction ProfileError(props: ErrorComponentProps) {\n return (\n <RouteErrorFallback\n {...props}\n onUnauthorized={() => {\n window.location.href = '/login';\n }}\n />\n );\n}\n\nexport const Route = createFileRoute('/profile/')({\n component: Profile,\n errorComponent: ProfileError,\n validateSearch: SearchSchema,\n beforeLoad: async ({ context }) => {\n if (!context.user) {\n throw redirect({\n to: '/login',\n });\n }\n },\n loader: async ({ context }) => {\n await Promise.all([\n context.queryClient.ensureQueryData(getSessionQueryOptions),\n context.queryClient.ensureQueryData(oauthAccountsQueryOptions),\n context.queryClient.ensureQueryData(appConfigQueryOptions),\n ]);\n },\n});\n\nfunction Profile() {\n const { t } = useTranslation();\n const router = useRouter();\n const queryClient = useQueryClient();\n const search = Route.useSearch();\n\n const oauthError = search.oauth_error;\n const oauthErrorMessage = oauthError\n ? t(OAUTH_ERROR_I18N_MAP[oauthError] ?? 'oauth.error.failed')\n : undefined;\n const [unlinkingProvider, setUnlinkingProvider] = useState<string | null>(\n null,\n );\n const [unlinkModal, setUnlinkModal] = useState<{\n id: string;\n name: string;\n } | null>(null);\n const [passwordModal, setPasswordModal] = useState<PasswordModalType>(null);\n const [totpModal, setTotpModal] = useState<TotpModalType>(null);\n const [passkeyModal, setPasskeyModal] = useState<PasskeyModalType>(null);\n const [showDeleteModal, setShowDeleteModal] = useState(false);\n\n const { data: session } = useSuspenseQuery(getSessionQueryOptions);\n const { data: oauthAccountsData } = useSuspenseQuery(\n oauthAccountsQueryOptions,\n );\n const { data: appConfig } = useSuspenseQuery(appConfigQueryOptions);\n\n const logoutMutation = useMutation({\n ...logoutMutationOptions,\n onSuccess: async () => {\n queryClient.setQueryData(getSessionQueryOptions.queryKey, {\n user: null,\n });\n await tick();\n router.navigate({\n to: '/login',\n });\n },\n onSettled: () => {\n queryClient.invalidateQueries({\n queryKey: getSessionQueryOptions.queryKey,\n });\n },\n });\n\n const unlinkMutation = useMutation({\n ...unlinkOAuthMutationOptions,\n onSuccess: () => {\n queryClient.invalidateQueries({\n queryKey: oauthAccountsQueryOptions.queryKey,\n });\n },\n onSettled: () => {\n setUnlinkingProvider(null);\n },\n });\n\n const handleUnlinkConfirm = async () => {\n if (!unlinkModal) return;\n setUnlinkingProvider(unlinkModal.id);\n await unlinkMutation.mutateAsync(unlinkModal.id);\n };\n\n if (!session.user) {\n return null;\n }\n\n const user = session.user;\n const availableProviders = oauthAccountsData.available_providers;\n const hasLinkedOAuth = availableProviders.some((p) => p.linked);\n const isConfigManaged = user.managed_by === 'config';\n\n const passwordAuthMethod = appConfig.auth.password;\n const passkeyAuthMethod = appConfig.auth.passkey;\n const totpEnabled = passwordAuthMethod.totp.enabled;\n const passkeyEnabled = passkeyAuthMethod.enabled;\n\n const accountDeletionEnabled = appConfig.account_deletion.enabled;\n const retentionPeriod = appConfig.account_deletion.retention;\n\n const retentionDays = (() => {\n const match = retentionPeriod.match(/^(\\d+)([dmy])$/);\n if (!match) return 30;\n const value = parseInt(match[1], 10);\n const unit = match[2];\n switch (unit) {\n case 'd':\n return value;\n case 'm':\n return value * 30;\n case 'y':\n return value * 365;\n default:\n return 30;\n }\n })();\n\n const showPasswordSection = true;\n const showTotpSection = !isConfigManaged && totpEnabled;\n const showPasskeySection = !isConfigManaged && passkeyEnabled;\n const showLinkedAccounts = availableProviders.length > 0;\n const hasSecurityOptions =\n showPasswordSection || showTotpSection || showPasskeySection;\n\n return (\n <AppLayout\n headerActions={\n <TRButton\n appearance=\"ghost\"\n /*\n The visible label collapses below `sm:`, which would leave the\n control unnamed on a phone. `profile.logout` is the same string\n the label renders, so this does not shadow it.\n */\n aria-label={t('profile.logout')}\n data-testid=\"profile-logout\"\n disabled={logoutMutation.isPending}\n loading={logoutMutation.isPending}\n onClick={() => logoutMutation.mutate()}\n type=\"button\"\n uiSize=\"sm\"\n >\n {logoutMutation.isPending ? undefined : (\n <>\n <LogOutIcon aria-hidden className=\"size-tinyrack-lg\" />\n <span className=\"hidden sm:inline\">{t('profile.logout')}</span>\n </>\n )}\n </TRButton>\n }\n >\n <div className=\"flex items-center gap-tinyrack-lg\">\n <InitialAvatar email={user.email} size=\"lg\" />\n <div className=\"flex min-w-0 flex-col gap-tinyrack-3xs\">\n <AuthPageHeader title={t('profile.title')} />\n {/*\n Rendered here rather than as the header's `subtitle`: this node\n carries a testid and needs to truncate, neither of which the\n shared header does, and growing its API for one non-auth caller\n would be the wrong trade.\n */}\n <TRText\n as=\"p\"\n color=\"muted\"\n data-testid=\"profile-user-email\"\n truncate\n variant=\"bodySm\"\n >\n {user.email}\n </TRText>\n </div>\n </div>\n\n {oauthErrorMessage && (\n <Alert icon={CircleAlertIcon} type=\"error\">\n {oauthErrorMessage}\n </Alert>\n )}\n\n {user && <UserInfoSection user={user} />}\n\n {hasSecurityOptions && (\n <TRCard.Root variant=\"outlined\">\n <TRCard.Header className=\"border-tinyrack-border border-b-tinyrack-default px-tinyrack-lg py-tinyrack-md\">\n <TRCard.Title>{t('profile.security.title')}</TRCard.Title>\n <TRCard.Description>\n {t('profile.security.description')}\n </TRCard.Description>\n </TRCard.Header>\n {showTotpSection && user.totp_recovery_codes_missing && (\n <div\n className=\"border-tinyrack-border border-b-tinyrack-default px-tinyrack-lg py-tinyrack-md\"\n data-testid=\"profile-totp-recovery-warning\"\n >\n <Alert icon={CircleAlertIcon} type=\"warning\">\n {t('profile.totp.recoveryCodesMissing')}\n </Alert>\n </div>\n )}\n <TRCard.Content className=\"divide-y divide-tinyrack-border p-0\">\n {showPasswordSection && (\n <PasswordSection\n hasLinkedOAuth={hasLinkedOAuth}\n hasPassword={user.has_password}\n hasSecondFactorOnly={\n user.has_password &&\n !hasLinkedOAuth &&\n (user.totp_registered || user.passkey_count > 0)\n }\n isConfigManaged={isConfigManaged}\n onOpenModal={setPasswordModal}\n />\n )}\n {showTotpSection && (\n <TotpSection\n onOpenModal={setTotpModal}\n recoveryCodesMissing={user.totp_recovery_codes_missing}\n totpEnabled={user.totp_registered}\n />\n )}\n {showPasskeySection && (\n <PasskeySection\n onOpenModal={setPasskeyModal}\n passkeyCount={user.passkey_count}\n />\n )}\n </TRCard.Content>\n </TRCard.Root>\n )}\n\n {showLinkedAccounts && (\n <LinkedAccountsSection\n getAuthorizeUrl={getOAuthAuthorizeUrl}\n onUnlinkRequest={(provider) =>\n setUnlinkModal({\n id: provider.id,\n name: provider.display_name,\n })\n }\n providers={availableProviders}\n unlinkingProvider={unlinkingProvider}\n />\n )}\n\n <DangerZoneSection\n isConfigManaged={isConfigManaged}\n isDeletionEnabled={accountDeletionEnabled}\n onDeleteClick={() => setShowDeleteModal(true)}\n />\n\n <SetPasswordModal\n isOpen={passwordModal === 'set'}\n onClose={() => setPasswordModal(null)}\n />\n <ChangePasswordModal\n isOpen={passwordModal === 'change'}\n onClose={() => setPasswordModal(null)}\n />\n <RemovePasswordModal\n isOpen={passwordModal === 'remove'}\n onClose={() => setPasswordModal(null)}\n />\n\n <SetupTotpModal\n isOpen={totpModal === 'setup'}\n onClose={() => setTotpModal(null)}\n />\n <DisableTotpModal\n isOpen={totpModal === 'disable'}\n onClose={() => setTotpModal(null)}\n />\n <RegenerateTotpRecoveryCodesModal\n isOpen={totpModal === 'regenerate'}\n onClose={() => setTotpModal(null)}\n />\n\n <SetupPasskeyModal\n isOpen={passkeyModal === 'setup'}\n onClose={() => setPasskeyModal(null)}\n />\n <ManagePasskeysModal\n isOpen={passkeyModal === 'manage'}\n onAddNew={() => setPasskeyModal('setup')}\n onClose={() => setPasskeyModal(null)}\n />\n\n <UnlinkOAuthModal\n isOpen={unlinkModal !== null}\n isPending={unlinkMutation.isPending}\n onClose={() => setUnlinkModal(null)}\n onConfirm={handleUnlinkConfirm}\n providerName={unlinkModal?.name ?? ''}\n />\n\n <DeleteAccountModal\n isOpen={showDeleteModal}\n onClose={() => setShowDeleteModal(false)}\n retentionDays={retentionDays}\n />\n </AppLayout>\n );\n}\n"],"x_google_ignoreList":[0,1,2,3,4],"mappings":"m5CAYA,IAAM,GAAQ,EAAiB,QAASA,CAFtC,CAAC,OAAQ,CAAE,EAAG,sDAAuD,IAAK,QAAS,CAAC,CAE9CA,CAAU,ECG5C,GAAW,EAAiB,aAAcC,CAL9C,CAAC,OAAQ,CAAE,EAAG,wBAAyB,IAAK,QAAS,CAAC,EACtD,CAAC,OAAQ,CAAE,EAAG,wBAAyB,IAAK,QAAS,CAAC,EACtD,CAAC,OAAQ,CAAE,GAAI,IAAK,GAAI,KAAM,GAAI,KAAM,GAAI,KAAM,IAAK,QAAS,CAAC,EACjE,CAAC,OAAQ,CAAE,GAAI,IAAK,GAAI,KAAM,GAAI,IAAK,GAAI,KAAM,IAAK,QAAS,CAAC,CAElBA,CAAU,ECIpD,GAAS,EAAiB,SAAUC,CATxC,CACE,OACA,CACE,EAAG,mIACH,IAAK,QACP,CACF,EACA,CAAC,OAAQ,CAAE,EAAG,YAAa,IAAK,QAAS,CAAC,CAEFA,CAAU,ECA9C,GAAW,EAAiB,WAAYC,CAT5C,CACE,OACA,CACE,EAAG,0UACH,IAAK,QACP,CACF,EACA,CAAC,SAAU,CAAE,GAAI,KAAM,GAAI,KAAM,EAAG,IAAK,IAAK,QAAS,CAAC,CAEZA,CAAU,ECNlD,GAAa,EAAiB,aAAc,CAHhD,CAAC,OAAQ,CAAE,MAAO,KAAM,OAAQ,KAAM,EAAG,IAAK,EAAG,IAAK,GAAI,IAAK,GAAI,IAAK,IAAK,QAAS,CAAC,EACvF,CAAC,OAAQ,CAAE,EAAG,aAAc,IAAK,QAAS,CAAC,CAEK,CAAU,mBCD5D,SAAA,GAAA,CAAA,kBAAA,oBAAA,iBAAA,q1CA0DA,CC1CA,SAAgB,EAAY,CAC1B,OACA,SACA,QACA,SACA,OACA,WACmB,CAOnB,IAAI,EAiBJ,MAhBA,CAIE,GAJF,EAAA,EAAA,eAAA,CAAmB,CAAI,EACd,GAIL,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,cAAA,GACA,UAAW,oBACT,EACI,mCACA,4BAEP,CAAA,GAKH,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAU,2EAAf,EACE,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAU,4DAAf,EACE,EAAA,EAAA,IAAA,CAAC,MAAD,CACE,UAAW,wEACT,EAAS,8BAAgC,uCAG1C,CACE,CAAA,GACL,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAU,yDAAf,EACE,EAAA,EAAA,IAAA,CAAC,EAAD,CAAQ,GAAG,MAAM,QAAQ,SAAS,OAAO,kBACtC,CACK,CAAA,GACR,EAAA,EAAA,IAAA,CAAC,EAAD,CAAQ,GAAG,MAAM,MAAM,QAAQ,QAAQ,mBACpC,CACK,CAAA,EACP,CACE,GACF,IACJ,IACC,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAU,+DACZ,CACE,CAAA,CAEJ,GAET,CC7DA,SAAgB,GAAsB,CACpC,YACA,oBACA,kBACA,mBAC6B,CAC7B,GAAM,CAAE,KAAM,EAAe,EAM7B,OAJI,EAAU,SAAW,EAChB,MAIP,EAAA,EAAA,KAAA,CAAC,EAAO,KAAR,CAAa,QAAQ,oBAArB,EACE,EAAA,EAAA,KAAA,CAAC,EAAO,OAAR,CAAe,UAAU,0FAAzB,EACE,EAAA,EAAA,IAAA,CAAC,EAAO,MAAR,CAAA,SAAe,EAAE,8BAA8B,CAAgB,CAAA,GAC/D,EAAA,EAAA,IAAA,CAAC,EAAO,YAAR,CAAA,SACG,EAAE,oCAAoC,CACrB,CAAA,CACP,KACf,EAAA,EAAA,IAAA,CAAC,EAAO,QAAR,CAAgB,UAAU,+CACvB,EAAU,IAAK,IACd,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,QACE,EAAS,QACP,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,WAAW,QACX,OAAO,SACP,QAAS,IAAsB,EAAS,GACxC,aAAc,EAAE,kCAAkC,EAClD,YAAe,EAAgB,CAAQ,EACvC,KAAK,SACL,OAAO,cAEN,IAAsB,EAAS,GAE5B,IAAA,GADA,EAAE,+BAA+B,CAE7B,CAAA,GAEV,EAAA,EAAA,IAAA,CAAC,GAAD,CACE,WAAW,UACX,OAAO,UACP,QACE,EAAA,EAAA,IAAA,CAAC,IAAD,CACE,KAAM,EAAgB,EAAS,GAAI,OAAQ,UAAU,CACtD,CAAA,EAEH,OAAO,cAEN,EAAE,6BAA6B,CACpB,CAAA,EAGlB,OAAQ,EAAS,OAOjB,MACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,UAAU,mBACV,QAAS,EAAS,SAClB,WAAY,EAAS,EACtB,CAAA,EAGH,OACE,EAAS,OACL,EAAE,kCAAkC,EACpC,EAAE,qCAAqC,EAE7C,MAAO,EAAS,YACjB,EAPM,EAAS,EAOf,CACF,CACa,CAAA,CACL,GAEjB,CC5FA,SAAgB,GAAe,CAC7B,eACA,eACsB,CACtB,GAAM,CAAE,KAAM,EAAe,EACvB,EAAc,EAAe,EAEnC,OACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,SACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,WAAW,QACX,OAAO,UACP,YAAe,EAAY,EAAc,SAAW,OAAO,EAC3D,KAAK,SACL,OAAO,cAEQ,EAAd,EAAgB,yBAA8B,qBAAqB,CAC5D,CAAA,EAEZ,OAAQ,EACR,KAAM,EACN,OACE,EACI,EAAE,iCAAkC,CAAE,MAAO,CAAa,CAAC,EAC3D,EAAE,iCAAiC,EAEzC,MAAO,EAAE,uBAAuB,CACjC,CAAA,CAEL,CCzBA,SAAgB,GAAgB,CAC9B,cACA,iBACA,kBACA,sBACA,eACuB,CACvB,GAAM,CAAE,KAAM,EAAe,EAE7B,OACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,QACE,EAAkB,IAAA,GAAY,GAC5B,EAAA,EAAA,KAAA,CAAA,EAAA,SAAA,CAAA,SAAA,EACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,WAAW,QACX,cAAY,0BACZ,OAAO,UACP,YAAe,EAAY,QAAQ,EACnC,KAAK,SACL,OAAO,cAEN,EAAE,yBAAyB,CACpB,CAAA,EACT,GACC,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,WAAW,QACX,cAAY,0BACZ,OAAO,SACP,YAAe,EAAY,QAAQ,EACnC,KAAK,SACL,OAAO,cAEN,EAAE,yBAAyB,CACpB,CAAA,EACR,KACH,CAAC,GAAkB,IAClB,EAAA,EAAA,KAAA,CAAC,EAAU,KAAX,CAAA,SAAA,EACE,EAAA,EAAA,IAAA,CAAC,EAAU,QAAX,CACE,QACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,WAAW,QACX,SAAA,GACA,KAAK,SACL,OAAO,cAEN,EAAE,yBAAyB,CACpB,CAAA,CAEb,CAAA,GACD,EAAA,EAAA,IAAA,CAAC,EAAU,OAAX,CAAA,UACE,EAAA,EAAA,IAAA,CAAC,EAAU,WAAX,CAAA,UACE,EAAA,EAAA,IAAA,CAAC,EAAU,MAAX,CAAiB,UAAU,sLACzB,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAU,4CAAf,EACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,cAAA,GACA,UAAU,4DACX,CAAA,GACD,EAAA,EAAA,IAAA,CAAC,MAAD,CAAA,SAAM,EAAE,uCAAuC,CAAO,CAAA,CACnD,GACU,CAAA,CACG,CAAA,CACN,CAAA,CACJ,CAAA,CAAA,CAElB,CAAA,CAAA,GAEF,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,WAAW,QACX,cAAY,uBACZ,OAAO,UACP,YAAe,EAAY,KAAK,EAChC,KAAK,SACL,OAAO,cAEN,EAAE,sBAAsB,CACjB,CAAA,EAGd,OAAQ,EACR,KAAM,EACN,KACE,GACE,EAAA,EAAA,KAAA,CAAC,EAAD,CACE,GAAG,MACH,UAAU,oCACV,MAAM,QACN,QAAQ,mBAJV,EAME,EAAA,EAAA,IAAA,CAAC,GAAD,CAAc,cAAA,GAAY,UAAU,2BAA6B,CAAA,GACjE,EAAA,EAAA,IAAA,CAAC,OAAD,CAAA,SAAO,EAAE,gCAAgC,CAAQ,CAAA,CAC3C,IACN,IAAA,GAEN,OAEM,EADJ,EACM,8BACA,gCAAgC,EAExC,MAAO,EAAE,wBAAwB,CAClC,CAAA,CAEL,CC1GA,SAAgB,GAAY,CAC1B,cACA,uBACA,eACmB,CACnB,GAAM,CAAE,KAAM,EAAe,EAE7B,OACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,QACE,GACE,EAAA,EAAA,KAAA,CAAA,EAAA,SAAA,CAAA,SAAA,EACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,WAAW,QACX,cAAY,0BACZ,OAAO,UACP,YAAe,EAAY,YAAY,EACvC,KAAK,SACL,OAAO,cAEN,EAAE,yBAAyB,CACpB,CAAA,GACV,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,WAAW,QACX,cAAY,uBACZ,OAAO,SACP,YAAe,EAAY,SAAS,EACpC,KAAK,SACL,OAAO,cAEN,EAAE,sBAAsB,CACjB,CAAA,CACV,CAAA,CAAA,GAEF,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,WAAW,QACX,cAAY,sBACZ,OAAO,UACP,YAAe,EAAY,OAAO,EAClC,KAAK,SACL,OAAO,cAEN,EAAE,qBAAqB,CAChB,CAAA,EAGd,OAAQ,EACR,KAAM,EACN,OAGQ,EAFN,EACI,EACI,2CACA,8BACF,8BAA8B,EAEtC,MAAO,EAAE,oBAAoB,CAC9B,CAAA,CAEL,CC5DA,IAAM,GAAqE,CACzE,MAAO,EACP,QAAS,EACT,KAAM,CACR,EAQA,SAAgB,EAAY,CAAE,UAAS,YAA8B,CACnE,OACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,cAAa,gBAAgB,IAC7B,KAAM,GAAa,GACnB,KAAM,EAEL,UACI,CAAA,CAEX,CClBA,SAAgB,GAAiB,CAC/B,SACA,UACA,eACA,YACA,aACwB,CACxB,GAAM,CAAE,KAAM,EAAe,EACvB,CAAC,EAAO,IAAA,EAAA,EAAA,SAAA,CAAoC,IAAI,EAEhD,MAAoB,CACpB,IACJ,EAAS,IAAI,EACb,EAAQ,EACV,EAYA,OACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,YAAa,EAAE,iDAAkD,CAC/D,SAAU,CACZ,CAAC,EACD,KAAM,GACE,SACR,QAAS,EACT,aAAc,EACd,MAAO,EAAE,0CAA0C,EACnD,QAAQ,wBAER,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAU,wDAAf,EACE,EAAA,EAAA,IAAA,CAAC,EAAD,CAAa,QAAQ,mBAClB,EAAE,6CAA8C,CAC/C,SAAU,CACZ,CAAC,CACU,CAAA,EAEZ,IAAS,EAAA,EAAA,IAAA,CAAC,EAAD,CAAa,QAAQ,iBAAS,CAAmB,CAAA,GAE3D,EAAA,EAAA,KAAA,CAAC,EAAD,CAAA,SAAA,EACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,WAAW,UACX,cAAY,sBACZ,SAAU,EACV,OAAO,UACP,QAAS,EACT,KAAK,SACL,OAAO,cAEN,EAAE,2CAA2C,CACtC,CAAA,GACV,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,cAAY,sBACZ,SAAU,EACV,OAAO,SACP,QAAS,EACT,aAAc,EAAE,kCAAkC,EAClD,QAAS,SAjDe,CAChC,EAAS,IAAI,EACb,GAAI,CACF,MAAM,EAAU,EAChB,EAAY,CACd,MAAQ,CACN,EAAS,EAAE,oCAAoC,CAAC,CAClD,CACF,EA0CU,KAAK,SACL,OAAO,cAEN,EAAE,+BAA+B,CAC1B,CAAA,CACE,CAAA,CAAA,CACX,GACA,CAAA,CAEX,CCtFA,SAAS,GAAQ,CAAE,QAAO,SAA2C,CACnE,OACE,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAU,2FAAf,EACE,EAAA,EAAA,IAAA,CAAC,EAAD,CAAQ,UAAU,WAAW,MAAM,QAAQ,QAAQ,mBAChD,CACK,CAAA,GACR,EAAA,EAAA,IAAA,CAAC,EAAD,CAAQ,SAAA,GAAS,QAAQ,SAAS,OAAO,kBACtC,CACK,CAAA,CACL,GAET,CASA,SAAgB,GAAgB,CAAE,QAA8B,CAC9D,GAAM,CAAE,GAAM,EAAe,EAE7B,OACE,EAAA,EAAA,KAAA,CAAC,EAAO,KAAR,CAAa,QAAQ,oBAArB,EACE,EAAA,EAAA,KAAA,CAAC,EAAO,OAAR,CAAe,UAAU,0FAAzB,EACE,EAAA,EAAA,IAAA,CAAC,EAAO,MAAR,CAAA,SAAe,EAAE,uBAAuB,CAAgB,CAAA,GACxD,EAAA,EAAA,IAAA,CAAC,EAAO,YAAR,CAAA,SACG,EAAE,6BAA6B,CACd,CAAA,CACP,KACf,EAAA,EAAA,KAAA,CAAC,EAAO,QAAR,CAAgB,UAAU,+CAA1B,EACE,EAAA,EAAA,IAAA,CAAC,GAAD,CAAS,MAAO,EAAE,kBAAkB,EAAG,MAAO,EAAK,GAAM,CAAA,GACzD,EAAA,EAAA,IAAA,CAAC,GAAD,CAAS,MAAO,EAAE,qBAAqB,EAAG,MAAO,EAAK,KAAQ,CAAA,CAChD,GACL,GAEjB,CCjBA,SAAgB,GAAU,CAAE,WAAU,iBAAiC,CACrE,OACE,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAU,uDAAf,EAME,EAAA,EAAA,IAAA,CAAC,EAAD,CAAU,CAAA,GACV,EAAA,EAAA,IAAA,CAAC,EAAD,CAAgB,QAAS,EAAe,MAAA,EAAO,CAAA,GAC/C,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAU,qEAKd,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAU,4EACZ,UACE,CAAA,CACD,CAAA,GACN,EAAA,EAAA,IAAA,CAAC,EAAD,CAAc,CAAA,CACX,GAET,CCnCA,IAAa,GAA6B,EAAgB,CACxD,WAAY,KAAO,IAIV,EAAO,MAHI,EAAO,IAAI,KAAK,SAAS,MAAM,CAC/C,KAAM,CACR,CAAC,CACgB,CAErB,CAAC,EASY,GAAgC,EAAgB,CAC3D,WAAY,KAAO,IAIV,EAAO,MAHI,EAAO,IAAI,KAAK,SAAS,KAAK,CAC9C,KAAM,CACR,CAAC,CACgB,CAErB,CAAC,EAUY,GAAgC,EAAgB,CAC3D,WAAY,KAAO,IAIV,EAAO,MAHI,EAAO,IAAI,KAAK,SAAS,QAAQ,CACjD,KAAM,CACR,CAAC,CACgB,CAErB,CAAC,EC1BD,SAAgB,GAAoB,CAClC,SACA,WAC2B,CAC3B,GAAM,CAAE,KAAM,EAAe,EACvB,EAAc,EAAe,EAC7B,CAAE,KAAM,GAAe,EAAiB,CAAqB,EAC7D,EAAiB,EAAW,KAAK,SAAS,OA8B1C,EAAO,EAAQ,CACnB,SAAU,GAAA,EAAA,EAAA,QAAA,KA3BR,EACG,OAAO,CACN,gBAAiB,EAAE,OAAO,CAAC,CAAC,IAAI,EAAG,EAAE,8BAA8B,CAAC,EACpE,YAAa,EACV,OAAO,CAAC,CACR,IACC,EAAe,WACf,EAAE,0BAA2B,CAC3B,MAAO,EAAe,UACxB,CAAC,CACH,CAAC,CACA,IACC,EAAe,WACf,EAAE,0BAA2B,CAC3B,MAAO,EAAe,UACxB,CAAC,CACH,EACF,gBAAiB,EAAE,OAAO,CAC5B,CAAC,CAAC,CACD,OAAQ,GAAS,EAAK,cAAgB,EAAK,gBAAiB,CAC3D,QAAS,EAAE,qCAAqC,EAChD,KAAM,CAAC,iBAAiB,CAC1B,CAAC,EACL,CAAC,EAAe,WAAY,EAAe,WAAY,CAAC,CAIvB,CAAM,EACvC,cAAe,CACb,gBAAiB,GACjB,YAAa,GACb,gBAAiB,EACnB,CACF,CAAC,EAEK,EAAW,EAAY,CAC3B,GAAG,GACH,cAAiB,CACf,EAAY,kBAAkB,CAC5B,SAAU,EAAuB,QACnC,CAAC,EACD,EAAY,CACd,CACF,CAAC,EAEK,MAAoB,CACxB,EAAK,MAAM,EACX,EAAQ,CACV,EAEM,EAAe,EAAK,aAAa,KAAO,IAAS,CACrD,GAAI,CACF,MAAM,EAAS,YAAY,CACzB,iBAAkB,EAAK,gBACvB,aAAc,EAAK,WACrB,CAAC,CACH,OAAS,EAAK,CAEV,aAAe,GACf,EAAI,OAAS,2BAEb,EAAK,SAAS,kBAAmB,CAC/B,QAAS,EAAE,6CAA6C,CAC1D,CAAC,EAED,EAAK,SAAS,OAAQ,CACpB,QAAS,EAAE,oCAAoC,CACjD,CAAC,CAEL,CACF,CAAC,EAED,OACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,YAAa,EAAE,0CAA0C,EACzD,KAAM,EACE,SACR,QAAS,EACT,MAAO,EAAE,oCAAoC,YAE7C,EAAA,EAAA,KAAA,CAAC,OAAD,CACE,UAAU,+CACV,SAAU,WAFZ,EAIE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,MAAO,EAAK,UAAU,OAAO,gBAC7B,YAAY,wCACZ,GAAG,mBACH,MAAO,EAAE,8CAA8C,EACvD,YAAa,EACX,yDACF,EACA,GAAI,EAAK,SAAS,iBAAiB,EACnC,KAAK,UACN,CAAA,GACD,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,MAAO,EAAK,UAAU,OAAO,YAC7B,YAAY,oCACZ,GAAG,sBACH,MAAO,EAAE,0CAA0C,EACnD,YAAa,EAAE,qDAAqD,EACpE,GAAI,EAAK,SAAS,aAAa,EAC/B,KAAK,UACN,CAAA,GACD,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,MAAO,EAAK,UAAU,OAAO,gBAC7B,YAAY,wCACZ,GAAG,0BACH,MAAO,EAAE,8CAA8C,EACvD,YAAa,EACX,yDACF,EACA,GAAI,EAAK,SAAS,iBAAiB,EACnC,KAAK,UACN,CAAA,EACA,EAAK,UAAU,OAAO,OACrB,EAAA,EAAA,IAAA,CAAC,EAAD,CAAa,QAAQ,iBAClB,EAAK,UAAU,OAAO,KAAK,OACjB,CAAA,GAEf,EAAA,EAAA,KAAA,CAAC,EAAD,CAAA,SAAA,EACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,WAAW,UACX,cAAY,yBACZ,SAAU,EAAS,UACnB,OAAO,UACP,QAAS,EACT,KAAK,SACL,OAAO,cAEN,EAAE,qCAAqC,CAChC,CAAA,GACV,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,cAAY,yBACZ,SAAU,EAAS,UACnB,OAAO,UACP,QAAS,EAAS,UAClB,aAAc,EAAE,yCAAyC,EACzD,KAAK,SACL,OAAO,cAEN,EAAS,UACN,IAAA,GACA,EAAE,qCAAqC,CACnC,CAAA,CACE,CAAA,CAAA,CACV,GACD,CAAA,CAEX,CChLA,IAAa,GAA+B,EAAgB,CAC1D,WAAY,SAEH,EAAO,MADI,EAAO,IAAI,KAAK,QAAQ,CACzB,CAErB,CAAC,ECQD,SAAgB,GAAmB,CACjC,SACA,UACA,iBAC0B,CAC1B,GAAM,CAAE,KAAM,EAAe,EACvB,EAAS,EAAU,EACnB,EAAc,EAAe,EAiB7B,EAAO,EAAQ,CACnB,SAAU,GAAA,EAAA,EAAA,QAAA,KAdR,EAAE,OAAO,CACP,aAAc,EACX,OAAO,CAAC,CACR,OACE,GACC,EAAI,YAAY,IAChB,EAAE,gDAAgD,CAAC,CAAC,YAAY,EAClE,CAAE,QAAS,EAAE,iCAAiC,CAAE,CAClD,CACJ,CAAC,EACH,CAAC,CAAC,CAI+B,CAAM,EACvC,cAAe,CAAE,aAAc,EAAG,CACpC,CAAC,EAEK,EAAW,EAAY,CAC3B,GAAG,GACH,UAAW,SAAY,CACrB,EAAY,aAAa,EAAuB,SAAU,CACxD,KAAM,IACR,CAAC,EACD,MAAM,EAAK,EACX,EAAO,SAAS,CAAE,GAAI,QAAS,CAAC,CAClC,CACF,CAAC,EAEK,MAAoB,CACpB,EAAS,YACb,EAAK,MAAM,EACX,EAAQ,EACV,EAEM,EAAe,EAAK,aAAa,SAAY,CACjD,GAAI,CACF,MAAM,EAAS,YAAY,CAC7B,MAAQ,CACN,EAAK,SAAS,OAAQ,CACpB,QAAS,EAAE,6BAA6B,CAC1C,CAAC,CACH,CACF,CAAC,EAED,OACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,KAAM,EACE,SACR,QAAS,EACT,aAAc,EAAS,UACvB,MAAO,EAAE,mCAAmC,EAC5C,QAAQ,wBAER,EAAA,EAAA,KAAA,CAAC,OAAD,CACE,UAAU,+CACV,SAAU,WAFZ,EAIE,EAAA,EAAA,IAAA,CAAC,EAAD,CAAa,QAAQ,kBACnB,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAU,yCAAf,EACE,EAAA,EAAA,IAAA,CAAC,IAAD,CAAA,SACG,EAAE,0CAA2C,CAC5C,KAAM,CACR,CAAC,CACA,CAAA,GACH,EAAA,EAAA,IAAA,CAAC,IAAD,CAAG,UAAU,mDACV,EAAE,qCAAqC,CACvC,CAAA,CACA,GACM,CAAA,GAEb,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,aAAa,MACb,MAAO,EAAK,UAAU,OAAO,aAC7B,YAAY,uBACZ,GAAG,sBACH,MAAO,EAAE,0CAA0C,EACnD,YAAa,EAAE,gDAAgD,EAC/D,GAAI,EAAK,SAAS,cAAc,EAChC,KAAK,MACN,CAAA,EAEA,EAAK,UAAU,OAAO,OACrB,EAAA,EAAA,IAAA,CAAC,EAAD,CAAa,QAAQ,iBAClB,EAAK,UAAU,OAAO,KAAK,OACjB,CAAA,GAGf,EAAA,EAAA,KAAA,CAAC,EAAD,CAAA,SAAA,EACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,WAAW,UACX,cAAY,wBACZ,SAAU,EAAS,UACnB,OAAO,UACP,QAAS,EACT,KAAK,SACL,OAAO,cAEN,EAAE,oCAAoC,CAC/B,CAAA,GACV,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,cAAY,wBACZ,SAAU,EAAS,UACnB,OAAO,SACP,QAAS,EAAS,UAClB,aAAc,EAAE,wCAAwC,EACxD,KAAK,SACL,OAAO,cAEN,EAAS,UACN,IAAA,GACA,EAAE,qCAAqC,CACnC,CAAA,CACE,CAAA,CAAA,CACV,GACD,CAAA,CAEX,CClIA,SAAgB,GAAiB,CAAE,SAAQ,WAAkC,CAC3E,GAAM,CAAE,KAAM,EAAe,EACvB,EAAc,EAAe,EAa7B,EAAO,EAAQ,CACnB,SAAU,GAAA,EAAA,EAAA,QAAA,KAVR,EAAE,OAAO,CACP,KAAM,EACH,OAAO,CAAC,CACR,OAAO,EAAG,EAAE,wBAAwB,CAAC,CAAC,CACtC,MAAM,UAAW,EAAE,wBAAwB,CAAC,CACjD,CAAC,EACH,CAAC,CAAC,CAI+B,CAAM,EACvC,cAAe,CAAE,KAAM,EAAG,CAC5B,CAAC,EAEK,EAAW,EAAY,CAC3B,GAAG,GACH,cAAiB,CACf,EAAY,kBAAkB,CAC5B,SAAU,EAAuB,QACnC,CAAC,EACD,EAAY,CACd,CACF,CAAC,EAEK,MAAoB,CACxB,EAAK,MAAM,EACX,EAAQ,CACV,EAEM,EAAe,EAAK,aAAa,KAAO,IAAS,CACrD,GAAI,CACF,MAAM,EAAS,YAAY,CAAE,KAAM,EAAK,IAAK,CAAC,CAChD,OAAS,EAAO,CACd,GAAI,aAAiB,GACf,EAAM,OAAS,mCAAoC,CACrD,EAAK,SAAS,OAAQ,CACpB,QAAS,EACP,wDACF,CACF,CAAC,EACD,MACF,CAEF,EAAK,SAAS,OAAQ,CACpB,QAAS,EAAE,iCAAiC,CAC9C,CAAC,CACH,CACF,CAAC,EAED,OACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,YAAa,EAAE,uCAAuC,EACtD,KAAM,EACE,SACR,QAAS,EACT,MAAO,EAAE,iCAAiC,EAC1C,QAAQ,wBAER,EAAA,EAAA,KAAA,CAAC,OAAD,CACE,UAAU,+CACV,SAAU,WAFZ,EAIE,EAAA,EAAA,IAAA,CAAC,EAAD,CAAa,QAAQ,mBAClB,EAAE,mCAAmC,CAC3B,CAAA,GAEb,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,aAAa,gBACb,MAAO,EAAK,UAAU,OAAO,KAC7B,YAAY,qBACZ,GAAG,oBACH,UAAU,UACV,MAAO,EAAE,qCAAqC,EAC9C,UAAW,EACX,QAAQ,SACR,YAAY,SACZ,GAAI,EAAK,SAAS,MAAM,EACxB,KAAK,MACN,CAAA,GAED,EAAA,EAAA,KAAA,CAAC,EAAD,CAAA,SAAA,EACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,WAAW,UACX,cAAY,sBACZ,SAAU,EAAS,UACnB,OAAO,UACP,QAAS,EACT,KAAK,SACL,OAAO,cAEN,EAAE,kCAAkC,CAC7B,CAAA,GACV,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,cAAY,sBACZ,SAAU,EAAS,UACnB,OAAO,SACP,QAAS,EAAS,UAClB,aAAc,EAAE,qCAAqC,EACrD,KAAK,SACL,OAAO,cAEN,EAAS,UACN,IAAA,GACA,EAAE,mCAAmC,CACjC,CAAA,CACE,CAAA,CAAA,CACV,GACD,CAAA,CAEX,CCpGA,SAAgB,GAAoB,CAClC,SACA,UACA,YAC2B,CAC3B,GAAM,CAAE,KAAM,EAAe,EACvB,EAAc,EAAe,EAC7B,CAAC,EAAgB,IAAA,EAAA,EAAA,SAAA,CACrB,IACF,EACM,CAAC,EAAY,IAAA,EAAA,EAAA,SAAA,CAAyC,IAAI,EAC1D,CAAC,EAAoB,IAAA,EAAA,EAAA,SAAA,CACzB,IACF,EACM,CAAC,EAAa,IAAA,EAAA,EAAA,SAAA,CAA0C,IAAI,EAE5D,CAAE,KAAM,EAAc,aAAc,EAAS,CACjD,GAAG,EACH,QAAS,CACX,CAAC,EAEK,EAAiB,EAAY,CACjC,GAAG,EACH,cAAiB,CACf,EAAY,kBAAkB,CAAE,SAAU,EAAU,SAAS,CAAE,CAAC,EAChE,EAAY,kBAAkB,CAC5B,SAAU,EAAuB,QACnC,CAAC,CACH,EACA,cAAiB,CACf,EAAc,IAAI,CACpB,CACF,CAAC,EAEK,GAAA,EAAA,EAAA,YAAA,KAAgC,CACpC,EAAkB,IAAI,EACtB,EAAc,IAAI,EAClB,EAAsB,IAAI,EAC1B,EAAe,IAAI,EACnB,EAAQ,CACV,EAAG,CAAC,CAAO,CAAC,EAEN,EAAuB,GAAyB,CACpD,EAAsB,EAAQ,EAAE,EAChC,EAAe,IAAI,CACrB,EAEM,MAA2B,CAC/B,EAAsB,IAAI,EAC1B,EAAe,IAAI,CACrB,EAEM,EAAsB,KAAO,IAAyB,CAC1D,EAAc,EAAQ,EAAE,EACxB,EAAsB,IAAI,EAC1B,GAAI,CACF,MAAM,EAAe,YAAY,CAAE,GAAI,EAAQ,EAAG,CAAC,CACrD,OAAS,EAAO,CACd,GAAI,aAAiB,GACf,EAAM,OAAS,mCAAoC,CACrD,EACE,EAAE,0DAA0D,CAC9D,EACA,MACF,CAEF,EAAe,EAAE,yCAAyC,CAAC,CAC7D,CACF,EAEM,EAAc,GACX,IAAI,KAAK,CAAU,CAAC,CAAC,mBAAmB,IAAA,GAAW,CACxD,KAAM,UACN,MAAO,QACP,IAAK,SACP,CAAC,EAGG,EAAW,GAAc,UAAY,CAAC,EAE5C,OACE,EAAA,EAAA,KAAA,CAAC,EAAD,CACE,KAAM,EACE,SACR,QAAS,EACT,KAAK,KACL,MAAO,EAAE,mCAAmC,WAL9C,EAOE,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAU,wDAAf,CACG,IACC,EAAA,EAAA,IAAA,CAAC,EAAD,CAAa,QAAQ,iBAAS,CAAyB,CAAA,EAGxD,IACC,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAU,+CACb,EAAA,EAAA,IAAA,CAAC,EAAD,CAAW,OAAO,IAAM,CAAA,CACrB,CAAA,EAGN,CAAC,GAAa,EAAS,SAAW,IACjC,EAAA,EAAA,IAAA,CAAC,MAAD,CACE,UAAU,uEACV,cAAY,2BAEZ,EAAA,EAAA,IAAA,CAAC,IAAD,CAAA,SAAI,EAAE,wCAAwC,CAAK,CAAA,CAChD,CAAA,EAGN,CAAC,GAAa,EAAS,OAAS,IAC/B,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAU,yCACZ,EAAS,IAAK,IACb,EAAA,EAAA,IAAA,CAAC,GAAD,CACc,aACZ,mBAAoB,IAAuB,EAAQ,GACnD,WAAY,IAAe,EAAQ,GACnC,UAAW,GAAgB,KAAO,EAAQ,GAE1C,eAAgB,EAChB,iBAAoB,EAAkB,IAAI,EAC1C,oBAAuB,EAAoB,CAAO,EAClD,WAAc,EAAkB,CAAO,EACvC,oBAAuB,EAAoB,CAAO,EACzC,SACV,EAPM,EAAQ,EAOd,CACF,CACE,CAAA,CAEJ,KAEL,EAAA,EAAA,KAAA,CAAC,EAAD,CAAA,SAAA,EACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,WAAW,UACX,cAAY,wBACZ,OAAO,UACP,QAAS,EACT,KAAK,SACL,OAAO,cAEN,EAAE,mCAAmC,CAC9B,CAAA,GACV,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,cAAY,0BACZ,OAAO,UACP,YAAe,CACb,EAAY,EACZ,EAAS,CACX,EACA,KAAK,SACL,OAAO,cAEN,EAAE,oCAAoC,CAC/B,CAAA,CACE,CAAA,CAAA,CACT,GAEX,CAeA,SAAS,GAAY,CACnB,UACA,aACA,YACA,qBACA,SACA,eACA,kBACA,kBACA,iBACA,cACmB,CACnB,GAAM,CAAE,KAAM,EAAe,EACvB,EAAc,EAAe,EAa7B,EAAO,EAAQ,CACnB,SAAU,GAAA,EAAA,EAAA,QAAA,KAVR,EAAE,OAAO,CACP,KAAM,EACH,OAAO,CAAC,CACR,IAAI,EAAG,EAAE,kCAAkC,CAAC,CAAC,CAC7C,IAAI,IAAK,EAAE,6BAA6B,CAAC,CAC9C,CAAC,EACH,CAAC,CAAC,CAI+B,CAAM,EACvC,cAAe,CAAE,KAAM,EAAQ,MAAQ,EAAG,CAC5C,CAAC,EAEK,EAAiB,EAAY,CACjC,GAAG,EACH,cAAiB,CACf,EAAY,kBAAkB,CAAE,SAAU,EAAU,SAAS,CAAE,CAAC,EAChE,EAAa,CACf,CACF,CAAC,EAEK,EAAe,EAAK,aAAa,KAAO,IAAS,CACrD,GAAI,CACF,MAAM,EAAe,YAAY,CAAE,GAAI,EAAQ,GAAI,KAAM,EAAK,IAAK,CAAC,CACtE,MAAQ,CACN,EAAK,SAAS,OAAQ,CACpB,QAAS,EAAE,yCAAyC,CACtD,CAAC,CACH,CACF,CAAC,EA6ED,OA3EI,GAEA,EAAA,EAAA,KAAA,CAAC,OAAD,CACE,UAAU,4FACV,SAAU,WAFZ,EAIE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,cAAY,uBACZ,MAAO,EAAK,UAAU,OAAO,KAC7B,YAAY,uBACZ,UAAA,GACA,MAAO,EAAE,uCAAuC,EAChD,YAAa,EAAE,6CAA6C,EAC5D,GAAI,EAAK,SAAS,MAAM,EACxB,KAAK,MACN,CAAA,GACD,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAU,4CAAf,EACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,WAAW,QACX,SAAU,EAAe,UACzB,YAAe,CACb,EAAK,MAAM,EACX,EAAa,CACf,EACA,KAAK,SACL,OAAO,cAEN,EAAE,wCAAwC,CACnC,CAAA,GACV,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,SAAU,EAAe,UACzB,OAAO,UACP,KAAK,SACL,OAAO,cAEN,EAAe,UACZ,EAAE,oCAAoC,EACtC,EAAE,kCAAkC,CAChC,CAAA,CACP,GACD,IAIN,GAEA,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAU,+KAAf,EACE,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAU,6CAAf,EACE,EAAA,EAAA,IAAA,CAAC,EAAD,CAAY,UAAU,kBAAoB,CAAA,GAC1C,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAU,4BACb,EAAE,iDAAiD,CAChD,CAAA,CACH,KACL,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAU,gCAAf,EACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,WAAW,QACX,QAAS,EACT,KAAK,SACL,OAAO,cAEN,EAAE,wCAAwC,CACnC,CAAA,GACV,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,OAAO,SACP,QAAS,EACT,KAAK,SACL,OAAO,cAEN,EAAE,oCAAoC,CAC/B,CAAA,CACP,GACF,KAKP,EAAA,EAAA,KAAA,CAAC,MAAD,CACE,UAAU,gGACV,cAAY,wBAFd,EAIE,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAU,6CAAf,CACG,EAAQ,cAAgB,eACvB,EAAA,EAAA,IAAA,CAAC,GAAD,CAAW,UAAU,mDAAqD,CAAA,GAE1E,EAAA,EAAA,IAAA,CAAC,GAAD,CAAgB,UAAU,mDAAqD,CAAA,GAEjF,EAAA,EAAA,KAAA,CAAC,MAAD,CAAA,SAAA,EACE,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAU,iDACZ,EAAQ,MAAQ,EAAE,4CAA4C,CAC5D,CAAA,GACL,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAU,uFAAf,EACE,EAAA,EAAA,IAAA,CAAC,OAAD,CAAA,SACG,EAAE,wCAAyC,CAC1C,KAAM,EAAW,EAAQ,UAAU,CACrC,CAAC,CACG,CAAA,EACL,EAAQ,YACP,EAAA,EAAA,IAAA,CAAC,GAAD,CAAS,OAAO,KAAK,QAAQ,mBAC1B,EAAE,sCAAsC,CAClC,CAAA,CAER,GACF,CAAA,CAAA,CACF,KACL,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAU,gCAAf,EACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,WAAW,QACX,aAAY,EAAE,oCAAoC,EAClD,QAAS,EACT,KAAK,SACL,OAAO,eAEP,EAAA,EAAA,IAAA,CAAC,GAAD,CAAY,UAAU,kBAAoB,CAAA,CAClC,CAAA,GACV,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,WAAW,QACX,aAAY,EAAE,oCAAoC,EAClD,SAAU,EACV,OAAO,SACP,QAAS,EACT,QAAS,EACT,KAAK,SACL,OAAO,cAEN,EAAa,IAAA,IAAY,EAAA,EAAA,IAAA,CAAC,EAAD,CAAY,UAAU,kBAAoB,CAAA,CAC5D,CAAA,CACP,GACF,GAET,CClWA,SAAgB,GAAiC,CAC/C,SACA,WACwC,CACxC,GAAM,CAAE,KAAM,EAAe,EACvB,EAAS,EAAU,EACnB,EAAc,EAAe,EAC7B,CAAE,KAAM,GAAc,EAAiB,CAAqB,EAC5D,CAAC,EAAM,IAAA,EAAA,EAAA,SAAA,CAAoC,QAAQ,EACnD,CAAC,EAAU,IAAA,EAAA,EAAA,SAAA,CACsC,IAAI,EACrD,CAAC,EAAc,IAAA,EAAA,EAAA,SAAA,CAA2C,IAAI,EAE9D,EAAW,EAAY,CAC3B,GAAG,GACH,UAAW,KAAO,IAAS,CACzB,EAAY,CAAI,EAChB,EAAQ,UAAU,EAClB,MAAM,EAAY,kBAAkB,CAClC,SAAU,EAAuB,QACnC,CAAC,CACH,CACF,CAAC,EAEK,GAAA,EAAA,EAAA,YAAA,KAA0B,CAC9B,EAAQ,QAAQ,EAChB,EAAY,IAAI,EAChB,EAAgB,IAAI,EACpB,EAAS,MAAM,CACjB,EAAG,CAAC,CAAQ,CAAC,EAEP,GAAA,EAAA,EAAA,YAAA,KAAgC,CACpC,EAAM,EACN,EAAQ,CACV,EAAG,CAAC,EAAS,CAAK,CAAC,EAEb,GAAA,EAAA,EAAA,YAAA,CACJ,KAAO,IAAiB,CACtB,EAAgB,IAAI,EAEpB,GAAI,CACF,MAAM,EAAS,YAAY,CAAE,MAAK,CAAC,CACrC,OAAS,EAAO,CACd,GAAI,aAAiB,EAAc,CACjC,GAAI,EAAM,OAAS,oBACjB,MAAM,EAGR,GAAI,EAAM,OAAS,eAAgB,CACjC,EAAY,aAAa,EAAuB,SAAU,CACxD,KAAM,IACR,CAAC,EACD,MAAM,EAAK,EACX,EAAO,SAAS,CAAE,GAAI,QAAS,CAAC,EAChC,MACF,CAEA,GACE,EAAM,OAAS,oBACf,EAAM,OAAS,mBACf,CACA,MAAM,EAAY,kBAAkB,CAClC,SAAU,EAAuB,QACnC,CAAC,EACD,EAAY,EACZ,MACF,CACF,CAEA,EAAgB,EAAE,8CAA8C,CAAC,CACnE,CACF,EACA,CAAC,EAAa,EAAU,EAAa,EAAQ,CAAC,CAChD,EAMA,OAJK,EAAU,KAAK,SAAS,KAAK,SAKhC,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,YAEM,EADJ,IAAS,SACH,2CACA,kDAAkD,EAE1D,KAAM,EACE,SACR,QAAS,EACT,aAAc,IAAS,WACvB,KAAK,KACL,MAEM,EADJ,IAAS,SACH,qCACA,4CAA4C,WAGnD,IAAS,UACR,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAU,0BAAf,CACG,IACC,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAU,2BACb,EAAA,EAAA,IAAA,CAAC,EAAD,CAAa,QAAQ,iBAAS,CAA0B,CAAA,CACrD,CAAA,GAEP,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,eAAgB,EAAE,oCAAoC,EACtD,UAAW,EAAS,UACpB,SAAU,EACV,YAAa,EAAE,yCAAyC,EACxD,YAAa,EAAE,qCAAqC,CACrD,CAAA,CACE,KAEL,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAU,2BACb,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,UAAW,EACX,cAAe,GAAU,gBAAkB,CAAC,CAC7C,CAAA,CACE,CAAA,CAEF,CAAA,EA5CA,IA8CX,CCnIA,SAAgB,GAAoB,CAClC,SACA,WAC2B,CAC3B,GAAM,CAAE,KAAM,EAAe,EACvB,EAAc,EAAe,EAU7B,EAAO,EAAQ,CACnB,SAAU,GAAA,EAAA,EAAA,QAAA,KAPR,EAAE,OAAO,CACP,gBAAiB,EAAE,OAAO,CAAC,CAAC,IAAI,EAAG,EAAE,8BAA8B,CAAC,CACtE,CAAC,EACH,CAAC,CAAC,CAI+B,CAAM,EACvC,cAAe,CAAE,gBAAiB,EAAG,CACvC,CAAC,EAEK,EAAW,EAAY,CAC3B,GAAG,GACH,cAAiB,CACf,EAAY,kBAAkB,CAC5B,SAAU,EAAuB,QACnC,CAAC,EACD,EAAY,CACd,CACF,CAAC,EAEK,MAAoB,CACxB,EAAK,MAAM,EACX,EAAQ,CACV,EAEM,EAAe,EAAK,aAAa,KAAO,IAAS,CACrD,GAAI,CACF,MAAM,EAAS,YAAY,CACzB,iBAAkB,EAAK,eACzB,CAAC,CACH,OAAS,EAAK,CACZ,GAAI,aAAe,EAAc,CAC/B,GAAI,EAAI,OAAS,2BAA4B,CAC3C,EAAK,SAAS,kBAAmB,CAC/B,QAAS,EAAE,6CAA6C,CAC1D,CAAC,EACD,MACF,CACA,GAAI,EAAI,OAAS,iCAAkC,CACjD,EAAK,SAAS,OAAQ,CACpB,QAAS,EAAE,sCAAsC,CACnD,CAAC,EACD,MACF,CACA,GAAI,EAAI,OAAS,iDAAkD,CACjE,EAAK,SAAS,OAAQ,CACpB,QAAS,EAAE,6CAA6C,CAC1D,CAAC,EACD,MACF,CACF,CACA,EAAK,SAAS,OAAQ,CACpB,QAAS,EAAE,oCAAoC,CACjD,CAAC,CACH,CACF,CAAC,EAED,OACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,YAAa,EAAE,0CAA0C,EACzD,KAAM,EACE,SACR,QAAS,EACT,MAAO,EAAE,oCAAoC,EAC7C,QAAQ,wBAER,EAAA,EAAA,KAAA,CAAC,OAAD,CACE,UAAU,+CACV,SAAU,WAFZ,EAIE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,MAAO,EAAK,UAAU,OAAO,gBAC7B,YAAY,wBACZ,GAAG,0BACH,MAAO,EAAE,8CAA8C,EACvD,YAAa,EACX,yDACF,EACA,GAAI,EAAK,SAAS,iBAAiB,EACnC,KAAK,UACN,CAAA,EACA,EAAK,UAAU,OAAO,OACrB,EAAA,EAAA,IAAA,CAAC,EAAD,CAAa,QAAQ,iBAClB,EAAK,UAAU,OAAO,KAAK,OACjB,CAAA,GAEf,EAAA,EAAA,KAAA,CAAC,EAAD,CAAA,SAAA,EACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,WAAW,UACX,cAAY,yBACZ,SAAU,EAAS,UACnB,OAAO,UACP,QAAS,EACT,KAAK,SACL,OAAO,cAEN,EAAE,qCAAqC,CAChC,CAAA,GACV,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,cAAY,yBACZ,SAAU,EAAS,UACnB,OAAO,SACP,QAAS,EAAS,UAClB,aAAc,EAAE,yCAAyC,EACzD,KAAK,SACL,OAAO,cAEN,EAAS,UACN,IAAA,GACA,EAAE,qCAAqC,CACnC,CAAA,CACE,CAAA,CAAA,CACV,GACD,CAAA,CAEX,CCzHA,SAAgB,GAAiB,CAAE,SAAQ,WAAkC,CAC3E,GAAM,CAAE,KAAM,EAAe,EACvB,EAAc,EAAe,EAC7B,CAAE,KAAM,GAAe,EAAiB,CAAqB,EAC7D,EAAiB,EAAW,KAAK,SAAS,OA6B1C,EAAO,EAAQ,CACnB,SAAU,GAAA,EAAA,EAAA,QAAA,KA1BR,EACG,OAAO,CACN,SAAU,EACP,OAAO,CAAC,CACR,IACC,EAAe,WACf,EAAE,0BAA2B,CAC3B,MAAO,EAAe,UACxB,CAAC,CACH,CAAC,CACA,IACC,EAAe,WACf,EAAE,0BAA2B,CAC3B,MAAO,EAAe,UACxB,CAAC,CACH,EACF,gBAAiB,EAAE,OAAO,CAC5B,CAAC,CAAC,CACD,OAAQ,GAAS,EAAK,WAAa,EAAK,gBAAiB,CACxD,QAAS,EAAE,qCAAqC,EAChD,KAAM,CAAC,iBAAiB,CAC1B,CAAC,EACL,CAAC,EAAe,WAAY,EAAe,WAAY,CAAC,CAIvB,CAAM,EACvC,cAAe,CAAE,SAAU,GAAI,gBAAiB,EAAG,CACrD,CAAC,EAEK,EAAW,EAAY,CAC3B,GAAG,GACH,cAAiB,CACf,EAAY,kBAAkB,CAC5B,SAAU,EAAuB,QACnC,CAAC,EACD,EAAY,CACd,CACF,CAAC,EAEK,MAAoB,CACxB,EAAK,MAAM,EACX,EAAQ,CACV,EAEM,EAAe,EAAK,aAAa,KAAO,IAAS,CACrD,GAAI,CACF,MAAM,EAAS,YAAY,CAAE,SAAU,EAAK,QAAS,CAAC,CACxD,MAAgB,CAEd,EAAK,SAAS,OAAQ,CACpB,QAAS,EAAE,iCAAiC,CAC9C,CAAC,CACH,CACF,CAAC,EAED,OACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,YAAa,EAAE,uCAAuC,EACtD,KAAM,EACE,SACR,QAAS,EACT,MAAO,EAAE,iCAAiC,YAE1C,EAAA,EAAA,KAAA,CAAC,OAAD,CACE,UAAU,+CACV,SAAU,WAFZ,EAIE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,MAAO,EAAK,UAAU,OAAO,SAC7B,YAAY,8BACZ,GAAG,eACH,MAAO,EAAE,uCAAuC,EAChD,YAAa,EAAE,kDAAkD,EACjE,GAAI,EAAK,SAAS,UAAU,EAC5B,KAAK,UACN,CAAA,GACD,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,MAAO,EAAK,UAAU,OAAO,gBAC7B,YAAY,qCACZ,GAAG,mBACH,MAAO,EAAE,2CAA2C,EACpD,YAAa,EACX,sDACF,EACA,GAAI,EAAK,SAAS,iBAAiB,EACnC,KAAK,UACN,CAAA,EACA,EAAK,UAAU,OAAO,OACrB,EAAA,EAAA,IAAA,CAAC,EAAD,CAAa,QAAQ,iBAClB,EAAK,UAAU,OAAO,KAAK,OACjB,CAAA,GAEf,EAAA,EAAA,KAAA,CAAC,EAAD,CAAA,SAAA,EACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,WAAW,UACX,cAAY,sBACZ,SAAU,EAAS,UACnB,OAAO,UACP,QAAS,EACT,KAAK,SACL,OAAO,cAEN,EAAE,kCAAkC,CAC7B,CAAA,GACV,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,cAAY,sBACZ,SAAU,EAAS,UACnB,OAAO,UACP,QAAS,EAAS,UAClB,aAAc,EAAE,sCAAsC,EACtD,KAAK,SACL,OAAO,cAEN,EAAS,UACN,IAAA,GACA,EAAE,kCAAkC,CAChC,CAAA,CACE,CAAA,CAAA,CACV,GACD,CAAA,CAEX,CChIA,SAAgB,GAAkB,CAChC,SACA,UACA,aAAa,GACb,kBAAkB,GAClB,kBACyB,CACzB,GAAM,CAAE,KAAM,EAAe,EACvB,EAAc,EAAe,EAC7B,CAAC,EAAM,IAAA,EAAA,EAAA,SAAA,CAA+B,MAAM,EAC5C,CAAC,EAAc,IAAA,EAAA,EAAA,SAAA,CAA2C,IAAI,EAc9D,EAAO,EAAQ,CACnB,SAAU,GAAA,EAAA,EAAA,QAAA,KAXR,EAAE,OAAO,CACP,KAAM,EACH,OAAO,CAAC,CACR,IAAI,IAAK,EAAE,6BAA6B,CAAC,CAAC,CAC1C,SAAS,CAAC,CACV,UAAW,GAAQ,GAAK,KAAK,GAAK,IAAA,EAAS,CAChD,CAAC,EACH,CAAC,CAAC,CAI+B,CAAM,EACvC,cAAe,CAAE,KAAM,EAAG,CAC5B,CAAC,EAEK,EAAmB,EAAY,CACnC,GAAG,EACH,cAAiB,CACf,EAAY,kBAAkB,CAAE,SAAU,EAAU,SAAS,CAAE,CAAC,EAChE,EAAY,kBAAkB,CAC5B,SAAU,EAAuB,QACnC,CAAC,EACD,EAAY,CACd,EACA,QAAU,GAAU,CAClB,EAAQ,MAAM,EACV,EAAM,OAAS,kBACjB,EAAgB,EAAE,sCAAsC,CAAC,EAEzD,EAAgB,EAAE,kCAAkC,CAAC,CAEzD,CACF,CAAC,EAEK,GAAA,EAAA,EAAA,YAAA,KAAgC,CACpC,EAAK,MAAM,EACX,EAAQ,MAAM,EACd,EAAgB,IAAI,EACpB,EAAQ,CACV,EAAG,CAAC,EAAM,CAAO,CAAC,EAEZ,EAAiB,EAAK,aAAa,KAAO,IAAS,CACvD,EAAQ,UAAU,EAClB,EAAgB,IAAI,EACpB,GAAI,CACF,MAAM,EAAiB,YAAY,CAAE,KAAM,EAAK,IAAK,CAAC,CACxD,MAAQ,CAER,CACF,CAAC,EAEK,EAAY,EAAiB,UAEnC,OACE,EAAA,EAAA,KAAA,CAAC,EAAD,CACE,YACE,EACI,EAAE,gDAAgD,EAClD,IAAA,GAEN,KAAM,EACE,SACR,QAAS,EACT,aAAc,EACd,KAAK,KACL,MAAO,EAAE,kCAAkC,WAX7C,CAaG,IAAS,SACR,EAAA,EAAA,KAAA,CAAC,OAAD,CACE,UAAU,+CACV,SAAU,WAFZ,EAIE,EAAA,EAAA,IAAA,CAAC,IAAD,CAAG,UAAU,qDACV,EAAE,wCAAwC,CAC1C,CAAA,EAEF,IACC,EAAA,EAAA,IAAA,CAAC,EAAD,CAAa,QAAQ,iBAAS,CAA0B,CAAA,GAG1D,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,MAAO,EAAK,UAAU,OAAO,KAC7B,YAAY,sBACZ,KAAM,EAAE,qCAAqC,EAC7C,GAAG,eACH,MAAO,EAAE,sCAAsC,EAC/C,YAAa,EAAE,4CAA4C,EAC3D,GAAI,EAAK,SAAS,MAAM,EACxB,KAAK,MACN,CAAA,GAED,EAAA,EAAA,KAAA,CAAC,EAAD,CAAA,SAAA,EACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,WAAW,UACX,cAAY,uBACZ,OAAO,UACP,QAAS,EACT,KAAK,SACL,OAAO,cAEN,EAAE,mCAAmC,CAC9B,CAAA,GACV,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,cAAY,yBACZ,OAAO,UACP,KAAK,SACL,OAAO,cAEN,EAAE,qCAAqC,CAChC,CAAA,CACE,CAAA,CAAA,EACb,GAAmB,IAClB,EAAA,EAAA,IAAA,CAAC,IAAD,CAAG,UAAU,wDACX,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,WAAW,QACX,OAAO,UACP,QAAS,EACT,KAAK,SACL,OAAO,cAEN,EAAE,yCAAyC,CACpC,CAAA,CACT,CAAA,CAED,IAGP,IAAS,aACR,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAU,0BAAf,EACE,EAAA,EAAA,KAAA,CAAC,MAAD,CACE,UAAU,4DACV,cAAY,iCAFd,EAIE,EAAA,EAAA,IAAA,CAAC,EAAD,CAAW,OAAO,IAAM,CAAA,GACxB,EAAA,EAAA,IAAA,CAAC,IAAD,CAAG,UAAU,iEACV,EAAE,6CAA6C,CAC/C,CAAA,CACA,KAEL,EAAA,EAAA,IAAA,CAAC,EAAD,CAAA,UACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,WAAW,UACX,cAAY,uBACZ,SAAU,EACV,OAAO,UACP,QAAS,EACT,KAAK,SACL,OAAO,cAEN,EAAE,mCAAmC,CAC9B,CAAA,CACE,CAAA,CACX,GAEF,GAEX,CChLA,SAAgB,GAAe,CAC7B,SACA,UACA,aAAa,GACb,qBAAqB,GACrB,qBACsB,CACtB,GAAM,CAAE,KAAM,EAAe,EAEvB,CACJ,OACA,YACA,gBACA,iBACA,kBACA,mBACA,aACA,SACA,aACA,SACA,uBACA,SACE,EAAa,CACf,UAAW,GACX,qBAAwB,CACtB,EAAM,EACN,EAAQ,CACV,CACF,CAAC,EAEK,GAAA,EAAA,EAAA,YAAA,KAAgC,CACpC,EAAM,EACN,EAAQ,CACV,EAAG,CAAC,EAAS,CAAK,CAAC,EAEb,GAAA,EAAA,EAAA,YAAA,KAAoC,CACpC,CAAC,GAAa,CAAC,GACjB,EAAW,CAEf,EAAG,CAAC,EAAW,EAAgB,CAAU,CAAC,EAEtC,GAAU,CAAC,GAAa,CAAC,GAAkB,IAAS,SACtD,EAAgB,EAGlB,IAAM,GAAA,EAAA,EAAA,YAAA,CACJ,KAAO,IAAiB,CACtB,MAAM,EAAO,CAAI,CACnB,EACA,CAAC,CAAM,CACT,EAEM,EAEA,EADJ,IAAS,WACH,gCACA,+BAA+B,EAEvC,OACE,EAAA,EAAA,KAAA,CAAC,EAAD,CACE,YACE,EACI,EAAE,6CAA6C,EAC/C,IAAA,GAEN,KAAM,EACE,SACR,QAAS,EACT,aAAc,GAAc,IAAS,WACrC,KAAK,KACL,MAAO,WAXT,CAaG,IACC,EAAA,EAAA,IAAA,CAAC,MAAD,CACE,UAAU,qCACV,cAAY,+BAEZ,EAAA,EAAA,IAAA,CAAC,EAAD,CAAW,OAAO,IAAM,CAAA,CACrB,CAAA,EAGN,IAAS,UACR,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAU,wDAAf,EACE,EAAA,EAAA,IAAA,CAAC,EAAD,CAAa,QAAQ,iBAClB,EAAE,oCAAoC,CAC5B,CAAA,GACb,EAAA,EAAA,KAAA,CAAC,EAAD,CAAA,SAAA,EACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,WAAW,UACX,OAAO,UACP,QAAS,EACT,KAAK,SACL,OAAO,cAEN,EAAE,gCAAgC,CAC3B,CAAA,GACV,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,OAAO,UACP,QAAS,EACT,KAAK,SACL,OAAO,cAEN,EAAE,+BAA+B,CAC1B,CAAA,CACE,CAAA,CAAA,CACX,IAGN,GAAa,IAAS,OACrB,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAU,2BACb,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,mBACE,EAAA,EAAA,KAAA,CAAA,EAAA,SAAA,CAAA,SAAA,EACE,EAAA,EAAA,IAAA,CAAC,EAAD,CAAA,UACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,WAAW,UACX,OAAO,UACP,QAAS,EACT,KAAK,SACL,OAAO,cAEN,EAAE,gCAAgC,CAC3B,CAAA,CACE,CAAA,EACb,GAAsB,IACrB,EAAA,EAAA,IAAA,CAAC,IAAD,CAAG,UAAU,wDACX,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,WAAW,QACX,OAAO,UACP,QAAS,EACT,KAAK,SACL,OAAO,cAEN,EAAE,yCAAyC,CACpC,CAAA,CACT,CAAA,CAEL,CAAA,CAAA,EAEJ,OAAQ,EACG,WACZ,CAAA,CACE,CAAA,EAGN,GAAa,IAAS,WACrB,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAU,2BACb,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,UAAW,EACX,OAAQ,EACR,SAAU,CACX,CAAA,CACE,CAAA,EAGN,IAAS,YAAc,EAAc,OAAS,IAC7C,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAU,2BACb,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,UAAW,EACX,UAAW,EACI,eAChB,CAAA,CACE,CAAA,CAEF,GAEX,CC/HA,IAAM0C,GAA+C,CACnDE,cAAe,2BACfC,wBAAyB,qCACzBC,aAAc,yBAChB,EAiCA,SAASE,IAAU,CACjB,GAAM,CAAEC,KAAMtC,EAAe,EACvBuC,EAAS9C,EAAU,EACnB+C,EAAcjD,EAAe,EAG7BoD,EAFSP,GAAMM,UAEFD,CAAAA,CAAOG,YACpBC,EAAoBF,EACtBL,EAAEP,GAAqBY,IAAe,oBAAoB,EAC1DG,IAAAA,GACE,CAACC,EAAmBC,IAAAA,EAAAA,EAAAA,SAAAA,CACxB,IACF,EACM,CAACC,EAAaC,IAAAA,EAAAA,EAAAA,SAAAA,CAGV,IAAI,EACR,CAACG,EAAeC,IAAAA,EAAAA,EAAAA,SAAAA,CAAgD,IAAI,EACpE,CAACC,EAAWC,IAAAA,EAAAA,EAAAA,SAAAA,CAAwC,IAAI,EACxD,CAACC,EAAcC,IAAAA,EAAAA,EAAAA,SAAAA,CAA8C,IAAI,EACjE,CAACC,EAAiBC,IAAAA,EAAAA,EAAAA,SAAAA,CAA+B,EAAK,EAEtD,CAAEC,KAAMC,GAAYtE,EAAiBmC,CAAsB,EAC3D,CAAEkC,KAAME,GAAsBvE,EAClCiC,CACF,EACM,CAAEoC,KAAMG,GAAcxE,EAAiB8B,CAAqB,EAE5D2C,EAAiB3E,EAAY,CACjC,GAAGiC,GACH2C,UAAW,SAAY,CACrB1B,EAAY2B,aAAaxC,EAAuByC,SAAU,CACxDC,KAAM,IACR,CAAC,EACD,MAAMhD,EAAK,EACXkB,EAAO+B,SAAS,CACdC,GAAI,QACN,CAAC,CACH,EACAC,cAAiB,CACfhC,EAAYiC,kBAAkB,CAC5BL,SAAUzC,EAAuByC,QACnC,CAAC,CACH,CACF,CAAC,EAEKM,EAAiBpF,EAAY,CACjC,GAAGoC,GACHwC,cAAiB,CACf1B,EAAYiC,kBAAkB,CAC5BL,SAAU3C,EAA0B2C,QACtC,CAAC,CACH,EACAI,cAAiB,CACfxB,EAAqB,IAAI,CAC3B,CACF,CAAC,EAEK2B,EAAsB,SAAY,CACjC1B,IACLD,EAAqBC,EAAYE,EAAE,EACnC,MAAMuB,EAAeE,YAAY3B,EAAYE,EAAE,EACjD,EAEA,GAAI,CAACW,EAAQO,KACX,OAAO,KAGT,IAAMA,EAAOP,EAAQO,KACfQ,EAAqBd,EAAkBe,oBACvCC,EAAiBF,EAAmBG,KAAMC,GAAMA,EAAEC,MAAM,EACxDC,EAAkBd,EAAKe,aAAe,SAEtCC,EAAqBrB,EAAUsB,KAAKC,SACpCC,GAAoBxB,EAAUsB,KAAKG,QACnCC,GAAcL,EAAmBM,KAAKC,QACtCC,EAAiBL,GAAkBI,QAEnCE,EAAyB9B,EAAU+B,iBAAiBH,QACpDI,EAAkBhC,EAAU+B,iBAAiBE,UAE7CC,OAAuB,CAC3B,IAAMC,EAAQH,EAAgBG,MAAM,gBAAgB,EACpD,GAAI,CAACA,EAAO,MAAO,IACnB,IAAMC,EAAQC,SAASF,EAAM,GAAI,EAAE,EAEnC,OADaA,EAAM,GACnB,CACE,IAAK,IACH,OAAOC,EACT,IAAK,IACH,OAAOA,EAAQ,GACjB,IAAK,IACH,OAAOA,EAAQ,IACjB,QACE,MAAO,GACX,CACF,EAAA,CAAG,EAGGI,EAAkB,CAACrB,GAAmBO,GACtCe,EAAqB,CAACtB,GAAmBU,EACzCa,EAAqB7B,EAAmB8B,OAAS,EAIvD,OACE,EAAA,EAAA,KAAA,CAAC,GAAD,CACE,eACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,WAAW,QAMX,aAAYrE,EAAE,gBAAgB,EAC9B,cAAY,iBACZ,SAAU2B,EAAe4C,UACzB,QAAS5C,EAAe4C,UACxB,YAAe5C,EAAe6C,OAAO,EACrC,KAAK,SACL,OAAO,cAEN7C,EAAe4C,UAAY/D,IAAAA,IAC1B,EAAA,EAAA,KAAA,CAAA,EAAA,SAAA,CAAA,SAAA,EACE,EAAA,EAAA,IAAA,CAAC,GAAD,CAAY,cAAA,GAAY,UAAU,kBAAkB,CAAA,GACpD,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAU,4BAAoBR,EAAE,gBAAgB,CAAQ,CAAA,CAChE,CAAA,CAAA,CAEM,CAAA,WAvBd,EA0BE,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAU,6CAAf,EACE,EAAA,EAAA,IAAA,CAAC,GAAD,CAAe,MAAO+B,EAAK0C,MAAO,KAAK,IAAI,CAAA,GAC3C,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAU,kDAAf,EACE,EAAA,EAAA,IAAA,CAAC,GAAD,CAAgB,MAAOzE,EAAE,eAAe,CAAE,CAAA,GAO1C,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,GAAG,IACH,MAAM,QACN,cAAY,qBACZ,SAAA,GACA,QAAQ,kBAEP+B,EAAK0C,KACA,CAAA,CACL,GACF,IAEJlE,IACC,EAAA,EAAA,IAAA,CAAC,EAAD,CAAO,KAAMhD,EAAiB,KAAK,iBAChCgD,CACI,CAAA,EAGRwB,IAAQ,EAAA,EAAA,IAAA,CAAC,GAAD,CAAuBA,MAAK,CAAA,GAGnC,EAAA,EAAA,KAAA,CAAC,EAAO,KAAR,CAAa,QAAQ,oBAArB,EACE,EAAA,EAAA,KAAA,CAAC,EAAO,OAAR,CAAe,UAAU,0FAAzB,EACE,EAAA,EAAA,IAAA,CAAC,EAAO,MAAR,CAAA,SAAe/B,EAAE,wBAAwB,CAAgB,CAAA,GACzD,EAAA,EAAA,IAAA,CAAC,EAAO,YAAR,CAAA,SACGA,EAAE,8BAA8B,CACf,CAAA,CACP,IACdkE,GAAmBnC,EAAK2C,8BACvB,EAAA,EAAA,IAAA,CAAC,MAAD,CACE,UAAU,iFACV,cAAY,0CAEZ,EAAA,EAAA,IAAA,CAAC,EAAD,CAAO,KAAMnH,EAAiB,KAAK,mBAChCyC,EAAE,mCAAmC,CACjC,CAAA,CACJ,CAAA,GAEP,EAAA,EAAA,KAAA,CAAC,EAAO,QAAR,CAAgB,UAAU,+CAA1B,EAEI,EAAA,EAAA,IAAA,CAAC,GAAD,CACkByC,iBAChB,YAAaV,EAAK4C,aAClB,oBACE5C,EAAK4C,cACL,CAAClC,IACAV,EAAK6C,iBAAmB7C,EAAK8C,cAAgB,GAE/BhC,kBACjB,YAAa7B,CAAiB,CAAA,EAGjCkD,IACC,EAAA,EAAA,IAAA,CAAC,GAAD,CACE,YAAahD,EACb,qBAAsBa,EAAK2C,4BAC3B,YAAa3C,EAAK6C,eAAgB,CAAA,EAGrCT,IACC,EAAA,EAAA,IAAA,CAAC,GAAD,CACE,YAAa/C,EACb,aAAcW,EAAK8C,aAAc,CAAA,CAGvB,GACL,IAGdT,IACC,EAAA,EAAA,IAAA,CAAC,GAAD,CACE,gBAAiBlF,GACjB,gBAAkB4F,GAChBlE,EAAe,CACbC,GAAIiE,EAASjE,GACbC,KAAMgE,EAASC,YACjB,CAAC,EAEH,UAAWxC,EACQ9B,mBAAkB,CAAA,GAIzC,EAAA,EAAA,IAAA,CAAC,GAAD,CACmBoC,kBACjB,kBAAmBW,EACnB,kBAAqBlC,EAAmB,EAAI,CAAE,CAAA,GAGhD,EAAA,EAAA,IAAA,CAAC,GAAD,CACE,OAAQP,IAAkB,MAC1B,YAAeC,EAAiB,IAAI,CAAE,CAAA,GAExC,EAAA,EAAA,IAAA,CAAC,GAAD,CACE,OAAQD,IAAkB,SAC1B,YAAeC,EAAiB,IAAI,CAAE,CAAA,GAExC,EAAA,EAAA,IAAA,CAAC,GAAD,CACE,OAAQD,IAAkB,SAC1B,YAAeC,EAAiB,IAAI,CAAE,CAAA,GAGxC,EAAA,EAAA,IAAA,CAAC,GAAD,CACE,OAAQC,IAAc,QACtB,YAAeC,EAAa,IAAI,CAAE,CAAA,GAEpC,EAAA,EAAA,IAAA,CAAC,GAAD,CACE,OAAQD,IAAc,UACtB,YAAeC,EAAa,IAAI,CAAE,CAAA,GAEpC,EAAA,EAAA,IAAA,CAAC,GAAD,CACE,OAAQD,IAAc,aACtB,YAAeC,EAAa,IAAI,CAAE,CAAA,GAGpC,EAAA,EAAA,IAAA,CAAC,GAAD,CACE,OAAQC,IAAiB,QACzB,YAAeC,EAAgB,IAAI,CAAE,CAAA,GAEvC,EAAA,EAAA,IAAA,CAAC,GAAD,CACE,OAAQD,IAAiB,SACzB,aAAgBC,EAAgB,OAAO,EACvC,YAAeA,EAAgB,IAAI,CAAE,CAAA,GAGvC,EAAA,EAAA,IAAA,CAAC,GAAD,CACE,OAAQT,IAAgB,KACxB,UAAWyB,EAAemC,UAC1B,YAAe3D,EAAe,IAAI,EAClC,UAAWyB,EACX,aAAc1B,GAAaG,MAAQ,EAAG,CAAA,GAGxC,EAAA,EAAA,IAAA,CAAC,GAAD,CACE,OAAQO,EACR,YAAeC,EAAmB,EAAK,EACxBsC,eAAc,CAAA,CAEtB,GAEf"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{J as e,Z as t,i as n,q as r,r as i,s as a}from"./createLucideIcon-BPpAIXR_.js";import{On as o,Sn as s,Sr as c}from"./config-DTQCUlfZ.js";import{t as l}from"./useMutation-BWZ0G4Td.js";import{c as u,o as d}from"./zod-C87S6Tec.js";import{F as f,N as p,R as m,S as h,j as g,t as _}from"./index-
|
|
2
|
-
//# sourceMappingURL=recovery-
|
|
1
|
+
import{J as e,Z as t,i as n,q as r,r as i,s as a}from"./createLucideIcon-BPpAIXR_.js";import{On as o,Sn as s,Sr as c}from"./config-DTQCUlfZ.js";import{t as l}from"./useMutation-BWZ0G4Td.js";import{c as u,o as d}from"./zod-C87S6Tec.js";import{F as f,N as p,R as m,S as h,j as g,t as _}from"./index-H6CJKjiN.js";import{t as v}from"./auth-page-header-ZLhtQ7Et.js";import{t as y}from"./alert-leqyvHOp.js";import{t as b}from"./auth-layout-CcJy6dVY.js";import{t as x}from"./promise-OpBtq8tG.js";import{t as S}from"./field-CEs4ugVJ.js";import{t as C}from"./input-BHPUSlfp.js";import{n as w,t as T}from"./auth-footer-Cz5ivsDc.js";import{r as E,t as D}from"./standard-schema-e8gtSzGE.js";import{a as O}from"./totp-BNZ904Zm.js";import{t as k}from"./labeled-separator-CV7nb0XW.js";var A=t(e()),j=r(),M={SECOND_FACTOR_SESSION_EXPIRED:`SECOND_FACTOR_SESSION_EXPIRED`,INVALID_RECOVERY_CODE:`INVALID_RECOVERY_CODE`,NO_RECOVERY_CODES_AVAILABLE:`NO_RECOVERY_CODES_AVAILABLE`,TOTP_NOT_ENABLED:`TOTP_NOT_ENABLED`},N=5;function P(){let{t:e}=n(),t=o(),r=c(),P=_.useSearch(),F=(0,A.useRef)(null),[I,L]=(0,A.useState)(!1),[R,z]=(0,A.useState)(N),B=(0,A.useMemo)(()=>d({code:u().transform(e=>e.toUpperCase()).pipe(u().regex(/^[A-HJ-NP-TV-Z2-9]{4}(?:-[A-HJ-NP-TV-Z2-9]{4}){3}$/,e(`verifyRecovery.error.invalid`)))}),[e]),V=l({...O,onSuccess:async e=>{r.setQueryData(h.queryKey,{user:e.user}),await x(),f(P)?window.location.href=g(P):t.navigate({to:`/profile`})},onSettled:()=>{r.invalidateQueries({queryKey:h.queryKey})}}),{register:H,setError:U,handleSubmit:W,setValue:G,formState:{errors:K}}=E({defaultValues:{code:``},resolver:D(B)}),q=(0,A.useCallback)(()=>{t.navigate({to:`/login`,search:p(P)})},[t,P]);(0,A.useEffect)(()=>{if(!I)return;let e=setInterval(()=>{z(t=>t<=1?(clearInterval(e),q(),0):t-1)},1e3);return()=>clearInterval(e)},[I,q]);let J=async t=>{try{await V.mutateAsync(t)}catch(t){if(t instanceof i)switch(t.code){case M.SECOND_FACTOR_SESSION_EXPIRED:L(!0),z(N);return;case M.TOTP_NOT_ENABLED:q();return;case M.NO_RECOVERY_CODES_AVAILABLE:U(`code`,{type:`manual`,message:e(`verifyRecovery.error.noCodesAvailable`)}),G(`code`,``),F.current?.focus();return;case M.INVALID_RECOVERY_CODE:U(`code`,{type:`manual`,message:e(`verifyRecovery.error.invalid`)}),G(`code`,``),F.current?.focus();return}U(`code`,{type:`manual`,message:e(`verifyRecovery.error.invalid`)}),G(`code`,``),F.current?.focus()}},{ref:Y,...X}=H(`code`);return(0,j.jsxs)(b,{children:[(0,j.jsx)(v,{subtitle:e(`verifyRecovery.subtitle`),title:e(`verifyRecovery.title`)}),I&&(0,j.jsx)(y,{"data-testid":`recovery-session-expired`,icon:m,type:`warning`,children:(0,j.jsxs)(`span`,{className:`flex flex-col items-start gap-tinyrack-3xs`,children:[(0,j.jsx)(`span`,{children:e(`verifyRecovery.error.expired`)}),(0,j.jsx)(`span`,{className:`text-tinyrack-sm opacity-tinyrack-hover`,children:e(`verifyRecovery.redirecting`,{seconds:R})}),(0,j.jsx)(a,{appearance:`ghost`,intent:`neutral`,onClick:q,type:`button`,uiSize:`sm`,children:e(`verifyRecovery.redirectNow`)})]})}),(0,j.jsxs)(`form`,{className:`flex flex-col gap-tinyrack-lg`,onSubmit:W(J),children:[(0,j.jsxs)(S.Root,{children:[(0,j.jsx)(C,{...X,autoComplete:`off`,className:`w-full text-center font-tinyrack-mono`,"data-testid":`recovery-code-input`,disabled:I,placeholder:e(`verifyRecovery.placeholder`),ref:e=>{Y(e),F.current=e},type:`text`}),K.code&&(0,j.jsx)(`div`,{className:`tr-field-error`,"data-testid":`recovery-error`,children:K.code.message})]}),I?(0,j.jsx)(a,{className:`w-full`,disabled:!0,intent:`primary`,type:`button`,uiSize:`lg`,children:e(`verifyRecovery.submit`)}):(0,j.jsx)(a,{className:`w-full`,intent:`primary`,loading:V.isPending,loadingLabel:e(`verifyRecovery.submitting`),type:`submit`,uiSize:`lg`,children:e(`verifyRecovery.submit`)})]}),(0,j.jsxs)(`div`,{className:`flex flex-col gap-tinyrack-md`,children:[(0,j.jsx)(k,{label:e(`common.or`)}),(0,j.jsx)(a,{appearance:`ghost`,className:`w-full`,"data-testid":`recovery-back-to-totp`,intent:`neutral`,onClick:()=>t.navigate({to:`/verify/totp`,search:p(P)}),type:`button`,children:e(`verifyRecovery.backToTotp`)})]}),(0,j.jsx)(T,{children:(0,j.jsx)(w,{link:(0,j.jsx)(s,{search:p(P),to:`/login`,children:e(`verifyRecovery.backToLogin`)})})})]})}export{P as component};
|
|
2
|
+
//# sourceMappingURL=recovery-CvNl-yt9.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recovery-DbgpqG6f.js","names":["standardSchemaResolver","useMutation","useQueryClient","Link","useRouter","TRButton","TRField","TRInput","CircleAlertIcon","useCallback","useEffect","useMemo","useRef","useState","useForm","useTranslation","z","AuthFooter","AuthFooterLink","AuthPageHeader","Alert","LabeledSeparator","AuthLayout","IssuaryError","buildAuthenticatedAuthorizeUrl","extractOAuthParams","isOAuthFlow","tick","getSessionQueryOptions","verifyRecoveryCodeMutationOptions","ERROR_CODES","SECOND_FACTOR_SESSION_EXPIRED","INVALID_RECOVERY_CODE","NO_RECOVERY_CODES_AVAILABLE","TOTP_NOT_ENABLED","const","Route","RecoveryFormValues","code","REDIRECT_COUNTDOWN_SECONDS","VerifyRecovery","t","router","queryClient","search","useSearch","inputRef","HTMLInputElement","sessionExpired","setSessionExpired","redirectCountdown","setRedirectCountdown","recoverySchema","object","string","transform","value","toUpperCase","pipe","regex","verifyMutation","onSuccess","data","setQueryData","queryKey","user","window","location","href","navigate","to","onSettled","invalidateQueries","register","setError","handleSubmit","setValue","formState","errors","defaultValues","resolver","redirectToLogin","timer","setInterval","prev","clearInterval","onSubmit","values","mutateAsync","error","type","message","current","focus","ref","formRef","registerRest","seconds","el","isPending","component"],"sources":["../../../frontend/src/routes/verify/totp/recovery/index.tsx?tsr-split=component"],"sourcesContent":["import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';\nimport { useMutation, useQueryClient } from '@tanstack/react-query';\nimport { createFileRoute, Link, useRouter } from '@tanstack/react-router';\nimport { TRButton } from '@tinyrack/ui/components/button';\nimport { TRField } from '@tinyrack/ui/components/field';\nimport { TRInput } from '@tinyrack/ui/components/input';\nimport { CircleAlertIcon } from 'lucide-react';\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport {\n AuthFooter,\n AuthFooterLink,\n} from '#frontend/components/auth/auth-footer.tsx';\nimport { AuthPageHeader } from '#frontend/components/auth/auth-page-header.tsx';\nimport { Alert } from '#frontend/components/ui/alert.tsx';\nimport { LabeledSeparator } from '#frontend/components/ui/labeled-separator.tsx';\nimport { AuthLayout } from '#frontend/features/layout/auth-layout.tsx';\nimport { IssuaryError } from '#frontend/libs/error.ts';\nimport {\n buildAuthenticatedAuthorizeUrl,\n extractOAuthParams,\n isOAuthFlow,\n OAuthSearchSchema,\n} from '#frontend/libs/oauth-search.ts';\nimport { tick } from '#frontend/libs/promise.ts';\nimport { getSessionQueryOptions } from '#frontend/queries/session.ts';\nimport { verifyRecoveryCodeMutationOptions } from '#frontend/queries/totp.ts';\n\n/** Error codes from backend */\nconst ERROR_CODES = {\n SECOND_FACTOR_SESSION_EXPIRED: 'SECOND_FACTOR_SESSION_EXPIRED',\n INVALID_RECOVERY_CODE: 'INVALID_RECOVERY_CODE',\n NO_RECOVERY_CODES_AVAILABLE: 'NO_RECOVERY_CODES_AVAILABLE',\n TOTP_NOT_ENABLED: 'TOTP_NOT_ENABLED',\n} as const;\n\nconst SearchSchema = OAuthSearchSchema;\n\nexport const Route = createFileRoute('/verify/totp/recovery/')({\n component: VerifyRecovery,\n validateSearch: SearchSchema,\n});\n\ntype RecoveryFormValues = {\n code: string;\n};\n\n/** Auto redirect countdown seconds */\nconst REDIRECT_COUNTDOWN_SECONDS = 5;\n\nfunction VerifyRecovery() {\n const { t } = useTranslation();\n const router = useRouter();\n const queryClient = useQueryClient();\n const search = Route.useSearch();\n const inputRef = useRef<HTMLInputElement | null>(null);\n\n const [sessionExpired, setSessionExpired] = useState(false);\n const [redirectCountdown, setRedirectCountdown] = useState(\n REDIRECT_COUNTDOWN_SECONDS,\n );\n\n const recoverySchema = useMemo(\n () =>\n z.object({\n code: z\n .string()\n .transform((value) => value.toUpperCase())\n .pipe(\n z\n .string()\n .regex(\n /^[A-HJ-NP-TV-Z2-9]{4}(?:-[A-HJ-NP-TV-Z2-9]{4}){3}$/,\n t('verifyRecovery.error.invalid'),\n ),\n ),\n }),\n [t],\n );\n\n const verifyMutation = useMutation({\n ...verifyRecoveryCodeMutationOptions,\n onSuccess: async (data) => {\n queryClient.setQueryData(getSessionQueryOptions.queryKey, {\n user: data.user,\n });\n await tick();\n\n if (isOAuthFlow(search)) {\n window.location.href = buildAuthenticatedAuthorizeUrl(search);\n } else {\n router.navigate({ to: '/profile' });\n }\n },\n onSettled: () => {\n queryClient.invalidateQueries({\n queryKey: getSessionQueryOptions.queryKey,\n });\n },\n });\n\n const {\n register,\n setError,\n handleSubmit,\n setValue,\n formState: { errors },\n } = useForm<RecoveryFormValues>({\n defaultValues: {\n code: '',\n },\n resolver: standardSchemaResolver(recoverySchema),\n });\n\n // Auto redirect when session expires\n const redirectToLogin = useCallback(() => {\n router.navigate({\n to: '/login',\n search: extractOAuthParams(search),\n });\n }, [router, search]);\n\n useEffect(() => {\n if (!sessionExpired) return;\n\n const timer = setInterval(() => {\n setRedirectCountdown((prev) => {\n if (prev <= 1) {\n clearInterval(timer);\n redirectToLogin();\n return 0;\n }\n return prev - 1;\n });\n }, 1000);\n\n return () => clearInterval(timer);\n }, [sessionExpired, redirectToLogin]);\n\n const onSubmit = async (values: RecoveryFormValues) => {\n try {\n await verifyMutation.mutateAsync(values);\n } catch (error) {\n if (error instanceof IssuaryError) {\n switch (error.code) {\n case ERROR_CODES.SECOND_FACTOR_SESSION_EXPIRED:\n setSessionExpired(true);\n setRedirectCountdown(REDIRECT_COUNTDOWN_SECONDS);\n return;\n\n case ERROR_CODES.TOTP_NOT_ENABLED:\n redirectToLogin();\n return;\n\n case ERROR_CODES.NO_RECOVERY_CODES_AVAILABLE:\n setError('code', {\n type: 'manual',\n message: t('verifyRecovery.error.noCodesAvailable'),\n });\n setValue('code', '');\n inputRef.current?.focus();\n return;\n\n case ERROR_CODES.INVALID_RECOVERY_CODE:\n setError('code', {\n type: 'manual',\n message: t('verifyRecovery.error.invalid'),\n });\n setValue('code', '');\n inputRef.current?.focus();\n return;\n }\n }\n\n // Generic error fallback\n setError('code', {\n type: 'manual',\n message: t('verifyRecovery.error.invalid'),\n });\n setValue('code', '');\n inputRef.current?.focus();\n }\n };\n\n const { ref: formRef, ...registerRest } = register('code');\n\n return (\n <AuthLayout>\n <AuthPageHeader\n subtitle={t('verifyRecovery.subtitle')}\n title={t('verifyRecovery.title')}\n />\n\n {sessionExpired && (\n <Alert\n data-testid=\"recovery-session-expired\"\n icon={CircleAlertIcon}\n type=\"warning\"\n >\n <span className=\"flex flex-col items-start gap-tinyrack-3xs\">\n <span>{t('verifyRecovery.error.expired')}</span>\n <span className=\"text-tinyrack-sm opacity-tinyrack-hover\">\n {t('verifyRecovery.redirecting', {\n seconds: redirectCountdown,\n })}\n </span>\n <TRButton\n appearance=\"ghost\"\n intent=\"neutral\"\n onClick={redirectToLogin}\n type=\"button\"\n uiSize=\"sm\"\n >\n {t('verifyRecovery.redirectNow')}\n </TRButton>\n </span>\n </Alert>\n )}\n\n <form\n className=\"flex flex-col gap-tinyrack-lg\"\n onSubmit={handleSubmit(onSubmit)}\n >\n <TRField.Root>\n <TRInput\n {...registerRest}\n autoComplete=\"off\"\n className=\"w-full text-center font-tinyrack-mono\"\n data-testid=\"recovery-code-input\"\n disabled={sessionExpired}\n placeholder={t('verifyRecovery.placeholder')}\n ref={(el) => {\n formRef(el);\n inputRef.current = el as HTMLInputElement | null;\n }}\n type=\"text\"\n />\n {errors.code && (\n <div className=\"tr-field-error\" data-testid=\"recovery-error\">\n {errors.code.message}\n </div>\n )}\n </TRField.Root>\n\n {sessionExpired ? (\n <TRButton\n className=\"w-full\"\n disabled\n intent=\"primary\"\n type=\"button\"\n uiSize=\"lg\"\n >\n {t('verifyRecovery.submit')}\n </TRButton>\n ) : (\n <TRButton\n className=\"w-full\"\n intent=\"primary\"\n loading={verifyMutation.isPending}\n loadingLabel={t('verifyRecovery.submitting')}\n type=\"submit\"\n uiSize=\"lg\"\n >\n {t('verifyRecovery.submit')}\n </TRButton>\n )}\n </form>\n\n {/* Mirrors /verify/totp: the other second factor sits below the rule. */}\n <div className=\"flex flex-col gap-tinyrack-md\">\n <LabeledSeparator label={t('common.or')} />\n <TRButton\n appearance=\"ghost\"\n className=\"w-full\"\n data-testid=\"recovery-back-to-totp\"\n intent=\"neutral\"\n onClick={() =>\n router.navigate({\n to: '/verify/totp',\n search: extractOAuthParams(search),\n })\n }\n type=\"button\"\n >\n {t('verifyRecovery.backToTotp')}\n </TRButton>\n </div>\n\n <AuthFooter>\n <AuthFooterLink\n link={\n <Link search={extractOAuthParams(search)} to=\"/login\">\n {t('verifyRecovery.backToLogin')}\n </Link>\n }\n />\n </AuthFooter>\n </AuthLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"recovery-CvNl-yt9.js","names":["standardSchemaResolver","useMutation","useQueryClient","Link","useRouter","TRButton","TRField","TRInput","CircleAlertIcon","useCallback","useEffect","useMemo","useRef","useState","useForm","useTranslation","z","AuthFooter","AuthFooterLink","AuthPageHeader","Alert","LabeledSeparator","AuthLayout","IssuaryError","buildAuthenticatedAuthorizeUrl","extractOAuthParams","isOAuthFlow","tick","getSessionQueryOptions","verifyRecoveryCodeMutationOptions","ERROR_CODES","SECOND_FACTOR_SESSION_EXPIRED","INVALID_RECOVERY_CODE","NO_RECOVERY_CODES_AVAILABLE","TOTP_NOT_ENABLED","const","Route","RecoveryFormValues","code","REDIRECT_COUNTDOWN_SECONDS","VerifyRecovery","t","router","queryClient","search","useSearch","inputRef","HTMLInputElement","sessionExpired","setSessionExpired","redirectCountdown","setRedirectCountdown","recoverySchema","object","string","transform","value","toUpperCase","pipe","regex","verifyMutation","onSuccess","data","setQueryData","queryKey","user","window","location","href","navigate","to","onSettled","invalidateQueries","register","setError","handleSubmit","setValue","formState","errors","defaultValues","resolver","redirectToLogin","timer","setInterval","prev","clearInterval","onSubmit","values","mutateAsync","error","type","message","current","focus","ref","formRef","registerRest","seconds","el","isPending","component"],"sources":["../../../frontend/src/routes/verify/totp/recovery/index.tsx?tsr-split=component"],"sourcesContent":["import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';\nimport { useMutation, useQueryClient } from '@tanstack/react-query';\nimport { createFileRoute, Link, useRouter } from '@tanstack/react-router';\nimport { TRButton } from '@tinyrack/ui/components/button';\nimport { TRField } from '@tinyrack/ui/components/field';\nimport { TRInput } from '@tinyrack/ui/components/input';\nimport { CircleAlertIcon } from 'lucide-react';\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport {\n AuthFooter,\n AuthFooterLink,\n} from '#frontend/components/auth/auth-footer.tsx';\nimport { AuthPageHeader } from '#frontend/components/auth/auth-page-header.tsx';\nimport { Alert } from '#frontend/components/ui/alert.tsx';\nimport { LabeledSeparator } from '#frontend/components/ui/labeled-separator.tsx';\nimport { AuthLayout } from '#frontend/features/layout/auth-layout.tsx';\nimport { IssuaryError } from '#frontend/libs/error.ts';\nimport {\n buildAuthenticatedAuthorizeUrl,\n extractOAuthParams,\n isOAuthFlow,\n OAuthSearchSchema,\n} from '#frontend/libs/oauth-search.ts';\nimport { tick } from '#frontend/libs/promise.ts';\nimport { getSessionQueryOptions } from '#frontend/queries/session.ts';\nimport { verifyRecoveryCodeMutationOptions } from '#frontend/queries/totp.ts';\n\n/** Error codes from backend */\nconst ERROR_CODES = {\n SECOND_FACTOR_SESSION_EXPIRED: 'SECOND_FACTOR_SESSION_EXPIRED',\n INVALID_RECOVERY_CODE: 'INVALID_RECOVERY_CODE',\n NO_RECOVERY_CODES_AVAILABLE: 'NO_RECOVERY_CODES_AVAILABLE',\n TOTP_NOT_ENABLED: 'TOTP_NOT_ENABLED',\n} as const;\n\nconst SearchSchema = OAuthSearchSchema;\n\nexport const Route = createFileRoute('/verify/totp/recovery/')({\n component: VerifyRecovery,\n validateSearch: SearchSchema,\n});\n\ntype RecoveryFormValues = {\n code: string;\n};\n\n/** Auto redirect countdown seconds */\nconst REDIRECT_COUNTDOWN_SECONDS = 5;\n\nfunction VerifyRecovery() {\n const { t } = useTranslation();\n const router = useRouter();\n const queryClient = useQueryClient();\n const search = Route.useSearch();\n const inputRef = useRef<HTMLInputElement | null>(null);\n\n const [sessionExpired, setSessionExpired] = useState(false);\n const [redirectCountdown, setRedirectCountdown] = useState(\n REDIRECT_COUNTDOWN_SECONDS,\n );\n\n const recoverySchema = useMemo(\n () =>\n z.object({\n code: z\n .string()\n .transform((value) => value.toUpperCase())\n .pipe(\n z\n .string()\n .regex(\n /^[A-HJ-NP-TV-Z2-9]{4}(?:-[A-HJ-NP-TV-Z2-9]{4}){3}$/,\n t('verifyRecovery.error.invalid'),\n ),\n ),\n }),\n [t],\n );\n\n const verifyMutation = useMutation({\n ...verifyRecoveryCodeMutationOptions,\n onSuccess: async (data) => {\n queryClient.setQueryData(getSessionQueryOptions.queryKey, {\n user: data.user,\n });\n await tick();\n\n if (isOAuthFlow(search)) {\n window.location.href = buildAuthenticatedAuthorizeUrl(search);\n } else {\n router.navigate({ to: '/profile' });\n }\n },\n onSettled: () => {\n queryClient.invalidateQueries({\n queryKey: getSessionQueryOptions.queryKey,\n });\n },\n });\n\n const {\n register,\n setError,\n handleSubmit,\n setValue,\n formState: { errors },\n } = useForm<RecoveryFormValues>({\n defaultValues: {\n code: '',\n },\n resolver: standardSchemaResolver(recoverySchema),\n });\n\n // Auto redirect when session expires\n const redirectToLogin = useCallback(() => {\n router.navigate({\n to: '/login',\n search: extractOAuthParams(search),\n });\n }, [router, search]);\n\n useEffect(() => {\n if (!sessionExpired) return;\n\n const timer = setInterval(() => {\n setRedirectCountdown((prev) => {\n if (prev <= 1) {\n clearInterval(timer);\n redirectToLogin();\n return 0;\n }\n return prev - 1;\n });\n }, 1000);\n\n return () => clearInterval(timer);\n }, [sessionExpired, redirectToLogin]);\n\n const onSubmit = async (values: RecoveryFormValues) => {\n try {\n await verifyMutation.mutateAsync(values);\n } catch (error) {\n if (error instanceof IssuaryError) {\n switch (error.code) {\n case ERROR_CODES.SECOND_FACTOR_SESSION_EXPIRED:\n setSessionExpired(true);\n setRedirectCountdown(REDIRECT_COUNTDOWN_SECONDS);\n return;\n\n case ERROR_CODES.TOTP_NOT_ENABLED:\n redirectToLogin();\n return;\n\n case ERROR_CODES.NO_RECOVERY_CODES_AVAILABLE:\n setError('code', {\n type: 'manual',\n message: t('verifyRecovery.error.noCodesAvailable'),\n });\n setValue('code', '');\n inputRef.current?.focus();\n return;\n\n case ERROR_CODES.INVALID_RECOVERY_CODE:\n setError('code', {\n type: 'manual',\n message: t('verifyRecovery.error.invalid'),\n });\n setValue('code', '');\n inputRef.current?.focus();\n return;\n }\n }\n\n // Generic error fallback\n setError('code', {\n type: 'manual',\n message: t('verifyRecovery.error.invalid'),\n });\n setValue('code', '');\n inputRef.current?.focus();\n }\n };\n\n const { ref: formRef, ...registerRest } = register('code');\n\n return (\n <AuthLayout>\n <AuthPageHeader\n subtitle={t('verifyRecovery.subtitle')}\n title={t('verifyRecovery.title')}\n />\n\n {sessionExpired && (\n <Alert\n data-testid=\"recovery-session-expired\"\n icon={CircleAlertIcon}\n type=\"warning\"\n >\n <span className=\"flex flex-col items-start gap-tinyrack-3xs\">\n <span>{t('verifyRecovery.error.expired')}</span>\n <span className=\"text-tinyrack-sm opacity-tinyrack-hover\">\n {t('verifyRecovery.redirecting', {\n seconds: redirectCountdown,\n })}\n </span>\n <TRButton\n appearance=\"ghost\"\n intent=\"neutral\"\n onClick={redirectToLogin}\n type=\"button\"\n uiSize=\"sm\"\n >\n {t('verifyRecovery.redirectNow')}\n </TRButton>\n </span>\n </Alert>\n )}\n\n <form\n className=\"flex flex-col gap-tinyrack-lg\"\n onSubmit={handleSubmit(onSubmit)}\n >\n <TRField.Root>\n <TRInput\n {...registerRest}\n autoComplete=\"off\"\n className=\"w-full text-center font-tinyrack-mono\"\n data-testid=\"recovery-code-input\"\n disabled={sessionExpired}\n placeholder={t('verifyRecovery.placeholder')}\n ref={(el) => {\n formRef(el);\n inputRef.current = el as HTMLInputElement | null;\n }}\n type=\"text\"\n />\n {errors.code && (\n <div className=\"tr-field-error\" data-testid=\"recovery-error\">\n {errors.code.message}\n </div>\n )}\n </TRField.Root>\n\n {sessionExpired ? (\n <TRButton\n className=\"w-full\"\n disabled\n intent=\"primary\"\n type=\"button\"\n uiSize=\"lg\"\n >\n {t('verifyRecovery.submit')}\n </TRButton>\n ) : (\n <TRButton\n className=\"w-full\"\n intent=\"primary\"\n loading={verifyMutation.isPending}\n loadingLabel={t('verifyRecovery.submitting')}\n type=\"submit\"\n uiSize=\"lg\"\n >\n {t('verifyRecovery.submit')}\n </TRButton>\n )}\n </form>\n\n {/* Mirrors /verify/totp: the other second factor sits below the rule. */}\n <div className=\"flex flex-col gap-tinyrack-md\">\n <LabeledSeparator label={t('common.or')} />\n <TRButton\n appearance=\"ghost\"\n className=\"w-full\"\n data-testid=\"recovery-back-to-totp\"\n intent=\"neutral\"\n onClick={() =>\n router.navigate({\n to: '/verify/totp',\n search: extractOAuthParams(search),\n })\n }\n type=\"button\"\n >\n {t('verifyRecovery.backToTotp')}\n </TRButton>\n </div>\n\n <AuthFooter>\n <AuthFooterLink\n link={\n <Link search={extractOAuthParams(search)} to=\"/login\">\n {t('verifyRecovery.backToLogin')}\n </Link>\n }\n />\n </AuthFooter>\n </AuthLayout>\n );\n}\n"],"mappings":"qxBA+BM8B,EAAc,CAClBC,8BAA+B,gCAC/BC,sBAAuB,wBACvBC,4BAA6B,8BAC7BC,iBAAkB,kBACpB,EAcMK,EAA6B,EAEnC,SAASC,GAAiB,CACxB,GAAM,CAAEC,KAAM1B,EAAe,EACvB2B,EAAStC,EAAU,EACnBuC,EAAczC,EAAe,EAC7B0C,EAASR,EAAMS,UAAU,EACzBC,GAAAA,EAAAA,EAAAA,OAAAA,CAA2C,IAAI,EAE/C,CAACE,EAAgBC,IAAAA,EAAAA,EAAAA,SAAAA,CAA8B,EAAK,EACpD,CAACC,EAAmBC,IAAAA,EAAAA,EAAAA,SAAAA,CACxBZ,CACF,EAEMa,GAAAA,EAAAA,EAAAA,QAAAA,KAEFpC,EAAS,CACPsB,KAAMtB,EACI,CAAC,CACRuC,UAAWC,GAAUA,EAAMC,YAAY,CAAC,CAAC,CACzCC,KACC1C,EACU,CAAC,CACR2C,MACC,qDACAlB,EAAE,8BAA8B,CAClC,CACJ,CACJ,CAAC,EACH,CAACA,CAAC,CACJ,EAEMmB,EAAiB3D,EAAY,CACjC,GAAG4B,EACHgC,UAAW,KAAOC,IAAS,CACzBnB,EAAYoB,aAAanC,EAAuBoC,SAAU,CACxDC,KAAMH,EAAKG,IACb,CAAC,EACD,MAAMtC,EAAK,EAEPD,EAAYkB,CAAM,EACpBsB,OAAOC,SAASC,KAAO5C,EAA+BoB,CAAM,EAE5DF,EAAO2B,SAAS,CAAEC,GAAI,UAAW,CAAC,CAEtC,EACAC,cAAiB,CACf5B,EAAY6B,kBAAkB,CAC5BR,SAAUpC,EAAuBoC,QACnC,CAAC,CACH,CACF,CAAC,EAEK,CACJS,WACAC,WACAC,eACAC,WACAC,UAAW,CAAEC,WACXhE,EAA4B,CAC9BiE,cAAe,CACbzC,KAAM,EACR,EACA0C,SAAUhF,EAAuBoD,CAAc,CACjD,CAAC,EAGK6B,GAAAA,EAAAA,EAAAA,YAAAA,KAAoC,CACxCvC,EAAO2B,SAAS,CACdC,GAAI,SACJ1B,OAAQnB,EAAmBmB,CAAM,CACnC,CAAC,CACH,EAAG,CAACF,EAAQE,CAAM,CAAC,GAEnBlC,EAAAA,EAAAA,UAAAA,KAAgB,CACd,GAAI,CAACsC,EAAgB,OAErB,IAAMkC,EAAQC,gBAAkB,CAC9BhC,EAAsBiC,GAChBA,GAAQ,GACVC,cAAcH,CAAK,EACnBD,EAAgB,EACT,GAEFG,EAAO,CACf,CACH,EAAG,GAAI,EAEP,UAAaC,cAAcH,CAAK,CAClC,EAAG,CAAClC,EAAgBiC,CAAe,CAAC,EAEpC,IAAMK,EAAW,KAAOC,IAA+B,CACrD,GAAI,CACF,MAAM3B,EAAe4B,YAAYD,CAAM,CACzC,OAASE,EAAO,CACd,GAAIA,aAAiBlE,EACnB,OAAQkE,EAAMnD,KAAd,CACE,KAAKR,EAAYC,8BACfkB,EAAkB,EAAI,EACtBE,EAAqBZ,CAA0B,EAC/C,OAEF,KAAKT,EAAYI,iBACf+C,EAAgB,EAChB,OAEF,KAAKnD,EAAYG,4BACfyC,EAAS,OAAQ,CACfgB,KAAM,SACNC,QAASlD,EAAE,uCAAuC,CACpD,CAAC,EACDmC,EAAS,OAAQ,EAAE,EACnB9B,EAAS8C,SAASC,MAAM,EACxB,OAEF,KAAK/D,EAAYE,sBACf0C,EAAS,OAAQ,CACfgB,KAAM,SACNC,QAASlD,EAAE,8BAA8B,CAC3C,CAAC,EACDmC,EAAS,OAAQ,EAAE,EACnB9B,EAAS8C,SAASC,MAAM,EACxB,MACJ,CAIFnB,EAAS,OAAQ,CACfgB,KAAM,SACNC,QAASlD,EAAE,8BAA8B,CAC3C,CAAC,EACDmC,EAAS,OAAQ,EAAE,EACnB9B,EAAS8C,SAASC,MAAM,CAC1B,CACF,EAEM,CAAEC,IAAKC,EAAS,GAAGC,GAAiBvB,EAAS,MAAM,EAEzD,OACE,EAAA,EAAA,KAAA,CAAC,EAAD,CAAA,SAAA,EACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,SAAUhC,EAAE,yBAAyB,EACrC,MAAOA,EAAE,sBAAsB,CAAE,CAAA,EAGlCO,IACC,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,cAAY,2BACZ,KAAMxC,EACN,KAAK,oBAEL,EAAA,EAAA,KAAA,CAAC,OAAD,CAAM,UAAU,sDAAhB,EACE,EAAA,EAAA,IAAA,CAAC,OAAD,CAAA,SAAOiC,EAAE,8BAA8B,CAAQ,CAAA,GAC/C,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAU,mDACbA,EAAE,6BAA8B,CAC/BwD,QAAS/C,CACX,CAAC,CACG,CAAA,GACN,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,WAAW,QACX,OAAO,UACP,QAAS+B,EACT,KAAK,SACL,OAAO,cAENxC,EAAE,4BAA4B,CACvB,CAAA,CACN,GACD,CAAA,GAGT,EAAA,EAAA,KAAA,CAAC,OAAD,CACE,UAAU,gCACV,SAAUkC,EAAaW,CAAQ,WAFjC,EAIE,EAAA,EAAA,KAAA,CAAC,EAAQ,KAAT,CAAA,SAAA,EACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,GAAIU,EACJ,aAAa,MACb,UAAU,wCACV,cAAY,sBACZ,SAAUhD,EACV,YAAaP,EAAE,4BAA4B,EAC3C,IAAMyD,GAAO,CACXH,EAAQG,CAAE,EACVpD,EAAS8C,QAAUM,CACrB,EACA,KAAK,MAAM,CAAA,EAEZpB,EAAOxC,OACN,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAU,iBAAiB,cAAY,0BACzCwC,EAAOxC,KAAKqD,OACV,CAAA,CAEK,CAAA,CAAA,EAEb3C,GACC,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,UAAU,SACV,SAAA,GACA,OAAO,UACP,KAAK,SACL,OAAO,cAENP,EAAE,uBAAuB,CAClB,CAAA,GAEV,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,UAAU,SACV,OAAO,UACP,QAASmB,EAAeuC,UACxB,aAAc1D,EAAE,2BAA2B,EAC3C,KAAK,SACL,OAAO,cAENA,EAAE,uBAAuB,CAClB,CAAA,CAER,KAGN,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAU,yCAAf,EACE,EAAA,EAAA,IAAA,CAAC,EAAD,CAAkB,MAAOA,EAAE,WAAW,CAAE,CAAA,GACxC,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,WAAW,QACX,UAAU,SACV,cAAY,wBACZ,OAAO,UACP,YACEC,EAAO2B,SAAS,CACdC,GAAI,eACJ1B,OAAQnB,EAAmBmB,CAAM,CACnC,CAAC,EAEH,KAAK,kBAEJH,EAAE,2BAA2B,CACtB,CAAA,CACP,KAEL,EAAA,EAAA,IAAA,CAAC,EAAD,CAAA,UACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,MACE,EAAA,EAAA,IAAA,CAAC,EAAD,CAAM,OAAQhB,EAAmBmB,CAAM,EAAG,GAAG,kBAC1CH,EAAE,4BAA4B,CAC3B,CAAA,CACP,CAAA,CAEO,CAAA,CACF,CAAA,CAAA,CAEhB"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{J as e,Z as t,i as n,q as r,r as ee,s as i}from"./createLucideIcon-BPpAIXR_.js";import{Sn as a,Sr as o,i as s,r as c,t as l}from"./config-DTQCUlfZ.js";import{t as u}from"./mutationOptions-DyYFUbyn.js";import{t as d}from"./useMutation-BWZ0G4Td.js";import{
|
|
2
|
-
//# sourceMappingURL=register-
|
|
1
|
+
import{J as e,Z as t,i as n,q as r,r as ee,s as i}from"./createLucideIcon-BPpAIXR_.js";import{Sn as a,Sr as o,i as s,r as c,t as l}from"./config-DTQCUlfZ.js";import{t as u}from"./mutationOptions-DyYFUbyn.js";import{t as d}from"./useMutation-BWZ0G4Td.js";import{nt as f}from"./shell-header-bar-BPgCjFPv.js";import{t as p}from"./auth-field-BSlAWkbx.js";import{t as m}from"./authorization-context-banner-DfkjwE93.js";import{t as te}from"./lock-7NRyvXBS.js";import{t as h}from"./mail-D0B_1Dzd.js";import{a as g,c as _,i as v,o as y,r as b}from"./zod-C87S6Tec.js";import{E as x,F as S,N as C,S as w,V as ne,j as T,k as E}from"./index-H6CJKjiN.js";import{t as D}from"./auth-page-header-ZLhtQ7Et.js";import{t as O}from"./auth-layout-CcJy6dVY.js";import{t as k}from"./promise-OpBtq8tG.js";import{n as A,t as j}from"./auth-footer-Cz5ivsDc.js";import{r as M,t as N}from"./standard-schema-e8gtSzGE.js";import{t as P}from"./password-strength-DtGMHOD-.js";import{t as F}from"./terms-checkbox-list-BKJcS2pJ.js";import{t as I}from"./labeled-separator-CV7nb0XW.js";var L=t(e()),R=u({mutationFn:async e=>s(await c.api.auth.register.$post({json:e,header:{}}))}),z=r();function B(){let{t:e,i18n:t}=n(),r=ne(),s=o(),c=x.useSearch(),u=c.lang??t.language,B=(0,L.useDeferredValue)(u),{data:V}=f(l),{data:H}=f(E(B)),U=V.auth.password.enabled,W=V.auth.password.policy,G=H.terms.length>0,K=(0,L.useMemo)(()=>H.terms.filter(e=>e.consentMode===`explicit`),[H.terms]),q=K.length>0,J=V.registration.signup_notice?.[u]??V.registration.signup_notice?.[V.i18n.fallback_language],Y=(0,L.useMemo)(()=>y({email:_().min(1,e(`validation.email.required`)).pipe(v(e(`validation.email.invalid`))),password:_().min(W.min_length,e(`validation.password.min`,{count:W.min_length})).max(W.max_length,e(`validation.password.max`,{count:W.max_length})),termsConsents:y(Object.fromEntries(K.map(t=>[t.id,t.required?g(!0,{message:e(`validation.terms.required`)}):b()])))}),[K,W.max_length,W.min_length,e]),X=d({...R,onSuccess:async(e,t)=>{let n=e.user;if(n.email_verification_required&&!n.email_verified)return r({to:`/verify/email`,search:{email:t.email,...C(c)}});if(s.setQueryData(w.queryKey,{user:n}),await k(),n.second_factor_required){let e=[];return V.auth.password.totp.enabled&&e.push(`totp`),V.auth.passkey.enabled&&e.push(`passkey`),e.length===1?e[0]===`totp`?r({to:`/setup/totp`,search:C(c)}):r({to:`/setup/passkey`,search:{...C(c),passkey_name:`default`}}):r({to:`/setup/2fa`,search:C(c)})}S(c)?window.location.href=T(c):r({to:`/profile`})},onError:t=>{t instanceof ee?t.code===`REGISTRATION_EMAIL_NOT_ALLOWED`?Q(`email`,{type:`manual`,message:e(`register.error.emailNotAllowed`)}):t.code===`REGISTRATION_DISABLED`?Q(`email`,{type:`manual`,message:e(`register.error.registrationDisabled`)}):Q(`email`,{type:`manual`,message:e(`register.error.emailExists`)}):Q(`email`,{type:`manual`,message:e(`register.error.emailExists`)})},onSettled:async()=>{await s.invalidateQueries({queryKey:w.queryKey})}}),{register:Z,setError:Q,handleSubmit:re,control:ie,setValue:ae,watch:oe,formState:{errors:$}}=M({defaultValues:{email:``,password:``,termsConsents:Object.fromEntries(K.map(e=>[e.id,!1]))},resolver:N(Y),mode:`onChange`}),se=oe(`password`);return(0,z.jsxs)(O,{children:[(0,z.jsx)(D,{subtitle:e(`register.subtitle`),title:e(`register.title`)}),(0,z.jsx)(m,{search:c}),U&&(0,z.jsxs)(`form`,{className:`flex flex-col gap-tinyrack-lg`,onSubmit:re(e=>{let t=G?K.map(t=>({termsId:t.id,agreed:e.termsConsents[t.id]??!1})):void 0;X.mutate({email:e.email,password:e.password,consents:t})}),children:[(0,z.jsx)(p,{autoComplete:`email`,error:$.email,icon:h,label:e(`register.email.label`),placeholder:e(`register.email.placeholder`),...Z(`email`),type:`email`}),(0,z.jsxs)(`div`,{className:`flex flex-col gap-tinyrack-sm`,children:[(0,z.jsx)(p,{autoComplete:`new-password`,error:$.password,hint:e(`register.password.hint`,{count:W.min_length}),icon:te,label:e(`register.password.label`),placeholder:e(`register.password.placeholder`),...Z(`password`),type:`password`}),(0,z.jsx)(P,{password:se,policy:W})]}),(0,z.jsxs)(`div`,{className:`flex flex-col gap-tinyrack-md`,children:[J&&(0,z.jsx)(`div`,{className:`text-center text-tinyrack-text-muted text-tinyrack-xs`,children:(0,z.jsx)(`div`,{className:`prose prose-sm text-tinyrack-xs! **:text-tinyrack-xs!`,dangerouslySetInnerHTML:{__html:J},"data-testid":`terms-implicit-notice`})}),J&&q&&(0,z.jsx)(I,{label:e(`terms.additionalOptionalConsent`)}),G&&q&&(0,z.jsx)(F,{control:ie,disabled:X.isPending,errors:$,setValue:ae,terms:K})]}),(0,z.jsx)(i,{className:`w-full`,intent:`primary`,loading:X.isPending,loadingLabel:e(`register.submitting`),type:`submit`,uiSize:`lg`,children:e(`register.submit`)})]}),(0,z.jsx)(j,{children:(0,z.jsx)(A,{link:(0,z.jsx)(a,{search:C(c),to:`/login`,children:e(`register.link.login`)}),text:e(`register.footer.haveAccount`)})})]})}export{B as component};
|
|
2
|
+
//# sourceMappingURL=register-SFXa5EeL.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"register-B2Lixapy.js","names":["standardSchemaResolver","useMutation","useQueryClient","useSuspenseQuery","Link","useNavigate","TRButton","LockIcon","MailIcon","useDeferredValue","useMemo","useForm","useTranslation","z","AuthField","AuthFooter","AuthFooterLink","AuthPageHeader","AuthorizationContextBanner","PasswordStrength","TermsCheckboxList","LabeledSeparator","AuthLayout","IssuaryError","buildAuthenticatedAuthorizeUrl","extractOAuthParams","isOAuthFlow","tick","appConfigQueryOptions","registerMutationOptions","getSessionQueryOptions","getTermsQueryOptions","Route","Register","t","i18n","navigate","queryClient","search","useSearch","lang","language","deferredLang","data","configData","termsData","isPasswordAuthEnabled","auth","password","enabled","passwordPolicy","policy","hasTerms","terms","length","explicitTerms","filter","term","consentMode","hasExplicitTerms","implicitNotice","registration","signup_notice","fallback_language","registerSchema","object","email","string","min","pipe","min_length","count","max","max_length","termsConsents","Object","fromEntries","map","id","required","literal","message","boolean","RegisterFormValues","infer","registerMutation","onSuccess","params","user","email_verification_required","email_verified","to","setQueryData","queryKey","second_factor_required","available_2fa_methods","SecondFactorMethod","totp","push","passkey","method","passkey_name","window","location","href","onError","error","code","setError","type","onSettled","invalidateQueries","register","handleSubmit","control","setValue","watch","formState","errors","defaultValues","resolver","mode","passwordValue","onSubmit","values","consents","termsId","agreed","undefined","mutate","__html","isPending","component"],"sources":["../../../frontend/src/queries/register.ts","../../../frontend/src/routes/register/index.tsx?tsr-split=component"],"sourcesContent":["import { mutationOptions } from '@tanstack/react-query';\nimport type { InferResponseType } from 'hono/client';\nimport { client, jsonOk } from '#frontend/libs/api.ts';\nimport type { TermsConsentItem } from './terms';\n\nexport type RegisterParams = {\n email: string;\n password: string;\n consents?: TermsConsentItem[];\n};\n\nexport type RegisterResponse = InferResponseType<\n (typeof client.api.auth.register)['$post'],\n 200\n>;\n\nexport const registerMutationOptions = mutationOptions({\n mutationFn: async (values: RegisterParams) => {\n const res = await client.api.auth.register.$post({\n json: values,\n header: {},\n });\n return jsonOk(res);\n },\n});\n","import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';\nimport {\n useMutation,\n useQueryClient,\n useSuspenseQuery,\n} from '@tanstack/react-query';\nimport {\n createFileRoute,\n Link,\n redirect,\n useNavigate,\n} from '@tanstack/react-router';\nimport { TRButton } from '@tinyrack/ui/components/button';\nimport { LockIcon, MailIcon } from 'lucide-react';\nimport { useDeferredValue, useMemo } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport { AuthField } from '#frontend/components/auth/auth-field.tsx';\nimport {\n AuthFooter,\n AuthFooterLink,\n} from '#frontend/components/auth/auth-footer.tsx';\nimport { AuthPageHeader } from '#frontend/components/auth/auth-page-header.tsx';\nimport { AuthorizationContextBanner } from '#frontend/components/auth/authorization-context-banner.tsx';\nimport { PasswordStrength } from '#frontend/components/auth/password-strength.tsx';\nimport { TermsCheckboxList } from '#frontend/components/terms/terms-checkbox-list.tsx';\nimport { LabeledSeparator } from '#frontend/components/ui/labeled-separator.tsx';\nimport { RouteErrorFallback } from '#frontend/components/ui/route-error-fallback.tsx';\nimport { AuthLayout } from '#frontend/features/layout/auth-layout.tsx';\nimport { IssuaryError } from '#frontend/libs/error.ts';\n\nimport {\n buildAuthenticatedAuthorizeUrl,\n extractOAuthParams,\n hasAuthorizationContext,\n isOAuthFlow,\n OAuthSearchSchema,\n type SecondFactorMethod,\n} from '#frontend/libs/oauth-search.ts';\nimport { tick } from '#frontend/libs/promise.ts';\nimport { getAuthorizationContextQueryOptions } from '#frontend/queries/authorization-context.ts';\nimport { appConfigQueryOptions } from '#frontend/queries/config.ts';\nimport { registerMutationOptions } from '#frontend/queries/register.ts';\nimport { getSessionQueryOptions } from '#frontend/queries/session.ts';\nimport { getTermsQueryOptions } from '#frontend/queries/terms.ts';\n\nexport const Route = createFileRoute('/register/')({\n component: Register,\n errorComponent: RouteErrorFallback,\n validateSearch: OAuthSearchSchema,\n beforeLoad: async ({ context }) => {\n const config = await context.queryClient.ensureQueryData(\n appConfigQueryOptions,\n );\n if (!config.registration.public_registration) {\n throw redirect({\n to: '/',\n replace: true,\n });\n }\n },\n loaderDeps: ({ search }) => ({\n lang: search.lang,\n search,\n }),\n loader: async ({ context, deps }) => {\n const lang = deps.lang ?? context.i18n.language;\n const queries: Array<Promise<unknown>> = [\n context.queryClient.ensureQueryData(appConfigQueryOptions),\n context.queryClient.ensureQueryData(getTermsQueryOptions(lang)),\n ];\n\n if (hasAuthorizationContext(deps.search)) {\n queries.push(\n context.queryClient.ensureQueryData(\n getAuthorizationContextQueryOptions(deps.search),\n ),\n );\n }\n\n await Promise.all(queries);\n },\n});\n\nfunction Register() {\n const { t, i18n } = useTranslation();\n const navigate = useNavigate();\n const queryClient = useQueryClient();\n const search = Route.useSearch();\n\n const lang = search.lang ?? i18n.language;\n const deferredLang = useDeferredValue(lang);\n\n const { data: configData } = useSuspenseQuery(appConfigQueryOptions);\n const { data: termsData } = useSuspenseQuery(\n getTermsQueryOptions(deferredLang),\n );\n\n const isPasswordAuthEnabled = configData.auth.password.enabled;\n const passwordPolicy = configData.auth.password.policy;\n\n // Check if there are any terms to display\n const hasTerms = termsData.terms.length > 0;\n const explicitTerms = useMemo(\n () => termsData.terms.filter((term) => term.consentMode === 'explicit'),\n [termsData.terms],\n );\n const hasExplicitTerms = explicitTerms.length > 0;\n\n // Get implicit notice from config\n const implicitNotice =\n configData.registration.signup_notice?.[lang] ??\n configData.registration.signup_notice?.[configData.i18n.fallback_language];\n\n const registerSchema = useMemo(\n () =>\n z.object({\n email: z\n .string()\n .min(1, t('validation.email.required'))\n .pipe(z.email(t('validation.email.invalid'))),\n password: z\n .string()\n .min(\n passwordPolicy.min_length,\n t('validation.password.min', {\n count: passwordPolicy.min_length,\n }),\n )\n .max(\n passwordPolicy.max_length,\n t('validation.password.max', {\n count: passwordPolicy.max_length,\n }),\n ),\n termsConsents: z.object(\n Object.fromEntries(\n explicitTerms.map((term) => [\n term.id,\n term.required\n ? z.literal(true, {\n message: t('validation.terms.required'),\n })\n : z.boolean(),\n ]),\n ),\n ),\n }),\n [explicitTerms, passwordPolicy.max_length, passwordPolicy.min_length, t],\n );\n\n type RegisterFormValues = z.infer<typeof registerSchema>;\n\n const registerMutation = useMutation({\n ...registerMutationOptions,\n onSuccess: async (data, params) => {\n const user = data.user;\n\n if (user.email_verification_required && !user.email_verified) {\n return navigate({\n to: '/verify/email',\n search: {\n email: params.email,\n ...extractOAuthParams(search),\n },\n });\n }\n\n queryClient.setQueryData(getSessionQueryOptions.queryKey, {\n user: user,\n });\n await tick();\n\n if (user.second_factor_required) {\n const available_2fa_methods: SecondFactorMethod[] = [];\n if (configData.auth.password.totp.enabled) {\n available_2fa_methods.push('totp');\n }\n if (configData.auth.passkey.enabled) {\n available_2fa_methods.push('passkey');\n }\n\n if (available_2fa_methods.length === 1) {\n const method = available_2fa_methods[0];\n if (method === 'totp') {\n return navigate({\n to: '/setup/totp',\n search: extractOAuthParams(search),\n });\n } else {\n return navigate({\n to: '/setup/passkey',\n search: {\n ...extractOAuthParams(search),\n passkey_name: 'default',\n },\n });\n }\n } else {\n return navigate({\n to: '/setup/2fa',\n search: extractOAuthParams(search),\n });\n }\n }\n\n if (isOAuthFlow(search)) {\n window.location.href = buildAuthenticatedAuthorizeUrl(search);\n } else {\n navigate({ to: '/profile' });\n }\n },\n onError: (error) => {\n if (error instanceof IssuaryError) {\n if (error.code === 'REGISTRATION_EMAIL_NOT_ALLOWED') {\n setError('email', {\n type: 'manual',\n message: t('register.error.emailNotAllowed'),\n });\n } else if (error.code === 'REGISTRATION_DISABLED') {\n setError('email', {\n type: 'manual',\n message: t('register.error.registrationDisabled'),\n });\n } else {\n setError('email', {\n type: 'manual',\n message: t('register.error.emailExists'),\n });\n }\n } else {\n setError('email', {\n type: 'manual',\n message: t('register.error.emailExists'),\n });\n }\n },\n onSettled: async () => {\n await queryClient.invalidateQueries({\n queryKey: getSessionQueryOptions.queryKey,\n });\n },\n });\n\n const {\n register,\n setError,\n handleSubmit,\n control,\n setValue,\n watch,\n formState: { errors },\n } = useForm<RegisterFormValues>({\n defaultValues: {\n email: '',\n password: '',\n termsConsents: Object.fromEntries(\n explicitTerms.map((term) => [term.id, false]),\n ),\n },\n resolver: standardSchemaResolver(registerSchema),\n mode: 'onChange',\n });\n\n const passwordValue = watch('password');\n\n const onSubmit = (values: RegisterFormValues) => {\n // Build consents array for registration\n // Only send explicit consents - implicit ones are handled by backend\n const consents = hasTerms\n ? explicitTerms.map((term) => ({\n termsId: term.id,\n agreed: values.termsConsents[term.id] ?? false,\n }))\n : undefined;\n\n registerMutation.mutate({\n email: values.email,\n password: values.password,\n consents,\n });\n };\n\n return (\n <AuthLayout>\n <AuthPageHeader\n subtitle={t('register.subtitle')}\n title={t('register.title')}\n />\n\n <AuthorizationContextBanner search={search} />\n\n {isPasswordAuthEnabled && (\n <form\n className=\"flex flex-col gap-tinyrack-lg\"\n onSubmit={handleSubmit(onSubmit)}\n >\n <AuthField\n autoComplete=\"email\"\n error={errors.email}\n icon={MailIcon}\n label={t('register.email.label')}\n placeholder={t('register.email.placeholder')}\n {...register('email')}\n type=\"email\"\n />\n\n <div className=\"flex flex-col gap-tinyrack-sm\">\n <AuthField\n autoComplete=\"new-password\"\n error={errors.password}\n hint={t('register.password.hint', {\n count: passwordPolicy.min_length,\n })}\n icon={LockIcon}\n label={t('register.password.label')}\n placeholder={t('register.password.placeholder')}\n {...register('password')}\n type=\"password\"\n />\n\n <PasswordStrength\n password={passwordValue}\n policy={passwordPolicy}\n />\n </div>\n\n <div className=\"flex flex-col gap-tinyrack-md\">\n {implicitNotice && (\n <div className=\"text-center text-tinyrack-text-muted text-tinyrack-xs\">\n <div\n className=\"prose prose-sm text-tinyrack-xs! **:text-tinyrack-xs!\"\n dangerouslySetInnerHTML={{\n __html: implicitNotice,\n }}\n data-testid=\"terms-implicit-notice\"\n />\n </div>\n )}\n\n {implicitNotice && hasExplicitTerms && (\n <LabeledSeparator label={t('terms.additionalOptionalConsent')} />\n )}\n\n {hasTerms && hasExplicitTerms && (\n <TermsCheckboxList\n control={control}\n disabled={registerMutation.isPending}\n errors={errors}\n setValue={setValue}\n terms={explicitTerms}\n />\n )}\n </div>\n\n <TRButton\n className=\"w-full\"\n intent=\"primary\"\n loading={registerMutation.isPending}\n loadingLabel={t('register.submitting')}\n type=\"submit\"\n uiSize=\"lg\"\n >\n {t('register.submit')}\n </TRButton>\n </form>\n )}\n\n <AuthFooter>\n <AuthFooterLink\n link={\n <Link search={extractOAuthParams(search)} to=\"/login\">\n {t('register.link.login')}\n </Link>\n }\n text={t('register.footer.haveAccount')}\n />\n </AuthFooter>\n </AuthLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"register-SFXa5EeL.js","names":["standardSchemaResolver","useMutation","useQueryClient","useSuspenseQuery","Link","useNavigate","TRButton","LockIcon","MailIcon","useDeferredValue","useMemo","useForm","useTranslation","z","AuthField","AuthFooter","AuthFooterLink","AuthPageHeader","AuthorizationContextBanner","PasswordStrength","TermsCheckboxList","LabeledSeparator","AuthLayout","IssuaryError","buildAuthenticatedAuthorizeUrl","extractOAuthParams","isOAuthFlow","tick","appConfigQueryOptions","registerMutationOptions","getSessionQueryOptions","getTermsQueryOptions","Route","Register","t","i18n","navigate","queryClient","search","useSearch","lang","language","deferredLang","data","configData","termsData","isPasswordAuthEnabled","auth","password","enabled","passwordPolicy","policy","hasTerms","terms","length","explicitTerms","filter","term","consentMode","hasExplicitTerms","implicitNotice","registration","signup_notice","fallback_language","registerSchema","object","email","string","min","pipe","min_length","count","max","max_length","termsConsents","Object","fromEntries","map","id","required","literal","message","boolean","RegisterFormValues","infer","registerMutation","onSuccess","params","user","email_verification_required","email_verified","to","setQueryData","queryKey","second_factor_required","available_2fa_methods","SecondFactorMethod","totp","push","passkey","method","passkey_name","window","location","href","onError","error","code","setError","type","onSettled","invalidateQueries","register","handleSubmit","control","setValue","watch","formState","errors","defaultValues","resolver","mode","passwordValue","onSubmit","values","consents","termsId","agreed","undefined","mutate","__html","isPending","component"],"sources":["../../../frontend/src/queries/register.ts","../../../frontend/src/routes/register/index.tsx?tsr-split=component"],"sourcesContent":["import { mutationOptions } from '@tanstack/react-query';\nimport type { InferResponseType } from 'hono/client';\nimport { client, jsonOk } from '#frontend/libs/api.ts';\nimport type { TermsConsentItem } from './terms';\n\nexport type RegisterParams = {\n email: string;\n password: string;\n consents?: TermsConsentItem[];\n};\n\nexport type RegisterResponse = InferResponseType<\n (typeof client.api.auth.register)['$post'],\n 200\n>;\n\nexport const registerMutationOptions = mutationOptions({\n mutationFn: async (values: RegisterParams) => {\n const res = await client.api.auth.register.$post({\n json: values,\n header: {},\n });\n return jsonOk(res);\n },\n});\n","import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';\nimport {\n useMutation,\n useQueryClient,\n useSuspenseQuery,\n} from '@tanstack/react-query';\nimport {\n createFileRoute,\n Link,\n redirect,\n useNavigate,\n} from '@tanstack/react-router';\nimport { TRButton } from '@tinyrack/ui/components/button';\nimport { LockIcon, MailIcon } from 'lucide-react';\nimport { useDeferredValue, useMemo } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport { AuthField } from '#frontend/components/auth/auth-field.tsx';\nimport {\n AuthFooter,\n AuthFooterLink,\n} from '#frontend/components/auth/auth-footer.tsx';\nimport { AuthPageHeader } from '#frontend/components/auth/auth-page-header.tsx';\nimport { AuthorizationContextBanner } from '#frontend/components/auth/authorization-context-banner.tsx';\nimport { PasswordStrength } from '#frontend/components/auth/password-strength.tsx';\nimport { TermsCheckboxList } from '#frontend/components/terms/terms-checkbox-list.tsx';\nimport { LabeledSeparator } from '#frontend/components/ui/labeled-separator.tsx';\nimport { RouteErrorFallback } from '#frontend/components/ui/route-error-fallback.tsx';\nimport { AuthLayout } from '#frontend/features/layout/auth-layout.tsx';\nimport { IssuaryError } from '#frontend/libs/error.ts';\n\nimport {\n buildAuthenticatedAuthorizeUrl,\n extractOAuthParams,\n hasAuthorizationContext,\n isOAuthFlow,\n OAuthSearchSchema,\n type SecondFactorMethod,\n} from '#frontend/libs/oauth-search.ts';\nimport { tick } from '#frontend/libs/promise.ts';\nimport { getAuthorizationContextQueryOptions } from '#frontend/queries/authorization-context.ts';\nimport { appConfigQueryOptions } from '#frontend/queries/config.ts';\nimport { registerMutationOptions } from '#frontend/queries/register.ts';\nimport { getSessionQueryOptions } from '#frontend/queries/session.ts';\nimport { getTermsQueryOptions } from '#frontend/queries/terms.ts';\n\nexport const Route = createFileRoute('/register/')({\n component: Register,\n errorComponent: RouteErrorFallback,\n validateSearch: OAuthSearchSchema,\n beforeLoad: async ({ context }) => {\n const config = await context.queryClient.ensureQueryData(\n appConfigQueryOptions,\n );\n if (!config.registration.public_registration) {\n throw redirect({\n to: '/',\n replace: true,\n });\n }\n },\n loaderDeps: ({ search }) => ({\n lang: search.lang,\n search,\n }),\n loader: async ({ context, deps }) => {\n const lang = deps.lang ?? context.i18n.language;\n const queries: Array<Promise<unknown>> = [\n context.queryClient.ensureQueryData(appConfigQueryOptions),\n context.queryClient.ensureQueryData(getTermsQueryOptions(lang)),\n ];\n\n if (hasAuthorizationContext(deps.search)) {\n queries.push(\n context.queryClient.ensureQueryData(\n getAuthorizationContextQueryOptions(deps.search),\n ),\n );\n }\n\n await Promise.all(queries);\n },\n});\n\nfunction Register() {\n const { t, i18n } = useTranslation();\n const navigate = useNavigate();\n const queryClient = useQueryClient();\n const search = Route.useSearch();\n\n const lang = search.lang ?? i18n.language;\n const deferredLang = useDeferredValue(lang);\n\n const { data: configData } = useSuspenseQuery(appConfigQueryOptions);\n const { data: termsData } = useSuspenseQuery(\n getTermsQueryOptions(deferredLang),\n );\n\n const isPasswordAuthEnabled = configData.auth.password.enabled;\n const passwordPolicy = configData.auth.password.policy;\n\n // Check if there are any terms to display\n const hasTerms = termsData.terms.length > 0;\n const explicitTerms = useMemo(\n () => termsData.terms.filter((term) => term.consentMode === 'explicit'),\n [termsData.terms],\n );\n const hasExplicitTerms = explicitTerms.length > 0;\n\n // Get implicit notice from config\n const implicitNotice =\n configData.registration.signup_notice?.[lang] ??\n configData.registration.signup_notice?.[configData.i18n.fallback_language];\n\n const registerSchema = useMemo(\n () =>\n z.object({\n email: z\n .string()\n .min(1, t('validation.email.required'))\n .pipe(z.email(t('validation.email.invalid'))),\n password: z\n .string()\n .min(\n passwordPolicy.min_length,\n t('validation.password.min', {\n count: passwordPolicy.min_length,\n }),\n )\n .max(\n passwordPolicy.max_length,\n t('validation.password.max', {\n count: passwordPolicy.max_length,\n }),\n ),\n termsConsents: z.object(\n Object.fromEntries(\n explicitTerms.map((term) => [\n term.id,\n term.required\n ? z.literal(true, {\n message: t('validation.terms.required'),\n })\n : z.boolean(),\n ]),\n ),\n ),\n }),\n [explicitTerms, passwordPolicy.max_length, passwordPolicy.min_length, t],\n );\n\n type RegisterFormValues = z.infer<typeof registerSchema>;\n\n const registerMutation = useMutation({\n ...registerMutationOptions,\n onSuccess: async (data, params) => {\n const user = data.user;\n\n if (user.email_verification_required && !user.email_verified) {\n return navigate({\n to: '/verify/email',\n search: {\n email: params.email,\n ...extractOAuthParams(search),\n },\n });\n }\n\n queryClient.setQueryData(getSessionQueryOptions.queryKey, {\n user: user,\n });\n await tick();\n\n if (user.second_factor_required) {\n const available_2fa_methods: SecondFactorMethod[] = [];\n if (configData.auth.password.totp.enabled) {\n available_2fa_methods.push('totp');\n }\n if (configData.auth.passkey.enabled) {\n available_2fa_methods.push('passkey');\n }\n\n if (available_2fa_methods.length === 1) {\n const method = available_2fa_methods[0];\n if (method === 'totp') {\n return navigate({\n to: '/setup/totp',\n search: extractOAuthParams(search),\n });\n } else {\n return navigate({\n to: '/setup/passkey',\n search: {\n ...extractOAuthParams(search),\n passkey_name: 'default',\n },\n });\n }\n } else {\n return navigate({\n to: '/setup/2fa',\n search: extractOAuthParams(search),\n });\n }\n }\n\n if (isOAuthFlow(search)) {\n window.location.href = buildAuthenticatedAuthorizeUrl(search);\n } else {\n navigate({ to: '/profile' });\n }\n },\n onError: (error) => {\n if (error instanceof IssuaryError) {\n if (error.code === 'REGISTRATION_EMAIL_NOT_ALLOWED') {\n setError('email', {\n type: 'manual',\n message: t('register.error.emailNotAllowed'),\n });\n } else if (error.code === 'REGISTRATION_DISABLED') {\n setError('email', {\n type: 'manual',\n message: t('register.error.registrationDisabled'),\n });\n } else {\n setError('email', {\n type: 'manual',\n message: t('register.error.emailExists'),\n });\n }\n } else {\n setError('email', {\n type: 'manual',\n message: t('register.error.emailExists'),\n });\n }\n },\n onSettled: async () => {\n await queryClient.invalidateQueries({\n queryKey: getSessionQueryOptions.queryKey,\n });\n },\n });\n\n const {\n register,\n setError,\n handleSubmit,\n control,\n setValue,\n watch,\n formState: { errors },\n } = useForm<RegisterFormValues>({\n defaultValues: {\n email: '',\n password: '',\n termsConsents: Object.fromEntries(\n explicitTerms.map((term) => [term.id, false]),\n ),\n },\n resolver: standardSchemaResolver(registerSchema),\n mode: 'onChange',\n });\n\n const passwordValue = watch('password');\n\n const onSubmit = (values: RegisterFormValues) => {\n // Build consents array for registration\n // Only send explicit consents - implicit ones are handled by backend\n const consents = hasTerms\n ? explicitTerms.map((term) => ({\n termsId: term.id,\n agreed: values.termsConsents[term.id] ?? false,\n }))\n : undefined;\n\n registerMutation.mutate({\n email: values.email,\n password: values.password,\n consents,\n });\n };\n\n return (\n <AuthLayout>\n <AuthPageHeader\n subtitle={t('register.subtitle')}\n title={t('register.title')}\n />\n\n <AuthorizationContextBanner search={search} />\n\n {isPasswordAuthEnabled && (\n <form\n className=\"flex flex-col gap-tinyrack-lg\"\n onSubmit={handleSubmit(onSubmit)}\n >\n <AuthField\n autoComplete=\"email\"\n error={errors.email}\n icon={MailIcon}\n label={t('register.email.label')}\n placeholder={t('register.email.placeholder')}\n {...register('email')}\n type=\"email\"\n />\n\n <div className=\"flex flex-col gap-tinyrack-sm\">\n <AuthField\n autoComplete=\"new-password\"\n error={errors.password}\n hint={t('register.password.hint', {\n count: passwordPolicy.min_length,\n })}\n icon={LockIcon}\n label={t('register.password.label')}\n placeholder={t('register.password.placeholder')}\n {...register('password')}\n type=\"password\"\n />\n\n <PasswordStrength\n password={passwordValue}\n policy={passwordPolicy}\n />\n </div>\n\n <div className=\"flex flex-col gap-tinyrack-md\">\n {implicitNotice && (\n <div className=\"text-center text-tinyrack-text-muted text-tinyrack-xs\">\n <div\n className=\"prose prose-sm text-tinyrack-xs! **:text-tinyrack-xs!\"\n dangerouslySetInnerHTML={{\n __html: implicitNotice,\n }}\n data-testid=\"terms-implicit-notice\"\n />\n </div>\n )}\n\n {implicitNotice && hasExplicitTerms && (\n <LabeledSeparator label={t('terms.additionalOptionalConsent')} />\n )}\n\n {hasTerms && hasExplicitTerms && (\n <TermsCheckboxList\n control={control}\n disabled={registerMutation.isPending}\n errors={errors}\n setValue={setValue}\n terms={explicitTerms}\n />\n )}\n </div>\n\n <TRButton\n className=\"w-full\"\n intent=\"primary\"\n loading={registerMutation.isPending}\n loadingLabel={t('register.submitting')}\n type=\"submit\"\n uiSize=\"lg\"\n >\n {t('register.submit')}\n </TRButton>\n </form>\n )}\n\n <AuthFooter>\n <AuthFooterLink\n link={\n <Link search={extractOAuthParams(search)} to=\"/login\">\n {t('register.link.login')}\n </Link>\n }\n text={t('register.footer.haveAccount')}\n />\n </AuthFooter>\n </AuthLayout>\n );\n}\n"],"mappings":"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"}
|