@tinyrack/tinyauth-server 0.6.0 → 0.6.1

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.
Files changed (107) hide show
  1. package/dist/entrypoints/app.d.ts +1 -1
  2. package/dist/routes/api/consent/index.d.ts +1 -1
  3. package/dist/routes/api/consent/post.d.ts +1 -1
  4. package/dist/routes/api/consent/post.d.ts.map +1 -1
  5. package/dist/routes/api/consent/post.js +9 -1
  6. package/dist/routes/api/consent/post.js.map +1 -1
  7. package/dist/routes/api/index.d.ts +1 -1
  8. package/dist/routes/index.d.ts +1 -1
  9. package/dist/routes/oauth/authorize/get.d.ts.map +1 -1
  10. package/dist/routes/oauth/authorize/get.js +9 -1
  11. package/dist/routes/oauth/authorize/get.js.map +1 -1
  12. package/dist/services/account-selection.service.d.ts.map +1 -1
  13. package/dist/services/account-selection.service.js +3 -1
  14. package/dist/services/account-selection.service.js.map +1 -1
  15. package/dist/services/oauth-authorize.service.d.ts.map +1 -1
  16. package/dist/services/oauth-authorize.service.js +15 -15
  17. package/dist/services/oauth-authorize.service.js.map +1 -1
  18. package/package.json +1 -1
  19. package/public/assets/2fa-BMEIiDXv.js +2 -0
  20. package/public/assets/{2fa-SSKfXB7c.js.map → 2fa-BMEIiDXv.js.map} +1 -1
  21. package/public/assets/{2fa-BoyBKrjD.js → 2fa-BhCgCfEc.js} +2 -2
  22. package/public/assets/{2fa-BoyBKrjD.js.map → 2fa-BhCgCfEc.js.map} +1 -1
  23. package/public/assets/2fa-Cteolf9l.js +2 -0
  24. package/public/assets/{2fa-IkQlgUP0.js.map → 2fa-Cteolf9l.js.map} +1 -1
  25. package/public/assets/{2fa-DfWvDjDW.js → 2fa-E0x3Samn.js} +2 -2
  26. package/public/assets/{2fa-DfWvDjDW.js.map → 2fa-E0x3Samn.js.map} +1 -1
  27. package/public/assets/{admin-D2CMlWzS.js → admin-CXJ0z2QN.js} +2 -2
  28. package/public/assets/{admin-D2CMlWzS.js.map → admin-CXJ0z2QN.js.map} +1 -1
  29. package/public/assets/{consent-DwuWkp63.js → consent-B17hGrta.js} +2 -2
  30. package/public/assets/{consent-DwuWkp63.js.map → consent-B17hGrta.js.map} +1 -1
  31. package/public/assets/{consent-C5Qo0iLd.js → consent-C-h_pWHQ.js} +2 -2
  32. package/public/assets/{consent-C5Qo0iLd.js.map → consent-C-h_pWHQ.js.map} +1 -1
  33. package/public/assets/email-BMEIiDXv.js +2 -0
  34. package/public/assets/{email-SSKfXB7c.js.map → email-BMEIiDXv.js.map} +1 -1
  35. package/public/assets/{email-CIttZRBe.js → email-DjE7mqnX.js} +2 -2
  36. package/public/assets/{email-CIttZRBe.js.map → email-DjE7mqnX.js.map} +1 -1
  37. package/public/assets/{error-D60wkdWN.js → error-BKXsXhdR.js} +2 -2
  38. package/public/assets/{error-D60wkdWN.js.map → error-BKXsXhdR.js.map} +1 -1
  39. package/public/assets/forgot-BMEIiDXv.js +2 -0
  40. package/public/assets/{forgot-SSKfXB7c.js.map → forgot-BMEIiDXv.js.map} +1 -1
  41. package/public/assets/{forgot-x-UDyHXT.js → forgot-C9zSfaX9.js} +2 -2
  42. package/public/assets/{forgot-x-UDyHXT.js.map → forgot-C9zSfaX9.js.map} +1 -1
  43. package/public/assets/{index-CsT6OVnP.js → index-BmfaaNx6.js} +3 -3
  44. package/public/assets/index-BmfaaNx6.js.map +1 -0
  45. package/public/assets/login-BMEIiDXv.js +2 -0
  46. package/public/assets/login-BMEIiDXv.js.map +1 -0
  47. package/public/assets/login-DG1Yfmb8.js +2 -0
  48. package/public/assets/login-DG1Yfmb8.js.map +1 -0
  49. package/public/assets/{passkey-BdISbWr7.js → passkey-BDrX3jVg.js} +2 -2
  50. package/public/assets/{passkey-BdISbWr7.js.map → passkey-BDrX3jVg.js.map} +1 -1
  51. package/public/assets/{passkey-Bv7zPLAZ.js → passkey-C-h8Fvqn.js} +2 -2
  52. package/public/assets/{passkey-Bv7zPLAZ.js.map → passkey-C-h8Fvqn.js.map} +1 -1
  53. package/public/assets/password-86C-hCze.js +2 -0
  54. package/public/assets/password-86C-hCze.js.map +1 -0
  55. package/public/assets/password-BMEIiDXv.js +2 -0
  56. package/public/assets/password-BMEIiDXv.js.map +1 -0
  57. package/public/assets/{profile-D2cuVYgE.js → profile-8buOY6GT.js} +2 -2
  58. package/public/assets/{profile-D2cuVYgE.js.map → profile-8buOY6GT.js.map} +1 -1
  59. package/public/assets/{profile-TKdT20x5.js → profile-DcpL_XUt.js} +2 -2
  60. package/public/assets/{profile-TKdT20x5.js.map → profile-DcpL_XUt.js.map} +1 -1
  61. package/public/assets/{recovery-DM8h2gbb.js → recovery-Csf6dtvj.js} +2 -2
  62. package/public/assets/{recovery-DM8h2gbb.js.map → recovery-Csf6dtvj.js.map} +1 -1
  63. package/public/assets/register-BMEIiDXv.js +2 -0
  64. package/public/assets/{register-SSKfXB7c.js.map → register-BMEIiDXv.js.map} +1 -1
  65. package/public/assets/{register-vWW_43cD.js → register-X6dycne4.js} +2 -2
  66. package/public/assets/{register-vWW_43cD.js.map → register-X6dycne4.js.map} +1 -1
  67. package/public/assets/{reset-CgACYrdp.js → reset-1nsqPlxL.js} +2 -2
  68. package/public/assets/{reset-CgACYrdp.js.map → reset-1nsqPlxL.js.map} +1 -1
  69. package/public/assets/reset-BMEIiDXv.js +2 -0
  70. package/public/assets/{reset-SSKfXB7c.js.map → reset-BMEIiDXv.js.map} +1 -1
  71. package/public/assets/select-BMEIiDXv.js +2 -0
  72. package/public/assets/select-BMEIiDXv.js.map +1 -0
  73. package/public/assets/{select-BCP5fwfB.js → select-CDdb3jyc.js} +2 -2
  74. package/public/assets/select-CDdb3jyc.js.map +1 -0
  75. package/public/assets/{terms-DPWrbYY2.js → terms-CUhvMN9k.js} +2 -2
  76. package/public/assets/{terms-DPWrbYY2.js.map → terms-CUhvMN9k.js.map} +1 -1
  77. package/public/assets/{terms-TKdT20x5.js → terms-DcpL_XUt.js} +2 -2
  78. package/public/assets/{terms-TKdT20x5.js.map → terms-DcpL_XUt.js.map} +1 -1
  79. package/public/assets/{totp-CKZ6N1NS.js → totp-DB9WqW4V.js} +2 -2
  80. package/public/assets/{totp-CKZ6N1NS.js.map → totp-DB9WqW4V.js.map} +1 -1
  81. package/public/assets/{totp-D-PVOsGQ.js → totp-UDT2P91H.js} +2 -2
  82. package/public/assets/{totp-D-PVOsGQ.js.map → totp-UDT2P91H.js.map} +1 -1
  83. package/public/assets/{use-totp-setup-BH75uEbE.js → use-totp-setup-9fktn_he.js} +2 -2
  84. package/public/assets/{use-totp-setup-BH75uEbE.js.map → use-totp-setup-9fktn_he.js.map} +1 -1
  85. package/public/assets/{useMutation-DVMopbtG.js → useMutation-Iu4AJCB4.js} +2 -2
  86. package/public/assets/{useMutation-DVMopbtG.js.map → useMutation-Iu4AJCB4.js.map} +1 -1
  87. package/public/assets/{users-B7ofdp72.js → users-Bi7gjreq.js} +2 -2
  88. package/public/assets/{users-B7ofdp72.js.map → users-Bi7gjreq.js.map} +1 -1
  89. package/public/index.html +1 -1
  90. package/public/assets/2fa-IkQlgUP0.js +0 -2
  91. package/public/assets/2fa-SSKfXB7c.js +0 -2
  92. package/public/assets/email-SSKfXB7c.js +0 -2
  93. package/public/assets/forgot-SSKfXB7c.js +0 -2
  94. package/public/assets/index-CsT6OVnP.js.map +0 -1
  95. package/public/assets/login-DhbnCudI.js +0 -2
  96. package/public/assets/login-DhbnCudI.js.map +0 -1
  97. package/public/assets/login-SSKfXB7c.js +0 -2
  98. package/public/assets/login-SSKfXB7c.js.map +0 -1
  99. package/public/assets/password-CkeV4qxb.js +0 -2
  100. package/public/assets/password-CkeV4qxb.js.map +0 -1
  101. package/public/assets/password-SSKfXB7c.js +0 -2
  102. package/public/assets/password-SSKfXB7c.js.map +0 -1
  103. package/public/assets/register-SSKfXB7c.js +0 -2
  104. package/public/assets/reset-SSKfXB7c.js +0 -2
  105. package/public/assets/select-BCP5fwfB.js.map +0 -1
  106. package/public/assets/select-SSKfXB7c.js +0 -2
  107. package/public/assets/select-SSKfXB7c.js.map +0 -1
@@ -1,2 +1,2 @@
1
- import{o as e}from"./IconBase.es-d5KP98Ac.js";import{N as t}from"./index-CsT6OVnP.js";var n=e();function r(e){return(0,n.jsx)(t,{...e,onUnauthorized:()=>{window.location.href=`/login`}})}export{r as errorComponent};
2
- //# sourceMappingURL=profile-TKdT20x5.js.map
1
+ import{o as e}from"./IconBase.es-d5KP98Ac.js";import{N as t}from"./index-BmfaaNx6.js";var n=e();function r(e){return(0,n.jsx)(t,{...e,onUnauthorized:()=>{window.location.href=`/login`}})}export{r as errorComponent};
2
+ //# sourceMappingURL=profile-DcpL_XUt.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"profile-TKdT20x5.js","names":["RouteErrorFallback","PasswordModalType","TotpModalType","PasskeyModalType","ProfileError","props","ErrorComponentProps","window","location","href","errorComponent"],"sources":["../../../frontend/src/routes/profile/index.tsx?tsr-split=errorComponent"],"sourcesContent":["import { SignOutIcon, WarningCircleIcon } from '@phosphor-icons/react';\nimport {\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 { useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\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 { PageLayout } from '#frontend/features/layout/page-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 // Handle non-authenticated states (should be redirected, but fallback)\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 // Check if TOTP and Passkey are enabled in config\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 // Account deletion settings\n const accountDeletionEnabled = appConfig.account_deletion.enabled;\n const retentionPeriod = appConfig.account_deletion.retention;\n\n // Parse retention period to get days for display\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 // Determine which security sections to show\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 <PageLayout maxWidth=\"xl\" responsivePadding>\n {/* Header */}\n <div className=\"border-base-200 border-b p-6\">\n <div className=\"flex items-start justify-between gap-4\">\n <div className=\"flex items-center gap-4\">\n <InitialAvatar email={user.email} size=\"lg\" />\n <div className=\"min-w-0\">\n <h1 className=\"font-bold text-xl\">{t('profile.title')}</h1>\n <p\n className=\"truncate text-base-content/70 text-sm\"\n data-testid=\"profile-user-email\"\n >\n {user.email}\n </p>\n </div>\n </div>\n <button\n className=\"btn btn-ghost btn-sm gap-2\"\n data-testid=\"profile-logout\"\n disabled={logoutMutation.isPending}\n onClick={() => logoutMutation.mutate()}\n type=\"button\"\n >\n {logoutMutation.isPending ? (\n <span className=\"loading loading-spinner loading-sm\" />\n ) : (\n <SignOutIcon className=\"size-4\" weight=\"bold\" />\n )}\n <span className=\"hidden sm:inline\">{t('profile.logout')}</span>\n </button>\n </div>\n </div>\n\n {/* OAuth Error Alert */}\n {oauthErrorMessage && (\n <div className=\"px-6 pt-4\">\n <Alert icon={WarningCircleIcon} type=\"error\">\n {oauthErrorMessage}\n </Alert>\n </div>\n )}\n\n {/* Content - Single Column */}\n <div className=\"space-y-5 p-6\">\n {/* Account Information */}\n {user && <UserInfoSection user={user} />}\n\n {/* Security Options */}\n {hasSecurityOptions && (\n <div className=\"rounded-xl border border-base-200\">\n <div className=\"border-base-200 border-b px-4 py-3\">\n <h2 className=\"font-semibold text-sm\">\n {t('profile.security.title')}\n </h2>\n <p className=\"text-base-content/60 text-xs\">\n {t('profile.security.description')}\n </p>\n </div>\n {showTotpSection && user.totp_recovery_codes_missing && (\n <div\n className=\"border-base-200 border-b px-4 py-3\"\n data-testid=\"profile-totp-recovery-warning\"\n >\n <Alert icon={WarningCircleIcon} type=\"warning\">\n {t('profile.totp.recoveryCodesMissing')}\n </Alert>\n </div>\n )}\n <div className=\"divide-y divide-base-200\">\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 </div>\n </div>\n )}\n\n {/* Linked OAuth Accounts */}\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 {/* Danger Zone */}\n <DangerZoneSection\n isConfigManaged={isConfigManaged}\n isDeletionEnabled={accountDeletionEnabled}\n onDeleteClick={() => setShowDeleteModal(true)}\n />\n </div>\n\n {/* Password Modals */}\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 {/* TOTP Modals */}\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 {/* Passkey Modals */}\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 {/* Unlink OAuth Modal */}\n <UnlinkOAuthModal\n isOpen={unlinkModal !== null}\n isPending={unlinkMutation.isPending}\n onClose={() => setUnlinkModal(null)}\n onConfirm={handleUnlinkConfirm}\n providerName={unlinkModal?.name ?? ''}\n />\n\n {/* Delete Account Modal */}\n <DeleteAccountModal\n isOpen={showDeleteModal}\n onClose={() => setShowDeleteModal(false)}\n retentionDays={retentionDays}\n />\n </PageLayout>\n );\n}\n"],"mappings":"gGA4DA,SAASI,EAAaC,EAA4B,CAChD,OACE,EAAA,EAAA,KAAC,EAAD,CACE,GAAIA,EACJ,mBAAsB,CACpBE,OAAOC,SAASC,KAAO,QACzB,CAAE,CAAA,CAGR"}
1
+ {"version":3,"file":"profile-DcpL_XUt.js","names":["RouteErrorFallback","PasswordModalType","TotpModalType","PasskeyModalType","ProfileError","props","ErrorComponentProps","window","location","href","errorComponent"],"sources":["../../../frontend/src/routes/profile/index.tsx?tsr-split=errorComponent"],"sourcesContent":["import { SignOutIcon, WarningCircleIcon } from '@phosphor-icons/react';\nimport {\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 { useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\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 { PageLayout } from '#frontend/features/layout/page-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 // Handle non-authenticated states (should be redirected, but fallback)\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 // Check if TOTP and Passkey are enabled in config\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 // Account deletion settings\n const accountDeletionEnabled = appConfig.account_deletion.enabled;\n const retentionPeriod = appConfig.account_deletion.retention;\n\n // Parse retention period to get days for display\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 // Determine which security sections to show\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 <PageLayout maxWidth=\"xl\" responsivePadding>\n {/* Header */}\n <div className=\"border-base-200 border-b p-6\">\n <div className=\"flex items-start justify-between gap-4\">\n <div className=\"flex items-center gap-4\">\n <InitialAvatar email={user.email} size=\"lg\" />\n <div className=\"min-w-0\">\n <h1 className=\"font-bold text-xl\">{t('profile.title')}</h1>\n <p\n className=\"truncate text-base-content/70 text-sm\"\n data-testid=\"profile-user-email\"\n >\n {user.email}\n </p>\n </div>\n </div>\n <button\n className=\"btn btn-ghost btn-sm gap-2\"\n data-testid=\"profile-logout\"\n disabled={logoutMutation.isPending}\n onClick={() => logoutMutation.mutate()}\n type=\"button\"\n >\n {logoutMutation.isPending ? (\n <span className=\"loading loading-spinner loading-sm\" />\n ) : (\n <SignOutIcon className=\"size-4\" weight=\"bold\" />\n )}\n <span className=\"hidden sm:inline\">{t('profile.logout')}</span>\n </button>\n </div>\n </div>\n\n {/* OAuth Error Alert */}\n {oauthErrorMessage && (\n <div className=\"px-6 pt-4\">\n <Alert icon={WarningCircleIcon} type=\"error\">\n {oauthErrorMessage}\n </Alert>\n </div>\n )}\n\n {/* Content - Single Column */}\n <div className=\"space-y-5 p-6\">\n {/* Account Information */}\n {user && <UserInfoSection user={user} />}\n\n {/* Security Options */}\n {hasSecurityOptions && (\n <div className=\"rounded-xl border border-base-200\">\n <div className=\"border-base-200 border-b px-4 py-3\">\n <h2 className=\"font-semibold text-sm\">\n {t('profile.security.title')}\n </h2>\n <p className=\"text-base-content/60 text-xs\">\n {t('profile.security.description')}\n </p>\n </div>\n {showTotpSection && user.totp_recovery_codes_missing && (\n <div\n className=\"border-base-200 border-b px-4 py-3\"\n data-testid=\"profile-totp-recovery-warning\"\n >\n <Alert icon={WarningCircleIcon} type=\"warning\">\n {t('profile.totp.recoveryCodesMissing')}\n </Alert>\n </div>\n )}\n <div className=\"divide-y divide-base-200\">\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 </div>\n </div>\n )}\n\n {/* Linked OAuth Accounts */}\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 {/* Danger Zone */}\n <DangerZoneSection\n isConfigManaged={isConfigManaged}\n isDeletionEnabled={accountDeletionEnabled}\n onDeleteClick={() => setShowDeleteModal(true)}\n />\n </div>\n\n {/* Password Modals */}\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 {/* TOTP Modals */}\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 {/* Passkey Modals */}\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 {/* Unlink OAuth Modal */}\n <UnlinkOAuthModal\n isOpen={unlinkModal !== null}\n isPending={unlinkMutation.isPending}\n onClose={() => setUnlinkModal(null)}\n onConfirm={handleUnlinkConfirm}\n providerName={unlinkModal?.name ?? ''}\n />\n\n {/* Delete Account Modal */}\n <DeleteAccountModal\n isOpen={showDeleteModal}\n onClose={() => setShowDeleteModal(false)}\n retentionDays={retentionDays}\n />\n </PageLayout>\n );\n}\n"],"mappings":"gGA4DA,SAASI,EAAaC,EAA4B,CAChD,OACE,EAAA,EAAA,KAAC,EAAD,CACE,GAAIA,EACJ,mBAAsB,CACpBE,OAAOC,SAASC,KAAO,QACzB,CAAE,CAAA,CAGR"}
@@ -1,2 +1,2 @@
1
- import{n as e,o as t,r as n,s as r,u as i}from"./IconBase.es-d5KP98Ac.js";import{f as a,h as o,ut as s}from"./use-theme-cVUDAjtt.js";import{t as c}from"./useMutation-DVMopbtG.js";import{t as l}from"./page-layout-C475gs09.js";import{c as u,o as d}from"./zod-BItJDQBQ.js";import{A as f,M as p,P as m,S as h,j as g,t as _}from"./index-CsT6OVnP.js";import{t as v}from"./page-header-BYMFSGfT.js";import{t as y}from"./promise-OpBtq8tG.js";import{r as b,t as x}from"./standard-schema-o4V-s4uY.js";import{t as S}from"./footer-link-Ib1Hd-fr.js";import{t as C}from"./submit-button-Xx6DwLyh.js";import{a as w}from"./totp-NlqqRp4a.js";var T=i(r()),E=t(),D={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`},O=5;function k(){let{t}=n(),r=o(),i=s(),k=_.useSearch(),A=(0,T.useRef)(null),[j,M]=(0,T.useState)(!1),[N,P]=(0,T.useState)(O),F=(0,T.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}$/,t(`verifyRecovery.error.invalid`)))}),[t]),I=c({...w,onSuccess:async e=>{i.setQueryData(h.queryKey,{user:e.user}),await y(),p(k)?window.location.href=f(k):r.navigate({to:`/profile`})},onSettled:()=>{i.invalidateQueries({queryKey:h.queryKey})}}),{register:L,setError:R,handleSubmit:z,setValue:B,formState:{errors:V}}=b({defaultValues:{code:``},resolver:x(F)}),H=(0,T.useCallback)(()=>{r.navigate({to:`/login`,search:g(k)})},[r,k]);(0,T.useEffect)(()=>{if(!j)return;let e=setInterval(()=>{P(t=>t<=1?(clearInterval(e),H(),0):t-1)},1e3);return()=>clearInterval(e)},[j,H]);let U=async n=>{try{await I.mutateAsync(n)}catch(n){if(n instanceof e)switch(n.code){case D.SECOND_FACTOR_SESSION_EXPIRED:M(!0),P(O);return;case D.TOTP_NOT_ENABLED:H();return;case D.NO_RECOVERY_CODES_AVAILABLE:R(`code`,{type:`manual`,message:t(`verifyRecovery.error.noCodesAvailable`)}),B(`code`,``),A.current?.focus();return;case D.INVALID_RECOVERY_CODE:R(`code`,{type:`manual`,message:t(`verifyRecovery.error.invalid`)}),B(`code`,``),A.current?.focus();return}R(`code`,{type:`manual`,message:t(`verifyRecovery.error.invalid`)}),B(`code`,``),A.current?.focus()}},{ref:W,...G}=L(`code`);return(0,E.jsxs)(l,{cardPadding:!0,maxWidth:`100`,children:[(0,E.jsx)(v,{subtitle:t(`verifyRecovery.subtitle`),title:t(`verifyRecovery.title`)}),j&&(0,E.jsxs)(`div`,{className:`alert alert-warning mb-4`,"data-testid":`recovery-session-expired`,children:[(0,E.jsx)(m,{className:`size-5`,weight:`fill`}),(0,E.jsxs)(`div`,{className:`flex flex-col gap-1`,children:[(0,E.jsx)(`span`,{children:t(`verifyRecovery.error.expired`)}),(0,E.jsx)(`span`,{className:`text-sm opacity-80`,children:t(`verifyRecovery.redirecting`,{seconds:N})}),(0,E.jsx)(`button`,{className:`btn btn-sm btn-ghost mt-2 w-fit`,onClick:H,type:`button`,children:t(`verifyRecovery.redirectNow`)})]})]}),(0,E.jsxs)(`form`,{className:`flex flex-col gap-4`,onSubmit:z(U),children:[(0,E.jsxs)(`fieldset`,{className:`fieldset`,children:[(0,E.jsx)(`input`,{...G,autoComplete:`off`,className:`input input-bordered w-full text-center font-mono ${V.code?`input-error`:``}`,"data-testid":`recovery-code-input`,disabled:j,placeholder:t(`verifyRecovery.placeholder`),ref:e=>{W(e),A.current=e},type:`text`}),V.code&&(0,E.jsx)(`p`,{className:`fieldset-label text-error`,"data-testid":`recovery-error`,children:V.code.message})]}),j?(0,E.jsx)(`button`,{className:`btn btn-block btn-disabled mt-2`,disabled:!0,type:`button`,children:t(`verifyRecovery.submit`)}):(0,E.jsx)(C,{className:`mt-2`,isPending:I.isPending,pendingText:t(`verifyRecovery.submitting`),children:t(`verifyRecovery.submit`)})]}),(0,E.jsx)(`div`,{className:`mt-4 text-center`,children:(0,E.jsx)(`button`,{className:`link link-info font-medium text-xs`,"data-testid":`recovery-back-to-totp`,onClick:()=>r.navigate({to:`/verify/totp`,search:g(k)}),type:`button`,children:t(`verifyRecovery.backToTotp`)})}),(0,E.jsx)(S,{as:a,linkText:t(`verifyRecovery.backToLogin`),search:g(k),text:``,to:`/login`})]})}export{k as component};
2
- //# sourceMappingURL=recovery-DM8h2gbb.js.map
1
+ import{n as e,o as t,r as n,s as r,u as i}from"./IconBase.es-d5KP98Ac.js";import{f as a,h as o,ut as s}from"./use-theme-cVUDAjtt.js";import{t as c}from"./useMutation-Iu4AJCB4.js";import{t as l}from"./page-layout-C475gs09.js";import{c as u,o as d}from"./zod-BItJDQBQ.js";import{A as f,M as p,P as m,S as h,j as g,t as _}from"./index-BmfaaNx6.js";import{t as v}from"./page-header-BYMFSGfT.js";import{t as y}from"./promise-OpBtq8tG.js";import{r as b,t as x}from"./standard-schema-o4V-s4uY.js";import{t as S}from"./footer-link-Ib1Hd-fr.js";import{t as C}from"./submit-button-Xx6DwLyh.js";import{a as w}from"./totp-NlqqRp4a.js";var T=i(r()),E=t(),D={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`},O=5;function k(){let{t}=n(),r=o(),i=s(),k=_.useSearch(),A=(0,T.useRef)(null),[j,M]=(0,T.useState)(!1),[N,P]=(0,T.useState)(O),F=(0,T.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}$/,t(`verifyRecovery.error.invalid`)))}),[t]),I=c({...w,onSuccess:async e=>{i.setQueryData(h.queryKey,{user:e.user}),await y(),p(k)?window.location.href=f(k):r.navigate({to:`/profile`})},onSettled:()=>{i.invalidateQueries({queryKey:h.queryKey})}}),{register:L,setError:R,handleSubmit:z,setValue:B,formState:{errors:V}}=b({defaultValues:{code:``},resolver:x(F)}),H=(0,T.useCallback)(()=>{r.navigate({to:`/login`,search:g(k)})},[r,k]);(0,T.useEffect)(()=>{if(!j)return;let e=setInterval(()=>{P(t=>t<=1?(clearInterval(e),H(),0):t-1)},1e3);return()=>clearInterval(e)},[j,H]);let U=async n=>{try{await I.mutateAsync(n)}catch(n){if(n instanceof e)switch(n.code){case D.SECOND_FACTOR_SESSION_EXPIRED:M(!0),P(O);return;case D.TOTP_NOT_ENABLED:H();return;case D.NO_RECOVERY_CODES_AVAILABLE:R(`code`,{type:`manual`,message:t(`verifyRecovery.error.noCodesAvailable`)}),B(`code`,``),A.current?.focus();return;case D.INVALID_RECOVERY_CODE:R(`code`,{type:`manual`,message:t(`verifyRecovery.error.invalid`)}),B(`code`,``),A.current?.focus();return}R(`code`,{type:`manual`,message:t(`verifyRecovery.error.invalid`)}),B(`code`,``),A.current?.focus()}},{ref:W,...G}=L(`code`);return(0,E.jsxs)(l,{cardPadding:!0,maxWidth:`100`,children:[(0,E.jsx)(v,{subtitle:t(`verifyRecovery.subtitle`),title:t(`verifyRecovery.title`)}),j&&(0,E.jsxs)(`div`,{className:`alert alert-warning mb-4`,"data-testid":`recovery-session-expired`,children:[(0,E.jsx)(m,{className:`size-5`,weight:`fill`}),(0,E.jsxs)(`div`,{className:`flex flex-col gap-1`,children:[(0,E.jsx)(`span`,{children:t(`verifyRecovery.error.expired`)}),(0,E.jsx)(`span`,{className:`text-sm opacity-80`,children:t(`verifyRecovery.redirecting`,{seconds:N})}),(0,E.jsx)(`button`,{className:`btn btn-sm btn-ghost mt-2 w-fit`,onClick:H,type:`button`,children:t(`verifyRecovery.redirectNow`)})]})]}),(0,E.jsxs)(`form`,{className:`flex flex-col gap-4`,onSubmit:z(U),children:[(0,E.jsxs)(`fieldset`,{className:`fieldset`,children:[(0,E.jsx)(`input`,{...G,autoComplete:`off`,className:`input input-bordered w-full text-center font-mono ${V.code?`input-error`:``}`,"data-testid":`recovery-code-input`,disabled:j,placeholder:t(`verifyRecovery.placeholder`),ref:e=>{W(e),A.current=e},type:`text`}),V.code&&(0,E.jsx)(`p`,{className:`fieldset-label text-error`,"data-testid":`recovery-error`,children:V.code.message})]}),j?(0,E.jsx)(`button`,{className:`btn btn-block btn-disabled mt-2`,disabled:!0,type:`button`,children:t(`verifyRecovery.submit`)}):(0,E.jsx)(C,{className:`mt-2`,isPending:I.isPending,pendingText:t(`verifyRecovery.submitting`),children:t(`verifyRecovery.submit`)})]}),(0,E.jsx)(`div`,{className:`mt-4 text-center`,children:(0,E.jsx)(`button`,{className:`link link-info font-medium text-xs`,"data-testid":`recovery-back-to-totp`,onClick:()=>r.navigate({to:`/verify/totp`,search:g(k)}),type:`button`,children:t(`verifyRecovery.backToTotp`)})}),(0,E.jsx)(S,{as:a,linkText:t(`verifyRecovery.backToLogin`),search:g(k),text:``,to:`/login`})]})}export{k as component};
2
+ //# sourceMappingURL=recovery-Csf6dtvj.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"recovery-DM8h2gbb.js","names":["standardSchemaResolver","WarningCircleIcon","useMutation","useQueryClient","Link","useRouter","useCallback","useEffect","useMemo","useRef","useState","useForm","useTranslation","z","FooterLink","PageHeader","SubmitButton","PageLayout","TinyAuthError","buildAuthorizeUrl","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 { WarningCircleIcon } from '@phosphor-icons/react';\nimport { useMutation, useQueryClient } from '@tanstack/react-query';\nimport { createFileRoute, Link, useRouter } from '@tanstack/react-router';\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport { FooterLink } from '#frontend/components/auth/footer-link.tsx';\nimport { PageHeader } from '#frontend/components/auth/page-header.tsx';\nimport { SubmitButton } from '#frontend/components/auth/submit-button.tsx';\nimport { PageLayout } from '#frontend/features/layout/page-layout.tsx';\nimport { TinyAuthError } from '#frontend/libs/error.ts';\nimport {\n buildAuthorizeUrl,\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 = buildAuthorizeUrl(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 TinyAuthError) {\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 <PageLayout cardPadding maxWidth=\"100\">\n <PageHeader\n subtitle={t('verifyRecovery.subtitle')}\n title={t('verifyRecovery.title')}\n />\n\n {sessionExpired && (\n <div\n className=\"alert alert-warning mb-4\"\n data-testid=\"recovery-session-expired\"\n >\n <WarningCircleIcon className=\"size-5\" weight=\"fill\" />\n <div className=\"flex flex-col gap-1\">\n <span>{t('verifyRecovery.error.expired')}</span>\n <span className=\"text-sm opacity-80\">\n {t('verifyRecovery.redirecting', {\n seconds: redirectCountdown,\n })}\n </span>\n <button\n className=\"btn btn-sm btn-ghost mt-2 w-fit\"\n onClick={redirectToLogin}\n type=\"button\"\n >\n {t('verifyRecovery.redirectNow')}\n </button>\n </div>\n </div>\n )}\n\n <form className=\"flex flex-col gap-4\" onSubmit={handleSubmit(onSubmit)}>\n <fieldset className=\"fieldset\">\n <input\n {...registerRest}\n autoComplete=\"off\"\n className={`input input-bordered w-full text-center font-mono ${\n errors.code ? 'input-error' : ''\n }`}\n data-testid=\"recovery-code-input\"\n disabled={sessionExpired}\n placeholder={t('verifyRecovery.placeholder')}\n ref={(el) => {\n formRef(el);\n inputRef.current = el;\n }}\n type=\"text\"\n />\n {errors.code && (\n <p\n className=\"fieldset-label text-error\"\n data-testid=\"recovery-error\"\n >\n {errors.code.message}\n </p>\n )}\n </fieldset>\n\n {sessionExpired ? (\n <button\n className=\"btn btn-block btn-disabled mt-2\"\n disabled\n type=\"button\"\n >\n {t('verifyRecovery.submit')}\n </button>\n ) : (\n <SubmitButton\n className=\"mt-2\"\n isPending={verifyMutation.isPending}\n pendingText={t('verifyRecovery.submitting')}\n >\n {t('verifyRecovery.submit')}\n </SubmitButton>\n )}\n </form>\n\n <div className=\"mt-4 text-center\">\n <button\n className=\"link link-info font-medium text-xs\"\n data-testid=\"recovery-back-to-totp\"\n onClick={() =>\n router.navigate({\n to: '/verify/totp',\n search: extractOAuthParams(search),\n })\n }\n type=\"button\"\n >\n {t('verifyRecovery.backToTotp')}\n </button>\n </div>\n\n <FooterLink\n as={Link}\n linkText={t('verifyRecovery.backToLogin')}\n search={extractOAuthParams(search)}\n text=\"\"\n to=\"/login\"\n />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"recovery-Csf6dtvj.js","names":["standardSchemaResolver","WarningCircleIcon","useMutation","useQueryClient","Link","useRouter","useCallback","useEffect","useMemo","useRef","useState","useForm","useTranslation","z","FooterLink","PageHeader","SubmitButton","PageLayout","TinyAuthError","buildAuthorizeUrl","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 { WarningCircleIcon } from '@phosphor-icons/react';\nimport { useMutation, useQueryClient } from '@tanstack/react-query';\nimport { createFileRoute, Link, useRouter } from '@tanstack/react-router';\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport { FooterLink } from '#frontend/components/auth/footer-link.tsx';\nimport { PageHeader } from '#frontend/components/auth/page-header.tsx';\nimport { SubmitButton } from '#frontend/components/auth/submit-button.tsx';\nimport { PageLayout } from '#frontend/features/layout/page-layout.tsx';\nimport { TinyAuthError } from '#frontend/libs/error.ts';\nimport {\n buildAuthorizeUrl,\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 = buildAuthorizeUrl(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 TinyAuthError) {\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 <PageLayout cardPadding maxWidth=\"100\">\n <PageHeader\n subtitle={t('verifyRecovery.subtitle')}\n title={t('verifyRecovery.title')}\n />\n\n {sessionExpired && (\n <div\n className=\"alert alert-warning mb-4\"\n data-testid=\"recovery-session-expired\"\n >\n <WarningCircleIcon className=\"size-5\" weight=\"fill\" />\n <div className=\"flex flex-col gap-1\">\n <span>{t('verifyRecovery.error.expired')}</span>\n <span className=\"text-sm opacity-80\">\n {t('verifyRecovery.redirecting', {\n seconds: redirectCountdown,\n })}\n </span>\n <button\n className=\"btn btn-sm btn-ghost mt-2 w-fit\"\n onClick={redirectToLogin}\n type=\"button\"\n >\n {t('verifyRecovery.redirectNow')}\n </button>\n </div>\n </div>\n )}\n\n <form className=\"flex flex-col gap-4\" onSubmit={handleSubmit(onSubmit)}>\n <fieldset className=\"fieldset\">\n <input\n {...registerRest}\n autoComplete=\"off\"\n className={`input input-bordered w-full text-center font-mono ${\n errors.code ? 'input-error' : ''\n }`}\n data-testid=\"recovery-code-input\"\n disabled={sessionExpired}\n placeholder={t('verifyRecovery.placeholder')}\n ref={(el) => {\n formRef(el);\n inputRef.current = el;\n }}\n type=\"text\"\n />\n {errors.code && (\n <p\n className=\"fieldset-label text-error\"\n data-testid=\"recovery-error\"\n >\n {errors.code.message}\n </p>\n )}\n </fieldset>\n\n {sessionExpired ? (\n <button\n className=\"btn btn-block btn-disabled mt-2\"\n disabled\n type=\"button\"\n >\n {t('verifyRecovery.submit')}\n </button>\n ) : (\n <SubmitButton\n className=\"mt-2\"\n isPending={verifyMutation.isPending}\n pendingText={t('verifyRecovery.submitting')}\n >\n {t('verifyRecovery.submit')}\n </SubmitButton>\n )}\n </form>\n\n <div className=\"mt-4 text-center\">\n <button\n className=\"link link-info font-medium text-xs\"\n data-testid=\"recovery-back-to-totp\"\n onClick={() =>\n router.navigate({\n to: '/verify/totp',\n search: extractOAuthParams(search),\n })\n }\n type=\"button\"\n >\n {t('verifyRecovery.backToTotp')}\n </button>\n </div>\n\n <FooterLink\n as={Link}\n linkText={t('verifyRecovery.backToLogin')}\n search={extractOAuthParams(search)}\n text=\"\"\n to=\"/login\"\n />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import{N as e}from"./index-BmfaaNx6.js";var t=e;export{t as errorComponent};
2
+ //# sourceMappingURL=register-BMEIiDXv.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"register-SSKfXB7c.js","names":["RouteErrorFallback","SplitErrorComponent","errorComponent"],"sources":["../../../frontend/src/routes/register/index.tsx?tsr-split=errorComponent"],"sourcesContent":["import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';\nimport { EnvelopeSimpleIcon, LockIcon } from '@phosphor-icons/react';\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 { useDeferredValue, useMemo } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport { FooterLink } from '#frontend/components/auth/footer-link.tsx';\nimport { IconInput } from '#frontend/components/auth/icon-input.tsx';\nimport { PageHeader } from '#frontend/components/auth/page-header.tsx';\nimport { SubmitButton } from '#frontend/components/auth/submit-button.tsx';\nimport { TermsCheckboxList } from '#frontend/components/terms/terms-checkbox-list.tsx';\nimport { RouteErrorFallback } from '#frontend/components/ui/route-error-fallback.tsx';\nimport { PageLayout } from '#frontend/features/layout/page-layout.tsx';\nimport { TinyAuthError } from '#frontend/libs/error.ts';\nimport {\n buildAuthorizeUrl,\n extractOAuthParams,\n isOAuthFlow,\n OAuthSearchSchema,\n type SecondFactorMethod,\n} from '#frontend/libs/oauth-search.ts';\nimport { tick } from '#frontend/libs/promise.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 }),\n loader: async ({ context, deps }) => {\n const lang = deps.lang ?? context.i18n.language;\n await Promise.all([\n context.queryClient.ensureQueryData(appConfigQueryOptions),\n context.queryClient.ensureQueryData(getTermsQueryOptions(lang)),\n ]);\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.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 = buildAuthorizeUrl(search);\n } else {\n navigate({ to: '/profile' });\n }\n },\n onError: (error) => {\n if (error instanceof TinyAuthError) {\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 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 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 <PageLayout cardPadding maxWidth=\"100\">\n <PageHeader\n subtitle={t('register.subtitle')}\n title={t('register.title')}\n />\n\n {isPasswordAuthEnabled && (\n <form className=\"flex flex-col gap-4\" onSubmit={handleSubmit(onSubmit)}>\n <IconInput\n autoComplete=\"email\"\n error={errors.email}\n icon={EnvelopeSimpleIcon}\n placeholder={t('register.email.placeholder')}\n {...register('email')}\n type=\"email\"\n />\n\n <IconInput\n autoComplete=\"new-password\"\n error={errors.password}\n icon={LockIcon}\n placeholder={t('register.password.placeholder')}\n {...register('password')}\n type=\"password\"\n />\n\n <div className=\"flex flex-col\">\n {implicitNotice && (\n <div className={'text-center text-base-content/60 text-xs'}>\n <div\n className=\"prose prose-sm text-xs! **:text-xs!\"\n dangerouslySetInnerHTML={{\n __html: implicitNotice,\n }}\n data-testid=\"terms-implicit-notice\"\n />\n </div>\n )}\n\n {implicitNotice && hasExplicitTerms && (\n <div className=\"divider text-xs\">AND</div>\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 <SubmitButton\n className=\"mt-6\"\n isPending={registerMutation.isPending}\n pendingText={t('register.submitting')}\n >\n {t('register.submit')}\n </SubmitButton>\n </form>\n )}\n\n <FooterLink\n as={Link}\n linkText={t('register.link.login')}\n search={extractOAuthParams(search)}\n text={t('register.footer.haveAccount')}\n to=\"/login\"\n />\n </PageLayout>\n );\n}\n"],"mappings":"wCAsBsF,IAAAC,EAA7ED"}
1
+ {"version":3,"file":"register-BMEIiDXv.js","names":["RouteErrorFallback","SplitErrorComponent","errorComponent"],"sources":["../../../frontend/src/routes/register/index.tsx?tsr-split=errorComponent"],"sourcesContent":["import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';\nimport { EnvelopeSimpleIcon, LockIcon } from '@phosphor-icons/react';\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 { useDeferredValue, useMemo } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport { FooterLink } from '#frontend/components/auth/footer-link.tsx';\nimport { IconInput } from '#frontend/components/auth/icon-input.tsx';\nimport { PageHeader } from '#frontend/components/auth/page-header.tsx';\nimport { SubmitButton } from '#frontend/components/auth/submit-button.tsx';\nimport { TermsCheckboxList } from '#frontend/components/terms/terms-checkbox-list.tsx';\nimport { RouteErrorFallback } from '#frontend/components/ui/route-error-fallback.tsx';\nimport { PageLayout } from '#frontend/features/layout/page-layout.tsx';\nimport { TinyAuthError } from '#frontend/libs/error.ts';\nimport {\n buildAuthorizeUrl,\n extractOAuthParams,\n isOAuthFlow,\n OAuthSearchSchema,\n type SecondFactorMethod,\n} from '#frontend/libs/oauth-search.ts';\nimport { tick } from '#frontend/libs/promise.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 }),\n loader: async ({ context, deps }) => {\n const lang = deps.lang ?? context.i18n.language;\n await Promise.all([\n context.queryClient.ensureQueryData(appConfigQueryOptions),\n context.queryClient.ensureQueryData(getTermsQueryOptions(lang)),\n ]);\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.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 = buildAuthorizeUrl(search);\n } else {\n navigate({ to: '/profile' });\n }\n },\n onError: (error) => {\n if (error instanceof TinyAuthError) {\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 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 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 <PageLayout cardPadding maxWidth=\"100\">\n <PageHeader\n subtitle={t('register.subtitle')}\n title={t('register.title')}\n />\n\n {isPasswordAuthEnabled && (\n <form className=\"flex flex-col gap-4\" onSubmit={handleSubmit(onSubmit)}>\n <IconInput\n autoComplete=\"email\"\n error={errors.email}\n icon={EnvelopeSimpleIcon}\n placeholder={t('register.email.placeholder')}\n {...register('email')}\n type=\"email\"\n />\n\n <IconInput\n autoComplete=\"new-password\"\n error={errors.password}\n icon={LockIcon}\n placeholder={t('register.password.placeholder')}\n {...register('password')}\n type=\"password\"\n />\n\n <div className=\"flex flex-col\">\n {implicitNotice && (\n <div className={'text-center text-base-content/60 text-xs'}>\n <div\n className=\"prose prose-sm text-xs! **:text-xs!\"\n dangerouslySetInnerHTML={{\n __html: implicitNotice,\n }}\n data-testid=\"terms-implicit-notice\"\n />\n </div>\n )}\n\n {implicitNotice && hasExplicitTerms && (\n <div className=\"divider text-xs\">AND</div>\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 <SubmitButton\n className=\"mt-6\"\n isPending={registerMutation.isPending}\n pendingText={t('register.submitting')}\n >\n {t('register.submit')}\n </SubmitButton>\n </form>\n )}\n\n <FooterLink\n as={Link}\n linkText={t('register.link.login')}\n search={extractOAuthParams(search)}\n text={t('register.footer.haveAccount')}\n to=\"/login\"\n />\n </PageLayout>\n );\n}\n"],"mappings":"wCAsBsF,IAAAC,EAA7ED"}
@@ -1,2 +1,2 @@
1
- import{n as e,o as t,r as n,s as r,u as i}from"./IconBase.es-d5KP98Ac.js";import{Q as a,a as o,f as s,i as c,n as l,ut as u}from"./use-theme-cVUDAjtt.js";import{t as d}from"./useMutation-DVMopbtG.js";import{t as f}from"./mutationOptions-Dfvzj6n2.js";import{t as p}from"./page-layout-C475gs09.js";import{t as m}from"./EnvelopeSimple.es-BZ7u3LYh.js";import{t as h}from"./Lock.es-Cb_uwQly.js";import{a as g,c as _,i as ee,o as v,r as y}from"./zod-BItJDQBQ.js";import{A as b,E as x,F as S,M as C,O as w,S as T,j as E}from"./index-CsT6OVnP.js";import{t as D}from"./page-header-BYMFSGfT.js";import{t as O}from"./promise-OpBtq8tG.js";import{r as k,t as A}from"./standard-schema-o4V-s4uY.js";import{t as j}from"./footer-link-Ib1Hd-fr.js";import{t as M}from"./icon-input-8iU7PNzd.js";import{t as N}from"./submit-button-Xx6DwLyh.js";import{t as P}from"./terms-checkbox-list-CdrbHxiF.js";var F=i(r()),I=f({mutationFn:async e=>o(await c.api.auth.register.$post({json:e,header:{}}))}),L=t();function R(){let{t,i18n:r}=n(),i=S(),o=u(),c=x.useSearch(),f=c.lang??r.language,R=(0,F.useDeferredValue)(f),{data:z}=a(l),{data:B}=a(w(R)),V=z.auth.password.enabled,H=z.auth.password.policy,U=B.terms.length>0,W=(0,F.useMemo)(()=>B.terms.filter(e=>e.consentMode===`explicit`),[B.terms]),G=W.length>0,K=z.registration.signup_notice?.[f]??z.registration.signup_notice?.[z.i18n.fallback_language],q=(0,F.useMemo)(()=>v({email:ee(t(`validation.email.invalid`)),password:_().min(H.min_length,t(`validation.password.min`,{count:H.min_length})).max(H.max_length,t(`validation.password.max`,{count:H.max_length})),termsConsents:v(Object.fromEntries(W.map(e=>[e.id,e.required?g(!0,{message:t(`validation.terms.required`)}):y()])))}),[W,H.max_length,H.min_length,t]),J=d({...I,onSuccess:async(e,t)=>{let n=e.user;if(n.email_verification_required&&!n.email_verified)return i({to:`/verify/email`,search:{email:t.email,...E(c)}});if(o.setQueryData(T.queryKey,{user:n}),await O(),n.second_factor_required){let e=[];return z.auth.password.totp.enabled&&e.push(`totp`),z.auth.passkey.enabled&&e.push(`passkey`),e.length===1?e[0]===`totp`?i({to:`/setup/totp`,search:E(c)}):i({to:`/setup/passkey`,search:{...E(c),passkey_name:`default`}}):i({to:`/setup/2fa`,search:E(c)})}C(c)?window.location.href=b(c):i({to:`/profile`})},onError:n=>{n instanceof e?n.code===`REGISTRATION_EMAIL_NOT_ALLOWED`?X(`email`,{type:`manual`,message:t(`register.error.emailNotAllowed`)}):n.code===`REGISTRATION_DISABLED`?X(`email`,{type:`manual`,message:t(`register.error.registrationDisabled`)}):X(`email`,{type:`manual`,message:t(`register.error.emailExists`)}):X(`email`,{type:`manual`,message:t(`register.error.emailExists`)})},onSettled:async()=>{await o.invalidateQueries({queryKey:T.queryKey})}}),{register:Y,setError:X,handleSubmit:Z,control:Q,setValue:te,formState:{errors:$}}=k({defaultValues:{email:``,password:``,termsConsents:Object.fromEntries(W.map(e=>[e.id,!1]))},resolver:A(q),mode:`onChange`});return(0,L.jsxs)(p,{cardPadding:!0,maxWidth:`100`,children:[(0,L.jsx)(D,{subtitle:t(`register.subtitle`),title:t(`register.title`)}),V&&(0,L.jsxs)(`form`,{className:`flex flex-col gap-4`,onSubmit:Z(e=>{let t=U?W.map(t=>({termsId:t.id,agreed:e.termsConsents[t.id]??!1})):void 0;J.mutate({email:e.email,password:e.password,consents:t})}),children:[(0,L.jsx)(M,{autoComplete:`email`,error:$.email,icon:m,placeholder:t(`register.email.placeholder`),...Y(`email`),type:`email`}),(0,L.jsx)(M,{autoComplete:`new-password`,error:$.password,icon:h,placeholder:t(`register.password.placeholder`),...Y(`password`),type:`password`}),(0,L.jsxs)(`div`,{className:`flex flex-col`,children:[K&&(0,L.jsx)(`div`,{className:`text-center text-base-content/60 text-xs`,children:(0,L.jsx)(`div`,{className:`prose prose-sm text-xs! **:text-xs!`,dangerouslySetInnerHTML:{__html:K},"data-testid":`terms-implicit-notice`})}),K&&G&&(0,L.jsx)(`div`,{className:`divider text-xs`,children:`AND`}),U&&G&&(0,L.jsx)(P,{control:Q,disabled:J.isPending,errors:$,setValue:te,terms:W})]}),(0,L.jsx)(N,{className:`mt-6`,isPending:J.isPending,pendingText:t(`register.submitting`),children:t(`register.submit`)})]}),(0,L.jsx)(j,{as:s,linkText:t(`register.link.login`),search:E(c),text:t(`register.footer.haveAccount`),to:`/login`})]})}export{R as component};
2
- //# sourceMappingURL=register-vWW_43cD.js.map
1
+ import{n as e,o as t,r as n,s as r,u as i}from"./IconBase.es-d5KP98Ac.js";import{Q as a,a as o,f as s,i as c,n as l,ut as u}from"./use-theme-cVUDAjtt.js";import{t as d}from"./useMutation-Iu4AJCB4.js";import{t as f}from"./mutationOptions-Dfvzj6n2.js";import{t as p}from"./page-layout-C475gs09.js";import{t as m}from"./EnvelopeSimple.es-BZ7u3LYh.js";import{t as h}from"./Lock.es-Cb_uwQly.js";import{a as g,c as _,i as ee,o as v,r as y}from"./zod-BItJDQBQ.js";import{A as b,E as x,F as S,M as C,O as w,S as T,j as E}from"./index-BmfaaNx6.js";import{t as D}from"./page-header-BYMFSGfT.js";import{t as O}from"./promise-OpBtq8tG.js";import{r as k,t as A}from"./standard-schema-o4V-s4uY.js";import{t as j}from"./footer-link-Ib1Hd-fr.js";import{t as M}from"./icon-input-8iU7PNzd.js";import{t as N}from"./submit-button-Xx6DwLyh.js";import{t as P}from"./terms-checkbox-list-CdrbHxiF.js";var F=i(r()),I=f({mutationFn:async e=>o(await c.api.auth.register.$post({json:e,header:{}}))}),L=t();function R(){let{t,i18n:r}=n(),i=S(),o=u(),c=x.useSearch(),f=c.lang??r.language,R=(0,F.useDeferredValue)(f),{data:z}=a(l),{data:B}=a(w(R)),V=z.auth.password.enabled,H=z.auth.password.policy,U=B.terms.length>0,W=(0,F.useMemo)(()=>B.terms.filter(e=>e.consentMode===`explicit`),[B.terms]),G=W.length>0,K=z.registration.signup_notice?.[f]??z.registration.signup_notice?.[z.i18n.fallback_language],q=(0,F.useMemo)(()=>v({email:ee(t(`validation.email.invalid`)),password:_().min(H.min_length,t(`validation.password.min`,{count:H.min_length})).max(H.max_length,t(`validation.password.max`,{count:H.max_length})),termsConsents:v(Object.fromEntries(W.map(e=>[e.id,e.required?g(!0,{message:t(`validation.terms.required`)}):y()])))}),[W,H.max_length,H.min_length,t]),J=d({...I,onSuccess:async(e,t)=>{let n=e.user;if(n.email_verification_required&&!n.email_verified)return i({to:`/verify/email`,search:{email:t.email,...E(c)}});if(o.setQueryData(T.queryKey,{user:n}),await O(),n.second_factor_required){let e=[];return z.auth.password.totp.enabled&&e.push(`totp`),z.auth.passkey.enabled&&e.push(`passkey`),e.length===1?e[0]===`totp`?i({to:`/setup/totp`,search:E(c)}):i({to:`/setup/passkey`,search:{...E(c),passkey_name:`default`}}):i({to:`/setup/2fa`,search:E(c)})}C(c)?window.location.href=b(c):i({to:`/profile`})},onError:n=>{n instanceof e?n.code===`REGISTRATION_EMAIL_NOT_ALLOWED`?X(`email`,{type:`manual`,message:t(`register.error.emailNotAllowed`)}):n.code===`REGISTRATION_DISABLED`?X(`email`,{type:`manual`,message:t(`register.error.registrationDisabled`)}):X(`email`,{type:`manual`,message:t(`register.error.emailExists`)}):X(`email`,{type:`manual`,message:t(`register.error.emailExists`)})},onSettled:async()=>{await o.invalidateQueries({queryKey:T.queryKey})}}),{register:Y,setError:X,handleSubmit:Z,control:Q,setValue:te,formState:{errors:$}}=k({defaultValues:{email:``,password:``,termsConsents:Object.fromEntries(W.map(e=>[e.id,!1]))},resolver:A(q),mode:`onChange`});return(0,L.jsxs)(p,{cardPadding:!0,maxWidth:`100`,children:[(0,L.jsx)(D,{subtitle:t(`register.subtitle`),title:t(`register.title`)}),V&&(0,L.jsxs)(`form`,{className:`flex flex-col gap-4`,onSubmit:Z(e=>{let t=U?W.map(t=>({termsId:t.id,agreed:e.termsConsents[t.id]??!1})):void 0;J.mutate({email:e.email,password:e.password,consents:t})}),children:[(0,L.jsx)(M,{autoComplete:`email`,error:$.email,icon:m,placeholder:t(`register.email.placeholder`),...Y(`email`),type:`email`}),(0,L.jsx)(M,{autoComplete:`new-password`,error:$.password,icon:h,placeholder:t(`register.password.placeholder`),...Y(`password`),type:`password`}),(0,L.jsxs)(`div`,{className:`flex flex-col`,children:[K&&(0,L.jsx)(`div`,{className:`text-center text-base-content/60 text-xs`,children:(0,L.jsx)(`div`,{className:`prose prose-sm text-xs! **:text-xs!`,dangerouslySetInnerHTML:{__html:K},"data-testid":`terms-implicit-notice`})}),K&&G&&(0,L.jsx)(`div`,{className:`divider text-xs`,children:`AND`}),U&&G&&(0,L.jsx)(P,{control:Q,disabled:J.isPending,errors:$,setValue:te,terms:W})]}),(0,L.jsx)(N,{className:`mt-6`,isPending:J.isPending,pendingText:t(`register.submitting`),children:t(`register.submit`)})]}),(0,L.jsx)(j,{as:s,linkText:t(`register.link.login`),search:E(c),text:t(`register.footer.haveAccount`),to:`/login`})]})}export{R as component};
2
+ //# sourceMappingURL=register-X6dycne4.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"register-vWW_43cD.js","names":["standardSchemaResolver","EnvelopeSimpleIcon","LockIcon","useMutation","useQueryClient","useSuspenseQuery","Link","useNavigate","useDeferredValue","useMemo","useForm","useTranslation","z","FooterLink","IconInput","PageHeader","SubmitButton","TermsCheckboxList","PageLayout","TinyAuthError","buildAuthorizeUrl","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","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","formState","errors","defaultValues","resolver","mode","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 { EnvelopeSimpleIcon, LockIcon } from '@phosphor-icons/react';\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 { useDeferredValue, useMemo } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport { FooterLink } from '#frontend/components/auth/footer-link.tsx';\nimport { IconInput } from '#frontend/components/auth/icon-input.tsx';\nimport { PageHeader } from '#frontend/components/auth/page-header.tsx';\nimport { SubmitButton } from '#frontend/components/auth/submit-button.tsx';\nimport { TermsCheckboxList } from '#frontend/components/terms/terms-checkbox-list.tsx';\nimport { RouteErrorFallback } from '#frontend/components/ui/route-error-fallback.tsx';\nimport { PageLayout } from '#frontend/features/layout/page-layout.tsx';\nimport { TinyAuthError } from '#frontend/libs/error.ts';\nimport {\n buildAuthorizeUrl,\n extractOAuthParams,\n isOAuthFlow,\n OAuthSearchSchema,\n type SecondFactorMethod,\n} from '#frontend/libs/oauth-search.ts';\nimport { tick } from '#frontend/libs/promise.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 }),\n loader: async ({ context, deps }) => {\n const lang = deps.lang ?? context.i18n.language;\n await Promise.all([\n context.queryClient.ensureQueryData(appConfigQueryOptions),\n context.queryClient.ensureQueryData(getTermsQueryOptions(lang)),\n ]);\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.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 = buildAuthorizeUrl(search);\n } else {\n navigate({ to: '/profile' });\n }\n },\n onError: (error) => {\n if (error instanceof TinyAuthError) {\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 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 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 <PageLayout cardPadding maxWidth=\"100\">\n <PageHeader\n subtitle={t('register.subtitle')}\n title={t('register.title')}\n />\n\n {isPasswordAuthEnabled && (\n <form className=\"flex flex-col gap-4\" onSubmit={handleSubmit(onSubmit)}>\n <IconInput\n autoComplete=\"email\"\n error={errors.email}\n icon={EnvelopeSimpleIcon}\n placeholder={t('register.email.placeholder')}\n {...register('email')}\n type=\"email\"\n />\n\n <IconInput\n autoComplete=\"new-password\"\n error={errors.password}\n icon={LockIcon}\n placeholder={t('register.password.placeholder')}\n {...register('password')}\n type=\"password\"\n />\n\n <div className=\"flex flex-col\">\n {implicitNotice && (\n <div className={'text-center text-base-content/60 text-xs'}>\n <div\n className=\"prose prose-sm text-xs! **:text-xs!\"\n dangerouslySetInnerHTML={{\n __html: implicitNotice,\n }}\n data-testid=\"terms-implicit-notice\"\n />\n </div>\n )}\n\n {implicitNotice && hasExplicitTerms && (\n <div className=\"divider text-xs\">AND</div>\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 <SubmitButton\n className=\"mt-6\"\n isPending={registerMutation.isPending}\n pendingText={t('register.submitting')}\n >\n {t('register.submit')}\n </SubmitButton>\n </form>\n )}\n\n <FooterLink\n as={Link}\n linkText={t('register.link.login')}\n search={extractOAuthParams(search)}\n text={t('register.footer.haveAccount')}\n to=\"/login\"\n />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"register-X6dycne4.js","names":["standardSchemaResolver","EnvelopeSimpleIcon","LockIcon","useMutation","useQueryClient","useSuspenseQuery","Link","useNavigate","useDeferredValue","useMemo","useForm","useTranslation","z","FooterLink","IconInput","PageHeader","SubmitButton","TermsCheckboxList","PageLayout","TinyAuthError","buildAuthorizeUrl","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","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","formState","errors","defaultValues","resolver","mode","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 { EnvelopeSimpleIcon, LockIcon } from '@phosphor-icons/react';\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 { useDeferredValue, useMemo } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport { FooterLink } from '#frontend/components/auth/footer-link.tsx';\nimport { IconInput } from '#frontend/components/auth/icon-input.tsx';\nimport { PageHeader } from '#frontend/components/auth/page-header.tsx';\nimport { SubmitButton } from '#frontend/components/auth/submit-button.tsx';\nimport { TermsCheckboxList } from '#frontend/components/terms/terms-checkbox-list.tsx';\nimport { RouteErrorFallback } from '#frontend/components/ui/route-error-fallback.tsx';\nimport { PageLayout } from '#frontend/features/layout/page-layout.tsx';\nimport { TinyAuthError } from '#frontend/libs/error.ts';\nimport {\n buildAuthorizeUrl,\n extractOAuthParams,\n isOAuthFlow,\n OAuthSearchSchema,\n type SecondFactorMethod,\n} from '#frontend/libs/oauth-search.ts';\nimport { tick } from '#frontend/libs/promise.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 }),\n loader: async ({ context, deps }) => {\n const lang = deps.lang ?? context.i18n.language;\n await Promise.all([\n context.queryClient.ensureQueryData(appConfigQueryOptions),\n context.queryClient.ensureQueryData(getTermsQueryOptions(lang)),\n ]);\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.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 = buildAuthorizeUrl(search);\n } else {\n navigate({ to: '/profile' });\n }\n },\n onError: (error) => {\n if (error instanceof TinyAuthError) {\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 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 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 <PageLayout cardPadding maxWidth=\"100\">\n <PageHeader\n subtitle={t('register.subtitle')}\n title={t('register.title')}\n />\n\n {isPasswordAuthEnabled && (\n <form className=\"flex flex-col gap-4\" onSubmit={handleSubmit(onSubmit)}>\n <IconInput\n autoComplete=\"email\"\n error={errors.email}\n icon={EnvelopeSimpleIcon}\n placeholder={t('register.email.placeholder')}\n {...register('email')}\n type=\"email\"\n />\n\n <IconInput\n autoComplete=\"new-password\"\n error={errors.password}\n icon={LockIcon}\n placeholder={t('register.password.placeholder')}\n {...register('password')}\n type=\"password\"\n />\n\n <div className=\"flex flex-col\">\n {implicitNotice && (\n <div className={'text-center text-base-content/60 text-xs'}>\n <div\n className=\"prose prose-sm text-xs! **:text-xs!\"\n dangerouslySetInnerHTML={{\n __html: implicitNotice,\n }}\n data-testid=\"terms-implicit-notice\"\n />\n </div>\n )}\n\n {implicitNotice && hasExplicitTerms && (\n <div className=\"divider text-xs\">AND</div>\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 <SubmitButton\n className=\"mt-6\"\n isPending={registerMutation.isPending}\n pendingText={t('register.submitting')}\n >\n {t('register.submit')}\n </SubmitButton>\n </form>\n )}\n\n <FooterLink\n as={Link}\n linkText={t('register.link.login')}\n search={extractOAuthParams(search)}\n text={t('register.footer.haveAccount')}\n to=\"/login\"\n />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import{o as e,r as t,s as n,t as r,u as i}from"./IconBase.es-d5KP98Ac.js";import{Q as a,f as o,n as s}from"./use-theme-cVUDAjtt.js";import{t as c}from"./useMutation-DVMopbtG.js";import{t as l}from"./CheckCircle.es-MnJIACCe.js";import{t as u}from"./page-layout-C475gs09.js";import{t as d}from"./Key.es-l5aSxw0I.js";import{t as f}from"./Lock.es-Cb_uwQly.js";import{c as p,o as m}from"./zod-BItJDQBQ.js";import{F as h,c as g}from"./index-CsT6OVnP.js";import{t as _}from"./page-header-BYMFSGfT.js";import{t as v}from"./alert-CSXqgDVi.js";import{r as y,t as b}from"./standard-schema-o4V-s4uY.js";import{t as x}from"./icon-input-8iU7PNzd.js";import{t as S}from"./submit-button-Xx6DwLyh.js";import{n as C}from"./password-reset-XZJTgJi3.js";var w=i(n(),1),T=new Map([[`bold`,w.createElement(w.Fragment,null,w.createElement(`path`,{d:`M208,76H180V56A52,52,0,0,0,76,56V76H48A20,20,0,0,0,28,96V208a20,20,0,0,0,20,20H208a20,20,0,0,0,20-20V96A20,20,0,0,0,208,76ZM100,56a28,28,0,0,1,56,0V76H100ZM204,204H52V100H204Zm-76-92a32,32,0,0,0-12,61.66V180a12,12,0,0,0,24,0v-6.34A32,32,0,0,0,128,112Zm0,24a8,8,0,1,1-8,8A8,8,0,0,1,128,136Z`}))],[`duotone`,w.createElement(w.Fragment,null,w.createElement(`path`,{d:`M208,88H48a8,8,0,0,0-8,8V208a8,8,0,0,0,8,8H208a8,8,0,0,0,8-8V96A8,8,0,0,0,208,88Zm-80,72a20,20,0,1,1,20-20A20,20,0,0,1,128,160Z`,opacity:`0.2`}),w.createElement(`path`,{d:`M208,80H176V56a48,48,0,0,0-96,0V80H48A16,16,0,0,0,32,96V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V96A16,16,0,0,0,208,80ZM96,56a32,32,0,0,1,64,0V80H96ZM208,208H48V96H208V208Zm-80-96a28,28,0,0,0-8,54.83V184a8,8,0,0,0,16,0V166.83A28,28,0,0,0,128,112Zm0,40a12,12,0,1,1,12-12A12,12,0,0,1,128,152Z`}))],[`fill`,w.createElement(w.Fragment,null,w.createElement(`path`,{d:`M208,80H176V56a48,48,0,0,0-96,0V80H48A16,16,0,0,0,32,96V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V96A16,16,0,0,0,208,80Zm-72,78.63V184a8,8,0,0,1-16,0V158.63a24,24,0,1,1,16,0ZM160,80H96V56a32,32,0,0,1,64,0Z`}))],[`light`,w.createElement(w.Fragment,null,w.createElement(`path`,{d:`M208,82H174V56a46,46,0,0,0-92,0V82H48A14,14,0,0,0,34,96V208a14,14,0,0,0,14,14H208a14,14,0,0,0,14-14V96A14,14,0,0,0,208,82ZM94,56a34,34,0,0,1,68,0V82H94ZM210,208a2,2,0,0,1-2,2H48a2,2,0,0,1-2-2V96a2,2,0,0,1,2-2H208a2,2,0,0,1,2,2Zm-82-94a26,26,0,0,0-6,51.29V184a6,6,0,0,0,12,0V165.29A26,26,0,0,0,128,114Zm0,40a14,14,0,1,1,14-14A14,14,0,0,1,128,154Z`}))],[`regular`,w.createElement(w.Fragment,null,w.createElement(`path`,{d:`M128,112a28,28,0,0,0-8,54.83V184a8,8,0,0,0,16,0V166.83A28,28,0,0,0,128,112Zm0,40a12,12,0,1,1,12-12A12,12,0,0,1,128,152Zm80-72H176V56a48,48,0,0,0-96,0V80H48A16,16,0,0,0,32,96V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V96A16,16,0,0,0,208,80ZM96,56a32,32,0,0,1,64,0V80H96ZM208,208H48V96H208V208Z`}))],[`thin`,w.createElement(w.Fragment,null,w.createElement(`path`,{d:`M208,84H172V56a44,44,0,0,0-88,0V84H48A12,12,0,0,0,36,96V208a12,12,0,0,0,12,12H208a12,12,0,0,0,12-12V96A12,12,0,0,0,208,84ZM92,56a36,36,0,0,1,72,0V84H92ZM212,208a4,4,0,0,1-4,4H48a4,4,0,0,1-4-4V96a4,4,0,0,1,4-4H208a4,4,0,0,1,4,4Zm-84-92a24,24,0,0,0-4,47.66V184a4,4,0,0,0,8,0V163.66A24,24,0,0,0,128,116Zm0,40a16,16,0,1,1,16-16A16,16,0,0,1,128,156Z`}))]]),E=w.forwardRef((e,t)=>w.createElement(r,{ref:t,...e,weights:T}));E.displayName=`LockKeyIcon`;var D=e();function O(){let{t:e}=t(),n=h(),{token:r}=g.useSearch(),[i,T]=(0,w.useState)(!1),{data:O}=a(s),k=O.auth.password.policy,A=(0,w.useMemo)(()=>m({token:p().min(1,e(`validation.token.required`)),password:p().min(k.min_length,e(`validation.password.min`,{count:k.min_length})).max(k.max_length,e(`validation.password.max`,{count:k.max_length})),confirmPassword:p().min(1,e(`validation.confirmPassword.required`))}).refine(e=>e.password===e.confirmPassword,{message:e(`validation.confirmPassword.mismatch`),path:[`confirmPassword`]}),[k.max_length,k.min_length,e]),j=c({...C,onSuccess:()=>{T(!0)}}),{register:M,setError:N,handleSubmit:P,formState:{errors:F}}=y({defaultValues:{token:r||``,password:``,confirmPassword:``},resolver:b(A)});return i?(0,D.jsxs)(u,{cardPadding:!0,maxWidth:`100`,children:[(0,D.jsx)(v,{className:`mb-4`,icon:l,type:`success`,children:e(`resetPassword.success.title`)}),(0,D.jsx)(_,{subtitle:e(`resetPassword.success.description`),title:e(`resetPassword.success.subtitle`)}),(0,D.jsx)(`button`,{className:`btn btn-block h-10 font-semibold text-[14px]`,"data-testid":`reset-password-go-login`,onClick:()=>n({to:`/login`}),type:`button`,children:e(`resetPassword.success.goToLogin`)})]}):(0,D.jsxs)(u,{cardPadding:!0,maxWidth:`100`,children:[(0,D.jsx)(_,{subtitle:e(`resetPassword.subtitle`),title:e(`resetPassword.title`)}),(0,D.jsxs)(`form`,{className:`flex flex-col gap-4`,onSubmit:P(async t=>{try{await j.mutateAsync({token:t.token,password:t.password})}catch(t){t&&typeof t==`object`&&`code`in t?t.code===`INVALID_PASSWORD_RESET_TOKEN`?N(`token`,{type:`manual`,message:e(`resetPassword.error.invalidToken`)}):t.code===`USER_NOT_EDITABLE`&&N(`token`,{type:`manual`,message:e(`resetPassword.error.notEditable`)}):N(`token`,{type:`manual`,message:e(`resetPassword.error.invalidToken`)})}}),children:[!r&&(0,D.jsx)(x,{error:F.token,icon:d,placeholder:e(`resetPassword.token.placeholder`),...M(`token`),type:`text`}),r&&F.token&&(0,D.jsx)(`div`,{className:`alert alert-error`,"data-testid":`reset-password-token-error`,children:(0,D.jsx)(`span`,{className:`text-sm`,children:F.token.message})}),(0,D.jsx)(x,{autoComplete:`new-password`,error:F.password,icon:f,placeholder:e(`resetPassword.password.placeholder`),...M(`password`),type:`password`}),(0,D.jsx)(x,{autoComplete:`new-password`,error:F.confirmPassword,icon:E,placeholder:e(`resetPassword.confirmPassword.placeholder`),...M(`confirmPassword`),type:`password`}),(0,D.jsx)(S,{className:`mt-2`,isPending:j.isPending,pendingText:e(`resetPassword.submitting`),children:e(`resetPassword.submit`)})]}),(0,D.jsx)(`div`,{className:`mt-6 text-center text-base-content/70 text-xs`,children:(0,D.jsx)(o,{className:`link link-info font-medium`,"data-testid":`reset-password-back-to-login`,to:`/login`,children:e(`resetPassword.backToLogin`)})})]})}export{O as component};
2
- //# sourceMappingURL=reset-CgACYrdp.js.map
1
+ import{o as e,r as t,s as n,t as r,u as i}from"./IconBase.es-d5KP98Ac.js";import{Q as a,f as o,n as s}from"./use-theme-cVUDAjtt.js";import{t as c}from"./useMutation-Iu4AJCB4.js";import{t as l}from"./CheckCircle.es-MnJIACCe.js";import{t as u}from"./page-layout-C475gs09.js";import{t as d}from"./Key.es-l5aSxw0I.js";import{t as f}from"./Lock.es-Cb_uwQly.js";import{c as p,o as m}from"./zod-BItJDQBQ.js";import{F as h,c as g}from"./index-BmfaaNx6.js";import{t as _}from"./page-header-BYMFSGfT.js";import{t as v}from"./alert-CSXqgDVi.js";import{r as y,t as b}from"./standard-schema-o4V-s4uY.js";import{t as x}from"./icon-input-8iU7PNzd.js";import{t as S}from"./submit-button-Xx6DwLyh.js";import{n as C}from"./password-reset-XZJTgJi3.js";var w=i(n(),1),T=new Map([[`bold`,w.createElement(w.Fragment,null,w.createElement(`path`,{d:`M208,76H180V56A52,52,0,0,0,76,56V76H48A20,20,0,0,0,28,96V208a20,20,0,0,0,20,20H208a20,20,0,0,0,20-20V96A20,20,0,0,0,208,76ZM100,56a28,28,0,0,1,56,0V76H100ZM204,204H52V100H204Zm-76-92a32,32,0,0,0-12,61.66V180a12,12,0,0,0,24,0v-6.34A32,32,0,0,0,128,112Zm0,24a8,8,0,1,1-8,8A8,8,0,0,1,128,136Z`}))],[`duotone`,w.createElement(w.Fragment,null,w.createElement(`path`,{d:`M208,88H48a8,8,0,0,0-8,8V208a8,8,0,0,0,8,8H208a8,8,0,0,0,8-8V96A8,8,0,0,0,208,88Zm-80,72a20,20,0,1,1,20-20A20,20,0,0,1,128,160Z`,opacity:`0.2`}),w.createElement(`path`,{d:`M208,80H176V56a48,48,0,0,0-96,0V80H48A16,16,0,0,0,32,96V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V96A16,16,0,0,0,208,80ZM96,56a32,32,0,0,1,64,0V80H96ZM208,208H48V96H208V208Zm-80-96a28,28,0,0,0-8,54.83V184a8,8,0,0,0,16,0V166.83A28,28,0,0,0,128,112Zm0,40a12,12,0,1,1,12-12A12,12,0,0,1,128,152Z`}))],[`fill`,w.createElement(w.Fragment,null,w.createElement(`path`,{d:`M208,80H176V56a48,48,0,0,0-96,0V80H48A16,16,0,0,0,32,96V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V96A16,16,0,0,0,208,80Zm-72,78.63V184a8,8,0,0,1-16,0V158.63a24,24,0,1,1,16,0ZM160,80H96V56a32,32,0,0,1,64,0Z`}))],[`light`,w.createElement(w.Fragment,null,w.createElement(`path`,{d:`M208,82H174V56a46,46,0,0,0-92,0V82H48A14,14,0,0,0,34,96V208a14,14,0,0,0,14,14H208a14,14,0,0,0,14-14V96A14,14,0,0,0,208,82ZM94,56a34,34,0,0,1,68,0V82H94ZM210,208a2,2,0,0,1-2,2H48a2,2,0,0,1-2-2V96a2,2,0,0,1,2-2H208a2,2,0,0,1,2,2Zm-82-94a26,26,0,0,0-6,51.29V184a6,6,0,0,0,12,0V165.29A26,26,0,0,0,128,114Zm0,40a14,14,0,1,1,14-14A14,14,0,0,1,128,154Z`}))],[`regular`,w.createElement(w.Fragment,null,w.createElement(`path`,{d:`M128,112a28,28,0,0,0-8,54.83V184a8,8,0,0,0,16,0V166.83A28,28,0,0,0,128,112Zm0,40a12,12,0,1,1,12-12A12,12,0,0,1,128,152Zm80-72H176V56a48,48,0,0,0-96,0V80H48A16,16,0,0,0,32,96V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V96A16,16,0,0,0,208,80ZM96,56a32,32,0,0,1,64,0V80H96ZM208,208H48V96H208V208Z`}))],[`thin`,w.createElement(w.Fragment,null,w.createElement(`path`,{d:`M208,84H172V56a44,44,0,0,0-88,0V84H48A12,12,0,0,0,36,96V208a12,12,0,0,0,12,12H208a12,12,0,0,0,12-12V96A12,12,0,0,0,208,84ZM92,56a36,36,0,0,1,72,0V84H92ZM212,208a4,4,0,0,1-4,4H48a4,4,0,0,1-4-4V96a4,4,0,0,1,4-4H208a4,4,0,0,1,4,4Zm-84-92a24,24,0,0,0-4,47.66V184a4,4,0,0,0,8,0V163.66A24,24,0,0,0,128,116Zm0,40a16,16,0,1,1,16-16A16,16,0,0,1,128,156Z`}))]]),E=w.forwardRef((e,t)=>w.createElement(r,{ref:t,...e,weights:T}));E.displayName=`LockKeyIcon`;var D=e();function O(){let{t:e}=t(),n=h(),{token:r}=g.useSearch(),[i,T]=(0,w.useState)(!1),{data:O}=a(s),k=O.auth.password.policy,A=(0,w.useMemo)(()=>m({token:p().min(1,e(`validation.token.required`)),password:p().min(k.min_length,e(`validation.password.min`,{count:k.min_length})).max(k.max_length,e(`validation.password.max`,{count:k.max_length})),confirmPassword:p().min(1,e(`validation.confirmPassword.required`))}).refine(e=>e.password===e.confirmPassword,{message:e(`validation.confirmPassword.mismatch`),path:[`confirmPassword`]}),[k.max_length,k.min_length,e]),j=c({...C,onSuccess:()=>{T(!0)}}),{register:M,setError:N,handleSubmit:P,formState:{errors:F}}=y({defaultValues:{token:r||``,password:``,confirmPassword:``},resolver:b(A)});return i?(0,D.jsxs)(u,{cardPadding:!0,maxWidth:`100`,children:[(0,D.jsx)(v,{className:`mb-4`,icon:l,type:`success`,children:e(`resetPassword.success.title`)}),(0,D.jsx)(_,{subtitle:e(`resetPassword.success.description`),title:e(`resetPassword.success.subtitle`)}),(0,D.jsx)(`button`,{className:`btn btn-block h-10 font-semibold text-[14px]`,"data-testid":`reset-password-go-login`,onClick:()=>n({to:`/login`}),type:`button`,children:e(`resetPassword.success.goToLogin`)})]}):(0,D.jsxs)(u,{cardPadding:!0,maxWidth:`100`,children:[(0,D.jsx)(_,{subtitle:e(`resetPassword.subtitle`),title:e(`resetPassword.title`)}),(0,D.jsxs)(`form`,{className:`flex flex-col gap-4`,onSubmit:P(async t=>{try{await j.mutateAsync({token:t.token,password:t.password})}catch(t){t&&typeof t==`object`&&`code`in t?t.code===`INVALID_PASSWORD_RESET_TOKEN`?N(`token`,{type:`manual`,message:e(`resetPassword.error.invalidToken`)}):t.code===`USER_NOT_EDITABLE`&&N(`token`,{type:`manual`,message:e(`resetPassword.error.notEditable`)}):N(`token`,{type:`manual`,message:e(`resetPassword.error.invalidToken`)})}}),children:[!r&&(0,D.jsx)(x,{error:F.token,icon:d,placeholder:e(`resetPassword.token.placeholder`),...M(`token`),type:`text`}),r&&F.token&&(0,D.jsx)(`div`,{className:`alert alert-error`,"data-testid":`reset-password-token-error`,children:(0,D.jsx)(`span`,{className:`text-sm`,children:F.token.message})}),(0,D.jsx)(x,{autoComplete:`new-password`,error:F.password,icon:f,placeholder:e(`resetPassword.password.placeholder`),...M(`password`),type:`password`}),(0,D.jsx)(x,{autoComplete:`new-password`,error:F.confirmPassword,icon:E,placeholder:e(`resetPassword.confirmPassword.placeholder`),...M(`confirmPassword`),type:`password`}),(0,D.jsx)(S,{className:`mt-2`,isPending:j.isPending,pendingText:e(`resetPassword.submitting`),children:e(`resetPassword.submit`)})]}),(0,D.jsx)(`div`,{className:`mt-6 text-center text-base-content/70 text-xs`,children:(0,D.jsx)(o,{className:`link link-info font-medium`,"data-testid":`reset-password-back-to-login`,to:`/login`,children:e(`resetPassword.backToLogin`)})})]})}export{O as component};
2
+ //# sourceMappingURL=reset-1nsqPlxL.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"reset-CgACYrdp.js","names":["e","t","a","standardSchemaResolver","CheckCircleIcon","KeyIcon","LockIcon","LockKeyIcon","useMutation","useSuspenseQuery","Link","useNavigate","useMemo","useState","useForm","useTranslation","z","IconInput","PageHeader","SubmitButton","Alert","PageLayout","appConfigQueryOptions","resetPasswordMutationOptions","Route","ResetPasswordFormValues","token","password","confirmPassword","ResetPassword","t","navigate","search","useSearch","queryToken","resetSuccess","setResetSuccess","data","configData","passwordPolicy","auth","policy","resetPasswordSchema","object","string","min","min_length","count","max","max_length","refine","message","path","resetPasswordMutation","onSuccess","register","setError","handleSubmit","formState","errors","defaultValues","resolver","onSubmit","values","mutateAsync","error","code","type","to","isPending","component"],"sources":["../../../../node_modules/.pnpm/@phosphor-icons+react@2.1.10_react-dom@19.2.7_react@19.2.7__react@19.2.7/node_modules/@phosphor-icons/react/dist/defs/LockKey.es.js","../../../../node_modules/.pnpm/@phosphor-icons+react@2.1.10_react-dom@19.2.7_react@19.2.7__react@19.2.7/node_modules/@phosphor-icons/react/dist/csr/LockKey.es.js","../../../frontend/src/routes/password/reset/index.tsx?tsr-split=component"],"sourcesContent":["import * as a from \"react\";\nconst e = /* @__PURE__ */ new Map([\n [\n \"bold\",\n /* @__PURE__ */ a.createElement(a.Fragment, null, /* @__PURE__ */ a.createElement(\"path\", { d: \"M208,76H180V56A52,52,0,0,0,76,56V76H48A20,20,0,0,0,28,96V208a20,20,0,0,0,20,20H208a20,20,0,0,0,20-20V96A20,20,0,0,0,208,76ZM100,56a28,28,0,0,1,56,0V76H100ZM204,204H52V100H204Zm-76-92a32,32,0,0,0-12,61.66V180a12,12,0,0,0,24,0v-6.34A32,32,0,0,0,128,112Zm0,24a8,8,0,1,1-8,8A8,8,0,0,1,128,136Z\" }))\n ],\n [\n \"duotone\",\n /* @__PURE__ */ a.createElement(a.Fragment, null, /* @__PURE__ */ a.createElement(\n \"path\",\n {\n d: \"M208,88H48a8,8,0,0,0-8,8V208a8,8,0,0,0,8,8H208a8,8,0,0,0,8-8V96A8,8,0,0,0,208,88Zm-80,72a20,20,0,1,1,20-20A20,20,0,0,1,128,160Z\",\n opacity: \"0.2\"\n }\n ), /* @__PURE__ */ a.createElement(\"path\", { d: \"M208,80H176V56a48,48,0,0,0-96,0V80H48A16,16,0,0,0,32,96V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V96A16,16,0,0,0,208,80ZM96,56a32,32,0,0,1,64,0V80H96ZM208,208H48V96H208V208Zm-80-96a28,28,0,0,0-8,54.83V184a8,8,0,0,0,16,0V166.83A28,28,0,0,0,128,112Zm0,40a12,12,0,1,1,12-12A12,12,0,0,1,128,152Z\" }))\n ],\n [\n \"fill\",\n /* @__PURE__ */ a.createElement(a.Fragment, null, /* @__PURE__ */ a.createElement(\"path\", { d: \"M208,80H176V56a48,48,0,0,0-96,0V80H48A16,16,0,0,0,32,96V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V96A16,16,0,0,0,208,80Zm-72,78.63V184a8,8,0,0,1-16,0V158.63a24,24,0,1,1,16,0ZM160,80H96V56a32,32,0,0,1,64,0Z\" }))\n ],\n [\n \"light\",\n /* @__PURE__ */ a.createElement(a.Fragment, null, /* @__PURE__ */ a.createElement(\"path\", { d: \"M208,82H174V56a46,46,0,0,0-92,0V82H48A14,14,0,0,0,34,96V208a14,14,0,0,0,14,14H208a14,14,0,0,0,14-14V96A14,14,0,0,0,208,82ZM94,56a34,34,0,0,1,68,0V82H94ZM210,208a2,2,0,0,1-2,2H48a2,2,0,0,1-2-2V96a2,2,0,0,1,2-2H208a2,2,0,0,1,2,2Zm-82-94a26,26,0,0,0-6,51.29V184a6,6,0,0,0,12,0V165.29A26,26,0,0,0,128,114Zm0,40a14,14,0,1,1,14-14A14,14,0,0,1,128,154Z\" }))\n ],\n [\n \"regular\",\n /* @__PURE__ */ a.createElement(a.Fragment, null, /* @__PURE__ */ a.createElement(\"path\", { d: \"M128,112a28,28,0,0,0-8,54.83V184a8,8,0,0,0,16,0V166.83A28,28,0,0,0,128,112Zm0,40a12,12,0,1,1,12-12A12,12,0,0,1,128,152Zm80-72H176V56a48,48,0,0,0-96,0V80H48A16,16,0,0,0,32,96V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V96A16,16,0,0,0,208,80ZM96,56a32,32,0,0,1,64,0V80H96ZM208,208H48V96H208V208Z\" }))\n ],\n [\n \"thin\",\n /* @__PURE__ */ a.createElement(a.Fragment, null, /* @__PURE__ */ a.createElement(\"path\", { d: \"M208,84H172V56a44,44,0,0,0-88,0V84H48A12,12,0,0,0,36,96V208a12,12,0,0,0,12,12H208a12,12,0,0,0,12-12V96A12,12,0,0,0,208,84ZM92,56a36,36,0,0,1,72,0V84H92ZM212,208a4,4,0,0,1-4,4H48a4,4,0,0,1-4-4V96a4,4,0,0,1,4-4H208a4,4,0,0,1,4,4Zm-84-92a24,24,0,0,0-4,47.66V184a4,4,0,0,0,8,0V163.66A24,24,0,0,0,128,116Zm0,40a16,16,0,1,1,16-16A16,16,0,0,1,128,156Z\" }))\n ]\n]);\nexport {\n e as default\n};\n","import * as o from \"react\";\nimport t from \"../lib/IconBase.es.js\";\nimport a from \"../defs/LockKey.es.js\";\nconst e = o.forwardRef((c, r) => /* @__PURE__ */ o.createElement(t, { ref: r, ...c, weights: a }));\ne.displayName = \"LockKeyIcon\";\nconst n = e;\nexport {\n n as LockKey,\n e as LockKeyIcon\n};\n","import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';\nimport {\n CheckCircleIcon,\n KeyIcon,\n LockIcon,\n LockKeyIcon,\n} from '@phosphor-icons/react';\nimport { useMutation, useSuspenseQuery } from '@tanstack/react-query';\nimport {\n createFileRoute,\n Link,\n redirect,\n useNavigate,\n} from '@tanstack/react-router';\nimport { useMemo, useState } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport { IconInput } from '#frontend/components/auth/icon-input.tsx';\nimport { PageHeader } from '#frontend/components/auth/page-header.tsx';\nimport { SubmitButton } from '#frontend/components/auth/submit-button.tsx';\nimport { Alert } from '#frontend/components/ui/alert.tsx';\nimport { RouteErrorFallback } from '#frontend/components/ui/route-error-fallback.tsx';\nimport { PageLayout } from '#frontend/features/layout/page-layout.tsx';\nimport { appConfigQueryOptions } from '#frontend/queries/config.ts';\nimport { resetPasswordMutationOptions } from '#frontend/queries/password-reset.ts';\n\nconst SearchSchema = z.object({\n token: z.string().default(''),\n});\n\nexport const Route = createFileRoute('/password/reset/')({\n component: ResetPassword,\n errorComponent: RouteErrorFallback,\n validateSearch: SearchSchema,\n beforeLoad: async ({ context }) => {\n const config = await context.queryClient.ensureQueryData(\n appConfigQueryOptions,\n );\n const isPasswordAuthEnabled = config.auth.password.enabled;\n if (!isPasswordAuthEnabled) {\n throw redirect({ to: '/login' });\n }\n },\n loader: async ({ context }) => {\n await context.queryClient.ensureQueryData(appConfigQueryOptions);\n },\n});\n\ntype ResetPasswordFormValues = {\n token: string;\n password: string;\n confirmPassword: string;\n};\n\nfunction ResetPassword() {\n const { t } = useTranslation();\n const navigate = useNavigate();\n const search = Route.useSearch();\n const { token: queryToken } = search;\n const [resetSuccess, setResetSuccess] = useState(false);\n const { data: configData } = useSuspenseQuery(appConfigQueryOptions);\n const passwordPolicy = configData.auth.password.policy;\n\n const resetPasswordSchema = useMemo(\n () =>\n z\n .object({\n token: z.string().min(1, t('validation.token.required')),\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\n .string()\n .min(1, t('validation.confirmPassword.required')),\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 resetPasswordMutation = useMutation({\n ...resetPasswordMutationOptions,\n onSuccess: () => {\n setResetSuccess(true);\n },\n });\n\n const {\n register,\n setError,\n handleSubmit,\n formState: { errors },\n } = useForm<ResetPasswordFormValues>({\n defaultValues: {\n token: queryToken || '',\n password: '',\n confirmPassword: '',\n },\n resolver: standardSchemaResolver(resetPasswordSchema),\n });\n\n const onSubmit = async (values: ResetPasswordFormValues) => {\n try {\n await resetPasswordMutation.mutateAsync({\n token: values.token,\n password: values.password,\n });\n } catch (error) {\n if (error && typeof error === 'object' && 'code' in error) {\n if (error.code === 'INVALID_PASSWORD_RESET_TOKEN') {\n setError('token', {\n type: 'manual',\n message: t('resetPassword.error.invalidToken'),\n });\n } else if (error.code === 'USER_NOT_EDITABLE') {\n setError('token', {\n type: 'manual',\n message: t('resetPassword.error.notEditable'),\n });\n }\n } else {\n setError('token', {\n type: 'manual',\n message: t('resetPassword.error.invalidToken'),\n });\n }\n }\n };\n\n if (resetSuccess) {\n return (\n <PageLayout cardPadding maxWidth=\"100\">\n <Alert className=\"mb-4\" icon={CheckCircleIcon} type=\"success\">\n {t('resetPassword.success.title')}\n </Alert>\n\n <PageHeader\n subtitle={t('resetPassword.success.description')}\n title={t('resetPassword.success.subtitle')}\n />\n\n <button\n className=\"btn btn-block h-10 font-semibold text-[14px]\"\n data-testid=\"reset-password-go-login\"\n onClick={() => navigate({ to: '/login' })}\n type=\"button\"\n >\n {t('resetPassword.success.goToLogin')}\n </button>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout cardPadding maxWidth=\"100\">\n <PageHeader\n subtitle={t('resetPassword.subtitle')}\n title={t('resetPassword.title')}\n />\n\n <form className=\"flex flex-col gap-4\" onSubmit={handleSubmit(onSubmit)}>\n {!queryToken && (\n <IconInput\n error={errors.token}\n icon={KeyIcon}\n placeholder={t('resetPassword.token.placeholder')}\n {...register('token')}\n type=\"text\"\n />\n )}\n\n {queryToken && errors.token && (\n <div\n className=\"alert alert-error\"\n data-testid=\"reset-password-token-error\"\n >\n <span className=\"text-sm\">{errors.token.message}</span>\n </div>\n )}\n\n <IconInput\n autoComplete=\"new-password\"\n error={errors.password}\n icon={LockIcon}\n placeholder={t('resetPassword.password.placeholder')}\n {...register('password')}\n type=\"password\"\n />\n\n <IconInput\n autoComplete=\"new-password\"\n error={errors.confirmPassword}\n icon={LockKeyIcon}\n placeholder={t('resetPassword.confirmPassword.placeholder')}\n {...register('confirmPassword')}\n type=\"password\"\n />\n\n <SubmitButton\n className=\"mt-2\"\n isPending={resetPasswordMutation.isPending}\n pendingText={t('resetPassword.submitting')}\n >\n {t('resetPassword.submit')}\n </SubmitButton>\n </form>\n\n <div className=\"mt-6 text-center text-base-content/70 text-xs\">\n <Link\n className=\"link link-info font-medium\"\n data-testid=\"reset-password-back-to-login\"\n to=\"/login\"\n >\n {t('resetPassword.backToLogin')}\n </Link>\n </div>\n </PageLayout>\n );\n}\n"],"x_google_ignoreList":[0,1],"mappings":"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"}
1
+ {"version":3,"file":"reset-1nsqPlxL.js","names":["e","t","a","standardSchemaResolver","CheckCircleIcon","KeyIcon","LockIcon","LockKeyIcon","useMutation","useSuspenseQuery","Link","useNavigate","useMemo","useState","useForm","useTranslation","z","IconInput","PageHeader","SubmitButton","Alert","PageLayout","appConfigQueryOptions","resetPasswordMutationOptions","Route","ResetPasswordFormValues","token","password","confirmPassword","ResetPassword","t","navigate","search","useSearch","queryToken","resetSuccess","setResetSuccess","data","configData","passwordPolicy","auth","policy","resetPasswordSchema","object","string","min","min_length","count","max","max_length","refine","message","path","resetPasswordMutation","onSuccess","register","setError","handleSubmit","formState","errors","defaultValues","resolver","onSubmit","values","mutateAsync","error","code","type","to","isPending","component"],"sources":["../../../../node_modules/.pnpm/@phosphor-icons+react@2.1.10_react-dom@19.2.7_react@19.2.7__react@19.2.7/node_modules/@phosphor-icons/react/dist/defs/LockKey.es.js","../../../../node_modules/.pnpm/@phosphor-icons+react@2.1.10_react-dom@19.2.7_react@19.2.7__react@19.2.7/node_modules/@phosphor-icons/react/dist/csr/LockKey.es.js","../../../frontend/src/routes/password/reset/index.tsx?tsr-split=component"],"sourcesContent":["import * as a from \"react\";\nconst e = /* @__PURE__ */ new Map([\n [\n \"bold\",\n /* @__PURE__ */ a.createElement(a.Fragment, null, /* @__PURE__ */ a.createElement(\"path\", { d: \"M208,76H180V56A52,52,0,0,0,76,56V76H48A20,20,0,0,0,28,96V208a20,20,0,0,0,20,20H208a20,20,0,0,0,20-20V96A20,20,0,0,0,208,76ZM100,56a28,28,0,0,1,56,0V76H100ZM204,204H52V100H204Zm-76-92a32,32,0,0,0-12,61.66V180a12,12,0,0,0,24,0v-6.34A32,32,0,0,0,128,112Zm0,24a8,8,0,1,1-8,8A8,8,0,0,1,128,136Z\" }))\n ],\n [\n \"duotone\",\n /* @__PURE__ */ a.createElement(a.Fragment, null, /* @__PURE__ */ a.createElement(\n \"path\",\n {\n d: \"M208,88H48a8,8,0,0,0-8,8V208a8,8,0,0,0,8,8H208a8,8,0,0,0,8-8V96A8,8,0,0,0,208,88Zm-80,72a20,20,0,1,1,20-20A20,20,0,0,1,128,160Z\",\n opacity: \"0.2\"\n }\n ), /* @__PURE__ */ a.createElement(\"path\", { d: \"M208,80H176V56a48,48,0,0,0-96,0V80H48A16,16,0,0,0,32,96V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V96A16,16,0,0,0,208,80ZM96,56a32,32,0,0,1,64,0V80H96ZM208,208H48V96H208V208Zm-80-96a28,28,0,0,0-8,54.83V184a8,8,0,0,0,16,0V166.83A28,28,0,0,0,128,112Zm0,40a12,12,0,1,1,12-12A12,12,0,0,1,128,152Z\" }))\n ],\n [\n \"fill\",\n /* @__PURE__ */ a.createElement(a.Fragment, null, /* @__PURE__ */ a.createElement(\"path\", { d: \"M208,80H176V56a48,48,0,0,0-96,0V80H48A16,16,0,0,0,32,96V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V96A16,16,0,0,0,208,80Zm-72,78.63V184a8,8,0,0,1-16,0V158.63a24,24,0,1,1,16,0ZM160,80H96V56a32,32,0,0,1,64,0Z\" }))\n ],\n [\n \"light\",\n /* @__PURE__ */ a.createElement(a.Fragment, null, /* @__PURE__ */ a.createElement(\"path\", { d: \"M208,82H174V56a46,46,0,0,0-92,0V82H48A14,14,0,0,0,34,96V208a14,14,0,0,0,14,14H208a14,14,0,0,0,14-14V96A14,14,0,0,0,208,82ZM94,56a34,34,0,0,1,68,0V82H94ZM210,208a2,2,0,0,1-2,2H48a2,2,0,0,1-2-2V96a2,2,0,0,1,2-2H208a2,2,0,0,1,2,2Zm-82-94a26,26,0,0,0-6,51.29V184a6,6,0,0,0,12,0V165.29A26,26,0,0,0,128,114Zm0,40a14,14,0,1,1,14-14A14,14,0,0,1,128,154Z\" }))\n ],\n [\n \"regular\",\n /* @__PURE__ */ a.createElement(a.Fragment, null, /* @__PURE__ */ a.createElement(\"path\", { d: \"M128,112a28,28,0,0,0-8,54.83V184a8,8,0,0,0,16,0V166.83A28,28,0,0,0,128,112Zm0,40a12,12,0,1,1,12-12A12,12,0,0,1,128,152Zm80-72H176V56a48,48,0,0,0-96,0V80H48A16,16,0,0,0,32,96V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V96A16,16,0,0,0,208,80ZM96,56a32,32,0,0,1,64,0V80H96ZM208,208H48V96H208V208Z\" }))\n ],\n [\n \"thin\",\n /* @__PURE__ */ a.createElement(a.Fragment, null, /* @__PURE__ */ a.createElement(\"path\", { d: \"M208,84H172V56a44,44,0,0,0-88,0V84H48A12,12,0,0,0,36,96V208a12,12,0,0,0,12,12H208a12,12,0,0,0,12-12V96A12,12,0,0,0,208,84ZM92,56a36,36,0,0,1,72,0V84H92ZM212,208a4,4,0,0,1-4,4H48a4,4,0,0,1-4-4V96a4,4,0,0,1,4-4H208a4,4,0,0,1,4,4Zm-84-92a24,24,0,0,0-4,47.66V184a4,4,0,0,0,8,0V163.66A24,24,0,0,0,128,116Zm0,40a16,16,0,1,1,16-16A16,16,0,0,1,128,156Z\" }))\n ]\n]);\nexport {\n e as default\n};\n","import * as o from \"react\";\nimport t from \"../lib/IconBase.es.js\";\nimport a from \"../defs/LockKey.es.js\";\nconst e = o.forwardRef((c, r) => /* @__PURE__ */ o.createElement(t, { ref: r, ...c, weights: a }));\ne.displayName = \"LockKeyIcon\";\nconst n = e;\nexport {\n n as LockKey,\n e as LockKeyIcon\n};\n","import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';\nimport {\n CheckCircleIcon,\n KeyIcon,\n LockIcon,\n LockKeyIcon,\n} from '@phosphor-icons/react';\nimport { useMutation, useSuspenseQuery } from '@tanstack/react-query';\nimport {\n createFileRoute,\n Link,\n redirect,\n useNavigate,\n} from '@tanstack/react-router';\nimport { useMemo, useState } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport { IconInput } from '#frontend/components/auth/icon-input.tsx';\nimport { PageHeader } from '#frontend/components/auth/page-header.tsx';\nimport { SubmitButton } from '#frontend/components/auth/submit-button.tsx';\nimport { Alert } from '#frontend/components/ui/alert.tsx';\nimport { RouteErrorFallback } from '#frontend/components/ui/route-error-fallback.tsx';\nimport { PageLayout } from '#frontend/features/layout/page-layout.tsx';\nimport { appConfigQueryOptions } from '#frontend/queries/config.ts';\nimport { resetPasswordMutationOptions } from '#frontend/queries/password-reset.ts';\n\nconst SearchSchema = z.object({\n token: z.string().default(''),\n});\n\nexport const Route = createFileRoute('/password/reset/')({\n component: ResetPassword,\n errorComponent: RouteErrorFallback,\n validateSearch: SearchSchema,\n beforeLoad: async ({ context }) => {\n const config = await context.queryClient.ensureQueryData(\n appConfigQueryOptions,\n );\n const isPasswordAuthEnabled = config.auth.password.enabled;\n if (!isPasswordAuthEnabled) {\n throw redirect({ to: '/login' });\n }\n },\n loader: async ({ context }) => {\n await context.queryClient.ensureQueryData(appConfigQueryOptions);\n },\n});\n\ntype ResetPasswordFormValues = {\n token: string;\n password: string;\n confirmPassword: string;\n};\n\nfunction ResetPassword() {\n const { t } = useTranslation();\n const navigate = useNavigate();\n const search = Route.useSearch();\n const { token: queryToken } = search;\n const [resetSuccess, setResetSuccess] = useState(false);\n const { data: configData } = useSuspenseQuery(appConfigQueryOptions);\n const passwordPolicy = configData.auth.password.policy;\n\n const resetPasswordSchema = useMemo(\n () =>\n z\n .object({\n token: z.string().min(1, t('validation.token.required')),\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\n .string()\n .min(1, t('validation.confirmPassword.required')),\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 resetPasswordMutation = useMutation({\n ...resetPasswordMutationOptions,\n onSuccess: () => {\n setResetSuccess(true);\n },\n });\n\n const {\n register,\n setError,\n handleSubmit,\n formState: { errors },\n } = useForm<ResetPasswordFormValues>({\n defaultValues: {\n token: queryToken || '',\n password: '',\n confirmPassword: '',\n },\n resolver: standardSchemaResolver(resetPasswordSchema),\n });\n\n const onSubmit = async (values: ResetPasswordFormValues) => {\n try {\n await resetPasswordMutation.mutateAsync({\n token: values.token,\n password: values.password,\n });\n } catch (error) {\n if (error && typeof error === 'object' && 'code' in error) {\n if (error.code === 'INVALID_PASSWORD_RESET_TOKEN') {\n setError('token', {\n type: 'manual',\n message: t('resetPassword.error.invalidToken'),\n });\n } else if (error.code === 'USER_NOT_EDITABLE') {\n setError('token', {\n type: 'manual',\n message: t('resetPassword.error.notEditable'),\n });\n }\n } else {\n setError('token', {\n type: 'manual',\n message: t('resetPassword.error.invalidToken'),\n });\n }\n }\n };\n\n if (resetSuccess) {\n return (\n <PageLayout cardPadding maxWidth=\"100\">\n <Alert className=\"mb-4\" icon={CheckCircleIcon} type=\"success\">\n {t('resetPassword.success.title')}\n </Alert>\n\n <PageHeader\n subtitle={t('resetPassword.success.description')}\n title={t('resetPassword.success.subtitle')}\n />\n\n <button\n className=\"btn btn-block h-10 font-semibold text-[14px]\"\n data-testid=\"reset-password-go-login\"\n onClick={() => navigate({ to: '/login' })}\n type=\"button\"\n >\n {t('resetPassword.success.goToLogin')}\n </button>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout cardPadding maxWidth=\"100\">\n <PageHeader\n subtitle={t('resetPassword.subtitle')}\n title={t('resetPassword.title')}\n />\n\n <form className=\"flex flex-col gap-4\" onSubmit={handleSubmit(onSubmit)}>\n {!queryToken && (\n <IconInput\n error={errors.token}\n icon={KeyIcon}\n placeholder={t('resetPassword.token.placeholder')}\n {...register('token')}\n type=\"text\"\n />\n )}\n\n {queryToken && errors.token && (\n <div\n className=\"alert alert-error\"\n data-testid=\"reset-password-token-error\"\n >\n <span className=\"text-sm\">{errors.token.message}</span>\n </div>\n )}\n\n <IconInput\n autoComplete=\"new-password\"\n error={errors.password}\n icon={LockIcon}\n placeholder={t('resetPassword.password.placeholder')}\n {...register('password')}\n type=\"password\"\n />\n\n <IconInput\n autoComplete=\"new-password\"\n error={errors.confirmPassword}\n icon={LockKeyIcon}\n placeholder={t('resetPassword.confirmPassword.placeholder')}\n {...register('confirmPassword')}\n type=\"password\"\n />\n\n <SubmitButton\n className=\"mt-2\"\n isPending={resetPasswordMutation.isPending}\n pendingText={t('resetPassword.submitting')}\n >\n {t('resetPassword.submit')}\n </SubmitButton>\n </form>\n\n <div className=\"mt-6 text-center text-base-content/70 text-xs\">\n <Link\n className=\"link link-info font-medium\"\n data-testid=\"reset-password-back-to-login\"\n to=\"/login\"\n >\n {t('resetPassword.backToLogin')}\n </Link>\n </div>\n </PageLayout>\n );\n}\n"],"x_google_ignoreList":[0,1],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import{N as e}from"./index-BmfaaNx6.js";var t=e;export{t as errorComponent};
2
+ //# sourceMappingURL=reset-BMEIiDXv.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"reset-SSKfXB7c.js","names":["RouteErrorFallback","ResetPasswordFormValues","token","password","confirmPassword","SplitErrorComponent","errorComponent"],"sources":["../../../frontend/src/routes/password/reset/index.tsx?tsr-split=errorComponent"],"sourcesContent":["import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';\nimport {\n CheckCircleIcon,\n KeyIcon,\n LockIcon,\n LockKeyIcon,\n} from '@phosphor-icons/react';\nimport { useMutation, useSuspenseQuery } from '@tanstack/react-query';\nimport {\n createFileRoute,\n Link,\n redirect,\n useNavigate,\n} from '@tanstack/react-router';\nimport { useMemo, useState } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport { IconInput } from '#frontend/components/auth/icon-input.tsx';\nimport { PageHeader } from '#frontend/components/auth/page-header.tsx';\nimport { SubmitButton } from '#frontend/components/auth/submit-button.tsx';\nimport { Alert } from '#frontend/components/ui/alert.tsx';\nimport { RouteErrorFallback } from '#frontend/components/ui/route-error-fallback.tsx';\nimport { PageLayout } from '#frontend/features/layout/page-layout.tsx';\nimport { appConfigQueryOptions } from '#frontend/queries/config.ts';\nimport { resetPasswordMutationOptions } from '#frontend/queries/password-reset.ts';\n\nconst SearchSchema = z.object({\n token: z.string().default(''),\n});\n\nexport const Route = createFileRoute('/password/reset/')({\n component: ResetPassword,\n errorComponent: RouteErrorFallback,\n validateSearch: SearchSchema,\n beforeLoad: async ({ context }) => {\n const config = await context.queryClient.ensureQueryData(\n appConfigQueryOptions,\n );\n const isPasswordAuthEnabled = config.auth.password.enabled;\n if (!isPasswordAuthEnabled) {\n throw redirect({ to: '/login' });\n }\n },\n loader: async ({ context }) => {\n await context.queryClient.ensureQueryData(appConfigQueryOptions);\n },\n});\n\ntype ResetPasswordFormValues = {\n token: string;\n password: string;\n confirmPassword: string;\n};\n\nfunction ResetPassword() {\n const { t } = useTranslation();\n const navigate = useNavigate();\n const search = Route.useSearch();\n const { token: queryToken } = search;\n const [resetSuccess, setResetSuccess] = useState(false);\n const { data: configData } = useSuspenseQuery(appConfigQueryOptions);\n const passwordPolicy = configData.auth.password.policy;\n\n const resetPasswordSchema = useMemo(\n () =>\n z\n .object({\n token: z.string().min(1, t('validation.token.required')),\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\n .string()\n .min(1, t('validation.confirmPassword.required')),\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 resetPasswordMutation = useMutation({\n ...resetPasswordMutationOptions,\n onSuccess: () => {\n setResetSuccess(true);\n },\n });\n\n const {\n register,\n setError,\n handleSubmit,\n formState: { errors },\n } = useForm<ResetPasswordFormValues>({\n defaultValues: {\n token: queryToken || '',\n password: '',\n confirmPassword: '',\n },\n resolver: standardSchemaResolver(resetPasswordSchema),\n });\n\n const onSubmit = async (values: ResetPasswordFormValues) => {\n try {\n await resetPasswordMutation.mutateAsync({\n token: values.token,\n password: values.password,\n });\n } catch (error) {\n if (error && typeof error === 'object' && 'code' in error) {\n if (error.code === 'INVALID_PASSWORD_RESET_TOKEN') {\n setError('token', {\n type: 'manual',\n message: t('resetPassword.error.invalidToken'),\n });\n } else if (error.code === 'USER_NOT_EDITABLE') {\n setError('token', {\n type: 'manual',\n message: t('resetPassword.error.notEditable'),\n });\n }\n } else {\n setError('token', {\n type: 'manual',\n message: t('resetPassword.error.invalidToken'),\n });\n }\n }\n };\n\n if (resetSuccess) {\n return (\n <PageLayout cardPadding maxWidth=\"100\">\n <Alert className=\"mb-4\" icon={CheckCircleIcon} type=\"success\">\n {t('resetPassword.success.title')}\n </Alert>\n\n <PageHeader\n subtitle={t('resetPassword.success.description')}\n title={t('resetPassword.success.subtitle')}\n />\n\n <button\n className=\"btn btn-block h-10 font-semibold text-[14px]\"\n data-testid=\"reset-password-go-login\"\n onClick={() => navigate({ to: '/login' })}\n type=\"button\"\n >\n {t('resetPassword.success.goToLogin')}\n </button>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout cardPadding maxWidth=\"100\">\n <PageHeader\n subtitle={t('resetPassword.subtitle')}\n title={t('resetPassword.title')}\n />\n\n <form className=\"flex flex-col gap-4\" onSubmit={handleSubmit(onSubmit)}>\n {!queryToken && (\n <IconInput\n error={errors.token}\n icon={KeyIcon}\n placeholder={t('resetPassword.token.placeholder')}\n {...register('token')}\n type=\"text\"\n />\n )}\n\n {queryToken && errors.token && (\n <div\n className=\"alert alert-error\"\n data-testid=\"reset-password-token-error\"\n >\n <span className=\"text-sm\">{errors.token.message}</span>\n </div>\n )}\n\n <IconInput\n autoComplete=\"new-password\"\n error={errors.password}\n icon={LockIcon}\n placeholder={t('resetPassword.password.placeholder')}\n {...register('password')}\n type=\"password\"\n />\n\n <IconInput\n autoComplete=\"new-password\"\n error={errors.confirmPassword}\n icon={LockKeyIcon}\n placeholder={t('resetPassword.confirmPassword.placeholder')}\n {...register('confirmPassword')}\n type=\"password\"\n />\n\n <SubmitButton\n className=\"mt-2\"\n isPending={resetPasswordMutation.isPending}\n pendingText={t('resetPassword.submitting')}\n >\n {t('resetPassword.submit')}\n </SubmitButton>\n </form>\n\n <div className=\"mt-6 text-center text-base-content/70 text-xs\">\n <Link\n className=\"link link-info font-medium\"\n data-testid=\"reset-password-back-to-login\"\n to=\"/login\"\n >\n {t('resetPassword.backToLogin')}\n </Link>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"wCAqDE,IAAAK,EA/BOL"}
1
+ {"version":3,"file":"reset-BMEIiDXv.js","names":["RouteErrorFallback","ResetPasswordFormValues","token","password","confirmPassword","SplitErrorComponent","errorComponent"],"sources":["../../../frontend/src/routes/password/reset/index.tsx?tsr-split=errorComponent"],"sourcesContent":["import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';\nimport {\n CheckCircleIcon,\n KeyIcon,\n LockIcon,\n LockKeyIcon,\n} from '@phosphor-icons/react';\nimport { useMutation, useSuspenseQuery } from '@tanstack/react-query';\nimport {\n createFileRoute,\n Link,\n redirect,\n useNavigate,\n} from '@tanstack/react-router';\nimport { useMemo, useState } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport { IconInput } from '#frontend/components/auth/icon-input.tsx';\nimport { PageHeader } from '#frontend/components/auth/page-header.tsx';\nimport { SubmitButton } from '#frontend/components/auth/submit-button.tsx';\nimport { Alert } from '#frontend/components/ui/alert.tsx';\nimport { RouteErrorFallback } from '#frontend/components/ui/route-error-fallback.tsx';\nimport { PageLayout } from '#frontend/features/layout/page-layout.tsx';\nimport { appConfigQueryOptions } from '#frontend/queries/config.ts';\nimport { resetPasswordMutationOptions } from '#frontend/queries/password-reset.ts';\n\nconst SearchSchema = z.object({\n token: z.string().default(''),\n});\n\nexport const Route = createFileRoute('/password/reset/')({\n component: ResetPassword,\n errorComponent: RouteErrorFallback,\n validateSearch: SearchSchema,\n beforeLoad: async ({ context }) => {\n const config = await context.queryClient.ensureQueryData(\n appConfigQueryOptions,\n );\n const isPasswordAuthEnabled = config.auth.password.enabled;\n if (!isPasswordAuthEnabled) {\n throw redirect({ to: '/login' });\n }\n },\n loader: async ({ context }) => {\n await context.queryClient.ensureQueryData(appConfigQueryOptions);\n },\n});\n\ntype ResetPasswordFormValues = {\n token: string;\n password: string;\n confirmPassword: string;\n};\n\nfunction ResetPassword() {\n const { t } = useTranslation();\n const navigate = useNavigate();\n const search = Route.useSearch();\n const { token: queryToken } = search;\n const [resetSuccess, setResetSuccess] = useState(false);\n const { data: configData } = useSuspenseQuery(appConfigQueryOptions);\n const passwordPolicy = configData.auth.password.policy;\n\n const resetPasswordSchema = useMemo(\n () =>\n z\n .object({\n token: z.string().min(1, t('validation.token.required')),\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\n .string()\n .min(1, t('validation.confirmPassword.required')),\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 resetPasswordMutation = useMutation({\n ...resetPasswordMutationOptions,\n onSuccess: () => {\n setResetSuccess(true);\n },\n });\n\n const {\n register,\n setError,\n handleSubmit,\n formState: { errors },\n } = useForm<ResetPasswordFormValues>({\n defaultValues: {\n token: queryToken || '',\n password: '',\n confirmPassword: '',\n },\n resolver: standardSchemaResolver(resetPasswordSchema),\n });\n\n const onSubmit = async (values: ResetPasswordFormValues) => {\n try {\n await resetPasswordMutation.mutateAsync({\n token: values.token,\n password: values.password,\n });\n } catch (error) {\n if (error && typeof error === 'object' && 'code' in error) {\n if (error.code === 'INVALID_PASSWORD_RESET_TOKEN') {\n setError('token', {\n type: 'manual',\n message: t('resetPassword.error.invalidToken'),\n });\n } else if (error.code === 'USER_NOT_EDITABLE') {\n setError('token', {\n type: 'manual',\n message: t('resetPassword.error.notEditable'),\n });\n }\n } else {\n setError('token', {\n type: 'manual',\n message: t('resetPassword.error.invalidToken'),\n });\n }\n }\n };\n\n if (resetSuccess) {\n return (\n <PageLayout cardPadding maxWidth=\"100\">\n <Alert className=\"mb-4\" icon={CheckCircleIcon} type=\"success\">\n {t('resetPassword.success.title')}\n </Alert>\n\n <PageHeader\n subtitle={t('resetPassword.success.description')}\n title={t('resetPassword.success.subtitle')}\n />\n\n <button\n className=\"btn btn-block h-10 font-semibold text-[14px]\"\n data-testid=\"reset-password-go-login\"\n onClick={() => navigate({ to: '/login' })}\n type=\"button\"\n >\n {t('resetPassword.success.goToLogin')}\n </button>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout cardPadding maxWidth=\"100\">\n <PageHeader\n subtitle={t('resetPassword.subtitle')}\n title={t('resetPassword.title')}\n />\n\n <form className=\"flex flex-col gap-4\" onSubmit={handleSubmit(onSubmit)}>\n {!queryToken && (\n <IconInput\n error={errors.token}\n icon={KeyIcon}\n placeholder={t('resetPassword.token.placeholder')}\n {...register('token')}\n type=\"text\"\n />\n )}\n\n {queryToken && errors.token && (\n <div\n className=\"alert alert-error\"\n data-testid=\"reset-password-token-error\"\n >\n <span className=\"text-sm\">{errors.token.message}</span>\n </div>\n )}\n\n <IconInput\n autoComplete=\"new-password\"\n error={errors.password}\n icon={LockIcon}\n placeholder={t('resetPassword.password.placeholder')}\n {...register('password')}\n type=\"password\"\n />\n\n <IconInput\n autoComplete=\"new-password\"\n error={errors.confirmPassword}\n icon={LockKeyIcon}\n placeholder={t('resetPassword.confirmPassword.placeholder')}\n {...register('confirmPassword')}\n type=\"password\"\n />\n\n <SubmitButton\n className=\"mt-2\"\n isPending={resetPasswordMutation.isPending}\n pendingText={t('resetPassword.submitting')}\n >\n {t('resetPassword.submit')}\n </SubmitButton>\n </form>\n\n <div className=\"mt-6 text-center text-base-content/70 text-xs\">\n <Link\n className=\"link link-info font-medium\"\n data-testid=\"reset-password-back-to-login\"\n to=\"/login\"\n >\n {t('resetPassword.backToLogin')}\n </Link>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"wCAqDE,IAAAK,EA/BOL"}
@@ -0,0 +1,2 @@
1
+ import{N as e}from"./index-BmfaaNx6.js";var t=e;export{t as errorComponent};
2
+ //# sourceMappingURL=select-BMEIiDXv.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"select-BMEIiDXv.js","names":["RouteErrorFallback","SplitErrorComponent","errorComponent"],"sources":["../../../frontend/src/routes/account/select/index.tsx?tsr-split=errorComponent"],"sourcesContent":["import {\n ArrowRightIcon,\n PlusIcon,\n TrashIcon,\n UserCircleIcon,\n} from '@phosphor-icons/react';\nimport {\n useMutation,\n useQueryClient,\n useSuspenseQuery,\n} from '@tanstack/react-query';\nimport { createFileRoute } from '@tanstack/react-router';\nimport { PageHeader } from '#frontend/components/auth/page-header.tsx';\nimport { Alert } from '#frontend/components/ui/alert.tsx';\nimport { RouteErrorFallback } from '#frontend/components/ui/route-error-fallback.tsx';\nimport { PageLayout } from '#frontend/features/layout/page-layout.tsx';\nimport {\n buildAuthorizeUrl,\n extractOAuthParams,\n OAuthSearchSchema,\n} from '#frontend/libs/oauth-search.ts';\nimport {\n accountsQueryOptions,\n removeAccountMutationOptions,\n selectAccountMutationOptions,\n} from '#frontend/queries/accounts.ts';\nimport { queryKeys } from '#frontend/queries/keys.ts';\n\nexport const Route = createFileRoute('/account/select/')({\n component: AccountSelect,\n errorComponent: RouteErrorFallback,\n validateSearch: OAuthSearchSchema,\n loader: async ({ context, location }) => {\n const search = location.search as { client_id?: string };\n await context.queryClient.ensureQueryData(\n accountsQueryOptions(search.client_id),\n );\n },\n});\n\nfunction appendLoginPrompt(prompt: string | undefined): string {\n const values = prompt?.split(' ').filter(Boolean) ?? [];\n if (!values.includes('login')) {\n values.push('login');\n }\n return values.join(' ');\n}\n\nfunction buildLoginHref(search: ReturnType<typeof Route.useSearch>) {\n const oauthParams = extractOAuthParams(search);\n const params = new URLSearchParams();\n for (const [key, value] of Object.entries({\n ...oauthParams,\n prompt: appendLoginPrompt(oauthParams.prompt),\n account_selected: '1',\n })) {\n if (value !== undefined) {\n params.set(key, String(value));\n }\n }\n return `/login?${params.toString()}`;\n}\n\nfunction AccountSelect() {\n const search = Route.useSearch();\n const queryClient = useQueryClient();\n const { data } = useSuspenseQuery(accountsQueryOptions(search.client_id));\n\n const continueWithSelectedAccount = () => {\n window.location.href = buildAuthorizeUrl({\n ...search,\n account_selected: '1',\n });\n };\n\n const selectMutation = useMutation({\n ...selectAccountMutationOptions,\n onSuccess: continueWithSelectedAccount,\n });\n const removeMutation = useMutation({\n ...removeAccountMutationOptions,\n onSuccess: () => {\n queryClient.invalidateQueries({\n queryKey: queryKeys.accounts(search.client_id),\n });\n },\n });\n\n return (\n <PageLayout cardPadding maxWidth=\"100\">\n <PageHeader\n subtitle=\"Choose an account to continue to this application\"\n title=\"Choose an account\"\n />\n\n {data.accounts.length === 0 ? (\n <Alert className=\"mb-4\" icon={UserCircleIcon} type=\"info\">\n No remembered accounts are available in this browser session.\n </Alert>\n ) : (\n <div className=\"flex flex-col gap-2\" data-testid=\"account-list\">\n {data.accounts.map((account) => (\n <div\n className=\"flex items-center gap-3 rounded-box border border-base-300 bg-base-100 p-3 shadow-sm\"\n data-testid=\"remembered-account\"\n key={account.sub}\n >\n <div className=\"avatar placeholder\">\n <div className=\"w-10 rounded-full bg-primary/10 text-primary\">\n <UserCircleIcon className=\"size-7\" weight=\"duotone\" />\n </div>\n </div>\n <button\n className=\"min-w-0 flex-1 text-left\"\n data-testid={`select-account-${account.sub}`}\n disabled={selectMutation.isPending}\n onClick={() => selectMutation.mutate({ sub: account.sub })}\n type=\"button\"\n >\n <div className=\"truncate font-medium text-sm\">\n {account.email}\n </div>\n <div className=\"text-base-content/60 text-xs\">\n {account.current ? 'Current account' : 'Remembered account'}\n </div>\n </button>\n {account.current ? (\n <span className=\"badge badge-primary badge-sm\">Current</span>\n ) : null}\n {data.allow_remove_account && !account.current ? (\n <button\n aria-label={`Remove ${account.email}`}\n className=\"btn btn-ghost btn-square btn-sm text-base-content/60\"\n data-testid={`remove-account-${account.sub}`}\n disabled={removeMutation.isPending}\n onClick={() => removeMutation.mutate({ sub: account.sub })}\n type=\"button\"\n >\n <TrashIcon className=\"size-4\" />\n </button>\n ) : null}\n </div>\n ))}\n </div>\n )}\n\n {data.allow_add_account ? (\n <a\n className=\"btn btn-outline mt-4 w-full justify-between\"\n href={buildLoginHref(search)}\n >\n <span className=\"inline-flex items-center gap-2\">\n <PlusIcon className=\"size-4\" />\n Use another account\n </span>\n <ArrowRightIcon className=\"size-4\" />\n </a>\n ) : null}\n </PageLayout>\n );\n}\n"],"mappings":"wCAcsF,IAAAC,EAA7ED"}