@tinyrack/issuary-server 0.21.0 → 0.22.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 (306) hide show
  1. package/dist/entities/oauth-client.entity.d.ts +18 -0
  2. package/dist/entities/oauth-client.entity.d.ts.map +1 -1
  3. package/dist/entities/oauth-code.entity.d.ts +18 -0
  4. package/dist/entities/oauth-code.entity.d.ts.map +1 -1
  5. package/dist/entities/oauth-device-code.entity.d.ts +18 -0
  6. package/dist/entities/oauth-device-code.entity.d.ts.map +1 -1
  7. package/dist/entities/revoked-token.entity.d.ts +18 -0
  8. package/dist/entities/revoked-token.entity.d.ts.map +1 -1
  9. package/dist/entities/terms-content.entity.d.ts +9 -0
  10. package/dist/entities/terms-content.entity.d.ts.map +1 -1
  11. package/dist/entities/terms.entity.d.ts +9 -0
  12. package/dist/entities/terms.entity.d.ts.map +1 -1
  13. package/dist/entities/user-consent.entity.d.ts +18 -0
  14. package/dist/entities/user-consent.entity.d.ts.map +1 -1
  15. package/dist/entities/user-terms-consent.entity.d.ts +9 -0
  16. package/dist/entities/user-terms-consent.entity.d.ts.map +1 -1
  17. package/dist/entities/user-totp-recovery-code.entity.d.ts +9 -0
  18. package/dist/entities/user-totp-recovery-code.entity.d.ts.map +1 -1
  19. package/dist/entities/user-totp.entity.d.ts +9 -0
  20. package/dist/entities/user-totp.entity.d.ts.map +1 -1
  21. package/dist/entities/user.entity.d.ts +9 -0
  22. package/dist/entities/user.entity.d.ts.map +1 -1
  23. package/dist/entities/user.entity.js +4 -0
  24. package/dist/entities/user.entity.js.map +1 -1
  25. package/dist/entrypoints/app.d.ts +53 -0
  26. package/dist/entrypoints/app.d.ts.map +1 -1
  27. package/dist/entrypoints/database/postgres/compiled-functions.d.ts +23 -20
  28. package/dist/entrypoints/database/postgres/compiled-functions.d.ts.map +1 -1
  29. package/dist/entrypoints/database/postgres/compiled-functions.js +134 -104
  30. package/dist/entrypoints/database/postgres/compiled-functions.js.map +1 -1
  31. package/dist/entrypoints/database/sqlite/compiled-functions.d.ts +23 -20
  32. package/dist/entrypoints/database/sqlite/compiled-functions.d.ts.map +1 -1
  33. package/dist/entrypoints/database/sqlite/compiled-functions.js +134 -104
  34. package/dist/entrypoints/database/sqlite/compiled-functions.js.map +1 -1
  35. package/dist/lib/config/resolved.d.ts +1 -0
  36. package/dist/lib/config/resolved.d.ts.map +1 -1
  37. package/dist/lib/config/security.d.ts +2 -0
  38. package/dist/lib/config/security.d.ts.map +1 -1
  39. package/dist/lib/config/security.js +5 -0
  40. package/dist/lib/config/security.js.map +1 -1
  41. package/dist/middleware/auth.d.ts +8 -0
  42. package/dist/middleware/auth.d.ts.map +1 -1
  43. package/dist/middleware/auth.js +64 -0
  44. package/dist/middleware/auth.js.map +1 -1
  45. package/dist/middleware/session.d.ts +9 -0
  46. package/dist/middleware/session.d.ts.map +1 -1
  47. package/dist/middleware/session.js +18 -0
  48. package/dist/middleware/session.js.map +1 -1
  49. package/dist/migrations/postgres/Migration20260825110000_add_password_reset_required.d.ts +6 -0
  50. package/dist/migrations/postgres/Migration20260825110000_add_password_reset_required.d.ts.map +1 -0
  51. package/dist/migrations/postgres/Migration20260825110000_add_password_reset_required.js +11 -0
  52. package/dist/migrations/postgres/Migration20260825110000_add_password_reset_required.js.map +1 -0
  53. package/dist/migrations/postgres/index.d.ts.map +1 -1
  54. package/dist/migrations/postgres/index.js +2 -0
  55. package/dist/migrations/postgres/index.js.map +1 -1
  56. package/dist/migrations/sqlite/Migration20260825110000_add_password_reset_required.d.ts +6 -0
  57. package/dist/migrations/sqlite/Migration20260825110000_add_password_reset_required.d.ts.map +1 -0
  58. package/dist/migrations/sqlite/Migration20260825110000_add_password_reset_required.js +10 -0
  59. package/dist/migrations/sqlite/Migration20260825110000_add_password_reset_required.js.map +1 -0
  60. package/dist/migrations/sqlite/index.d.ts.map +1 -1
  61. package/dist/migrations/sqlite/index.js +2 -0
  62. package/dist/migrations/sqlite/index.js.map +1 -1
  63. package/dist/repositories/oauth-code.repository.d.ts +2 -2
  64. package/dist/repositories/oauth-code.repository.d.ts.map +1 -1
  65. package/dist/repositories/oauth-code.repository.js +7 -3
  66. package/dist/repositories/oauth-code.repository.js.map +1 -1
  67. package/dist/repositories/oauth-device-code.repository.d.ts +4 -4
  68. package/dist/repositories/oauth-device-code.repository.d.ts.map +1 -1
  69. package/dist/repositories/oauth-device-code.repository.js +18 -6
  70. package/dist/repositories/oauth-device-code.repository.js.map +1 -1
  71. package/dist/repositories/user-totp-recovery-code.repository.d.ts +7 -1
  72. package/dist/repositories/user-totp-recovery-code.repository.d.ts.map +1 -1
  73. package/dist/repositories/user-totp-recovery-code.repository.js +2 -1
  74. package/dist/repositories/user-totp-recovery-code.repository.js.map +1 -1
  75. package/dist/repositories/user-totp.repository.d.ts +9 -0
  76. package/dist/repositories/user-totp.repository.d.ts.map +1 -1
  77. package/dist/routes/api/auth/index.d.ts +52 -0
  78. package/dist/routes/api/auth/index.d.ts.map +1 -1
  79. package/dist/routes/api/auth/index.js +2 -0
  80. package/dist/routes/api/auth/index.js.map +1 -1
  81. package/dist/routes/api/auth/passkey/verify/post.d.ts +37 -0
  82. package/dist/routes/api/auth/passkey/verify/post.d.ts.map +1 -1
  83. package/dist/routes/api/auth/passkey/verify/post.js +8 -2
  84. package/dist/routes/api/auth/passkey/verify/post.js.map +1 -1
  85. package/dist/routes/api/auth/password/reset-required/post.d.ts +18 -0
  86. package/dist/routes/api/auth/password/reset-required/post.d.ts.map +1 -0
  87. package/dist/routes/api/auth/password/reset-required/post.js +39 -0
  88. package/dist/routes/api/auth/password/reset-required/post.js.map +1 -0
  89. package/dist/routes/api/index.d.ts +52 -0
  90. package/dist/routes/api/index.d.ts.map +1 -1
  91. package/dist/routes/index.d.ts +52 -0
  92. package/dist/routes/index.d.ts.map +1 -1
  93. package/dist/routes/oauth/device/get-post.js +5 -5
  94. package/dist/routes/oauth/device/get-post.js.map +1 -1
  95. package/dist/schemas/error.d.ts +25 -0
  96. package/dist/schemas/error.d.ts.map +1 -1
  97. package/dist/schemas/error.js +1 -0
  98. package/dist/schemas/error.js.map +1 -1
  99. package/dist/schemas/response.d.ts +22 -0
  100. package/dist/schemas/response.d.ts.map +1 -1
  101. package/dist/schemas/response.js +8 -0
  102. package/dist/schemas/response.js.map +1 -1
  103. package/dist/seeders/config.seeder.js +2 -1
  104. package/dist/seeders/config.seeder.js.map +1 -1
  105. package/dist/services/container.d.ts +1 -0
  106. package/dist/services/container.d.ts.map +1 -1
  107. package/dist/services/container.js +15 -0
  108. package/dist/services/container.js.map +1 -1
  109. package/dist/services/legacy-credential-retirement.service.d.ts +20 -0
  110. package/dist/services/legacy-credential-retirement.service.d.ts.map +1 -0
  111. package/dist/services/legacy-credential-retirement.service.js +90 -0
  112. package/dist/services/legacy-credential-retirement.service.js.map +1 -0
  113. package/dist/services/oauth-client.service.d.ts +1 -0
  114. package/dist/services/oauth-client.service.d.ts.map +1 -1
  115. package/dist/services/oauth-client.service.js +17 -2
  116. package/dist/services/oauth-client.service.js.map +1 -1
  117. package/dist/services/oauth-connect.service.d.ts.map +1 -1
  118. package/dist/services/oauth-connect.service.js +4 -0
  119. package/dist/services/oauth-connect.service.js.map +1 -1
  120. package/dist/services/oauth-token.service.d.ts.map +1 -1
  121. package/dist/services/oauth-token.service.js +4 -4
  122. package/dist/services/oauth-token.service.js.map +1 -1
  123. package/dist/services/password-auth.service.d.ts.map +1 -1
  124. package/dist/services/password-auth.service.js +12 -3
  125. package/dist/services/password-auth.service.js.map +1 -1
  126. package/dist/services/security.service.d.ts +8 -0
  127. package/dist/services/security.service.d.ts.map +1 -1
  128. package/dist/services/security.service.js +85 -22
  129. package/dist/services/security.service.js.map +1 -1
  130. package/dist/services/totp.service.js +2 -2
  131. package/dist/services/totp.service.js.map +1 -1
  132. package/package.json +1 -1
  133. package/public/assets/2fa-BgveHSB_.js +2 -0
  134. package/public/assets/{2fa-B5vLgDXQ.js.map → 2fa-BgveHSB_.js.map} +1 -1
  135. package/public/assets/2fa-C37MUkye.js +2 -0
  136. package/public/assets/{2fa-CCOTIHDZ.js.map → 2fa-C37MUkye.js.map} +1 -1
  137. package/public/assets/2fa-UL_cNRnQ.js +2 -0
  138. package/public/assets/{2fa-C5vUVgr_.js.map → 2fa-UL_cNRnQ.js.map} +1 -1
  139. package/public/assets/2fa-kiqzGSz2.js +2 -0
  140. package/public/assets/{2fa-BqsM_D-X.js.map → 2fa-kiqzGSz2.js.map} +1 -1
  141. package/public/assets/{DialogHandle-BmtK2pRn.js → DialogHandle-Zvz8SgS0.js} +2 -2
  142. package/public/assets/{DialogHandle-BmtK2pRn.js.map → DialogHandle-Zvz8SgS0.js.map} +1 -1
  143. package/public/assets/admin-DbDuu6AI.js +2 -0
  144. package/public/assets/{admin-BPijkLw-.js.map → admin-DbDuu6AI.js.map} +1 -1
  145. package/public/assets/admin-users-CwTJdiih.js +2 -0
  146. package/public/assets/{admin-users-DvPbw1Vq.js.map → admin-users-CwTJdiih.js.map} +1 -1
  147. package/public/assets/{auth-choice-row-BCGjuedo.js → auth-choice-row-BlSsUI7d.js} +2 -2
  148. package/public/assets/{auth-choice-row-BCGjuedo.js.map → auth-choice-row-BlSsUI7d.js.map} +1 -1
  149. package/public/assets/{auth-field-DD8kvl--.js → auth-field-HwxENJnB.js} +2 -2
  150. package/public/assets/{auth-field-DD8kvl--.js.map → auth-field-HwxENJnB.js.map} +1 -1
  151. package/public/assets/{auth-layout-VtpryDof.js → auth-layout-CoeqNhTT.js} +2 -2
  152. package/public/assets/{auth-layout-VtpryDof.js.map → auth-layout-CoeqNhTT.js.map} +1 -1
  153. package/public/assets/{auth-page-header-CIn2E4zI.js → auth-page-header-CIs08f5Y.js} +2 -2
  154. package/public/assets/{auth-page-header-CIn2E4zI.js.map → auth-page-header-CIs08f5Y.js.map} +1 -1
  155. package/public/assets/{authorization-context-banner-SOuBHWIb.js → authorization-context-banner-BbtQmMYH.js} +2 -2
  156. package/public/assets/{authorization-context-banner-SOuBHWIb.js.map → authorization-context-banner-BbtQmMYH.js.map} +1 -1
  157. package/public/assets/{avatar-BEQjf2x6.js → avatar-CttWiIv3.js} +2 -2
  158. package/public/assets/{avatar-BEQjf2x6.js.map → avatar-CttWiIv3.js.map} +1 -1
  159. package/public/assets/{checkbox-gdHnSFQ-.js → checkbox-CJrRV8S1.js} +2 -2
  160. package/public/assets/{checkbox-gdHnSFQ-.js.map → checkbox-CJrRV8S1.js.map} +1 -1
  161. package/public/assets/config-v3-oy_ef.js +2 -0
  162. package/public/assets/config-v3-oy_ef.js.map +1 -0
  163. package/public/assets/{consent-BqqFDdsr.js → consent-BIhxtXHx.js} +2 -2
  164. package/public/assets/{consent-BqqFDdsr.js.map → consent-BIhxtXHx.js.map} +1 -1
  165. package/public/assets/{consent-CfRbz2R7.js → consent-DRmENn26.js} +2 -2
  166. package/public/assets/{consent-CfRbz2R7.js.map → consent-DRmENn26.js.map} +1 -1
  167. package/public/assets/{email-CKKakxPJ.js → email-CVRDeJ78.js} +2 -2
  168. package/public/assets/{email-CKKakxPJ.js.map → email-CVRDeJ78.js.map} +1 -1
  169. package/public/assets/email-UL_cNRnQ.js +2 -0
  170. package/public/assets/{email-C5vUVgr_.js.map → email-UL_cNRnQ.js.map} +1 -1
  171. package/public/assets/error-2VPY5w8V.js +2 -0
  172. package/public/assets/{error-BMXxpgfR.js.map → error-2VPY5w8V.js.map} +1 -1
  173. package/public/assets/{field-Cn4H1CUj.js → field-DDt6F9TZ.js} +2 -2
  174. package/public/assets/{field-Cn4H1CUj.js.map → field-DDt6F9TZ.js.map} +1 -1
  175. package/public/assets/forgot-DzDcduho.js +2 -0
  176. package/public/assets/{forgot-rO9sByfV.js.map → forgot-DzDcduho.js.map} +1 -1
  177. package/public/assets/forgot-UL_cNRnQ.js +2 -0
  178. package/public/assets/{forgot-C5vUVgr_.js.map → forgot-UL_cNRnQ.js.map} +1 -1
  179. package/public/assets/index-gliHKncz.js +11 -0
  180. package/public/assets/index-gliHKncz.js.map +1 -0
  181. package/public/assets/{initial-avatar-CtuDUz_W.js → initial-avatar-CZzHAPm3.js} +2 -2
  182. package/public/assets/{initial-avatar-CtuDUz_W.js.map → initial-avatar-CZzHAPm3.js.map} +1 -1
  183. package/public/assets/{input-CzQki7k6.js → input-CNRpSR08.js} +2 -2
  184. package/public/assets/{input-CzQki7k6.js.map → input-CNRpSR08.js.map} +1 -1
  185. package/public/assets/{labeled-separator-CV7nb0XW.js → labeled-separator-fnKySKv2.js} +2 -2
  186. package/public/assets/{labeled-separator-CV7nb0XW.js.map → labeled-separator-fnKySKv2.js.map} +1 -1
  187. package/public/assets/link-BN90emXr.js +2 -0
  188. package/public/assets/link-BN90emXr.js.map +1 -0
  189. package/public/assets/lock-keyhole-Cz72xkCn.js +2 -0
  190. package/public/assets/lock-keyhole-Cz72xkCn.js.map +1 -0
  191. package/public/assets/login-C6m2qSUt.js +2 -0
  192. package/public/assets/login-C6m2qSUt.js.map +1 -0
  193. package/public/assets/login-UL_cNRnQ.js +2 -0
  194. package/public/assets/login-UL_cNRnQ.js.map +1 -0
  195. package/public/assets/{logout-B5T52P8u.js → logout-CHdmF_16.js} +2 -2
  196. package/public/assets/{logout-B5T52P8u.js.map → logout-CHdmF_16.js.map} +1 -1
  197. package/public/assets/{modal-BDQFucjz.js → modal-sCM_zT82.js} +2 -2
  198. package/public/assets/{modal-BDQFucjz.js.map → modal-sCM_zT82.js.map} +1 -1
  199. package/public/assets/{mutationOptions-DyYFUbyn.js → mutationOptions-CrswitPs.js} +2 -2
  200. package/public/assets/{mutationOptions-DyYFUbyn.js.map → mutationOptions-CrswitPs.js.map} +1 -1
  201. package/public/assets/passkey-B2Blw47e.js +2 -0
  202. package/public/assets/{passkey-J1qnYp9A.js.map → passkey-B2Blw47e.js.map} +1 -1
  203. package/public/assets/passkey-CEx1VZ5R.js +2 -0
  204. package/public/assets/{passkey-CynCz1Dt.js.map → passkey-CEx1VZ5R.js.map} +1 -1
  205. package/public/assets/passkey-CU8x9J2w.js +2 -0
  206. package/public/assets/{passkey-BE9JvJOP.js.map → passkey-CU8x9J2w.js.map} +1 -1
  207. package/public/assets/passkey-UL_cNRnQ.js +2 -0
  208. package/public/assets/{passkey-C5vUVgr_.js.map → passkey-UL_cNRnQ.js.map} +1 -1
  209. package/public/assets/password-BhU_a0cR.js +2 -0
  210. package/public/assets/password-BhU_a0cR.js.map +1 -0
  211. package/public/assets/password-UL_cNRnQ.js +2 -0
  212. package/public/assets/password-UL_cNRnQ.js.map +1 -0
  213. package/public/assets/password-reset-CIacHnvA.js +2 -0
  214. package/public/assets/{password-reset-sU5KkBKi.js.map → password-reset-CIacHnvA.js.map} +1 -1
  215. package/public/assets/{password-strength-BeSOu_Ga.js → password-strength-C0Zkrrct.js} +2 -2
  216. package/public/assets/{password-strength-BeSOu_Ga.js.map → password-strength-C0Zkrrct.js.map} +1 -1
  217. package/public/assets/{pin-input-Cc-VkrQ3.js → pin-input-BlD4tPZJ.js} +2 -2
  218. package/public/assets/{pin-input-Cc-VkrQ3.js.map → pin-input-BlD4tPZJ.js.map} +1 -1
  219. package/public/assets/{profile-BcjsVYOx.js → profile-D1cEjfs8.js} +2 -2
  220. package/public/assets/{profile-BcjsVYOx.js.map → profile-D1cEjfs8.js.map} +1 -1
  221. package/public/assets/{profile-B5NaJfeS.js → profile-D43Yq-6S.js} +2 -2
  222. package/public/assets/{profile-B5NaJfeS.js.map → profile-D43Yq-6S.js.map} +1 -1
  223. package/public/assets/recovery-CLExnfYX.js +2 -0
  224. package/public/assets/{recovery-PnUATiUi.js.map → recovery-CLExnfYX.js.map} +1 -1
  225. package/public/assets/register-CZUyWXRO.js +2 -0
  226. package/public/assets/{register-DQXhru0_.js.map → register-CZUyWXRO.js.map} +1 -1
  227. package/public/assets/register-UL_cNRnQ.js +2 -0
  228. package/public/assets/{register-C5vUVgr_.js.map → register-UL_cNRnQ.js.map} +1 -1
  229. package/public/assets/reset-N3RQTYEB.js +2 -0
  230. package/public/assets/reset-N3RQTYEB.js.map +1 -0
  231. package/public/assets/reset-UL_cNRnQ.js +2 -0
  232. package/public/assets/{reset-C5vUVgr_.js.map → reset-UL_cNRnQ.js.map} +1 -1
  233. package/public/assets/reset-required-Cv5edDS6.js +2 -0
  234. package/public/assets/reset-required-Cv5edDS6.js.map +1 -0
  235. package/public/assets/reset-required-UL_cNRnQ.js +2 -0
  236. package/public/assets/reset-required-UL_cNRnQ.js.map +1 -0
  237. package/public/assets/{select-DqZynXSt.js → select-CWdSzsVo.js} +2 -2
  238. package/public/assets/{select-DqZynXSt.js.map → select-CWdSzsVo.js.map} +1 -1
  239. package/public/assets/select-UL_cNRnQ.js +2 -0
  240. package/public/assets/{select-C5vUVgr_.js.map → select-UL_cNRnQ.js.map} +1 -1
  241. package/public/assets/{separator-B0Bvh1he.js → separator-BWGym8uM.js} +2 -2
  242. package/public/assets/{separator-B0Bvh1he.js.map → separator-BWGym8uM.js.map} +1 -1
  243. package/public/assets/shell-header-bar-C9AB5lT-.js +2 -0
  244. package/public/assets/{shell-header-bar-CjG5qvPg.js.map → shell-header-bar-C9AB5lT-.js.map} +1 -1
  245. package/public/assets/{terms-BHDAgn-H.js → terms-COVFmzyl.js} +2 -2
  246. package/public/assets/{terms-BHDAgn-H.js.map → terms-COVFmzyl.js.map} +1 -1
  247. package/public/assets/{terms-B5NaJfeS.js → terms-D43Yq-6S.js} +2 -2
  248. package/public/assets/{terms-B5NaJfeS.js.map → terms-D43Yq-6S.js.map} +1 -1
  249. package/public/assets/{terms-checkbox-list-6jteUTIZ.js → terms-checkbox-list-Cw0zB-lO.js} +2 -2
  250. package/public/assets/{terms-checkbox-list-6jteUTIZ.js.map → terms-checkbox-list-Cw0zB-lO.js.map} +1 -1
  251. package/public/assets/totp-BFJEXxk0.js +2 -0
  252. package/public/assets/{totp-CJtfVNEq.js.map → totp-BFJEXxk0.js.map} +1 -1
  253. package/public/assets/{totp-BNZ904Zm.js → totp-BfCm6IWl.js} +2 -2
  254. package/public/assets/{totp-BNZ904Zm.js.map → totp-BfCm6IWl.js.map} +1 -1
  255. package/public/assets/totp-MINhsVxT.js +2 -0
  256. package/public/assets/{totp-ECvOUcFx.js.map → totp-MINhsVxT.js.map} +1 -1
  257. package/public/assets/use-totp-setup-C7Zk_2IY.js +3 -0
  258. package/public/assets/{use-totp-setup-BbS3XcZA.js.map → use-totp-setup-C7Zk_2IY.js.map} +1 -1
  259. package/public/assets/{useAriaLabelledBy-ByuMi3Ry.js → useAriaLabelledBy-CiOCg6X5.js} +2 -2
  260. package/public/assets/{useAriaLabelledBy-ByuMi3Ry.js.map → useAriaLabelledBy-CiOCg6X5.js.map} +1 -1
  261. package/public/assets/{useMutation-BWZ0G4Td.js → useMutation-ZLFeJg75.js} +2 -2
  262. package/public/assets/{useMutation-BWZ0G4Td.js.map → useMutation-ZLFeJg75.js.map} +1 -1
  263. package/public/assets/useQuery-BvMz34BW.js +2 -0
  264. package/public/assets/{useQuery-Ckxjmvtl.js.map → useQuery-BvMz34BW.js.map} +1 -1
  265. package/public/assets/{users-CmFxrCQT.js → users-COM7u3A-.js} +2 -2
  266. package/public/assets/{users-CmFxrCQT.js.map → users-COM7u3A-.js.map} +1 -1
  267. package/public/index.html +4 -3
  268. package/public/assets/2fa-B5vLgDXQ.js +0 -2
  269. package/public/assets/2fa-BqsM_D-X.js +0 -2
  270. package/public/assets/2fa-C5vUVgr_.js +0 -2
  271. package/public/assets/2fa-CCOTIHDZ.js +0 -2
  272. package/public/assets/admin-BPijkLw-.js +0 -2
  273. package/public/assets/admin-users-DvPbw1Vq.js +0 -2
  274. package/public/assets/config-DTQCUlfZ.js +0 -2
  275. package/public/assets/config-DTQCUlfZ.js.map +0 -1
  276. package/public/assets/email-C5vUVgr_.js +0 -2
  277. package/public/assets/error-BMXxpgfR.js +0 -2
  278. package/public/assets/forgot-C5vUVgr_.js +0 -2
  279. package/public/assets/forgot-rO9sByfV.js +0 -2
  280. package/public/assets/index-CS23Ozt6.js +0 -11
  281. package/public/assets/index-CS23Ozt6.js.map +0 -1
  282. package/public/assets/login-C5vUVgr_.js +0 -2
  283. package/public/assets/login-C5vUVgr_.js.map +0 -1
  284. package/public/assets/login-nukebli4.js +0 -2
  285. package/public/assets/login-nukebli4.js.map +0 -1
  286. package/public/assets/passkey-BE9JvJOP.js +0 -2
  287. package/public/assets/passkey-C5vUVgr_.js +0 -2
  288. package/public/assets/passkey-CynCz1Dt.js +0 -2
  289. package/public/assets/passkey-J1qnYp9A.js +0 -2
  290. package/public/assets/password-C5vUVgr_.js +0 -2
  291. package/public/assets/password-C5vUVgr_.js.map +0 -1
  292. package/public/assets/password-RkUttbhD.js +0 -2
  293. package/public/assets/password-RkUttbhD.js.map +0 -1
  294. package/public/assets/password-reset-sU5KkBKi.js +0 -2
  295. package/public/assets/recovery-PnUATiUi.js +0 -2
  296. package/public/assets/register-C5vUVgr_.js +0 -2
  297. package/public/assets/register-DQXhru0_.js +0 -2
  298. package/public/assets/reset-C5vUVgr_.js +0 -2
  299. package/public/assets/reset-CQgIa3yf.js +0 -2
  300. package/public/assets/reset-CQgIa3yf.js.map +0 -1
  301. package/public/assets/select-C5vUVgr_.js +0 -2
  302. package/public/assets/shell-header-bar-CjG5qvPg.js +0 -2
  303. package/public/assets/totp-CJtfVNEq.js +0 -2
  304. package/public/assets/totp-ECvOUcFx.js +0 -2
  305. package/public/assets/use-totp-setup-BbS3XcZA.js +0 -3
  306. package/public/assets/useQuery-Ckxjmvtl.js +0 -2
@@ -1,2 +1,2 @@
1
- import{q as e}from"./createLucideIcon-BPpAIXR_.js";import{I as t}from"./index-CS23Ozt6.js";var n=e();function r(e){return(0,n.jsx)(t,{...e,onUnauthorized:()=>{window.location.href=`/login`}})}export{r as errorComponent};
2
- //# sourceMappingURL=profile-B5NaJfeS.js.map
1
+ import{q as e}from"./createLucideIcon-BPpAIXR_.js";import{I as t}from"./index-gliHKncz.js";var n=e();function r(e){return(0,n.jsx)(t,{...e,onUnauthorized:()=>{window.location.href=`/login`}})}export{r as errorComponent};
2
+ //# sourceMappingURL=profile-D43Yq-6S.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"profile-B5NaJfeS.js","names":["RouteErrorFallback","PasswordModalType","TotpModalType","PasskeyModalType","ProfileError","props","ErrorComponentProps","window","location","href","errorComponent"],"sources":["../../../frontend/src/routes/profile/index.tsx?tsr-split=errorComponent"],"sourcesContent":["import {\n useMutation,\n useQueryClient,\n useSuspenseQuery,\n} from '@tanstack/react-query';\nimport {\n createFileRoute,\n type ErrorComponentProps,\n redirect,\n useRouter,\n} from '@tanstack/react-router';\nimport { TRButton } from '@tinyrack/ui/components/button';\nimport { TRCard } from '@tinyrack/ui/components/card';\nimport { TRText } from '@tinyrack/ui/components/text';\nimport { CircleAlertIcon, LogOutIcon } from 'lucide-react';\nimport { useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport { AuthPageHeader } from '#frontend/components/auth/auth-page-header.tsx';\nimport { DangerZoneSection } from '#frontend/components/profile/danger-zone-section.tsx';\nimport { LinkedAccountsSection } from '#frontend/components/profile/linked-accounts-section.tsx';\nimport { PasskeySection } from '#frontend/components/profile/passkey-section.tsx';\nimport { PasswordSection } from '#frontend/components/profile/password-section.tsx';\nimport { TotpSection } from '#frontend/components/profile/totp-section.tsx';\nimport { UnlinkOAuthModal } from '#frontend/components/profile/unlink-oauth-modal.tsx';\nimport { UserInfoSection } from '#frontend/components/profile/user-info-section.tsx';\nimport { Alert } from '#frontend/components/ui/alert.tsx';\nimport { InitialAvatar } from '#frontend/components/ui/initial-avatar.tsx';\nimport { RouteErrorFallback } from '#frontend/components/ui/route-error-fallback.tsx';\nimport { AppLayout } from '#frontend/features/layout/app-layout.tsx';\nimport { ChangePasswordModal } from '#frontend/features/profile/change-password-modal.tsx';\nimport { DeleteAccountModal } from '#frontend/features/profile/delete-account-modal.tsx';\nimport { DisableTotpModal } from '#frontend/features/profile/disable-totp-modal.tsx';\nimport { ManagePasskeysModal } from '#frontend/features/profile/manage-passkeys-modal.tsx';\nimport { RegenerateTotpRecoveryCodesModal } from '#frontend/features/profile/regenerate-totp-recovery-codes-modal.tsx';\nimport { RemovePasswordModal } from '#frontend/features/profile/remove-password-modal.tsx';\nimport { SetPasswordModal } from '#frontend/features/profile/set-password-modal.tsx';\nimport { SetupPasskeyModal } from '#frontend/features/profile/setup-passkey-modal.tsx';\nimport { SetupTotpModal } from '#frontend/features/profile/setup-totp-modal.tsx';\nimport { tick } from '#frontend/libs/promise.ts';\nimport { appConfigQueryOptions } from '#frontend/queries/config.ts';\nimport { logoutMutationOptions } from '#frontend/queries/logout.ts';\nimport {\n getOAuthAuthorizeUrl,\n oauthAccountsQueryOptions,\n unlinkOAuthMutationOptions,\n} from '#frontend/queries/oauth.ts';\nimport { getSessionQueryOptions } from '#frontend/queries/session.ts';\n\ntype PasswordModalType = 'set' | 'change' | 'remove' | null;\ntype TotpModalType = 'setup' | 'disable' | 'regenerate' | null;\ntype PasskeyModalType = 'setup' | 'manage' | null;\n\nconst SearchSchema = z.object({\n oauth_error: z.string().optional(),\n oauth_error_description: z.string().optional(),\n});\n\nconst OAUTH_ERROR_I18N_MAP: Record<string, string> = {\n access_denied: 'oauth.error.accessDenied',\n temporarily_unavailable: 'oauth.error.temporarilyUnavailable',\n server_error: 'oauth.error.serverError',\n};\n\nfunction ProfileError(props: ErrorComponentProps) {\n return (\n <RouteErrorFallback\n {...props}\n onUnauthorized={() => {\n window.location.href = '/login';\n }}\n />\n );\n}\n\nexport const Route = createFileRoute('/profile/')({\n component: Profile,\n errorComponent: ProfileError,\n validateSearch: SearchSchema,\n beforeLoad: async ({ context }) => {\n if (!context.user) {\n throw redirect({\n to: '/login',\n });\n }\n },\n loader: async ({ context }) => {\n await Promise.all([\n context.queryClient.ensureQueryData(getSessionQueryOptions),\n context.queryClient.ensureQueryData(oauthAccountsQueryOptions),\n context.queryClient.ensureQueryData(appConfigQueryOptions),\n ]);\n },\n});\n\nfunction Profile() {\n const { t } = useTranslation();\n const router = useRouter();\n const queryClient = useQueryClient();\n const search = Route.useSearch();\n\n const oauthError = search.oauth_error;\n const oauthErrorMessage = oauthError\n ? t(OAUTH_ERROR_I18N_MAP[oauthError] ?? 'oauth.error.failed')\n : undefined;\n const [unlinkingProvider, setUnlinkingProvider] = useState<string | null>(\n null,\n );\n const [unlinkModal, setUnlinkModal] = useState<{\n id: string;\n name: string;\n } | null>(null);\n const [passwordModal, setPasswordModal] = useState<PasswordModalType>(null);\n const [totpModal, setTotpModal] = useState<TotpModalType>(null);\n const [passkeyModal, setPasskeyModal] = useState<PasskeyModalType>(null);\n const [showDeleteModal, setShowDeleteModal] = useState(false);\n\n const { data: session } = useSuspenseQuery(getSessionQueryOptions);\n const { data: oauthAccountsData } = useSuspenseQuery(\n oauthAccountsQueryOptions,\n );\n const { data: appConfig } = useSuspenseQuery(appConfigQueryOptions);\n\n const logoutMutation = useMutation({\n ...logoutMutationOptions,\n onSuccess: async () => {\n queryClient.setQueryData(getSessionQueryOptions.queryKey, {\n user: null,\n });\n await tick();\n router.navigate({\n to: '/login',\n });\n },\n onSettled: () => {\n queryClient.invalidateQueries({\n queryKey: getSessionQueryOptions.queryKey,\n });\n },\n });\n\n const unlinkMutation = useMutation({\n ...unlinkOAuthMutationOptions,\n onSuccess: () => {\n queryClient.invalidateQueries({\n queryKey: oauthAccountsQueryOptions.queryKey,\n });\n },\n onSettled: () => {\n setUnlinkingProvider(null);\n },\n });\n\n const handleUnlinkConfirm = async () => {\n if (!unlinkModal) return;\n setUnlinkingProvider(unlinkModal.id);\n await unlinkMutation.mutateAsync(unlinkModal.id);\n };\n\n if (!session.user) {\n return null;\n }\n\n const user = session.user;\n const availableProviders = oauthAccountsData.available_providers;\n const hasLinkedOAuth = availableProviders.some((p) => p.linked);\n const isConfigManaged = user.managed_by === 'config';\n\n const passwordAuthMethod = appConfig.auth.password;\n const passkeyAuthMethod = appConfig.auth.passkey;\n const totpEnabled = passwordAuthMethod.totp.enabled;\n const passkeyEnabled = passkeyAuthMethod.enabled;\n\n const accountDeletionEnabled = appConfig.account_deletion.enabled;\n const retentionPeriod = appConfig.account_deletion.retention;\n\n const retentionDays = (() => {\n const match = retentionPeriod.match(/^(\\d+)([dmy])$/);\n if (!match) return 30;\n const value = parseInt(match[1], 10);\n const unit = match[2];\n switch (unit) {\n case 'd':\n return value;\n case 'm':\n return value * 30;\n case 'y':\n return value * 365;\n default:\n return 30;\n }\n })();\n\n const showPasswordSection = true;\n const showTotpSection = !isConfigManaged && totpEnabled;\n const showPasskeySection = !isConfigManaged && passkeyEnabled;\n const showLinkedAccounts = availableProviders.length > 0;\n const hasSecurityOptions =\n showPasswordSection || showTotpSection || showPasskeySection;\n\n return (\n <AppLayout\n headerActions={\n <TRButton\n appearance=\"ghost\"\n /*\n The visible label collapses below `sm:`, which would leave the\n control unnamed on a phone. `profile.logout` is the same string\n the label renders, so this does not shadow it.\n */\n aria-label={t('profile.logout')}\n data-testid=\"profile-logout\"\n disabled={logoutMutation.isPending}\n loading={logoutMutation.isPending}\n onClick={() => logoutMutation.mutate()}\n type=\"button\"\n uiSize=\"sm\"\n >\n {logoutMutation.isPending ? undefined : (\n <>\n <LogOutIcon aria-hidden className=\"size-tinyrack-lg\" />\n <span className=\"hidden sm:inline\">{t('profile.logout')}</span>\n </>\n )}\n </TRButton>\n }\n >\n <div className=\"flex items-center gap-tinyrack-lg\">\n <InitialAvatar email={user.email} size=\"lg\" />\n <div className=\"flex min-w-0 flex-col gap-tinyrack-3xs\">\n <AuthPageHeader title={t('profile.title')} />\n {/*\n Rendered here rather than as the header's `subtitle`: this node\n carries a testid and needs to truncate, neither of which the\n shared header does, and growing its API for one non-auth caller\n would be the wrong trade.\n */}\n <TRText\n as=\"p\"\n color=\"muted\"\n data-testid=\"profile-user-email\"\n truncate\n variant=\"bodySm\"\n >\n {user.email}\n </TRText>\n </div>\n </div>\n\n {oauthErrorMessage && (\n <Alert icon={CircleAlertIcon} type=\"error\">\n {oauthErrorMessage}\n </Alert>\n )}\n\n {user && <UserInfoSection user={user} />}\n\n {hasSecurityOptions && (\n <TRCard.Root variant=\"outlined\">\n <TRCard.Header className=\"border-tinyrack-border border-b-tinyrack-default px-tinyrack-lg py-tinyrack-md\">\n <TRCard.Title>{t('profile.security.title')}</TRCard.Title>\n <TRCard.Description>\n {t('profile.security.description')}\n </TRCard.Description>\n </TRCard.Header>\n {showTotpSection && user.totp_recovery_codes_missing && (\n <div\n className=\"border-tinyrack-border border-b-tinyrack-default px-tinyrack-lg py-tinyrack-md\"\n data-testid=\"profile-totp-recovery-warning\"\n >\n <Alert icon={CircleAlertIcon} type=\"warning\">\n {t('profile.totp.recoveryCodesMissing')}\n </Alert>\n </div>\n )}\n <TRCard.Content className=\"divide-y divide-tinyrack-border p-0\">\n {showPasswordSection && (\n <PasswordSection\n hasLinkedOAuth={hasLinkedOAuth}\n hasPassword={user.has_password}\n hasSecondFactorOnly={\n user.has_password &&\n !hasLinkedOAuth &&\n (user.totp_registered || user.passkey_count > 0)\n }\n isConfigManaged={isConfigManaged}\n onOpenModal={setPasswordModal}\n />\n )}\n {showTotpSection && (\n <TotpSection\n onOpenModal={setTotpModal}\n recoveryCodesMissing={user.totp_recovery_codes_missing}\n totpEnabled={user.totp_registered}\n />\n )}\n {showPasskeySection && (\n <PasskeySection\n onOpenModal={setPasskeyModal}\n passkeyCount={user.passkey_count}\n />\n )}\n </TRCard.Content>\n </TRCard.Root>\n )}\n\n {showLinkedAccounts && (\n <LinkedAccountsSection\n getAuthorizeUrl={getOAuthAuthorizeUrl}\n onUnlinkRequest={(provider) =>\n setUnlinkModal({\n id: provider.id,\n name: provider.display_name,\n })\n }\n providers={availableProviders}\n unlinkingProvider={unlinkingProvider}\n />\n )}\n\n <DangerZoneSection\n isConfigManaged={isConfigManaged}\n isDeletionEnabled={accountDeletionEnabled}\n onDeleteClick={() => setShowDeleteModal(true)}\n />\n\n <SetPasswordModal\n isOpen={passwordModal === 'set'}\n onClose={() => setPasswordModal(null)}\n />\n <ChangePasswordModal\n isOpen={passwordModal === 'change'}\n onClose={() => setPasswordModal(null)}\n />\n <RemovePasswordModal\n isOpen={passwordModal === 'remove'}\n onClose={() => setPasswordModal(null)}\n />\n\n <SetupTotpModal\n isOpen={totpModal === 'setup'}\n onClose={() => setTotpModal(null)}\n />\n <DisableTotpModal\n isOpen={totpModal === 'disable'}\n onClose={() => setTotpModal(null)}\n />\n <RegenerateTotpRecoveryCodesModal\n isOpen={totpModal === 'regenerate'}\n onClose={() => setTotpModal(null)}\n />\n\n <SetupPasskeyModal\n isOpen={passkeyModal === 'setup'}\n onClose={() => setPasskeyModal(null)}\n />\n <ManagePasskeysModal\n isOpen={passkeyModal === 'manage'}\n onAddNew={() => setPasskeyModal('setup')}\n onClose={() => setPasskeyModal(null)}\n />\n\n <UnlinkOAuthModal\n isOpen={unlinkModal !== null}\n isPending={unlinkMutation.isPending}\n onClose={() => setUnlinkModal(null)}\n onConfirm={handleUnlinkConfirm}\n providerName={unlinkModal?.name ?? ''}\n />\n\n <DeleteAccountModal\n isOpen={showDeleteModal}\n onClose={() => setShowDeleteModal(false)}\n retentionDays={retentionDays}\n />\n </AppLayout>\n );\n}\n"],"mappings":"qGAgEA,SAASI,EAAaC,EAA4B,CAChD,OACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,GAAIA,EACJ,mBAAsB,CACpBE,OAAOC,SAASC,KAAO,QACzB,CAAE,CAAA,CAGR"}
1
+ {"version":3,"file":"profile-D43Yq-6S.js","names":["RouteErrorFallback","PasswordModalType","TotpModalType","PasskeyModalType","ProfileError","props","ErrorComponentProps","window","location","href","errorComponent"],"sources":["../../../frontend/src/routes/profile/index.tsx?tsr-split=errorComponent"],"sourcesContent":["import {\n useMutation,\n useQueryClient,\n useSuspenseQuery,\n} from '@tanstack/react-query';\nimport {\n createFileRoute,\n type ErrorComponentProps,\n redirect,\n useRouter,\n} from '@tanstack/react-router';\nimport { TRButton } from '@tinyrack/ui/components/button';\nimport { TRCard } from '@tinyrack/ui/components/card';\nimport { TRText } from '@tinyrack/ui/components/text';\nimport { CircleAlertIcon, LogOutIcon } from 'lucide-react';\nimport { useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport { AuthPageHeader } from '#frontend/components/auth/auth-page-header.tsx';\nimport { DangerZoneSection } from '#frontend/components/profile/danger-zone-section.tsx';\nimport { LinkedAccountsSection } from '#frontend/components/profile/linked-accounts-section.tsx';\nimport { PasskeySection } from '#frontend/components/profile/passkey-section.tsx';\nimport { PasswordSection } from '#frontend/components/profile/password-section.tsx';\nimport { TotpSection } from '#frontend/components/profile/totp-section.tsx';\nimport { UnlinkOAuthModal } from '#frontend/components/profile/unlink-oauth-modal.tsx';\nimport { UserInfoSection } from '#frontend/components/profile/user-info-section.tsx';\nimport { Alert } from '#frontend/components/ui/alert.tsx';\nimport { InitialAvatar } from '#frontend/components/ui/initial-avatar.tsx';\nimport { RouteErrorFallback } from '#frontend/components/ui/route-error-fallback.tsx';\nimport { AppLayout } from '#frontend/features/layout/app-layout.tsx';\nimport { ChangePasswordModal } from '#frontend/features/profile/change-password-modal.tsx';\nimport { DeleteAccountModal } from '#frontend/features/profile/delete-account-modal.tsx';\nimport { DisableTotpModal } from '#frontend/features/profile/disable-totp-modal.tsx';\nimport { ManagePasskeysModal } from '#frontend/features/profile/manage-passkeys-modal.tsx';\nimport { RegenerateTotpRecoveryCodesModal } from '#frontend/features/profile/regenerate-totp-recovery-codes-modal.tsx';\nimport { RemovePasswordModal } from '#frontend/features/profile/remove-password-modal.tsx';\nimport { SetPasswordModal } from '#frontend/features/profile/set-password-modal.tsx';\nimport { SetupPasskeyModal } from '#frontend/features/profile/setup-passkey-modal.tsx';\nimport { SetupTotpModal } from '#frontend/features/profile/setup-totp-modal.tsx';\nimport { tick } from '#frontend/libs/promise.ts';\nimport { appConfigQueryOptions } from '#frontend/queries/config.ts';\nimport { logoutMutationOptions } from '#frontend/queries/logout.ts';\nimport {\n getOAuthAuthorizeUrl,\n oauthAccountsQueryOptions,\n unlinkOAuthMutationOptions,\n} from '#frontend/queries/oauth.ts';\nimport { getSessionQueryOptions } from '#frontend/queries/session.ts';\n\ntype PasswordModalType = 'set' | 'change' | 'remove' | null;\ntype TotpModalType = 'setup' | 'disable' | 'regenerate' | null;\ntype PasskeyModalType = 'setup' | 'manage' | null;\n\nconst SearchSchema = z.object({\n oauth_error: z.string().optional(),\n oauth_error_description: z.string().optional(),\n});\n\nconst OAUTH_ERROR_I18N_MAP: Record<string, string> = {\n access_denied: 'oauth.error.accessDenied',\n temporarily_unavailable: 'oauth.error.temporarilyUnavailable',\n server_error: 'oauth.error.serverError',\n};\n\nfunction ProfileError(props: ErrorComponentProps) {\n return (\n <RouteErrorFallback\n {...props}\n onUnauthorized={() => {\n window.location.href = '/login';\n }}\n />\n );\n}\n\nexport const Route = createFileRoute('/profile/')({\n component: Profile,\n errorComponent: ProfileError,\n validateSearch: SearchSchema,\n beforeLoad: async ({ context }) => {\n if (!context.user) {\n throw redirect({\n to: '/login',\n });\n }\n },\n loader: async ({ context }) => {\n await Promise.all([\n context.queryClient.ensureQueryData(getSessionQueryOptions),\n context.queryClient.ensureQueryData(oauthAccountsQueryOptions),\n context.queryClient.ensureQueryData(appConfigQueryOptions),\n ]);\n },\n});\n\nfunction Profile() {\n const { t } = useTranslation();\n const router = useRouter();\n const queryClient = useQueryClient();\n const search = Route.useSearch();\n\n const oauthError = search.oauth_error;\n const oauthErrorMessage = oauthError\n ? t(OAUTH_ERROR_I18N_MAP[oauthError] ?? 'oauth.error.failed')\n : undefined;\n const [unlinkingProvider, setUnlinkingProvider] = useState<string | null>(\n null,\n );\n const [unlinkModal, setUnlinkModal] = useState<{\n id: string;\n name: string;\n } | null>(null);\n const [passwordModal, setPasswordModal] = useState<PasswordModalType>(null);\n const [totpModal, setTotpModal] = useState<TotpModalType>(null);\n const [passkeyModal, setPasskeyModal] = useState<PasskeyModalType>(null);\n const [showDeleteModal, setShowDeleteModal] = useState(false);\n\n const { data: session } = useSuspenseQuery(getSessionQueryOptions);\n const { data: oauthAccountsData } = useSuspenseQuery(\n oauthAccountsQueryOptions,\n );\n const { data: appConfig } = useSuspenseQuery(appConfigQueryOptions);\n\n const logoutMutation = useMutation({\n ...logoutMutationOptions,\n onSuccess: async () => {\n queryClient.setQueryData(getSessionQueryOptions.queryKey, {\n user: null,\n });\n await tick();\n router.navigate({\n to: '/login',\n });\n },\n onSettled: () => {\n queryClient.invalidateQueries({\n queryKey: getSessionQueryOptions.queryKey,\n });\n },\n });\n\n const unlinkMutation = useMutation({\n ...unlinkOAuthMutationOptions,\n onSuccess: () => {\n queryClient.invalidateQueries({\n queryKey: oauthAccountsQueryOptions.queryKey,\n });\n },\n onSettled: () => {\n setUnlinkingProvider(null);\n },\n });\n\n const handleUnlinkConfirm = async () => {\n if (!unlinkModal) return;\n setUnlinkingProvider(unlinkModal.id);\n await unlinkMutation.mutateAsync(unlinkModal.id);\n };\n\n if (!session.user) {\n return null;\n }\n\n const user = session.user;\n const availableProviders = oauthAccountsData.available_providers;\n const hasLinkedOAuth = availableProviders.some((p) => p.linked);\n const isConfigManaged = user.managed_by === 'config';\n\n const passwordAuthMethod = appConfig.auth.password;\n const passkeyAuthMethod = appConfig.auth.passkey;\n const totpEnabled = passwordAuthMethod.totp.enabled;\n const passkeyEnabled = passkeyAuthMethod.enabled;\n\n const accountDeletionEnabled = appConfig.account_deletion.enabled;\n const retentionPeriod = appConfig.account_deletion.retention;\n\n const retentionDays = (() => {\n const match = retentionPeriod.match(/^(\\d+)([dmy])$/);\n if (!match) return 30;\n const value = parseInt(match[1], 10);\n const unit = match[2];\n switch (unit) {\n case 'd':\n return value;\n case 'm':\n return value * 30;\n case 'y':\n return value * 365;\n default:\n return 30;\n }\n })();\n\n const showPasswordSection = true;\n const showTotpSection = !isConfigManaged && totpEnabled;\n const showPasskeySection = !isConfigManaged && passkeyEnabled;\n const showLinkedAccounts = availableProviders.length > 0;\n const hasSecurityOptions =\n showPasswordSection || showTotpSection || showPasskeySection;\n\n return (\n <AppLayout\n headerActions={\n <TRButton\n appearance=\"ghost\"\n /*\n The visible label collapses below `sm:`, which would leave the\n control unnamed on a phone. `profile.logout` is the same string\n the label renders, so this does not shadow it.\n */\n aria-label={t('profile.logout')}\n data-testid=\"profile-logout\"\n disabled={logoutMutation.isPending}\n loading={logoutMutation.isPending}\n onClick={() => logoutMutation.mutate()}\n type=\"button\"\n uiSize=\"sm\"\n >\n {logoutMutation.isPending ? undefined : (\n <>\n <LogOutIcon aria-hidden className=\"size-tinyrack-lg\" />\n <span className=\"hidden sm:inline\">{t('profile.logout')}</span>\n </>\n )}\n </TRButton>\n }\n >\n <div className=\"flex items-center gap-tinyrack-lg\">\n <InitialAvatar email={user.email} size=\"lg\" />\n <div className=\"flex min-w-0 flex-col gap-tinyrack-3xs\">\n <AuthPageHeader title={t('profile.title')} />\n {/*\n Rendered here rather than as the header's `subtitle`: this node\n carries a testid and needs to truncate, neither of which the\n shared header does, and growing its API for one non-auth caller\n would be the wrong trade.\n */}\n <TRText\n as=\"p\"\n color=\"muted\"\n data-testid=\"profile-user-email\"\n truncate\n variant=\"bodySm\"\n >\n {user.email}\n </TRText>\n </div>\n </div>\n\n {oauthErrorMessage && (\n <Alert icon={CircleAlertIcon} type=\"error\">\n {oauthErrorMessage}\n </Alert>\n )}\n\n {user && <UserInfoSection user={user} />}\n\n {hasSecurityOptions && (\n <TRCard.Root variant=\"outlined\">\n <TRCard.Header className=\"border-tinyrack-border border-b-tinyrack-default px-tinyrack-lg py-tinyrack-md\">\n <TRCard.Title>{t('profile.security.title')}</TRCard.Title>\n <TRCard.Description>\n {t('profile.security.description')}\n </TRCard.Description>\n </TRCard.Header>\n {showTotpSection && user.totp_recovery_codes_missing && (\n <div\n className=\"border-tinyrack-border border-b-tinyrack-default px-tinyrack-lg py-tinyrack-md\"\n data-testid=\"profile-totp-recovery-warning\"\n >\n <Alert icon={CircleAlertIcon} type=\"warning\">\n {t('profile.totp.recoveryCodesMissing')}\n </Alert>\n </div>\n )}\n <TRCard.Content className=\"divide-y divide-tinyrack-border p-0\">\n {showPasswordSection && (\n <PasswordSection\n hasLinkedOAuth={hasLinkedOAuth}\n hasPassword={user.has_password}\n hasSecondFactorOnly={\n user.has_password &&\n !hasLinkedOAuth &&\n (user.totp_registered || user.passkey_count > 0)\n }\n isConfigManaged={isConfigManaged}\n onOpenModal={setPasswordModal}\n />\n )}\n {showTotpSection && (\n <TotpSection\n onOpenModal={setTotpModal}\n recoveryCodesMissing={user.totp_recovery_codes_missing}\n totpEnabled={user.totp_registered}\n />\n )}\n {showPasskeySection && (\n <PasskeySection\n onOpenModal={setPasskeyModal}\n passkeyCount={user.passkey_count}\n />\n )}\n </TRCard.Content>\n </TRCard.Root>\n )}\n\n {showLinkedAccounts && (\n <LinkedAccountsSection\n getAuthorizeUrl={getOAuthAuthorizeUrl}\n onUnlinkRequest={(provider) =>\n setUnlinkModal({\n id: provider.id,\n name: provider.display_name,\n })\n }\n providers={availableProviders}\n unlinkingProvider={unlinkingProvider}\n />\n )}\n\n <DangerZoneSection\n isConfigManaged={isConfigManaged}\n isDeletionEnabled={accountDeletionEnabled}\n onDeleteClick={() => setShowDeleteModal(true)}\n />\n\n <SetPasswordModal\n isOpen={passwordModal === 'set'}\n onClose={() => setPasswordModal(null)}\n />\n <ChangePasswordModal\n isOpen={passwordModal === 'change'}\n onClose={() => setPasswordModal(null)}\n />\n <RemovePasswordModal\n isOpen={passwordModal === 'remove'}\n onClose={() => setPasswordModal(null)}\n />\n\n <SetupTotpModal\n isOpen={totpModal === 'setup'}\n onClose={() => setTotpModal(null)}\n />\n <DisableTotpModal\n isOpen={totpModal === 'disable'}\n onClose={() => setTotpModal(null)}\n />\n <RegenerateTotpRecoveryCodesModal\n isOpen={totpModal === 'regenerate'}\n onClose={() => setTotpModal(null)}\n />\n\n <SetupPasskeyModal\n isOpen={passkeyModal === 'setup'}\n onClose={() => setPasskeyModal(null)}\n />\n <ManagePasskeysModal\n isOpen={passkeyModal === 'manage'}\n onAddNew={() => setPasskeyModal('setup')}\n onClose={() => setPasskeyModal(null)}\n />\n\n <UnlinkOAuthModal\n isOpen={unlinkModal !== null}\n isPending={unlinkMutation.isPending}\n onClose={() => setUnlinkModal(null)}\n onConfirm={handleUnlinkConfirm}\n providerName={unlinkModal?.name ?? ''}\n />\n\n <DeleteAccountModal\n isOpen={showDeleteModal}\n onClose={() => setShowDeleteModal(false)}\n retentionDays={retentionDays}\n />\n </AppLayout>\n );\n}\n"],"mappings":"qGAgEA,SAASI,EAAaC,EAA4B,CAChD,OACE,EAAA,EAAA,IAAA,CAAC,EAAD,CACE,GAAIA,EACJ,mBAAsB,CACpBE,OAAOC,SAASC,KAAO,QACzB,CAAE,CAAA,CAGR"}
@@ -0,0 +1,2 @@
1
+ import{J as e,Z as t,i as n,q as r,r as i,s as a}from"./createLucideIcon-BPpAIXR_.js";import{Dn as o,sr as s}from"./config-v3-oy_ef.js";import{t as c}from"./useMutation-ZLFeJg75.js";import{t as l}from"./link-BN90emXr.js";import{c as u,o as d}from"./zod-C87S6Tec.js";import{F as f,N as p,R as m,S as h,j as g,t as _}from"./index-gliHKncz.js";import{t as v}from"./auth-page-header-CIs08f5Y.js";import{t as y}from"./alert-leqyvHOp.js";import{t as b}from"./auth-layout-CoeqNhTT.js";import{t as x}from"./promise-OpBtq8tG.js";import{t as S}from"./field-DDt6F9TZ.js";import{t as C}from"./input-CNRpSR08.js";import{n as w,t as T}from"./auth-footer-Cz5ivsDc.js";import{r as E,t as D}from"./standard-schema-e8gtSzGE.js";import{a as O}from"./totp-BfCm6IWl.js";import{t as k}from"./labeled-separator-fnKySKv2.js";var A=t(e()),j=r(),M={SECOND_FACTOR_SESSION_EXPIRED:`SECOND_FACTOR_SESSION_EXPIRED`,INVALID_RECOVERY_CODE:`INVALID_RECOVERY_CODE`,NO_RECOVERY_CODES_AVAILABLE:`NO_RECOVERY_CODES_AVAILABLE`,TOTP_NOT_ENABLED:`TOTP_NOT_ENABLED`},N=5;function P(){let{t:e}=n(),t=o(),r=s(),P=_.useSearch(),F=(0,A.useRef)(null),[I,L]=(0,A.useState)(!1),[R,z]=(0,A.useState)(N),B=(0,A.useMemo)(()=>d({code:u().transform(e=>e.toUpperCase()).pipe(u().regex(/^[A-HJ-NP-TV-Z2-9]{4}(?:-[A-HJ-NP-TV-Z2-9]{4}){3}$/,e(`verifyRecovery.error.invalid`)))}),[e]),V=c({...O,onSuccess:async e=>{r.setQueryData(h.queryKey,{user:e.user}),await x(),f(P)?window.location.href=g(P):t.navigate({to:`/profile`})},onSettled:()=>{r.invalidateQueries({queryKey:h.queryKey})}}),{register:H,setError:U,handleSubmit:W,setValue:G,formState:{errors:K}}=E({defaultValues:{code:``},resolver:D(B)}),q=(0,A.useCallback)(()=>{t.navigate({to:`/login`,search:p(P)})},[t,P]);(0,A.useEffect)(()=>{if(!I)return;let e=setInterval(()=>{z(t=>t<=1?(clearInterval(e),q(),0):t-1)},1e3);return()=>clearInterval(e)},[I,q]);let J=async t=>{try{await V.mutateAsync(t)}catch(t){if(t instanceof i)switch(t.code){case M.SECOND_FACTOR_SESSION_EXPIRED:L(!0),z(N);return;case M.TOTP_NOT_ENABLED:q();return;case M.NO_RECOVERY_CODES_AVAILABLE:U(`code`,{type:`manual`,message:e(`verifyRecovery.error.noCodesAvailable`)}),G(`code`,``),F.current?.focus();return;case M.INVALID_RECOVERY_CODE:U(`code`,{type:`manual`,message:e(`verifyRecovery.error.invalid`)}),G(`code`,``),F.current?.focus();return}U(`code`,{type:`manual`,message:e(`verifyRecovery.error.invalid`)}),G(`code`,``),F.current?.focus()}},{ref:Y,...X}=H(`code`);return(0,j.jsxs)(b,{children:[(0,j.jsx)(v,{subtitle:e(`verifyRecovery.subtitle`),title:e(`verifyRecovery.title`)}),I&&(0,j.jsx)(y,{"data-testid":`recovery-session-expired`,icon:m,type:`warning`,children:(0,j.jsxs)(`span`,{className:`flex flex-col items-start gap-tinyrack-3xs`,children:[(0,j.jsx)(`span`,{children:e(`verifyRecovery.error.expired`)}),(0,j.jsx)(`span`,{className:`text-tinyrack-sm opacity-tinyrack-hover`,children:e(`verifyRecovery.redirecting`,{seconds:R})}),(0,j.jsx)(a,{appearance:`ghost`,intent:`neutral`,onClick:q,type:`button`,uiSize:`sm`,children:e(`verifyRecovery.redirectNow`)})]})}),(0,j.jsxs)(`form`,{className:`flex flex-col gap-tinyrack-lg`,onSubmit:W(J),children:[(0,j.jsxs)(S.Root,{children:[(0,j.jsx)(C,{...X,autoComplete:`off`,className:`w-full text-center font-tinyrack-mono`,"data-testid":`recovery-code-input`,disabled:I,placeholder:e(`verifyRecovery.placeholder`),ref:e=>{Y(e),F.current=e},type:`text`}),K.code&&(0,j.jsx)(`div`,{className:`tr-field-error`,"data-testid":`recovery-error`,children:K.code.message})]}),I?(0,j.jsx)(a,{className:`w-full`,disabled:!0,intent:`primary`,type:`button`,uiSize:`lg`,children:e(`verifyRecovery.submit`)}):(0,j.jsx)(a,{className:`w-full`,intent:`primary`,loading:V.isPending,loadingLabel:e(`verifyRecovery.submitting`),type:`submit`,uiSize:`lg`,children:e(`verifyRecovery.submit`)})]}),(0,j.jsxs)(`div`,{className:`flex flex-col gap-tinyrack-md`,children:[(0,j.jsx)(k,{label:e(`common.or`)}),(0,j.jsx)(a,{appearance:`ghost`,className:`w-full`,"data-testid":`recovery-back-to-totp`,intent:`neutral`,onClick:()=>t.navigate({to:`/verify/totp`,search:p(P)}),type:`button`,children:e(`verifyRecovery.backToTotp`)})]}),(0,j.jsx)(T,{children:(0,j.jsx)(w,{link:(0,j.jsx)(l,{search:p(P),to:`/login`,children:e(`verifyRecovery.backToLogin`)})})})]})}export{P as component};
2
+ //# sourceMappingURL=recovery-CLExnfYX.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"recovery-PnUATiUi.js","names":["standardSchemaResolver","useMutation","useQueryClient","Link","useRouter","TRButton","TRField","TRInput","CircleAlertIcon","useCallback","useEffect","useMemo","useRef","useState","useForm","useTranslation","z","AuthFooter","AuthFooterLink","AuthPageHeader","Alert","LabeledSeparator","AuthLayout","IssuaryError","buildAuthenticatedAuthorizeUrl","extractOAuthParams","isOAuthFlow","tick","getSessionQueryOptions","verifyRecoveryCodeMutationOptions","ERROR_CODES","SECOND_FACTOR_SESSION_EXPIRED","INVALID_RECOVERY_CODE","NO_RECOVERY_CODES_AVAILABLE","TOTP_NOT_ENABLED","const","Route","RecoveryFormValues","code","REDIRECT_COUNTDOWN_SECONDS","VerifyRecovery","t","router","queryClient","search","useSearch","inputRef","HTMLInputElement","sessionExpired","setSessionExpired","redirectCountdown","setRedirectCountdown","recoverySchema","object","string","transform","value","toUpperCase","pipe","regex","verifyMutation","onSuccess","data","setQueryData","queryKey","user","window","location","href","navigate","to","onSettled","invalidateQueries","register","setError","handleSubmit","setValue","formState","errors","defaultValues","resolver","redirectToLogin","timer","setInterval","prev","clearInterval","onSubmit","values","mutateAsync","error","type","message","current","focus","ref","formRef","registerRest","seconds","el","isPending","component"],"sources":["../../../frontend/src/routes/verify/totp/recovery/index.tsx?tsr-split=component"],"sourcesContent":["import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';\nimport { useMutation, useQueryClient } from '@tanstack/react-query';\nimport { createFileRoute, Link, useRouter } from '@tanstack/react-router';\nimport { TRButton } from '@tinyrack/ui/components/button';\nimport { TRField } from '@tinyrack/ui/components/field';\nimport { TRInput } from '@tinyrack/ui/components/input';\nimport { CircleAlertIcon } from 'lucide-react';\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport {\n AuthFooter,\n AuthFooterLink,\n} from '#frontend/components/auth/auth-footer.tsx';\nimport { AuthPageHeader } from '#frontend/components/auth/auth-page-header.tsx';\nimport { Alert } from '#frontend/components/ui/alert.tsx';\nimport { LabeledSeparator } from '#frontend/components/ui/labeled-separator.tsx';\nimport { AuthLayout } from '#frontend/features/layout/auth-layout.tsx';\nimport { IssuaryError } from '#frontend/libs/error.ts';\nimport {\n buildAuthenticatedAuthorizeUrl,\n extractOAuthParams,\n isOAuthFlow,\n OAuthSearchSchema,\n} from '#frontend/libs/oauth-search.ts';\nimport { tick } from '#frontend/libs/promise.ts';\nimport { getSessionQueryOptions } from '#frontend/queries/session.ts';\nimport { verifyRecoveryCodeMutationOptions } from '#frontend/queries/totp.ts';\n\n/** Error codes from backend */\nconst ERROR_CODES = {\n SECOND_FACTOR_SESSION_EXPIRED: 'SECOND_FACTOR_SESSION_EXPIRED',\n INVALID_RECOVERY_CODE: 'INVALID_RECOVERY_CODE',\n NO_RECOVERY_CODES_AVAILABLE: 'NO_RECOVERY_CODES_AVAILABLE',\n TOTP_NOT_ENABLED: 'TOTP_NOT_ENABLED',\n} as const;\n\nconst SearchSchema = OAuthSearchSchema;\n\nexport const Route = createFileRoute('/verify/totp/recovery/')({\n component: VerifyRecovery,\n validateSearch: SearchSchema,\n});\n\ntype RecoveryFormValues = {\n code: string;\n};\n\n/** Auto redirect countdown seconds */\nconst REDIRECT_COUNTDOWN_SECONDS = 5;\n\nfunction VerifyRecovery() {\n const { t } = useTranslation();\n const router = useRouter();\n const queryClient = useQueryClient();\n const search = Route.useSearch();\n const inputRef = useRef<HTMLInputElement | null>(null);\n\n const [sessionExpired, setSessionExpired] = useState(false);\n const [redirectCountdown, setRedirectCountdown] = useState(\n REDIRECT_COUNTDOWN_SECONDS,\n );\n\n const recoverySchema = useMemo(\n () =>\n z.object({\n code: z\n .string()\n .transform((value) => value.toUpperCase())\n .pipe(\n z\n .string()\n .regex(\n /^[A-HJ-NP-TV-Z2-9]{4}(?:-[A-HJ-NP-TV-Z2-9]{4}){3}$/,\n t('verifyRecovery.error.invalid'),\n ),\n ),\n }),\n [t],\n );\n\n const verifyMutation = useMutation({\n ...verifyRecoveryCodeMutationOptions,\n onSuccess: async (data) => {\n queryClient.setQueryData(getSessionQueryOptions.queryKey, {\n user: data.user,\n });\n await tick();\n\n if (isOAuthFlow(search)) {\n window.location.href = buildAuthenticatedAuthorizeUrl(search);\n } else {\n router.navigate({ to: '/profile' });\n }\n },\n onSettled: () => {\n queryClient.invalidateQueries({\n queryKey: getSessionQueryOptions.queryKey,\n });\n },\n });\n\n const {\n register,\n setError,\n handleSubmit,\n setValue,\n formState: { errors },\n } = useForm<RecoveryFormValues>({\n defaultValues: {\n code: '',\n },\n resolver: standardSchemaResolver(recoverySchema),\n });\n\n // Auto redirect when session expires\n const redirectToLogin = useCallback(() => {\n router.navigate({\n to: '/login',\n search: extractOAuthParams(search),\n });\n }, [router, search]);\n\n useEffect(() => {\n if (!sessionExpired) return;\n\n const timer = setInterval(() => {\n setRedirectCountdown((prev) => {\n if (prev <= 1) {\n clearInterval(timer);\n redirectToLogin();\n return 0;\n }\n return prev - 1;\n });\n }, 1000);\n\n return () => clearInterval(timer);\n }, [sessionExpired, redirectToLogin]);\n\n const onSubmit = async (values: RecoveryFormValues) => {\n try {\n await verifyMutation.mutateAsync(values);\n } catch (error) {\n if (error instanceof IssuaryError) {\n switch (error.code) {\n case ERROR_CODES.SECOND_FACTOR_SESSION_EXPIRED:\n setSessionExpired(true);\n setRedirectCountdown(REDIRECT_COUNTDOWN_SECONDS);\n return;\n\n case ERROR_CODES.TOTP_NOT_ENABLED:\n redirectToLogin();\n return;\n\n case ERROR_CODES.NO_RECOVERY_CODES_AVAILABLE:\n setError('code', {\n type: 'manual',\n message: t('verifyRecovery.error.noCodesAvailable'),\n });\n setValue('code', '');\n inputRef.current?.focus();\n return;\n\n case ERROR_CODES.INVALID_RECOVERY_CODE:\n setError('code', {\n type: 'manual',\n message: t('verifyRecovery.error.invalid'),\n });\n setValue('code', '');\n inputRef.current?.focus();\n return;\n }\n }\n\n // Generic error fallback\n setError('code', {\n type: 'manual',\n message: t('verifyRecovery.error.invalid'),\n });\n setValue('code', '');\n inputRef.current?.focus();\n }\n };\n\n const { ref: formRef, ...registerRest } = register('code');\n\n return (\n <AuthLayout>\n <AuthPageHeader\n subtitle={t('verifyRecovery.subtitle')}\n title={t('verifyRecovery.title')}\n />\n\n {sessionExpired && (\n <Alert\n data-testid=\"recovery-session-expired\"\n icon={CircleAlertIcon}\n type=\"warning\"\n >\n <span className=\"flex flex-col items-start gap-tinyrack-3xs\">\n <span>{t('verifyRecovery.error.expired')}</span>\n <span className=\"text-tinyrack-sm opacity-tinyrack-hover\">\n {t('verifyRecovery.redirecting', {\n seconds: redirectCountdown,\n })}\n </span>\n <TRButton\n appearance=\"ghost\"\n intent=\"neutral\"\n onClick={redirectToLogin}\n type=\"button\"\n uiSize=\"sm\"\n >\n {t('verifyRecovery.redirectNow')}\n </TRButton>\n </span>\n </Alert>\n )}\n\n <form\n className=\"flex flex-col gap-tinyrack-lg\"\n onSubmit={handleSubmit(onSubmit)}\n >\n <TRField.Root>\n <TRInput\n {...registerRest}\n autoComplete=\"off\"\n className=\"w-full text-center font-tinyrack-mono\"\n data-testid=\"recovery-code-input\"\n disabled={sessionExpired}\n placeholder={t('verifyRecovery.placeholder')}\n ref={(el) => {\n formRef(el);\n inputRef.current = el as HTMLInputElement | null;\n }}\n type=\"text\"\n />\n {errors.code && (\n <div className=\"tr-field-error\" data-testid=\"recovery-error\">\n {errors.code.message}\n </div>\n )}\n </TRField.Root>\n\n {sessionExpired ? (\n <TRButton\n className=\"w-full\"\n disabled\n intent=\"primary\"\n type=\"button\"\n uiSize=\"lg\"\n >\n {t('verifyRecovery.submit')}\n </TRButton>\n ) : (\n <TRButton\n className=\"w-full\"\n intent=\"primary\"\n loading={verifyMutation.isPending}\n loadingLabel={t('verifyRecovery.submitting')}\n type=\"submit\"\n uiSize=\"lg\"\n >\n {t('verifyRecovery.submit')}\n </TRButton>\n )}\n </form>\n\n {/* Mirrors /verify/totp: the other second factor sits below the rule. */}\n <div className=\"flex flex-col gap-tinyrack-md\">\n <LabeledSeparator label={t('common.or')} />\n <TRButton\n appearance=\"ghost\"\n className=\"w-full\"\n data-testid=\"recovery-back-to-totp\"\n intent=\"neutral\"\n onClick={() =>\n router.navigate({\n to: '/verify/totp',\n search: extractOAuthParams(search),\n })\n }\n type=\"button\"\n >\n {t('verifyRecovery.backToTotp')}\n </TRButton>\n </div>\n\n <AuthFooter>\n <AuthFooterLink\n link={\n <Link search={extractOAuthParams(search)} to=\"/login\">\n {t('verifyRecovery.backToLogin')}\n </Link>\n }\n />\n </AuthFooter>\n </AuthLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"recovery-CLExnfYX.js","names":["standardSchemaResolver","useMutation","useQueryClient","Link","useRouter","TRButton","TRField","TRInput","CircleAlertIcon","useCallback","useEffect","useMemo","useRef","useState","useForm","useTranslation","z","AuthFooter","AuthFooterLink","AuthPageHeader","Alert","LabeledSeparator","AuthLayout","IssuaryError","buildAuthenticatedAuthorizeUrl","extractOAuthParams","isOAuthFlow","tick","getSessionQueryOptions","verifyRecoveryCodeMutationOptions","ERROR_CODES","SECOND_FACTOR_SESSION_EXPIRED","INVALID_RECOVERY_CODE","NO_RECOVERY_CODES_AVAILABLE","TOTP_NOT_ENABLED","const","Route","RecoveryFormValues","code","REDIRECT_COUNTDOWN_SECONDS","VerifyRecovery","t","router","queryClient","search","useSearch","inputRef","HTMLInputElement","sessionExpired","setSessionExpired","redirectCountdown","setRedirectCountdown","recoverySchema","object","string","transform","value","toUpperCase","pipe","regex","verifyMutation","onSuccess","data","setQueryData","queryKey","user","window","location","href","navigate","to","onSettled","invalidateQueries","register","setError","handleSubmit","setValue","formState","errors","defaultValues","resolver","redirectToLogin","timer","setInterval","prev","clearInterval","onSubmit","values","mutateAsync","error","type","message","current","focus","ref","formRef","registerRest","seconds","el","isPending","component"],"sources":["../../../frontend/src/routes/verify/totp/recovery/index.tsx?tsr-split=component"],"sourcesContent":["import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';\nimport { useMutation, useQueryClient } from '@tanstack/react-query';\nimport { createFileRoute, Link, useRouter } from '@tanstack/react-router';\nimport { TRButton } from '@tinyrack/ui/components/button';\nimport { TRField } from '@tinyrack/ui/components/field';\nimport { TRInput } from '@tinyrack/ui/components/input';\nimport { CircleAlertIcon } from 'lucide-react';\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport {\n AuthFooter,\n AuthFooterLink,\n} from '#frontend/components/auth/auth-footer.tsx';\nimport { AuthPageHeader } from '#frontend/components/auth/auth-page-header.tsx';\nimport { Alert } from '#frontend/components/ui/alert.tsx';\nimport { LabeledSeparator } from '#frontend/components/ui/labeled-separator.tsx';\nimport { AuthLayout } from '#frontend/features/layout/auth-layout.tsx';\nimport { IssuaryError } from '#frontend/libs/error.ts';\nimport {\n buildAuthenticatedAuthorizeUrl,\n extractOAuthParams,\n isOAuthFlow,\n OAuthSearchSchema,\n} from '#frontend/libs/oauth-search.ts';\nimport { tick } from '#frontend/libs/promise.ts';\nimport { getSessionQueryOptions } from '#frontend/queries/session.ts';\nimport { verifyRecoveryCodeMutationOptions } from '#frontend/queries/totp.ts';\n\n/** Error codes from backend */\nconst ERROR_CODES = {\n SECOND_FACTOR_SESSION_EXPIRED: 'SECOND_FACTOR_SESSION_EXPIRED',\n INVALID_RECOVERY_CODE: 'INVALID_RECOVERY_CODE',\n NO_RECOVERY_CODES_AVAILABLE: 'NO_RECOVERY_CODES_AVAILABLE',\n TOTP_NOT_ENABLED: 'TOTP_NOT_ENABLED',\n} as const;\n\nconst SearchSchema = OAuthSearchSchema;\n\nexport const Route = createFileRoute('/verify/totp/recovery/')({\n component: VerifyRecovery,\n validateSearch: SearchSchema,\n});\n\ntype RecoveryFormValues = {\n code: string;\n};\n\n/** Auto redirect countdown seconds */\nconst REDIRECT_COUNTDOWN_SECONDS = 5;\n\nfunction VerifyRecovery() {\n const { t } = useTranslation();\n const router = useRouter();\n const queryClient = useQueryClient();\n const search = Route.useSearch();\n const inputRef = useRef<HTMLInputElement | null>(null);\n\n const [sessionExpired, setSessionExpired] = useState(false);\n const [redirectCountdown, setRedirectCountdown] = useState(\n REDIRECT_COUNTDOWN_SECONDS,\n );\n\n const recoverySchema = useMemo(\n () =>\n z.object({\n code: z\n .string()\n .transform((value) => value.toUpperCase())\n .pipe(\n z\n .string()\n .regex(\n /^[A-HJ-NP-TV-Z2-9]{4}(?:-[A-HJ-NP-TV-Z2-9]{4}){3}$/,\n t('verifyRecovery.error.invalid'),\n ),\n ),\n }),\n [t],\n );\n\n const verifyMutation = useMutation({\n ...verifyRecoveryCodeMutationOptions,\n onSuccess: async (data) => {\n queryClient.setQueryData(getSessionQueryOptions.queryKey, {\n user: data.user,\n });\n await tick();\n\n if (isOAuthFlow(search)) {\n window.location.href = buildAuthenticatedAuthorizeUrl(search);\n } else {\n router.navigate({ to: '/profile' });\n }\n },\n onSettled: () => {\n queryClient.invalidateQueries({\n queryKey: getSessionQueryOptions.queryKey,\n });\n },\n });\n\n const {\n register,\n setError,\n handleSubmit,\n setValue,\n formState: { errors },\n } = useForm<RecoveryFormValues>({\n defaultValues: {\n code: '',\n },\n resolver: standardSchemaResolver(recoverySchema),\n });\n\n // Auto redirect when session expires\n const redirectToLogin = useCallback(() => {\n router.navigate({\n to: '/login',\n search: extractOAuthParams(search),\n });\n }, [router, search]);\n\n useEffect(() => {\n if (!sessionExpired) return;\n\n const timer = setInterval(() => {\n setRedirectCountdown((prev) => {\n if (prev <= 1) {\n clearInterval(timer);\n redirectToLogin();\n return 0;\n }\n return prev - 1;\n });\n }, 1000);\n\n return () => clearInterval(timer);\n }, [sessionExpired, redirectToLogin]);\n\n const onSubmit = async (values: RecoveryFormValues) => {\n try {\n await verifyMutation.mutateAsync(values);\n } catch (error) {\n if (error instanceof IssuaryError) {\n switch (error.code) {\n case ERROR_CODES.SECOND_FACTOR_SESSION_EXPIRED:\n setSessionExpired(true);\n setRedirectCountdown(REDIRECT_COUNTDOWN_SECONDS);\n return;\n\n case ERROR_CODES.TOTP_NOT_ENABLED:\n redirectToLogin();\n return;\n\n case ERROR_CODES.NO_RECOVERY_CODES_AVAILABLE:\n setError('code', {\n type: 'manual',\n message: t('verifyRecovery.error.noCodesAvailable'),\n });\n setValue('code', '');\n inputRef.current?.focus();\n return;\n\n case ERROR_CODES.INVALID_RECOVERY_CODE:\n setError('code', {\n type: 'manual',\n message: t('verifyRecovery.error.invalid'),\n });\n setValue('code', '');\n inputRef.current?.focus();\n return;\n }\n }\n\n // Generic error fallback\n setError('code', {\n type: 'manual',\n message: t('verifyRecovery.error.invalid'),\n });\n setValue('code', '');\n inputRef.current?.focus();\n }\n };\n\n const { ref: formRef, ...registerRest } = register('code');\n\n return (\n <AuthLayout>\n <AuthPageHeader\n subtitle={t('verifyRecovery.subtitle')}\n title={t('verifyRecovery.title')}\n />\n\n {sessionExpired && (\n <Alert\n data-testid=\"recovery-session-expired\"\n icon={CircleAlertIcon}\n type=\"warning\"\n >\n <span className=\"flex flex-col items-start gap-tinyrack-3xs\">\n <span>{t('verifyRecovery.error.expired')}</span>\n <span className=\"text-tinyrack-sm opacity-tinyrack-hover\">\n {t('verifyRecovery.redirecting', {\n seconds: redirectCountdown,\n })}\n </span>\n <TRButton\n appearance=\"ghost\"\n intent=\"neutral\"\n onClick={redirectToLogin}\n type=\"button\"\n uiSize=\"sm\"\n >\n {t('verifyRecovery.redirectNow')}\n </TRButton>\n </span>\n </Alert>\n )}\n\n <form\n className=\"flex flex-col gap-tinyrack-lg\"\n onSubmit={handleSubmit(onSubmit)}\n >\n <TRField.Root>\n <TRInput\n {...registerRest}\n autoComplete=\"off\"\n className=\"w-full text-center font-tinyrack-mono\"\n data-testid=\"recovery-code-input\"\n disabled={sessionExpired}\n placeholder={t('verifyRecovery.placeholder')}\n ref={(el) => {\n formRef(el);\n inputRef.current = el as HTMLInputElement | null;\n }}\n type=\"text\"\n />\n {errors.code && (\n <div className=\"tr-field-error\" data-testid=\"recovery-error\">\n {errors.code.message}\n </div>\n )}\n </TRField.Root>\n\n {sessionExpired ? (\n <TRButton\n className=\"w-full\"\n disabled\n intent=\"primary\"\n type=\"button\"\n uiSize=\"lg\"\n >\n {t('verifyRecovery.submit')}\n </TRButton>\n ) : (\n <TRButton\n className=\"w-full\"\n intent=\"primary\"\n loading={verifyMutation.isPending}\n loadingLabel={t('verifyRecovery.submitting')}\n type=\"submit\"\n uiSize=\"lg\"\n >\n {t('verifyRecovery.submit')}\n </TRButton>\n )}\n </form>\n\n {/* Mirrors /verify/totp: the other second factor sits below the rule. */}\n <div className=\"flex flex-col gap-tinyrack-md\">\n <LabeledSeparator label={t('common.or')} />\n <TRButton\n appearance=\"ghost\"\n className=\"w-full\"\n data-testid=\"recovery-back-to-totp\"\n intent=\"neutral\"\n onClick={() =>\n router.navigate({\n to: '/verify/totp',\n search: extractOAuthParams(search),\n })\n }\n type=\"button\"\n >\n {t('verifyRecovery.backToTotp')}\n </TRButton>\n </div>\n\n <AuthFooter>\n <AuthFooterLink\n link={\n <Link search={extractOAuthParams(search)} to=\"/login\">\n {t('verifyRecovery.backToLogin')}\n </Link>\n }\n />\n </AuthFooter>\n </AuthLayout>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import{J as e,Z as t,i as n,q as r,r as ee,s as i}from"./createLucideIcon-BPpAIXR_.js";import{i as a,r as o,sr as s,t as c}from"./config-v3-oy_ef.js";import{t as l}from"./mutationOptions-CrswitPs.js";import{t as u}from"./useMutation-ZLFeJg75.js";import{tt as d}from"./shell-header-bar-C9AB5lT-.js";import{t as f}from"./link-BN90emXr.js";import{t as p}from"./auth-field-HwxENJnB.js";import{t as m}from"./authorization-context-banner-BbtQmMYH.js";import{t as h}from"./lock-7NRyvXBS.js";import{t as g}from"./mail-D0B_1Dzd.js";import{a as _,c as v,i as y,o as b,r as x}from"./zod-C87S6Tec.js";import{E as S,F as C,N as w,S as T,V as te,j as ne,k as E}from"./index-gliHKncz.js";import{t as D}from"./auth-page-header-CIs08f5Y.js";import{t as O}from"./auth-layout-CoeqNhTT.js";import{t as k}from"./promise-OpBtq8tG.js";import{n as A,t as j}from"./auth-footer-Cz5ivsDc.js";import{r as M,t as N}from"./standard-schema-e8gtSzGE.js";import{t as P}from"./password-strength-C0Zkrrct.js";import{t as F}from"./terms-checkbox-list-Cw0zB-lO.js";import{t as I}from"./labeled-separator-fnKySKv2.js";var L=t(e()),R=l({mutationFn:async e=>a(await o.api.auth.register.$post({json:e,header:{}}))}),z=r();function B(){let{t:e,i18n:t}=n(),r=te(),a=s(),o=S.useSearch(),l=o.lang??t.language,B=(0,L.useDeferredValue)(l),{data:V}=d(c),{data:H}=d(E(B)),U=V.auth.password.enabled,W=V.auth.password.policy,G=H.terms.length>0,K=(0,L.useMemo)(()=>H.terms.filter(e=>e.consentMode===`explicit`),[H.terms]),q=K.length>0,J=V.registration.signup_notice?.[l]??V.registration.signup_notice?.[V.i18n.fallback_language],Y=(0,L.useMemo)(()=>b({email:v().min(1,e(`validation.email.required`)).pipe(y(e(`validation.email.invalid`))),password:v().min(W.min_length,e(`validation.password.min`,{count:W.min_length})).max(W.max_length,e(`validation.password.max`,{count:W.max_length})),termsConsents:b(Object.fromEntries(K.map(t=>[t.id,t.required?_(!0,{message:e(`validation.terms.required`)}):x()])))}),[K,W.max_length,W.min_length,e]),X=u({...R,onSuccess:async(e,t)=>{let n=e.user;if(n.email_verification_required&&!n.email_verified)return r({to:`/verify/email`,search:{email:t.email,...w(o)}});if(a.setQueryData(T.queryKey,{user:n}),await k(),n.second_factor_required){let e=[];return V.auth.password.totp.enabled&&e.push(`totp`),V.auth.passkey.enabled&&e.push(`passkey`),e.length===1?e[0]===`totp`?r({to:`/setup/totp`,search:w(o)}):r({to:`/setup/passkey`,search:{...w(o),passkey_name:`default`}}):r({to:`/setup/2fa`,search:w(o)})}C(o)?window.location.href=ne(o):r({to:`/profile`})},onError:t=>{t instanceof ee?t.code===`REGISTRATION_EMAIL_NOT_ALLOWED`?Q(`email`,{type:`manual`,message:e(`register.error.emailNotAllowed`)}):t.code===`REGISTRATION_DISABLED`?Q(`email`,{type:`manual`,message:e(`register.error.registrationDisabled`)}):Q(`email`,{type:`manual`,message:e(`register.error.emailExists`)}):Q(`email`,{type:`manual`,message:e(`register.error.emailExists`)})},onSettled:async()=>{await a.invalidateQueries({queryKey:T.queryKey})}}),{register:Z,setError:Q,handleSubmit:re,control:ie,setValue:ae,watch:oe,formState:{errors:$}}=M({defaultValues:{email:``,password:``,termsConsents:Object.fromEntries(K.map(e=>[e.id,!1]))},resolver:N(Y),mode:`onChange`}),se=oe(`password`);return(0,z.jsxs)(O,{children:[(0,z.jsx)(D,{subtitle:e(`register.subtitle`),title:e(`register.title`)}),(0,z.jsx)(m,{search:o}),U&&(0,z.jsxs)(`form`,{className:`flex flex-col gap-tinyrack-lg`,onSubmit:re(e=>{let t=G?K.map(t=>({termsId:t.id,agreed:e.termsConsents[t.id]??!1})):void 0;X.mutate({email:e.email,password:e.password,consents:t})}),children:[(0,z.jsx)(p,{autoComplete:`email`,error:$.email,icon:g,label:e(`register.email.label`),placeholder:e(`register.email.placeholder`),...Z(`email`),type:`email`}),(0,z.jsxs)(`div`,{className:`flex flex-col gap-tinyrack-sm`,children:[(0,z.jsx)(p,{autoComplete:`new-password`,error:$.password,hint:e(`register.password.hint`,{count:W.min_length}),icon:h,label:e(`register.password.label`),placeholder:e(`register.password.placeholder`),...Z(`password`),type:`password`}),(0,z.jsx)(P,{password:se,policy:W})]}),(0,z.jsxs)(`div`,{className:`flex flex-col gap-tinyrack-md`,children:[J&&(0,z.jsx)(`div`,{className:`text-center text-tinyrack-text-muted text-tinyrack-xs`,children:(0,z.jsx)(`div`,{className:`prose prose-sm text-tinyrack-xs! **:text-tinyrack-xs!`,dangerouslySetInnerHTML:{__html:J},"data-testid":`terms-implicit-notice`})}),J&&q&&(0,z.jsx)(I,{label:e(`terms.additionalOptionalConsent`)}),G&&q&&(0,z.jsx)(F,{control:ie,disabled:X.isPending,errors:$,setValue:ae,terms:K})]}),(0,z.jsx)(i,{className:`w-full`,intent:`primary`,loading:X.isPending,loadingLabel:e(`register.submitting`),type:`submit`,uiSize:`lg`,children:e(`register.submit`)})]}),(0,z.jsx)(j,{children:(0,z.jsx)(A,{link:(0,z.jsx)(f,{search:w(o),to:`/login`,children:e(`register.link.login`)}),text:e(`register.footer.haveAccount`)})})]})}export{B as component};
2
+ //# sourceMappingURL=register-CZUyWXRO.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"register-DQXhru0_.js","names":["standardSchemaResolver","useMutation","useQueryClient","useSuspenseQuery","Link","useNavigate","TRButton","LockIcon","MailIcon","useDeferredValue","useMemo","useForm","useTranslation","z","AuthField","AuthFooter","AuthFooterLink","AuthPageHeader","AuthorizationContextBanner","PasswordStrength","TermsCheckboxList","LabeledSeparator","AuthLayout","IssuaryError","buildAuthenticatedAuthorizeUrl","extractOAuthParams","isOAuthFlow","tick","appConfigQueryOptions","registerMutationOptions","getSessionQueryOptions","getTermsQueryOptions","Route","Register","t","i18n","navigate","queryClient","search","useSearch","lang","language","deferredLang","data","configData","termsData","isPasswordAuthEnabled","auth","password","enabled","passwordPolicy","policy","hasTerms","terms","length","explicitTerms","filter","term","consentMode","hasExplicitTerms","implicitNotice","registration","signup_notice","fallback_language","registerSchema","object","email","string","min","pipe","min_length","count","max","max_length","termsConsents","Object","fromEntries","map","id","required","literal","message","boolean","RegisterFormValues","infer","registerMutation","onSuccess","params","user","email_verification_required","email_verified","to","setQueryData","queryKey","second_factor_required","available_2fa_methods","SecondFactorMethod","totp","push","passkey","method","passkey_name","window","location","href","onError","error","code","setError","type","onSettled","invalidateQueries","register","handleSubmit","control","setValue","watch","formState","errors","defaultValues","resolver","mode","passwordValue","onSubmit","values","consents","termsId","agreed","undefined","mutate","__html","isPending","component"],"sources":["../../../frontend/src/queries/register.ts","../../../frontend/src/routes/register/index.tsx?tsr-split=component"],"sourcesContent":["import { mutationOptions } from '@tanstack/react-query';\nimport type { InferResponseType } from 'hono/client';\nimport { client, jsonOk } from '#frontend/libs/api.ts';\nimport type { TermsConsentItem } from './terms';\n\nexport type RegisterParams = {\n email: string;\n password: string;\n consents?: TermsConsentItem[];\n};\n\nexport type RegisterResponse = InferResponseType<\n (typeof client.api.auth.register)['$post'],\n 200\n>;\n\nexport const registerMutationOptions = mutationOptions({\n mutationFn: async (values: RegisterParams) => {\n const res = await client.api.auth.register.$post({\n json: values,\n header: {},\n });\n return jsonOk(res);\n },\n});\n","import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';\nimport {\n useMutation,\n useQueryClient,\n useSuspenseQuery,\n} from '@tanstack/react-query';\nimport {\n createFileRoute,\n Link,\n redirect,\n useNavigate,\n} from '@tanstack/react-router';\nimport { TRButton } from '@tinyrack/ui/components/button';\nimport { LockIcon, MailIcon } from 'lucide-react';\nimport { useDeferredValue, useMemo } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport { AuthField } from '#frontend/components/auth/auth-field.tsx';\nimport {\n AuthFooter,\n AuthFooterLink,\n} from '#frontend/components/auth/auth-footer.tsx';\nimport { AuthPageHeader } from '#frontend/components/auth/auth-page-header.tsx';\nimport { AuthorizationContextBanner } from '#frontend/components/auth/authorization-context-banner.tsx';\nimport { PasswordStrength } from '#frontend/components/auth/password-strength.tsx';\nimport { TermsCheckboxList } from '#frontend/components/terms/terms-checkbox-list.tsx';\nimport { LabeledSeparator } from '#frontend/components/ui/labeled-separator.tsx';\nimport { RouteErrorFallback } from '#frontend/components/ui/route-error-fallback.tsx';\nimport { AuthLayout } from '#frontend/features/layout/auth-layout.tsx';\nimport { IssuaryError } from '#frontend/libs/error.ts';\n\nimport {\n buildAuthenticatedAuthorizeUrl,\n extractOAuthParams,\n hasAuthorizationContext,\n isOAuthFlow,\n OAuthSearchSchema,\n type SecondFactorMethod,\n} from '#frontend/libs/oauth-search.ts';\nimport { tick } from '#frontend/libs/promise.ts';\nimport { getAuthorizationContextQueryOptions } from '#frontend/queries/authorization-context.ts';\nimport { appConfigQueryOptions } from '#frontend/queries/config.ts';\nimport { registerMutationOptions } from '#frontend/queries/register.ts';\nimport { getSessionQueryOptions } from '#frontend/queries/session.ts';\nimport { getTermsQueryOptions } from '#frontend/queries/terms.ts';\n\nexport const Route = createFileRoute('/register/')({\n component: Register,\n errorComponent: RouteErrorFallback,\n validateSearch: OAuthSearchSchema,\n beforeLoad: async ({ context }) => {\n const config = await context.queryClient.ensureQueryData(\n appConfigQueryOptions,\n );\n if (!config.registration.public_registration) {\n throw redirect({\n to: '/',\n replace: true,\n });\n }\n },\n loaderDeps: ({ search }) => ({\n lang: search.lang,\n search,\n }),\n loader: async ({ context, deps }) => {\n const lang = deps.lang ?? context.i18n.language;\n const queries: Array<Promise<unknown>> = [\n context.queryClient.ensureQueryData(appConfigQueryOptions),\n context.queryClient.ensureQueryData(getTermsQueryOptions(lang)),\n ];\n\n if (hasAuthorizationContext(deps.search)) {\n queries.push(\n context.queryClient.ensureQueryData(\n getAuthorizationContextQueryOptions(deps.search),\n ),\n );\n }\n\n await Promise.all(queries);\n },\n});\n\nfunction Register() {\n const { t, i18n } = useTranslation();\n const navigate = useNavigate();\n const queryClient = useQueryClient();\n const search = Route.useSearch();\n\n const lang = search.lang ?? i18n.language;\n const deferredLang = useDeferredValue(lang);\n\n const { data: configData } = useSuspenseQuery(appConfigQueryOptions);\n const { data: termsData } = useSuspenseQuery(\n getTermsQueryOptions(deferredLang),\n );\n\n const isPasswordAuthEnabled = configData.auth.password.enabled;\n const passwordPolicy = configData.auth.password.policy;\n\n // Check if there are any terms to display\n const hasTerms = termsData.terms.length > 0;\n const explicitTerms = useMemo(\n () => termsData.terms.filter((term) => term.consentMode === 'explicit'),\n [termsData.terms],\n );\n const hasExplicitTerms = explicitTerms.length > 0;\n\n // Get implicit notice from config\n const implicitNotice =\n configData.registration.signup_notice?.[lang] ??\n configData.registration.signup_notice?.[configData.i18n.fallback_language];\n\n const registerSchema = useMemo(\n () =>\n z.object({\n email: z\n .string()\n .min(1, t('validation.email.required'))\n .pipe(z.email(t('validation.email.invalid'))),\n password: z\n .string()\n .min(\n passwordPolicy.min_length,\n t('validation.password.min', {\n count: passwordPolicy.min_length,\n }),\n )\n .max(\n passwordPolicy.max_length,\n t('validation.password.max', {\n count: passwordPolicy.max_length,\n }),\n ),\n termsConsents: z.object(\n Object.fromEntries(\n explicitTerms.map((term) => [\n term.id,\n term.required\n ? z.literal(true, {\n message: t('validation.terms.required'),\n })\n : z.boolean(),\n ]),\n ),\n ),\n }),\n [explicitTerms, passwordPolicy.max_length, passwordPolicy.min_length, t],\n );\n\n type RegisterFormValues = z.infer<typeof registerSchema>;\n\n const registerMutation = useMutation({\n ...registerMutationOptions,\n onSuccess: async (data, params) => {\n const user = data.user;\n\n if (user.email_verification_required && !user.email_verified) {\n return navigate({\n to: '/verify/email',\n search: {\n email: params.email,\n ...extractOAuthParams(search),\n },\n });\n }\n\n queryClient.setQueryData(getSessionQueryOptions.queryKey, {\n user: user,\n });\n await tick();\n\n if (user.second_factor_required) {\n const available_2fa_methods: SecondFactorMethod[] = [];\n if (configData.auth.password.totp.enabled) {\n available_2fa_methods.push('totp');\n }\n if (configData.auth.passkey.enabled) {\n available_2fa_methods.push('passkey');\n }\n\n if (available_2fa_methods.length === 1) {\n const method = available_2fa_methods[0];\n if (method === 'totp') {\n return navigate({\n to: '/setup/totp',\n search: extractOAuthParams(search),\n });\n } else {\n return navigate({\n to: '/setup/passkey',\n search: {\n ...extractOAuthParams(search),\n passkey_name: 'default',\n },\n });\n }\n } else {\n return navigate({\n to: '/setup/2fa',\n search: extractOAuthParams(search),\n });\n }\n }\n\n if (isOAuthFlow(search)) {\n window.location.href = buildAuthenticatedAuthorizeUrl(search);\n } else {\n navigate({ to: '/profile' });\n }\n },\n onError: (error) => {\n if (error instanceof IssuaryError) {\n if (error.code === 'REGISTRATION_EMAIL_NOT_ALLOWED') {\n setError('email', {\n type: 'manual',\n message: t('register.error.emailNotAllowed'),\n });\n } else if (error.code === 'REGISTRATION_DISABLED') {\n setError('email', {\n type: 'manual',\n message: t('register.error.registrationDisabled'),\n });\n } else {\n setError('email', {\n type: 'manual',\n message: t('register.error.emailExists'),\n });\n }\n } else {\n setError('email', {\n type: 'manual',\n message: t('register.error.emailExists'),\n });\n }\n },\n onSettled: async () => {\n await queryClient.invalidateQueries({\n queryKey: getSessionQueryOptions.queryKey,\n });\n },\n });\n\n const {\n register,\n setError,\n handleSubmit,\n control,\n setValue,\n watch,\n formState: { errors },\n } = useForm<RegisterFormValues>({\n defaultValues: {\n email: '',\n password: '',\n termsConsents: Object.fromEntries(\n explicitTerms.map((term) => [term.id, false]),\n ),\n },\n resolver: standardSchemaResolver(registerSchema),\n mode: 'onChange',\n });\n\n const passwordValue = watch('password');\n\n const onSubmit = (values: RegisterFormValues) => {\n // Build consents array for registration\n // Only send explicit consents - implicit ones are handled by backend\n const consents = hasTerms\n ? explicitTerms.map((term) => ({\n termsId: term.id,\n agreed: values.termsConsents[term.id] ?? false,\n }))\n : undefined;\n\n registerMutation.mutate({\n email: values.email,\n password: values.password,\n consents,\n });\n };\n\n return (\n <AuthLayout>\n <AuthPageHeader\n subtitle={t('register.subtitle')}\n title={t('register.title')}\n />\n\n <AuthorizationContextBanner search={search} />\n\n {isPasswordAuthEnabled && (\n <form\n className=\"flex flex-col gap-tinyrack-lg\"\n onSubmit={handleSubmit(onSubmit)}\n >\n <AuthField\n autoComplete=\"email\"\n error={errors.email}\n icon={MailIcon}\n label={t('register.email.label')}\n placeholder={t('register.email.placeholder')}\n {...register('email')}\n type=\"email\"\n />\n\n <div className=\"flex flex-col gap-tinyrack-sm\">\n <AuthField\n autoComplete=\"new-password\"\n error={errors.password}\n hint={t('register.password.hint', {\n count: passwordPolicy.min_length,\n })}\n icon={LockIcon}\n label={t('register.password.label')}\n placeholder={t('register.password.placeholder')}\n {...register('password')}\n type=\"password\"\n />\n\n <PasswordStrength\n password={passwordValue}\n policy={passwordPolicy}\n />\n </div>\n\n <div className=\"flex flex-col gap-tinyrack-md\">\n {implicitNotice && (\n <div className=\"text-center text-tinyrack-text-muted text-tinyrack-xs\">\n <div\n className=\"prose prose-sm text-tinyrack-xs! **:text-tinyrack-xs!\"\n dangerouslySetInnerHTML={{\n __html: implicitNotice,\n }}\n data-testid=\"terms-implicit-notice\"\n />\n </div>\n )}\n\n {implicitNotice && hasExplicitTerms && (\n <LabeledSeparator label={t('terms.additionalOptionalConsent')} />\n )}\n\n {hasTerms && hasExplicitTerms && (\n <TermsCheckboxList\n control={control}\n disabled={registerMutation.isPending}\n errors={errors}\n setValue={setValue}\n terms={explicitTerms}\n />\n )}\n </div>\n\n <TRButton\n className=\"w-full\"\n intent=\"primary\"\n loading={registerMutation.isPending}\n loadingLabel={t('register.submitting')}\n type=\"submit\"\n uiSize=\"lg\"\n >\n {t('register.submit')}\n </TRButton>\n </form>\n )}\n\n <AuthFooter>\n <AuthFooterLink\n link={\n <Link search={extractOAuthParams(search)} to=\"/login\">\n {t('register.link.login')}\n </Link>\n }\n text={t('register.footer.haveAccount')}\n />\n </AuthFooter>\n </AuthLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"register-CZUyWXRO.js","names":["standardSchemaResolver","useMutation","useQueryClient","useSuspenseQuery","Link","useNavigate","TRButton","LockIcon","MailIcon","useDeferredValue","useMemo","useForm","useTranslation","z","AuthField","AuthFooter","AuthFooterLink","AuthPageHeader","AuthorizationContextBanner","PasswordStrength","TermsCheckboxList","LabeledSeparator","AuthLayout","IssuaryError","buildAuthenticatedAuthorizeUrl","extractOAuthParams","isOAuthFlow","tick","appConfigQueryOptions","registerMutationOptions","getSessionQueryOptions","getTermsQueryOptions","Route","Register","t","i18n","navigate","queryClient","search","useSearch","lang","language","deferredLang","data","configData","termsData","isPasswordAuthEnabled","auth","password","enabled","passwordPolicy","policy","hasTerms","terms","length","explicitTerms","filter","term","consentMode","hasExplicitTerms","implicitNotice","registration","signup_notice","fallback_language","registerSchema","object","email","string","min","pipe","min_length","count","max","max_length","termsConsents","Object","fromEntries","map","id","required","literal","message","boolean","RegisterFormValues","infer","registerMutation","onSuccess","params","user","email_verification_required","email_verified","to","setQueryData","queryKey","second_factor_required","available_2fa_methods","SecondFactorMethod","totp","push","passkey","method","passkey_name","window","location","href","onError","error","code","setError","type","onSettled","invalidateQueries","register","handleSubmit","control","setValue","watch","formState","errors","defaultValues","resolver","mode","passwordValue","onSubmit","values","consents","termsId","agreed","undefined","mutate","__html","isPending","component"],"sources":["../../../frontend/src/queries/register.ts","../../../frontend/src/routes/register/index.tsx?tsr-split=component"],"sourcesContent":["import { mutationOptions } from '@tanstack/react-query';\nimport type { InferResponseType } from 'hono/client';\nimport { client, jsonOk } from '#frontend/libs/api.ts';\nimport type { TermsConsentItem } from './terms';\n\nexport type RegisterParams = {\n email: string;\n password: string;\n consents?: TermsConsentItem[];\n};\n\nexport type RegisterResponse = InferResponseType<\n (typeof client.api.auth.register)['$post'],\n 200\n>;\n\nexport const registerMutationOptions = mutationOptions({\n mutationFn: async (values: RegisterParams) => {\n const res = await client.api.auth.register.$post({\n json: values,\n header: {},\n });\n return jsonOk(res);\n },\n});\n","import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';\nimport {\n useMutation,\n useQueryClient,\n useSuspenseQuery,\n} from '@tanstack/react-query';\nimport {\n createFileRoute,\n Link,\n redirect,\n useNavigate,\n} from '@tanstack/react-router';\nimport { TRButton } from '@tinyrack/ui/components/button';\nimport { LockIcon, MailIcon } from 'lucide-react';\nimport { useDeferredValue, useMemo } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport { AuthField } from '#frontend/components/auth/auth-field.tsx';\nimport {\n AuthFooter,\n AuthFooterLink,\n} from '#frontend/components/auth/auth-footer.tsx';\nimport { AuthPageHeader } from '#frontend/components/auth/auth-page-header.tsx';\nimport { AuthorizationContextBanner } from '#frontend/components/auth/authorization-context-banner.tsx';\nimport { PasswordStrength } from '#frontend/components/auth/password-strength.tsx';\nimport { TermsCheckboxList } from '#frontend/components/terms/terms-checkbox-list.tsx';\nimport { LabeledSeparator } from '#frontend/components/ui/labeled-separator.tsx';\nimport { RouteErrorFallback } from '#frontend/components/ui/route-error-fallback.tsx';\nimport { AuthLayout } from '#frontend/features/layout/auth-layout.tsx';\nimport { IssuaryError } from '#frontend/libs/error.ts';\n\nimport {\n buildAuthenticatedAuthorizeUrl,\n extractOAuthParams,\n hasAuthorizationContext,\n isOAuthFlow,\n OAuthSearchSchema,\n type SecondFactorMethod,\n} from '#frontend/libs/oauth-search.ts';\nimport { tick } from '#frontend/libs/promise.ts';\nimport { getAuthorizationContextQueryOptions } from '#frontend/queries/authorization-context.ts';\nimport { appConfigQueryOptions } from '#frontend/queries/config.ts';\nimport { registerMutationOptions } from '#frontend/queries/register.ts';\nimport { getSessionQueryOptions } from '#frontend/queries/session.ts';\nimport { getTermsQueryOptions } from '#frontend/queries/terms.ts';\n\nexport const Route = createFileRoute('/register/')({\n component: Register,\n errorComponent: RouteErrorFallback,\n validateSearch: OAuthSearchSchema,\n beforeLoad: async ({ context }) => {\n const config = await context.queryClient.ensureQueryData(\n appConfigQueryOptions,\n );\n if (!config.registration.public_registration) {\n throw redirect({\n to: '/',\n replace: true,\n });\n }\n },\n loaderDeps: ({ search }) => ({\n lang: search.lang,\n search,\n }),\n loader: async ({ context, deps }) => {\n const lang = deps.lang ?? context.i18n.language;\n const queries: Array<Promise<unknown>> = [\n context.queryClient.ensureQueryData(appConfigQueryOptions),\n context.queryClient.ensureQueryData(getTermsQueryOptions(lang)),\n ];\n\n if (hasAuthorizationContext(deps.search)) {\n queries.push(\n context.queryClient.ensureQueryData(\n getAuthorizationContextQueryOptions(deps.search),\n ),\n );\n }\n\n await Promise.all(queries);\n },\n});\n\nfunction Register() {\n const { t, i18n } = useTranslation();\n const navigate = useNavigate();\n const queryClient = useQueryClient();\n const search = Route.useSearch();\n\n const lang = search.lang ?? i18n.language;\n const deferredLang = useDeferredValue(lang);\n\n const { data: configData } = useSuspenseQuery(appConfigQueryOptions);\n const { data: termsData } = useSuspenseQuery(\n getTermsQueryOptions(deferredLang),\n );\n\n const isPasswordAuthEnabled = configData.auth.password.enabled;\n const passwordPolicy = configData.auth.password.policy;\n\n // Check if there are any terms to display\n const hasTerms = termsData.terms.length > 0;\n const explicitTerms = useMemo(\n () => termsData.terms.filter((term) => term.consentMode === 'explicit'),\n [termsData.terms],\n );\n const hasExplicitTerms = explicitTerms.length > 0;\n\n // Get implicit notice from config\n const implicitNotice =\n configData.registration.signup_notice?.[lang] ??\n configData.registration.signup_notice?.[configData.i18n.fallback_language];\n\n const registerSchema = useMemo(\n () =>\n z.object({\n email: z\n .string()\n .min(1, t('validation.email.required'))\n .pipe(z.email(t('validation.email.invalid'))),\n password: z\n .string()\n .min(\n passwordPolicy.min_length,\n t('validation.password.min', {\n count: passwordPolicy.min_length,\n }),\n )\n .max(\n passwordPolicy.max_length,\n t('validation.password.max', {\n count: passwordPolicy.max_length,\n }),\n ),\n termsConsents: z.object(\n Object.fromEntries(\n explicitTerms.map((term) => [\n term.id,\n term.required\n ? z.literal(true, {\n message: t('validation.terms.required'),\n })\n : z.boolean(),\n ]),\n ),\n ),\n }),\n [explicitTerms, passwordPolicy.max_length, passwordPolicy.min_length, t],\n );\n\n type RegisterFormValues = z.infer<typeof registerSchema>;\n\n const registerMutation = useMutation({\n ...registerMutationOptions,\n onSuccess: async (data, params) => {\n const user = data.user;\n\n if (user.email_verification_required && !user.email_verified) {\n return navigate({\n to: '/verify/email',\n search: {\n email: params.email,\n ...extractOAuthParams(search),\n },\n });\n }\n\n queryClient.setQueryData(getSessionQueryOptions.queryKey, {\n user: user,\n });\n await tick();\n\n if (user.second_factor_required) {\n const available_2fa_methods: SecondFactorMethod[] = [];\n if (configData.auth.password.totp.enabled) {\n available_2fa_methods.push('totp');\n }\n if (configData.auth.passkey.enabled) {\n available_2fa_methods.push('passkey');\n }\n\n if (available_2fa_methods.length === 1) {\n const method = available_2fa_methods[0];\n if (method === 'totp') {\n return navigate({\n to: '/setup/totp',\n search: extractOAuthParams(search),\n });\n } else {\n return navigate({\n to: '/setup/passkey',\n search: {\n ...extractOAuthParams(search),\n passkey_name: 'default',\n },\n });\n }\n } else {\n return navigate({\n to: '/setup/2fa',\n search: extractOAuthParams(search),\n });\n }\n }\n\n if (isOAuthFlow(search)) {\n window.location.href = buildAuthenticatedAuthorizeUrl(search);\n } else {\n navigate({ to: '/profile' });\n }\n },\n onError: (error) => {\n if (error instanceof IssuaryError) {\n if (error.code === 'REGISTRATION_EMAIL_NOT_ALLOWED') {\n setError('email', {\n type: 'manual',\n message: t('register.error.emailNotAllowed'),\n });\n } else if (error.code === 'REGISTRATION_DISABLED') {\n setError('email', {\n type: 'manual',\n message: t('register.error.registrationDisabled'),\n });\n } else {\n setError('email', {\n type: 'manual',\n message: t('register.error.emailExists'),\n });\n }\n } else {\n setError('email', {\n type: 'manual',\n message: t('register.error.emailExists'),\n });\n }\n },\n onSettled: async () => {\n await queryClient.invalidateQueries({\n queryKey: getSessionQueryOptions.queryKey,\n });\n },\n });\n\n const {\n register,\n setError,\n handleSubmit,\n control,\n setValue,\n watch,\n formState: { errors },\n } = useForm<RegisterFormValues>({\n defaultValues: {\n email: '',\n password: '',\n termsConsents: Object.fromEntries(\n explicitTerms.map((term) => [term.id, false]),\n ),\n },\n resolver: standardSchemaResolver(registerSchema),\n mode: 'onChange',\n });\n\n const passwordValue = watch('password');\n\n const onSubmit = (values: RegisterFormValues) => {\n // Build consents array for registration\n // Only send explicit consents - implicit ones are handled by backend\n const consents = hasTerms\n ? explicitTerms.map((term) => ({\n termsId: term.id,\n agreed: values.termsConsents[term.id] ?? false,\n }))\n : undefined;\n\n registerMutation.mutate({\n email: values.email,\n password: values.password,\n consents,\n });\n };\n\n return (\n <AuthLayout>\n <AuthPageHeader\n subtitle={t('register.subtitle')}\n title={t('register.title')}\n />\n\n <AuthorizationContextBanner search={search} />\n\n {isPasswordAuthEnabled && (\n <form\n className=\"flex flex-col gap-tinyrack-lg\"\n onSubmit={handleSubmit(onSubmit)}\n >\n <AuthField\n autoComplete=\"email\"\n error={errors.email}\n icon={MailIcon}\n label={t('register.email.label')}\n placeholder={t('register.email.placeholder')}\n {...register('email')}\n type=\"email\"\n />\n\n <div className=\"flex flex-col gap-tinyrack-sm\">\n <AuthField\n autoComplete=\"new-password\"\n error={errors.password}\n hint={t('register.password.hint', {\n count: passwordPolicy.min_length,\n })}\n icon={LockIcon}\n label={t('register.password.label')}\n placeholder={t('register.password.placeholder')}\n {...register('password')}\n type=\"password\"\n />\n\n <PasswordStrength\n password={passwordValue}\n policy={passwordPolicy}\n />\n </div>\n\n <div className=\"flex flex-col gap-tinyrack-md\">\n {implicitNotice && (\n <div className=\"text-center text-tinyrack-text-muted text-tinyrack-xs\">\n <div\n className=\"prose prose-sm text-tinyrack-xs! **:text-tinyrack-xs!\"\n dangerouslySetInnerHTML={{\n __html: implicitNotice,\n }}\n data-testid=\"terms-implicit-notice\"\n />\n </div>\n )}\n\n {implicitNotice && hasExplicitTerms && (\n <LabeledSeparator label={t('terms.additionalOptionalConsent')} />\n )}\n\n {hasTerms && hasExplicitTerms && (\n <TermsCheckboxList\n control={control}\n disabled={registerMutation.isPending}\n errors={errors}\n setValue={setValue}\n terms={explicitTerms}\n />\n )}\n </div>\n\n <TRButton\n className=\"w-full\"\n intent=\"primary\"\n loading={registerMutation.isPending}\n loadingLabel={t('register.submitting')}\n type=\"submit\"\n uiSize=\"lg\"\n >\n {t('register.submit')}\n </TRButton>\n </form>\n )}\n\n <AuthFooter>\n <AuthFooterLink\n link={\n <Link search={extractOAuthParams(search)} to=\"/login\">\n {t('register.link.login')}\n </Link>\n }\n text={t('register.footer.haveAccount')}\n />\n </AuthFooter>\n </AuthLayout>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import{I as e}from"./index-gliHKncz.js";var t=e;export{t as errorComponent};
2
+ //# sourceMappingURL=register-UL_cNRnQ.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"register-C5vUVgr_.js","names":["RouteErrorFallback","SplitErrorComponent","errorComponent"],"sources":["../../../frontend/src/routes/register/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 {\n createFileRoute,\n Link,\n redirect,\n useNavigate,\n} from '@tanstack/react-router';\nimport { TRButton } from '@tinyrack/ui/components/button';\nimport { LockIcon, MailIcon } from 'lucide-react';\nimport { useDeferredValue, useMemo } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport { AuthField } from '#frontend/components/auth/auth-field.tsx';\nimport {\n AuthFooter,\n AuthFooterLink,\n} from '#frontend/components/auth/auth-footer.tsx';\nimport { AuthPageHeader } from '#frontend/components/auth/auth-page-header.tsx';\nimport { AuthorizationContextBanner } from '#frontend/components/auth/authorization-context-banner.tsx';\nimport { PasswordStrength } from '#frontend/components/auth/password-strength.tsx';\nimport { TermsCheckboxList } from '#frontend/components/terms/terms-checkbox-list.tsx';\nimport { LabeledSeparator } from '#frontend/components/ui/labeled-separator.tsx';\nimport { RouteErrorFallback } from '#frontend/components/ui/route-error-fallback.tsx';\nimport { AuthLayout } from '#frontend/features/layout/auth-layout.tsx';\nimport { IssuaryError } from '#frontend/libs/error.ts';\n\nimport {\n buildAuthenticatedAuthorizeUrl,\n extractOAuthParams,\n hasAuthorizationContext,\n isOAuthFlow,\n OAuthSearchSchema,\n type SecondFactorMethod,\n} from '#frontend/libs/oauth-search.ts';\nimport { tick } from '#frontend/libs/promise.ts';\nimport { getAuthorizationContextQueryOptions } from '#frontend/queries/authorization-context.ts';\nimport { appConfigQueryOptions } from '#frontend/queries/config.ts';\nimport { registerMutationOptions } from '#frontend/queries/register.ts';\nimport { getSessionQueryOptions } from '#frontend/queries/session.ts';\nimport { getTermsQueryOptions } from '#frontend/queries/terms.ts';\n\nexport const Route = createFileRoute('/register/')({\n component: Register,\n errorComponent: RouteErrorFallback,\n validateSearch: OAuthSearchSchema,\n beforeLoad: async ({ context }) => {\n const config = await context.queryClient.ensureQueryData(\n appConfigQueryOptions,\n );\n if (!config.registration.public_registration) {\n throw redirect({\n to: '/',\n replace: true,\n });\n }\n },\n loaderDeps: ({ search }) => ({\n lang: search.lang,\n search,\n }),\n loader: async ({ context, deps }) => {\n const lang = deps.lang ?? context.i18n.language;\n const queries: Array<Promise<unknown>> = [\n context.queryClient.ensureQueryData(appConfigQueryOptions),\n context.queryClient.ensureQueryData(getTermsQueryOptions(lang)),\n ];\n\n if (hasAuthorizationContext(deps.search)) {\n queries.push(\n context.queryClient.ensureQueryData(\n getAuthorizationContextQueryOptions(deps.search),\n ),\n );\n }\n\n await Promise.all(queries);\n },\n});\n\nfunction Register() {\n const { t, i18n } = useTranslation();\n const navigate = useNavigate();\n const queryClient = useQueryClient();\n const search = Route.useSearch();\n\n const lang = search.lang ?? i18n.language;\n const deferredLang = useDeferredValue(lang);\n\n const { data: configData } = useSuspenseQuery(appConfigQueryOptions);\n const { data: termsData } = useSuspenseQuery(\n getTermsQueryOptions(deferredLang),\n );\n\n const isPasswordAuthEnabled = configData.auth.password.enabled;\n const passwordPolicy = configData.auth.password.policy;\n\n // Check if there are any terms to display\n const hasTerms = termsData.terms.length > 0;\n const explicitTerms = useMemo(\n () => termsData.terms.filter((term) => term.consentMode === 'explicit'),\n [termsData.terms],\n );\n const hasExplicitTerms = explicitTerms.length > 0;\n\n // Get implicit notice from config\n const implicitNotice =\n configData.registration.signup_notice?.[lang] ??\n configData.registration.signup_notice?.[configData.i18n.fallback_language];\n\n const registerSchema = useMemo(\n () =>\n z.object({\n email: z\n .string()\n .min(1, t('validation.email.required'))\n .pipe(z.email(t('validation.email.invalid'))),\n password: z\n .string()\n .min(\n passwordPolicy.min_length,\n t('validation.password.min', {\n count: passwordPolicy.min_length,\n }),\n )\n .max(\n passwordPolicy.max_length,\n t('validation.password.max', {\n count: passwordPolicy.max_length,\n }),\n ),\n termsConsents: z.object(\n Object.fromEntries(\n explicitTerms.map((term) => [\n term.id,\n term.required\n ? z.literal(true, {\n message: t('validation.terms.required'),\n })\n : z.boolean(),\n ]),\n ),\n ),\n }),\n [explicitTerms, passwordPolicy.max_length, passwordPolicy.min_length, t],\n );\n\n type RegisterFormValues = z.infer<typeof registerSchema>;\n\n const registerMutation = useMutation({\n ...registerMutationOptions,\n onSuccess: async (data, params) => {\n const user = data.user;\n\n if (user.email_verification_required && !user.email_verified) {\n return navigate({\n to: '/verify/email',\n search: {\n email: params.email,\n ...extractOAuthParams(search),\n },\n });\n }\n\n queryClient.setQueryData(getSessionQueryOptions.queryKey, {\n user: user,\n });\n await tick();\n\n if (user.second_factor_required) {\n const available_2fa_methods: SecondFactorMethod[] = [];\n if (configData.auth.password.totp.enabled) {\n available_2fa_methods.push('totp');\n }\n if (configData.auth.passkey.enabled) {\n available_2fa_methods.push('passkey');\n }\n\n if (available_2fa_methods.length === 1) {\n const method = available_2fa_methods[0];\n if (method === 'totp') {\n return navigate({\n to: '/setup/totp',\n search: extractOAuthParams(search),\n });\n } else {\n return navigate({\n to: '/setup/passkey',\n search: {\n ...extractOAuthParams(search),\n passkey_name: 'default',\n },\n });\n }\n } else {\n return navigate({\n to: '/setup/2fa',\n search: extractOAuthParams(search),\n });\n }\n }\n\n if (isOAuthFlow(search)) {\n window.location.href = buildAuthenticatedAuthorizeUrl(search);\n } else {\n navigate({ to: '/profile' });\n }\n },\n onError: (error) => {\n if (error instanceof IssuaryError) {\n if (error.code === 'REGISTRATION_EMAIL_NOT_ALLOWED') {\n setError('email', {\n type: 'manual',\n message: t('register.error.emailNotAllowed'),\n });\n } else if (error.code === 'REGISTRATION_DISABLED') {\n setError('email', {\n type: 'manual',\n message: t('register.error.registrationDisabled'),\n });\n } else {\n setError('email', {\n type: 'manual',\n message: t('register.error.emailExists'),\n });\n }\n } else {\n setError('email', {\n type: 'manual',\n message: t('register.error.emailExists'),\n });\n }\n },\n onSettled: async () => {\n await queryClient.invalidateQueries({\n queryKey: getSessionQueryOptions.queryKey,\n });\n },\n });\n\n const {\n register,\n setError,\n handleSubmit,\n control,\n setValue,\n watch,\n formState: { errors },\n } = useForm<RegisterFormValues>({\n defaultValues: {\n email: '',\n password: '',\n termsConsents: Object.fromEntries(\n explicitTerms.map((term) => [term.id, false]),\n ),\n },\n resolver: standardSchemaResolver(registerSchema),\n mode: 'onChange',\n });\n\n const passwordValue = watch('password');\n\n const onSubmit = (values: RegisterFormValues) => {\n // Build consents array for registration\n // Only send explicit consents - implicit ones are handled by backend\n const consents = hasTerms\n ? explicitTerms.map((term) => ({\n termsId: term.id,\n agreed: values.termsConsents[term.id] ?? false,\n }))\n : undefined;\n\n registerMutation.mutate({\n email: values.email,\n password: values.password,\n consents,\n });\n };\n\n return (\n <AuthLayout>\n <AuthPageHeader\n subtitle={t('register.subtitle')}\n title={t('register.title')}\n />\n\n <AuthorizationContextBanner search={search} />\n\n {isPasswordAuthEnabled && (\n <form\n className=\"flex flex-col gap-tinyrack-lg\"\n onSubmit={handleSubmit(onSubmit)}\n >\n <AuthField\n autoComplete=\"email\"\n error={errors.email}\n icon={MailIcon}\n label={t('register.email.label')}\n placeholder={t('register.email.placeholder')}\n {...register('email')}\n type=\"email\"\n />\n\n <div className=\"flex flex-col gap-tinyrack-sm\">\n <AuthField\n autoComplete=\"new-password\"\n error={errors.password}\n hint={t('register.password.hint', {\n count: passwordPolicy.min_length,\n })}\n icon={LockIcon}\n label={t('register.password.label')}\n placeholder={t('register.password.placeholder')}\n {...register('password')}\n type=\"password\"\n />\n\n <PasswordStrength\n password={passwordValue}\n policy={passwordPolicy}\n />\n </div>\n\n <div className=\"flex flex-col gap-tinyrack-md\">\n {implicitNotice && (\n <div className=\"text-center text-tinyrack-text-muted text-tinyrack-xs\">\n <div\n className=\"prose prose-sm text-tinyrack-xs! **:text-tinyrack-xs!\"\n dangerouslySetInnerHTML={{\n __html: implicitNotice,\n }}\n data-testid=\"terms-implicit-notice\"\n />\n </div>\n )}\n\n {implicitNotice && hasExplicitTerms && (\n <LabeledSeparator label={t('terms.additionalOptionalConsent')} />\n )}\n\n {hasTerms && hasExplicitTerms && (\n <TermsCheckboxList\n control={control}\n disabled={registerMutation.isPending}\n errors={errors}\n setValue={setValue}\n terms={explicitTerms}\n />\n )}\n </div>\n\n <TRButton\n className=\"w-full\"\n intent=\"primary\"\n loading={registerMutation.isPending}\n loadingLabel={t('register.submitting')}\n type=\"submit\"\n uiSize=\"lg\"\n >\n {t('register.submit')}\n </TRButton>\n </form>\n )}\n\n <AuthFooter>\n <AuthFooterLink\n link={\n <Link search={extractOAuthParams(search)} to=\"/login\">\n {t('register.link.login')}\n </Link>\n }\n text={t('register.footer.haveAccount')}\n />\n </AuthFooter>\n </AuthLayout>\n );\n}\n"],"mappings":"wCA4BsF,IAAAC,EAA7ED"}
1
+ {"version":3,"file":"register-UL_cNRnQ.js","names":["RouteErrorFallback","SplitErrorComponent","errorComponent"],"sources":["../../../frontend/src/routes/register/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 {\n createFileRoute,\n Link,\n redirect,\n useNavigate,\n} from '@tanstack/react-router';\nimport { TRButton } from '@tinyrack/ui/components/button';\nimport { LockIcon, MailIcon } from 'lucide-react';\nimport { useDeferredValue, useMemo } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport { AuthField } from '#frontend/components/auth/auth-field.tsx';\nimport {\n AuthFooter,\n AuthFooterLink,\n} from '#frontend/components/auth/auth-footer.tsx';\nimport { AuthPageHeader } from '#frontend/components/auth/auth-page-header.tsx';\nimport { AuthorizationContextBanner } from '#frontend/components/auth/authorization-context-banner.tsx';\nimport { PasswordStrength } from '#frontend/components/auth/password-strength.tsx';\nimport { TermsCheckboxList } from '#frontend/components/terms/terms-checkbox-list.tsx';\nimport { LabeledSeparator } from '#frontend/components/ui/labeled-separator.tsx';\nimport { RouteErrorFallback } from '#frontend/components/ui/route-error-fallback.tsx';\nimport { AuthLayout } from '#frontend/features/layout/auth-layout.tsx';\nimport { IssuaryError } from '#frontend/libs/error.ts';\n\nimport {\n buildAuthenticatedAuthorizeUrl,\n extractOAuthParams,\n hasAuthorizationContext,\n isOAuthFlow,\n OAuthSearchSchema,\n type SecondFactorMethod,\n} from '#frontend/libs/oauth-search.ts';\nimport { tick } from '#frontend/libs/promise.ts';\nimport { getAuthorizationContextQueryOptions } from '#frontend/queries/authorization-context.ts';\nimport { appConfigQueryOptions } from '#frontend/queries/config.ts';\nimport { registerMutationOptions } from '#frontend/queries/register.ts';\nimport { getSessionQueryOptions } from '#frontend/queries/session.ts';\nimport { getTermsQueryOptions } from '#frontend/queries/terms.ts';\n\nexport const Route = createFileRoute('/register/')({\n component: Register,\n errorComponent: RouteErrorFallback,\n validateSearch: OAuthSearchSchema,\n beforeLoad: async ({ context }) => {\n const config = await context.queryClient.ensureQueryData(\n appConfigQueryOptions,\n );\n if (!config.registration.public_registration) {\n throw redirect({\n to: '/',\n replace: true,\n });\n }\n },\n loaderDeps: ({ search }) => ({\n lang: search.lang,\n search,\n }),\n loader: async ({ context, deps }) => {\n const lang = deps.lang ?? context.i18n.language;\n const queries: Array<Promise<unknown>> = [\n context.queryClient.ensureQueryData(appConfigQueryOptions),\n context.queryClient.ensureQueryData(getTermsQueryOptions(lang)),\n ];\n\n if (hasAuthorizationContext(deps.search)) {\n queries.push(\n context.queryClient.ensureQueryData(\n getAuthorizationContextQueryOptions(deps.search),\n ),\n );\n }\n\n await Promise.all(queries);\n },\n});\n\nfunction Register() {\n const { t, i18n } = useTranslation();\n const navigate = useNavigate();\n const queryClient = useQueryClient();\n const search = Route.useSearch();\n\n const lang = search.lang ?? i18n.language;\n const deferredLang = useDeferredValue(lang);\n\n const { data: configData } = useSuspenseQuery(appConfigQueryOptions);\n const { data: termsData } = useSuspenseQuery(\n getTermsQueryOptions(deferredLang),\n );\n\n const isPasswordAuthEnabled = configData.auth.password.enabled;\n const passwordPolicy = configData.auth.password.policy;\n\n // Check if there are any terms to display\n const hasTerms = termsData.terms.length > 0;\n const explicitTerms = useMemo(\n () => termsData.terms.filter((term) => term.consentMode === 'explicit'),\n [termsData.terms],\n );\n const hasExplicitTerms = explicitTerms.length > 0;\n\n // Get implicit notice from config\n const implicitNotice =\n configData.registration.signup_notice?.[lang] ??\n configData.registration.signup_notice?.[configData.i18n.fallback_language];\n\n const registerSchema = useMemo(\n () =>\n z.object({\n email: z\n .string()\n .min(1, t('validation.email.required'))\n .pipe(z.email(t('validation.email.invalid'))),\n password: z\n .string()\n .min(\n passwordPolicy.min_length,\n t('validation.password.min', {\n count: passwordPolicy.min_length,\n }),\n )\n .max(\n passwordPolicy.max_length,\n t('validation.password.max', {\n count: passwordPolicy.max_length,\n }),\n ),\n termsConsents: z.object(\n Object.fromEntries(\n explicitTerms.map((term) => [\n term.id,\n term.required\n ? z.literal(true, {\n message: t('validation.terms.required'),\n })\n : z.boolean(),\n ]),\n ),\n ),\n }),\n [explicitTerms, passwordPolicy.max_length, passwordPolicy.min_length, t],\n );\n\n type RegisterFormValues = z.infer<typeof registerSchema>;\n\n const registerMutation = useMutation({\n ...registerMutationOptions,\n onSuccess: async (data, params) => {\n const user = data.user;\n\n if (user.email_verification_required && !user.email_verified) {\n return navigate({\n to: '/verify/email',\n search: {\n email: params.email,\n ...extractOAuthParams(search),\n },\n });\n }\n\n queryClient.setQueryData(getSessionQueryOptions.queryKey, {\n user: user,\n });\n await tick();\n\n if (user.second_factor_required) {\n const available_2fa_methods: SecondFactorMethod[] = [];\n if (configData.auth.password.totp.enabled) {\n available_2fa_methods.push('totp');\n }\n if (configData.auth.passkey.enabled) {\n available_2fa_methods.push('passkey');\n }\n\n if (available_2fa_methods.length === 1) {\n const method = available_2fa_methods[0];\n if (method === 'totp') {\n return navigate({\n to: '/setup/totp',\n search: extractOAuthParams(search),\n });\n } else {\n return navigate({\n to: '/setup/passkey',\n search: {\n ...extractOAuthParams(search),\n passkey_name: 'default',\n },\n });\n }\n } else {\n return navigate({\n to: '/setup/2fa',\n search: extractOAuthParams(search),\n });\n }\n }\n\n if (isOAuthFlow(search)) {\n window.location.href = buildAuthenticatedAuthorizeUrl(search);\n } else {\n navigate({ to: '/profile' });\n }\n },\n onError: (error) => {\n if (error instanceof IssuaryError) {\n if (error.code === 'REGISTRATION_EMAIL_NOT_ALLOWED') {\n setError('email', {\n type: 'manual',\n message: t('register.error.emailNotAllowed'),\n });\n } else if (error.code === 'REGISTRATION_DISABLED') {\n setError('email', {\n type: 'manual',\n message: t('register.error.registrationDisabled'),\n });\n } else {\n setError('email', {\n type: 'manual',\n message: t('register.error.emailExists'),\n });\n }\n } else {\n setError('email', {\n type: 'manual',\n message: t('register.error.emailExists'),\n });\n }\n },\n onSettled: async () => {\n await queryClient.invalidateQueries({\n queryKey: getSessionQueryOptions.queryKey,\n });\n },\n });\n\n const {\n register,\n setError,\n handleSubmit,\n control,\n setValue,\n watch,\n formState: { errors },\n } = useForm<RegisterFormValues>({\n defaultValues: {\n email: '',\n password: '',\n termsConsents: Object.fromEntries(\n explicitTerms.map((term) => [term.id, false]),\n ),\n },\n resolver: standardSchemaResolver(registerSchema),\n mode: 'onChange',\n });\n\n const passwordValue = watch('password');\n\n const onSubmit = (values: RegisterFormValues) => {\n // Build consents array for registration\n // Only send explicit consents - implicit ones are handled by backend\n const consents = hasTerms\n ? explicitTerms.map((term) => ({\n termsId: term.id,\n agreed: values.termsConsents[term.id] ?? false,\n }))\n : undefined;\n\n registerMutation.mutate({\n email: values.email,\n password: values.password,\n consents,\n });\n };\n\n return (\n <AuthLayout>\n <AuthPageHeader\n subtitle={t('register.subtitle')}\n title={t('register.title')}\n />\n\n <AuthorizationContextBanner search={search} />\n\n {isPasswordAuthEnabled && (\n <form\n className=\"flex flex-col gap-tinyrack-lg\"\n onSubmit={handleSubmit(onSubmit)}\n >\n <AuthField\n autoComplete=\"email\"\n error={errors.email}\n icon={MailIcon}\n label={t('register.email.label')}\n placeholder={t('register.email.placeholder')}\n {...register('email')}\n type=\"email\"\n />\n\n <div className=\"flex flex-col gap-tinyrack-sm\">\n <AuthField\n autoComplete=\"new-password\"\n error={errors.password}\n hint={t('register.password.hint', {\n count: passwordPolicy.min_length,\n })}\n icon={LockIcon}\n label={t('register.password.label')}\n placeholder={t('register.password.placeholder')}\n {...register('password')}\n type=\"password\"\n />\n\n <PasswordStrength\n password={passwordValue}\n policy={passwordPolicy}\n />\n </div>\n\n <div className=\"flex flex-col gap-tinyrack-md\">\n {implicitNotice && (\n <div className=\"text-center text-tinyrack-text-muted text-tinyrack-xs\">\n <div\n className=\"prose prose-sm text-tinyrack-xs! **:text-tinyrack-xs!\"\n dangerouslySetInnerHTML={{\n __html: implicitNotice,\n }}\n data-testid=\"terms-implicit-notice\"\n />\n </div>\n )}\n\n {implicitNotice && hasExplicitTerms && (\n <LabeledSeparator label={t('terms.additionalOptionalConsent')} />\n )}\n\n {hasTerms && hasExplicitTerms && (\n <TermsCheckboxList\n control={control}\n disabled={registerMutation.isPending}\n errors={errors}\n setValue={setValue}\n terms={explicitTerms}\n />\n )}\n </div>\n\n <TRButton\n className=\"w-full\"\n intent=\"primary\"\n loading={registerMutation.isPending}\n loadingLabel={t('register.submitting')}\n type=\"submit\"\n uiSize=\"lg\"\n >\n {t('register.submit')}\n </TRButton>\n </form>\n )}\n\n <AuthFooter>\n <AuthFooterLink\n link={\n <Link search={extractOAuthParams(search)} to=\"/login\">\n {t('register.link.login')}\n </Link>\n }\n text={t('register.footer.haveAccount')}\n />\n </AuthFooter>\n </AuthLayout>\n );\n}\n"],"mappings":"wCA4BsF,IAAAC,EAA7ED"}
@@ -0,0 +1,2 @@
1
+ import{J as e,Z as t,i as n,q as r,s as i}from"./createLucideIcon-BPpAIXR_.js";import{t as a}from"./config-v3-oy_ef.js";import{t as o}from"./useMutation-ZLFeJg75.js";import{tt as s}from"./shell-header-bar-C9AB5lT-.js";import{t as c}from"./link-BN90emXr.js";import{t as l}from"./circle-check-6QTLhwM_.js";import{t as u}from"./auth-field-HwxENJnB.js";import{t as d}from"./key-round-CdvHb8vT.js";import{t as f}from"./lock-keyhole-Cz72xkCn.js";import{t as p}from"./lock-7NRyvXBS.js";import{c as m,o as h}from"./zod-C87S6Tec.js";import{L as g,R as _,V as v,c as y}from"./index-gliHKncz.js";import{t as b}from"./auth-page-header-CIs08f5Y.js";import{t as x}from"./alert-leqyvHOp.js";import{t as S}from"./auth-layout-CoeqNhTT.js";import{n as C,t as w}from"./auth-footer-Cz5ivsDc.js";import{r as T,t as E}from"./standard-schema-e8gtSzGE.js";import{n as D}from"./password-reset-CIacHnvA.js";import{t as O}from"./password-strength-C0Zkrrct.js";var k=t(e()),A=r();function j(){let{t:e}=n(),t=v(),{token:r}=y.useSearch(),[j,M]=(0,k.useState)(!1),{data:N}=s(a),P=N.auth.password.policy,F=(0,k.useMemo)(()=>h({token:m().min(1,e(`validation.token.required`)),password:m().min(P.min_length,e(`validation.password.min`,{count:P.min_length})).max(P.max_length,e(`validation.password.max`,{count:P.max_length})),confirmPassword:m().min(1,e(`validation.confirmPassword.required`))}).refine(e=>e.password===e.confirmPassword,{message:e(`validation.confirmPassword.mismatch`),path:[`confirmPassword`]}),[P.max_length,P.min_length,e]),I=o({...D,onSuccess:()=>{M(!0)}}),{register:L,setError:R,handleSubmit:z,watch:B,formState:{errors:V}}=T({defaultValues:{token:r||``,password:``,confirmPassword:``},resolver:E(F)}),H=B(`password`);return j?(0,A.jsx)(S,{children:(0,A.jsx)(`div`,{"data-testid":`alert-success`,children:(0,A.jsx)(g,{description:(0,A.jsxs)(A.Fragment,{children:[e(`resetPassword.success.subtitle`),(0,A.jsx)(`br`,{}),e(`resetPassword.success.description`)]}),icon:l,title:e(`resetPassword.success.title`),tone:`success`,children:(0,A.jsx)(i,{className:`w-full`,"data-testid":`reset-password-go-login`,intent:`primary`,onClick:()=>t({to:`/login`}),type:`button`,uiSize:`lg`,children:e(`resetPassword.success.goToLogin`)})})})}):(0,A.jsxs)(S,{children:[(0,A.jsx)(b,{subtitle:e(`resetPassword.subtitle`),title:e(`resetPassword.title`)}),(0,A.jsxs)(`form`,{className:`flex flex-col gap-tinyrack-lg`,onSubmit:z(async t=>{try{await I.mutateAsync({token:t.token,password:t.password})}catch(t){t&&typeof t==`object`&&`code`in t?t.code===`INVALID_PASSWORD_RESET_TOKEN`?R(`token`,{type:`manual`,message:e(`resetPassword.error.invalidToken`)}):t.code===`USER_NOT_EDITABLE`&&R(`token`,{type:`manual`,message:e(`resetPassword.error.notEditable`)}):R(`token`,{type:`manual`,message:e(`resetPassword.error.invalidToken`)})}}),children:[!r&&(0,A.jsx)(u,{error:V.token,icon:d,label:e(`resetPassword.token.label`),placeholder:e(`resetPassword.token.placeholder`),...L(`token`),type:`text`}),r&&V.token&&(0,A.jsx)(x,{"data-testid":`reset-password-token-error`,icon:_,type:`error`,children:V.token.message}),(0,A.jsxs)(`div`,{className:`flex flex-col gap-tinyrack-sm`,children:[(0,A.jsx)(u,{autoComplete:`new-password`,error:V.password,icon:p,label:e(`resetPassword.password.label`),placeholder:e(`resetPassword.password.placeholder`),...L(`password`),type:`password`}),(0,A.jsx)(O,{password:H,policy:P})]}),(0,A.jsx)(u,{autoComplete:`new-password`,error:V.confirmPassword,icon:f,label:e(`resetPassword.confirmPassword.label`),placeholder:e(`resetPassword.confirmPassword.placeholder`),...L(`confirmPassword`),type:`password`}),(0,A.jsx)(i,{className:`w-full`,intent:`primary`,loading:I.isPending,loadingLabel:e(`resetPassword.submitting`),type:`submit`,uiSize:`lg`,children:e(`resetPassword.submit`)})]}),(0,A.jsx)(w,{children:(0,A.jsx)(C,{link:(0,A.jsx)(c,{"data-testid":`reset-password-back-to-login`,to:`/login`,children:e(`resetPassword.backToLogin`)})})})]})}export{j as component};
2
+ //# sourceMappingURL=reset-N3RQTYEB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reset-N3RQTYEB.js","names":["standardSchemaResolver","useMutation","useSuspenseQuery","Link","useNavigate","TRButton","CircleAlertIcon","CircleCheckIcon","KeyRoundIcon","LockIcon","LockKeyholeIcon","useMemo","useState","useForm","useTranslation","z","AuthField","AuthFooter","AuthFooterLink","AuthOutcome","AuthPageHeader","PasswordStrength","Alert","AuthLayout","appConfigQueryOptions","resetPasswordMutationOptions","Route","ResetPasswordFormValues","token","password","confirmPassword","ResetPassword","t","navigate","search","useSearch","queryToken","resetSuccess","setResetSuccess","data","configData","passwordPolicy","auth","policy","resetPasswordSchema","object","string","min","min_length","count","max","max_length","refine","message","path","resetPasswordMutation","onSuccess","register","setError","handleSubmit","watch","formState","errors","defaultValues","resolver","passwordValue","onSubmit","values","mutateAsync","error","code","type","to","isPending","component"],"sources":["../../../frontend/src/routes/password/reset/index.tsx?tsr-split=component"],"sourcesContent":["import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';\nimport { useMutation, useSuspenseQuery } from '@tanstack/react-query';\nimport {\n createFileRoute,\n Link,\n redirect,\n useNavigate,\n} from '@tanstack/react-router';\nimport { TRButton } from '@tinyrack/ui/components/button';\nimport {\n CircleAlertIcon,\n CircleCheckIcon,\n KeyRoundIcon,\n LockIcon,\n LockKeyholeIcon,\n} from 'lucide-react';\nimport { useMemo, useState } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport { AuthField } from '#frontend/components/auth/auth-field.tsx';\nimport {\n AuthFooter,\n AuthFooterLink,\n} from '#frontend/components/auth/auth-footer.tsx';\nimport { AuthOutcome } from '#frontend/components/auth/auth-outcome.tsx';\nimport { AuthPageHeader } from '#frontend/components/auth/auth-page-header.tsx';\nimport { PasswordStrength } from '#frontend/components/auth/password-strength.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';\n\nimport { appConfigQueryOptions } from '#frontend/queries/config.ts';\nimport { resetPasswordMutationOptions } from '#frontend/queries/password-reset.ts';\n\nconst SearchSchema = z.object({\n token: z.string().default(''),\n});\n\nexport const Route = createFileRoute('/password/reset/')({\n component: ResetPassword,\n errorComponent: RouteErrorFallback,\n validateSearch: SearchSchema,\n beforeLoad: async ({ context }) => {\n const config = await context.queryClient.ensureQueryData(\n appConfigQueryOptions,\n );\n const isPasswordAuthEnabled = config.auth.password.enabled;\n if (!isPasswordAuthEnabled) {\n throw redirect({ to: '/login' });\n }\n },\n loader: async ({ context }) => {\n await context.queryClient.ensureQueryData(appConfigQueryOptions);\n },\n});\n\ntype ResetPasswordFormValues = {\n token: string;\n password: string;\n confirmPassword: string;\n};\n\nfunction ResetPassword() {\n const { t } = useTranslation();\n const navigate = useNavigate();\n const search = Route.useSearch();\n const { token: queryToken } = search;\n const [resetSuccess, setResetSuccess] = useState(false);\n const { data: configData } = useSuspenseQuery(appConfigQueryOptions);\n const passwordPolicy = configData.auth.password.policy;\n\n const resetPasswordSchema = useMemo(\n () =>\n z\n .object({\n token: z.string().min(1, t('validation.token.required')),\n password: z\n .string()\n .min(\n passwordPolicy.min_length,\n t('validation.password.min', {\n count: passwordPolicy.min_length,\n }),\n )\n .max(\n passwordPolicy.max_length,\n t('validation.password.max', {\n count: passwordPolicy.max_length,\n }),\n ),\n confirmPassword: z\n .string()\n .min(1, t('validation.confirmPassword.required')),\n })\n .refine((data) => data.password === data.confirmPassword, {\n message: t('validation.confirmPassword.mismatch'),\n path: ['confirmPassword'],\n }),\n [passwordPolicy.max_length, passwordPolicy.min_length, t],\n );\n\n const resetPasswordMutation = useMutation({\n ...resetPasswordMutationOptions,\n onSuccess: () => {\n setResetSuccess(true);\n },\n });\n\n const {\n register,\n setError,\n handleSubmit,\n watch,\n formState: { errors },\n } = useForm<ResetPasswordFormValues>({\n defaultValues: {\n token: queryToken || '',\n password: '',\n confirmPassword: '',\n },\n resolver: standardSchemaResolver(resetPasswordSchema),\n });\n\n const passwordValue = watch('password');\n\n const onSubmit = async (values: ResetPasswordFormValues) => {\n try {\n await resetPasswordMutation.mutateAsync({\n token: values.token,\n password: values.password,\n });\n } catch (error) {\n if (error && typeof error === 'object' && 'code' in error) {\n if (error.code === 'INVALID_PASSWORD_RESET_TOKEN') {\n setError('token', {\n type: 'manual',\n message: t('resetPassword.error.invalidToken'),\n });\n } else if (error.code === 'USER_NOT_EDITABLE') {\n setError('token', {\n type: 'manual',\n message: t('resetPassword.error.notEditable'),\n });\n }\n } else {\n setError('token', {\n type: 'manual',\n message: t('resetPassword.error.invalidToken'),\n });\n }\n }\n };\n\n if (resetSuccess) {\n return (\n <AuthLayout>\n {/*\n The testid stays on a wrapper: e2e treats \"the success alert\" as the\n whole terminal state, which is now the outcome block rather than a\n banner stacked above a header.\n */}\n <div data-testid=\"alert-success\">\n <AuthOutcome\n description={\n <>\n {t('resetPassword.success.subtitle')}\n <br />\n {t('resetPassword.success.description')}\n </>\n }\n icon={CircleCheckIcon}\n title={t('resetPassword.success.title')}\n tone=\"success\"\n >\n <TRButton\n className=\"w-full\"\n data-testid=\"reset-password-go-login\"\n intent=\"primary\"\n onClick={() => navigate({ to: '/login' })}\n type=\"button\"\n uiSize=\"lg\"\n >\n {t('resetPassword.success.goToLogin')}\n </TRButton>\n </AuthOutcome>\n </div>\n </AuthLayout>\n );\n }\n\n return (\n <AuthLayout>\n <AuthPageHeader\n subtitle={t('resetPassword.subtitle')}\n title={t('resetPassword.title')}\n />\n\n <form\n className=\"flex flex-col gap-tinyrack-lg\"\n onSubmit={handleSubmit(onSubmit)}\n >\n {!queryToken && (\n <AuthField\n error={errors.token}\n icon={KeyRoundIcon}\n label={t('resetPassword.token.label')}\n placeholder={t('resetPassword.token.placeholder')}\n {...register('token')}\n type=\"text\"\n />\n )}\n\n {/*\n A token that arrived in the URL has no field to attach an error to,\n so a rejected link is reported as a banner above the form. The form\n stays put: the user can paste a fresh token without a round trip.\n */}\n {queryToken && errors.token && (\n <Alert\n data-testid=\"reset-password-token-error\"\n icon={CircleAlertIcon}\n type=\"error\"\n >\n {errors.token.message}\n </Alert>\n )}\n\n <div className=\"flex flex-col gap-tinyrack-sm\">\n <AuthField\n autoComplete=\"new-password\"\n error={errors.password}\n icon={LockIcon}\n label={t('resetPassword.password.label')}\n placeholder={t('resetPassword.password.placeholder')}\n {...register('password')}\n type=\"password\"\n />\n\n <PasswordStrength password={passwordValue} policy={passwordPolicy} />\n </div>\n\n <AuthField\n autoComplete=\"new-password\"\n error={errors.confirmPassword}\n icon={LockKeyholeIcon}\n label={t('resetPassword.confirmPassword.label')}\n placeholder={t('resetPassword.confirmPassword.placeholder')}\n {...register('confirmPassword')}\n type=\"password\"\n />\n\n <TRButton\n className=\"w-full\"\n intent=\"primary\"\n loading={resetPasswordMutation.isPending}\n loadingLabel={t('resetPassword.submitting')}\n type=\"submit\"\n uiSize=\"lg\"\n >\n {t('resetPassword.submit')}\n </TRButton>\n </form>\n\n <AuthFooter>\n <AuthFooterLink\n link={\n <Link data-testid=\"reset-password-back-to-login\" to=\"/login\">\n {t('resetPassword.backToLogin')}\n </Link>\n }\n />\n </AuthFooter>\n </AuthLayout>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import{I as e}from"./index-gliHKncz.js";var t=e;export{t as errorComponent};
2
+ //# sourceMappingURL=reset-UL_cNRnQ.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"reset-C5vUVgr_.js","names":["RouteErrorFallback","ResetPasswordFormValues","token","password","confirmPassword","SplitErrorComponent","errorComponent"],"sources":["../../../frontend/src/routes/password/reset/index.tsx?tsr-split=errorComponent"],"sourcesContent":["import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';\nimport { useMutation, useSuspenseQuery } from '@tanstack/react-query';\nimport {\n createFileRoute,\n Link,\n redirect,\n useNavigate,\n} from '@tanstack/react-router';\nimport { TRButton } from '@tinyrack/ui/components/button';\nimport {\n CircleAlertIcon,\n CircleCheckIcon,\n KeyRoundIcon,\n LockIcon,\n LockKeyholeIcon,\n} from 'lucide-react';\nimport { useMemo, useState } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport { AuthField } from '#frontend/components/auth/auth-field.tsx';\nimport {\n AuthFooter,\n AuthFooterLink,\n} from '#frontend/components/auth/auth-footer.tsx';\nimport { AuthOutcome } from '#frontend/components/auth/auth-outcome.tsx';\nimport { AuthPageHeader } from '#frontend/components/auth/auth-page-header.tsx';\nimport { PasswordStrength } from '#frontend/components/auth/password-strength.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';\n\nimport { appConfigQueryOptions } from '#frontend/queries/config.ts';\nimport { resetPasswordMutationOptions } from '#frontend/queries/password-reset.ts';\n\nconst SearchSchema = z.object({\n token: z.string().default(''),\n});\n\nexport const Route = createFileRoute('/password/reset/')({\n component: ResetPassword,\n errorComponent: RouteErrorFallback,\n validateSearch: SearchSchema,\n beforeLoad: async ({ context }) => {\n const config = await context.queryClient.ensureQueryData(\n appConfigQueryOptions,\n );\n const isPasswordAuthEnabled = config.auth.password.enabled;\n if (!isPasswordAuthEnabled) {\n throw redirect({ to: '/login' });\n }\n },\n loader: async ({ context }) => {\n await context.queryClient.ensureQueryData(appConfigQueryOptions);\n },\n});\n\ntype ResetPasswordFormValues = {\n token: string;\n password: string;\n confirmPassword: string;\n};\n\nfunction ResetPassword() {\n const { t } = useTranslation();\n const navigate = useNavigate();\n const search = Route.useSearch();\n const { token: queryToken } = search;\n const [resetSuccess, setResetSuccess] = useState(false);\n const { data: configData } = useSuspenseQuery(appConfigQueryOptions);\n const passwordPolicy = configData.auth.password.policy;\n\n const resetPasswordSchema = useMemo(\n () =>\n z\n .object({\n token: z.string().min(1, t('validation.token.required')),\n password: z\n .string()\n .min(\n passwordPolicy.min_length,\n t('validation.password.min', {\n count: passwordPolicy.min_length,\n }),\n )\n .max(\n passwordPolicy.max_length,\n t('validation.password.max', {\n count: passwordPolicy.max_length,\n }),\n ),\n confirmPassword: z\n .string()\n .min(1, t('validation.confirmPassword.required')),\n })\n .refine((data) => data.password === data.confirmPassword, {\n message: t('validation.confirmPassword.mismatch'),\n path: ['confirmPassword'],\n }),\n [passwordPolicy.max_length, passwordPolicy.min_length, t],\n );\n\n const resetPasswordMutation = useMutation({\n ...resetPasswordMutationOptions,\n onSuccess: () => {\n setResetSuccess(true);\n },\n });\n\n const {\n register,\n setError,\n handleSubmit,\n watch,\n formState: { errors },\n } = useForm<ResetPasswordFormValues>({\n defaultValues: {\n token: queryToken || '',\n password: '',\n confirmPassword: '',\n },\n resolver: standardSchemaResolver(resetPasswordSchema),\n });\n\n const passwordValue = watch('password');\n\n const onSubmit = async (values: ResetPasswordFormValues) => {\n try {\n await resetPasswordMutation.mutateAsync({\n token: values.token,\n password: values.password,\n });\n } catch (error) {\n if (error && typeof error === 'object' && 'code' in error) {\n if (error.code === 'INVALID_PASSWORD_RESET_TOKEN') {\n setError('token', {\n type: 'manual',\n message: t('resetPassword.error.invalidToken'),\n });\n } else if (error.code === 'USER_NOT_EDITABLE') {\n setError('token', {\n type: 'manual',\n message: t('resetPassword.error.notEditable'),\n });\n }\n } else {\n setError('token', {\n type: 'manual',\n message: t('resetPassword.error.invalidToken'),\n });\n }\n }\n };\n\n if (resetSuccess) {\n return (\n <AuthLayout>\n {/*\n The testid stays on a wrapper: e2e treats \"the success alert\" as the\n whole terminal state, which is now the outcome block rather than a\n banner stacked above a header.\n */}\n <div data-testid=\"alert-success\">\n <AuthOutcome\n description={\n <>\n {t('resetPassword.success.subtitle')}\n <br />\n {t('resetPassword.success.description')}\n </>\n }\n icon={CircleCheckIcon}\n title={t('resetPassword.success.title')}\n tone=\"success\"\n >\n <TRButton\n className=\"w-full\"\n data-testid=\"reset-password-go-login\"\n intent=\"primary\"\n onClick={() => navigate({ to: '/login' })}\n type=\"button\"\n uiSize=\"lg\"\n >\n {t('resetPassword.success.goToLogin')}\n </TRButton>\n </AuthOutcome>\n </div>\n </AuthLayout>\n );\n }\n\n return (\n <AuthLayout>\n <AuthPageHeader\n subtitle={t('resetPassword.subtitle')}\n title={t('resetPassword.title')}\n />\n\n <form\n className=\"flex flex-col gap-tinyrack-lg\"\n onSubmit={handleSubmit(onSubmit)}\n >\n {!queryToken && (\n <AuthField\n error={errors.token}\n icon={KeyRoundIcon}\n label={t('resetPassword.token.label')}\n placeholder={t('resetPassword.token.placeholder')}\n {...register('token')}\n type=\"text\"\n />\n )}\n\n {/*\n A token that arrived in the URL has no field to attach an error to,\n so a rejected link is reported as a banner above the form. The form\n stays put: the user can paste a fresh token without a round trip.\n */}\n {queryToken && errors.token && (\n <Alert\n data-testid=\"reset-password-token-error\"\n icon={CircleAlertIcon}\n type=\"error\"\n >\n {errors.token.message}\n </Alert>\n )}\n\n <div className=\"flex flex-col gap-tinyrack-sm\">\n <AuthField\n autoComplete=\"new-password\"\n error={errors.password}\n icon={LockIcon}\n label={t('resetPassword.password.label')}\n placeholder={t('resetPassword.password.placeholder')}\n {...register('password')}\n type=\"password\"\n />\n\n <PasswordStrength password={passwordValue} policy={passwordPolicy} />\n </div>\n\n <AuthField\n autoComplete=\"new-password\"\n error={errors.confirmPassword}\n icon={LockKeyholeIcon}\n label={t('resetPassword.confirmPassword.label')}\n placeholder={t('resetPassword.confirmPassword.placeholder')}\n {...register('confirmPassword')}\n type=\"password\"\n />\n\n <TRButton\n className=\"w-full\"\n intent=\"primary\"\n loading={resetPasswordMutation.isPending}\n loadingLabel={t('resetPassword.submitting')}\n type=\"submit\"\n uiSize=\"lg\"\n >\n {t('resetPassword.submit')}\n </TRButton>\n </form>\n\n <AuthFooter>\n <AuthFooterLink\n link={\n <Link data-testid=\"reset-password-back-to-login\" to=\"/login\">\n {t('resetPassword.backToLogin')}\n </Link>\n }\n />\n </AuthFooter>\n </AuthLayout>\n );\n}\n"],"mappings":"wCA6DE,IAAAK,EAhCOL"}
1
+ {"version":3,"file":"reset-UL_cNRnQ.js","names":["RouteErrorFallback","ResetPasswordFormValues","token","password","confirmPassword","SplitErrorComponent","errorComponent"],"sources":["../../../frontend/src/routes/password/reset/index.tsx?tsr-split=errorComponent"],"sourcesContent":["import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';\nimport { useMutation, useSuspenseQuery } from '@tanstack/react-query';\nimport {\n createFileRoute,\n Link,\n redirect,\n useNavigate,\n} from '@tanstack/react-router';\nimport { TRButton } from '@tinyrack/ui/components/button';\nimport {\n CircleAlertIcon,\n CircleCheckIcon,\n KeyRoundIcon,\n LockIcon,\n LockKeyholeIcon,\n} from 'lucide-react';\nimport { useMemo, useState } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport { AuthField } from '#frontend/components/auth/auth-field.tsx';\nimport {\n AuthFooter,\n AuthFooterLink,\n} from '#frontend/components/auth/auth-footer.tsx';\nimport { AuthOutcome } from '#frontend/components/auth/auth-outcome.tsx';\nimport { AuthPageHeader } from '#frontend/components/auth/auth-page-header.tsx';\nimport { PasswordStrength } from '#frontend/components/auth/password-strength.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';\n\nimport { appConfigQueryOptions } from '#frontend/queries/config.ts';\nimport { resetPasswordMutationOptions } from '#frontend/queries/password-reset.ts';\n\nconst SearchSchema = z.object({\n token: z.string().default(''),\n});\n\nexport const Route = createFileRoute('/password/reset/')({\n component: ResetPassword,\n errorComponent: RouteErrorFallback,\n validateSearch: SearchSchema,\n beforeLoad: async ({ context }) => {\n const config = await context.queryClient.ensureQueryData(\n appConfigQueryOptions,\n );\n const isPasswordAuthEnabled = config.auth.password.enabled;\n if (!isPasswordAuthEnabled) {\n throw redirect({ to: '/login' });\n }\n },\n loader: async ({ context }) => {\n await context.queryClient.ensureQueryData(appConfigQueryOptions);\n },\n});\n\ntype ResetPasswordFormValues = {\n token: string;\n password: string;\n confirmPassword: string;\n};\n\nfunction ResetPassword() {\n const { t } = useTranslation();\n const navigate = useNavigate();\n const search = Route.useSearch();\n const { token: queryToken } = search;\n const [resetSuccess, setResetSuccess] = useState(false);\n const { data: configData } = useSuspenseQuery(appConfigQueryOptions);\n const passwordPolicy = configData.auth.password.policy;\n\n const resetPasswordSchema = useMemo(\n () =>\n z\n .object({\n token: z.string().min(1, t('validation.token.required')),\n password: z\n .string()\n .min(\n passwordPolicy.min_length,\n t('validation.password.min', {\n count: passwordPolicy.min_length,\n }),\n )\n .max(\n passwordPolicy.max_length,\n t('validation.password.max', {\n count: passwordPolicy.max_length,\n }),\n ),\n confirmPassword: z\n .string()\n .min(1, t('validation.confirmPassword.required')),\n })\n .refine((data) => data.password === data.confirmPassword, {\n message: t('validation.confirmPassword.mismatch'),\n path: ['confirmPassword'],\n }),\n [passwordPolicy.max_length, passwordPolicy.min_length, t],\n );\n\n const resetPasswordMutation = useMutation({\n ...resetPasswordMutationOptions,\n onSuccess: () => {\n setResetSuccess(true);\n },\n });\n\n const {\n register,\n setError,\n handleSubmit,\n watch,\n formState: { errors },\n } = useForm<ResetPasswordFormValues>({\n defaultValues: {\n token: queryToken || '',\n password: '',\n confirmPassword: '',\n },\n resolver: standardSchemaResolver(resetPasswordSchema),\n });\n\n const passwordValue = watch('password');\n\n const onSubmit = async (values: ResetPasswordFormValues) => {\n try {\n await resetPasswordMutation.mutateAsync({\n token: values.token,\n password: values.password,\n });\n } catch (error) {\n if (error && typeof error === 'object' && 'code' in error) {\n if (error.code === 'INVALID_PASSWORD_RESET_TOKEN') {\n setError('token', {\n type: 'manual',\n message: t('resetPassword.error.invalidToken'),\n });\n } else if (error.code === 'USER_NOT_EDITABLE') {\n setError('token', {\n type: 'manual',\n message: t('resetPassword.error.notEditable'),\n });\n }\n } else {\n setError('token', {\n type: 'manual',\n message: t('resetPassword.error.invalidToken'),\n });\n }\n }\n };\n\n if (resetSuccess) {\n return (\n <AuthLayout>\n {/*\n The testid stays on a wrapper: e2e treats \"the success alert\" as the\n whole terminal state, which is now the outcome block rather than a\n banner stacked above a header.\n */}\n <div data-testid=\"alert-success\">\n <AuthOutcome\n description={\n <>\n {t('resetPassword.success.subtitle')}\n <br />\n {t('resetPassword.success.description')}\n </>\n }\n icon={CircleCheckIcon}\n title={t('resetPassword.success.title')}\n tone=\"success\"\n >\n <TRButton\n className=\"w-full\"\n data-testid=\"reset-password-go-login\"\n intent=\"primary\"\n onClick={() => navigate({ to: '/login' })}\n type=\"button\"\n uiSize=\"lg\"\n >\n {t('resetPassword.success.goToLogin')}\n </TRButton>\n </AuthOutcome>\n </div>\n </AuthLayout>\n );\n }\n\n return (\n <AuthLayout>\n <AuthPageHeader\n subtitle={t('resetPassword.subtitle')}\n title={t('resetPassword.title')}\n />\n\n <form\n className=\"flex flex-col gap-tinyrack-lg\"\n onSubmit={handleSubmit(onSubmit)}\n >\n {!queryToken && (\n <AuthField\n error={errors.token}\n icon={KeyRoundIcon}\n label={t('resetPassword.token.label')}\n placeholder={t('resetPassword.token.placeholder')}\n {...register('token')}\n type=\"text\"\n />\n )}\n\n {/*\n A token that arrived in the URL has no field to attach an error to,\n so a rejected link is reported as a banner above the form. The form\n stays put: the user can paste a fresh token without a round trip.\n */}\n {queryToken && errors.token && (\n <Alert\n data-testid=\"reset-password-token-error\"\n icon={CircleAlertIcon}\n type=\"error\"\n >\n {errors.token.message}\n </Alert>\n )}\n\n <div className=\"flex flex-col gap-tinyrack-sm\">\n <AuthField\n autoComplete=\"new-password\"\n error={errors.password}\n icon={LockIcon}\n label={t('resetPassword.password.label')}\n placeholder={t('resetPassword.password.placeholder')}\n {...register('password')}\n type=\"password\"\n />\n\n <PasswordStrength password={passwordValue} policy={passwordPolicy} />\n </div>\n\n <AuthField\n autoComplete=\"new-password\"\n error={errors.confirmPassword}\n icon={LockKeyholeIcon}\n label={t('resetPassword.confirmPassword.label')}\n placeholder={t('resetPassword.confirmPassword.placeholder')}\n {...register('confirmPassword')}\n type=\"password\"\n />\n\n <TRButton\n className=\"w-full\"\n intent=\"primary\"\n loading={resetPasswordMutation.isPending}\n loadingLabel={t('resetPassword.submitting')}\n type=\"submit\"\n uiSize=\"lg\"\n >\n {t('resetPassword.submit')}\n </TRButton>\n </form>\n\n <AuthFooter>\n <AuthFooterLink\n link={\n <Link data-testid=\"reset-password-back-to-login\" to=\"/login\">\n {t('resetPassword.backToLogin')}\n </Link>\n }\n />\n </AuthFooter>\n </AuthLayout>\n );\n}\n"],"mappings":"wCA6DE,IAAAK,EAhCOL"}
@@ -0,0 +1,2 @@
1
+ import{J as e,Z as t,i as n,q as r,s as i}from"./createLucideIcon-BPpAIXR_.js";import{t as a}from"./config-v3-oy_ef.js";import{t as o}from"./useMutation-ZLFeJg75.js";import{tt as s}from"./shell-header-bar-C9AB5lT-.js";import{t as c}from"./circle-check-6QTLhwM_.js";import{t as l}from"./auth-field-HwxENJnB.js";import{t as u}from"./lock-keyhole-Cz72xkCn.js";import{t as d}from"./lock-7NRyvXBS.js";import{c as f,o as p}from"./zod-C87S6Tec.js";import{L as m,R as h,V as g}from"./index-gliHKncz.js";import{t as _}from"./auth-page-header-CIs08f5Y.js";import{t as v}from"./alert-leqyvHOp.js";import{t as y}from"./auth-layout-CoeqNhTT.js";import{r as b,t as x}from"./standard-schema-e8gtSzGE.js";import{r as S}from"./password-reset-CIacHnvA.js";import{t as C}from"./password-strength-C0Zkrrct.js";var w=t(e()),T=r();function E(){let{t:e}=n(),t=g(),[r,E]=(0,w.useState)(!1),{data:D}=s(a),O=D.auth.password.policy,k=(0,w.useMemo)(()=>p({password:f().min(O.min_length,e(`validation.password.min`,{count:O.min_length})).max(O.max_length,e(`validation.password.max`,{count:O.max_length})),confirmPassword:f().min(1,e(`validation.confirmPassword.required`))}).refine(e=>e.password===e.confirmPassword,{message:e(`validation.confirmPassword.mismatch`),path:[`confirmPassword`]}),[O.max_length,O.min_length,e]),A=o({...S,onSuccess:()=>E(!0)}),{register:j,handleSubmit:M,watch:N,formState:{errors:P}}=b({defaultValues:{password:``,confirmPassword:``},resolver:x(k)});return r?(0,T.jsx)(y,{children:(0,T.jsx)(m,{description:e(`resetRequiredPassword.success.description`),icon:c,title:e(`resetRequiredPassword.success.title`),tone:`success`,children:(0,T.jsx)(i,{className:`w-full`,intent:`primary`,onClick:()=>t({to:`/login`}),type:`button`,uiSize:`lg`,children:e(`resetPassword.success.goToLogin`)})})}):(0,T.jsxs)(y,{children:[(0,T.jsx)(_,{subtitle:e(`resetRequiredPassword.subtitle`),title:e(`resetRequiredPassword.title`)}),(0,T.jsxs)(`form`,{className:`flex flex-col gap-tinyrack-lg`,onSubmit:M(e=>A.mutate({password:e.password})),children:[A.isError&&(0,T.jsx)(v,{icon:h,type:`error`,children:e(`resetRequiredPassword.error.expired`)}),(0,T.jsxs)(`div`,{className:`flex flex-col gap-tinyrack-sm`,children:[(0,T.jsx)(l,{autoComplete:`new-password`,error:P.password,icon:d,label:e(`resetPassword.password.label`),...j(`password`),type:`password`}),(0,T.jsx)(C,{password:N(`password`),policy:O})]}),(0,T.jsx)(l,{autoComplete:`new-password`,error:P.confirmPassword,icon:u,label:e(`resetPassword.confirmPassword.label`),...j(`confirmPassword`),type:`password`}),(0,T.jsx)(i,{className:`w-full`,intent:`primary`,loading:A.isPending,loadingLabel:e(`resetPassword.submitting`),type:`submit`,uiSize:`lg`,children:e(`resetPassword.submit`)}),A.isError&&(0,T.jsx)(i,{className:`w-full`,intent:`neutral`,onClick:()=>t({to:`/login`}),type:`button`,uiSize:`lg`,children:e(`resetPassword.success.goToLogin`)})]})]})}export{E as component};
2
+ //# sourceMappingURL=reset-required-Cv5edDS6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reset-required-Cv5edDS6.js","names":["standardSchemaResolver","useMutation","useSuspenseQuery","useNavigate","TRButton","CircleAlertIcon","CircleCheckIcon","LockIcon","LockKeyholeIcon","useMemo","useState","useForm","useTranslation","z","AuthField","AuthOutcome","AuthPageHeader","PasswordStrength","Alert","AuthLayout","appConfigQueryOptions","resetRequiredPasswordMutationOptions","FormValues","password","confirmPassword","ResetRequiredPassword","t","navigate","success","setSuccess","data","config","policy","auth","schema","object","string","min","min_length","count","max","max_length","refine","value","message","path","mutation","onSuccess","register","handleSubmit","watch","formState","errors","defaultValues","resolver","to","values","mutate","isError","isPending","component"],"sources":["../../../frontend/src/routes/password/reset-required/index.tsx?tsr-split=component"],"sourcesContent":["import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';\nimport { useMutation, useSuspenseQuery } from '@tanstack/react-query';\nimport { createFileRoute, redirect, useNavigate } from '@tanstack/react-router';\nimport { TRButton } from '@tinyrack/ui/components/button';\nimport {\n CircleAlertIcon,\n CircleCheckIcon,\n LockIcon,\n LockKeyholeIcon,\n} from 'lucide-react';\nimport { useMemo, useState } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport { AuthField } from '#frontend/components/auth/auth-field.tsx';\nimport { AuthOutcome } from '#frontend/components/auth/auth-outcome.tsx';\nimport { AuthPageHeader } from '#frontend/components/auth/auth-page-header.tsx';\nimport { PasswordStrength } from '#frontend/components/auth/password-strength.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 { appConfigQueryOptions } from '#frontend/queries/config.ts';\nimport { resetRequiredPasswordMutationOptions } from '#frontend/queries/password-reset.ts';\n\nexport const Route = createFileRoute('/password/reset-required/')({\n component: ResetRequiredPassword,\n errorComponent: RouteErrorFallback,\n beforeLoad: async ({ context }) => {\n const config = await context.queryClient.ensureQueryData(\n appConfigQueryOptions,\n );\n if (!config.auth.password.enabled || !config.auth.passkey.enabled) {\n throw redirect({ to: '/login' });\n }\n },\n});\n\ntype FormValues = {\n password: string;\n confirmPassword: string;\n};\n\nfunction ResetRequiredPassword() {\n const { t } = useTranslation();\n const navigate = useNavigate();\n const [success, setSuccess] = useState(false);\n const { data: config } = useSuspenseQuery(appConfigQueryOptions);\n const policy = config.auth.password.policy;\n const schema = useMemo(\n () =>\n z\n .object({\n password: z\n .string()\n .min(\n policy.min_length,\n t('validation.password.min', { count: policy.min_length }),\n )\n .max(\n policy.max_length,\n t('validation.password.max', { count: policy.max_length }),\n ),\n confirmPassword: z\n .string()\n .min(1, t('validation.confirmPassword.required')),\n })\n .refine((value) => value.password === value.confirmPassword, {\n message: t('validation.confirmPassword.mismatch'),\n path: ['confirmPassword'],\n }),\n [policy.max_length, policy.min_length, t],\n );\n const mutation = useMutation({\n ...resetRequiredPasswordMutationOptions,\n onSuccess: () => setSuccess(true),\n });\n const {\n register,\n handleSubmit,\n watch,\n formState: { errors },\n } = useForm<FormValues>({\n defaultValues: { password: '', confirmPassword: '' },\n resolver: standardSchemaResolver(schema),\n });\n\n if (success) {\n return (\n <AuthLayout>\n <AuthOutcome\n description={t('resetRequiredPassword.success.description')}\n icon={CircleCheckIcon}\n title={t('resetRequiredPassword.success.title')}\n tone=\"success\"\n >\n <TRButton\n className=\"w-full\"\n intent=\"primary\"\n onClick={() => navigate({ to: '/login' })}\n type=\"button\"\n uiSize=\"lg\"\n >\n {t('resetPassword.success.goToLogin')}\n </TRButton>\n </AuthOutcome>\n </AuthLayout>\n );\n }\n\n return (\n <AuthLayout>\n <AuthPageHeader\n subtitle={t('resetRequiredPassword.subtitle')}\n title={t('resetRequiredPassword.title')}\n />\n <form\n className=\"flex flex-col gap-tinyrack-lg\"\n onSubmit={handleSubmit((values) =>\n mutation.mutate({ password: values.password }),\n )}\n >\n {mutation.isError && (\n <Alert icon={CircleAlertIcon} type=\"error\">\n {t('resetRequiredPassword.error.expired')}\n </Alert>\n )}\n <div className=\"flex flex-col gap-tinyrack-sm\">\n <AuthField\n autoComplete=\"new-password\"\n error={errors.password}\n icon={LockIcon}\n label={t('resetPassword.password.label')}\n {...register('password')}\n type=\"password\"\n />\n <PasswordStrength password={watch('password')} policy={policy} />\n </div>\n <AuthField\n autoComplete=\"new-password\"\n error={errors.confirmPassword}\n icon={LockKeyholeIcon}\n label={t('resetPassword.confirmPassword.label')}\n {...register('confirmPassword')}\n type=\"password\"\n />\n <TRButton\n className=\"w-full\"\n intent=\"primary\"\n loading={mutation.isPending}\n loadingLabel={t('resetPassword.submitting')}\n type=\"submit\"\n uiSize=\"lg\"\n >\n {t('resetPassword.submit')}\n </TRButton>\n {mutation.isError && (\n <TRButton\n className=\"w-full\"\n intent=\"neutral\"\n onClick={() => navigate({ to: '/login' })}\n type=\"button\"\n uiSize=\"lg\"\n >\n {t('resetPassword.success.goToLogin')}\n </TRButton>\n )}\n </form>\n </AuthLayout>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import{I as e}from"./index-gliHKncz.js";var t=e;export{t as errorComponent};
2
+ //# sourceMappingURL=reset-required-UL_cNRnQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reset-required-UL_cNRnQ.js","names":["RouteErrorFallback","FormValues","password","confirmPassword","SplitErrorComponent","errorComponent"],"sources":["../../../frontend/src/routes/password/reset-required/index.tsx?tsr-split=errorComponent"],"sourcesContent":["import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';\nimport { useMutation, useSuspenseQuery } from '@tanstack/react-query';\nimport { createFileRoute, redirect, useNavigate } from '@tanstack/react-router';\nimport { TRButton } from '@tinyrack/ui/components/button';\nimport {\n CircleAlertIcon,\n CircleCheckIcon,\n LockIcon,\n LockKeyholeIcon,\n} from 'lucide-react';\nimport { useMemo, useState } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { useTranslation } from 'react-i18next';\nimport { z } from 'zod';\nimport { AuthField } from '#frontend/components/auth/auth-field.tsx';\nimport { AuthOutcome } from '#frontend/components/auth/auth-outcome.tsx';\nimport { AuthPageHeader } from '#frontend/components/auth/auth-page-header.tsx';\nimport { PasswordStrength } from '#frontend/components/auth/password-strength.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 { appConfigQueryOptions } from '#frontend/queries/config.ts';\nimport { resetRequiredPasswordMutationOptions } from '#frontend/queries/password-reset.ts';\n\nexport const Route = createFileRoute('/password/reset-required/')({\n component: ResetRequiredPassword,\n errorComponent: RouteErrorFallback,\n beforeLoad: async ({ context }) => {\n const config = await context.queryClient.ensureQueryData(\n appConfigQueryOptions,\n );\n if (!config.auth.password.enabled || !config.auth.passkey.enabled) {\n throw redirect({ to: '/login' });\n }\n },\n});\n\ntype FormValues = {\n password: string;\n confirmPassword: string;\n};\n\nfunction ResetRequiredPassword() {\n const { t } = useTranslation();\n const navigate = useNavigate();\n const [success, setSuccess] = useState(false);\n const { data: config } = useSuspenseQuery(appConfigQueryOptions);\n const policy = config.auth.password.policy;\n const schema = useMemo(\n () =>\n z\n .object({\n password: z\n .string()\n .min(\n policy.min_length,\n t('validation.password.min', { count: policy.min_length }),\n )\n .max(\n policy.max_length,\n t('validation.password.max', { count: policy.max_length }),\n ),\n confirmPassword: z\n .string()\n .min(1, t('validation.confirmPassword.required')),\n })\n .refine((value) => value.password === value.confirmPassword, {\n message: t('validation.confirmPassword.mismatch'),\n path: ['confirmPassword'],\n }),\n [policy.max_length, policy.min_length, t],\n );\n const mutation = useMutation({\n ...resetRequiredPasswordMutationOptions,\n onSuccess: () => setSuccess(true),\n });\n const {\n register,\n handleSubmit,\n watch,\n formState: { errors },\n } = useForm<FormValues>({\n defaultValues: { password: '', confirmPassword: '' },\n resolver: standardSchemaResolver(schema),\n });\n\n if (success) {\n return (\n <AuthLayout>\n <AuthOutcome\n description={t('resetRequiredPassword.success.description')}\n icon={CircleCheckIcon}\n title={t('resetRequiredPassword.success.title')}\n tone=\"success\"\n >\n <TRButton\n className=\"w-full\"\n intent=\"primary\"\n onClick={() => navigate({ to: '/login' })}\n type=\"button\"\n uiSize=\"lg\"\n >\n {t('resetPassword.success.goToLogin')}\n </TRButton>\n </AuthOutcome>\n </AuthLayout>\n );\n }\n\n return (\n <AuthLayout>\n <AuthPageHeader\n subtitle={t('resetRequiredPassword.subtitle')}\n title={t('resetRequiredPassword.title')}\n />\n <form\n className=\"flex flex-col gap-tinyrack-lg\"\n onSubmit={handleSubmit((values) =>\n mutation.mutate({ password: values.password }),\n )}\n >\n {mutation.isError && (\n <Alert icon={CircleAlertIcon} type=\"error\">\n {t('resetRequiredPassword.error.expired')}\n </Alert>\n )}\n <div className=\"flex flex-col gap-tinyrack-sm\">\n <AuthField\n autoComplete=\"new-password\"\n error={errors.password}\n icon={LockIcon}\n label={t('resetPassword.password.label')}\n {...register('password')}\n type=\"password\"\n />\n <PasswordStrength password={watch('password')} policy={policy} />\n </div>\n <AuthField\n autoComplete=\"new-password\"\n error={errors.confirmPassword}\n icon={LockKeyholeIcon}\n label={t('resetPassword.confirmPassword.label')}\n {...register('confirmPassword')}\n type=\"password\"\n />\n <TRButton\n className=\"w-full\"\n intent=\"primary\"\n loading={mutation.isPending}\n loadingLabel={t('resetPassword.submitting')}\n type=\"submit\"\n uiSize=\"lg\"\n >\n {t('resetPassword.submit')}\n </TRButton>\n {mutation.isError && (\n <TRButton\n className=\"w-full\"\n intent=\"neutral\"\n onClick={() => navigate({ to: '/login' })}\n type=\"button\"\n uiSize=\"lg\"\n >\n {t('resetPassword.success.goToLogin')}\n </TRButton>\n )}\n </form>\n </AuthLayout>\n );\n}\n"],"mappings":"wCAwCE,IAAAI,EArBOJ"}
@@ -1,2 +1,2 @@
1
- import{i as e,q as t,s as n,t as r}from"./createLucideIcon-BPpAIXR_.js";import{Sr as i,n as a}from"./config-DTQCUlfZ.js";import{t as o}from"./useMutation-BWZ0G4Td.js";import{M as s,tt as c}from"./shell-header-bar-CjG5qvPg.js";import{t as l}from"./plus-DSBBSrgj.js";import{t as u}from"./trash-2-DZdKBLvC.js";import{M as d,N as f,a as p,i as m,n as h,r as g,z as _}from"./index-CS23Ozt6.js";import{t as v}from"./badge-CsOfUji0.js";import{t as y}from"./auth-choice-row-BCGjuedo.js";import{t as b}from"./auth-page-header-CIn2E4zI.js";import{t as x}from"./alert-leqyvHOp.js";import{t as S}from"./initial-avatar-CtuDUz_W.js";import{t as C}from"./auth-layout-VtpryDof.js";var w=r(`arrow-right`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`m12 5 7 7-7 7`,key:`xquz4c`}]]),T=r(`circle-user-round`,[[`path`,{d:`M17.925 20.056a6 6 0 0 0-11.851.001`,key:`z69sun`}],[`circle`,{cx:`12`,cy:`11`,r:`4`,key:`1gt34v`}],[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}]]),E=t();function D(e){let t=e?.split(` `).filter(Boolean)??[];return t.includes(`login`)||t.push(`login`),t.join(` `)}function O(e){let t=f(e),n=new URLSearchParams;for(let[e,r]of Object.entries({...t,prompt:D(t.prompt),account_selected:1}))r!==void 0&&n.set(e,String(r));return`/login?${n.toString()}`}function k(){let{t}=e(),r=h.useSearch(),f=i(),{data:D}=c(g(r.client_id)),k=D.accounts.length===0&&!D.allow_add_account,A=()=>{window.location.href=d({...r,account_selected:1})},j=o({...p,onSuccess:A}),M=o({...m,onSuccess:()=>{f.invalidateQueries({queryKey:a.accounts(r.client_id)})}});return(0,E.jsxs)(C,{width:`wide`,children:[(0,E.jsx)(b,{subtitle:t(`accountSelect.subtitle`),title:t(`accountSelect.title`)}),D.accounts.length===0?(0,E.jsx)(x,{icon:T,type:`info`,children:t(k?`accountSelect.unavailable`:`accountSelect.noRememberedAccounts`)}):(0,E.jsx)(`div`,{className:`flex flex-col gap-tinyrack-xs`,"data-testid":`account-list`,children:D.accounts.map(e=>(0,E.jsx)(`div`,{"data-testid":`remembered-account`,children:(0,E.jsx)(y,{description:e.current?t(`accountSelect.currentAccount`):t(`accountSelect.rememberedAccount`),label:e.email,leading:(0,E.jsx)(S,{email:e.email,size:`sm`}),render:(0,E.jsx)(n,{"data-testid":`select-account-${e.sub}`,disabled:j.isPending,onClick:()=>j.mutate({sub:e.sub}),type:`button`}),trailing:(0,E.jsxs)(E.Fragment,{children:[e.current?(0,E.jsx)(v,{uiSize:`md`,variant:`info`,children:t(`accountSelect.current`)}):null,D.allow_remove_account&&!e.current?(0,E.jsx)(s,{appearance:`ghost`,"aria-label":t(`accountSelect.removeAccount`,{email:e.email}),"data-testid":`remove-account-${e.sub}`,disabled:M.isPending,intent:`neutral`,onClick:()=>M.mutate({sub:e.sub}),type:`button`,uiSize:`sm`,children:(0,E.jsx)(u,{"aria-hidden":!0,className:`size-tinyrack-lg`})}):null]})})},e.sub))}),D.allow_add_account?(0,E.jsxs)(_,{appearance:`outline`,className:`w-full justify-between`,intent:`neutral`,render:(0,E.jsx)(`a`,{href:O(r)}),children:[(0,E.jsxs)(`span`,{className:`inline-flex items-center gap-tinyrack-sm`,children:[(0,E.jsx)(l,{"aria-hidden":!0,className:`size-tinyrack-lg`}),t(`accountSelect.useAnotherAccount`)]}),(0,E.jsx)(w,{"aria-hidden":!0,className:`size-tinyrack-lg`})]}):null]})}export{k as component};
2
- //# sourceMappingURL=select-DqZynXSt.js.map
1
+ import{i as e,q as t,s as n,t as r}from"./createLucideIcon-BPpAIXR_.js";import{n as i,sr as a}from"./config-v3-oy_ef.js";import{t as o}from"./useMutation-ZLFeJg75.js";import{M as s,tt as c}from"./shell-header-bar-C9AB5lT-.js";import{t as l}from"./plus-DSBBSrgj.js";import{t as u}from"./trash-2-DZdKBLvC.js";import{M as d,N as f,a as p,i as m,n as h,r as g,z as _}from"./index-gliHKncz.js";import{t as v}from"./badge-CsOfUji0.js";import{t as y}from"./auth-choice-row-BlSsUI7d.js";import{t as b}from"./auth-page-header-CIs08f5Y.js";import{t as x}from"./alert-leqyvHOp.js";import{t as S}from"./initial-avatar-CZzHAPm3.js";import{t as C}from"./auth-layout-CoeqNhTT.js";var w=r(`arrow-right`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`m12 5 7 7-7 7`,key:`xquz4c`}]]),T=r(`circle-user-round`,[[`path`,{d:`M17.925 20.056a6 6 0 0 0-11.851.001`,key:`z69sun`}],[`circle`,{cx:`12`,cy:`11`,r:`4`,key:`1gt34v`}],[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}]]),E=t();function D(e){let t=e?.split(` `).filter(Boolean)??[];return t.includes(`login`)||t.push(`login`),t.join(` `)}function O(e){let t=f(e),n=new URLSearchParams;for(let[e,r]of Object.entries({...t,prompt:D(t.prompt),account_selected:1}))r!==void 0&&n.set(e,String(r));return`/login?${n.toString()}`}function k(){let{t}=e(),r=h.useSearch(),f=a(),{data:D}=c(g(r.client_id)),k=D.accounts.length===0&&!D.allow_add_account,A=()=>{window.location.href=d({...r,account_selected:1})},j=o({...p,onSuccess:A}),M=o({...m,onSuccess:()=>{f.invalidateQueries({queryKey:i.accounts(r.client_id)})}});return(0,E.jsxs)(C,{width:`wide`,children:[(0,E.jsx)(b,{subtitle:t(`accountSelect.subtitle`),title:t(`accountSelect.title`)}),D.accounts.length===0?(0,E.jsx)(x,{icon:T,type:`info`,children:t(k?`accountSelect.unavailable`:`accountSelect.noRememberedAccounts`)}):(0,E.jsx)(`div`,{className:`flex flex-col gap-tinyrack-xs`,"data-testid":`account-list`,children:D.accounts.map(e=>(0,E.jsx)(`div`,{"data-testid":`remembered-account`,children:(0,E.jsx)(y,{description:e.current?t(`accountSelect.currentAccount`):t(`accountSelect.rememberedAccount`),label:e.email,leading:(0,E.jsx)(S,{email:e.email,size:`sm`}),render:(0,E.jsx)(n,{"data-testid":`select-account-${e.sub}`,disabled:j.isPending,onClick:()=>j.mutate({sub:e.sub}),type:`button`}),trailing:(0,E.jsxs)(E.Fragment,{children:[e.current?(0,E.jsx)(v,{uiSize:`md`,variant:`info`,children:t(`accountSelect.current`)}):null,D.allow_remove_account&&!e.current?(0,E.jsx)(s,{appearance:`ghost`,"aria-label":t(`accountSelect.removeAccount`,{email:e.email}),"data-testid":`remove-account-${e.sub}`,disabled:M.isPending,intent:`neutral`,onClick:()=>M.mutate({sub:e.sub}),type:`button`,uiSize:`sm`,children:(0,E.jsx)(u,{"aria-hidden":!0,className:`size-tinyrack-lg`})}):null]})})},e.sub))}),D.allow_add_account?(0,E.jsxs)(_,{appearance:`outline`,className:`w-full justify-between`,intent:`neutral`,render:(0,E.jsx)(`a`,{href:O(r)}),children:[(0,E.jsxs)(`span`,{className:`inline-flex items-center gap-tinyrack-sm`,children:[(0,E.jsx)(l,{"aria-hidden":!0,className:`size-tinyrack-lg`}),t(`accountSelect.useAnotherAccount`)]}),(0,E.jsx)(w,{"aria-hidden":!0,className:`size-tinyrack-lg`})]}):null]})}export{k as component};
2
+ //# sourceMappingURL=select-CWdSzsVo.js.map