@tinyrack/tinyauth-server 0.6.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/services/account-selection.service.d.ts.map +1 -1
- package/dist/services/account-selection.service.js +5 -2
- package/dist/services/account-selection.service.js.map +1 -1
- package/dist/services/oauth-authorize.service.d.ts.map +1 -1
- package/dist/services/oauth-authorize.service.js +3 -1
- package/dist/services/oauth-authorize.service.js.map +1 -1
- package/package.json +1 -1
- package/public/assets/2fa-DZSOQLyd.js +2 -0
- package/public/assets/{2fa-Cteolf9l.js.map → 2fa-DZSOQLyd.js.map} +1 -1
- package/public/assets/2fa-DvD9N_wj.js +2 -0
- package/public/assets/{2fa-BMEIiDXv.js.map → 2fa-DvD9N_wj.js.map} +1 -1
- package/public/assets/{2fa-BhCgCfEc.js → 2fa-P8YqnDXC.js} +2 -2
- package/public/assets/{2fa-BhCgCfEc.js.map → 2fa-P8YqnDXC.js.map} +1 -1
- package/public/assets/{2fa-E0x3Samn.js → 2fa-fLX-LED8.js} +2 -2
- package/public/assets/{2fa-E0x3Samn.js.map → 2fa-fLX-LED8.js.map} +1 -1
- package/public/assets/{admin-CXJ0z2QN.js → admin-BLPp8bT8.js} +2 -2
- package/public/assets/{admin-CXJ0z2QN.js.map → admin-BLPp8bT8.js.map} +1 -1
- package/public/assets/{consent-C-h_pWHQ.js → consent-CjXe4bU5.js} +2 -2
- package/public/assets/{consent-C-h_pWHQ.js.map → consent-CjXe4bU5.js.map} +1 -1
- package/public/assets/consent-Cq6tCfwA.js +2 -0
- package/public/assets/{consent-B17hGrta.js.map → consent-Cq6tCfwA.js.map} +1 -1
- package/public/assets/{email-DjE7mqnX.js → email-CpOX6Ig_.js} +2 -2
- package/public/assets/email-CpOX6Ig_.js.map +1 -0
- package/public/assets/email-DvD9N_wj.js +2 -0
- package/public/assets/email-DvD9N_wj.js.map +1 -0
- package/public/assets/{error-BKXsXhdR.js → error-DMzYyopT.js} +2 -2
- package/public/assets/{error-BKXsXhdR.js.map → error-DMzYyopT.js.map} +1 -1
- package/public/assets/forgot-DvD9N_wj.js +2 -0
- package/public/assets/{forgot-BMEIiDXv.js.map → forgot-DvD9N_wj.js.map} +1 -1
- package/public/assets/{forgot-C9zSfaX9.js → forgot-RbSzzjWF.js} +2 -2
- package/public/assets/{forgot-C9zSfaX9.js.map → forgot-RbSzzjWF.js.map} +1 -1
- package/public/assets/{index-BmfaaNx6.js → index-CgkA6nnx.js} +3 -3
- package/public/assets/index-CgkA6nnx.js.map +1 -0
- package/public/assets/{login-DG1Yfmb8.js → login-BQI7QY9W.js} +2 -2
- package/public/assets/login-BQI7QY9W.js.map +1 -0
- package/public/assets/login-DvD9N_wj.js +2 -0
- package/public/assets/login-DvD9N_wj.js.map +1 -0
- package/public/assets/{passkey-BDrX3jVg.js → passkey-C0BKdldQ.js} +2 -2
- package/public/assets/passkey-C0BKdldQ.js.map +1 -0
- package/public/assets/{passkey-C-h8Fvqn.js → passkey-D9qQ8xoR.js} +2 -2
- package/public/assets/passkey-D9qQ8xoR.js.map +1 -0
- package/public/assets/password-CqjbZggE.js +2 -0
- package/public/assets/password-CqjbZggE.js.map +1 -0
- package/public/assets/password-DvD9N_wj.js +2 -0
- package/public/assets/password-DvD9N_wj.js.map +1 -0
- package/public/assets/{profile-8buOY6GT.js → profile-B5qQlPwW.js} +2 -2
- package/public/assets/{profile-8buOY6GT.js.map → profile-B5qQlPwW.js.map} +1 -1
- package/public/assets/profile-fwH7_EJ0.js +2 -0
- package/public/assets/{profile-DcpL_XUt.js.map → profile-fwH7_EJ0.js.map} +1 -1
- package/public/assets/{recovery-Csf6dtvj.js → recovery-DqEhUHzv.js} +2 -2
- package/public/assets/{recovery-Csf6dtvj.js.map → recovery-DqEhUHzv.js.map} +1 -1
- package/public/assets/{register-X6dycne4.js → register-CntgI-rd.js} +2 -2
- package/public/assets/register-CntgI-rd.js.map +1 -0
- package/public/assets/register-DvD9N_wj.js +2 -0
- package/public/assets/register-DvD9N_wj.js.map +1 -0
- package/public/assets/{reset-1nsqPlxL.js → reset-BK1XYJiv.js} +2 -2
- package/public/assets/{reset-1nsqPlxL.js.map → reset-BK1XYJiv.js.map} +1 -1
- package/public/assets/reset-DvD9N_wj.js +2 -0
- package/public/assets/{reset-BMEIiDXv.js.map → reset-DvD9N_wj.js.map} +1 -1
- package/public/assets/{select-CDdb3jyc.js → select-DeUoTgDw.js} +2 -2
- package/public/assets/{select-CDdb3jyc.js.map → select-DeUoTgDw.js.map} +1 -1
- package/public/assets/select-DvD9N_wj.js +2 -0
- package/public/assets/{select-BMEIiDXv.js.map → select-DvD9N_wj.js.map} +1 -1
- package/public/assets/{terms-CUhvMN9k.js → terms-CPEuXMaO.js} +2 -2
- package/public/assets/{terms-CUhvMN9k.js.map → terms-CPEuXMaO.js.map} +1 -1
- package/public/assets/terms-fwH7_EJ0.js +2 -0
- package/public/assets/{terms-DcpL_XUt.js.map → terms-fwH7_EJ0.js.map} +1 -1
- package/public/assets/{totp-DB9WqW4V.js → totp-B1ebEiOW.js} +2 -2
- package/public/assets/totp-B1ebEiOW.js.map +1 -0
- package/public/assets/{totp-UDT2P91H.js → totp-DWWZe-tJ.js} +2 -2
- package/public/assets/totp-DWWZe-tJ.js.map +1 -0
- package/public/assets/{use-totp-setup-9fktn_he.js → use-totp-setup-Z96pOMXt.js} +2 -2
- package/public/assets/{use-totp-setup-9fktn_he.js.map → use-totp-setup-Z96pOMXt.js.map} +1 -1
- package/public/assets/{useMutation-Iu4AJCB4.js → useMutation-Biy2-pH_.js} +2 -2
- package/public/assets/{useMutation-Iu4AJCB4.js.map → useMutation-Biy2-pH_.js.map} +1 -1
- package/public/assets/{users-Bi7gjreq.js → users-YtDGJlDl.js} +2 -2
- package/public/assets/{users-Bi7gjreq.js.map → users-YtDGJlDl.js.map} +1 -1
- package/public/index.html +1 -1
- package/public/assets/2fa-BMEIiDXv.js +0 -2
- package/public/assets/2fa-Cteolf9l.js +0 -2
- package/public/assets/consent-B17hGrta.js +0 -2
- package/public/assets/email-BMEIiDXv.js +0 -2
- package/public/assets/email-BMEIiDXv.js.map +0 -1
- package/public/assets/email-DjE7mqnX.js.map +0 -1
- package/public/assets/forgot-BMEIiDXv.js +0 -2
- package/public/assets/index-BmfaaNx6.js.map +0 -1
- package/public/assets/login-BMEIiDXv.js +0 -2
- package/public/assets/login-BMEIiDXv.js.map +0 -1
- package/public/assets/login-DG1Yfmb8.js.map +0 -1
- package/public/assets/passkey-BDrX3jVg.js.map +0 -1
- package/public/assets/passkey-C-h8Fvqn.js.map +0 -1
- package/public/assets/password-86C-hCze.js +0 -2
- package/public/assets/password-86C-hCze.js.map +0 -1
- package/public/assets/password-BMEIiDXv.js +0 -2
- package/public/assets/password-BMEIiDXv.js.map +0 -1
- package/public/assets/profile-DcpL_XUt.js +0 -2
- package/public/assets/register-BMEIiDXv.js +0 -2
- package/public/assets/register-BMEIiDXv.js.map +0 -1
- package/public/assets/register-X6dycne4.js.map +0 -1
- package/public/assets/reset-BMEIiDXv.js +0 -2
- package/public/assets/select-BMEIiDXv.js +0 -2
- package/public/assets/terms-DcpL_XUt.js +0 -2
- package/public/assets/totp-DB9WqW4V.js.map +0 -1
- package/public/assets/totp-UDT2P91H.js.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"login-BMEIiDXv.js","names":["RouteErrorFallback","SplitErrorComponent","errorComponent"],"sources":["../../../frontend/src/routes/login/index.tsx?tsr-split=errorComponent"],"sourcesContent":["import {\n EnvelopeSimpleIcon,\n FingerprintIcon,\n WarningCircleIcon,\n} from '@phosphor-icons/react';\nimport {\n useMutation,\n useQueryClient,\n useSuspenseQuery,\n} from '@tanstack/react-query';\nimport { createFileRoute, useRouter } from '@tanstack/react-router';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport { LoginMethodButton } from '#frontend/components/auth/login-method-button.tsx';\nimport { LoginMethodList } from '#frontend/components/auth/login-method-list.tsx';\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 isOAuthFlow,\n OAuthSearchSchema,\n} from '#frontend/libs/oauth-search.ts';\nimport { tick } from '#frontend/libs/promise.ts';\nimport { appConfigQueryOptions } from '#frontend/queries/config.ts';\nimport { authenticateWithPasskeyMutationOptions } from '#frontend/queries/passkey.ts';\nimport {\n type AuthResponse,\n getSessionQueryOptions,\n} from '#frontend/queries/session.ts';\n\nconst SearchSchema = OAuthSearchSchema.extend({\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 registration_email_not_allowed: 'oauth.error.registrationEmailNotAllowed',\n};\n\nexport const Route = createFileRoute('/login/')({\n component: Login,\n errorComponent: RouteErrorFallback,\n validateSearch: SearchSchema,\n loader: async ({ context }) => {\n await context.queryClient.ensureQueryData(appConfigQueryOptions);\n },\n});\n\nfunction Login() {\n const { t, i18n } = useTranslation();\n const router = useRouter();\n const queryClient = useQueryClient();\n const search = Route.useSearch();\n\n const lang = search.lang ?? i18n.language;\n const { data: configData } = useSuspenseQuery(appConfigQueryOptions);\n\n // Get implicit notice from config for OAuth signup\n const implicitNotice =\n configData.registration.signup_notice?.[lang] ??\n configData.registration.signup_notice?.[configData.i18n.fallback_language];\n const oauthProviders = configData.identity_providers;\n\n const oauthError = search.oauth_error;\n const oauthErrorMessage = oauthError\n ? t(OAUTH_ERROR_I18N_MAP[oauthError] ?? 'oauth.error.failed')\n : undefined;\n\n const isPasswordAuthEnabled = configData.auth.password.enabled;\n const isPasskeyEnabled = configData.auth.passkey.enabled;\n\n // Config-based title/subtitle (overrides i18n defaults)\n const customTitle =\n configData.branding.title?.[lang] ??\n configData.branding.title?.[configData.i18n.fallback_language];\n const customSubtitle =\n configData.branding.subtitle?.[lang] ??\n configData.branding.subtitle?.[configData.i18n.fallback_language];\n\n const handlePasskeySuccess = async (data: AuthResponse) => {\n if (data.user) {\n queryClient.setQueryData(getSessionQueryOptions.queryKey, data);\n await tick();\n\n if (isOAuthFlow(search)) {\n window.location.href = buildAuthorizeUrl(search);\n } else {\n router.navigate({ to: '/profile' });\n }\n }\n queryClient.invalidateQueries({\n queryKey: getSessionQueryOptions.queryKey,\n });\n };\n\n const passkeyLoginMutation = useMutation({\n ...authenticateWithPasskeyMutationOptions,\n onSuccess: handlePasskeySuccess,\n onSettled: () => {\n queryClient.invalidateQueries({\n queryKey: getSessionQueryOptions.queryKey,\n });\n },\n });\n\n const buildOAuthUrl = (providerId: string) => {\n let oauthUrl = `/api/oauth/${providerId}/authorize?mode=login`;\n\n if (isOAuthFlow(search)) {\n const authUrl = new URL(buildAuthorizeUrl(search));\n const returnUrl = `${authUrl.pathname}${authUrl.search}${authUrl.hash}`;\n oauthUrl += `&return_url=${encodeURIComponent(returnUrl)}`;\n }\n\n return oauthUrl;\n };\n\n const buildPasswordLoginHref = () => {\n const params = new URLSearchParams();\n for (const [key, value] of Object.entries(extractOAuthParams(search))) {\n if (value !== undefined) {\n const stringValue = String(value);\n params.set(\n key,\n key === 'account_selected'\n ? decodeURIComponent(stringValue).replaceAll('\"', '')\n : stringValue,\n );\n }\n }\n const query = params.toString();\n return query ? `/login/password?${query}` : '/login/password';\n };\n\n return (\n <PageLayout cardPadding maxWidth=\"100\">\n <PageHeader\n iconUrl={configData.branding.icon_url}\n subtitle={customSubtitle ?? t('login.selectMethod.subtitle')}\n title={customTitle ?? t('login.title')}\n />\n\n {oauthErrorMessage && (\n <Alert className=\"mb-4\" icon={WarningCircleIcon} type=\"error\">\n {oauthErrorMessage}\n </Alert>\n )}\n\n <LoginMethodList>\n {/* OAuth Providers */}\n {oauthProviders.map((provider) => (\n <LoginMethodButton\n as=\"a\"\n href={buildOAuthUrl(provider.id)}\n icon={provider.icon_url}\n key={provider.id}\n label={provider.display_name}\n providerType={provider.type}\n />\n ))}\n\n {/* Password Login */}\n {isPasswordAuthEnabled && (\n <LoginMethodButton\n as=\"a\"\n href={buildPasswordLoginHref()}\n icon={<EnvelopeSimpleIcon className=\"size-6\" weight=\"regular\" />}\n label={t('login.method.password')}\n />\n )}\n\n {/* Passkey Login */}\n {isPasskeyEnabled && (\n <LoginMethodButton\n as=\"button\"\n disabled={passkeyLoginMutation.isPending}\n icon={<FingerprintIcon className=\"size-6\" weight=\"regular\" />}\n isLoading={passkeyLoginMutation.isPending}\n label={t('login.method.passkey')}\n onClick={() => passkeyLoginMutation.mutate()}\n type=\"button\"\n />\n )}\n </LoginMethodList>\n\n {implicitNotice && (\n <div className=\"mt-6 text-center text-base-content/60 text-xs\">\n <div\n className=\"prose prose-sm text-xs! **:text-xs!\"\n dangerouslySetInnerHTML={{ __html: implicitNotice }}\n />\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"wCAiBsF,IAAAC,EAA7ED"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"login-DG1Yfmb8.js","names":["e","e","t","e","t","p","a","a","i","a","m","EnvelopeSimpleIcon","FingerprintIcon","WarningCircleIcon","useMutation","useQueryClient","useSuspenseQuery","useRouter","useTranslation","LoginMethodButton","LoginMethodList","PageHeader","Alert","PageLayout","buildAuthorizeUrl","extractOAuthParams","isOAuthFlow","tick","appConfigQueryOptions","authenticateWithPasskeyMutationOptions","getSessionQueryOptions","OAUTH_ERROR_I18N_MAP","Record","access_denied","temporarily_unavailable","server_error","registration_email_not_allowed","Route","Login","t","i18n","router","queryClient","search","useSearch","lang","language","data","configData","implicitNotice","registration","signup_notice","fallback_language","oauthProviders","identity_providers","oauthError","oauth_error","oauthErrorMessage","undefined","isPasswordAuthEnabled","auth","password","enabled","isPasskeyEnabled","passkey","customTitle","branding","title","customSubtitle","subtitle","handlePasskeySuccess","AuthResponse","user","setQueryData","queryKey","window","location","href","navigate","to","invalidateQueries","passkeyLoginMutation","onSuccess","onSettled","buildOAuthUrl","providerId","oauthUrl","authUrl","URL","returnUrl","pathname","hash","encodeURIComponent","buildPasswordLoginHref","params","URLSearchParams","key","value","Object","entries","stringValue","String","set","decodeURIComponent","replaceAll","query","toString","icon_url","map","provider","id","display_name","type","isPending","mutate","__html","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/AppleLogo.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/defs/GithubLogo.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/defs/GoogleLogo.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/AppleLogo.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/GithubLogo.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/GoogleLogo.es.js","../../../frontend/src/components/auth/login-method-button.tsx","../../../frontend/src/components/auth/login-method-list.tsx","../../../frontend/src/routes/login/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: \"M227,168a12,12,0,0,0-4.21-5.09C207.25,152.22,204,133.68,204,120c0-16.17,12.68-30.6,20.25-37.76a12,12,0,0,0,0-17.43C210.89,52.17,188.81,44,168,44a76.29,76.29,0,0,0-40,11.37,75.59,75.59,0,0,0-93.58,11A78.64,78.64,0,0,0,12,123.51,131,131,0,0,0,53.43,216,43.81,43.81,0,0,0,83.6,228h87.69a43.87,43.87,0,0,0,32.05-13.85,127.63,127.63,0,0,0,18.4-25.39c1.57-2.88,3-5.71,4.14-8.41C227.47,176.67,229.12,172.87,227,168Zm-41.23,29.82A19.78,19.78,0,0,1,171.29,204H83.6a19.85,19.85,0,0,1-13.7-5.42A107.18,107.18,0,0,1,36,122.88,54.49,54.49,0,0,1,51.5,83.28,50.86,50.86,0,0,1,88,68h.72A51.5,51.5,0,0,1,120.48,79.4a12,12,0,0,0,15,0A51.41,51.41,0,0,1,168,68a67.24,67.24,0,0,1,29.88,7.4C186.26,89.66,180,105.13,180,120c0,23.33,7.47,42.89,21.25,56.19A103.3,103.3,0,0,1,185.76,197.81ZM128.75,13A43.83,43.83,0,0,1,142.17,1.51a12,12,0,0,1,11.64,21,19.84,19.84,0,0,0-6.11,5.24A12,12,0,0,1,128.75,13Z\" }))\n ],\n [\n \"duotone\",\n /* @__PURE__ */ a.createElement(a.Fragment, null, /* @__PURE__ */ a.createElement(\n \"path\",\n {\n d: \"M216,73.52Zm0,99.26c-16.79-11.53-24-30.87-24-52.78,0-18.3,11.68-34.81,24-46.48C204.53,62.66,185,56,168,56a63.72,63.72,0,0,0-40,14h0A63.71,63.71,0,0,0,88.88,56C52,55.5,23.06,86.3,24,123.19a119.62,119.62,0,0,0,37.65,84.12A32,32,0,0,0,83.6,216h87.7a31.75,31.75,0,0,0,23.26-10c15.85-17,21.44-33.2,21.44-33.2Z\",\n opacity: \"0.2\"\n }\n ), /* @__PURE__ */ a.createElement(\"path\", { d: \"M223.3,169.59a8.07,8.07,0,0,0-2.8-3.4C203.53,154.53,200,134.64,200,120c0-17.67,13.47-33.06,21.5-40.67a8,8,0,0,0,0-11.62C208.82,55.74,187.82,48,168,48a72.23,72.23,0,0,0-40,12.13,71.56,71.56,0,0,0-90.71,9.09A74.63,74.63,0,0,0,16,123.4a127,127,0,0,0,40.14,89.73A39.8,39.8,0,0,0,83.59,224h87.68a39.84,39.84,0,0,0,29.12-12.57,125,125,0,0,0,17.82-24.6C225.23,174,224.33,172,223.3,169.59Zm-34.63,30.94a23.76,23.76,0,0,1-17.4,7.47H83.59a23.82,23.82,0,0,1-16.44-6.51A111.14,111.14,0,0,1,32,123,58.5,58.5,0,0,1,48.65,80.47,54.81,54.81,0,0,1,88,64h.78A55.45,55.45,0,0,1,123,76.28a8,8,0,0,0,10,0A55.39,55.39,0,0,1,168,64a70.64,70.64,0,0,1,36,10.35c-13,14.52-20,30.47-20,45.65,0,23.77,7.64,42.73,22.18,55.3A105.52,105.52,0,0,1,188.67,200.53ZM128.23,30A40,40,0,0,1,167,0h1a8,8,0,0,1,0,16h-1a24,24,0,0,0-23.24,18,8,8,0,1,1-15.5-4Z\" }))\n ],\n [\n \"fill\",\n /* @__PURE__ */ a.createElement(a.Fragment, null, /* @__PURE__ */ a.createElement(\"path\", { d: \"M128.23,30A40,40,0,0,1,167,0h1a8,8,0,0,1,0,16h-1a24,24,0,0,0-23.24,18,8,8,0,1,1-15.5-4ZM223.3,169.59a8.07,8.07,0,0,0-2.8-3.4C203.53,154.53,200,134.64,200,120c0-17.67,13.47-33.06,21.5-40.67a8,8,0,0,0,0-11.62C208.82,55.74,187.82,48,168,48a72.23,72.23,0,0,0-40,12.13,71.56,71.56,0,0,0-90.71,9.09A74.63,74.63,0,0,0,16,123.4a127,127,0,0,0,40.14,89.73A39.8,39.8,0,0,0,83.59,224h87.68a39.84,39.84,0,0,0,29.12-12.57,125,125,0,0,0,17.82-24.6C225.23,174,224.33,172,223.3,169.59Z\" }))\n ],\n [\n \"light\",\n /* @__PURE__ */ a.createElement(a.Fragment, null, /* @__PURE__ */ a.createElement(\"path\", { d: \"M219.4,167.84C201.71,155.69,198,135.12,198,120c0-18.42,13.86-34.29,22.12-42.12a6,6,0,0,0,0-8.71C208,57.7,187.07,50,168,50a70.23,70.23,0,0,0-40,12.55,69.6,69.6,0,0,0-89.31,8.08A72.63,72.63,0,0,0,18,123.35a125.11,125.11,0,0,0,39.53,88.33A37.85,37.85,0,0,0,83.6,222h87.7A37.83,37.83,0,0,0,199,210.07a122.6,122.6,0,0,0,17.54-24.2c6.55-12,5.77-13.75,5-15.48A6.07,6.07,0,0,0,219.4,167.84Zm-29.23,34A25.82,25.82,0,0,1,171.3,210H83.6A25.85,25.85,0,0,1,65.78,203,113.21,113.21,0,0,1,30,123a60.55,60.55,0,0,1,17.21-44A56.82,56.82,0,0,1,88,62h.81a57.35,57.35,0,0,1,35.44,12.71,6,6,0,0,0,7.5,0A57.39,57.39,0,0,1,168,62c13.89,0,28.81,4.68,39.11,12-9.44,10.14-21.1,26.59-21.1,46,0,23.78,7.81,42.6,22.66,54.77A107.33,107.33,0,0,1,190.17,201.89Zm-60-171.39A38,38,0,0,1,167,2h1a6,6,0,0,1,0,12h-1a26,26,0,0,0-25.18,19.5,6,6,0,1,1-11.62-3Z\" }))\n ],\n [\n \"regular\",\n /* @__PURE__ */ a.createElement(a.Fragment, null, /* @__PURE__ */ a.createElement(\"path\", { d: \"M223.3,169.59a8.07,8.07,0,0,0-2.8-3.4C203.53,154.53,200,134.64,200,120c0-17.67,13.47-33.06,21.5-40.67a8,8,0,0,0,0-11.62C208.82,55.74,187.82,48,168,48a72.2,72.2,0,0,0-40,12.13,71.56,71.56,0,0,0-90.71,9.09A74.63,74.63,0,0,0,16,123.4a127.06,127.06,0,0,0,40.14,89.73A39.8,39.8,0,0,0,83.59,224h87.68a39.84,39.84,0,0,0,29.12-12.57,125,125,0,0,0,17.82-24.6C225.23,174,224.33,172,223.3,169.59Zm-34.63,30.94a23.76,23.76,0,0,1-17.4,7.47H83.59a23.82,23.82,0,0,1-16.44-6.51A111.14,111.14,0,0,1,32,123,58.5,58.5,0,0,1,48.65,80.47,54.81,54.81,0,0,1,88,64h.78A55.45,55.45,0,0,1,123,76.28a8,8,0,0,0,10,0A55.44,55.44,0,0,1,168,64a70.64,70.64,0,0,1,36,10.35c-13,14.52-20,30.47-20,45.65,0,23.77,7.64,42.73,22.18,55.3A105.82,105.82,0,0,1,188.67,200.53ZM128.23,30A40,40,0,0,1,167,0h1a8,8,0,0,1,0,16h-1a24,24,0,0,0-23.24,18,8,8,0,1,1-15.5-4Z\" }))\n ],\n [\n \"thin\",\n /* @__PURE__ */ a.createElement(a.Fragment, null, /* @__PURE__ */ a.createElement(\"path\", { d: \"M218.27,169.49C199.86,156.84,196,135.6,196,120c0-19.17,14.25-35.53,22.75-43.57a4,4,0,0,0,0-5.81C207,59.48,186.59,52,168,52a68.3,68.3,0,0,0-40,13,67.61,67.61,0,0,0-87.88,7A70.65,70.65,0,0,0,20,123.3a123.11,123.11,0,0,0,38.9,86.92A35.81,35.81,0,0,0,83.6,220h87.7a35.84,35.84,0,0,0,26.19-11.3,119.93,119.93,0,0,0,17.24-23.79c6.08-11.1,5.42-12.62,4.94-13.72A4,4,0,0,0,218.27,169.49Zm-26.64,33.77A27.83,27.83,0,0,1,171.3,212H83.6a27.84,27.84,0,0,1-19.19-7.6A115.15,115.15,0,0,1,28,123.09,62.55,62.55,0,0,1,45.81,77.66,58.78,58.78,0,0,1,88,60h.84a59.37,59.37,0,0,1,36.66,13.15,4,4,0,0,0,5,0A59.35,59.35,0,0,1,168,60c15.12,0,31.45,5.41,42.11,13.73C200.68,83.42,188,100.16,188,120c0,23.79,8,42.44,23.12,54.17A107.64,107.64,0,0,1,191.63,203.26ZM132.13,31A36,36,0,0,1,167,4h1a4,4,0,0,1,0,8h-1a28,28,0,0,0-27.12,21A4,4,0,0,1,136,36a3.87,3.87,0,0,1-1-.13A4,4,0,0,1,132.13,31Z\" }))\n ]\n]);\nexport {\n e as default\n};\n","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: \"M212.62,75.17A63.7,63.7,0,0,0,206.39,26,12,12,0,0,0,196,20a63.71,63.71,0,0,0-50,24H126A63.71,63.71,0,0,0,76,20a12,12,0,0,0-10.39,6,63.7,63.7,0,0,0-6.23,49.17A61.5,61.5,0,0,0,52,104v8a60.1,60.1,0,0,0,45.76,58.28A43.66,43.66,0,0,0,92,192v4H76a20,20,0,0,1-20-20,44.05,44.05,0,0,0-44-44,12,12,0,0,0,0,24,20,20,0,0,1,20,20,44.05,44.05,0,0,0,44,44H92v12a12,12,0,0,0,24,0V192a20,20,0,0,1,40,0v40a12,12,0,0,0,24,0V192a43.66,43.66,0,0,0-5.76-21.72A60.1,60.1,0,0,0,220,112v-8A61.5,61.5,0,0,0,212.62,75.17ZM196,112a36,36,0,0,1-36,36H112a36,36,0,0,1-36-36v-8a37.87,37.87,0,0,1,6.13-20.12,11.65,11.65,0,0,0,1.58-11.49,39.9,39.9,0,0,1-.4-27.72,39.87,39.87,0,0,1,26.41,17.8A12,12,0,0,0,119.82,68h32.35a12,12,0,0,0,10.11-5.53,39.84,39.84,0,0,1,26.41-17.8,39.9,39.9,0,0,1-.4,27.72,12,12,0,0,0,1.61,11.53A37.85,37.85,0,0,1,196,104Z\" }))\n ],\n [\n \"duotone\",\n /* @__PURE__ */ a.createElement(a.Fragment, null, /* @__PURE__ */ a.createElement(\n \"path\",\n {\n d: \"M208,104v8a48,48,0,0,1-48,48H136a32,32,0,0,1,32,32v40H104V192a32,32,0,0,1,32-32H112a48,48,0,0,1-48-48v-8a49.28,49.28,0,0,1,8.51-27.3A51.92,51.92,0,0,1,76,32a52,52,0,0,1,43.83,24h32.34A52,52,0,0,1,196,32a51.92,51.92,0,0,1,3.49,44.7A49.28,49.28,0,0,1,208,104Z\",\n opacity: \"0.2\"\n }\n ), /* @__PURE__ */ a.createElement(\"path\", { d: \"M208.3,75.68A59.74,59.74,0,0,0,202.93,28,8,8,0,0,0,196,24a59.75,59.75,0,0,0-48,24H124A59.75,59.75,0,0,0,76,24a8,8,0,0,0-6.93,4,59.78,59.78,0,0,0-5.38,47.68A58.14,58.14,0,0,0,56,104v8a56.06,56.06,0,0,0,48.44,55.47A39.8,39.8,0,0,0,96,192v8H72a24,24,0,0,1-24-24A40,40,0,0,0,8,136a8,8,0,0,0,0,16,24,24,0,0,1,24,24,40,40,0,0,0,40,40H96v16a8,8,0,0,0,16,0V192a24,24,0,0,1,48,0v40a8,8,0,0,0,16,0V192a39.8,39.8,0,0,0-8.44-24.53A56.06,56.06,0,0,0,216,112v-8A58,58,0,0,0,208.3,75.68ZM200,112a40,40,0,0,1-40,40H112a40,40,0,0,1-40-40v-8a41.74,41.74,0,0,1,6.9-22.48A8,8,0,0,0,80,73.83a43.81,43.81,0,0,1,.79-33.58,43.88,43.88,0,0,1,32.32,20.06A8,8,0,0,0,119.82,64h32.35a8,8,0,0,0,6.74-3.69,43.87,43.87,0,0,1,32.32-20.06A43.81,43.81,0,0,1,192,73.83a8.09,8.09,0,0,0,1,7.65A41.76,41.76,0,0,1,200,104Z\" }))\n ],\n [\n \"fill\",\n /* @__PURE__ */ a.createElement(a.Fragment, null, /* @__PURE__ */ a.createElement(\"path\", { d: \"M216,104v8a56.06,56.06,0,0,1-48.44,55.47A39.8,39.8,0,0,1,176,192v40a8,8,0,0,1-8,8H104a8,8,0,0,1-8-8V216H72a40,40,0,0,1-40-40A24,24,0,0,0,8,152a8,8,0,0,1,0-16,40,40,0,0,1,40,40,24,24,0,0,0,24,24H96v-8a39.8,39.8,0,0,1,8.44-24.53A56.06,56.06,0,0,1,56,112v-8a58.14,58.14,0,0,1,7.69-28.32A59.78,59.78,0,0,1,69.07,28,8,8,0,0,1,76,24a59.75,59.75,0,0,1,48,24h24a59.75,59.75,0,0,1,48-24,8,8,0,0,1,6.93,4,59.74,59.74,0,0,1,5.37,47.68A58,58,0,0,1,216,104Z\" }))\n ],\n [\n \"light\",\n /* @__PURE__ */ a.createElement(a.Fragment, null, /* @__PURE__ */ a.createElement(\"path\", { d: \"M206.13,75.92A57.79,57.79,0,0,0,201.2,29a6,6,0,0,0-5.2-3,57.77,57.77,0,0,0-47,24H123A57.77,57.77,0,0,0,76,26a6,6,0,0,0-5.2,3,57.79,57.79,0,0,0-4.93,46.92A55.88,55.88,0,0,0,58,104v8a54.06,54.06,0,0,0,50.45,53.87A37.85,37.85,0,0,0,98,192v10H72a26,26,0,0,1-26-26A38,38,0,0,0,8,138a6,6,0,0,0,0,12,26,26,0,0,1,26,26,38,38,0,0,0,38,38H98v18a6,6,0,0,0,12,0V192a26,26,0,0,1,52,0v40a6,6,0,0,0,12,0V192a37.85,37.85,0,0,0-10.45-26.13A54.06,54.06,0,0,0,214,112v-8A55.88,55.88,0,0,0,206.13,75.92ZM202,112a42,42,0,0,1-42,42H112a42,42,0,0,1-42-42v-8a43.86,43.86,0,0,1,7.3-23.69,6,6,0,0,0,.81-5.76,45.85,45.85,0,0,1,1.43-36.42,45.85,45.85,0,0,1,35.23,21.1A6,6,0,0,0,119.83,62h32.34a6,6,0,0,0,5.06-2.76,45.83,45.83,0,0,1,35.23-21.11,45.85,45.85,0,0,1,1.43,36.42,6,6,0,0,0,.79,5.74A43.78,43.78,0,0,1,202,104Z\" }))\n ],\n [\n \"regular\",\n /* @__PURE__ */ a.createElement(a.Fragment, null, /* @__PURE__ */ a.createElement(\"path\", { d: \"M208.31,75.68A59.78,59.78,0,0,0,202.93,28,8,8,0,0,0,196,24a59.75,59.75,0,0,0-48,24H124A59.75,59.75,0,0,0,76,24a8,8,0,0,0-6.93,4,59.78,59.78,0,0,0-5.38,47.68A58.14,58.14,0,0,0,56,104v8a56.06,56.06,0,0,0,48.44,55.47A39.8,39.8,0,0,0,96,192v8H72a24,24,0,0,1-24-24A40,40,0,0,0,8,136a8,8,0,0,0,0,16,24,24,0,0,1,24,24,40,40,0,0,0,40,40H96v16a8,8,0,0,0,16,0V192a24,24,0,0,1,48,0v40a8,8,0,0,0,16,0V192a39.8,39.8,0,0,0-8.44-24.53A56.06,56.06,0,0,0,216,112v-8A58.14,58.14,0,0,0,208.31,75.68ZM200,112a40,40,0,0,1-40,40H112a40,40,0,0,1-40-40v-8a41.74,41.74,0,0,1,6.9-22.48A8,8,0,0,0,80,73.83a43.81,43.81,0,0,1,.79-33.58,43.88,43.88,0,0,1,32.32,20.06A8,8,0,0,0,119.82,64h32.35a8,8,0,0,0,6.74-3.69,43.87,43.87,0,0,1,32.32-20.06A43.81,43.81,0,0,1,192,73.83a8.09,8.09,0,0,0,1,7.65A41.72,41.72,0,0,1,200,104Z\" }))\n ],\n [\n \"thin\",\n /* @__PURE__ */ a.createElement(a.Fragment, null, /* @__PURE__ */ a.createElement(\"path\", { d: \"M203.94,76.16A55.73,55.73,0,0,0,199.46,30,4,4,0,0,0,196,28a55.78,55.78,0,0,0-46,24H122A55.78,55.78,0,0,0,76,28a4,4,0,0,0-3.46,2,55.73,55.73,0,0,0-4.48,46.16A53.78,53.78,0,0,0,60,104v8a52.06,52.06,0,0,0,52,52h1.41A36,36,0,0,0,100,192v12H72a28,28,0,0,1-28-28A36,36,0,0,0,8,140a4,4,0,0,0,0,8,28,28,0,0,1,28,28,36,36,0,0,0,36,36h28v20a4,4,0,0,0,8,0V192a28,28,0,0,1,56,0v40a4,4,0,0,0,8,0V192a36,36,0,0,0-13.41-28H160a52.06,52.06,0,0,0,52-52v-8A53.78,53.78,0,0,0,203.94,76.16ZM204,112a44.05,44.05,0,0,1-44,44H112a44.05,44.05,0,0,1-44-44v-8a45.76,45.76,0,0,1,7.71-24.89,4,4,0,0,0,.53-3.84,47.82,47.82,0,0,1,2.1-39.21,47.8,47.8,0,0,1,38.12,22.1A4,4,0,0,0,119.83,60h32.34a4,4,0,0,0,3.37-1.84,47.8,47.8,0,0,1,38.12-22.1,47.82,47.82,0,0,1,2.1,39.21,4,4,0,0,0,.53,3.83A45.85,45.85,0,0,1,204,104Z\" }))\n ]\n]);\nexport {\n e as default\n};\n","import * as e from \"react\";\nconst t = /* @__PURE__ */ new Map([\n [\n \"bold\",\n /* @__PURE__ */ e.createElement(e.Fragment, null, /* @__PURE__ */ e.createElement(\"path\", { d: \"M228,128a100,100,0,1,1-22.86-63.64,12,12,0,0,1-18.51,15.28A76,76,0,1,0,203.05,140H128a12,12,0,0,1,0-24h88A12,12,0,0,1,228,128Z\" }))\n ],\n [\n \"duotone\",\n /* @__PURE__ */ e.createElement(e.Fragment, null, /* @__PURE__ */ e.createElement(\"path\", { d: \"M216,128a88,88,0,1,1-88-88A88,88,0,0,1,216,128Z\", opacity: \"0.2\" }), /* @__PURE__ */ e.createElement(\"path\", { d: \"M224,128a96,96,0,1,1-21.95-61.09,8,8,0,1,1-12.33,10.18A80,80,0,1,0,207.6,136H128a8,8,0,0,1,0-16h88A8,8,0,0,1,224,128Z\" }))\n ],\n [\n \"fill\",\n /* @__PURE__ */ e.createElement(e.Fragment, null, /* @__PURE__ */ e.createElement(\"path\", { d: \"M128,24A104,104,0,1,0,232,128,104,104,0,0,0,128,24Zm0,184A80,80,0,1,1,181.34,68.37a8,8,0,0,1-10.67,11.92A64,64,0,1,0,191.5,136H128a8,8,0,0,1,0-16h72a8,8,0,0,1,8,8A80.09,80.09,0,0,1,128,208Z\" }))\n ],\n [\n \"light\",\n /* @__PURE__ */ e.createElement(e.Fragment, null, /* @__PURE__ */ e.createElement(\"path\", { d: \"M222,128a94,94,0,1,1-21.49-59.82,6,6,0,1,1-9.25,7.64A82,82,0,1,0,209.78,134H128a6,6,0,0,1,0-12h88A6,6,0,0,1,222,128Z\" }))\n ],\n [\n \"regular\",\n /* @__PURE__ */ e.createElement(e.Fragment, null, /* @__PURE__ */ e.createElement(\"path\", { d: \"M224,128a96,96,0,1,1-21.95-61.09,8,8,0,1,1-12.33,10.18A80,80,0,1,0,207.6,136H128a8,8,0,0,1,0-16h88A8,8,0,0,1,224,128Z\" }))\n ],\n [\n \"thin\",\n /* @__PURE__ */ e.createElement(e.Fragment, null, /* @__PURE__ */ e.createElement(\"path\", { d: \"M220,128a92,92,0,1,1-21-58.55,4,4,0,0,1-6.17,5.1A84,84,0,1,0,211.91,132H128a4,4,0,0,1,0-8h88A4,4,0,0,1,220,128Z\" }))\n ]\n]);\nexport {\n t as default\n};\n","import * as o from \"react\";\nimport t from \"../lib/IconBase.es.js\";\nimport a from \"../defs/AppleLogo.es.js\";\nconst e = o.forwardRef((p, r) => /* @__PURE__ */ o.createElement(t, { ref: r, ...p, weights: a }));\ne.displayName = \"AppleLogoIcon\";\nconst s = e;\nexport {\n s as AppleLogo,\n e as AppleLogoIcon\n};\n","import * as o from \"react\";\nimport a from \"../lib/IconBase.es.js\";\nimport i from \"../defs/GithubLogo.es.js\";\nconst t = o.forwardRef((e, r) => /* @__PURE__ */ o.createElement(a, { ref: r, ...e, weights: i }));\nt.displayName = \"GithubLogoIcon\";\nconst s = t;\nexport {\n s as GithubLogo,\n t as GithubLogoIcon\n};\n","import * as o from \"react\";\nimport a from \"../lib/IconBase.es.js\";\nimport m from \"../defs/GoogleLogo.es.js\";\nconst e = o.forwardRef((r, t) => /* @__PURE__ */ o.createElement(a, { ref: t, ...r, weights: m }));\ne.displayName = \"GoogleLogoIcon\";\nconst s = e;\nexport {\n s as GoogleLogo,\n e as GoogleLogoIcon\n};\n","import {\n AppleLogoIcon,\n GithubLogoIcon,\n GoogleLogoIcon,\n LinkIcon,\n} from '@phosphor-icons/react';\nimport { createElement, type ElementType, type ReactNode } from 'react';\nimport type { OAuthProviderType } from '#frontend/queries/config.ts';\n\ntype LoginMethodButtonProps<C extends ElementType> = {\n as: C;\n icon?: string | ReactNode;\n label: string;\n providerType?: OAuthProviderType;\n isLoading?: boolean;\n} & Record<string, unknown>;\n\nconst PROVIDER_ICONS: Record<\n Exclude<OAuthProviderType, 'generic_oauth'>,\n ReactNode\n> = {\n google: <GoogleLogoIcon className=\"size-6\" weight=\"regular\" />,\n github: <GithubLogoIcon className=\"size-6\" weight=\"regular\" />,\n apple: <AppleLogoIcon className=\"size-6\" weight=\"regular\" />,\n};\n\nexport function LoginMethodButton<C extends ElementType>({\n as: Component,\n icon,\n label,\n providerType,\n isLoading,\n ...rest\n}: LoginMethodButtonProps<C>) {\n let iconElement: ReactNode;\n if (providerType && providerType !== 'generic_oauth') {\n iconElement = PROVIDER_ICONS[providerType];\n } else if (typeof icon === 'string') {\n iconElement = <img alt=\"\" className=\"size-6\" src={icon} />;\n } else if (icon) {\n iconElement = icon;\n } else {\n iconElement = <LinkIcon className=\"size-6\" weight=\"regular\" />;\n }\n\n const content = isLoading ? (\n <span className=\"loading loading-spinner loading-md\" />\n ) : (\n <>\n {iconElement}\n <span className=\"text-xs\">{label}</span>\n </>\n );\n\n return createElement(\n Component,\n {\n className:\n 'btn btn-ghost flex h-auto flex-row gap-2 border-base-300 py-3 sm:flex-col',\n ...rest,\n },\n content,\n );\n}\n","import type { ReactNode } from 'react';\n\ntype LoginMethodListProps = {\n children: ReactNode;\n};\n\nexport function LoginMethodList({ children }: LoginMethodListProps) {\n return (\n <div className=\"grid grid-cols-1 gap-2 sm:grid-cols-3\">{children}</div>\n );\n}\n","import {\n EnvelopeSimpleIcon,\n FingerprintIcon,\n WarningCircleIcon,\n} from '@phosphor-icons/react';\nimport {\n useMutation,\n useQueryClient,\n useSuspenseQuery,\n} from '@tanstack/react-query';\nimport { createFileRoute, useRouter } from '@tanstack/react-router';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport { LoginMethodButton } from '#frontend/components/auth/login-method-button.tsx';\nimport { LoginMethodList } from '#frontend/components/auth/login-method-list.tsx';\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 isOAuthFlow,\n OAuthSearchSchema,\n} from '#frontend/libs/oauth-search.ts';\nimport { tick } from '#frontend/libs/promise.ts';\nimport { appConfigQueryOptions } from '#frontend/queries/config.ts';\nimport { authenticateWithPasskeyMutationOptions } from '#frontend/queries/passkey.ts';\nimport {\n type AuthResponse,\n getSessionQueryOptions,\n} from '#frontend/queries/session.ts';\n\nconst SearchSchema = OAuthSearchSchema.extend({\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 registration_email_not_allowed: 'oauth.error.registrationEmailNotAllowed',\n};\n\nexport const Route = createFileRoute('/login/')({\n component: Login,\n errorComponent: RouteErrorFallback,\n validateSearch: SearchSchema,\n loader: async ({ context }) => {\n await context.queryClient.ensureQueryData(appConfigQueryOptions);\n },\n});\n\nfunction Login() {\n const { t, i18n } = useTranslation();\n const router = useRouter();\n const queryClient = useQueryClient();\n const search = Route.useSearch();\n\n const lang = search.lang ?? i18n.language;\n const { data: configData } = useSuspenseQuery(appConfigQueryOptions);\n\n // Get implicit notice from config for OAuth signup\n const implicitNotice =\n configData.registration.signup_notice?.[lang] ??\n configData.registration.signup_notice?.[configData.i18n.fallback_language];\n const oauthProviders = configData.identity_providers;\n\n const oauthError = search.oauth_error;\n const oauthErrorMessage = oauthError\n ? t(OAUTH_ERROR_I18N_MAP[oauthError] ?? 'oauth.error.failed')\n : undefined;\n\n const isPasswordAuthEnabled = configData.auth.password.enabled;\n const isPasskeyEnabled = configData.auth.passkey.enabled;\n\n // Config-based title/subtitle (overrides i18n defaults)\n const customTitle =\n configData.branding.title?.[lang] ??\n configData.branding.title?.[configData.i18n.fallback_language];\n const customSubtitle =\n configData.branding.subtitle?.[lang] ??\n configData.branding.subtitle?.[configData.i18n.fallback_language];\n\n const handlePasskeySuccess = async (data: AuthResponse) => {\n if (data.user) {\n queryClient.setQueryData(getSessionQueryOptions.queryKey, data);\n await tick();\n\n if (isOAuthFlow(search)) {\n window.location.href = buildAuthorizeUrl(search);\n } else {\n router.navigate({ to: '/profile' });\n }\n }\n queryClient.invalidateQueries({\n queryKey: getSessionQueryOptions.queryKey,\n });\n };\n\n const passkeyLoginMutation = useMutation({\n ...authenticateWithPasskeyMutationOptions,\n onSuccess: handlePasskeySuccess,\n onSettled: () => {\n queryClient.invalidateQueries({\n queryKey: getSessionQueryOptions.queryKey,\n });\n },\n });\n\n const buildOAuthUrl = (providerId: string) => {\n let oauthUrl = `/api/oauth/${providerId}/authorize?mode=login`;\n\n if (isOAuthFlow(search)) {\n const authUrl = new URL(buildAuthorizeUrl(search));\n const returnUrl = `${authUrl.pathname}${authUrl.search}${authUrl.hash}`;\n oauthUrl += `&return_url=${encodeURIComponent(returnUrl)}`;\n }\n\n return oauthUrl;\n };\n\n const buildPasswordLoginHref = () => {\n const params = new URLSearchParams();\n for (const [key, value] of Object.entries(extractOAuthParams(search))) {\n if (value !== undefined) {\n const stringValue = String(value);\n params.set(\n key,\n key === 'account_selected'\n ? decodeURIComponent(stringValue).replaceAll('\"', '')\n : stringValue,\n );\n }\n }\n const query = params.toString();\n return query ? `/login/password?${query}` : '/login/password';\n };\n\n return (\n <PageLayout cardPadding maxWidth=\"100\">\n <PageHeader\n iconUrl={configData.branding.icon_url}\n subtitle={customSubtitle ?? t('login.selectMethod.subtitle')}\n title={customTitle ?? t('login.title')}\n />\n\n {oauthErrorMessage && (\n <Alert className=\"mb-4\" icon={WarningCircleIcon} type=\"error\">\n {oauthErrorMessage}\n </Alert>\n )}\n\n <LoginMethodList>\n {/* OAuth Providers */}\n {oauthProviders.map((provider) => (\n <LoginMethodButton\n as=\"a\"\n href={buildOAuthUrl(provider.id)}\n icon={provider.icon_url}\n key={provider.id}\n label={provider.display_name}\n providerType={provider.type}\n />\n ))}\n\n {/* Password Login */}\n {isPasswordAuthEnabled && (\n <LoginMethodButton\n as=\"a\"\n href={buildPasswordLoginHref()}\n icon={<EnvelopeSimpleIcon className=\"size-6\" weight=\"regular\" />}\n label={t('login.method.password')}\n />\n )}\n\n {/* Passkey Login */}\n {isPasskeyEnabled && (\n <LoginMethodButton\n as=\"button\"\n disabled={passkeyLoginMutation.isPending}\n icon={<FingerprintIcon className=\"size-6\" weight=\"regular\" />}\n isLoading={passkeyLoginMutation.isPending}\n label={t('login.method.passkey')}\n onClick={() => passkeyLoginMutation.mutate()}\n type=\"button\"\n />\n )}\n </LoginMethodList>\n\n {implicitNotice && (\n <div className=\"mt-6 text-center text-base-content/60 text-xs\">\n <div\n className=\"prose prose-sm text-xs! **:text-xs!\"\n dangerouslySetInnerHTML={{ __html: implicitNotice }}\n />\n </div>\n )}\n </PageLayout>\n );\n}\n"],"x_google_ignoreList":[0,1,2,3,4,5],"mappings":"2nBACMA,EAAoB,IAAI,IAAI,CAChC,CACE,OACgB,EAAE,cAAA,EAAgB,SAAU,KAAsB,EAAE,cAAc,OAAQ,CAAE,EAAG,82BAA+2B,CAAC,CAAC,CACl9B,EACA,CACE,UACgB,EAAE,cAAA,EAAgB,SAAU,KAAsB,EAAE,cAClE,OACA,CACE,EAAG,mTACH,QAAS,KACX,CACF,EAAmB,EAAE,cAAc,OAAQ,CAAE,EAAG,izBAAkzB,CAAC,CAAC,CACt2B,EACA,CACE,OACgB,EAAE,cAAA,EAAgB,SAAU,KAAsB,EAAE,cAAc,OAAQ,CAAE,EAAG,sdAAud,CAAC,CAAC,CAC1jB,EACA,CACE,QACgB,EAAE,cAAA,EAAgB,SAAU,KAAsB,EAAE,cAAc,OAAQ,CAAE,EAAG,szBAAuzB,CAAC,CAAC,CAC15B,EACA,CACE,UACgB,EAAE,cAAA,EAAgB,SAAU,KAAsB,EAAE,cAAc,OAAQ,CAAE,EAAG,qzBAAszB,CAAC,CAAC,CACz5B,EACA,CACE,OACgB,EAAE,cAAA,EAAgB,SAAU,KAAsB,EAAE,cAAc,OAAQ,CAAE,EAAG,i2BAAk2B,CAAC,CAAC,CACr8B,CACF,CAAC,EC/BKC,EAAoB,IAAI,IAAI,CAChC,CACE,OACgB,EAAE,cAAA,EAAgB,SAAU,KAAsB,EAAE,cAAc,OAAQ,CAAE,EAAG,+yBAAgzB,CAAC,CAAC,CACn5B,EACA,CACE,UACgB,EAAE,cAAA,EAAgB,SAAU,KAAsB,EAAE,cAClE,OACA,CACE,EAAG,oQACH,QAAS,KACX,CACF,EAAmB,EAAE,cAAc,OAAQ,CAAE,EAAG,gxBAAixB,CAAC,CAAC,CACr0B,EACA,CACE,OACgB,EAAE,cAAA,EAAgB,SAAU,KAAsB,EAAE,cAAc,OAAQ,CAAE,EAAG,8bAA+b,CAAC,CAAC,CACliB,EACA,CACE,QACgB,EAAE,cAAA,EAAgB,SAAU,KAAsB,EAAE,cAAc,OAAQ,CAAE,EAAG,wxBAAyxB,CAAC,CAAC,CAC53B,EACA,CACE,UACgB,EAAE,cAAA,EAAgB,SAAU,KAAsB,EAAE,cAAc,OAAQ,CAAE,EAAG,wxBAAyxB,CAAC,CAAC,CAC53B,EACA,CACE,OACgB,EAAE,cAAA,EAAgB,SAAU,KAAsB,EAAE,cAAc,OAAQ,CAAE,EAAG,ixBAAkxB,CAAC,CAAC,CACr3B,CACF,CAAC,EC/BKC,EAAoB,IAAI,IAAI,CAChC,CACE,OACgB,EAAE,cAAA,EAAgB,SAAU,KAAsB,EAAE,cAAc,OAAQ,CAAE,EAAG,gIAAiI,CAAC,CAAC,CACpO,EACA,CACE,UACgB,EAAE,cAAA,EAAgB,SAAU,KAAsB,EAAE,cAAc,OAAQ,CAAE,EAAG,kDAAmD,QAAS,KAAM,CAAC,EAAmB,EAAE,cAAc,OAAQ,CAAE,EAAG,uHAAwH,CAAC,CAAC,CAC9U,EACA,CACE,OACgB,EAAE,cAAA,EAAgB,SAAU,KAAsB,EAAE,cAAc,OAAQ,CAAE,EAAG,+LAAgM,CAAC,CAAC,CACnS,EACA,CACE,QACgB,EAAE,cAAA,EAAgB,SAAU,KAAsB,EAAE,cAAc,OAAQ,CAAE,EAAG,sHAAuH,CAAC,CAAC,CAC1N,EACA,CACE,UACgB,EAAE,cAAA,EAAgB,SAAU,KAAsB,EAAE,cAAc,OAAQ,CAAE,EAAG,uHAAwH,CAAC,CAAC,CAC3N,EACA,CACE,OACgB,EAAE,cAAA,EAAgB,SAAU,KAAsB,EAAE,cAAc,OAAQ,CAAE,EAAG,iHAAkH,CAAC,CAAC,CACrN,CACF,CAAC,ECvBKC,EAAAA,EAAM,YAAY,EAAG,IAAsB,EAAE,cAAcC,EAAG,CAAE,IAAK,EAAG,GAAGC,EAAG,QAASC,CAAE,CAAC,CAAC,EACjG,EAAE,YAAc,gBCDhB,IAAM,EAAA,EAAM,YAAY,EAAG,IAAsB,EAAE,cAAcC,EAAG,CAAE,IAAK,EAAG,GAAG,EAAG,QAASC,CAAE,CAAC,CAAC,EACjG,EAAE,YAAc,iBCDhB,IAAM,EAAA,EAAM,YAAY,EAAG,IAAsB,EAAE,cAAcC,EAAG,CAAE,IAAK,EAAG,GAAG,EAAG,QAASC,CAAE,CAAC,CAAC,EACjG,EAAE,YAAc,2BCaV,EAGF,CACF,QAAQ,EAAA,EAAA,KAAC,EAAD,CAAgB,UAAU,SAAS,OAAO,SAAW,CAAA,EAC7D,QAAQ,EAAA,EAAA,KAAC,EAAD,CAAgB,UAAU,SAAS,OAAO,SAAW,CAAA,EAC7D,OAAO,EAAA,EAAA,KAAC,EAAD,CAAe,UAAU,SAAS,OAAO,SAAW,CAAA,CAC7D,EAEA,SAAgB,EAAyC,CACvD,GAAI,EACJ,OACA,QACA,eACA,YACA,GAAG,GACyB,CAC5B,IAAI,EACJ,AAOE,EAPE,GAAgB,IAAiB,gBACrB,EAAe,GACpB,OAAO,GAAS,UACX,EAAA,EAAA,KAAC,MAAD,CAAK,IAAI,GAAG,UAAU,SAAS,IAAK,CAAO,CAAA,EAChD,IAGK,EAAA,EAAA,KAAC,EAAD,CAAU,UAAU,SAAS,OAAO,SAAW,CAAA,EAG/D,IAAM,EAAU,GACd,EAAA,EAAA,KAAC,OAAD,CAAM,UAAU,oCAAsC,CAAA,GAEtD,EAAA,EAAA,MAAA,EAAA,SAAA,CAAA,SAAA,CACG,GACD,EAAA,EAAA,KAAC,OAAD,CAAM,UAAU,mBAAW,CAAY,CAAA,CACvC,CAAA,CAAA,EAGJ,OAAA,EAAA,EAAA,eACE,EACA,CACE,UACE,4EACF,GAAG,CACL,EACA,CACF,CACF,CCzDA,SAAgB,EAAgB,CAAE,YAAkC,CAClE,OACE,EAAA,EAAA,KAAC,MAAD,CAAK,UAAU,wCAAyC,UAAc,CAAA,CAE1E,CC4BA,IAAMqB,EAA+C,CACnDE,cAAe,2BACfC,wBAAyB,qCACzBC,aAAc,0BACdC,+BAAgC,yCAClC,EAWA,SAASE,GAAQ,CACf,GAAM,CAAEC,IAAGC,QAAStB,EAAe,EAC7BuB,EAASxB,EAAU,EACnByB,EAAc3B,EAAe,EAC7B4B,EAASN,EAAMO,UAAU,EAEzBC,EAAOF,EAAOE,MAAQL,EAAKM,SAC3B,CAAEC,KAAMC,GAAehC,EAAiBY,CAAqB,EAG7DqB,EACJD,EAAWE,aAAaC,gBAAgBN,IACxCG,EAAWE,aAAaC,gBAAgBH,EAAWR,KAAKY,mBACpDC,EAAiBL,EAAWM,mBAE5BC,EAAaZ,EAAOa,YACpBC,EAAoBF,EACtBhB,EAAER,EAAqBwB,IAAe,oBAAoB,EAC1DG,IAAAA,GAEEC,EAAwBX,EAAWY,KAAKC,SAASC,QACjDC,EAAmBf,EAAWY,KAAKI,QAAQF,QAG3CG,EACJjB,EAAWkB,SAASC,QAAQtB,IAC5BG,EAAWkB,SAASC,QAAQnB,EAAWR,KAAKY,mBACxCgB,EACJpB,EAAWkB,SAASG,WAAWxB,IAC/BG,EAAWkB,SAASG,WAAWrB,EAAWR,KAAKY,mBAE3CkB,EAAuB,KAAOvB,IAAuB,CACrDA,EAAKyB,OACP9B,EAAY+B,aAAa3C,EAAuB4C,SAAU3B,CAAI,EAC9D,MAAMpB,EAAK,EAEPD,EAAYiB,CAAM,EACpBgC,OAAOC,SAASC,KAAOrD,EAAkBmB,CAAM,EAE/CF,EAAOqC,SAAS,CAAEC,GAAI,UAAW,CAAC,GAGtCrC,EAAYsC,kBAAkB,CAC5BN,SAAU5C,EAAuB4C,QACnC,CAAC,CACH,EAEMO,EAAuBnE,EAAY,CACvC,GAAGe,EACHqD,UAAWZ,EACXa,cAAiB,CACfzC,EAAYsC,kBAAkB,CAC5BN,SAAU5C,EAAuB4C,QACnC,CAAC,CACH,CACF,CAAC,EAEKU,EAAiBC,GAAuB,CAC5C,IAAIC,EAAW,cAAcD,EAAU,uBAEvC,GAAI3D,EAAYiB,CAAM,EAAG,CACvB,IAAM4C,EAAU,IAAIC,IAAIhE,EAAkBmB,CAAM,CAAC,EAC3C8C,EAAY,GAAGF,EAAQG,WAAWH,EAAQ5C,SAAS4C,EAAQI,OACjEL,GAAY,eAAeM,mBAAmBH,CAAS,GACzD,CAEA,OAAOH,CACT,EAmBA,OACE,EAAA,EAAA,MAAC,EAAD,CAAY,YAAA,GAAY,SAAS,eAAjC,EACE,EAAA,EAAA,KAAC,EAAD,CACE,QAAStC,EAAWkB,SAASyC,SAC7B,SAAUvC,GAAkB7B,EAAE,6BAA6B,EAC3D,MAAO0B,GAAe1B,EAAE,aAAa,CAAE,CAAA,EAGxCkB,IACC,EAAA,EAAA,KAAC,EAAD,CAAO,UAAU,OAAO,KAAM5C,EAAmB,KAAK,iBACnD4C,CACI,CAAA,GAGT,EAAA,EAAA,MAAC,EAAD,CAAA,SAAA,CAEGJ,EAAeuD,IAAKC,IACnB,EAAA,EAAA,KAAC,EAAD,CACE,GAAG,IACH,KAAMzB,EAAcyB,EAASC,EAAE,EAC/B,KAAMD,EAASF,SAEf,MAAOE,EAASE,aAChB,aAAcF,EAASG,IAAK,EAFvBH,EAASC,EAEc,CAE/B,EAGAnD,IACC,EAAA,EAAA,KAAC,EAAD,CACE,GAAG,IACH,UAhD2B,CACnC,IAAMmC,EAAS,IAAIC,gBACnB,IAAK,GAAM,CAACC,EAAKC,KAAUC,OAAOC,QAAQ1E,EAAmBkB,CAAM,CAAC,EAClE,GAAIsD,IAAUvC,IAAAA,GAAW,CACvB,IAAM0C,EAAcC,OAAOJ,CAAK,EAChCH,EAAOQ,IACLN,EACAA,IAAQ,mBACJO,mBAAmBH,CAAW,EAAEI,WAAW,IAAK,EAAE,EAClDJ,CACN,CACF,CAEF,IAAMK,EAAQX,EAAOY,SAAS,EAC9B,OAAOD,EAAQ,mBAAmBA,IAAU,iBAC9C,GAiCuC,EAC7B,MAAM,EAAA,EAAA,KAAC,EAAD,CAAoB,UAAU,SAAS,OAAO,SAAS,CAAA,EAC7D,MAAOlE,EAAE,uBAAuB,CAAE,CAAA,EAKrCwB,IACC,EAAA,EAAA,KAAC,EAAD,CACE,GAAG,SACH,SAAUkB,EAAqBgC,UAC/B,MAAM,EAAA,EAAA,KAAC,EAAD,CAAiB,UAAU,SAAS,OAAO,SAAS,CAAA,EAC1D,UAAWhC,EAAqBgC,UAChC,MAAO1E,EAAE,sBAAsB,EAC/B,YAAe0C,EAAqBiC,OAAO,EAC3C,KAAK,QAAQ,CAAA,CAGF,CAAA,CAAA,EAEhBjE,IACC,EAAA,EAAA,KAAC,MAAD,CAAK,UAAU,0DACb,EAAA,EAAA,KAAC,MAAD,CACE,UAAU,sCACV,wBAAyB,CAAEkE,OAAQlE,CAAe,CAAE,CAAA,CAEnD,CAAA,CAEG,GAEhB"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"passkey-BDrX3jVg.js","names":["standardSchemaResolver","FingerprintIcon","ShieldCheckIcon","WarningCircleIcon","useMutation","useQueryClient","Link","useRouter","useEffect","useMemo","useRef","useState","useForm","useTranslation","z","FooterLink","PageHeader","SubmitButton","Alert","PageLayout","buildAuthorizeUrl","extractOAuthParams","isOAuthFlow","tick","registerPasskeyMutationOptions","getSessionQueryOptions","Route","SetupStep","SetupPasskey","t","router","queryClient","search","useSearch","step","setStep","passkey_name","errorMessage","setErrorMessage","autoRegisterCalledRef","formSchema","object","name","string","max","FormValues","infer","register","handleSubmit","formState","errors","defaultValues","resolver","registerMutation","onSuccess","data","second_factor_setup_completed","user","setQueryData","queryKey","window","location","href","navigate","to","invalidateQueries","onError","error","Error","onSettled","current","mutate","onSubmit","values","undefined","message","isPending","component"],"sources":["../../../frontend/src/routes/setup/passkey/index.tsx?tsr-split=component"],"sourcesContent":["import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';\nimport {\n FingerprintIcon,\n ShieldCheckIcon,\n WarningCircleIcon,\n} from '@phosphor-icons/react';\nimport { useMutation, useQueryClient } from '@tanstack/react-query';\nimport { createFileRoute, Link, useRouter } from '@tanstack/react-router';\nimport { 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 { Alert } from '#frontend/components/ui/alert.tsx';\nimport { PageLayout } from '#frontend/features/layout/page-layout.tsx';\nimport {\n buildAuthorizeUrl,\n extractOAuthParams,\n isOAuthFlow,\n OAuthSearchSchema,\n} from '#frontend/libs/oauth-search.ts';\nimport { tick } from '#frontend/libs/promise.ts';\nimport { registerPasskeyMutationOptions } from '#frontend/queries/passkey.ts';\nimport { getSessionQueryOptions } from '#frontend/queries/session.ts';\n\nconst SearchSchema = OAuthSearchSchema.extend({\n passkey_name: z.string().optional(),\n});\n\nexport const Route = createFileRoute('/setup/passkey/')({\n component: SetupPasskey,\n validateSearch: SearchSchema,\n});\n\ntype SetupStep = 'form' | 'registering' | 'error';\n\nfunction SetupPasskey() {\n const { t } = useTranslation();\n const router = useRouter();\n const queryClient = useQueryClient();\n const search = Route.useSearch();\n\n const [step, setStep] = useState<SetupStep>(\n search.passkey_name ? 'registering' : 'form',\n );\n const [errorMessage, setErrorMessage] = useState<string>('');\n const autoRegisterCalledRef = useRef(false);\n\n const formSchema = useMemo(\n () =>\n z.object({\n name: z.string().max(100, t('validation.passkey.name.max')),\n }),\n [t],\n );\n\n type FormValues = z.infer<typeof formSchema>;\n\n const {\n register,\n handleSubmit,\n formState: { errors },\n } = useForm<FormValues>({\n defaultValues: { name: '' },\n resolver: standardSchemaResolver(formSchema),\n });\n\n const registerMutation = useMutation({\n ...registerPasskeyMutationOptions,\n onSuccess: async (data) => {\n if (data.second_factor_setup_completed && data.user) {\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 } else {\n queryClient.invalidateQueries({\n queryKey: getSessionQueryOptions.queryKey,\n });\n router.navigate({ to: '/profile' });\n }\n },\n onError: (error) => {\n setStep('error');\n if (error instanceof Error) {\n if (error.name === 'NotAllowedError') {\n setErrorMessage(t('setupPasskey.error.cancelled'));\n } else {\n setErrorMessage(t('setupPasskey.error.failed'));\n }\n } else {\n setErrorMessage(t('setupPasskey.error.failed'));\n }\n },\n onSettled: () => {\n queryClient.invalidateQueries({\n queryKey: getSessionQueryOptions.queryKey,\n });\n },\n });\n\n useEffect(() => {\n if (search.passkey_name && !autoRegisterCalledRef.current) {\n autoRegisterCalledRef.current = true;\n registerMutation.mutate({ name: search.passkey_name });\n }\n }, [registerMutation, search.passkey_name]);\n\n const onSubmit = (values: FormValues) => {\n setStep('registering');\n setErrorMessage('');\n registerMutation.mutate({ name: values.name || undefined });\n };\n\n // Registering state - waiting for WebAuthn\n if (step === 'registering') {\n return (\n <PageLayout cardPadding maxWidth=\"100\">\n <PageHeader\n subtitle={t('setupPasskey.subtitle')}\n title={t('setupPasskey.title')}\n />\n <div className=\"flex flex-col items-center gap-4 py-8\">\n <FingerprintIcon className=\"size-16 animate-pulse text-primary\" />\n <p className=\"text-center text-base-content/70\">\n {t('setupPasskey.waiting')}\n </p>\n </div>\n </PageLayout>\n );\n }\n\n // Error state\n if (step === 'error') {\n return (\n <PageLayout cardPadding maxWidth=\"100\">\n <PageHeader\n subtitle={t('setupPasskey.subtitle')}\n title={t('setupPasskey.title')}\n />\n <Alert icon={WarningCircleIcon} type=\"error\">\n {errorMessage}\n </Alert>\n <button\n className=\"btn btn-primary btn-block mt-4\"\n onClick={() => {\n if (search.passkey_name) {\n setStep('registering');\n setErrorMessage('');\n registerMutation.mutate({ name: search.passkey_name });\n } else {\n setStep('form');\n }\n }}\n type=\"button\"\n >\n {t('setupPasskey.retry')}\n </button>\n <FooterLink\n as={Link}\n linkText={t('setupPasskey.backToLogin')}\n search={extractOAuthParams(search)}\n text=\"\"\n to=\"/login\"\n />\n </PageLayout>\n );\n }\n\n // Form state\n return (\n <PageLayout cardPadding maxWidth=\"100\">\n <PageHeader\n subtitle={t('setupPasskey.subtitle')}\n title={t('setupPasskey.title')}\n />\n\n <Alert icon={ShieldCheckIcon} type=\"info\">\n {t('setupPasskey.required')}\n </Alert>\n\n <p className=\"mt-4 text-center text-base-content/60 text-sm\">\n {t('setupPasskey.description')}\n </p>\n\n <form\n className=\"mt-4 flex flex-col gap-4\"\n onSubmit={handleSubmit(onSubmit)}\n >\n <div className=\"form-control\">\n <label className=\"label\" htmlFor=\"passkey-name\">\n <span className=\"label-text\">{t('setupPasskey.name.label')}</span>\n </label>\n <input\n className={`input input-bordered ${\n errors.name ? 'input-error' : ''\n }`}\n id=\"passkey-name\"\n placeholder={t('setupPasskey.name.placeholder')}\n type=\"text\"\n {...register('name')}\n />\n <span className=\"label-text-alt mt-1 text-base-content/50\">\n {t('setupPasskey.name.hint')}\n </span>\n {errors.name && (\n <span className=\"label-text-alt mt-1 text-error\">\n {errors.name.message}\n </span>\n )}\n </div>\n\n <SubmitButton\n className=\"mt-2\"\n isPending={registerMutation.isPending}\n pendingText={t('setupPasskey.registering')}\n >\n {t('setupPasskey.continue')}\n </SubmitButton>\n </form>\n\n <FooterLink\n as={Link}\n linkText={t('setupPasskey.backToLogin')}\n search={extractOAuthParams(search)}\n text=\"\"\n to=\"/login\"\n />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"passkey-C-h8Fvqn.js","names":["FingerprintIcon","WarningCircleIcon","useMutation","useQueryClient","Link","useRouter","useEffect","useRef","useState","useTranslation","FooterLink","PageHeader","Alert","PageLayout","TinyAuthError","buildAuthorizeUrl","isOAuthFlow","tick","authenticateWithPasskeyMutationOptions","getSessionQueryOptions","Route","VerifyPasskey","t","router","queryClient","search","useSearch","error","setError","hasStarted","verifyMutation","onSuccess","data","user","setQueryData","queryKey","window","location","href","navigate","to","onError","err","code","onSettled","invalidateQueries","current","mutate","isPending","component"],"sources":["../../../frontend/src/routes/verify/passkey/index.tsx?tsr-split=component"],"sourcesContent":["import { FingerprintIcon, WarningCircleIcon } from '@phosphor-icons/react';\nimport { useMutation, useQueryClient } from '@tanstack/react-query';\nimport { createFileRoute, Link, useRouter } from '@tanstack/react-router';\nimport { useEffect, useRef, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { FooterLink } from '#frontend/components/auth/footer-link.tsx';\nimport { PageHeader } from '#frontend/components/auth/page-header.tsx';\nimport { Alert } from '#frontend/components/ui/alert.tsx';\nimport { PageLayout } from '#frontend/features/layout/page-layout.tsx';\nimport { TinyAuthError } from '#frontend/libs/error.ts';\nimport {\n buildAuthorizeUrl,\n isOAuthFlow,\n OAuthSearchSchema,\n} from '#frontend/libs/oauth-search.ts';\nimport { tick } from '#frontend/libs/promise.ts';\nimport { authenticateWithPasskeyMutationOptions } from '#frontend/queries/passkey.ts';\nimport { getSessionQueryOptions } from '#frontend/queries/session.ts';\n\nconst SearchSchema = OAuthSearchSchema;\n\nexport const Route = createFileRoute('/verify/passkey/')({\n component: VerifyPasskey,\n validateSearch: SearchSchema,\n});\n\nfunction VerifyPasskey() {\n const { t } = useTranslation();\n const router = useRouter();\n const queryClient = useQueryClient();\n const search = Route.useSearch();\n const [error, setError] = useState<string | null>(null);\n const hasStarted = useRef(false);\n\n const verifyMutation = useMutation({\n ...authenticateWithPasskeyMutationOptions,\n onSuccess: async (data) => {\n if (data.user) {\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 },\n onError: async (err) => {\n if (err instanceof TinyAuthError) {\n if (err.code === 'SECOND_FACTOR_SESSION_EXPIRED') {\n setError(t('verifyPasskey.error.expired'));\n return;\n }\n if (err.code === 'PASSKEY_USER_MISMATCH') {\n setError(t('verifyPasskey.error.userMismatch'));\n return;\n }\n } else {\n setError(t('verifyPasskey.error.failed'));\n return;\n }\n },\n onSettled: () => {\n queryClient.invalidateQueries({\n queryKey: getSessionQueryOptions.queryKey,\n });\n },\n });\n\n // Auto-start passkey authentication on mount (with guard for StrictMode)\n useEffect(() => {\n if (hasStarted.current) return;\n hasStarted.current = true;\n verifyMutation.mutate();\n }, [verifyMutation]);\n\n return (\n <PageLayout cardPadding maxWidth=\"100\">\n <PageHeader\n subtitle={t('verifyPasskey.subtitle')}\n title={t('verifyPasskey.title')}\n />\n\n <div className=\"flex flex-col items-center gap-6\">\n {verifyMutation.isPending && (\n <div className=\"flex flex-col items-center gap-4\">\n <div className=\"flex size-20 items-center justify-center rounded-full bg-base-200\">\n <FingerprintIcon className=\"size-10 animate-pulse text-primary\" />\n </div>\n <p className=\"text-center text-base-content/70 text-sm\">\n {t('verifyPasskey.waiting')}\n </p>\n </div>\n )}\n\n {error && (\n <>\n <Alert className=\"w-full\" icon={WarningCircleIcon} type=\"error\">\n {error}\n </Alert>\n <button\n className=\"btn btn-primary btn-block\"\n onClick={() => {\n setError(null);\n verifyMutation.mutate();\n }}\n type=\"button\"\n >\n <FingerprintIcon className=\"size-5\" weight=\"regular\" />\n {t('verifyPasskey.retry')}\n </button>\n </>\n )}\n </div>\n\n <FooterLink\n as={Link}\n linkText={t('verifyPasskey.backToLogin')}\n search={search}\n text=\"\"\n to=\"/login\"\n />\n </PageLayout>\n );\n}\n"],"mappings":"ikBA0BA,SAASqB,GAAgB,CACvB,GAAM,CAAEC,GAAMb,EAAe,EACvBc,EAASlB,EAAU,EACnBmB,EAAcrB,EAAe,EAC7BsB,EAASL,EAAMM,UAAU,EACzB,CAACC,EAAOC,IAAAA,EAAAA,EAAAA,UAAoC,IAAI,EAChDC,GAAAA,EAAAA,EAAAA,QAAoB,EAAK,EAEzBC,EAAiB5B,EAAY,CACjC,GAAGgB,EACHa,UAAW,KAAOC,IAAS,CACrBA,EAAKC,OACPT,EAAYU,aAAaf,EAAuBgB,SAAU,CACxDF,KAAMD,EAAKC,IACb,CAAC,EACD,MAAMhB,EAAK,EAEPD,EAAYS,CAAM,EACpBW,OAAOC,SAASC,KAAOvB,EAAkBU,CAAM,EAE/CF,EAAOgB,SAAS,CAAEC,GAAI,UAAW,CAAC,EAGxC,EACAC,QAAS,KAAOC,IAAQ,CACtB,GAAIA,aAAe5B,EAAe,CAChC,GAAI4B,EAAIC,OAAS,gCAAiC,CAChDf,EAASN,EAAE,6BAA6B,CAAC,EACzC,MACF,CACA,GAAIoB,EAAIC,OAAS,wBAAyB,CACxCf,EAASN,EAAE,kCAAkC,CAAC,EAC9C,MACF,CACF,KAAO,CACLM,EAASN,EAAE,4BAA4B,CAAC,EACxC,MACF,CACF,EACAsB,cAAiB,CACfpB,EAAYqB,kBAAkB,CAC5BV,SAAUhB,EAAuBgB,QACnC,CAAC,CACH,CACF,CAAC,EASD,OANA7B,EAAAA,EAAAA,eAAgB,CACVuB,EAAWiB,UACfjB,EAAWiB,QAAU,GACrBhB,EAAeiB,OAAO,EACxB,EAAG,CAACjB,CAAc,CAAC,GAGjB,EAAA,EAAA,MAAC,EAAD,CAAY,YAAA,GAAY,SAAS,eAAjC,EACE,EAAA,EAAA,KAAC,EAAD,CACE,SAAUR,EAAE,wBAAwB,EACpC,MAAOA,EAAE,qBAAqB,CAAE,CAAA,GAGlC,EAAA,EAAA,MAAC,MAAD,CAAK,UAAU,4CAAf,CACGQ,EAAekB,YACd,EAAA,EAAA,MAAC,MAAD,CAAK,UAAU,4CAAf,EACE,EAAA,EAAA,KAAC,MAAD,CAAK,UAAU,8EACb,EAAA,EAAA,KAAC,EAAD,CAAiB,UAAU,oCAAoC,CAAA,CAC5D,CAAA,GACL,EAAA,EAAA,KAAC,IAAD,CAAG,UAAU,oDACV1B,EAAE,uBAAuB,CACzB,CAAA,CACA,IAGNK,IACC,EAAA,EAAA,MAAA,EAAA,SAAA,CAAA,SAAA,EACE,EAAA,EAAA,KAAC,EAAD,CAAO,UAAU,SAAS,KAAM1B,EAAmB,KAAK,iBACrD0B,CACI,CAAA,GACP,EAAA,EAAA,MAAC,SAAD,CACE,UAAU,4BACV,YAAe,CACbC,EAAS,IAAI,EACbE,EAAeiB,OAAO,CACxB,EACA,KAAK,kBANP,EAQE,EAAA,EAAA,KAAC,EAAD,CAAiB,UAAU,SAAS,OAAO,SAAS,CAAA,EACnDzB,EAAE,qBAAqB,CAClB,GACV,CAAA,CAAA,CAEC,KAEL,EAAA,EAAA,KAAC,EAAD,CACE,GAAIlB,EACJ,SAAUkB,EAAE,2BAA2B,EAC/BG,SACR,KAAK,GACL,GAAG,QAAQ,CAAA,CAEH,GAEhB"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{o as e,r as t,s as n,u as r}from"./IconBase.es-d5KP98Ac.js";import{Q as i,a,f as o,h 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{c as g,i as _,o as v}from"./zod-BItJDQBQ.js";import{A as y,M as b,S as x,j as S,s as C}from"./index-BmfaaNx6.js";import{t as w}from"./page-header-BYMFSGfT.js";import{t as T}from"./promise-OpBtq8tG.js";import{o as E}from"./passkey-e6uvApHa.js";import{r as D,t as O}from"./standard-schema-o4V-s4uY.js";import{t as k}from"./footer-link-Ib1Hd-fr.js";import{t as A}from"./icon-input-8iU7PNzd.js";import{t as j}from"./submit-button-Xx6DwLyh.js";var M=r(n()),N=f({mutationFn:async e=>a(await c.api.auth.login.$post({json:e}))}),P=e();function F(){let{t:e,i18n:n}=t(),r=s(),a=u(),c=C.useSearch(),f=c.account_selection_state&&c.prompt?.split(` `).includes(`login`)?{...c,account_selected:`1`}:c,F=c.lang??n.language,{data:I}=i(l),L=I.branding.title?.[F]??I.branding.title?.[I.i18n.fallback_language],R=I.branding.subtitle?.[F]??I.branding.subtitle?.[I.i18n.fallback_language],z=I.auth.password.enabled,B=I.auth.passkey.enabled,V=(0,M.useMemo)(()=>v({email:_(e(`validation.email.invalid`)),password:g().min(1,e(`validation.password.required`))}),[e]),H=d({...N,onSuccess:async(e,t)=>{let n=e.user;if(n.email_verification_required&&!n.email_verified){r.navigate({to:`/verify/email`,search:{email:t.email,...S(c)}});return}a.setQueryData(x.queryKey,{user:n}),await T();let i=[];if(I.auth.password.totp.enabled&&n.totp_registered&&i.push(`totp`),I.auth.passkey.enabled&&n.passkey_count>0&&i.push(`passkey`),n.second_factor_required&&i.length===0){if(I.available_2fa_setup_methods.length>1)return r.navigate({to:`/setup/2fa`,search:S(c)});if(I.available_2fa_setup_methods.length===1)return I.available_2fa_setup_methods[0]===`totp`?r.navigate({to:`/setup/totp`,search:S(c)}):r.navigate({to:`/setup/passkey`,search:{...S(c),passkey_name:`default`}})}if(i.length>1)return r.navigate({to:`/verify/2fa`,search:S(c)});if(i.length===1)return i[0]===`totp`?r.navigate({to:`/verify/totp`,search:S(c)}):r.navigate({to:`/verify/passkey`,search:S(c)});if(b(c))window.location.href=y(f);else return r.navigate({to:`/profile`})},onError:t=>{K(`email`,{type:`manual`,message:e(`login.error.failed`)})},onSettled:()=>{a.invalidateQueries({queryKey:x.queryKey})}}),U=(0,M.useCallback)(async e=>{e.user&&(a.setQueryData(x.queryKey,e),await T(),b(c)?window.location.href=y(f):r.navigate({to:`/profile`})),a.invalidateQueries({queryKey:x.queryKey})},[a,r,c,f]),W=(0,M.useRef)(null);(0,M.useEffect)(()=>{if(!(!B||!z))return W.current=new AbortController,E(U,W.current.signal),()=>{W.current?.abort()}},[B,z,U]);let{register:G,setError:K,handleSubmit:q,formState:{errors:J}}=D({defaultValues:{email:``,password:``},resolver:O(V)});return(0,P.jsxs)(p,{cardPadding:!0,maxWidth:`100`,children:[(0,P.jsx)(w,{iconUrl:I.branding.icon_url,subtitle:R??e(`login.selectMethod.subtitle`),title:L??e(`login.title`)}),z&&(0,P.jsxs)(`form`,{className:`flex flex-col gap-4`,onSubmit:q(async e=>{H.mutate(e)}),children:[(0,P.jsx)(A,{autoComplete:`username webauthn`,error:J.email,icon:m,placeholder:e(`login.email.placeholder`),...G(`email`),type:`email`}),(0,P.jsx)(A,{autoComplete:`current-password`,error:J.password,icon:h,placeholder:e(`login.password.placeholder`),...G(`password`),type:`password`}),I.email.enabled&&(0,P.jsx)(`div`,{className:`flex items-center justify-end`,children:(0,P.jsx)(o,{className:`link text-sm`,to:`/password/forgot`,children:e(`login.link.forgotPassword`)})}),(0,P.jsx)(j,{className:`mt-2`,isPending:H.isPending,pendingText:e(`login.submitting`),children:e(`login.submit`)})]}),I.registration.public_registration&&(0,P.jsx)(k,{as:o,linkText:e(`login.link.register`),search:S(c),text:e(`login.footer.noAccount`),to:`/register`})]})}export{F as component};
|
|
2
|
-
//# sourceMappingURL=password-86C-hCze.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"password-86C-hCze.js","names":["standardSchemaResolver","EnvelopeSimpleIcon","LockIcon","useMutation","useQueryClient","useSuspenseQuery","Link","useRouter","useCallback","useEffect","useMemo","useRef","useForm","useTranslation","z","FooterLink","IconInput","PageHeader","SubmitButton","PageLayout","buildAuthorizeUrl","extractOAuthParams","isOAuthFlow","tick","appConfigQueryOptions","loginMutationOptions","startConditionalPasskeyAuth","getSessionQueryOptions","Route","LoginPassword","t","i18n","router","queryClient","search","useSearch","authorizeSearch","account_selection_state","prompt","split","includes","account_selected","const","lang","language","data","configData","customTitle","branding","title","fallback_language","customSubtitle","subtitle","isPasswordAuthEnabled","auth","password","enabled","isPasskeyEnabled","passkey","loginSchema","object","email","string","min","loginMutation","onSuccess","params","user","email_verification_required","email_verified","navigate","to","setQueryData","queryKey","registered_2fa_methods","SecondFactorMethod","totp","totp_registered","push","passkey_count","second_factor_required","length","available_2fa_setup_methods","method","passkey_name","window","location","href","onError","_error","setError","type","message","onSettled","invalidateQueries","handlePasskeySuccess","AuthResponse","abortControllerRef","AbortController","current","signal","abort","register","handleSubmit","formState","errors","defaultValues","resolver","onSubmit","values","infer","mutate","icon_url","isPending","registration","public_registration","component"],"sources":["../../../frontend/src/queries/login.ts","../../../frontend/src/routes/login/password/index.tsx?tsr-split=component"],"sourcesContent":["import { mutationOptions } from '@tanstack/react-query';\nimport type { InferRequestType, InferResponseType } from 'hono/client';\nimport { client, jsonOk } from '#frontend/libs/api.ts';\n\nexport type LoginParams = InferRequestType<\n (typeof client.api.auth.login)['$post']\n>['json'];\n\nexport type LoginResponse = InferResponseType<\n (typeof client.api.auth.login)['$post'],\n 200\n>;\n\nexport const loginMutationOptions = mutationOptions({\n mutationFn: async (values: LoginParams) => {\n const res = await client.api.auth.login.$post({\n json: values,\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 { createFileRoute, Link, useRouter } from '@tanstack/react-router';\nimport { useCallback, useEffect, useMemo, useRef } 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 { RouteErrorFallback } from '#frontend/components/ui/route-error-fallback.tsx';\nimport { PageLayout } from '#frontend/features/layout/page-layout.tsx';\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 { loginMutationOptions } from '#frontend/queries/login.ts';\nimport { startConditionalPasskeyAuth } from '#frontend/queries/passkey.ts';\nimport {\n type AuthResponse,\n getSessionQueryOptions,\n} from '#frontend/queries/session.ts';\n\nconst SearchSchema = OAuthSearchSchema;\n\nexport const Route = createFileRoute('/login/password/')({\n component: LoginPassword,\n errorComponent: RouteErrorFallback,\n validateSearch: SearchSchema,\n loader: async ({ context }) => {\n await context.queryClient.ensureQueryData(appConfigQueryOptions);\n },\n});\n\nfunction LoginPassword() {\n const { t, i18n } = useTranslation();\n const router = useRouter();\n const queryClient = useQueryClient();\n const search = Route.useSearch();\n const authorizeSearch =\n search.account_selection_state &&\n search.prompt?.split(' ').includes('login')\n ? { ...search, account_selected: '1' as const }\n : search;\n const lang = search.lang ?? i18n.language;\n\n const { data: configData } = useSuspenseQuery(appConfigQueryOptions);\n\n const customTitle =\n configData.branding.title?.[lang] ??\n configData.branding.title?.[configData.i18n.fallback_language];\n const customSubtitle =\n configData.branding.subtitle?.[lang] ??\n configData.branding.subtitle?.[configData.i18n.fallback_language];\n const isPasswordAuthEnabled = configData.auth.password.enabled;\n const isPasskeyEnabled = configData.auth.passkey.enabled;\n\n const loginSchema = useMemo(\n () =>\n z.object({\n email: z.email(t('validation.email.invalid')),\n password: z.string().min(1, t('validation.password.required')),\n }),\n [t],\n );\n\n const loginMutation = useMutation({\n ...loginMutationOptions,\n onSuccess: async (data, params) => {\n const user = data.user;\n\n if (user.email_verification_required && !user.email_verified) {\n router.navigate({\n to: '/verify/email',\n search: {\n email: params.email,\n ...extractOAuthParams(search),\n },\n });\n return;\n }\n\n queryClient.setQueryData(getSessionQueryOptions.queryKey, {\n user: user,\n });\n await tick();\n\n const registered_2fa_methods: SecondFactorMethod[] = [];\n if (configData.auth.password.totp.enabled && user.totp_registered) {\n registered_2fa_methods.push('totp');\n }\n if (configData.auth.passkey.enabled && user.passkey_count > 0) {\n registered_2fa_methods.push('passkey');\n }\n\n if (user.second_factor_required && registered_2fa_methods.length === 0) {\n if (configData.available_2fa_setup_methods.length > 1) {\n return router.navigate({\n to: '/setup/2fa',\n search: extractOAuthParams(search),\n });\n } else if (configData.available_2fa_setup_methods.length === 1) {\n const method = configData.available_2fa_setup_methods[0];\n if (method === 'totp') {\n return router.navigate({\n to: '/setup/totp',\n search: extractOAuthParams(search),\n });\n } else {\n return router.navigate({\n to: '/setup/passkey',\n search: {\n ...extractOAuthParams(search),\n passkey_name: 'default',\n },\n });\n }\n }\n }\n\n if (registered_2fa_methods.length > 1) {\n return router.navigate({\n to: '/verify/2fa',\n search: extractOAuthParams(search),\n });\n } else if (registered_2fa_methods.length === 1) {\n const method = registered_2fa_methods[0];\n if (method === 'totp') {\n return router.navigate({\n to: '/verify/totp',\n search: extractOAuthParams(search),\n });\n } else {\n return router.navigate({\n to: '/verify/passkey',\n search: extractOAuthParams(search),\n });\n }\n } else {\n if (isOAuthFlow(search)) {\n window.location.href = buildAuthorizeUrl(authorizeSearch);\n } else {\n return router.navigate({ to: '/profile' });\n }\n }\n },\n onError: (_error) => {\n setError('email', {\n type: 'manual',\n message: t('login.error.failed'),\n });\n },\n onSettled: () => {\n queryClient.invalidateQueries({\n queryKey: getSessionQueryOptions.queryKey,\n });\n },\n });\n\n const handlePasskeySuccess = useCallback(\n async (data: AuthResponse) => {\n if (data.user) {\n queryClient.setQueryData(getSessionQueryOptions.queryKey, data);\n await tick();\n\n if (isOAuthFlow(search)) {\n window.location.href = buildAuthorizeUrl(authorizeSearch);\n } else {\n router.navigate({ to: '/profile' });\n }\n }\n queryClient.invalidateQueries({\n queryKey: getSessionQueryOptions.queryKey,\n });\n },\n [queryClient, router, search, authorizeSearch],\n );\n\n // Conditional UI: Start passkey autofill on page load\n const abortControllerRef = useRef<AbortController | null>(null);\n\n useEffect(() => {\n if (!isPasskeyEnabled || !isPasswordAuthEnabled) {\n return;\n }\n\n abortControllerRef.current = new AbortController();\n startConditionalPasskeyAuth(\n handlePasskeySuccess,\n abortControllerRef.current.signal,\n );\n\n return () => {\n abortControllerRef.current?.abort();\n };\n }, [isPasskeyEnabled, isPasswordAuthEnabled, handlePasskeySuccess]);\n\n const {\n register,\n setError,\n handleSubmit,\n formState: { errors },\n } = useForm({\n defaultValues: {\n email: '',\n password: '',\n },\n resolver: standardSchemaResolver(loginSchema),\n });\n\n const onSubmit = async (values: z.infer<typeof loginSchema>) => {\n loginMutation.mutate(values);\n };\n\n return (\n <PageLayout cardPadding maxWidth=\"100\">\n <PageHeader\n iconUrl={configData.branding.icon_url}\n subtitle={customSubtitle ?? t('login.selectMethod.subtitle')}\n title={customTitle ?? t('login.title')}\n />\n\n {isPasswordAuthEnabled && (\n <form className=\"flex flex-col gap-4\" onSubmit={handleSubmit(onSubmit)}>\n <IconInput\n autoComplete=\"username webauthn\"\n error={errors.email}\n icon={EnvelopeSimpleIcon}\n placeholder={t('login.email.placeholder')}\n {...register('email')}\n type=\"email\"\n />\n\n <IconInput\n autoComplete=\"current-password\"\n error={errors.password}\n icon={LockIcon}\n placeholder={t('login.password.placeholder')}\n {...register('password')}\n type=\"password\"\n />\n\n {configData.email.enabled && (\n <div className=\"flex items-center justify-end\">\n <Link className=\"link text-sm\" to=\"/password/forgot\">\n {t('login.link.forgotPassword')}\n </Link>\n </div>\n )}\n\n <SubmitButton\n className=\"mt-2\"\n isPending={loginMutation.isPending}\n pendingText={t('login.submitting')}\n >\n {t('login.submit')}\n </SubmitButton>\n </form>\n )}\n\n {configData.registration.public_registration && (\n <FooterLink\n as={Link}\n linkText={t('login.link.register')}\n search={extractOAuthParams(search)}\n text={t('login.footer.noAccount')}\n to=\"/register\"\n />\n )}\n\n {/* <div className=\"flex flex-col items-center gap-2\">\n <Link\n to=\"/login\"\n search={extractOAuthParams(search)}\n className=\"link text-sm\"\n >\n {t('login.password.backToMethods')}\n </Link>\n </div> */}\n </PageLayout>\n );\n}\n"],"mappings":"40BAaA,EAAA,EAAA,CAAA,WAAA,KAAA,6CAME,CAAA,QC0BF,SAAS6B,GAAgB,CACvB,GAAM,CAAEC,IAAGC,QAASlB,EAAe,EAC7BmB,EAASzB,EAAU,EACnB0B,EAAc7B,EAAe,EAC7B8B,EAASN,EAAMO,UAAU,EACzBC,EACJF,EAAOG,yBACPH,EAAOI,QAAQC,MAAM,GAAG,EAAEC,SAAS,OAAO,EACtC,CAAE,GAAGN,EAAQO,iBAAkB,GAAa,EAC5CP,EACAS,EAAOT,EAAOS,MAAQZ,EAAKa,SAE3B,CAAEC,KAAMC,GAAezC,EAAiBmB,CAAqB,EAE7DuB,EACJD,EAAWE,SAASC,QAAQN,IAC5BG,EAAWE,SAASC,QAAQH,EAAWf,KAAKmB,mBACxCC,EACJL,EAAWE,SAASI,WAAWT,IAC/BG,EAAWE,SAASI,WAAWN,EAAWf,KAAKmB,mBAC3CG,EAAwBP,EAAWQ,KAAKC,SAASC,QACjDC,EAAmBX,EAAWQ,KAAKI,QAAQF,QAE3CG,GAAAA,EAAAA,EAAAA,aAEF7C,EAAS,CACP+C,MAAO/C,EAAQgB,EAAE,0BAA0B,CAAC,EAC5CyB,SAAUzC,EAAS,EAAEiD,IAAI,EAAGjC,EAAE,8BAA8B,CAAC,CAC/D,CAAC,EACH,CAACA,CAAC,CACJ,EAEMkC,EAAgB7D,EAAY,CAChC,GAAGsB,EACHwC,UAAW,MAAOpB,EAAMqB,IAAW,CACjC,IAAMC,EAAOtB,EAAKsB,KAElB,GAAIA,EAAKC,6BAA+B,CAACD,EAAKE,eAAgB,CAC5DrC,EAAOsC,SAAS,CACdC,GAAI,gBACJrC,OAAQ,CACN2B,MAAOK,EAAOL,MACd,GAAGxC,EAAmBa,CAAM,CAC9B,CACF,CAAC,EACD,MACF,CAEAD,EAAYuC,aAAa7C,EAAuB8C,SAAU,CAClDN,MACR,CAAC,EACD,MAAM5C,EAAK,EAEX,IAAMmD,EAA+C,CAAA,EAQrD,GAPI5B,EAAWQ,KAAKC,SAASqB,KAAKpB,SAAWW,EAAKU,iBAChDH,EAAuBI,KAAK,MAAM,EAEhChC,EAAWQ,KAAKI,QAAQF,SAAWW,EAAKY,cAAgB,GAC1DL,EAAuBI,KAAK,SAAS,EAGnCX,EAAKa,wBAA0BN,EAAuBO,SAAW,EACnE,IAAInC,EAAWoC,4BAA4BD,OAAS,EAClD,OAAOjD,EAAOsC,SAAS,CACrBC,GAAI,aACJrC,OAAQb,EAAmBa,CAAM,CACnC,CAAC,EACI,GAAIY,EAAWoC,4BAA4BD,SAAW,EAQzD,OAPanC,EAAWoC,4BAA4B,KACvC,OACNlD,EAAOsC,SAAS,CACrBC,GAAI,cACJrC,OAAQb,EAAmBa,CAAM,CACnC,CAAC,EAEMF,EAAOsC,SAAS,CACrBC,GAAI,iBACJrC,OAAQ,CACN,GAAGb,EAAmBa,CAAM,EAC5BkD,aAAc,SAChB,CACF,CAAC,CAEL,CAGF,GAAIV,EAAuBO,OAAS,EAClC,OAAOjD,EAAOsC,SAAS,CACrBC,GAAI,cACJrC,OAAQb,EAAmBa,CAAM,CACnC,CAAC,EACI,GAAIwC,EAAuBO,SAAW,EAQzC,OAPaP,EAAuB,KACvB,OACN1C,EAAOsC,SAAS,CACrBC,GAAI,eACJrC,OAAQb,EAAmBa,CAAM,CACnC,CAAC,EAEMF,EAAOsC,SAAS,CACrBC,GAAI,kBACJrC,OAAQb,EAAmBa,CAAM,CACnC,CAAC,EAGH,GAAIZ,EAAYY,CAAM,EACpBmD,OAAOC,SAASC,KAAOnE,EAAkBgB,CAAe,OAExD,OAAOJ,EAAOsC,SAAS,CAAEC,GAAI,UAAW,CAAC,CAG/C,EACAiB,QAAUC,GAAW,CACnBC,EAAS,QAAS,CAChBC,KAAM,SACNC,QAAS9D,EAAE,oBAAoB,CACjC,CAAC,CACH,EACA+D,cAAiB,CACf5D,EAAY6D,kBAAkB,CAC5BrB,SAAU9C,EAAuB8C,QACnC,CAAC,CACH,CACF,CAAC,EAEKsB,GAAAA,EAAAA,EAAAA,aACJ,KAAOlD,IAAuB,CACxBA,EAAKsB,OACPlC,EAAYuC,aAAa7C,EAAuB8C,SAAU5B,CAAI,EAC9D,MAAMtB,EAAK,EAEPD,EAAYY,CAAM,EACpBmD,OAAOC,SAASC,KAAOnE,EAAkBgB,CAAe,EAExDJ,EAAOsC,SAAS,CAAEC,GAAI,UAAW,CAAC,GAGtCtC,EAAY6D,kBAAkB,CAC5BrB,SAAU9C,EAAuB8C,QACnC,CAAC,CACH,EACA,CAACxC,EAAaD,EAAQE,EAAQE,CAAe,CAC/C,EAGM6D,GAAAA,EAAAA,EAAAA,QAAoD,IAAI,GAE9DxF,EAAAA,EAAAA,eAAgB,CACV,MAACgD,GAAoB,CAACJ,GAU1B,MANA4C,GAAmBE,QAAU,IAAID,gBACjCxE,EACEqE,EACAE,EAAmBE,QAAQC,MAC7B,MAEa,CACXH,EAAmBE,SAASE,MAAM,CACpC,CACF,EAAG,CAAC5C,EAAkBJ,EAAuB0C,CAAoB,CAAC,EAElE,GAAM,CACJO,WACAZ,WACAa,eACAC,UAAW,CAAEC,WACX7F,EAAQ,CACV8F,cAAe,CACb7C,MAAO,GACPN,SAAU,EACZ,EACAoD,SAAU3G,EAAuB2D,CAAW,CAC9C,CAAC,EAMD,OACE,EAAA,EAAA,MAAC,EAAD,CAAY,YAAA,GAAY,SAAS,eAAjC,EACE,EAAA,EAAA,KAAC,EAAD,CACE,QAASb,EAAWE,SAASgE,SAC7B,SAAU7D,GAAkBrB,EAAE,6BAA6B,EAC3D,MAAOiB,GAAejB,EAAE,aAAa,CAAE,CAAA,EAGxCuB,IACC,EAAA,EAAA,MAAC,OAAD,CAAM,UAAU,sBAAsB,SAAUkD,EAAaK,KAb3CC,IAAwC,CAC9D7C,EAAc+C,OAAOF,CAAM,CAC7B,CAW2E,WAArE,EACE,EAAA,EAAA,KAAC,EAAD,CACE,aAAa,oBACb,MAAOJ,EAAO5C,MACd,KAAM5D,EACN,YAAa6B,EAAE,yBAAyB,EACxC,GAAIwE,EAAS,OAAO,EACpB,KAAK,OAAO,CAAA,GAGd,EAAA,EAAA,KAAC,EAAD,CACE,aAAa,mBACb,MAAOG,EAAOlD,SACd,KAAMrD,EACN,YAAa4B,EAAE,4BAA4B,EAC3C,GAAIwE,EAAS,UAAU,EACvB,KAAK,UAAU,CAAA,EAGhBxD,EAAWe,MAAML,UAChB,EAAA,EAAA,KAAC,MAAD,CAAK,UAAU,0CACb,EAAA,EAAA,KAAC,EAAD,CAAM,UAAU,eAAe,GAAG,4BAC/B1B,EAAE,2BAA2B,CAC1B,CAAA,CACH,CAAA,GAGP,EAAA,EAAA,KAAC,EAAD,CACE,UAAU,OACV,UAAWkC,EAAciD,UACzB,YAAanF,EAAE,kBAAkB,WAEhCA,EAAE,cAAc,CACL,CAAA,CACV,IAGPgB,EAAWoE,aAAaC,sBACvB,EAAA,EAAA,KAAC,EAAD,CACE,GAAI7G,EACJ,SAAUwB,EAAE,qBAAqB,EACjC,OAAQT,EAAmBa,CAAM,EACjC,KAAMJ,EAAE,wBAAwB,EAChC,GAAG,WAAW,CAAA,CAaR,GAEhB"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"password-BMEIiDXv.js","names":["RouteErrorFallback","SplitErrorComponent","errorComponent"],"sources":["../../../frontend/src/routes/login/password/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 { createFileRoute, Link, useRouter } from '@tanstack/react-router';\nimport { useCallback, useEffect, useMemo, useRef } 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 { RouteErrorFallback } from '#frontend/components/ui/route-error-fallback.tsx';\nimport { PageLayout } from '#frontend/features/layout/page-layout.tsx';\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 { loginMutationOptions } from '#frontend/queries/login.ts';\nimport { startConditionalPasskeyAuth } from '#frontend/queries/passkey.ts';\nimport {\n type AuthResponse,\n getSessionQueryOptions,\n} from '#frontend/queries/session.ts';\n\nconst SearchSchema = OAuthSearchSchema;\n\nexport const Route = createFileRoute('/login/password/')({\n component: LoginPassword,\n errorComponent: RouteErrorFallback,\n validateSearch: SearchSchema,\n loader: async ({ context }) => {\n await context.queryClient.ensureQueryData(appConfigQueryOptions);\n },\n});\n\nfunction LoginPassword() {\n const { t, i18n } = useTranslation();\n const router = useRouter();\n const queryClient = useQueryClient();\n const search = Route.useSearch();\n const authorizeSearch =\n search.account_selection_state &&\n search.prompt?.split(' ').includes('login')\n ? { ...search, account_selected: '1' as const }\n : search;\n const lang = search.lang ?? i18n.language;\n\n const { data: configData } = useSuspenseQuery(appConfigQueryOptions);\n\n const customTitle =\n configData.branding.title?.[lang] ??\n configData.branding.title?.[configData.i18n.fallback_language];\n const customSubtitle =\n configData.branding.subtitle?.[lang] ??\n configData.branding.subtitle?.[configData.i18n.fallback_language];\n const isPasswordAuthEnabled = configData.auth.password.enabled;\n const isPasskeyEnabled = configData.auth.passkey.enabled;\n\n const loginSchema = useMemo(\n () =>\n z.object({\n email: z.email(t('validation.email.invalid')),\n password: z.string().min(1, t('validation.password.required')),\n }),\n [t],\n );\n\n const loginMutation = useMutation({\n ...loginMutationOptions,\n onSuccess: async (data, params) => {\n const user = data.user;\n\n if (user.email_verification_required && !user.email_verified) {\n router.navigate({\n to: '/verify/email',\n search: {\n email: params.email,\n ...extractOAuthParams(search),\n },\n });\n return;\n }\n\n queryClient.setQueryData(getSessionQueryOptions.queryKey, {\n user: user,\n });\n await tick();\n\n const registered_2fa_methods: SecondFactorMethod[] = [];\n if (configData.auth.password.totp.enabled && user.totp_registered) {\n registered_2fa_methods.push('totp');\n }\n if (configData.auth.passkey.enabled && user.passkey_count > 0) {\n registered_2fa_methods.push('passkey');\n }\n\n if (user.second_factor_required && registered_2fa_methods.length === 0) {\n if (configData.available_2fa_setup_methods.length > 1) {\n return router.navigate({\n to: '/setup/2fa',\n search: extractOAuthParams(search),\n });\n } else if (configData.available_2fa_setup_methods.length === 1) {\n const method = configData.available_2fa_setup_methods[0];\n if (method === 'totp') {\n return router.navigate({\n to: '/setup/totp',\n search: extractOAuthParams(search),\n });\n } else {\n return router.navigate({\n to: '/setup/passkey',\n search: {\n ...extractOAuthParams(search),\n passkey_name: 'default',\n },\n });\n }\n }\n }\n\n if (registered_2fa_methods.length > 1) {\n return router.navigate({\n to: '/verify/2fa',\n search: extractOAuthParams(search),\n });\n } else if (registered_2fa_methods.length === 1) {\n const method = registered_2fa_methods[0];\n if (method === 'totp') {\n return router.navigate({\n to: '/verify/totp',\n search: extractOAuthParams(search),\n });\n } else {\n return router.navigate({\n to: '/verify/passkey',\n search: extractOAuthParams(search),\n });\n }\n } else {\n if (isOAuthFlow(search)) {\n window.location.href = buildAuthorizeUrl(authorizeSearch);\n } else {\n return router.navigate({ to: '/profile' });\n }\n }\n },\n onError: (_error) => {\n setError('email', {\n type: 'manual',\n message: t('login.error.failed'),\n });\n },\n onSettled: () => {\n queryClient.invalidateQueries({\n queryKey: getSessionQueryOptions.queryKey,\n });\n },\n });\n\n const handlePasskeySuccess = useCallback(\n async (data: AuthResponse) => {\n if (data.user) {\n queryClient.setQueryData(getSessionQueryOptions.queryKey, data);\n await tick();\n\n if (isOAuthFlow(search)) {\n window.location.href = buildAuthorizeUrl(authorizeSearch);\n } else {\n router.navigate({ to: '/profile' });\n }\n }\n queryClient.invalidateQueries({\n queryKey: getSessionQueryOptions.queryKey,\n });\n },\n [queryClient, router, search, authorizeSearch],\n );\n\n // Conditional UI: Start passkey autofill on page load\n const abortControllerRef = useRef<AbortController | null>(null);\n\n useEffect(() => {\n if (!isPasskeyEnabled || !isPasswordAuthEnabled) {\n return;\n }\n\n abortControllerRef.current = new AbortController();\n startConditionalPasskeyAuth(\n handlePasskeySuccess,\n abortControllerRef.current.signal,\n );\n\n return () => {\n abortControllerRef.current?.abort();\n };\n }, [isPasskeyEnabled, isPasswordAuthEnabled, handlePasskeySuccess]);\n\n const {\n register,\n setError,\n handleSubmit,\n formState: { errors },\n } = useForm({\n defaultValues: {\n email: '',\n password: '',\n },\n resolver: standardSchemaResolver(loginSchema),\n });\n\n const onSubmit = async (values: z.infer<typeof loginSchema>) => {\n loginMutation.mutate(values);\n };\n\n return (\n <PageLayout cardPadding maxWidth=\"100\">\n <PageHeader\n iconUrl={configData.branding.icon_url}\n subtitle={customSubtitle ?? t('login.selectMethod.subtitle')}\n title={customTitle ?? t('login.title')}\n />\n\n {isPasswordAuthEnabled && (\n <form className=\"flex flex-col gap-4\" onSubmit={handleSubmit(onSubmit)}>\n <IconInput\n autoComplete=\"username webauthn\"\n error={errors.email}\n icon={EnvelopeSimpleIcon}\n placeholder={t('login.email.placeholder')}\n {...register('email')}\n type=\"email\"\n />\n\n <IconInput\n autoComplete=\"current-password\"\n error={errors.password}\n icon={LockIcon}\n placeholder={t('login.password.placeholder')}\n {...register('password')}\n type=\"password\"\n />\n\n {configData.email.enabled && (\n <div className=\"flex items-center justify-end\">\n <Link className=\"link text-sm\" to=\"/password/forgot\">\n {t('login.link.forgotPassword')}\n </Link>\n </div>\n )}\n\n <SubmitButton\n className=\"mt-2\"\n isPending={loginMutation.isPending}\n pendingText={t('login.submitting')}\n >\n {t('login.submit')}\n </SubmitButton>\n </form>\n )}\n\n {configData.registration.public_registration && (\n <FooterLink\n as={Link}\n linkText={t('login.link.register')}\n search={extractOAuthParams(search)}\n text={t('login.footer.noAccount')}\n to=\"/register\"\n />\n )}\n\n {/* <div className=\"flex flex-col items-center gap-2\">\n <Link\n to=\"/login\"\n search={extractOAuthParams(search)}\n className=\"link text-sm\"\n >\n {t('login.password.backToMethods')}\n </Link>\n </div> */}\n </PageLayout>\n );\n}\n"],"mappings":"wCAgBsF,IAAAC,EAA7ED"}
|
|
@@ -1 +0,0 @@
|
|
|
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 +0,0 @@
|
|
|
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 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"totp-DB9WqW4V.js","names":["t","InfoIcon","ShieldCheckIcon","WarningCircleIcon","XCircleIcon","useQueryClient","Link","useRouter","useCallback","useEffect","useState","useTranslation","FooterLink","PageHeader","QrStep","RecoveryCodesStep","VerifyStep","Alert","PageLayout","useTotpSetup","TinyAuthError","buildAuthorizeUrl","extractOAuthParams","isOAuthFlow","tick","getSessionQueryOptions","ERROR_CODES","TOTP_ALREADY_ENABLED","TOTP_NOT_SETUP","INVALID_TOTP_CODE","UNAUTHORIZED","SECOND_FACTOR_SESSION_EXPIRED","const","Route","ErrorType","REDIRECT_COUNTDOWN_SECONDS","SetupTotp","t","router","queryClient","search","useSearch","errorType","setErrorType","redirectCountdown","setRedirectCountdown","redirectToLogin","navigate","to","redirectToProfile","handleSetupError","error","Error","code","handleVerifySuccess","_data","TotpSetupVerifyResponse","handleConfirmSuccess","data","TotpConfirmResponse","setQueryData","queryKey","user","window","location","href","step","setupData","recoveryCodes","isSetupPending","isVerifyPending","isConfirmPending","startSetup","verify","goToQr","goToVerify","confirmRecoveryCodes","autoStart","onSetupError","onVerifySuccess","onConfirmSuccess","onVerifyError","timer","setInterval","prev","clearInterval","handleVerify","seconds","length","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/XCircle.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/XCircle.es.js","../../../frontend/src/routes/setup/totp/index.tsx?tsr-split=component"],"sourcesContent":["import * as e from \"react\";\nconst a = /* @__PURE__ */ new Map([\n [\n \"bold\",\n /* @__PURE__ */ e.createElement(e.Fragment, null, /* @__PURE__ */ e.createElement(\"path\", { d: \"M168.49,104.49,145,128l23.52,23.51a12,12,0,0,1-17,17L128,145l-23.51,23.52a12,12,0,0,1-17-17L111,128,87.51,104.49a12,12,0,0,1,17-17L128,111l23.51-23.52a12,12,0,0,1,17,17ZM236,128A108,108,0,1,1,128,20,108.12,108.12,0,0,1,236,128Zm-24,0a84,84,0,1,0-84,84A84.09,84.09,0,0,0,212,128Z\" }))\n ],\n [\n \"duotone\",\n /* @__PURE__ */ e.createElement(e.Fragment, null, /* @__PURE__ */ e.createElement(\"path\", { d: \"M224,128a96,96,0,1,1-96-96A96,96,0,0,1,224,128Z\", opacity: \"0.2\" }), /* @__PURE__ */ e.createElement(\"path\", { d: \"M165.66,101.66,139.31,128l26.35,26.34a8,8,0,0,1-11.32,11.32L128,139.31l-26.34,26.35a8,8,0,0,1-11.32-11.32L116.69,128,90.34,101.66a8,8,0,0,1,11.32-11.32L128,116.69l26.34-26.35a8,8,0,0,1,11.32,11.32ZM232,128A104,104,0,1,1,128,24,104.11,104.11,0,0,1,232,128Zm-16,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128Z\" }))\n ],\n [\n \"fill\",\n /* @__PURE__ */ e.createElement(e.Fragment, null, /* @__PURE__ */ e.createElement(\"path\", { d: \"M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm37.66,130.34a8,8,0,0,1-11.32,11.32L128,139.31l-26.34,26.35a8,8,0,0,1-11.32-11.32L116.69,128,90.34,101.66a8,8,0,0,1,11.32-11.32L128,116.69l26.34-26.35a8,8,0,0,1,11.32,11.32L139.31,128Z\" }))\n ],\n [\n \"light\",\n /* @__PURE__ */ e.createElement(e.Fragment, null, /* @__PURE__ */ e.createElement(\"path\", { d: \"M164.24,100.24,136.48,128l27.76,27.76a6,6,0,1,1-8.48,8.48L128,136.48l-27.76,27.76a6,6,0,0,1-8.48-8.48L119.52,128,91.76,100.24a6,6,0,0,1,8.48-8.48L128,119.52l27.76-27.76a6,6,0,0,1,8.48,8.48ZM230,128A102,102,0,1,1,128,26,102.12,102.12,0,0,1,230,128Zm-12,0a90,90,0,1,0-90,90A90.1,90.1,0,0,0,218,128Z\" }))\n ],\n [\n \"regular\",\n /* @__PURE__ */ e.createElement(e.Fragment, null, /* @__PURE__ */ e.createElement(\"path\", { d: \"M165.66,101.66,139.31,128l26.35,26.34a8,8,0,0,1-11.32,11.32L128,139.31l-26.34,26.35a8,8,0,0,1-11.32-11.32L116.69,128,90.34,101.66a8,8,0,0,1,11.32-11.32L128,116.69l26.34-26.35a8,8,0,0,1,11.32,11.32ZM232,128A104,104,0,1,1,128,24,104.11,104.11,0,0,1,232,128Zm-16,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128Z\" }))\n ],\n [\n \"thin\",\n /* @__PURE__ */ e.createElement(e.Fragment, null, /* @__PURE__ */ e.createElement(\"path\", { d: \"M162.83,98.83,133.66,128l29.17,29.17a4,4,0,0,1-5.66,5.66L128,133.66,98.83,162.83a4,4,0,0,1-5.66-5.66L122.34,128,93.17,98.83a4,4,0,0,1,5.66-5.66L128,122.34l29.17-29.17a4,4,0,1,1,5.66,5.66ZM228,128A100,100,0,1,1,128,28,100.11,100.11,0,0,1,228,128Zm-8,0a92,92,0,1,0-92,92A92.1,92.1,0,0,0,220,128Z\" }))\n ]\n]);\nexport {\n a as default\n};\n","import * as e from \"react\";\nimport t from \"../lib/IconBase.es.js\";\nimport a from \"../defs/XCircle.es.js\";\nconst o = e.forwardRef((r, c) => /* @__PURE__ */ e.createElement(t, { ref: c, ...r, weights: a }));\no.displayName = \"XCircleIcon\";\nconst s = o;\nexport {\n s as XCircle,\n o as XCircleIcon\n};\n","import {\n InfoIcon,\n ShieldCheckIcon,\n WarningCircleIcon,\n XCircleIcon,\n} from '@phosphor-icons/react';\nimport { useQueryClient } from '@tanstack/react-query';\nimport { createFileRoute, Link, useRouter } from '@tanstack/react-router';\nimport { useCallback, useEffect, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { FooterLink } from '#frontend/components/auth/footer-link.tsx';\nimport { PageHeader } from '#frontend/components/auth/page-header.tsx';\nimport { QrStep } from '#frontend/components/totp/qr-step.tsx';\nimport { RecoveryCodesStep } from '#frontend/components/totp/recovery-codes-step.tsx';\nimport { VerifyStep } from '#frontend/components/totp/verify-step.tsx';\nimport { Alert } from '#frontend/components/ui/alert.tsx';\nimport { PageLayout } from '#frontend/features/layout/page-layout.tsx';\nimport { useTotpSetup } from '#frontend/features/totp/use-totp-setup.ts';\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 type {\n TotpConfirmResponse,\n TotpSetupVerifyResponse,\n} from '#frontend/queries/totp.ts';\n\n/** Error codes from backend */\nconst ERROR_CODES = {\n TOTP_ALREADY_ENABLED: 'TOTP_ALREADY_ENABLED',\n TOTP_NOT_SETUP: 'TOTP_NOT_SETUP',\n INVALID_TOTP_CODE: 'INVALID_TOTP_CODE',\n UNAUTHORIZED: 'UNAUTHORIZED',\n SECOND_FACTOR_SESSION_EXPIRED: 'SECOND_FACTOR_SESSION_EXPIRED',\n} as const;\n\nconst SearchSchema = OAuthSearchSchema;\n\nexport const Route = createFileRoute('/setup/totp/')({\n component: SetupTotp,\n validateSearch: SearchSchema,\n});\n\ntype ErrorType = 'generic' | 'already_enabled' | 'session_expired';\n\n/** Auto redirect countdown seconds */\nconst REDIRECT_COUNTDOWN_SECONDS = 5;\n\nfunction SetupTotp() {\n const { t } = useTranslation();\n const router = useRouter();\n const queryClient = useQueryClient();\n const search = Route.useSearch();\n\n const [errorType, setErrorType] = useState<ErrorType>('generic');\n const [redirectCountdown, setRedirectCountdown] = useState(\n REDIRECT_COUNTDOWN_SECONDS,\n );\n\n const redirectToLogin = useCallback(() => {\n router.navigate({\n to: '/login',\n search: extractOAuthParams(search),\n });\n }, [router, search]);\n\n const redirectToProfile = useCallback(() => {\n router.navigate({ to: '/profile' });\n }, [router]);\n\n const handleSetupError = useCallback((error: Error) => {\n if (error instanceof TinyAuthError) {\n switch (error.code) {\n case ERROR_CODES.TOTP_ALREADY_ENABLED:\n setErrorType('already_enabled');\n break;\n case ERROR_CODES.UNAUTHORIZED:\n case ERROR_CODES.SECOND_FACTOR_SESSION_EXPIRED:\n setErrorType('session_expired');\n setRedirectCountdown(REDIRECT_COUNTDOWN_SECONDS);\n break;\n default:\n setErrorType('generic');\n }\n } else {\n setErrorType('generic');\n }\n }, []);\n\n const handleVerifySuccess = useCallback(\n async (_data: TotpSetupVerifyResponse) => {\n // Recovery codes step is handled by useTotpSetup hook\n // (step transitions to 'recovery' automatically)\n // Session update happens after confirm, not here\n await tick();\n },\n [],\n );\n\n const handleConfirmSuccess = useCallback(\n async (data: TotpConfirmResponse) => {\n queryClient.setQueryData(getSessionQueryOptions.queryKey, {\n user: data.user,\n });\n await tick();\n // Navigate after successful confirm\n if (isOAuthFlow(search)) {\n window.location.href = buildAuthorizeUrl(search);\n } else {\n router.navigate({ to: '/profile' });\n }\n },\n [queryClient, router, search],\n );\n\n const {\n step,\n setupData,\n recoveryCodes,\n isSetupPending,\n isVerifyPending,\n isConfirmPending,\n startSetup,\n verify,\n goToQr,\n goToVerify,\n confirmRecoveryCodes,\n } = useTotpSetup({\n autoStart: true,\n onSetupError: handleSetupError,\n onVerifySuccess: handleVerifySuccess,\n onConfirmSuccess: handleConfirmSuccess,\n onVerifyError: (error) => {\n if (error instanceof TinyAuthError) {\n switch (error.code) {\n case ERROR_CODES.TOTP_ALREADY_ENABLED:\n redirectToProfile();\n break;\n case ERROR_CODES.UNAUTHORIZED:\n case ERROR_CODES.SECOND_FACTOR_SESSION_EXPIRED:\n setErrorType('session_expired');\n setRedirectCountdown(REDIRECT_COUNTDOWN_SECONDS);\n break;\n case ERROR_CODES.TOTP_NOT_SETUP:\n startSetup();\n break;\n }\n }\n },\n });\n\n // Auto redirect when session expires\n useEffect(() => {\n if (errorType !== 'session_expired' || step !== 'error') 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 }, [errorType, step, redirectToLogin]);\n\n const handleVerify = useCallback(\n async (code: string) => {\n await verify(code);\n },\n [verify],\n );\n\n // Loading state\n if (step === 'loading') {\n return (\n <PageLayout cardPadding maxWidth=\"100\">\n <PageHeader\n subtitle={t('setupTotp.subtitle')}\n title={t('setupTotp.title')}\n />\n <div\n className=\"flex justify-center py-8\"\n data-testid=\"totp-setup-loading\"\n >\n <span className=\"loading loading-spinner loading-lg\" />\n </div>\n </PageLayout>\n );\n }\n\n // Error state\n if (step === 'error') {\n // Session expired error - show countdown and redirect\n if (errorType === 'session_expired') {\n return (\n <PageLayout cardPadding maxWidth=\"100\">\n <PageHeader\n subtitle={t('setupTotp.subtitle')}\n title={t('setupTotp.title')}\n />\n <div\n className=\"alert alert-warning mb-4\"\n data-testid=\"totp-setup-session-expired\"\n >\n <WarningCircleIcon className=\"size-5\" weight=\"fill\" />\n <div className=\"flex flex-col gap-1\">\n <span>{t('setupTotp.error.expired')}</span>\n <span className=\"text-sm opacity-80\">\n {t('setupTotp.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('setupTotp.redirectNow')}\n </button>\n </div>\n </div>\n <FooterLink\n as={Link}\n linkText={t('setupTotp.backToLogin')}\n search={extractOAuthParams(search)}\n text=\"\"\n to=\"/login\"\n />\n </PageLayout>\n );\n }\n\n // TOTP already enabled error - redirect to profile\n if (errorType === 'already_enabled') {\n return (\n <PageLayout cardPadding maxWidth=\"100\">\n <PageHeader\n subtitle={t('setupTotp.subtitle')}\n title={t('setupTotp.title')}\n />\n <Alert className=\"mb-4\" icon={InfoIcon} type=\"info\">\n {t('setupTotp.error.alreadyEnabled')}\n </Alert>\n <button\n className=\"btn btn-primary btn-block\"\n onClick={redirectToProfile}\n type=\"button\"\n >\n {t('setupTotp.goToProfile')}\n </button>\n <FooterLink\n as={Link}\n linkText={t('setupTotp.backToLogin')}\n search={extractOAuthParams(search)}\n text=\"\"\n to=\"/login\"\n />\n </PageLayout>\n );\n }\n\n // Generic error - show retry button\n return (\n <PageLayout cardPadding maxWidth=\"100\">\n <PageHeader\n subtitle={t('setupTotp.subtitle')}\n title={t('setupTotp.title')}\n />\n <Alert className=\"mb-4\" icon={XCircleIcon} type=\"error\">\n {t('setupTotp.error.setupFailed')}\n </Alert>\n <button\n className=\"btn btn-primary btn-block\"\n disabled={isSetupPending}\n onClick={startSetup}\n type=\"button\"\n >\n {t('setupTotp.retry')}\n </button>\n <FooterLink\n as={Link}\n linkText={t('setupTotp.backToLogin')}\n search={extractOAuthParams(search)}\n text=\"\"\n to=\"/login\"\n />\n </PageLayout>\n );\n }\n\n // Recovery codes step\n if (step === 'recovery' && recoveryCodes.length > 0) {\n return (\n <PageLayout cardPadding maxWidth=\"100\">\n <PageHeader\n subtitle={t('setupTotp.subtitle')}\n title={t('setupTotp.recoveryCodes.title')}\n />\n <RecoveryCodesStep\n isLoading={isConfirmPending}\n onConfirm={confirmRecoveryCodes}\n recoveryCodes={recoveryCodes}\n />\n </PageLayout>\n );\n }\n\n // QR code step\n if (step === 'qr' && setupData) {\n return (\n <PageLayout cardPadding maxWidth=\"100\">\n <PageHeader\n subtitle={t('setupTotp.subtitle')}\n title={t('setupTotp.title')}\n />\n\n <div className=\"alert alert-info mb-4\">\n <ShieldCheckIcon className=\"size-5\" weight=\"fill\" />\n <span>{t('setupTotp.required')}</span>\n </div>\n\n <QrStep onNext={goToVerify} setupData={setupData} />\n\n <FooterLink\n as={Link}\n linkText={t('setupTotp.backToLogin')}\n search={extractOAuthParams(search)}\n text=\"\"\n to=\"/login\"\n />\n </PageLayout>\n );\n }\n\n // Verify step\n return (\n <PageLayout cardPadding maxWidth=\"100\">\n <PageHeader\n subtitle={t('setupTotp.verifySubtitle')}\n title={t('setupTotp.verifyTitle')}\n />\n\n <VerifyStep\n isPending={isVerifyPending}\n onBack={goToQr}\n onSubmit={handleVerify}\n />\n </PageLayout>\n );\n}\n"],"x_google_ignoreList":[0,1],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"totp-UDT2P91H.js","names":["standardSchemaResolver","WarningCircleIcon","useMutation","useQueryClient","Link","useRouter","useCallback","useEffect","useMemo","useRef","useState","useForm","useTranslation","z","FooterLink","PageHeader","SubmitButton","PinInput","PageLayout","TinyAuthError","buildAuthorizeUrl","extractOAuthParams","isOAuthFlow","tick","getSessionQueryOptions","verifyTotpLoginMutationOptions","ERROR_CODES","SECOND_FACTOR_SESSION_EXPIRED","INVALID_TOTP_CODE","TOTP_NOT_ENABLED","const","Route","VerifyTotpFormValues","code","REDIRECT_COUNTDOWN_SECONDS","VerifyTotp","t","router","queryClient","search","useSearch","pinInputRef","PinInputRef","sessionExpired","setSessionExpired","redirectCountdown","setRedirectCountdown","verifySchema","object","string","length","regex","verifyMutation","onSuccess","data","setQueryData","queryKey","user","window","location","href","navigate","to","onSettled","invalidateQueries","setValue","setError","handleSubmit","watch","formState","errors","defaultValues","resolver","codeValue","redirectToLogin","timer","setInterval","prev","clearInterval","onSubmit","values","mutateAsync","error","type","message","current","focus","seconds","value","isPending","component"],"sources":["../../../frontend/src/routes/verify/totp/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 {\n PinInput,\n type PinInputRef,\n} from '#frontend/components/ui/pin-input.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 { verifyTotpLoginMutationOptions } 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_TOTP_CODE: 'INVALID_TOTP_CODE',\n TOTP_NOT_ENABLED: 'TOTP_NOT_ENABLED',\n} as const;\n\nconst SearchSchema = OAuthSearchSchema;\n\nexport const Route = createFileRoute('/verify/totp/')({\n component: VerifyTotp,\n validateSearch: SearchSchema,\n});\n\ntype VerifyTotpFormValues = {\n code: string;\n};\n\n/** Auto redirect countdown seconds */\nconst REDIRECT_COUNTDOWN_SECONDS = 5;\n\nfunction VerifyTotp() {\n const { t } = useTranslation();\n const router = useRouter();\n const queryClient = useQueryClient();\n const search = Route.useSearch();\n const pinInputRef = useRef<PinInputRef>(null);\n\n const [sessionExpired, setSessionExpired] = useState(false);\n const [redirectCountdown, setRedirectCountdown] = useState(\n REDIRECT_COUNTDOWN_SECONDS,\n );\n\n const verifySchema = useMemo(\n () =>\n z.object({\n code: z\n .string()\n .length(6, t('validation.totp.length'))\n .regex(/^\\d{6}$/, t('validation.totp.digits')),\n }),\n [t],\n );\n\n const verifyMutation = useMutation({\n ...verifyTotpLoginMutationOptions,\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 setValue,\n setError,\n handleSubmit,\n watch,\n formState: { errors },\n } = useForm<VerifyTotpFormValues>({\n defaultValues: {\n code: '',\n },\n resolver: standardSchemaResolver(verifySchema),\n });\n\n const codeValue = watch('code');\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: VerifyTotpFormValues) => {\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 // Session expired - show alert and start auto redirect\n setSessionExpired(true);\n setRedirectCountdown(REDIRECT_COUNTDOWN_SECONDS);\n return;\n\n case ERROR_CODES.TOTP_NOT_ENABLED:\n // TOTP not enabled - redirect to login\n redirectToLogin();\n return;\n\n case ERROR_CODES.INVALID_TOTP_CODE:\n // Invalid code - show specific error and clear input\n setError('code', {\n type: 'manual',\n message: t('verifyTotp.error.invalid'),\n });\n setValue('code', '');\n pinInputRef.current?.focus();\n return;\n }\n }\n\n // Generic error fallback\n setError('code', {\n type: 'manual',\n message: t('verifyTotp.error.invalid'),\n });\n setValue('code', '');\n pinInputRef.current?.focus();\n }\n };\n\n return (\n <PageLayout cardPadding maxWidth=\"100\">\n <PageHeader\n subtitle={t('verifyTotp.subtitle')}\n title={t('verifyTotp.title')}\n />\n\n {sessionExpired && (\n <div\n className=\"alert alert-warning mb-4\"\n data-testid=\"totp-verify-session-expired\"\n >\n <WarningCircleIcon className=\"size-5\" weight=\"fill\" />\n <div className=\"flex flex-col gap-1\">\n <span>{t('verifyTotp.error.expired')}</span>\n <span className=\"text-sm opacity-80\">\n {t('verifyTotp.redirecting', { seconds: redirectCountdown })}\n </span>\n <button\n className=\"btn btn-sm btn-ghost mt-2 w-fit\"\n onClick={redirectToLogin}\n type=\"button\"\n >\n {t('verifyTotp.redirectNow')}\n </button>\n </div>\n </div>\n )}\n\n <form className=\"flex flex-col gap-4\" onSubmit={handleSubmit(onSubmit)}>\n <PinInput\n autoFocus\n disabled={sessionExpired}\n error={errors.code}\n length={6}\n onChange={(value) => setValue('code', value)}\n onComplete={() => handleSubmit(onSubmit)()}\n ref={pinInputRef}\n value={codeValue}\n />\n\n {sessionExpired ? (\n <button\n className=\"btn btn-block btn-disabled mt-2\"\n disabled\n type=\"button\"\n >\n {t('verifyTotp.submit')}\n </button>\n ) : (\n <SubmitButton\n className=\"mt-2\"\n isPending={verifyMutation.isPending}\n pendingText={t('verifyTotp.submitting')}\n >\n {t('verifyTotp.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=\"totp-verify-recovery-link\"\n onClick={() =>\n router.navigate({\n to: '/verify/totp/recovery',\n search: extractOAuthParams(search),\n })\n }\n type=\"button\"\n >\n {t('verifyTotp.useRecoveryCode')}\n </button>\n </div>\n\n <FooterLink\n as={Link}\n linkText={t('verifyTotp.backToLogin')}\n search={extractOAuthParams(search)}\n text=\"\"\n to=\"/login\"\n />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|