@tinyrack/issuary-server 0.24.0 → 0.26.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.
Files changed (144) hide show
  1. package/package.json +1 -1
  2. package/public/assets/{2fa-D3ieXkuh.js → 2fa-C9wUlSBS.js} +2 -2
  3. package/public/assets/{2fa-D3ieXkuh.js.map → 2fa-C9wUlSBS.js.map} +1 -1
  4. package/public/assets/2fa-DFTcWEMb.js +2 -0
  5. package/public/assets/{2fa-6btYq1AL.js.map → 2fa-DFTcWEMb.js.map} +1 -1
  6. package/public/assets/{2fa-Dxh4Kxy-.js → 2fa-TtshNxXv.js} +2 -2
  7. package/public/assets/{2fa-Dxh4Kxy-.js.map → 2fa-TtshNxXv.js.map} +1 -1
  8. package/public/assets/2fa-huNZDYU9.js +2 -0
  9. package/public/assets/{2fa-UcpXAI9J.js.map → 2fa-huNZDYU9.js.map} +1 -1
  10. package/public/assets/DialogHandle-DICBMcjK.js +2 -0
  11. package/public/assets/{DialogHandle-tiMUnjZQ.js.map → DialogHandle-DICBMcjK.js.map} +1 -1
  12. package/public/assets/{admin-B8RQbf4_.js → admin-CFZaO1bM.js} +2 -2
  13. package/public/assets/{admin-B8RQbf4_.js.map → admin-CFZaO1bM.js.map} +1 -1
  14. package/public/assets/{admin-users-Dz1QNdy3.js → admin-users-BK_BN8jm.js} +2 -2
  15. package/public/assets/{admin-users-Dz1QNdy3.js.map → admin-users-BK_BN8jm.js.map} +1 -1
  16. package/public/assets/alert-leqyvHOp.js.map +1 -1
  17. package/public/assets/auth-choice-row-CXuyJgkR.js +2 -0
  18. package/public/assets/{auth-choice-row-DWzfcZQY.js.map → auth-choice-row-CXuyJgkR.js.map} +1 -1
  19. package/public/assets/{auth-field-DsLGh1U-.js → auth-field-CWyXGKoC.js} +2 -2
  20. package/public/assets/{auth-field-DsLGh1U-.js.map → auth-field-CWyXGKoC.js.map} +1 -1
  21. package/public/assets/auth-layout-CiNn2-Nb.js +2 -0
  22. package/public/assets/auth-layout-CiNn2-Nb.js.map +1 -0
  23. package/public/assets/{auth-page-header-COvqWINf.js → auth-page-header-DgGAaIiP.js} +2 -2
  24. package/public/assets/{auth-page-header-COvqWINf.js.map → auth-page-header-DgGAaIiP.js.map} +1 -1
  25. package/public/assets/{authorization-context-banner-DVx1TsPY.js → authorization-context-banner-DznMDPOX.js} +2 -2
  26. package/public/assets/{authorization-context-banner-DVx1TsPY.js.map → authorization-context-banner-DznMDPOX.js.map} +1 -1
  27. package/public/assets/{avatar-BCLugfaZ.js → avatar-D-FRGa1m.js} +2 -2
  28. package/public/assets/{avatar-BCLugfaZ.js.map → avatar-D-FRGa1m.js.map} +1 -1
  29. package/public/assets/badge-CsOfUji0.js.map +1 -1
  30. package/public/assets/checkbox-C90dDDza.js +2 -0
  31. package/public/assets/{checkbox-CFVDS3zJ.js.map → checkbox-C90dDDza.js.map} +1 -1
  32. package/public/assets/config-DTQCUlfZ.js.map +1 -1
  33. package/public/assets/{consent-CKOohdYI.js → consent-B9sQEyO8.js} +2 -2
  34. package/public/assets/{consent-CKOohdYI.js.map → consent-B9sQEyO8.js.map} +1 -1
  35. package/public/assets/{consent-ChiWc7Ql.js → consent-RtCi_85E.js} +2 -2
  36. package/public/assets/{consent-ChiWc7Ql.js.map → consent-RtCi_85E.js.map} +1 -1
  37. package/public/assets/createLucideIcon-BPpAIXR_.js.map +1 -1
  38. package/public/assets/{email-Crjg1dcr.js → email-8RKQzeig.js} +2 -2
  39. package/public/assets/{email-Crjg1dcr.js.map → email-8RKQzeig.js.map} +1 -1
  40. package/public/assets/email-huNZDYU9.js +2 -0
  41. package/public/assets/{email-UcpXAI9J.js.map → email-huNZDYU9.js.map} +1 -1
  42. package/public/assets/{error-VfMv226c.js → error-BYe3gUMO.js} +2 -2
  43. package/public/assets/{error-VfMv226c.js.map → error-BYe3gUMO.js.map} +1 -1
  44. package/public/assets/field-BMT214_m.js +3 -0
  45. package/public/assets/{field-89zKYDVL.js.map → field-BMT214_m.js.map} +1 -1
  46. package/public/assets/{forgot-Dr88fVMq.js → forgot-BenoOUxC.js} +2 -2
  47. package/public/assets/{forgot-Dr88fVMq.js.map → forgot-BenoOUxC.js.map} +1 -1
  48. package/public/assets/forgot-huNZDYU9.js +2 -0
  49. package/public/assets/{forgot-UcpXAI9J.js.map → forgot-huNZDYU9.js.map} +1 -1
  50. package/public/assets/{index-CZ0rWM7l.js → index-CKpnrw9M.js} +3 -3
  51. package/public/assets/index-CKpnrw9M.js.map +1 -0
  52. package/public/assets/{index-CDD8owGK.css → index-Ct9o0oCZ.css} +1 -1
  53. package/public/assets/{initial-avatar-ocwNWL38.js → initial-avatar-E5DK9Igm.js} +2 -2
  54. package/public/assets/{initial-avatar-ocwNWL38.js.map → initial-avatar-E5DK9Igm.js.map} +1 -1
  55. package/public/assets/{input-GPigtjgi.js → input-CxVeC8Pn.js} +2 -2
  56. package/public/assets/{input-GPigtjgi.js.map → input-CxVeC8Pn.js.map} +1 -1
  57. package/public/assets/link-C-WdTj3F.js.map +1 -1
  58. package/public/assets/{login-CQeTLmIu.js → login-1YuiZD30.js} +2 -2
  59. package/public/assets/{login-CQeTLmIu.js.map → login-1YuiZD30.js.map} +1 -1
  60. package/public/assets/login-huNZDYU9.js +2 -0
  61. package/public/assets/{login-UcpXAI9J.js.map → login-huNZDYU9.js.map} +1 -1
  62. package/public/assets/{modal-BV3gpI-p.js → modal-tByFpy18.js} +2 -2
  63. package/public/assets/{modal-BV3gpI-p.js.map → modal-tByFpy18.js.map} +1 -1
  64. package/public/assets/{passkey-CT3G7pCL.js → passkey-CgAHaQ1p.js} +2 -2
  65. package/public/assets/{passkey-CT3G7pCL.js.map → passkey-CgAHaQ1p.js.map} +1 -1
  66. package/public/assets/passkey-huNZDYU9.js +2 -0
  67. package/public/assets/{passkey-UcpXAI9J.js.map → passkey-huNZDYU9.js.map} +1 -1
  68. package/public/assets/{passkey-Bf7n49Z7.js → passkey-v9Pzv-4A.js} +2 -2
  69. package/public/assets/{passkey-Bf7n49Z7.js.map → passkey-v9Pzv-4A.js.map} +1 -1
  70. package/public/assets/{password-BKI5GTRs.js → password-Cuh9Hnjp.js} +2 -2
  71. package/public/assets/{password-BKI5GTRs.js.map → password-Cuh9Hnjp.js.map} +1 -1
  72. package/public/assets/password-huNZDYU9.js +2 -0
  73. package/public/assets/{password-UcpXAI9J.js.map → password-huNZDYU9.js.map} +1 -1
  74. package/public/assets/{password-strength-DTWF07Px.js → password-strength-BIPcWiM_.js} +2 -2
  75. package/public/assets/{password-strength-DTWF07Px.js.map → password-strength-BIPcWiM_.js.map} +1 -1
  76. package/public/assets/pin-input-CITrr-Yc.js +2 -0
  77. package/public/assets/{pin-input-BGOLfkwL.js.map → pin-input-CITrr-Yc.js.map} +1 -1
  78. package/public/assets/{profile-DSTMtoUf.js → profile-BHPL9Px_.js} +2 -2
  79. package/public/assets/{profile-DSTMtoUf.js.map → profile-BHPL9Px_.js.map} +1 -1
  80. package/public/assets/profile-C8KaELVe.js +2 -0
  81. package/public/assets/profile-C8KaELVe.js.map +1 -0
  82. package/public/assets/{recovery-DbgpqG6f.js → recovery-CVROV9nj.js} +2 -2
  83. package/public/assets/{recovery-DbgpqG6f.js.map → recovery-CVROV9nj.js.map} +1 -1
  84. package/public/assets/{register-B2Lixapy.js → register-cpxjX3qr.js} +2 -2
  85. package/public/assets/{register-B2Lixapy.js.map → register-cpxjX3qr.js.map} +1 -1
  86. package/public/assets/register-huNZDYU9.js +2 -0
  87. package/public/assets/{register-UcpXAI9J.js.map → register-huNZDYU9.js.map} +1 -1
  88. package/public/assets/reset-huNZDYU9.js +2 -0
  89. package/public/assets/{reset-UcpXAI9J.js.map → reset-huNZDYU9.js.map} +1 -1
  90. package/public/assets/{reset-C-Qjl2Qy.js → reset-iNqYdsoa.js} +2 -2
  91. package/public/assets/{reset-C-Qjl2Qy.js.map → reset-iNqYdsoa.js.map} +1 -1
  92. package/public/assets/{select-BKx4bF4A.js → select-DdjQtcLJ.js} +2 -2
  93. package/public/assets/{select-BKx4bF4A.js.map → select-DdjQtcLJ.js.map} +1 -1
  94. package/public/assets/select-huNZDYU9.js +2 -0
  95. package/public/assets/{select-UcpXAI9J.js.map → select-huNZDYU9.js.map} +1 -1
  96. package/public/assets/separator-B0Bvh1he.js.map +1 -1
  97. package/public/assets/shell-header-bar-JcFhS7mb.js +2 -0
  98. package/public/assets/shell-header-bar-JcFhS7mb.js.map +1 -0
  99. package/public/assets/{terms-DSTMtoUf.js → terms-BHPL9Px_.js} +2 -2
  100. package/public/assets/{terms-DSTMtoUf.js.map → terms-BHPL9Px_.js.map} +1 -1
  101. package/public/assets/{terms-Vgpg5IZK.js → terms-BjwMWDFp.js} +2 -2
  102. package/public/assets/{terms-Vgpg5IZK.js.map → terms-BjwMWDFp.js.map} +1 -1
  103. package/public/assets/terms-checkbox-list-DNOq4Pb2.js +2 -0
  104. package/public/assets/terms-checkbox-list-DNOq4Pb2.js.map +1 -0
  105. package/public/assets/{totp-Cjns-PoB.js → totp-D_EWG1h8.js} +2 -2
  106. package/public/assets/{totp-Cjns-PoB.js.map → totp-D_EWG1h8.js.map} +1 -1
  107. package/public/assets/{totp-g0-s50HZ.js → totp-DetFwOkk.js} +2 -2
  108. package/public/assets/{totp-g0-s50HZ.js.map → totp-DetFwOkk.js.map} +1 -1
  109. package/public/assets/use-totp-setup-DtlgPUPJ.js +3 -0
  110. package/public/assets/use-totp-setup-DtlgPUPJ.js.map +1 -0
  111. package/public/assets/{useAriaLabelledBy-7uIPXK-0.js → useAriaLabelledBy-IIwdHUss.js} +2 -2
  112. package/public/assets/{useAriaLabelledBy-7uIPXK-0.js.map → useAriaLabelledBy-IIwdHUss.js.map} +1 -1
  113. package/public/assets/useQuery-Ccv_nzQl.js +2 -0
  114. package/public/assets/{useQuery-pOdkMV4B.js.map → useQuery-Ccv_nzQl.js.map} +1 -1
  115. package/public/assets/{users-B1hvVo3B.js → users-B1gvPM_x.js} +2 -2
  116. package/public/assets/{users-B1hvVo3B.js.map → users-B1gvPM_x.js.map} +1 -1
  117. package/public/index.html +2 -2
  118. package/public/assets/2fa-6btYq1AL.js +0 -2
  119. package/public/assets/2fa-UcpXAI9J.js +0 -2
  120. package/public/assets/DialogHandle-tiMUnjZQ.js +0 -2
  121. package/public/assets/auth-choice-row-DWzfcZQY.js +0 -2
  122. package/public/assets/auth-layout-DGTylGtX.js +0 -2
  123. package/public/assets/auth-layout-DGTylGtX.js.map +0 -1
  124. package/public/assets/checkbox-CFVDS3zJ.js +0 -2
  125. package/public/assets/email-UcpXAI9J.js +0 -2
  126. package/public/assets/field-89zKYDVL.js +0 -3
  127. package/public/assets/forgot-UcpXAI9J.js +0 -2
  128. package/public/assets/index-CZ0rWM7l.js.map +0 -1
  129. package/public/assets/login-UcpXAI9J.js +0 -2
  130. package/public/assets/passkey-UcpXAI9J.js +0 -2
  131. package/public/assets/password-UcpXAI9J.js +0 -2
  132. package/public/assets/pin-input-BGOLfkwL.js +0 -2
  133. package/public/assets/profile-RpDmbWNA.js +0 -2
  134. package/public/assets/profile-RpDmbWNA.js.map +0 -1
  135. package/public/assets/register-UcpXAI9J.js +0 -2
  136. package/public/assets/reset-UcpXAI9J.js +0 -2
  137. package/public/assets/select-UcpXAI9J.js +0 -2
  138. package/public/assets/shell-header-bar-CgyNXbPS.js +0 -2
  139. package/public/assets/shell-header-bar-CgyNXbPS.js.map +0 -1
  140. package/public/assets/terms-checkbox-list-DqbXU4qw.js +0 -2
  141. package/public/assets/terms-checkbox-list-DqbXU4qw.js.map +0 -1
  142. package/public/assets/use-totp-setup-CtTaklH-.js +0 -3
  143. package/public/assets/use-totp-setup-CtTaklH-.js.map +0 -1
  144. package/public/assets/useQuery-pOdkMV4B.js +0 -2
@@ -1 +1 @@
1
- {"version":3,"file":"passkey-UcpXAI9J.js","names":["RouteErrorFallback","SetupStep","SplitErrorComponent","errorComponent"],"sources":["../../../frontend/src/routes/setup/passkey/index.tsx?tsr-split=errorComponent"],"sourcesContent":["import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';\nimport {\n useMutation,\n useQueryClient,\n useSuspenseQuery,\n} from '@tanstack/react-query';\nimport { createFileRoute, Link, useRouter } from '@tanstack/react-router';\nimport { TRButton } from '@tinyrack/ui/components/button';\nimport { TRLinkButton } from '@tinyrack/ui/components/link-button';\nimport { TRText } from '@tinyrack/ui/components/text';\nimport {\n CircleAlertIcon,\n FingerprintIcon,\n ShieldCheckIcon,\n TagIcon,\n} from 'lucide-react';\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport { AuthField } from '#frontend/components/auth/auth-field.tsx';\nimport {\n AuthFooter,\n AuthFooterLink,\n} from '#frontend/components/auth/auth-footer.tsx';\nimport { AuthPageHeader } from '#frontend/components/auth/auth-page-header.tsx';\nimport { Alert } from '#frontend/components/ui/alert.tsx';\nimport { RouteErrorFallback } from '#frontend/components/ui/route-error-fallback.tsx';\nimport { AuthLayout } from '#frontend/features/layout/auth-layout.tsx';\nimport {\n buildAuthenticatedAuthorizeUrl,\n extractOAuthParams,\n isOAuthFlow,\n OAuthSearchSchema,\n} from '#frontend/libs/oauth-search.ts';\nimport { classifyPasskeyError } from '#frontend/libs/passkey-error.ts';\nimport { tick } from '#frontend/libs/promise.ts';\nimport { appConfigQueryOptions } from '#frontend/queries/config.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 errorComponent: RouteErrorFallback,\n loader: async ({ context }) => {\n await context.queryClient.ensureQueryData(appConfigQueryOptions);\n },\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 const { data: appConfig } = useSuspenseQuery(appConfigQueryOptions);\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 const canUseTotpSetup =\n appConfig.auth.password.enabled &&\n appConfig.auth.password.two_factor.enrollment_required &&\n appConfig.auth.password.totp.enabled;\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 = buildAuthenticatedAuthorizeUrl(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 const reason = classifyPasskeyError(error);\n\n switch (reason) {\n case 'unsupported':\n setErrorMessage(t('setupPasskey.error.unsupported'));\n return;\n case 'not_allowed':\n setErrorMessage(t('setupPasskey.error.notAllowed'));\n return;\n case 'expired':\n setErrorMessage(t('setupPasskey.error.expired'));\n return;\n case 'user_mismatch':\n case 'verification_failed':\n setErrorMessage(t('setupPasskey.error.failed'));\n return;\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 <AuthLayout>\n <AuthPageHeader\n subtitle={t('setupPasskey.subtitle')}\n title={t('setupPasskey.title')}\n />\n <div className=\"flex flex-col items-center gap-tinyrack-lg py-tinyrack-xl\">\n <FingerprintIcon\n aria-hidden\n className=\"size-tinyrack-3xl animate-pulse text-tinyrack-primary-foreground\"\n />\n <TRText align=\"center\" as=\"p\" color=\"muted\" variant=\"body\">\n {t('setupPasskey.waiting')}\n </TRText>\n </div>\n </AuthLayout>\n );\n }\n\n // Error state\n if (step === 'error') {\n return (\n <AuthLayout>\n <AuthPageHeader\n subtitle={t('setupPasskey.subtitle')}\n title={t('setupPasskey.title')}\n />\n <Alert icon={CircleAlertIcon} type=\"error\">\n {errorMessage}\n </Alert>\n\n <div className=\"flex flex-col gap-tinyrack-sm\">\n {canUseTotpSetup && (\n <TRLinkButton\n className=\"w-full gap-tinyrack-sm\"\n intent=\"primary\"\n render={\n <Link search={extractOAuthParams(search)} to=\"/setup/totp\" />\n }\n uiSize=\"lg\"\n >\n <ShieldCheckIcon aria-hidden className=\"size-tinyrack-xl\" />\n {t('setupPasskey.useTotp')}\n </TRLinkButton>\n )}\n <TRButton\n appearance={canUseTotpSetup ? 'outline' : 'solid'}\n className=\"w-full\"\n intent={canUseTotpSetup ? 'neutral' : 'primary'}\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 uiSize=\"lg\"\n >\n {t('setupPasskey.retry')}\n </TRButton>\n </div>\n\n <AuthFooter>\n <AuthFooterLink\n link={\n <Link search={extractOAuthParams(search)} to=\"/login\">\n {t('setupPasskey.backToLogin')}\n </Link>\n }\n />\n </AuthFooter>\n </AuthLayout>\n );\n }\n\n // Form state\n return (\n <AuthLayout>\n <AuthPageHeader\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 <TRText align=\"center\" as=\"p\" color=\"muted\" variant=\"bodySm\">\n {t('setupPasskey.description')}\n </TRText>\n\n <form\n className=\"flex flex-col gap-tinyrack-lg\"\n onSubmit={handleSubmit(onSubmit)}\n >\n <AuthField\n error={errors.name}\n hint={t('setupPasskey.name.hint')}\n icon={TagIcon}\n id=\"passkey-name\"\n label={t('setupPasskey.name.label')}\n placeholder={t('setupPasskey.name.placeholder')}\n {...register('name')}\n type=\"text\"\n />\n\n <TRButton\n className=\"w-full\"\n intent=\"primary\"\n loading={registerMutation.isPending}\n loadingLabel={t('setupPasskey.registering')}\n type=\"submit\"\n uiSize=\"lg\"\n >\n {t('setupPasskey.continue')}\n </TRButton>\n </form>\n\n <AuthFooter>\n <AuthFooterLink\n link={\n <Link search={extractOAuthParams(search)} to=\"/login\">\n {t('setupPasskey.backToLogin')}\n </Link>\n }\n />\n </AuthFooter>\n </AuthLayout>\n );\n}\n"],"mappings":"wCAsDkD,IAAAE,EA3BzCF"}
1
+ {"version":3,"file":"passkey-huNZDYU9.js","names":["RouteErrorFallback","SetupStep","SplitErrorComponent","errorComponent"],"sources":["../../../frontend/src/routes/setup/passkey/index.tsx?tsr-split=errorComponent"],"sourcesContent":["import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';\nimport {\n useMutation,\n useQueryClient,\n useSuspenseQuery,\n} from '@tanstack/react-query';\nimport { createFileRoute, Link, useRouter } from '@tanstack/react-router';\nimport { TRButton } from '@tinyrack/ui/components/button';\nimport { TRLinkButton } from '@tinyrack/ui/components/link-button';\nimport { TRText } from '@tinyrack/ui/components/text';\nimport {\n CircleAlertIcon,\n FingerprintIcon,\n ShieldCheckIcon,\n TagIcon,\n} from 'lucide-react';\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport { AuthField } from '#frontend/components/auth/auth-field.tsx';\nimport {\n AuthFooter,\n AuthFooterLink,\n} from '#frontend/components/auth/auth-footer.tsx';\nimport { AuthPageHeader } from '#frontend/components/auth/auth-page-header.tsx';\nimport { Alert } from '#frontend/components/ui/alert.tsx';\nimport { RouteErrorFallback } from '#frontend/components/ui/route-error-fallback.tsx';\nimport { AuthLayout } from '#frontend/features/layout/auth-layout.tsx';\nimport {\n buildAuthenticatedAuthorizeUrl,\n extractOAuthParams,\n isOAuthFlow,\n OAuthSearchSchema,\n} from '#frontend/libs/oauth-search.ts';\nimport { classifyPasskeyError } from '#frontend/libs/passkey-error.ts';\nimport { tick } from '#frontend/libs/promise.ts';\nimport { appConfigQueryOptions } from '#frontend/queries/config.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 errorComponent: RouteErrorFallback,\n loader: async ({ context }) => {\n await context.queryClient.ensureQueryData(appConfigQueryOptions);\n },\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 const { data: appConfig } = useSuspenseQuery(appConfigQueryOptions);\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 const canUseTotpSetup =\n appConfig.auth.password.enabled &&\n appConfig.auth.password.two_factor.enrollment_required &&\n appConfig.auth.password.totp.enabled;\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 = buildAuthenticatedAuthorizeUrl(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 const reason = classifyPasskeyError(error);\n\n switch (reason) {\n case 'unsupported':\n setErrorMessage(t('setupPasskey.error.unsupported'));\n return;\n case 'not_allowed':\n setErrorMessage(t('setupPasskey.error.notAllowed'));\n return;\n case 'expired':\n setErrorMessage(t('setupPasskey.error.expired'));\n return;\n case 'user_mismatch':\n case 'verification_failed':\n setErrorMessage(t('setupPasskey.error.failed'));\n return;\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 <AuthLayout>\n <AuthPageHeader\n subtitle={t('setupPasskey.subtitle')}\n title={t('setupPasskey.title')}\n />\n <div className=\"flex flex-col items-center gap-tinyrack-lg py-tinyrack-xl\">\n <FingerprintIcon\n aria-hidden\n className=\"size-tinyrack-3xl animate-pulse text-tinyrack-primary-foreground\"\n />\n <TRText align=\"center\" as=\"p\" color=\"muted\" variant=\"body\">\n {t('setupPasskey.waiting')}\n </TRText>\n </div>\n </AuthLayout>\n );\n }\n\n // Error state\n if (step === 'error') {\n return (\n <AuthLayout>\n <AuthPageHeader\n subtitle={t('setupPasskey.subtitle')}\n title={t('setupPasskey.title')}\n />\n <Alert icon={CircleAlertIcon} type=\"error\">\n {errorMessage}\n </Alert>\n\n <div className=\"flex flex-col gap-tinyrack-sm\">\n {canUseTotpSetup && (\n <TRLinkButton\n className=\"w-full gap-tinyrack-sm\"\n intent=\"primary\"\n render={\n <Link search={extractOAuthParams(search)} to=\"/setup/totp\" />\n }\n uiSize=\"lg\"\n >\n <ShieldCheckIcon aria-hidden className=\"size-tinyrack-xl\" />\n {t('setupPasskey.useTotp')}\n </TRLinkButton>\n )}\n <TRButton\n appearance={canUseTotpSetup ? 'outline' : 'solid'}\n className=\"w-full\"\n intent={canUseTotpSetup ? 'neutral' : 'primary'}\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 uiSize=\"lg\"\n >\n {t('setupPasskey.retry')}\n </TRButton>\n </div>\n\n <AuthFooter>\n <AuthFooterLink\n link={\n <Link search={extractOAuthParams(search)} to=\"/login\">\n {t('setupPasskey.backToLogin')}\n </Link>\n }\n />\n </AuthFooter>\n </AuthLayout>\n );\n }\n\n // Form state\n return (\n <AuthLayout>\n <AuthPageHeader\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 <TRText align=\"center\" as=\"p\" color=\"muted\" variant=\"bodySm\">\n {t('setupPasskey.description')}\n </TRText>\n\n <form\n className=\"flex flex-col gap-tinyrack-lg\"\n onSubmit={handleSubmit(onSubmit)}\n >\n <AuthField\n error={errors.name}\n hint={t('setupPasskey.name.hint')}\n icon={TagIcon}\n id=\"passkey-name\"\n label={t('setupPasskey.name.label')}\n placeholder={t('setupPasskey.name.placeholder')}\n {...register('name')}\n type=\"text\"\n />\n\n <TRButton\n className=\"w-full\"\n intent=\"primary\"\n loading={registerMutation.isPending}\n loadingLabel={t('setupPasskey.registering')}\n type=\"submit\"\n uiSize=\"lg\"\n >\n {t('setupPasskey.continue')}\n </TRButton>\n </form>\n\n <AuthFooter>\n <AuthFooterLink\n link={\n <Link search={extractOAuthParams(search)} to=\"/login\">\n {t('setupPasskey.backToLogin')}\n </Link>\n }\n />\n </AuthFooter>\n </AuthLayout>\n );\n}\n"],"mappings":"wCAsDkD,IAAAE,EA3BzCF"}
@@ -1,2 +1,2 @@
1
- import{J as e,U as t,Z as n,i as r,q as i,s as a}from"./createLucideIcon-BPpAIXR_.js";import{On as o,Sn as s,Sr as c,i as l,n as u,r as d,ur as f}from"./config-DTQCUlfZ.js";import{t as p}from"./useMutation-BWZ0G4Td.js";import{t as m}from"./useQuery-pOdkMV4B.js";import{t as h}from"./fingerprint-pattern-DP62Vk8x.js";import{F as g,N as _,R as v,S as y,j as b,m as x,z as S}from"./index-CZ0rWM7l.js";import{t as C}from"./auth-page-header-COvqWINf.js";import{t as w}from"./alert-leqyvHOp.js";import{t as T}from"./auth-layout-DGTylGtX.js";import{t as E}from"./promise-OpBtq8tG.js";import{n as D,t as O}from"./auth-footer-Cz5ivsDc.js";import{t as k}from"./passkey-error-ZqqPQUta.js";import{t as A}from"./passkey-J1qnYp9A.js";var j=n(e()),M=f({queryKey:u.pendingSecondFactorMethods(),queryFn:async()=>l(await d.api.auth[`2fa`].methods.$get()),retry:!1}),N=i();function P(){let{t:e}=r(),n=o(),i=c(),l=x.useSearch(),[u,d]=(0,j.useState)(null),f=(0,j.useRef)(!1),P=m({...M,enabled:u!==null}).data?.methods.includes(`totp`)??!1,F=t=>{switch(t){case`unsupported`:return e(`verifyPasskey.error.unsupported`);case`not_allowed`:return e(`verifyPasskey.error.notAllowed`);case`expired`:return e(`verifyPasskey.error.expired`);case`user_mismatch`:return e(`verifyPasskey.error.userMismatch`);case`verification_failed`:return e(`verifyPasskey.error.failed`)}},I=p({...A,onSuccess:async e=>{e.user&&(i.setQueryData(y.queryKey,{user:e.user}),await E(),g(l)?window.location.href=b(l):n.navigate({to:`/profile`}))},onError:async e=>{let t=k(e);d({message:F(t),reason:t})},onSettled:()=>{i.invalidateQueries({queryKey:y.queryKey})}});return(0,j.useEffect)(()=>{f.current||(f.current=!0,I.mutate())},[I]),(0,N.jsxs)(T,{children:[(0,N.jsx)(C,{align:`center`,subtitle:e(`verifyPasskey.subtitle`),title:e(`verifyPasskey.title`)}),(0,N.jsxs)(`div`,{className:`flex flex-col items-center gap-tinyrack-lg`,children:[(0,N.jsx)(`div`,{className:`flex size-tinyrack-3xl items-center justify-center rounded-tinyrack-full bg-tinyrack-surface-muted`,children:(0,N.jsx)(h,{"aria-hidden":!0,className:`size-tinyrack-2xl text-tinyrack-text-muted`})}),I.isPending&&(0,N.jsxs)(`div`,{className:`flex flex-col items-center gap-tinyrack-sm`,children:[(0,N.jsx)(t,{uiSize:`md`}),(0,N.jsx)(`p`,{className:`text-center text-tinyrack-sm text-tinyrack-text-muted`,children:e(`verifyPasskey.waiting`)})]}),u&&(0,N.jsxs)(`div`,{className:`flex w-full flex-col items-center gap-tinyrack-md`,children:[(0,N.jsx)(w,{className:`w-full`,icon:v,type:`error`,children:u.message}),P&&(0,N.jsx)(S,{className:`w-full`,intent:`primary`,render:(0,N.jsx)(s,{search:_(l),to:`/verify/totp`}),uiSize:`lg`,children:e(`verifyPasskey.useTotp`)}),(0,N.jsx)(a,{appearance:P?`outline`:`solid`,className:`w-full`,intent:P?`neutral`:`primary`,onClick:()=>{d(null),I.mutate()},type:`button`,uiSize:`lg`,children:e(`verifyPasskey.retry`)})]})]}),(0,N.jsx)(O,{children:(0,N.jsx)(D,{link:(0,N.jsx)(s,{search:l,to:`/login`,children:e(`verifyPasskey.backToLogin`)})})})]})}export{P as component};
2
- //# sourceMappingURL=passkey-Bf7n49Z7.js.map
1
+ import{J as e,U as t,Z as n,i as r,q as i,s as a}from"./createLucideIcon-BPpAIXR_.js";import{On as o,Sn as s,Sr as c,i as l,n as u,r as d,ur as f}from"./config-DTQCUlfZ.js";import{t as p}from"./useMutation-BWZ0G4Td.js";import{t as m}from"./useQuery-Ccv_nzQl.js";import{t as h}from"./fingerprint-pattern-DP62Vk8x.js";import{F as g,N as _,R as v,S as y,j as b,m as x,z as S}from"./index-CKpnrw9M.js";import{t as C}from"./auth-page-header-DgGAaIiP.js";import{t as w}from"./alert-leqyvHOp.js";import{t as T}from"./auth-layout-CiNn2-Nb.js";import{t as E}from"./promise-OpBtq8tG.js";import{n as D,t as O}from"./auth-footer-Cz5ivsDc.js";import{t as k}from"./passkey-error-ZqqPQUta.js";import{t as A}from"./passkey-J1qnYp9A.js";var j=n(e()),M=f({queryKey:u.pendingSecondFactorMethods(),queryFn:async()=>l(await d.api.auth[`2fa`].methods.$get()),retry:!1}),N=i();function P(){let{t:e}=r(),n=o(),i=c(),l=x.useSearch(),[u,d]=(0,j.useState)(null),f=(0,j.useRef)(!1),P=m({...M,enabled:u!==null}).data?.methods.includes(`totp`)??!1,F=t=>{switch(t){case`unsupported`:return e(`verifyPasskey.error.unsupported`);case`not_allowed`:return e(`verifyPasskey.error.notAllowed`);case`expired`:return e(`verifyPasskey.error.expired`);case`user_mismatch`:return e(`verifyPasskey.error.userMismatch`);case`verification_failed`:return e(`verifyPasskey.error.failed`)}},I=p({...A,onSuccess:async e=>{e.user&&(i.setQueryData(y.queryKey,{user:e.user}),await E(),g(l)?window.location.href=b(l):n.navigate({to:`/profile`}))},onError:async e=>{let t=k(e);d({message:F(t),reason:t})},onSettled:()=>{i.invalidateQueries({queryKey:y.queryKey})}});return(0,j.useEffect)(()=>{f.current||(f.current=!0,I.mutate())},[I]),(0,N.jsxs)(T,{children:[(0,N.jsx)(C,{align:`center`,subtitle:e(`verifyPasskey.subtitle`),title:e(`verifyPasskey.title`)}),(0,N.jsxs)(`div`,{className:`flex flex-col items-center gap-tinyrack-lg`,children:[(0,N.jsx)(`div`,{className:`flex size-tinyrack-3xl items-center justify-center rounded-tinyrack-full bg-tinyrack-surface-muted`,children:(0,N.jsx)(h,{"aria-hidden":!0,className:`size-tinyrack-2xl text-tinyrack-text-muted`})}),I.isPending&&(0,N.jsxs)(`div`,{className:`flex flex-col items-center gap-tinyrack-sm`,children:[(0,N.jsx)(t,{uiSize:`md`}),(0,N.jsx)(`p`,{className:`text-center text-tinyrack-sm text-tinyrack-text-muted`,children:e(`verifyPasskey.waiting`)})]}),u&&(0,N.jsxs)(`div`,{className:`flex w-full flex-col items-center gap-tinyrack-md`,children:[(0,N.jsx)(w,{className:`w-full`,icon:v,type:`error`,children:u.message}),P&&(0,N.jsx)(S,{className:`w-full`,intent:`primary`,render:(0,N.jsx)(s,{search:_(l),to:`/verify/totp`}),uiSize:`lg`,children:e(`verifyPasskey.useTotp`)}),(0,N.jsx)(a,{appearance:P?`outline`:`solid`,className:`w-full`,intent:P?`neutral`:`primary`,onClick:()=>{d(null),I.mutate()},type:`button`,uiSize:`lg`,children:e(`verifyPasskey.retry`)})]})]}),(0,N.jsx)(O,{children:(0,N.jsx)(D,{link:(0,N.jsx)(s,{search:l,to:`/login`,children:e(`verifyPasskey.backToLogin`)})})})]})}export{P as component};
2
+ //# sourceMappingURL=passkey-v9Pzv-4A.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"passkey-Bf7n49Z7.js","names":["useMutation","useQuery","useQueryClient","Link","useRouter","TRButton","TRLinkButton","TRSpinner","CircleAlertIcon","FingerprintIcon","useEffect","useRef","useState","useTranslation","AuthFooter","AuthFooterLink","AuthPageHeader","Alert","AuthLayout","buildAuthenticatedAuthorizeUrl","extractOAuthParams","isOAuthFlow","classifyPasskeyError","tick","authenticateWithPasskeyMutationOptions","getPendingSecondFactorMethodsQueryOptions","getSessionQueryOptions","Route","VerifyPasskey","t","router","queryClient","search","useSearch","error","setError","message","reason","PasskeyErrorReason","hasStarted","pendingMethodsQuery","enabled","canUseTotp","data","methods","includes","getErrorMessage","verifyMutation","onSuccess","user","setQueryData","queryKey","window","location","href","navigate","to","onError","err","onSettled","invalidateQueries","current","mutate","isPending","component"],"sources":["../../../frontend/src/queries/second-factor.ts","../../../frontend/src/routes/verify/passkey/index.tsx?tsr-split=component"],"sourcesContent":["import { queryOptions } from '@tanstack/react-query';\nimport type { InferResponseType } from 'hono/client';\nimport { client, jsonOk } from '#frontend/libs/api.ts';\nimport { queryKeys } from './keys';\n\nexport type PendingSecondFactorMethodsResponse = InferResponseType<\n (typeof client.api.auth)['2fa']['methods']['$get'],\n 200\n>;\n\nexport const getPendingSecondFactorMethodsQueryOptions = queryOptions({\n queryKey: queryKeys.pendingSecondFactorMethods(),\n queryFn: async () => {\n const res = await client.api.auth['2fa'].methods.$get();\n return jsonOk(res);\n },\n retry: false,\n});\n","import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\nimport { createFileRoute, Link, useRouter } from '@tanstack/react-router';\nimport { TRButton } from '@tinyrack/ui/components/button';\nimport { TRLinkButton } from '@tinyrack/ui/components/link-button';\nimport { TRSpinner } from '@tinyrack/ui/components/spinner';\nimport { CircleAlertIcon, FingerprintIcon } from 'lucide-react';\nimport { useEffect, useRef, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport {\n AuthFooter,\n AuthFooterLink,\n} from '#frontend/components/auth/auth-footer.tsx';\nimport { AuthPageHeader } from '#frontend/components/auth/auth-page-header.tsx';\nimport { Alert } from '#frontend/components/ui/alert.tsx';\nimport { AuthLayout } from '#frontend/features/layout/auth-layout.tsx';\nimport {\n buildAuthenticatedAuthorizeUrl,\n extractOAuthParams,\n isOAuthFlow,\n OAuthSearchSchema,\n} from '#frontend/libs/oauth-search.ts';\nimport {\n classifyPasskeyError,\n type PasskeyErrorReason,\n} from '#frontend/libs/passkey-error.ts';\nimport { tick } from '#frontend/libs/promise.ts';\nimport { authenticateWithPasskeyMutationOptions } from '#frontend/queries/passkey.ts';\nimport { getPendingSecondFactorMethodsQueryOptions } from '#frontend/queries/second-factor.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<{\n message: string;\n reason: PasskeyErrorReason;\n } | null>(null);\n const hasStarted = useRef(false);\n const pendingMethodsQuery = useQuery({\n ...getPendingSecondFactorMethodsQueryOptions,\n enabled: error !== null,\n });\n const canUseTotp =\n pendingMethodsQuery.data?.methods.includes('totp') ?? false;\n\n const getErrorMessage = (reason: PasskeyErrorReason) => {\n switch (reason) {\n case 'unsupported':\n return t('verifyPasskey.error.unsupported');\n case 'not_allowed':\n return t('verifyPasskey.error.notAllowed');\n case 'expired':\n return t('verifyPasskey.error.expired');\n case 'user_mismatch':\n return t('verifyPasskey.error.userMismatch');\n case 'verification_failed':\n return t('verifyPasskey.error.failed');\n }\n };\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 = buildAuthenticatedAuthorizeUrl(search);\n } else {\n router.navigate({ to: '/profile' });\n }\n }\n },\n onError: async (err) => {\n const reason = classifyPasskeyError(err);\n setError({\n message: getErrorMessage(reason),\n reason,\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 <AuthLayout>\n {/*\n Centred: this screen has no form to scan, only a device prompt to wait\n on, so the eye has nothing to follow down a left edge.\n */}\n <AuthPageHeader\n align=\"center\"\n subtitle={t('verifyPasskey.subtitle')}\n title={t('verifyPasskey.title')}\n />\n\n <div className=\"flex flex-col items-center gap-tinyrack-lg\">\n <div className=\"flex size-tinyrack-3xl items-center justify-center rounded-tinyrack-full bg-tinyrack-surface-muted\">\n <FingerprintIcon\n aria-hidden\n className=\"size-tinyrack-2xl text-tinyrack-text-muted\"\n />\n </div>\n\n {verifyMutation.isPending && (\n <div className=\"flex flex-col items-center gap-tinyrack-sm\">\n <TRSpinner uiSize=\"md\" />\n <p className=\"text-center text-tinyrack-sm text-tinyrack-text-muted\">\n {t('verifyPasskey.waiting')}\n </p>\n </div>\n )}\n\n {error && (\n <div className=\"flex w-full flex-col items-center gap-tinyrack-md\">\n <Alert className=\"w-full\" icon={CircleAlertIcon} type=\"error\">\n {error.message}\n </Alert>\n {canUseTotp && (\n <TRLinkButton\n className=\"w-full\"\n intent=\"primary\"\n render={\n <Link search={extractOAuthParams(search)} to=\"/verify/totp\" />\n }\n uiSize=\"lg\"\n >\n {t('verifyPasskey.useTotp')}\n </TRLinkButton>\n )}\n <TRButton\n appearance={canUseTotp ? 'outline' : 'solid'}\n className=\"w-full\"\n intent={canUseTotp ? 'neutral' : 'primary'}\n onClick={() => {\n setError(null);\n verifyMutation.mutate();\n }}\n type=\"button\"\n uiSize=\"lg\"\n >\n {t('verifyPasskey.retry')}\n </TRButton>\n </div>\n )}\n </div>\n\n <AuthFooter>\n <AuthFooterLink\n link={\n <Link search={search} to=\"/login\">\n {t('verifyPasskey.backToLogin')}\n </Link>\n }\n />\n </AuthFooter>\n </AuthLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"passkey-v9Pzv-4A.js","names":["useMutation","useQuery","useQueryClient","Link","useRouter","TRButton","TRLinkButton","TRSpinner","CircleAlertIcon","FingerprintIcon","useEffect","useRef","useState","useTranslation","AuthFooter","AuthFooterLink","AuthPageHeader","Alert","AuthLayout","buildAuthenticatedAuthorizeUrl","extractOAuthParams","isOAuthFlow","classifyPasskeyError","tick","authenticateWithPasskeyMutationOptions","getPendingSecondFactorMethodsQueryOptions","getSessionQueryOptions","Route","VerifyPasskey","t","router","queryClient","search","useSearch","error","setError","message","reason","PasskeyErrorReason","hasStarted","pendingMethodsQuery","enabled","canUseTotp","data","methods","includes","getErrorMessage","verifyMutation","onSuccess","user","setQueryData","queryKey","window","location","href","navigate","to","onError","err","onSettled","invalidateQueries","current","mutate","isPending","component"],"sources":["../../../frontend/src/queries/second-factor.ts","../../../frontend/src/routes/verify/passkey/index.tsx?tsr-split=component"],"sourcesContent":["import { queryOptions } from '@tanstack/react-query';\nimport type { InferResponseType } from 'hono/client';\nimport { client, jsonOk } from '#frontend/libs/api.ts';\nimport { queryKeys } from './keys';\n\nexport type PendingSecondFactorMethodsResponse = InferResponseType<\n (typeof client.api.auth)['2fa']['methods']['$get'],\n 200\n>;\n\nexport const getPendingSecondFactorMethodsQueryOptions = queryOptions({\n queryKey: queryKeys.pendingSecondFactorMethods(),\n queryFn: async () => {\n const res = await client.api.auth['2fa'].methods.$get();\n return jsonOk(res);\n },\n retry: false,\n});\n","import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\nimport { createFileRoute, Link, useRouter } from '@tanstack/react-router';\nimport { TRButton } from '@tinyrack/ui/components/button';\nimport { TRLinkButton } from '@tinyrack/ui/components/link-button';\nimport { TRSpinner } from '@tinyrack/ui/components/spinner';\nimport { CircleAlertIcon, FingerprintIcon } from 'lucide-react';\nimport { useEffect, useRef, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport {\n AuthFooter,\n AuthFooterLink,\n} from '#frontend/components/auth/auth-footer.tsx';\nimport { AuthPageHeader } from '#frontend/components/auth/auth-page-header.tsx';\nimport { Alert } from '#frontend/components/ui/alert.tsx';\nimport { AuthLayout } from '#frontend/features/layout/auth-layout.tsx';\nimport {\n buildAuthenticatedAuthorizeUrl,\n extractOAuthParams,\n isOAuthFlow,\n OAuthSearchSchema,\n} from '#frontend/libs/oauth-search.ts';\nimport {\n classifyPasskeyError,\n type PasskeyErrorReason,\n} from '#frontend/libs/passkey-error.ts';\nimport { tick } from '#frontend/libs/promise.ts';\nimport { authenticateWithPasskeyMutationOptions } from '#frontend/queries/passkey.ts';\nimport { getPendingSecondFactorMethodsQueryOptions } from '#frontend/queries/second-factor.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<{\n message: string;\n reason: PasskeyErrorReason;\n } | null>(null);\n const hasStarted = useRef(false);\n const pendingMethodsQuery = useQuery({\n ...getPendingSecondFactorMethodsQueryOptions,\n enabled: error !== null,\n });\n const canUseTotp =\n pendingMethodsQuery.data?.methods.includes('totp') ?? false;\n\n const getErrorMessage = (reason: PasskeyErrorReason) => {\n switch (reason) {\n case 'unsupported':\n return t('verifyPasskey.error.unsupported');\n case 'not_allowed':\n return t('verifyPasskey.error.notAllowed');\n case 'expired':\n return t('verifyPasskey.error.expired');\n case 'user_mismatch':\n return t('verifyPasskey.error.userMismatch');\n case 'verification_failed':\n return t('verifyPasskey.error.failed');\n }\n };\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 = buildAuthenticatedAuthorizeUrl(search);\n } else {\n router.navigate({ to: '/profile' });\n }\n }\n },\n onError: async (err) => {\n const reason = classifyPasskeyError(err);\n setError({\n message: getErrorMessage(reason),\n reason,\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 <AuthLayout>\n {/*\n Centred: this screen has no form to scan, only a device prompt to wait\n on, so the eye has nothing to follow down a left edge.\n */}\n <AuthPageHeader\n align=\"center\"\n subtitle={t('verifyPasskey.subtitle')}\n title={t('verifyPasskey.title')}\n />\n\n <div className=\"flex flex-col items-center gap-tinyrack-lg\">\n <div className=\"flex size-tinyrack-3xl items-center justify-center rounded-tinyrack-full bg-tinyrack-surface-muted\">\n <FingerprintIcon\n aria-hidden\n className=\"size-tinyrack-2xl text-tinyrack-text-muted\"\n />\n </div>\n\n {verifyMutation.isPending && (\n <div className=\"flex flex-col items-center gap-tinyrack-sm\">\n <TRSpinner uiSize=\"md\" />\n <p className=\"text-center text-tinyrack-sm text-tinyrack-text-muted\">\n {t('verifyPasskey.waiting')}\n </p>\n </div>\n )}\n\n {error && (\n <div className=\"flex w-full flex-col items-center gap-tinyrack-md\">\n <Alert className=\"w-full\" icon={CircleAlertIcon} type=\"error\">\n {error.message}\n </Alert>\n {canUseTotp && (\n <TRLinkButton\n className=\"w-full\"\n intent=\"primary\"\n render={\n <Link search={extractOAuthParams(search)} to=\"/verify/totp\" />\n }\n uiSize=\"lg\"\n >\n {t('verifyPasskey.useTotp')}\n </TRLinkButton>\n )}\n <TRButton\n appearance={canUseTotp ? 'outline' : 'solid'}\n className=\"w-full\"\n intent={canUseTotp ? 'neutral' : 'primary'}\n onClick={() => {\n setError(null);\n verifyMutation.mutate();\n }}\n type=\"button\"\n uiSize=\"lg\"\n >\n {t('verifyPasskey.retry')}\n </TRButton>\n </div>\n )}\n </div>\n\n <AuthFooter>\n <AuthFooterLink\n link={\n <Link search={search} to=\"/login\">\n {t('verifyPasskey.backToLogin')}\n </Link>\n }\n />\n </AuthFooter>\n </AuthLayout>\n );\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import{J as e,Z as t,i as n,q as r,s as i}from"./createLucideIcon-BPpAIXR_.js";import{On as a,Sn as o,Sr as s,i as c,r as l,t as u}from"./config-DTQCUlfZ.js";import{t as d}from"./mutationOptions-DyYFUbyn.js";import{t as f}from"./useMutation-BWZ0G4Td.js";import{tt as p}from"./shell-header-bar-CgyNXbPS.js";import{t as m}from"./auth-field-DsLGh1U-.js";import{t as h}from"./authorization-context-banner-DVx1TsPY.js";import{t as g}from"./lock-7NRyvXBS.js";import{t as _}from"./mail-D0B_1Dzd.js";import{c as v,i as y,o as b}from"./zod-C87S6Tec.js";import{F as x,N as S,S as C,j as w,s as T}from"./index-CZ0rWM7l.js";import{t as E}from"./auth-page-header-COvqWINf.js";import{t as D}from"./auth-layout-DGTylGtX.js";import{t as O}from"./promise-OpBtq8tG.js";import{t as k}from"./link-C-WdTj3F.js";import{n as A,t as j}from"./auth-footer-Cz5ivsDc.js";import{o as M}from"./passkey-J1qnYp9A.js";import{r as N,t as P}from"./standard-schema-e8gtSzGE.js";var F=t(e()),I=d({mutationFn:async e=>c(await l.api.auth.login.$post({json:e}))}),L=r();function R(){let{t:e,i18n:t}=n(),r=a(),c=s(),l=T.useSearch(),d=l.account_selection_state&&l.prompt?.split(` `).includes(`login`)?{...l,account_selected:1}:l,R=l.lang??t.language,{data:z}=p(u),B=z.registration.signup_notice?.[R]??z.registration.signup_notice?.[z.i18n.fallback_language],V=z.auth.password.enabled,H=z.auth.passkey.enabled,U=z.identity_providers.length+ +!!V+ +!!H>1,W=(0,F.useMemo)(()=>b({email:v().min(1,e(`validation.email.required`)).pipe(y(e(`validation.email.invalid`))),password:v().min(1,e(`validation.password.required`))}),[e]),G=f({...I,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(l)}});return}c.setQueryData(C.queryKey,{user:n}),await O();let i=[];if(z.auth.password.totp.enabled&&n.totp_registered&&i.push(`totp`),z.auth.passkey.enabled&&n.passkey_count>0&&i.push(`passkey`),n.second_factor_required&&i.length===0){if(z.available_2fa_setup_methods.length>1)return r.navigate({to:`/setup/2fa`,search:S(l)});if(z.available_2fa_setup_methods.length===1)return z.available_2fa_setup_methods[0]===`totp`?r.navigate({to:`/setup/totp`,search:S(l)}):r.navigate({to:`/setup/passkey`,search:{...S(l),passkey_name:`default`}})}if(i.length>1)return r.navigate({to:`/verify/2fa`,search:S(l)});if(i.length===1)return i[0]===`totp`?r.navigate({to:`/verify/totp`,search:S(l)}):r.navigate({to:`/verify/passkey`,search:S(l)});if(x(l))window.location.href=w(d);else return r.navigate({to:`/profile`})},onError:t=>{Y(`email`,{type:`manual`,message:e(`login.error.failed`)})},onSettled:()=>{c.invalidateQueries({queryKey:C.queryKey})}}),K=(0,F.useCallback)(async e=>{e.user&&(c.setQueryData(C.queryKey,e),await O(),x(l)?window.location.href=w(d):r.navigate({to:`/profile`})),c.invalidateQueries({queryKey:C.queryKey})},[c,r,l,d]),q=(0,F.useRef)(null);(0,F.useEffect)(()=>{if(!(!H||!V))return q.current=new AbortController,M(K,q.current.signal),()=>{q.current?.abort()}},[H,V,K]);let{register:J,setError:Y,handleSubmit:X,formState:{errors:Z}}=N({defaultValues:{email:``,password:``},resolver:P(W)});return(0,L.jsxs)(D,{children:[(0,L.jsx)(E,{subtitle:e(`login.selectMethod.subtitle`)||void 0,title:e(`login.title`)}),(0,L.jsx)(h,{search:l}),V&&(0,L.jsxs)(`form`,{className:`flex flex-col gap-tinyrack-lg`,onSubmit:X(async e=>{G.mutate(e)}),children:[(0,L.jsx)(m,{autoComplete:`username webauthn`,error:Z.email,icon:_,label:e(`login.email.label`),placeholder:e(`login.email.placeholder`),...J(`email`),type:`email`}),(0,L.jsx)(m,{autoComplete:`current-password`,error:Z.password,icon:g,label:e(`login.password.label`),labelAction:z.email.enabled?(0,L.jsx)(k,{className:`text-tinyrack-xs`,render:(0,L.jsx)(o,{to:`/password/forgot`}),children:e(`login.link.forgotPassword`)}):void 0,placeholder:e(`login.password.placeholder`),...J(`password`),type:`password`}),(0,L.jsx)(i,{className:`w-full`,intent:`primary`,loading:G.isPending,loadingLabel:e(`login.submitting`),type:`submit`,uiSize:`lg`,children:e(`login.submit`)})]}),B&&(0,L.jsx)(`div`,{className:`prose prose-sm text-center text-tinyrack-text-muted text-tinyrack-xs! **:text-tinyrack-xs!`,dangerouslySetInnerHTML:{__html:B}}),(z.registration.public_registration||U)&&(0,L.jsxs)(j,{children:[z.registration.public_registration&&(0,L.jsx)(A,{link:(0,L.jsx)(o,{search:S(l),to:`/register`,children:e(`login.link.register`)}),text:e(`login.footer.noAccount`)}),U&&(0,L.jsx)(A,{link:(0,L.jsx)(o,{search:S(l),to:`/login`,children:e(`login.password.backToMethods`)})})]})]})}export{R as component};
2
- //# sourceMappingURL=password-BKI5GTRs.js.map
1
+ import{J as e,Z as t,i as n,q as r,s as i}from"./createLucideIcon-BPpAIXR_.js";import{On as a,Sn as o,Sr as s,i as c,r as l,t as u}from"./config-DTQCUlfZ.js";import{t as d}from"./mutationOptions-DyYFUbyn.js";import{t as f}from"./useMutation-BWZ0G4Td.js";import{nt as p}from"./shell-header-bar-JcFhS7mb.js";import{t as m}from"./auth-field-CWyXGKoC.js";import{t as h}from"./authorization-context-banner-DznMDPOX.js";import{t as g}from"./lock-7NRyvXBS.js";import{t as _}from"./mail-D0B_1Dzd.js";import{c as v,i as y,o as b}from"./zod-C87S6Tec.js";import{F as x,N as S,S as C,j as w,s as T}from"./index-CKpnrw9M.js";import{t as E}from"./auth-page-header-DgGAaIiP.js";import{t as D}from"./auth-layout-CiNn2-Nb.js";import{t as O}from"./promise-OpBtq8tG.js";import{t as k}from"./link-C-WdTj3F.js";import{n as A,t as j}from"./auth-footer-Cz5ivsDc.js";import{o as M}from"./passkey-J1qnYp9A.js";import{r as N,t as P}from"./standard-schema-e8gtSzGE.js";var F=t(e()),I=d({mutationFn:async e=>c(await l.api.auth.login.$post({json:e}))}),L=r();function R(){let{t:e,i18n:t}=n(),r=a(),c=s(),l=T.useSearch(),d=l.account_selection_state&&l.prompt?.split(` `).includes(`login`)?{...l,account_selected:1}:l,R=l.lang??t.language,{data:z}=p(u),B=z.registration.signup_notice?.[R]??z.registration.signup_notice?.[z.i18n.fallback_language],V=z.auth.password.enabled,H=z.auth.passkey.enabled,U=z.identity_providers.length+ +!!V+ +!!H>1,W=(0,F.useMemo)(()=>b({email:v().min(1,e(`validation.email.required`)).pipe(y(e(`validation.email.invalid`))),password:v().min(1,e(`validation.password.required`))}),[e]),G=f({...I,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(l)}});return}c.setQueryData(C.queryKey,{user:n}),await O();let i=[];if(z.auth.password.totp.enabled&&n.totp_registered&&i.push(`totp`),z.auth.passkey.enabled&&n.passkey_count>0&&i.push(`passkey`),n.second_factor_required&&i.length===0){if(z.available_2fa_setup_methods.length>1)return r.navigate({to:`/setup/2fa`,search:S(l)});if(z.available_2fa_setup_methods.length===1)return z.available_2fa_setup_methods[0]===`totp`?r.navigate({to:`/setup/totp`,search:S(l)}):r.navigate({to:`/setup/passkey`,search:{...S(l),passkey_name:`default`}})}if(i.length>1)return r.navigate({to:`/verify/2fa`,search:S(l)});if(i.length===1)return i[0]===`totp`?r.navigate({to:`/verify/totp`,search:S(l)}):r.navigate({to:`/verify/passkey`,search:S(l)});if(x(l))window.location.href=w(d);else return r.navigate({to:`/profile`})},onError:t=>{Y(`email`,{type:`manual`,message:e(`login.error.failed`)})},onSettled:()=>{c.invalidateQueries({queryKey:C.queryKey})}}),K=(0,F.useCallback)(async e=>{e.user&&(c.setQueryData(C.queryKey,e),await O(),x(l)?window.location.href=w(d):r.navigate({to:`/profile`})),c.invalidateQueries({queryKey:C.queryKey})},[c,r,l,d]),q=(0,F.useRef)(null);(0,F.useEffect)(()=>{if(!(!H||!V))return q.current=new AbortController,M(K,q.current.signal),()=>{q.current?.abort()}},[H,V,K]);let{register:J,setError:Y,handleSubmit:X,formState:{errors:Z}}=N({defaultValues:{email:``,password:``},resolver:P(W)});return(0,L.jsxs)(D,{children:[(0,L.jsx)(E,{subtitle:e(`login.selectMethod.subtitle`)||void 0,title:e(`login.title`)}),(0,L.jsx)(h,{search:l}),V&&(0,L.jsxs)(`form`,{className:`flex flex-col gap-tinyrack-lg`,onSubmit:X(async e=>{G.mutate(e)}),children:[(0,L.jsx)(m,{autoComplete:`username webauthn`,error:Z.email,icon:_,label:e(`login.email.label`),placeholder:e(`login.email.placeholder`),...J(`email`),type:`email`}),(0,L.jsx)(m,{autoComplete:`current-password`,error:Z.password,icon:g,label:e(`login.password.label`),labelAction:z.email.enabled?(0,L.jsx)(k,{className:`text-tinyrack-xs`,render:(0,L.jsx)(o,{to:`/password/forgot`}),children:e(`login.link.forgotPassword`)}):void 0,placeholder:e(`login.password.placeholder`),...J(`password`),type:`password`}),(0,L.jsx)(i,{className:`w-full`,intent:`primary`,loading:G.isPending,loadingLabel:e(`login.submitting`),type:`submit`,uiSize:`lg`,children:e(`login.submit`)})]}),B&&(0,L.jsx)(`div`,{className:`prose prose-sm text-center text-tinyrack-text-muted text-tinyrack-xs! **:text-tinyrack-xs!`,dangerouslySetInnerHTML:{__html:B}}),(z.registration.public_registration||U)&&(0,L.jsxs)(j,{children:[z.registration.public_registration&&(0,L.jsx)(A,{link:(0,L.jsx)(o,{search:S(l),to:`/register`,children:e(`login.link.register`)}),text:e(`login.footer.noAccount`)}),U&&(0,L.jsx)(A,{link:(0,L.jsx)(o,{search:S(l),to:`/login`,children:e(`login.password.backToMethods`)})})]})]})}export{R as component};
2
+ //# sourceMappingURL=password-Cuh9Hnjp.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"password-BKI5GTRs.js","names":["standardSchemaResolver","useMutation","useQueryClient","useSuspenseQuery","Link","useRouter","TRButton","TRLink","LockIcon","MailIcon","useCallback","useEffect","useMemo","useRef","useForm","useTranslation","z","AuthField","AuthFooter","AuthFooterLink","AuthPageHeader","AuthorizationContextBanner","AuthLayout","buildAuthenticatedAuthorizeUrl","extractOAuthParams","isOAuthFlow","tick","appConfigQueryOptions","loginMutationOptions","startConditionalPasskeyAuth","getSessionQueryOptions","Route","LoginPassword","t","i18n","router","queryClient","search","useSearch","authorizeSearch","OAuthSearch","account_selection_state","prompt","split","includes","account_selected","lang","language","data","configData","implicitNotice","registration","signup_notice","fallback_language","isPasswordAuthEnabled","auth","password","enabled","isPasskeyEnabled","passkey","hasMultipleLoginMethods","identity_providers","length","loginSchema","object","email","string","min","pipe","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","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","undefined","isPending","__html","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 {\n useMutation,\n useQueryClient,\n useSuspenseQuery,\n} from '@tanstack/react-query';\nimport { createFileRoute, Link, useRouter } from '@tanstack/react-router';\nimport { TRButton } from '@tinyrack/ui/components/button';\nimport { TRLink } from '@tinyrack/ui/components/link';\nimport { LockIcon, MailIcon } from 'lucide-react';\nimport { useCallback, useEffect, useMemo, useRef } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport { AuthField } from '#frontend/components/auth/auth-field.tsx';\nimport {\n AuthFooter,\n AuthFooterLink,\n} from '#frontend/components/auth/auth-footer.tsx';\nimport { AuthPageHeader } from '#frontend/components/auth/auth-page-header.tsx';\nimport { AuthorizationContextBanner } from '#frontend/components/auth/authorization-context-banner.tsx';\nimport { RouteErrorFallback } from '#frontend/components/ui/route-error-fallback.tsx';\nimport { AuthLayout } from '#frontend/features/layout/auth-layout.tsx';\nimport {\n buildAuthenticatedAuthorizeUrl,\n extractOAuthParams,\n hasAuthorizationContext,\n isOAuthFlow,\n type OAuthSearch,\n OAuthSearchSchema,\n type SecondFactorMethod,\n} from '#frontend/libs/oauth-search.ts';\nimport { tick } from '#frontend/libs/promise.ts';\nimport { getAuthorizationContextQueryOptions } from '#frontend/queries/authorization-context.ts';\nimport { appConfigQueryOptions } from '#frontend/queries/config.ts';\nimport { 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 loaderDeps: ({ search }) => ({\n search,\n }),\n loader: async ({ context, deps }) => {\n await context.queryClient.ensureQueryData(appConfigQueryOptions);\n if (hasAuthorizationContext(deps.search)) {\n await context.queryClient.ensureQueryData(\n getAuthorizationContextQueryOptions(deps.search),\n );\n }\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: OAuthSearch =\n search.account_selection_state &&\n search.prompt?.split(' ').includes('login')\n ? { ...search, account_selected: 1 }\n : search;\n const lang = search.lang ?? i18n.language;\n\n const { data: configData } = useSuspenseQuery(appConfigQueryOptions);\n\n const implicitNotice =\n configData.registration.signup_notice?.[lang] ??\n configData.registration.signup_notice?.[configData.i18n.fallback_language];\n const isPasswordAuthEnabled = configData.auth.password.enabled;\n const isPasskeyEnabled = configData.auth.passkey.enabled;\n const hasMultipleLoginMethods =\n configData.identity_providers.length +\n (isPasswordAuthEnabled ? 1 : 0) +\n (isPasskeyEnabled ? 1 : 0) >\n 1;\n\n const loginSchema = useMemo(\n () =>\n z.object({\n email: z\n .string()\n .min(1, t('validation.email.required'))\n .pipe(z.email(t('validation.email.invalid'))),\n password: z.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 =\n buildAuthenticatedAuthorizeUrl(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 =\n buildAuthenticatedAuthorizeUrl(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 <AuthLayout>\n {/* Branding lives in the layout's brand panel — see /login. */}\n <AuthPageHeader\n subtitle={t('login.selectMethod.subtitle') || undefined}\n title={t('login.title')}\n />\n\n <AuthorizationContextBanner search={search} />\n\n {isPasswordAuthEnabled && (\n <form\n className=\"flex flex-col gap-tinyrack-lg\"\n onSubmit={handleSubmit(onSubmit)}\n >\n <AuthField\n autoComplete=\"username webauthn\"\n error={errors.email}\n icon={MailIcon}\n label={t('login.email.label')}\n placeholder={t('login.email.placeholder')}\n {...register('email')}\n type=\"email\"\n />\n\n <AuthField\n autoComplete=\"current-password\"\n error={errors.password}\n icon={LockIcon}\n label={t('login.password.label')}\n /*\n * On the label row rather than under the field: it belongs to the\n * password, and putting it between the field and the submit button\n * separated the two things the user is actually trying to do.\n */\n labelAction={\n configData.email.enabled ? (\n <TRLink\n className=\"text-tinyrack-xs\"\n render={<Link to=\"/password/forgot\" />}\n >\n {t('login.link.forgotPassword')}\n </TRLink>\n ) : undefined\n }\n placeholder={t('login.password.placeholder')}\n {...register('password')}\n type=\"password\"\n />\n\n <TRButton\n className=\"w-full\"\n intent=\"primary\"\n loading={loginMutation.isPending}\n loadingLabel={t('login.submitting')}\n type=\"submit\"\n uiSize=\"lg\"\n >\n {t('login.submit')}\n </TRButton>\n </form>\n )}\n\n {implicitNotice && (\n <div\n className=\"prose prose-sm text-center text-tinyrack-text-muted text-tinyrack-xs! **:text-tinyrack-xs!\"\n dangerouslySetInnerHTML={{ __html: implicitNotice }}\n />\n )}\n\n {(configData.registration.public_registration ||\n hasMultipleLoginMethods) && (\n <AuthFooter>\n {configData.registration.public_registration && (\n <AuthFooterLink\n link={\n <Link search={extractOAuthParams(search)} to=\"/register\">\n {t('login.link.register')}\n </Link>\n }\n text={t('login.footer.noAccount')}\n />\n )}\n {hasMultipleLoginMethods && (\n <AuthFooterLink\n link={\n <Link search={extractOAuthParams(search)} to=\"/login\">\n {t('login.password.backToMethods')}\n </Link>\n }\n />\n )}\n </AuthFooter>\n )}\n </AuthLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"password-Cuh9Hnjp.js","names":["standardSchemaResolver","useMutation","useQueryClient","useSuspenseQuery","Link","useRouter","TRButton","TRLink","LockIcon","MailIcon","useCallback","useEffect","useMemo","useRef","useForm","useTranslation","z","AuthField","AuthFooter","AuthFooterLink","AuthPageHeader","AuthorizationContextBanner","AuthLayout","buildAuthenticatedAuthorizeUrl","extractOAuthParams","isOAuthFlow","tick","appConfigQueryOptions","loginMutationOptions","startConditionalPasskeyAuth","getSessionQueryOptions","Route","LoginPassword","t","i18n","router","queryClient","search","useSearch","authorizeSearch","OAuthSearch","account_selection_state","prompt","split","includes","account_selected","lang","language","data","configData","implicitNotice","registration","signup_notice","fallback_language","isPasswordAuthEnabled","auth","password","enabled","isPasskeyEnabled","passkey","hasMultipleLoginMethods","identity_providers","length","loginSchema","object","email","string","min","pipe","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","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","undefined","isPending","__html","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 {\n useMutation,\n useQueryClient,\n useSuspenseQuery,\n} from '@tanstack/react-query';\nimport { createFileRoute, Link, useRouter } from '@tanstack/react-router';\nimport { TRButton } from '@tinyrack/ui/components/button';\nimport { TRLink } from '@tinyrack/ui/components/link';\nimport { LockIcon, MailIcon } from 'lucide-react';\nimport { useCallback, useEffect, useMemo, useRef } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport { AuthField } from '#frontend/components/auth/auth-field.tsx';\nimport {\n AuthFooter,\n AuthFooterLink,\n} from '#frontend/components/auth/auth-footer.tsx';\nimport { AuthPageHeader } from '#frontend/components/auth/auth-page-header.tsx';\nimport { AuthorizationContextBanner } from '#frontend/components/auth/authorization-context-banner.tsx';\nimport { RouteErrorFallback } from '#frontend/components/ui/route-error-fallback.tsx';\nimport { AuthLayout } from '#frontend/features/layout/auth-layout.tsx';\nimport {\n buildAuthenticatedAuthorizeUrl,\n extractOAuthParams,\n hasAuthorizationContext,\n isOAuthFlow,\n type OAuthSearch,\n OAuthSearchSchema,\n type SecondFactorMethod,\n} from '#frontend/libs/oauth-search.ts';\nimport { tick } from '#frontend/libs/promise.ts';\nimport { getAuthorizationContextQueryOptions } from '#frontend/queries/authorization-context.ts';\nimport { appConfigQueryOptions } from '#frontend/queries/config.ts';\nimport { 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 loaderDeps: ({ search }) => ({\n search,\n }),\n loader: async ({ context, deps }) => {\n await context.queryClient.ensureQueryData(appConfigQueryOptions);\n if (hasAuthorizationContext(deps.search)) {\n await context.queryClient.ensureQueryData(\n getAuthorizationContextQueryOptions(deps.search),\n );\n }\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: OAuthSearch =\n search.account_selection_state &&\n search.prompt?.split(' ').includes('login')\n ? { ...search, account_selected: 1 }\n : search;\n const lang = search.lang ?? i18n.language;\n\n const { data: configData } = useSuspenseQuery(appConfigQueryOptions);\n\n const implicitNotice =\n configData.registration.signup_notice?.[lang] ??\n configData.registration.signup_notice?.[configData.i18n.fallback_language];\n const isPasswordAuthEnabled = configData.auth.password.enabled;\n const isPasskeyEnabled = configData.auth.passkey.enabled;\n const hasMultipleLoginMethods =\n configData.identity_providers.length +\n (isPasswordAuthEnabled ? 1 : 0) +\n (isPasskeyEnabled ? 1 : 0) >\n 1;\n\n const loginSchema = useMemo(\n () =>\n z.object({\n email: z\n .string()\n .min(1, t('validation.email.required'))\n .pipe(z.email(t('validation.email.invalid'))),\n password: z.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 =\n buildAuthenticatedAuthorizeUrl(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 =\n buildAuthenticatedAuthorizeUrl(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 <AuthLayout>\n {/* Branding lives in the layout's brand panel — see /login. */}\n <AuthPageHeader\n subtitle={t('login.selectMethod.subtitle') || undefined}\n title={t('login.title')}\n />\n\n <AuthorizationContextBanner search={search} />\n\n {isPasswordAuthEnabled && (\n <form\n className=\"flex flex-col gap-tinyrack-lg\"\n onSubmit={handleSubmit(onSubmit)}\n >\n <AuthField\n autoComplete=\"username webauthn\"\n error={errors.email}\n icon={MailIcon}\n label={t('login.email.label')}\n placeholder={t('login.email.placeholder')}\n {...register('email')}\n type=\"email\"\n />\n\n <AuthField\n autoComplete=\"current-password\"\n error={errors.password}\n icon={LockIcon}\n label={t('login.password.label')}\n /*\n * On the label row rather than under the field: it belongs to the\n * password, and putting it between the field and the submit button\n * separated the two things the user is actually trying to do.\n */\n labelAction={\n configData.email.enabled ? (\n <TRLink\n className=\"text-tinyrack-xs\"\n render={<Link to=\"/password/forgot\" />}\n >\n {t('login.link.forgotPassword')}\n </TRLink>\n ) : undefined\n }\n placeholder={t('login.password.placeholder')}\n {...register('password')}\n type=\"password\"\n />\n\n <TRButton\n className=\"w-full\"\n intent=\"primary\"\n loading={loginMutation.isPending}\n loadingLabel={t('login.submitting')}\n type=\"submit\"\n uiSize=\"lg\"\n >\n {t('login.submit')}\n </TRButton>\n </form>\n )}\n\n {implicitNotice && (\n <div\n className=\"prose prose-sm text-center text-tinyrack-text-muted text-tinyrack-xs! **:text-tinyrack-xs!\"\n dangerouslySetInnerHTML={{ __html: implicitNotice }}\n />\n )}\n\n {(configData.registration.public_registration ||\n hasMultipleLoginMethods) && (\n <AuthFooter>\n {configData.registration.public_registration && (\n <AuthFooterLink\n link={\n <Link search={extractOAuthParams(search)} to=\"/register\">\n {t('login.link.register')}\n </Link>\n }\n text={t('login.footer.noAccount')}\n />\n )}\n {hasMultipleLoginMethods && (\n <AuthFooterLink\n link={\n <Link search={extractOAuthParams(search)} to=\"/login\">\n {t('login.password.backToMethods')}\n </Link>\n }\n />\n )}\n </AuthFooter>\n )}\n </AuthLayout>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import{I as e}from"./index-CKpnrw9M.js";var t=e;export{t as errorComponent};
2
+ //# sourceMappingURL=password-huNZDYU9.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"password-UcpXAI9J.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 {\n useMutation,\n useQueryClient,\n useSuspenseQuery,\n} from '@tanstack/react-query';\nimport { createFileRoute, Link, useRouter } from '@tanstack/react-router';\nimport { TRButton } from '@tinyrack/ui/components/button';\nimport { TRLink } from '@tinyrack/ui/components/link';\nimport { LockIcon, MailIcon } from 'lucide-react';\nimport { useCallback, useEffect, useMemo, useRef } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport { AuthField } from '#frontend/components/auth/auth-field.tsx';\nimport {\n AuthFooter,\n AuthFooterLink,\n} from '#frontend/components/auth/auth-footer.tsx';\nimport { AuthPageHeader } from '#frontend/components/auth/auth-page-header.tsx';\nimport { AuthorizationContextBanner } from '#frontend/components/auth/authorization-context-banner.tsx';\nimport { RouteErrorFallback } from '#frontend/components/ui/route-error-fallback.tsx';\nimport { AuthLayout } from '#frontend/features/layout/auth-layout.tsx';\nimport {\n buildAuthenticatedAuthorizeUrl,\n extractOAuthParams,\n hasAuthorizationContext,\n isOAuthFlow,\n type OAuthSearch,\n OAuthSearchSchema,\n type SecondFactorMethod,\n} from '#frontend/libs/oauth-search.ts';\nimport { tick } from '#frontend/libs/promise.ts';\nimport { getAuthorizationContextQueryOptions } from '#frontend/queries/authorization-context.ts';\nimport { appConfigQueryOptions } from '#frontend/queries/config.ts';\nimport { 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 loaderDeps: ({ search }) => ({\n search,\n }),\n loader: async ({ context, deps }) => {\n await context.queryClient.ensureQueryData(appConfigQueryOptions);\n if (hasAuthorizationContext(deps.search)) {\n await context.queryClient.ensureQueryData(\n getAuthorizationContextQueryOptions(deps.search),\n );\n }\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: OAuthSearch =\n search.account_selection_state &&\n search.prompt?.split(' ').includes('login')\n ? { ...search, account_selected: 1 }\n : search;\n const lang = search.lang ?? i18n.language;\n\n const { data: configData } = useSuspenseQuery(appConfigQueryOptions);\n\n const implicitNotice =\n configData.registration.signup_notice?.[lang] ??\n configData.registration.signup_notice?.[configData.i18n.fallback_language];\n const isPasswordAuthEnabled = configData.auth.password.enabled;\n const isPasskeyEnabled = configData.auth.passkey.enabled;\n const hasMultipleLoginMethods =\n configData.identity_providers.length +\n (isPasswordAuthEnabled ? 1 : 0) +\n (isPasskeyEnabled ? 1 : 0) >\n 1;\n\n const loginSchema = useMemo(\n () =>\n z.object({\n email: z\n .string()\n .min(1, t('validation.email.required'))\n .pipe(z.email(t('validation.email.invalid'))),\n password: z.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 =\n buildAuthenticatedAuthorizeUrl(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 =\n buildAuthenticatedAuthorizeUrl(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 <AuthLayout>\n {/* Branding lives in the layout's brand panel — see /login. */}\n <AuthPageHeader\n subtitle={t('login.selectMethod.subtitle') || undefined}\n title={t('login.title')}\n />\n\n <AuthorizationContextBanner search={search} />\n\n {isPasswordAuthEnabled && (\n <form\n className=\"flex flex-col gap-tinyrack-lg\"\n onSubmit={handleSubmit(onSubmit)}\n >\n <AuthField\n autoComplete=\"username webauthn\"\n error={errors.email}\n icon={MailIcon}\n label={t('login.email.label')}\n placeholder={t('login.email.placeholder')}\n {...register('email')}\n type=\"email\"\n />\n\n <AuthField\n autoComplete=\"current-password\"\n error={errors.password}\n icon={LockIcon}\n label={t('login.password.label')}\n /*\n * On the label row rather than under the field: it belongs to the\n * password, and putting it between the field and the submit button\n * separated the two things the user is actually trying to do.\n */\n labelAction={\n configData.email.enabled ? (\n <TRLink\n className=\"text-tinyrack-xs\"\n render={<Link to=\"/password/forgot\" />}\n >\n {t('login.link.forgotPassword')}\n </TRLink>\n ) : undefined\n }\n placeholder={t('login.password.placeholder')}\n {...register('password')}\n type=\"password\"\n />\n\n <TRButton\n className=\"w-full\"\n intent=\"primary\"\n loading={loginMutation.isPending}\n loadingLabel={t('login.submitting')}\n type=\"submit\"\n uiSize=\"lg\"\n >\n {t('login.submit')}\n </TRButton>\n </form>\n )}\n\n {implicitNotice && (\n <div\n className=\"prose prose-sm text-center text-tinyrack-text-muted text-tinyrack-xs! **:text-tinyrack-xs!\"\n dangerouslySetInnerHTML={{ __html: implicitNotice }}\n />\n )}\n\n {(configData.registration.public_registration ||\n hasMultipleLoginMethods) && (\n <AuthFooter>\n {configData.registration.public_registration && (\n <AuthFooterLink\n link={\n <Link search={extractOAuthParams(search)} to=\"/register\">\n {t('login.link.register')}\n </Link>\n }\n text={t('login.footer.noAccount')}\n />\n )}\n {hasMultipleLoginMethods && (\n <AuthFooterLink\n link={\n <Link search={extractOAuthParams(search)} to=\"/login\">\n {t('login.password.backToMethods')}\n </Link>\n }\n />\n )}\n </AuthFooter>\n )}\n </AuthLayout>\n );\n}\n"],"mappings":"wCAqBsF,IAAAC,EAA7ED"}
1
+ {"version":3,"file":"password-huNZDYU9.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 {\n useMutation,\n useQueryClient,\n useSuspenseQuery,\n} from '@tanstack/react-query';\nimport { createFileRoute, Link, useRouter } from '@tanstack/react-router';\nimport { TRButton } from '@tinyrack/ui/components/button';\nimport { TRLink } from '@tinyrack/ui/components/link';\nimport { LockIcon, MailIcon } from 'lucide-react';\nimport { useCallback, useEffect, useMemo, useRef } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport { AuthField } from '#frontend/components/auth/auth-field.tsx';\nimport {\n AuthFooter,\n AuthFooterLink,\n} from '#frontend/components/auth/auth-footer.tsx';\nimport { AuthPageHeader } from '#frontend/components/auth/auth-page-header.tsx';\nimport { AuthorizationContextBanner } from '#frontend/components/auth/authorization-context-banner.tsx';\nimport { RouteErrorFallback } from '#frontend/components/ui/route-error-fallback.tsx';\nimport { AuthLayout } from '#frontend/features/layout/auth-layout.tsx';\nimport {\n buildAuthenticatedAuthorizeUrl,\n extractOAuthParams,\n hasAuthorizationContext,\n isOAuthFlow,\n type OAuthSearch,\n OAuthSearchSchema,\n type SecondFactorMethod,\n} from '#frontend/libs/oauth-search.ts';\nimport { tick } from '#frontend/libs/promise.ts';\nimport { getAuthorizationContextQueryOptions } from '#frontend/queries/authorization-context.ts';\nimport { appConfigQueryOptions } from '#frontend/queries/config.ts';\nimport { 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 loaderDeps: ({ search }) => ({\n search,\n }),\n loader: async ({ context, deps }) => {\n await context.queryClient.ensureQueryData(appConfigQueryOptions);\n if (hasAuthorizationContext(deps.search)) {\n await context.queryClient.ensureQueryData(\n getAuthorizationContextQueryOptions(deps.search),\n );\n }\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: OAuthSearch =\n search.account_selection_state &&\n search.prompt?.split(' ').includes('login')\n ? { ...search, account_selected: 1 }\n : search;\n const lang = search.lang ?? i18n.language;\n\n const { data: configData } = useSuspenseQuery(appConfigQueryOptions);\n\n const implicitNotice =\n configData.registration.signup_notice?.[lang] ??\n configData.registration.signup_notice?.[configData.i18n.fallback_language];\n const isPasswordAuthEnabled = configData.auth.password.enabled;\n const isPasskeyEnabled = configData.auth.passkey.enabled;\n const hasMultipleLoginMethods =\n configData.identity_providers.length +\n (isPasswordAuthEnabled ? 1 : 0) +\n (isPasskeyEnabled ? 1 : 0) >\n 1;\n\n const loginSchema = useMemo(\n () =>\n z.object({\n email: z\n .string()\n .min(1, t('validation.email.required'))\n .pipe(z.email(t('validation.email.invalid'))),\n password: z.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 =\n buildAuthenticatedAuthorizeUrl(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 =\n buildAuthenticatedAuthorizeUrl(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 <AuthLayout>\n {/* Branding lives in the layout's brand panel — see /login. */}\n <AuthPageHeader\n subtitle={t('login.selectMethod.subtitle') || undefined}\n title={t('login.title')}\n />\n\n <AuthorizationContextBanner search={search} />\n\n {isPasswordAuthEnabled && (\n <form\n className=\"flex flex-col gap-tinyrack-lg\"\n onSubmit={handleSubmit(onSubmit)}\n >\n <AuthField\n autoComplete=\"username webauthn\"\n error={errors.email}\n icon={MailIcon}\n label={t('login.email.label')}\n placeholder={t('login.email.placeholder')}\n {...register('email')}\n type=\"email\"\n />\n\n <AuthField\n autoComplete=\"current-password\"\n error={errors.password}\n icon={LockIcon}\n label={t('login.password.label')}\n /*\n * On the label row rather than under the field: it belongs to the\n * password, and putting it between the field and the submit button\n * separated the two things the user is actually trying to do.\n */\n labelAction={\n configData.email.enabled ? (\n <TRLink\n className=\"text-tinyrack-xs\"\n render={<Link to=\"/password/forgot\" />}\n >\n {t('login.link.forgotPassword')}\n </TRLink>\n ) : undefined\n }\n placeholder={t('login.password.placeholder')}\n {...register('password')}\n type=\"password\"\n />\n\n <TRButton\n className=\"w-full\"\n intent=\"primary\"\n loading={loginMutation.isPending}\n loadingLabel={t('login.submitting')}\n type=\"submit\"\n uiSize=\"lg\"\n >\n {t('login.submit')}\n </TRButton>\n </form>\n )}\n\n {implicitNotice && (\n <div\n className=\"prose prose-sm text-center text-tinyrack-text-muted text-tinyrack-xs! **:text-tinyrack-xs!\"\n dangerouslySetInnerHTML={{ __html: implicitNotice }}\n />\n )}\n\n {(configData.registration.public_registration ||\n hasMultipleLoginMethods) && (\n <AuthFooter>\n {configData.registration.public_registration && (\n <AuthFooterLink\n link={\n <Link search={extractOAuthParams(search)} to=\"/register\">\n {t('login.link.register')}\n </Link>\n }\n text={t('login.footer.noAccount')}\n />\n )}\n {hasMultipleLoginMethods && (\n <AuthFooterLink\n link={\n <Link search={extractOAuthParams(search)} to=\"/login\">\n {t('login.password.backToMethods')}\n </Link>\n }\n />\n )}\n </AuthFooter>\n )}\n </AuthLayout>\n );\n}\n"],"mappings":"wCAqBsF,IAAAC,EAA7ED"}
@@ -1,2 +1,2 @@
1
- import{G as e,J as t,Z as n,c as r,i,n as a,q as o,v as s}from"./createLucideIcon-BPpAIXR_.js";import{bn as c,bt as l,st as u}from"./config-DTQCUlfZ.js";import{f as d}from"./shell-header-bar-CgyNXbPS.js";import{n as f,t as p}from"./valueToPercent-LgbY_prH.js";var m=n(t(),1),h=m.createContext(void 0);function g(){let e=m.useContext(h);if(e===void 0)throw Error(s(38));return e}var _=o(),v=m.forwardRef(function(e,t){let{format:n,getAriaValueText:i,locale:a,max:o=100,min:s=0,value:c,render:d,className:g,children:v,style:y,...b}=e,[x,S]=m.useState(),C=p(c,s,o),w=u(Number.isNaN(C)?0:C,0,100),T=u(Number.isNaN(c)?s:c,s,o),E=n?f(T,a,n):f(w/100,a,{style:`percent`}),D=E;i&&(D=i(E,c));let O={"aria-labelledby":x,"aria-valuemax":o,"aria-valuemin":s,"aria-valuenow":T,"aria-valuetext":D,role:`meter`,children:(0,_.jsxs)(m.Fragment,{children:[v,(0,_.jsx)(`span`,{role:`presentation`,style:l,children:`x`})]})},k=m.useMemo(()=>({formattedValue:E,percentageValue:w,setLabelId:S,value:c}),[E,w,S,c]),A=r(`div`,e,{ref:t,props:[O,b]});return(0,_.jsx)(h.Provider,{value:k,children:A})}),y=m.forwardRef(function(e,t){let{render:n,className:i,style:a,...o}=e;return r(`div`,e,{ref:t,props:o})}),b=m.forwardRef(function(e,t){let{render:n,className:i,style:a,...o}=e,{percentageValue:s}=g();return r(`div`,e,{ref:t,props:[{style:{insetInlineStart:0,height:`inherit`,width:`${s}%`}},o]})}),x=m.forwardRef(function(e,t){let{className:n,render:i,children:a,style:o,...s}=e,{value:c,formattedValue:l}=g();return r(`span`,e,{ref:t,props:[{"aria-hidden":!0,children:typeof a==`function`?a(l,c):l},s]})}),S=m.forwardRef(function(e,t){let{render:n,className:i,style:a,id:o,...s}=e,{setLabelId:c}=g();return r(`span`,e,{ref:t,props:[{id:d(o,c),role:`presentation`},s]})}),C=c(b,`tr-meter-indicator`),w=c(S,`tr-meter-label`);function T({className:t,max:n=100,min:r=0,value:i,variant:a=`neutral`,...o}){let s=Math.min(n,Math.max(r,Number.isNaN(i)?r:i));return(0,_.jsx)(v,{...o,className:e(`tr-meter`,t),"data-variant":a,max:n,min:r,value:s})}var E={Root:T,Track:c(y,`tr-meter-track`),Indicator:C,Value:c(x,`tr-meter-value`),Label:w},D=[`weak`,`fair`,`good`,`strong`],O={weak:`danger`,fair:`warning`,good:`info`,strong:`success`};function k(e,t){if(e.length===0)return 0;if(e.length<t.min_length)return 1;let n=[/[a-z]/,/[A-Z]/,/\d/,/[^a-zA-Z0-9]/].filter(t=>t.test(e)).length,r=e.length>=t.min_length+4;return n>=3&&r?4:n>=2&&r?3:2}function A({password:e,policy:t}){let{t:n}=i(),r=k(e,t);if(r===0)return null;let o=D[r-1];return(0,_.jsxs)(E.Root,{"aria-live":`polite`,className:`flex flex-col gap-tinyrack-xs`,max:4,min:0,value:r,variant:O[o],children:[(0,_.jsxs)(`div`,{className:`flex items-baseline justify-between gap-tinyrack-sm`,children:[(0,_.jsx)(E.Label,{render:(0,_.jsx)(a,{color:`muted`,variant:`caption`,children:n(`validation.password.strength.label`)})}),(0,_.jsx)(E.Value,{render:(0,_.jsx)(a,{color:`muted`,variant:`caption`,children:n(`validation.password.strength.${o}`)})})]}),(0,_.jsx)(E.Track,{children:(0,_.jsx)(E.Indicator,{})})]})}export{A as t};
2
- //# sourceMappingURL=password-strength-DTWF07Px.js.map
1
+ import{G as e,J as t,Z as n,c as r,i,n as a,q as o,v as s}from"./createLucideIcon-BPpAIXR_.js";import{bn as c,bt as l,st as u}from"./config-DTQCUlfZ.js";import{g as d}from"./shell-header-bar-JcFhS7mb.js";import{n as f,t as p}from"./valueToPercent-LgbY_prH.js";var m=n(t(),1),h=m.createContext(void 0);function g(){let e=m.useContext(h);if(e===void 0)throw Error(s(38));return e}var _=o(),v=m.forwardRef(function(e,t){let{format:n,getAriaValueText:i,locale:a,max:o=100,min:s=0,value:c,render:d,className:g,children:v,style:y,...b}=e,[x,S]=m.useState(),C=p(c,s,o),w=u(Number.isNaN(C)?0:C,0,100),T=u(Number.isNaN(c)?s:c,s,o),E=n?f(T,a,n):f(w/100,a,{style:`percent`}),D=E;i&&(D=i(E,c));let O={"aria-labelledby":x,"aria-valuemax":o,"aria-valuemin":s,"aria-valuenow":T,"aria-valuetext":D,role:`meter`,children:(0,_.jsxs)(m.Fragment,{children:[v,(0,_.jsx)(`span`,{role:`presentation`,style:l,children:`x`})]})},k=m.useMemo(()=>({formattedValue:E,percentageValue:w,setLabelId:S,value:c}),[E,w,S,c]),A=r(`div`,e,{ref:t,props:[O,b]});return(0,_.jsx)(h.Provider,{value:k,children:A})}),y=m.forwardRef(function(e,t){let{render:n,className:i,style:a,...o}=e;return r(`div`,e,{ref:t,props:o})}),b=m.forwardRef(function(e,t){let{render:n,className:i,style:a,...o}=e,{percentageValue:s}=g();return r(`div`,e,{ref:t,props:[{style:{insetInlineStart:0,height:`inherit`,width:`${s}%`}},o]})}),x=m.forwardRef(function(e,t){let{className:n,render:i,children:a,style:o,...s}=e,{value:c,formattedValue:l}=g();return r(`span`,e,{ref:t,props:[{"aria-hidden":!0,children:typeof a==`function`?a(l,c):l},s]})}),S=m.forwardRef(function(e,t){let{render:n,className:i,style:a,id:o,...s}=e,{setLabelId:c}=g();return r(`span`,e,{ref:t,props:[{id:d(o,c),role:`presentation`},s]})}),C=c(b,`tr-meter-indicator`),w=c(S,`tr-meter-label`);function T({className:t,max:n=100,min:r=0,value:i,variant:a=`neutral`,...o}){let s=Math.min(n,Math.max(r,Number.isNaN(i)?r:i));return(0,_.jsx)(v,{...o,className:e(`tr-meter`,t),"data-variant":a,max:n,min:r,value:s})}var E={Root:T,Track:c(y,`tr-meter-track`),Indicator:C,Value:c(x,`tr-meter-value`),Label:w},D=[`weak`,`fair`,`good`,`strong`],O={weak:`danger`,fair:`warning`,good:`info`,strong:`success`};function k(e,t){if(e.length===0)return 0;if(e.length<t.min_length)return 1;let n=[/[a-z]/,/[A-Z]/,/\d/,/[^a-zA-Z0-9]/].filter(t=>t.test(e)).length,r=e.length>=t.min_length+4;return n>=3&&r?4:n>=2&&r?3:2}function A({password:e,policy:t}){let{t:n}=i(),r=k(e,t);if(r===0)return null;let o=D[r-1];return(0,_.jsxs)(E.Root,{"aria-live":`polite`,className:`flex flex-col gap-tinyrack-xs`,max:4,min:0,value:r,variant:O[o],children:[(0,_.jsxs)(`div`,{className:`flex items-baseline justify-between gap-tinyrack-sm`,children:[(0,_.jsx)(E.Label,{render:(0,_.jsx)(a,{color:`muted`,variant:`caption`,children:n(`validation.password.strength.label`)})}),(0,_.jsx)(E.Value,{render:(0,_.jsx)(a,{color:`muted`,variant:`caption`,children:n(`validation.password.strength.${o}`)})})]}),(0,_.jsx)(E.Track,{children:(0,_.jsx)(E.Indicator,{})})]})}export{A as t};
2
+ //# sourceMappingURL=password-strength-BIPcWiM_.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"password-strength-DTWF07Px.js","names":["_formatErrorMessage","Meter.Indicator","Meter.Label","Meter.Root","Meter.Track","Meter.Value"],"sources":["../../../../node_modules/.pnpm/@base-ui+react@1.7.0_@types+react@19.2.17_react-dom@19.2.7_react@19.2.7__react@19.2.7/node_modules/@base-ui/react/meter/root/MeterRootContext.mjs","../../../../node_modules/.pnpm/@base-ui+react@1.7.0_@types+react@19.2.17_react-dom@19.2.7_react@19.2.7__react@19.2.7/node_modules/@base-ui/react/meter/root/MeterRoot.mjs","../../../../node_modules/.pnpm/@base-ui+react@1.7.0_@types+react@19.2.17_react-dom@19.2.7_react@19.2.7__react@19.2.7/node_modules/@base-ui/react/meter/track/MeterTrack.mjs","../../../../node_modules/.pnpm/@base-ui+react@1.7.0_@types+react@19.2.17_react-dom@19.2.7_react@19.2.7__react@19.2.7/node_modules/@base-ui/react/meter/indicator/MeterIndicator.mjs","../../../../node_modules/.pnpm/@base-ui+react@1.7.0_@types+react@19.2.17_react-dom@19.2.7_react@19.2.7__react@19.2.7/node_modules/@base-ui/react/meter/value/MeterValue.mjs","../../../../node_modules/.pnpm/@base-ui+react@1.7.0_@types+react@19.2.17_react-dom@19.2.7_react@19.2.7__react@19.2.7/node_modules/@base-ui/react/meter/label/MeterLabel.mjs","../../../../node_modules/.pnpm/@tinyrack+ui@0.27.0_@types+react@19.2.17_react-dom@19.2.7_react@19.2.7__react@19.2.7_shiki@4.4.2_tailwindcss@4.3.2/node_modules/@tinyrack/ui/dist/components/meter/meter-indicator.js","../../../../node_modules/.pnpm/@tinyrack+ui@0.27.0_@types+react@19.2.17_react-dom@19.2.7_react@19.2.7__react@19.2.7_shiki@4.4.2_tailwindcss@4.3.2/node_modules/@tinyrack/ui/dist/components/meter/meter-label.js","../../../../node_modules/.pnpm/@tinyrack+ui@0.27.0_@types+react@19.2.17_react-dom@19.2.7_react@19.2.7__react@19.2.7_shiki@4.4.2_tailwindcss@4.3.2/node_modules/@tinyrack/ui/dist/components/meter/meter-root.js","../../../../node_modules/.pnpm/@tinyrack+ui@0.27.0_@types+react@19.2.17_react-dom@19.2.7_react@19.2.7__react@19.2.7_shiki@4.4.2_tailwindcss@4.3.2/node_modules/@tinyrack/ui/dist/components/meter/meter-track.js","../../../../node_modules/.pnpm/@tinyrack+ui@0.27.0_@types+react@19.2.17_react-dom@19.2.7_react@19.2.7__react@19.2.7_shiki@4.4.2_tailwindcss@4.3.2/node_modules/@tinyrack/ui/dist/components/meter/meter-value.js","../../../../node_modules/.pnpm/@tinyrack+ui@0.27.0_@types+react@19.2.17_react-dom@19.2.7_react@19.2.7__react@19.2.7_shiki@4.4.2_tailwindcss@4.3.2/node_modules/@tinyrack/ui/dist/components/meter/index.js","../../../frontend/src/components/auth/password-strength.tsx"],"sourcesContent":["'use client';\n\nimport _formatErrorMessage from \"@base-ui/utils/formatErrorMessage\";\nimport * as React from 'react';\nexport const MeterRootContext = /*#__PURE__*/React.createContext(undefined);\nif (process.env.NODE_ENV !== \"production\") MeterRootContext.displayName = \"MeterRootContext\";\nexport function useMeterRootContext() {\n const context = React.useContext(MeterRootContext);\n if (context === undefined) {\n throw new Error(process.env.NODE_ENV !== \"production\" ? 'Base UI: MeterRootContext is missing. Meter parts must be placed within <Meter.Root>.' : _formatErrorMessage(38));\n }\n return context;\n}","'use client';\n\nimport * as React from 'react';\nimport { visuallyHidden } from '@base-ui/utils/visuallyHidden';\nimport { MeterRootContext } from \"./MeterRootContext.mjs\";\nimport { formatNumber } from \"../../utils/formatNumber.mjs\";\nimport { valueToPercent } from \"../../utils/valueToPercent.mjs\";\nimport { clamp } from \"../../internals/clamp.mjs\";\nimport { useRenderElement } from \"../../internals/useRenderElement.mjs\";\n\n/**\n * Groups all parts of the meter and provides the value for screen readers.\n * Renders a `<div>` element.\n *\n * Documentation: [Base UI Meter](https://base-ui.com/react/components/meter)\n */\nimport { jsx as _jsx, jsxs as _jsxs } from \"react/jsx-runtime\";\nexport const MeterRoot = /*#__PURE__*/React.forwardRef(function MeterRoot(componentProps, forwardedRef) {\n const {\n format,\n getAriaValueText,\n locale,\n max = 100,\n min = 0,\n value: valueProp,\n render,\n className,\n children,\n style,\n ...elementProps\n } = componentProps;\n const [labelId, setLabelId] = React.useState();\n\n // `clamp` handles infinity, but NaN needs an explicit fallback before normalizing range outputs.\n const rawPercentage = valueToPercent(valueProp, min, max);\n const percentageValue = clamp(Number.isNaN(rawPercentage) ? 0 : rawPercentage, 0, 100);\n const clampedValue = clamp(Number.isNaN(valueProp) ? min : valueProp, min, max);\n\n // Format the clamped value so visible and accessible text stay in sync with `aria-valuenow` and\n // the indicator fill. The raw value remains available as the second `getAriaValueText` argument.\n const formattedValue = format ? formatNumber(clampedValue, locale, format) : formatNumber(percentageValue / 100, locale, {\n style: 'percent'\n });\n let ariaValuetext = formattedValue;\n if (getAriaValueText) {\n ariaValuetext = getAriaValueText(formattedValue, valueProp);\n }\n const defaultProps = {\n 'aria-labelledby': labelId,\n 'aria-valuemax': max,\n 'aria-valuemin': min,\n 'aria-valuenow': clampedValue,\n 'aria-valuetext': ariaValuetext,\n role: 'meter',\n children: /*#__PURE__*/_jsxs(React.Fragment, {\n children: [children, /*#__PURE__*/_jsx(\"span\", {\n role: \"presentation\",\n style: visuallyHidden,\n children: \"x\"\n })]\n })\n };\n const contextValue = React.useMemo(() => ({\n formattedValue,\n percentageValue,\n setLabelId,\n value: valueProp\n }), [formattedValue, percentageValue, setLabelId, valueProp]);\n const element = useRenderElement('div', componentProps, {\n ref: forwardedRef,\n props: [defaultProps, elementProps]\n });\n return /*#__PURE__*/_jsx(MeterRootContext.Provider, {\n value: contextValue,\n children: element\n });\n});\nif (process.env.NODE_ENV !== \"production\") MeterRoot.displayName = \"MeterRoot\";","'use client';\n\nimport * as React from 'react';\nimport { useRenderElement } from \"../../internals/useRenderElement.mjs\";\n\n/**\n * Contains the meter indicator and represents the entire range of the meter.\n * Renders a `<div>` element.\n *\n * Documentation: [Base UI Meter](https://base-ui.com/react/components/meter)\n */\nexport const MeterTrack = /*#__PURE__*/React.forwardRef(function MeterTrack(componentProps, forwardedRef) {\n const {\n render,\n className,\n style,\n ...elementProps\n } = componentProps;\n return useRenderElement('div', componentProps, {\n ref: forwardedRef,\n props: elementProps\n });\n});\nif (process.env.NODE_ENV !== \"production\") MeterTrack.displayName = \"MeterTrack\";","'use client';\n\nimport * as React from 'react';\nimport { useMeterRootContext } from \"../root/MeterRootContext.mjs\";\nimport { useRenderElement } from \"../../internals/useRenderElement.mjs\";\n\n/**\n * Visualizes the position of the value along the range.\n * Renders a `<div>` element.\n *\n * Documentation: [Base UI Meter](https://base-ui.com/react/components/meter)\n */\nexport const MeterIndicator = /*#__PURE__*/React.forwardRef(function MeterIndicator(componentProps, forwardedRef) {\n const {\n render,\n className,\n style,\n ...elementProps\n } = componentProps;\n const {\n percentageValue\n } = useMeterRootContext();\n return useRenderElement('div', componentProps, {\n ref: forwardedRef,\n props: [{\n style: {\n insetInlineStart: 0,\n height: 'inherit',\n width: `${percentageValue}%`\n }\n }, elementProps]\n });\n});\nif (process.env.NODE_ENV !== \"production\") MeterIndicator.displayName = \"MeterIndicator\";","'use client';\n\nimport * as React from 'react';\nimport { useMeterRootContext } from \"../root/MeterRootContext.mjs\";\nimport { useRenderElement } from \"../../internals/useRenderElement.mjs\";\n\n/**\n * A text element displaying the current value.\n * Renders a `<span>` element.\n *\n * Documentation: [Base UI Meter](https://base-ui.com/react/components/meter)\n */\nexport const MeterValue = /*#__PURE__*/React.forwardRef(function MeterValue(componentProps, forwardedRef) {\n const {\n className,\n render,\n children,\n style,\n ...elementProps\n } = componentProps;\n const {\n value,\n formattedValue\n } = useMeterRootContext();\n return useRenderElement('span', componentProps, {\n ref: forwardedRef,\n props: [{\n 'aria-hidden': true,\n children: typeof children === 'function' ? children(formattedValue, value) : formattedValue\n }, elementProps]\n });\n});\nif (process.env.NODE_ENV !== \"production\") MeterValue.displayName = \"MeterValue\";","'use client';\n\nimport * as React from 'react';\nimport { useMeterRootContext } from \"../root/MeterRootContext.mjs\";\nimport { useRenderElement } from \"../../internals/useRenderElement.mjs\";\nimport { useRegisteredLabelId } from \"../../utils/useRegisteredLabelId.mjs\";\n\n/**\n * An accessible label for the meter.\n * Renders a `<span>` element.\n *\n * Documentation: [Base UI Meter](https://base-ui.com/react/components/meter)\n */\nexport const MeterLabel = /*#__PURE__*/React.forwardRef(function MeterLabel(componentProps, forwardedRef) {\n const {\n render,\n className,\n style,\n id: idProp,\n ...elementProps\n } = componentProps;\n const {\n setLabelId\n } = useMeterRootContext();\n const id = useRegisteredLabelId(idProp, setLabelId);\n return useRenderElement('span', componentProps, {\n ref: forwardedRef,\n props: [{\n id,\n role: 'presentation'\n }, elementProps]\n });\n});\nif (process.env.NODE_ENV !== \"production\") MeterLabel.displayName = \"MeterLabel\";","\"use client\";\nimport { createComponentPart } from \"../../internal/component-part.js\";\nimport { Meter } from \"@base-ui/react/meter\";\n//#region src/components/meter/meter-indicator.tsx\nconst TRMeterIndicator = createComponentPart(Meter.Indicator, \"tr-meter-indicator\");\n//#endregion\nexport { TRMeterIndicator };\n\n//# sourceMappingURL=meter-indicator.js.map","\"use client\";\nimport { createComponentPart } from \"../../internal/component-part.js\";\nimport { Meter } from \"@base-ui/react/meter\";\n//#region src/components/meter/meter-label.tsx\nconst TRMeterLabel = createComponentPart(Meter.Label, \"tr-meter-label\");\n//#endregion\nexport { TRMeterLabel };\n\n//# sourceMappingURL=meter-label.js.map","\"use client\";\nimport { mergeComponentClassName } from \"../../internal/component-class-name.js\";\nimport { jsx } from \"react/jsx-runtime\";\nimport { Meter } from \"@base-ui/react/meter\";\n//#region src/components/meter/meter-root.tsx\nfunction TRMeterRoot({ className, max = 100, min = 0, value, variant = \"neutral\", ...props }) {\n\tconst clampedValue = Math.min(max, Math.max(min, Number.isNaN(value) ? min : value));\n\treturn /* @__PURE__ */ jsx(Meter.Root, {\n\t\t...props,\n\t\tclassName: mergeComponentClassName(\"tr-meter\", className),\n\t\t\"data-variant\": variant,\n\t\tmax,\n\t\tmin,\n\t\tvalue: clampedValue\n\t});\n}\n//#endregion\nexport { TRMeterRoot };\n\n//# sourceMappingURL=meter-root.js.map","\"use client\";\nimport { createComponentPart } from \"../../internal/component-part.js\";\nimport { Meter } from \"@base-ui/react/meter\";\n//#region src/components/meter/meter-track.tsx\nconst TRMeterTrack = createComponentPart(Meter.Track, \"tr-meter-track\");\n//#endregion\nexport { TRMeterTrack };\n\n//# sourceMappingURL=meter-track.js.map","\"use client\";\nimport { createComponentPart } from \"../../internal/component-part.js\";\nimport { Meter } from \"@base-ui/react/meter\";\n//#region src/components/meter/meter-value.tsx\nconst TRMeterValue = createComponentPart(Meter.Value, \"tr-meter-value\");\n//#endregion\nexport { TRMeterValue };\n\n//# sourceMappingURL=meter-value.js.map","import { TRMeterIndicator } from \"./meter-indicator.js\";\nimport { TRMeterLabel } from \"./meter-label.js\";\nimport { TRMeterRoot } from \"./meter-root.js\";\nimport { TRMeterTrack } from \"./meter-track.js\";\nimport { TRMeterValue } from \"./meter-value.js\";\n//#region src/components/meter/index.tsx\nconst TRMeter = {\n\tRoot: TRMeterRoot,\n\tTrack: TRMeterTrack,\n\tIndicator: TRMeterIndicator,\n\tValue: TRMeterValue,\n\tLabel: TRMeterLabel\n};\n//#endregion\nexport { TRMeter, TRMeterIndicator, TRMeterLabel, TRMeterRoot, TRMeterTrack, TRMeterValue };\n\n//# sourceMappingURL=index.js.map","import { TRMeter } from '@tinyrack/ui/components/meter';\nimport { TRText } from '@tinyrack/ui/components/text';\nimport { useTranslation } from 'react-i18next';\n\ntype PasswordPolicy = {\n min_length: number;\n max_length: number;\n};\n\ntype PasswordStrengthProps = {\n password: string;\n policy: PasswordPolicy;\n};\n\nconst LEVELS = ['weak', 'fair', 'good', 'strong'] as const;\n\nconst VARIANTS = {\n weak: 'danger',\n fair: 'warning',\n good: 'info',\n strong: 'success',\n} as const;\n\n/**\n * Scores a password 1-4 for feedback only.\n *\n * Deliberately simple: length against the deployment's own policy plus\n * character-class variety. The server is the authority on what is acceptable,\n * so this only needs to steer the user, and a full entropy estimator would be\n * a large dependency for a hint bar.\n *\n * Returns 0 for \"nothing to say yet\" so the caller can stay silent.\n */\nfunction scorePassword(password: string, policy: PasswordPolicy): number {\n if (password.length === 0) {\n return 0;\n }\n if (password.length < policy.min_length) {\n return 1;\n }\n\n const classes = [/[a-z]/, /[A-Z]/, /\\d/, /[^a-zA-Z0-9]/].filter((pattern) =>\n pattern.test(password),\n ).length;\n\n const isLong = password.length >= policy.min_length + 4;\n\n if (classes >= 3 && isLong) {\n return 4;\n }\n if (classes >= 2 && isLong) {\n return 3;\n }\n return 2;\n}\n\n/**\n * Strength hint under a new-password field.\n *\n * Renders nothing for an empty field so the form does not shift as soon as it\n * is focused. Announced politely rather than assertively — it updates on every\n * keystroke and should not interrupt typing.\n */\nexport function PasswordStrength({ password, policy }: PasswordStrengthProps) {\n const { t } = useTranslation();\n const score = scorePassword(password, policy);\n\n if (score === 0) {\n return null;\n }\n\n const level = LEVELS[score - 1];\n\n return (\n <TRMeter.Root\n aria-live=\"polite\"\n className=\"flex flex-col gap-tinyrack-xs\"\n max={4}\n min={0}\n value={score}\n variant={VARIANTS[level]}\n >\n <div className=\"flex items-baseline justify-between gap-tinyrack-sm\">\n <TRMeter.Label\n render={\n <TRText color=\"muted\" variant=\"caption\">\n {t('validation.password.strength.label')}\n </TRText>\n }\n />\n <TRMeter.Value\n render={\n <TRText color=\"muted\" variant=\"caption\">\n {t(`validation.password.strength.${level}`)}\n </TRText>\n }\n />\n </div>\n <TRMeter.Track>\n <TRMeter.Indicator />\n </TRMeter.Track>\n </TRMeter.Root>\n );\n}\n"],"x_google_ignoreList":[0,1,2,3,4,5,6,7,8,9,10,11],"mappings":"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"}
1
+ {"version":3,"file":"password-strength-BIPcWiM_.js","names":["_formatErrorMessage","Meter.Indicator","Meter.Label","Meter.Root","Meter.Track","Meter.Value"],"sources":["../../../../node_modules/.pnpm/@base-ui+react@1.7.0_@types+react@19.2.17_react-dom@19.2.7_react@19.2.7__react@19.2.7/node_modules/@base-ui/react/meter/root/MeterRootContext.mjs","../../../../node_modules/.pnpm/@base-ui+react@1.7.0_@types+react@19.2.17_react-dom@19.2.7_react@19.2.7__react@19.2.7/node_modules/@base-ui/react/meter/root/MeterRoot.mjs","../../../../node_modules/.pnpm/@base-ui+react@1.7.0_@types+react@19.2.17_react-dom@19.2.7_react@19.2.7__react@19.2.7/node_modules/@base-ui/react/meter/track/MeterTrack.mjs","../../../../node_modules/.pnpm/@base-ui+react@1.7.0_@types+react@19.2.17_react-dom@19.2.7_react@19.2.7__react@19.2.7/node_modules/@base-ui/react/meter/indicator/MeterIndicator.mjs","../../../../node_modules/.pnpm/@base-ui+react@1.7.0_@types+react@19.2.17_react-dom@19.2.7_react@19.2.7__react@19.2.7/node_modules/@base-ui/react/meter/value/MeterValue.mjs","../../../../node_modules/.pnpm/@base-ui+react@1.7.0_@types+react@19.2.17_react-dom@19.2.7_react@19.2.7__react@19.2.7/node_modules/@base-ui/react/meter/label/MeterLabel.mjs","../../../../node_modules/.pnpm/@tinyrack+ui@0.29.0_@types+react@19.2.17_react-dom@19.2.7_react@19.2.7__react@19.2.7_shiki@4.4.2_tailwindcss@4.3.2/node_modules/@tinyrack/ui/dist/components/meter/meter-indicator.js","../../../../node_modules/.pnpm/@tinyrack+ui@0.29.0_@types+react@19.2.17_react-dom@19.2.7_react@19.2.7__react@19.2.7_shiki@4.4.2_tailwindcss@4.3.2/node_modules/@tinyrack/ui/dist/components/meter/meter-label.js","../../../../node_modules/.pnpm/@tinyrack+ui@0.29.0_@types+react@19.2.17_react-dom@19.2.7_react@19.2.7__react@19.2.7_shiki@4.4.2_tailwindcss@4.3.2/node_modules/@tinyrack/ui/dist/components/meter/meter-root.js","../../../../node_modules/.pnpm/@tinyrack+ui@0.29.0_@types+react@19.2.17_react-dom@19.2.7_react@19.2.7__react@19.2.7_shiki@4.4.2_tailwindcss@4.3.2/node_modules/@tinyrack/ui/dist/components/meter/meter-track.js","../../../../node_modules/.pnpm/@tinyrack+ui@0.29.0_@types+react@19.2.17_react-dom@19.2.7_react@19.2.7__react@19.2.7_shiki@4.4.2_tailwindcss@4.3.2/node_modules/@tinyrack/ui/dist/components/meter/meter-value.js","../../../../node_modules/.pnpm/@tinyrack+ui@0.29.0_@types+react@19.2.17_react-dom@19.2.7_react@19.2.7__react@19.2.7_shiki@4.4.2_tailwindcss@4.3.2/node_modules/@tinyrack/ui/dist/components/meter/index.js","../../../frontend/src/components/auth/password-strength.tsx"],"sourcesContent":["'use client';\n\nimport _formatErrorMessage from \"@base-ui/utils/formatErrorMessage\";\nimport * as React from 'react';\nexport const MeterRootContext = /*#__PURE__*/React.createContext(undefined);\nif (process.env.NODE_ENV !== \"production\") MeterRootContext.displayName = \"MeterRootContext\";\nexport function useMeterRootContext() {\n const context = React.useContext(MeterRootContext);\n if (context === undefined) {\n throw new Error(process.env.NODE_ENV !== \"production\" ? 'Base UI: MeterRootContext is missing. Meter parts must be placed within <Meter.Root>.' : _formatErrorMessage(38));\n }\n return context;\n}","'use client';\n\nimport * as React from 'react';\nimport { visuallyHidden } from '@base-ui/utils/visuallyHidden';\nimport { MeterRootContext } from \"./MeterRootContext.mjs\";\nimport { formatNumber } from \"../../utils/formatNumber.mjs\";\nimport { valueToPercent } from \"../../utils/valueToPercent.mjs\";\nimport { clamp } from \"../../internals/clamp.mjs\";\nimport { useRenderElement } from \"../../internals/useRenderElement.mjs\";\n\n/**\n * Groups all parts of the meter and provides the value for screen readers.\n * Renders a `<div>` element.\n *\n * Documentation: [Base UI Meter](https://base-ui.com/react/components/meter)\n */\nimport { jsx as _jsx, jsxs as _jsxs } from \"react/jsx-runtime\";\nexport const MeterRoot = /*#__PURE__*/React.forwardRef(function MeterRoot(componentProps, forwardedRef) {\n const {\n format,\n getAriaValueText,\n locale,\n max = 100,\n min = 0,\n value: valueProp,\n render,\n className,\n children,\n style,\n ...elementProps\n } = componentProps;\n const [labelId, setLabelId] = React.useState();\n\n // `clamp` handles infinity, but NaN needs an explicit fallback before normalizing range outputs.\n const rawPercentage = valueToPercent(valueProp, min, max);\n const percentageValue = clamp(Number.isNaN(rawPercentage) ? 0 : rawPercentage, 0, 100);\n const clampedValue = clamp(Number.isNaN(valueProp) ? min : valueProp, min, max);\n\n // Format the clamped value so visible and accessible text stay in sync with `aria-valuenow` and\n // the indicator fill. The raw value remains available as the second `getAriaValueText` argument.\n const formattedValue = format ? formatNumber(clampedValue, locale, format) : formatNumber(percentageValue / 100, locale, {\n style: 'percent'\n });\n let ariaValuetext = formattedValue;\n if (getAriaValueText) {\n ariaValuetext = getAriaValueText(formattedValue, valueProp);\n }\n const defaultProps = {\n 'aria-labelledby': labelId,\n 'aria-valuemax': max,\n 'aria-valuemin': min,\n 'aria-valuenow': clampedValue,\n 'aria-valuetext': ariaValuetext,\n role: 'meter',\n children: /*#__PURE__*/_jsxs(React.Fragment, {\n children: [children, /*#__PURE__*/_jsx(\"span\", {\n role: \"presentation\",\n style: visuallyHidden,\n children: \"x\"\n })]\n })\n };\n const contextValue = React.useMemo(() => ({\n formattedValue,\n percentageValue,\n setLabelId,\n value: valueProp\n }), [formattedValue, percentageValue, setLabelId, valueProp]);\n const element = useRenderElement('div', componentProps, {\n ref: forwardedRef,\n props: [defaultProps, elementProps]\n });\n return /*#__PURE__*/_jsx(MeterRootContext.Provider, {\n value: contextValue,\n children: element\n });\n});\nif (process.env.NODE_ENV !== \"production\") MeterRoot.displayName = \"MeterRoot\";","'use client';\n\nimport * as React from 'react';\nimport { useRenderElement } from \"../../internals/useRenderElement.mjs\";\n\n/**\n * Contains the meter indicator and represents the entire range of the meter.\n * Renders a `<div>` element.\n *\n * Documentation: [Base UI Meter](https://base-ui.com/react/components/meter)\n */\nexport const MeterTrack = /*#__PURE__*/React.forwardRef(function MeterTrack(componentProps, forwardedRef) {\n const {\n render,\n className,\n style,\n ...elementProps\n } = componentProps;\n return useRenderElement('div', componentProps, {\n ref: forwardedRef,\n props: elementProps\n });\n});\nif (process.env.NODE_ENV !== \"production\") MeterTrack.displayName = \"MeterTrack\";","'use client';\n\nimport * as React from 'react';\nimport { useMeterRootContext } from \"../root/MeterRootContext.mjs\";\nimport { useRenderElement } from \"../../internals/useRenderElement.mjs\";\n\n/**\n * Visualizes the position of the value along the range.\n * Renders a `<div>` element.\n *\n * Documentation: [Base UI Meter](https://base-ui.com/react/components/meter)\n */\nexport const MeterIndicator = /*#__PURE__*/React.forwardRef(function MeterIndicator(componentProps, forwardedRef) {\n const {\n render,\n className,\n style,\n ...elementProps\n } = componentProps;\n const {\n percentageValue\n } = useMeterRootContext();\n return useRenderElement('div', componentProps, {\n ref: forwardedRef,\n props: [{\n style: {\n insetInlineStart: 0,\n height: 'inherit',\n width: `${percentageValue}%`\n }\n }, elementProps]\n });\n});\nif (process.env.NODE_ENV !== \"production\") MeterIndicator.displayName = \"MeterIndicator\";","'use client';\n\nimport * as React from 'react';\nimport { useMeterRootContext } from \"../root/MeterRootContext.mjs\";\nimport { useRenderElement } from \"../../internals/useRenderElement.mjs\";\n\n/**\n * A text element displaying the current value.\n * Renders a `<span>` element.\n *\n * Documentation: [Base UI Meter](https://base-ui.com/react/components/meter)\n */\nexport const MeterValue = /*#__PURE__*/React.forwardRef(function MeterValue(componentProps, forwardedRef) {\n const {\n className,\n render,\n children,\n style,\n ...elementProps\n } = componentProps;\n const {\n value,\n formattedValue\n } = useMeterRootContext();\n return useRenderElement('span', componentProps, {\n ref: forwardedRef,\n props: [{\n 'aria-hidden': true,\n children: typeof children === 'function' ? children(formattedValue, value) : formattedValue\n }, elementProps]\n });\n});\nif (process.env.NODE_ENV !== \"production\") MeterValue.displayName = \"MeterValue\";","'use client';\n\nimport * as React from 'react';\nimport { useMeterRootContext } from \"../root/MeterRootContext.mjs\";\nimport { useRenderElement } from \"../../internals/useRenderElement.mjs\";\nimport { useRegisteredLabelId } from \"../../utils/useRegisteredLabelId.mjs\";\n\n/**\n * An accessible label for the meter.\n * Renders a `<span>` element.\n *\n * Documentation: [Base UI Meter](https://base-ui.com/react/components/meter)\n */\nexport const MeterLabel = /*#__PURE__*/React.forwardRef(function MeterLabel(componentProps, forwardedRef) {\n const {\n render,\n className,\n style,\n id: idProp,\n ...elementProps\n } = componentProps;\n const {\n setLabelId\n } = useMeterRootContext();\n const id = useRegisteredLabelId(idProp, setLabelId);\n return useRenderElement('span', componentProps, {\n ref: forwardedRef,\n props: [{\n id,\n role: 'presentation'\n }, elementProps]\n });\n});\nif (process.env.NODE_ENV !== \"production\") MeterLabel.displayName = \"MeterLabel\";","\"use client\";\nimport { createComponentPart } from \"../../internal/component-part.js\";\nimport { Meter } from \"@base-ui/react/meter\";\n//#region src/components/meter/meter-indicator.tsx\nconst TRMeterIndicator = createComponentPart(Meter.Indicator, \"tr-meter-indicator\");\n//#endregion\nexport { TRMeterIndicator };\n\n//# sourceMappingURL=meter-indicator.js.map","\"use client\";\nimport { createComponentPart } from \"../../internal/component-part.js\";\nimport { Meter } from \"@base-ui/react/meter\";\n//#region src/components/meter/meter-label.tsx\nconst TRMeterLabel = createComponentPart(Meter.Label, \"tr-meter-label\");\n//#endregion\nexport { TRMeterLabel };\n\n//# sourceMappingURL=meter-label.js.map","\"use client\";\nimport { mergeComponentClassName } from \"../../internal/component-class-name.js\";\nimport { jsx } from \"react/jsx-runtime\";\nimport { Meter } from \"@base-ui/react/meter\";\n//#region src/components/meter/meter-root.tsx\nfunction TRMeterRoot({ className, max = 100, min = 0, value, variant = \"neutral\", ...props }) {\n\tconst clampedValue = Math.min(max, Math.max(min, Number.isNaN(value) ? min : value));\n\treturn /* @__PURE__ */ jsx(Meter.Root, {\n\t\t...props,\n\t\tclassName: mergeComponentClassName(\"tr-meter\", className),\n\t\t\"data-variant\": variant,\n\t\tmax,\n\t\tmin,\n\t\tvalue: clampedValue\n\t});\n}\n//#endregion\nexport { TRMeterRoot };\n\n//# sourceMappingURL=meter-root.js.map","\"use client\";\nimport { createComponentPart } from \"../../internal/component-part.js\";\nimport { Meter } from \"@base-ui/react/meter\";\n//#region src/components/meter/meter-track.tsx\nconst TRMeterTrack = createComponentPart(Meter.Track, \"tr-meter-track\");\n//#endregion\nexport { TRMeterTrack };\n\n//# sourceMappingURL=meter-track.js.map","\"use client\";\nimport { createComponentPart } from \"../../internal/component-part.js\";\nimport { Meter } from \"@base-ui/react/meter\";\n//#region src/components/meter/meter-value.tsx\nconst TRMeterValue = createComponentPart(Meter.Value, \"tr-meter-value\");\n//#endregion\nexport { TRMeterValue };\n\n//# sourceMappingURL=meter-value.js.map","import { TRMeterIndicator } from \"./meter-indicator.js\";\nimport { TRMeterLabel } from \"./meter-label.js\";\nimport { TRMeterRoot } from \"./meter-root.js\";\nimport { TRMeterTrack } from \"./meter-track.js\";\nimport { TRMeterValue } from \"./meter-value.js\";\n//#region src/components/meter/index.tsx\nconst TRMeter = {\n\tRoot: TRMeterRoot,\n\tTrack: TRMeterTrack,\n\tIndicator: TRMeterIndicator,\n\tValue: TRMeterValue,\n\tLabel: TRMeterLabel\n};\n//#endregion\nexport { TRMeter, TRMeterIndicator, TRMeterLabel, TRMeterRoot, TRMeterTrack, TRMeterValue };\n\n//# sourceMappingURL=index.js.map","import { TRMeter } from '@tinyrack/ui/components/meter';\nimport { TRText } from '@tinyrack/ui/components/text';\nimport { useTranslation } from 'react-i18next';\n\ntype PasswordPolicy = {\n min_length: number;\n max_length: number;\n};\n\ntype PasswordStrengthProps = {\n password: string;\n policy: PasswordPolicy;\n};\n\nconst LEVELS = ['weak', 'fair', 'good', 'strong'] as const;\n\nconst VARIANTS = {\n weak: 'danger',\n fair: 'warning',\n good: 'info',\n strong: 'success',\n} as const;\n\n/**\n * Scores a password 1-4 for feedback only.\n *\n * Deliberately simple: length against the deployment's own policy plus\n * character-class variety. The server is the authority on what is acceptable,\n * so this only needs to steer the user, and a full entropy estimator would be\n * a large dependency for a hint bar.\n *\n * Returns 0 for \"nothing to say yet\" so the caller can stay silent.\n */\nfunction scorePassword(password: string, policy: PasswordPolicy): number {\n if (password.length === 0) {\n return 0;\n }\n if (password.length < policy.min_length) {\n return 1;\n }\n\n const classes = [/[a-z]/, /[A-Z]/, /\\d/, /[^a-zA-Z0-9]/].filter((pattern) =>\n pattern.test(password),\n ).length;\n\n const isLong = password.length >= policy.min_length + 4;\n\n if (classes >= 3 && isLong) {\n return 4;\n }\n if (classes >= 2 && isLong) {\n return 3;\n }\n return 2;\n}\n\n/**\n * Strength hint under a new-password field.\n *\n * Renders nothing for an empty field so the form does not shift as soon as it\n * is focused. Announced politely rather than assertively — it updates on every\n * keystroke and should not interrupt typing.\n */\nexport function PasswordStrength({ password, policy }: PasswordStrengthProps) {\n const { t } = useTranslation();\n const score = scorePassword(password, policy);\n\n if (score === 0) {\n return null;\n }\n\n const level = LEVELS[score - 1];\n\n return (\n <TRMeter.Root\n aria-live=\"polite\"\n className=\"flex flex-col gap-tinyrack-xs\"\n max={4}\n min={0}\n value={score}\n variant={VARIANTS[level]}\n >\n <div className=\"flex items-baseline justify-between gap-tinyrack-sm\">\n <TRMeter.Label\n render={\n <TRText color=\"muted\" variant=\"caption\">\n {t('validation.password.strength.label')}\n </TRText>\n }\n />\n <TRMeter.Value\n render={\n <TRText color=\"muted\" variant=\"caption\">\n {t(`validation.password.strength.${level}`)}\n </TRText>\n }\n />\n </div>\n <TRMeter.Track>\n <TRMeter.Indicator />\n </TRMeter.Track>\n </TRMeter.Root>\n );\n}\n"],"x_google_ignoreList":[0,1,2,3,4,5,6,7,8,9,10,11],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import{G as e,J as t,Z as n,_ as r,b as i,c as a,i as o,q as s,v as c,x as l}from"./createLucideIcon-BPpAIXR_.js";import{F as u,G as d,P as f,S as ee,U as p,Vt as m,W as h,Zt as te,bn as g,bt as ne,k as _,q as re,xt as v}from"./config-DTQCUlfZ.js";import{A as ie,C as ae,D as oe,T as se,b as ce,c as le,k as y,m as b,x as ue,y as de}from"./shell-header-bar-JcFhS7mb.js";import{t as x}from"./useAriaLabelledBy-IIwdHUss.js";import{t as S}from"./field-BMT214_m.js";import{t as C}from"./separator-B0Bvh1he.js";var w=n(t(),1),T=w.createContext(void 0);function fe(){let e=w.useContext(T);if(e===void 0)throw Error(c(98));return e}function E(e,t,n){return{...e,value:t,index:n,filled:t!==``}}var D={value:()=>null,length:()=>null,...y},O={value:()=>null,index:()=>null,...y},k={numeric:{slotPattern:`\\d{1}`,getRootPattern:e=>`\\d{${e}}`,regexp:/[^\d]/g,inputMode:`numeric`},alpha:{slotPattern:`[a-zA-Z]{1}`,getRootPattern:e=>`[a-zA-Z]{${e}}`,regexp:/[^a-zA-Z]/g,inputMode:`text`},alphanumeric:{slotPattern:`[a-zA-Z0-9]{1}`,getRootPattern:e=>`[a-zA-Z0-9]{${e}}`,regexp:/[^a-zA-Z0-9]/g,inputMode:`text`}};function A(e){return e===`none`?null:k[e]}function j(e){return(e??``).replace(/\s/g,``)}function pe(e,t){return t?e.replace(t.regexp,``):e}function M(e,t,n,r){let i=j(e),a=A(n),o=pe(i,a),s=i.length>o.length;if(r){let e=r(o);s||=o.length>e.length,o=pe(e,a),s||=e.length>o.length}let c=t<0?0:t,l=Array.from(o);return[l.slice(0,c).join(``),s||l.length>c]}function N(e,t,n,r){return M(e,t,n,r)[0]}function me(e,t,n,r,i,a){let o=N(n,r,i,a);return N(`${e.slice(0,t)}${o}${e.slice(t+o.length)}`,r,i,a)}function P(e,t){return t<0||t>=e.length?e:`${e.slice(0,t)}${e.slice(t+1)}`}var F=s(),I=w.forwardRef(function(e,t){let{"aria-describedby":n,"aria-labelledby":o,id:s,autoComplete:c=`one-time-code`,defaultValue:d,value:ee,onValueChange:p,onValueComplete:m,form:h,length:g,autoSubmit:re=!1,mask:le=!1,inputMode:y,validationType:S=`numeric`,normalizeValue:C,disabled:fe=!1,readOnly:E=!1,required:O=!1,name:k,onValueInvalid:j,render:pe,className:me,style:P,...I}=e,{setDirty:R,validityData:z,disabled:B,setFilled:V,invalid:H,name:he,state:ge,validation:U,validationMode:_e,setFocused:ve,setTouched:ye}=oe(),{clearErrors:be}=ce(),{getDescriptionProps:xe,labelId:Se}=ae(),W=B||fe,G=he??k,[Ce,we]=ie({controlled:ee,default:d,name:`OTPField`,state:`value`}),K=w.useRef(null),q=w.useRef([]),Te=w.useRef(null),J=w.useRef(null),Ee=w.useMemo(()=>({get current(){return q.current[0]??null}}),[]),Y=ue({id:s}),De=x(o,Se,Ee,!0,Y),Oe=o==null?De:void 0,ke=L(n,xe({})[`aria-describedby`]),Ae=A(S),je=Ae?.slotPattern,Me=Ae?.getRootPattern(g),Ne=y??Ae?.inputMode,Pe=Number.isInteger(g)&&g>0,X=N(Ce,g,S,C),Z=_(X),Q=X!==``,[Fe,Ie]=w.useState(0),[Le,Re]=w.useState(()=>Math.min(X.length,g-1)),[ze,Be]=w.useState(!1),Ve=ze?Math.min(Le,Math.max(g-1,0)):Math.min(X.length,g-1);i(()=>{V(Q)},[Q,V]),se(Ee,Y,X,void 0,!W,k);let $=l(e=>{let t=Math.min(Math.max(e,0),Math.max(q.current.length-1,0)),n=q.current[t];n?.focus(),n?.select()}),He=l((e,t)=>{Te.current={index:e,value:t}});function Ue(){let e=U.inputRef.current?.form??q.current[0]?.form??null;if(h){let t=r(K.current).getElementById(h);t?.tagName===`FORM`&&(e=t)}e&&typeof e.requestSubmit==`function`&&e.requestSubmit()}function We(e,t){m?.(e,t),re&&Ue()}de(X,()=>{be(G),R(X!==z.initialValue),U.change(X);let e=Te.current;e!=null&&(Te.current=null,e.value===X&&$(e.index));let t=J.current;t!=null&&(J.current=null,t.value===X&&We(X,t.eventDetails))});let Ge=l((e,t)=>{let n=N(e,g,S,C),r=(t.reason===`input-change`||t.reason===`input-paste`)&&n.length===g&&(Z.current.length!==g||t.reason===`input-paste`)?u(t.reason,t.event):null;return n===Z.current?(r!=null&&We(n,r),null):(p?.(n,t),t.isCanceled?null:(we(n),r==null?n.length!==g&&(J.current=null):J.current={value:n,eventDetails:r},n))}),Ke=l((e,t)=>{j?.(e,t)}),qe=l((e,t)=>{if(e>Z.current.length){$(Math.min(Z.current.length,g-1));return}Re(e),Be(!0),ve(!0),t.currentTarget.select()}),Je=l(e=>{te(K.current,e.relatedTarget)||(ye(!0),Be(!1),ve(!1),_e===`onBlur`&&U.commit(Z.current))}),Ye=w.useCallback(e=>{if(Y!=null)return e===0?Y:`${Y}-${e+1}`},[Y]),Xe=w.useMemo(()=>({...ge,complete:X.length===g,disabled:W,filled:Q,focused:ze,length:g,readOnly:E,required:O,value:X}),[W,ge,Q,ze,g,E,O,X]),Ze=w.useMemo(()=>({autoComplete:c,activeIndex:Ve,disabled:W,form:h,focusInput:$,queueFocusInput:He,getInputId:Ye,handleInputBlur:Je,handleInputFocus:qe,inputMode:Ne,inputAriaLabelledBy:Oe,invalid:H,length:g,mask:le,pattern:je,reportValueInvalid:Ke,readOnly:E,required:O,normalizeValue:C,setValue:Ge,state:Xe,validationType:S,value:X}),[Ve,c,W,$,h,Ye,Je,qe,Ne,Oe,H,g,le,je,He,E,Ke,O,C,Ge,Xe,S,X]),Qe=a(`div`,e,{ref:[t,K],state:Xe,props:[{role:`group`,"aria-describedby":ke,"aria-labelledby":De},I],stateAttributesMapping:D});return(0,F.jsx)(b,{elementsRef:q,onMapChange:e=>{Ie(e.size)},children:(0,F.jsxs)(T.Provider,{value:Ze,children:[Qe,Pe&&(0,F.jsx)(`input`,{...U.getValidationProps(W,{onFocus(){$(0)},onChange(e){if(e.nativeEvent.defaultPrevented||W||E)return;let t=e.currentTarget.value,[n,r]=M(t,g,S,C);r&&Ke(t,u(`input-change`,e.nativeEvent));let i=Ge(n,f(`input-change`,e.nativeEvent));i!=null&&i!==``&&He(i.length-1,i)}}),ref:U.inputRef,type:`text`,id:Y&&G==null?`${Y}-hidden-input`:void 0,form:h,name:G,value:X,autoComplete:c,inputMode:Ne,minLength:g,maxLength:g,pattern:Me,disabled:W,readOnly:E,required:O,"aria-hidden":!0,tabIndex:-1,style:G?v:ne})]})})});function L(...e){let t=e.flatMap(e=>e?.split(/\s+/).filter(Boolean)??[]);return t.length>0?Array.from(new Set(t)).join(` `):void 0}var R=g(w.forwardRef(function(e,t){let{"aria-label":n,"aria-labelledby":r,render:i,className:o,style:s,...c}=e,{activeIndex:l,autoComplete:te,disabled:g,form:ne,focusInput:_,queueFocusInput:v,getInputId:ie,handleInputBlur:ae,handleInputFocus:oe,inputMode:se,inputAriaLabelledBy:ce,invalid:y,length:b,mask:ue,pattern:de,reportValueInvalid:x,readOnly:S,required:C,normalizeValue:T,setValue:D,state:k,validationType:A,value:j}=fe(),{ref:pe,index:N}=le({guess:!0}),F=w.useRef(null),I=ee(),L=j[N]??``,R=E(k,L,N),z=n,B=r??ce,V=N===0?void 0:z,H={id:ie(N),value:L,type:ue?`password`:`text`,inputMode:se,autoComplete:N===0?te:`off`,autoCorrect:`off`,spellCheck:`false`,enterKeyHint:N===b-1?`done`:`next`,maxLength:N===0?b:void 0,tabIndex:l===N?0:-1,disabled:g,form:ne,pattern:de,readOnly:S,required:C,"aria-labelledby":V==null?B:void 0,"aria-invalid":!g&&y?!0:void 0,"aria-label":V,onMouseDown(e){e.defaultPrevented||g||(e.preventDefault(),_(N))},onFocus(e){e.defaultPrevented||g||oe(N,e)},onBlur(e){e.defaultPrevented||ae(e)},onChange(e){if(e.defaultPrevented||g||S)return;let t=e.currentTarget.value,[n,r]=M(t,b,A,T);if(r&&x(t,u(p,e.nativeEvent)),n===``){t===``?D(P(j,N),f(h,e.nativeEvent)):L!==``&&(e.currentTarget.value=L,e.currentTarget.select());return}let i=me(j,N,n,b,A,T),a=D(i,f(p,e.nativeEvent));if(a!=null){let e=Math.min(N+n.length,b-1);v(e,a)}},onKeyDown(e){if(e.defaultPrevented||g)return;let t=Math.max(b-1,0),n=Math.min(j.length,t),r=(e.ctrlKey||e.metaKey)&&!e.altKey,i=I===`rtl`,a=i?`ArrowRight`:`ArrowLeft`,o=i?`ArrowLeft`:`ArrowRight`;if(e.key===a){m(e),_(r?0:Math.max(0,N-1));return}if(e.key===o){m(e),_(r?n:Math.min(t,N+1));return}if(e.key===`Home`||e.key===`ArrowUp`){m(e),_(0);return}if(e.key===`End`||e.key===`ArrowDown`){m(e),_(n);return}if(S)return;function s(t,n){let r=D(t,f(re,e.nativeEvent));r!=null&&v(n,r)}if(e.key===`Backspace`&&r){m(e),s(``,0);return}if(e.key===`Delete`){m(e),s(P(j,N),N);return}let c=e.currentTarget.value,l=e.currentTarget.selectionStart===0&&e.currentTarget.selectionEnd===c.length;if(e.key.length===1&&l&&L===e.key){m(e),N<b-1&&_(N+1);return}if(e.key===`Backspace`){m(e);let t=Math.max(0,N-1);s(P(j,L===``?t:N),t)}},onPaste(e){if(e.defaultPrevented||g||S)return;let t=``;try{t=e.clipboardData?.getData(`text/plain`)??``}catch{return}e.preventDefault();let[n,r]=M(t,b,A,T);if(r&&x(t,u(d,e.nativeEvent)),n===``)return;let i=D(me(j,N,n,b,A,T),f(d,e.nativeEvent));if(i!=null){let e=Math.min(N+n.length,b-1);v(e,i)}}};return a(`input`,e,{ref:[t,pe,F],state:R,props:[H,c],stateAttributesMapping:O})}),`tr-otp-field-digit`,{tracksFocusModality:!0});function z({appearance:t=`solid`,className:n,layout:r=`compact`,uiSize:i=`md`,...a}){return(0,F.jsx)(I,{...a,className:e(`tr-otp-field`,n),"data-appearance":t,"data-layout":r,"data-ui-size":i})}function B({className:t,orientation:n=`vertical`,...r}){return(0,F.jsx)(C,{...r,className:e(`tr-otp-field-separator`,t),orientation:n})}var V={Root:z,Input:R,Separator:B},H=(0,w.forwardRef)(function({length:e=6,error:t,disabled:n=!1,onComplete:r,onChange:i,value:a,autoFocus:s=!1,className:c=``},l){let{t:u}=o(),d=(0,w.useId)(),f=(0,w.useRef)(null),[ee,p]=(0,w.useState)(a??``);(0,w.useEffect)(()=>{a!==void 0&&p(a)},[a]),(0,w.useImperativeHandle)(l,()=>({focus:()=>{(f.current?.querySelector(`input`))?.focus()},clear:()=>{p(``),(f.current?.querySelector(`input`))?.focus()}}),[]);let m=(0,w.useCallback)(e=>{p(e),i?.(e)},[i]),h=(0,w.useCallback)(e=>{r?.(e)},[r]);return(0,F.jsxs)(S.Root,{className:`w-full ${c}`,ref:f,children:[(0,F.jsx)(S.Label,{className:`sr-only`,htmlFor:d,children:u(`common.otp.label`)}),(0,F.jsx)(V.Root,{disabled:n,id:d,layout:`stretch`,length:e,onValueChange:m,onValueComplete:h,uiSize:`lg`,validationType:`numeric`,value:ee,children:Array.from({length:e},(t,n)=>(0,F.jsx)(V.Input,{"aria-label":n===0?void 0:u(`common.otp.digit`,{label:u(`common.otp.label`),position:n+1,total:e}),autoFocus:s&&n===0},n))}),t&&(0,F.jsx)(`p`,{className:`mt-tinyrack-sm text-center text-tinyrack-danger-foreground text-tinyrack-sm`,"data-testid":`pin-input-error`,children:t.message})]})});export{H as t};
2
+ //# sourceMappingURL=pin-input-CITrr-Yc.js.map