create-mithaq 0.0.0-stage → 0.2.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 (396) hide show
  1. package/LICENSE +22 -0
  2. package/LICENSES/openstatus-time-picker.txt +21 -0
  3. package/LICENSES/payload.txt +22 -0
  4. package/LICENSES/ponytail.txt +21 -0
  5. package/index.js +521 -0
  6. package/package.json +19 -4
  7. package/template/apps/asas-backoffice/components.json +25 -0
  8. package/template/apps/asas-backoffice/gitignore +3 -0
  9. package/template/apps/asas-backoffice/package.json +57 -0
  10. package/template/apps/asas-backoffice/public/favicon.svg +1 -0
  11. package/template/apps/asas-backoffice/src/api.ts +147 -0
  12. package/template/apps/asas-backoffice/src/auth-frame.tsx +150 -0
  13. package/template/apps/asas-backoffice/src/components/ui/accordion.tsx +74 -0
  14. package/template/apps/asas-backoffice/src/components/ui/alert-dialog.tsx +185 -0
  15. package/template/apps/asas-backoffice/src/components/ui/avatar.tsx +106 -0
  16. package/template/apps/asas-backoffice/src/components/ui/badge.tsx +48 -0
  17. package/template/apps/asas-backoffice/src/components/ui/breadcrumb.tsx +124 -0
  18. package/template/apps/asas-backoffice/src/components/ui/button-group.tsx +87 -0
  19. package/template/apps/asas-backoffice/src/components/ui/button.tsx +55 -0
  20. package/template/apps/asas-backoffice/src/components/ui/calendar.tsx +223 -0
  21. package/template/apps/asas-backoffice/src/components/ui/card.tsx +102 -0
  22. package/template/apps/asas-backoffice/src/components/ui/chart.tsx +371 -0
  23. package/template/apps/asas-backoffice/src/components/ui/checkbox.tsx +28 -0
  24. package/template/apps/asas-backoffice/src/components/ui/collapsible.tsx +19 -0
  25. package/template/apps/asas-backoffice/src/components/ui/combobox.tsx +308 -0
  26. package/template/apps/asas-backoffice/src/components/ui/dialog.tsx +158 -0
  27. package/template/apps/asas-backoffice/src/components/ui/direction.tsx +6 -0
  28. package/template/apps/asas-backoffice/src/components/ui/directional-icon.tsx +20 -0
  29. package/template/apps/asas-backoffice/src/components/ui/dropdown-menu.tsx +267 -0
  30. package/template/apps/asas-backoffice/src/components/ui/field.tsx +236 -0
  31. package/template/apps/asas-backoffice/src/components/ui/input-group.tsx +155 -0
  32. package/template/apps/asas-backoffice/src/components/ui/input-otp.tsx +84 -0
  33. package/template/apps/asas-backoffice/src/components/ui/input.tsx +19 -0
  34. package/template/apps/asas-backoffice/src/components/ui/label.tsx +19 -0
  35. package/template/apps/asas-backoffice/src/components/ui/popover.tsx +90 -0
  36. package/template/apps/asas-backoffice/src/components/ui/radio-group.tsx +37 -0
  37. package/template/apps/asas-backoffice/src/components/ui/resizable.tsx +47 -0
  38. package/template/apps/asas-backoffice/src/components/ui/scroll-area.tsx +54 -0
  39. package/template/apps/asas-backoffice/src/components/ui/select.tsx +204 -0
  40. package/template/apps/asas-backoffice/src/components/ui/separator.tsx +24 -0
  41. package/template/apps/asas-backoffice/src/components/ui/sheet.tsx +139 -0
  42. package/template/apps/asas-backoffice/src/components/ui/sidebar.tsx +714 -0
  43. package/template/apps/asas-backoffice/src/components/ui/skeleton.tsx +13 -0
  44. package/template/apps/asas-backoffice/src/components/ui/slider.tsx +55 -0
  45. package/template/apps/asas-backoffice/src/components/ui/spinner.tsx +10 -0
  46. package/template/apps/asas-backoffice/src/components/ui/switch.tsx +29 -0
  47. package/template/apps/asas-backoffice/src/components/ui/table.tsx +115 -0
  48. package/template/apps/asas-backoffice/src/components/ui/tabs.tsx +79 -0
  49. package/template/apps/asas-backoffice/src/components/ui/textarea.tsx +17 -0
  50. package/template/apps/asas-backoffice/src/components/ui/toggle-group.tsx +89 -0
  51. package/template/apps/asas-backoffice/src/components/ui/toggle.tsx +44 -0
  52. package/template/apps/asas-backoffice/src/components/ui/tooltip.tsx +63 -0
  53. package/template/apps/asas-backoffice/src/components.ts +35 -0
  54. package/template/apps/asas-backoffice/src/copy.ts +1 -0
  55. package/template/apps/asas-backoffice/src/first-user.ts +14 -0
  56. package/template/apps/asas-backoffice/src/hooks/use-mobile.ts +19 -0
  57. package/template/apps/asas-backoffice/src/lib/utils.ts +1 -0
  58. package/template/apps/asas-backoffice/src/links.ts +38 -0
  59. package/template/apps/asas-backoffice/src/locale-switch.tsx +28 -0
  60. package/template/apps/asas-backoffice/src/no-organization.tsx +5 -0
  61. package/template/apps/asas-backoffice/src/redirect.ts +1 -0
  62. package/template/apps/asas-backoffice/src/request-headers.ts +11 -0
  63. package/template/apps/asas-backoffice/src/routeTree.gen.ts +206 -0
  64. package/template/apps/asas-backoffice/src/router.tsx +18 -0
  65. package/template/apps/asas-backoffice/src/routes/(screens)/backoffice/files.ts +10 -0
  66. package/template/apps/asas-backoffice/src/routes/(screens)/backoffice/screen.json +3971 -0
  67. package/template/apps/asas-backoffice/src/routes/(screens)/backoffice/types.ts +163 -0
  68. package/template/apps/asas-backoffice/src/routes/(screens)/backoffice.$.tsx +17 -0
  69. package/template/apps/asas-backoffice/src/routes/(screens)/backoffice.index.tsx +10 -0
  70. package/template/apps/asas-backoffice/src/routes/(screens)/backoffice.tsx +13 -0
  71. package/template/apps/asas-backoffice/src/routes/__root.tsx +95 -0
  72. package/template/apps/asas-backoffice/src/routes/api/$.ts +34 -0
  73. package/template/apps/asas-backoffice/src/routes/auth.create-first-user.tsx +113 -0
  74. package/template/apps/asas-backoffice/src/routes/auth.sign-in.tsx +77 -0
  75. package/template/apps/asas-backoffice/src/routes/unauthorized.tsx +134 -0
  76. package/template/apps/asas-backoffice/src/screen-form.tsx +36 -0
  77. package/template/apps/asas-backoffice/src/screen-list.tsx +140 -0
  78. package/template/apps/asas-backoffice/src/screen-names.ts +10 -0
  79. package/template/apps/asas-backoffice/src/screens.tsx +888 -0
  80. package/template/apps/asas-backoffice/src/server.ts +30 -0
  81. package/template/apps/asas-backoffice/src/slide.ts +22 -0
  82. package/template/apps/asas-backoffice/src/start.ts +42 -0
  83. package/template/apps/asas-backoffice/src/styles.css +202 -0
  84. package/template/apps/asas-backoffice/src/theme.ts +2 -0
  85. package/template/apps/asas-backoffice/tsconfig.json +19 -0
  86. package/template/apps/asas-backoffice/vite.config.ts +32 -0
  87. package/template/apps/asas-tanstack/components.json +25 -0
  88. package/template/apps/asas-tanstack/gitignore +3 -0
  89. package/template/apps/asas-tanstack/package.json +58 -0
  90. package/template/apps/asas-tanstack/public/favicon.svg +1 -0
  91. package/template/apps/asas-tanstack/src/api.ts +147 -0
  92. package/template/apps/asas-tanstack/src/auth-frame.tsx +150 -0
  93. package/template/apps/asas-tanstack/src/choose-password.tsx +150 -0
  94. package/template/apps/asas-tanstack/src/components/ui/accordion.tsx +10 -0
  95. package/template/apps/asas-tanstack/src/components/ui/alert-dialog.tsx +173 -0
  96. package/template/apps/asas-tanstack/src/components/ui/avatar.tsx +104 -0
  97. package/template/apps/asas-tanstack/src/components/ui/badge.tsx +48 -0
  98. package/template/apps/asas-tanstack/src/components/ui/breadcrumb.tsx +125 -0
  99. package/template/apps/asas-tanstack/src/components/ui/button-group.tsx +87 -0
  100. package/template/apps/asas-tanstack/src/components/ui/button.tsx +56 -0
  101. package/template/apps/asas-tanstack/src/components/ui/calendar.tsx +223 -0
  102. package/template/apps/asas-tanstack/src/components/ui/card.tsx +94 -0
  103. package/template/apps/asas-tanstack/src/components/ui/chart.tsx +369 -0
  104. package/template/apps/asas-tanstack/src/components/ui/checkbox.tsx +27 -0
  105. package/template/apps/asas-tanstack/src/components/ui/collapsible.tsx +19 -0
  106. package/template/apps/asas-tanstack/src/components/ui/combobox.tsx +300 -0
  107. package/template/apps/asas-tanstack/src/components/ui/dialog.tsx +149 -0
  108. package/template/apps/asas-tanstack/src/components/ui/direction.tsx +5 -0
  109. package/template/apps/asas-tanstack/src/components/ui/directional-icon.tsx +20 -0
  110. package/template/apps/asas-tanstack/src/components/ui/dropdown-menu.tsx +267 -0
  111. package/template/apps/asas-tanstack/src/components/ui/field.tsx +225 -0
  112. package/template/apps/asas-tanstack/src/components/ui/input-group.tsx +147 -0
  113. package/template/apps/asas-tanstack/src/components/ui/input-otp.tsx +20 -0
  114. package/template/apps/asas-tanstack/src/components/ui/input.tsx +20 -0
  115. package/template/apps/asas-tanstack/src/components/ui/label.tsx +18 -0
  116. package/template/apps/asas-tanstack/src/components/ui/popover.tsx +50 -0
  117. package/template/apps/asas-tanstack/src/components/ui/radio-group.tsx +10 -0
  118. package/template/apps/asas-tanstack/src/components/ui/resizable.tsx +47 -0
  119. package/template/apps/asas-tanstack/src/components/ui/scroll-area.tsx +52 -0
  120. package/template/apps/asas-tanstack/src/components/ui/select.tsx +190 -0
  121. package/template/apps/asas-tanstack/src/components/ui/separator.tsx +23 -0
  122. package/template/apps/asas-tanstack/src/components/ui/sheet.tsx +127 -0
  123. package/template/apps/asas-tanstack/src/components/ui/sidebar.tsx +716 -0
  124. package/template/apps/asas-tanstack/src/components/ui/skeleton.tsx +13 -0
  125. package/template/apps/asas-tanstack/src/components/ui/slider.tsx +13 -0
  126. package/template/apps/asas-tanstack/src/components/ui/spinner.tsx +11 -0
  127. package/template/apps/asas-tanstack/src/components/ui/switch.tsx +8 -0
  128. package/template/apps/asas-tanstack/src/components/ui/table.tsx +119 -0
  129. package/template/apps/asas-tanstack/src/components/ui/tabs.tsx +80 -0
  130. package/template/apps/asas-tanstack/src/components/ui/textarea.tsx +18 -0
  131. package/template/apps/asas-tanstack/src/components/ui/toggle-group.tsx +8 -0
  132. package/template/apps/asas-tanstack/src/components/ui/toggle.tsx +44 -0
  133. package/template/apps/asas-tanstack/src/components/ui/tooltip.tsx +68 -0
  134. package/template/apps/asas-tanstack/src/components.ts +35 -0
  135. package/template/apps/asas-tanstack/src/copy.ts +1207 -0
  136. package/template/apps/asas-tanstack/src/first-run.ts +16 -0
  137. package/template/apps/asas-tanstack/src/hooks/use-mobile.ts +19 -0
  138. package/template/apps/asas-tanstack/src/lib/utils.ts +6 -0
  139. package/template/apps/asas-tanstack/src/links.ts +38 -0
  140. package/template/apps/asas-tanstack/src/locale-switch.tsx +28 -0
  141. package/template/apps/asas-tanstack/src/no-organization.tsx +108 -0
  142. package/template/apps/asas-tanstack/src/quiet-abort.ts +25 -0
  143. package/template/apps/asas-tanstack/src/redirect.ts +39 -0
  144. package/template/apps/asas-tanstack/src/request-headers.ts +11 -0
  145. package/template/apps/asas-tanstack/src/routeTree.gen.ts +424 -0
  146. package/template/apps/asas-tanstack/src/router.tsx +18 -0
  147. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/files.ts +11 -0
  148. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/screen.json +4242 -0
  149. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/types.ts +160 -0
  150. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard.$.tsx +17 -0
  151. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard.index.tsx +10 -0
  152. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard.tsx +13 -0
  153. package/template/apps/asas-tanstack/src/routes/(screens)/portal/files.ts +9 -0
  154. package/template/apps/asas-tanstack/src/routes/(screens)/portal/screen.json +1564 -0
  155. package/template/apps/asas-tanstack/src/routes/(screens)/portal/types.ts +94 -0
  156. package/template/apps/asas-tanstack/src/routes/(screens)/portal.$.tsx +17 -0
  157. package/template/apps/asas-tanstack/src/routes/(screens)/portal.index.tsx +10 -0
  158. package/template/apps/asas-tanstack/src/routes/(screens)/portal.tsx +13 -0
  159. package/template/apps/asas-tanstack/src/routes/__root.tsx +99 -0
  160. package/template/apps/asas-tanstack/src/routes/api/$.ts +11 -0
  161. package/template/apps/asas-tanstack/src/routes/auth.accept-invitation.tsx +108 -0
  162. package/template/apps/asas-tanstack/src/routes/auth.first-run.tsx +114 -0
  163. package/template/apps/asas-tanstack/src/routes/auth.forgot-password.tsx +94 -0
  164. package/template/apps/asas-tanstack/src/routes/auth.handoff.ts +39 -0
  165. package/template/apps/asas-tanstack/src/routes/auth.reset-password.tsx +16 -0
  166. package/template/apps/asas-tanstack/src/routes/auth.set-password.tsx +16 -0
  167. package/template/apps/asas-tanstack/src/routes/auth.sign-in.tsx +98 -0
  168. package/template/apps/asas-tanstack/src/routes/auth.sign-up.tsx +143 -0
  169. package/template/apps/asas-tanstack/src/routes/index.tsx +146 -0
  170. package/template/apps/asas-tanstack/src/routes/unauthorized.tsx +134 -0
  171. package/template/apps/asas-tanstack/src/screen-form.tsx +36 -0
  172. package/template/apps/asas-tanstack/src/screen-list.tsx +140 -0
  173. package/template/apps/asas-tanstack/src/screen-names.ts +10 -0
  174. package/template/apps/asas-tanstack/src/screens.tsx +888 -0
  175. package/template/apps/asas-tanstack/src/server.ts +26 -0
  176. package/template/apps/asas-tanstack/src/slide.ts +22 -0
  177. package/template/apps/asas-tanstack/src/start.ts +57 -0
  178. package/template/apps/asas-tanstack/src/styles.css +163 -0
  179. package/template/apps/asas-tanstack/src/theme.ts +5 -0
  180. package/template/apps/asas-tanstack/tsconfig.json +19 -0
  181. package/template/apps/asas-tanstack/vite.config.ts +34 -0
  182. package/template/package.json +16 -0
  183. package/template/templates/asas/.claude/skills/add-field/SKILL.md +27 -0
  184. package/template/templates/asas/.claude/skills/add-kayan/SKILL.md +26 -0
  185. package/template/templates/asas/.claude/skills/eject-admin-piece/SKILL.md +28 -0
  186. package/template/templates/asas/.claude/skills/upgrade-mithaq/SKILL.md +35 -0
  187. package/template/templates/asas/.env.example +33 -0
  188. package/template/templates/asas/AGENTS.md +101 -0
  189. package/template/templates/asas/CLAUDE.md +1 -0
  190. package/template/templates/asas/README.md +1056 -0
  191. package/template/templates/asas/mithaq.hints.json +25 -0
  192. package/template/templates/asas/package.json +53 -0
  193. package/template/templates/asas/src/asas.ts +118 -0
  194. package/template/templates/asas/src/backoffice-http.ts +117 -0
  195. package/template/templates/asas/src/dev-setup.ts +112 -0
  196. package/template/templates/asas/src/local-database.ts +22 -0
  197. package/template/templates/asas/src/messages/account.json +132 -0
  198. package/template/templates/asas/src/messages/auditLog.json +304 -0
  199. package/template/templates/asas/src/messages/backofficeLog.json +132 -0
  200. package/template/templates/asas/src/messages/courses.json +312 -0
  201. package/template/templates/asas/src/messages/enrollments.json +60 -0
  202. package/template/templates/asas/src/messages/invitation.json +82 -0
  203. package/template/templates/asas/src/messages/localUsers.json +48 -0
  204. package/template/templates/asas/src/messages/media.json +76 -0
  205. package/template/templates/asas/src/messages/member.json +62 -0
  206. package/template/templates/asas/src/messages/memberRoles.json +48 -0
  207. package/template/templates/asas/src/messages/mithaq.json +214 -0
  208. package/template/templates/asas/src/messages/organization.json +72 -0
  209. package/template/templates/asas/src/messages/organizationRole.json +62 -0
  210. package/template/templates/asas/src/messages/roles.json +72 -0
  211. package/template/templates/asas/src/messages/session.json +102 -0
  212. package/template/templates/asas/src/messages/sessions.json +136 -0
  213. package/template/templates/asas/src/messages/settings.json +64 -0
  214. package/template/templates/asas/src/messages/tags.json +48 -0
  215. package/template/templates/asas/src/messages/user.json +112 -0
  216. package/template/templates/asas/src/messages/verification.json +62 -0
  217. package/template/templates/asas/src/migrations/20260920080701_parched_tombstone/migration.sql +177 -0
  218. package/template/templates/asas/src/migrations/20260920080701_parched_tombstone/snapshot.json +2169 -0
  219. package/template/templates/asas/src/migrations/20260920230505_zippy_ben_urich/migration.sql +1 -0
  220. package/template/templates/asas/src/migrations/20260920230505_zippy_ben_urich/snapshot.json +2182 -0
  221. package/template/templates/asas/src/migrations/20260920231219_slim_retro_girl/migration.sql +20 -0
  222. package/template/templates/asas/src/migrations/20260920231219_slim_retro_girl/snapshot.json +2409 -0
  223. package/template/templates/asas/src/migrations/20260922082945_military_ironclad/migration.sql +15 -0
  224. package/template/templates/asas/src/migrations/20260922082945_military_ironclad/snapshot.json +2627 -0
  225. package/template/templates/asas/src/migrations/20260922112630_bitter_chat/migration.sql +5 -0
  226. package/template/templates/asas/src/migrations/20260922112630_bitter_chat/snapshot.json +2691 -0
  227. package/template/templates/asas/src/migrations/20260923235915_foamy_rachel_grey/migration.sql +8 -0
  228. package/template/templates/asas/src/migrations/20260923235915_foamy_rachel_grey/snapshot.json +2795 -0
  229. package/template/templates/asas/src/migrations/20260926145709_clean_the_initiative/migration.sql +3 -0
  230. package/template/templates/asas/src/migrations/20260926145709_clean_the_initiative/snapshot.json +2834 -0
  231. package/template/templates/asas/src/migrations/20260927212525_bent_prowler/migration.sql +9 -0
  232. package/template/templates/asas/src/migrations/20260927212525_bent_prowler/snapshot.json +2870 -0
  233. package/template/templates/asas/src/migrations/20260927223441_tranquil_bromley/migration.sql +1 -0
  234. package/template/templates/asas/src/migrations/20260927223441_tranquil_bromley/snapshot.json +2881 -0
  235. package/template/templates/asas/src/migrations/20260928030047_outgoing_wraith/migration.sql +15 -0
  236. package/template/templates/asas/src/migrations/20260928030047_outgoing_wraith/snapshot.json +3073 -0
  237. package/template/templates/asas/src/migrations/20260929024407_grey_morlun/migration.sql +21 -0
  238. package/template/templates/asas/src/migrations/20260929024407_grey_morlun/snapshot.json +3098 -0
  239. package/template/templates/asas/src/migrations/20260929130530_bored_jackal/migration.sql +21 -0
  240. package/template/templates/asas/src/migrations/20260929130530_bored_jackal/snapshot.json +3124 -0
  241. package/template/templates/asas/src/migrations/20260930102924_material_newton_destine/migration.sql +32 -0
  242. package/template/templates/asas/src/migrations/20260930102924_material_newton_destine/snapshot.json +3324 -0
  243. package/template/templates/asas/src/migrations/20261003022353_clumsy_taskmaster/migration.sql +2 -0
  244. package/template/templates/asas/src/migrations/20261003022353_clumsy_taskmaster/snapshot.json +3350 -0
  245. package/template/templates/asas/src/migrations/20261003143249_premium_red_shift/migration.sql +21 -0
  246. package/template/templates/asas/src/migrations/20261003143249_premium_red_shift/snapshot.json +3635 -0
  247. package/template/templates/asas/src/migrations/20261008215349_square_norrin_radd/migration.sql +8 -0
  248. package/template/templates/asas/src/migrations/20261008215349_square_norrin_radd/snapshot.json +3635 -0
  249. package/template/templates/asas/src/migrations/20261008230204_many_scarecrow/migration.sql +18 -0
  250. package/template/templates/asas/src/migrations/20261008230204_many_scarecrow/snapshot.json +3635 -0
  251. package/template/templates/asas/src/migrations/20261010005521_elite_giant_girl/migration.sql +18 -0
  252. package/template/templates/asas/src/migrations/20261010005521_elite_giant_girl/snapshot.json +3635 -0
  253. package/template/templates/asas/src/migrations/20261010120822_needy_thunderbolt/migration.sql +11 -0
  254. package/template/templates/asas/src/migrations/20261010120822_needy_thunderbolt/snapshot.json +3667 -0
  255. package/template/templates/asas/src/migrations/20261012000752_organization_ownership/migration.sql +48 -0
  256. package/template/templates/asas/src/migrations/20261012000752_organization_ownership/snapshot.json +3891 -0
  257. package/template/templates/asas/src/migrations/20261014221355_tags_restrict/migration.sql +1 -0
  258. package/template/templates/asas/src/migrations/20261014221355_tags_restrict/snapshot.json +3881 -0
  259. package/template/templates/asas/src/migrations/20261014222218_one_seat_per_session/migration.sql +1 -0
  260. package/template/templates/asas/src/migrations/20261014222218_one_seat_per_session/snapshot.json +3894 -0
  261. package/template/templates/asas/src/migrations/20261014223522_session_archive_reason/migration.sql +1 -0
  262. package/template/templates/asas/src/migrations/20261014223522_session_archive_reason/snapshot.json +3907 -0
  263. package/template/templates/asas/src/migrations/20261014225728_course_archive_by_operation/migration.sql +1 -0
  264. package/template/templates/asas/src/migrations/20261014225728_course_archive_by_operation/snapshot.json +3907 -0
  265. package/template/templates/asas/src/migrations/20261015042444_organization_names/migration.sql +1 -0
  266. package/template/templates/asas/src/migrations/20261015042444_organization_names/snapshot.json +3920 -0
  267. package/template/templates/asas/src/migrations/20261015053212_member_last_seen/migration.sql +1 -0
  268. package/template/templates/asas/src/migrations/20261015053212_member_last_seen/snapshot.json +3933 -0
  269. package/template/templates/asas/src/migrations/20261015093203_activity_and_staff_history/migration.sql +15 -0
  270. package/template/templates/asas/src/migrations/20261015093203_activity_and_staff_history/snapshot.json +4113 -0
  271. package/template/templates/asas/src/migrations/20261015095752_portal/migration.sql +2 -0
  272. package/template/templates/asas/src/migrations/20261015095752_portal/snapshot.json +4113 -0
  273. package/template/templates/asas/src/migrations/20261015135529_better_auth_indexes/migration.sql +9 -0
  274. package/template/templates/asas/src/migrations/20261015135529_better_auth_indexes/snapshot.json +4302 -0
  275. package/template/templates/asas/src/migrations/20261015162438_audit_changes_json/migration.sql +27 -0
  276. package/template/templates/asas/src/migrations/20261015162438_audit_changes_json/snapshot.json +4289 -0
  277. package/template/templates/asas/src/migrations/drizzle.config.ts +11 -0
  278. package/template/templates/asas/src/migrations/schema.ts +30 -0
  279. package/template/templates/asas/src/mithaq.ts +4 -0
  280. package/template/templates/asas/src/organizations.ts +3 -0
  281. package/template/templates/asas/src/seed.ts +427 -0
  282. package/template/templates/asas/src/ui-backoffice/home.tsx +63 -0
  283. package/template/templates/asas/src/ui-backoffice/organization-actions.tsx +121 -0
  284. package/template/templates/asas/src/ui-backoffice/record-page.tsx +30 -0
  285. package/template/templates/asas/src/ui-backoffice/role-cells.tsx +59 -0
  286. package/template/templates/asas/src/ui-backoffice/user-actions.tsx +249 -0
  287. package/template/templates/asas/src/ui-dashboard/dashboard.tsx +300 -0
  288. package/template/templates/asas/src/ui-dashboard/home.tsx +73 -0
  289. package/template/templates/asas/src/ui-dashboard/members-card.tsx +147 -0
  290. package/template/templates/asas/src/ui-dashboard/members.tsx +829 -0
  291. package/template/templates/asas/src/ui-dashboard/theme-provider.tsx +10 -0
  292. package/template/templates/asas/src/ui-dashboard/versions.tsx +446 -0
  293. package/template/templates/asas/src/ui-portal/courses-home.tsx +96 -0
  294. package/template/templates/asas/src/ui-portal/home.tsx +73 -0
  295. package/template/templates/asas/src/ui-portal/page.tsx +11 -0
  296. package/template/templates/features/audit-log/audit-log/audit-log.ts +284 -0
  297. package/template/templates/features/audit-log/backoffice-log/backoffice-log.ts +67 -0
  298. package/template/templates/features/audit-log/index.ts +4 -0
  299. package/template/templates/features/audit-log/package.json +13 -0
  300. package/template/templates/features/auth/index.ts +11 -0
  301. package/template/templates/features/auth/local-users/local-users.ts +143 -0
  302. package/template/templates/features/auth/package.json +13 -0
  303. package/template/templates/features/auth/user/account.access.ts +8 -0
  304. package/template/templates/features/auth/user/account.ts +31 -0
  305. package/template/templates/features/auth/user/index.ts +18 -0
  306. package/template/templates/features/auth/user/invitation.access.ts +4 -0
  307. package/template/templates/features/auth/user/invitation.ts +27 -0
  308. package/template/templates/features/auth/user/member.access.ts +10 -0
  309. package/template/templates/features/auth/user/member.screens.ts +37 -0
  310. package/template/templates/features/auth/user/member.ts +26 -0
  311. package/template/templates/features/auth/user/organization.access.ts +20 -0
  312. package/template/templates/features/auth/user/organization.screens.ts +37 -0
  313. package/template/templates/features/auth/user/organization.ts +25 -0
  314. package/template/templates/features/auth/user/organizationRole.access.ts +8 -0
  315. package/template/templates/features/auth/user/organizationRole.ts +25 -0
  316. package/template/templates/features/auth/user/session.access.ts +8 -0
  317. package/template/templates/features/auth/user/session.ts +28 -0
  318. package/template/templates/features/auth/user/user.access.ts +30 -0
  319. package/template/templates/features/auth/user/user.screens.ts +45 -0
  320. package/template/templates/features/auth/user/user.ts +31 -0
  321. package/template/templates/features/auth/user/verification.access.ts +8 -0
  322. package/template/templates/features/auth/user/verification.ts +23 -0
  323. package/template/templates/features/courses/courses/courses.ts +601 -0
  324. package/template/templates/features/courses/courses/cover-reads.ts +33 -0
  325. package/template/templates/features/courses/courses/ui/archived-badge.tsx +9 -0
  326. package/template/templates/features/courses/courses/ui/fee-cell.tsx +19 -0
  327. package/template/templates/features/courses/courses/ui/no-courses.tsx +13 -0
  328. package/template/templates/features/courses/courses/ui/status-cell.tsx +33 -0
  329. package/template/templates/features/courses/courses/ui/unpublish-button.tsx +15 -0
  330. package/template/templates/features/courses/domain/archive.ts +39 -0
  331. package/template/templates/features/courses/domain/on-offer.ts +6 -0
  332. package/template/templates/features/courses/enrollments/enrollments.ts +309 -0
  333. package/template/templates/features/courses/enrollments/ui/session-cell.tsx +75 -0
  334. package/template/templates/features/courses/enrollments/ui/session-labels.tsx +36 -0
  335. package/template/templates/features/courses/index.ts +8 -0
  336. package/template/templates/features/courses/package.json +24 -0
  337. package/template/templates/features/courses/portal/ui/catalogue.tsx +86 -0
  338. package/template/templates/features/courses/portal/ui/course.tsx +176 -0
  339. package/template/templates/features/courses/portal/ui/enrollments.tsx +170 -0
  340. package/template/templates/features/courses/portal/ui/parts.tsx +95 -0
  341. package/template/templates/features/courses/portal/ui/reads.ts +76 -0
  342. package/template/templates/features/courses/portal/ui/rules.ts +87 -0
  343. package/template/templates/features/courses/portal/ui/shell.tsx +194 -0
  344. package/template/templates/features/courses/sessions/seats.ts +42 -0
  345. package/template/templates/features/courses/sessions/sessions.ts +186 -0
  346. package/template/templates/features/media/index.ts +10 -0
  347. package/template/templates/features/media/media/media.ts +131 -0
  348. package/template/templates/features/media/media/ui/preview.tsx +143 -0
  349. package/template/templates/features/media/package.json +19 -0
  350. package/template/templates/features/roles/index.ts +4 -0
  351. package/template/templates/features/roles/member-roles/member-roles.ts +207 -0
  352. package/template/templates/features/roles/package.json +21 -0
  353. package/template/templates/features/roles/roles/roles.ts +184 -0
  354. package/template/templates/features/roles/roles/ui/role-form.tsx +267 -0
  355. package/template/templates/features/roles/roles/ui/roles.tsx +190 -0
  356. package/template/templates/features/settings/index.ts +3 -0
  357. package/template/templates/features/settings/package.json +13 -0
  358. package/template/templates/features/settings/settings/settings.ts +64 -0
  359. package/template/templates/features/tags/index.ts +3 -0
  360. package/template/templates/features/tags/package.json +14 -0
  361. package/template/templates/features/tags/tags/tags.ts +80 -0
  362. package/template/templates/mithaq/components.json +22 -0
  363. package/template/templates/mithaq/package.json +33 -0
  364. package/template/templates/mithaq/src/mail/index.ts +32 -0
  365. package/template/templates/mithaq/src/mail/mailers.ts +36 -0
  366. package/template/templates/mithaq/src/mithaq/audit-sink.ts +111 -0
  367. package/template/templates/mithaq/src/mithaq/auth/backoffice-roles.ts +108 -0
  368. package/template/templates/mithaq/src/mithaq/auth/index.ts +1859 -0
  369. package/template/templates/mithaq/src/mithaq/auth/organizations.ts +80 -0
  370. package/template/templates/mithaq/src/mithaq/pages.ts +83 -0
  371. package/template/templates/mithaq/src/mithaq/rich-text-nodes.ts +19 -0
  372. package/template/templates/mithaq/src/mithaq.ts +593 -0
  373. package/template/templates/mithaq/src/roles/default-member-roles.ts +29 -0
  374. package/template/templates/mithaq/src/roles/grants.ts +47 -0
  375. package/template/templates/mithaq/src/roles/member-access.ts +11 -0
  376. package/template/templates/mithaq/src/roles/statements.ts +13 -0
  377. package/template/templates/mithaq/src/roles/system-roles.ts +16 -0
  378. package/template/templates/mithaq/src/roles/tenant-access.ts +260 -0
  379. package/template/templates/mithaq/src/roles/tenant-roles.ts +321 -0
  380. package/template/templates/mithaq/src/steps/expected-updated-at.ts +41 -0
  381. package/template/templates/mithaq/src/storage/defaults.ts +156 -0
  382. package/template/templates/mithaq/src/storage/index.ts +97 -0
  383. package/template/templates/mithaq/src/storage/local.ts +143 -0
  384. package/template/templates/mithaq/src/storage/r2.ts +5 -0
  385. package/template/templates/mithaq/src/storage/s3.ts +91 -0
  386. package/template/templates/mithaq/src/storage/uploadthing.ts +59 -0
  387. package/template/templates/mithaq/src/ui/activity-page.tsx +33 -0
  388. package/template/templates/mithaq/src/ui/activity.tsx +527 -0
  389. package/template/templates/mithaq/src/ui/app-shell.tsx +268 -0
  390. package/template/templates/mithaq/src/ui/kpi-card.tsx +25 -0
  391. package/template/templates/mithaq/src/ui/record-page.tsx +50 -0
  392. package/template/templates/mithaq/src/ui/rich-text-nodes.tsx +33 -0
  393. package/template/templates/mithaq/src/ui/screen-nav.tsx +28 -0
  394. package/template/templates/mithaq/src/ui/table-page.tsx +9 -0
  395. package/template/templates/mithaq/src/ui/top-shell.tsx +60 -0
  396. package/README.md +0 -3
@@ -0,0 +1,94 @@
1
+ // Written by `mithaq generate screens`. Do not edit.
2
+ import type { ScreenClient as Client } from 'mithaq/admin'
3
+ export type { PageClient } from 'mithaq/admin'
4
+ export type CoursesValues = {
5
+ id: string
6
+ updatedAt: Date
7
+ createdAt: Date
8
+ organization: unknown
9
+ title: string
10
+ summary: string | null
11
+ fee: number | null
12
+ currency: 'SAR' | 'USD' | null
13
+ level: number | null
14
+ notifications: boolean | null
15
+ delivery: 'live' | 'recorded' | null
16
+ audience: 'beginner' | 'advanced' | null
17
+ accessCode: string | null
18
+ archived: boolean | null
19
+ topics: string[] | null
20
+ opensOn: string | null
21
+ archiveReason: string | null
22
+ tags: unknown[] | null
23
+ scheduledSessions: Values['sessions'][]
24
+ _status: 'draft' | 'published' | null
25
+ details: { root: Record<string, unknown> } | null
26
+ cover: string | null
27
+ selfEnroll: boolean | null
28
+ }
29
+ export type EnrollmentsValues = {
30
+ id: string
31
+ updatedAt: Date
32
+ createdAt: Date
33
+ organization: unknown
34
+ session: string
35
+ student: string
36
+ deletedAt: Date | null
37
+ }
38
+ export type SessionsValues = {
39
+ id: string
40
+ updatedAt: Date
41
+ createdAt: Date
42
+ organization: unknown
43
+ course: string
44
+ startsAt: Date
45
+ capacity: number | null
46
+ venue: 'online' | 'campus' | null
47
+ format: 'workshop' | 'lecture' | null
48
+ archived: boolean | null
49
+ details: { root: Record<string, unknown> } | null
50
+ enrollments: Values['enrollments'][]
51
+ archiveReason: string | null
52
+ }
53
+ export type MediaValues = {
54
+ id: string
55
+ updatedAt: Date
56
+ createdAt: Date
57
+ organization: unknown
58
+ filename: string
59
+ mimeType: string
60
+ filesize: number
61
+ width: number | null
62
+ height: number | null
63
+ key: string
64
+ storage: string
65
+ alt: Record<'ar' | 'en', string>
66
+ }
67
+ export type UserValues = {
68
+ id: string
69
+ updatedAt: Date
70
+ createdAt: Date
71
+ name: string
72
+ email: string
73
+ emailVerified: boolean
74
+ image: string | null
75
+ role: string | null
76
+ banned: boolean | null
77
+ banReason: string | null
78
+ banExpires: Date | null
79
+ }
80
+ export type Values = {
81
+ courses: CoursesValues
82
+ enrollments: EnrollmentsValues
83
+ sessions: SessionsValues
84
+ media: MediaValues
85
+ user: UserValues
86
+ }
87
+ export type Pages = {
88
+ courses: 'list' | 'view'
89
+ enrollments: 'list' | 'create' | 'edit'
90
+ sessions: never
91
+ media: never
92
+ user: never
93
+ }
94
+ export type ScreenClient = Client<Pages>
@@ -0,0 +1,17 @@
1
+ // Written by `mithaq generate screens`. Do not edit.
2
+ import { createFileRoute } from '@tanstack/react-router'
3
+ import { ScreenNotFound, ScreenPageView, askedFor, pageHead, pageLoader } from '@/screens.tsx'
4
+ import { files } from './portal/files.ts'
5
+
6
+ export const Route = createFileRoute('/(screens)/portal/$')({
7
+ validateSearch: askedFor,
8
+ loader: ({ location, params }) => pageLoader({ screen: 'portal', files, path: params._splat ?? '', query: location.search, href: location.href }),
9
+ head: ({ loaderData, match }) => pageHead(loaderData, match.context),
10
+ notFoundComponent: () => <ScreenNotFound files={files} screen={'portal'} />,
11
+ component: Page,
12
+ })
13
+
14
+ function Page() {
15
+ const navigate = Route.useNavigate()
16
+ return <ScreenPageView files={files} data={Route.useLoaderData()} query={Route.useSearch()} onQuery={(search) => void navigate({ search })} />
17
+ }
@@ -0,0 +1,10 @@
1
+ // Written by `mithaq generate screens`. Do not edit.
2
+ import { createFileRoute } from '@tanstack/react-router'
3
+ import { ScreenPageView, askedFor, pageHead, pageLoader } from '@/screens.tsx'
4
+ import { files } from './portal/files.ts'
5
+
6
+ export const Route = createFileRoute('/(screens)/portal/')({
7
+ loader: ({ location }) => pageLoader({ screen: 'portal', files, path: '', query: {}, href: location.href }),
8
+ head: ({ loaderData, match }) => pageHead(loaderData, match.context),
9
+ component: () => <ScreenPageView files={files} data={Route.useLoaderData()} query={askedFor({})} onQuery={() => {}} />,
10
+ })
@@ -0,0 +1,13 @@
1
+ // Written by `mithaq generate screens`. Do not edit.
2
+ import { Outlet, createFileRoute } from '@tanstack/react-router'
3
+ import { ScreenFrame, frameLoader } from '@/screens.tsx'
4
+ import { files } from './portal/files.ts'
5
+
6
+ export const Route = createFileRoute('/(screens)/portal')({
7
+ loader: ({ location }) => frameLoader({ screen: 'portal', files, href: location.href }),
8
+ component: () => (
9
+ <ScreenFrame files={files} data={Route.useLoaderData()}>
10
+ <Outlet />
11
+ </ScreenFrame>
12
+ ),
13
+ })
@@ -0,0 +1,99 @@
1
+ import { HeadContent, Outlet, Scripts, createRootRoute } from '@tanstack/react-router'
2
+ import { DirectionProvider } from '@/components/ui/direction'
3
+ export { locales } from '../copy.ts'
4
+ import { dirOf, documentHead, localeOf, translator } from '../copy.ts'
5
+ import { ThemeProvider } from 'mithaq-admin/theme.tsx'
6
+ import { themeStorageKey } from '../theme.ts'
7
+ import appCss from '../styles.css?url'
8
+
9
+ /**
10
+ * Puts the chosen theme on the document before the first paint, which React cannot do: it
11
+ * reads `localStorage` only after mount. Also reloads a page restored from the back-forward
12
+ * cache, so Back after signing out asks the server again instead of showing the signed-in page.
13
+ */
14
+ const theme = `
15
+ (function() {
16
+ var storageKey = ${JSON.stringify(themeStorageKey)};
17
+ var defaultTheme = "system";
18
+ try {
19
+ var theme = localStorage.getItem(storageKey) || defaultTheme;
20
+ var systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
21
+ var root = document.documentElement;
22
+ root.classList.remove("light", "dark");
23
+ root.classList.add(theme === "system" ? systemTheme : theme);
24
+ } catch (e) {}
25
+ window.addEventListener("pageshow", function (event) {
26
+ if (event.persisted) location.reload();
27
+ });
28
+ })()
29
+ `
30
+
31
+ /**
32
+ * An unregistered request context is `never` in the router's types, so the shape is named
33
+ * here and read through a cast; `localeOf` turns it back into a declared locale.
34
+ */
35
+ type FromRequest = { serverContext?: { locale?: unknown } }
36
+
37
+ export const Route = createRootRoute({
38
+ // On the client the locale is the `lang` the server wrote on `<html>`, so the hydrated tree matches
39
+ // the one sent. `localeOf` on both sides refuses a hand-edited `lang` or cookie.
40
+ beforeLoad: (ctx) => {
41
+ const locale =
42
+ typeof document === 'undefined'
43
+ ? localeOf((ctx as unknown as FromRequest).serverContext?.locale as string | undefined)
44
+ : localeOf(document.documentElement.lang)
45
+ return { locale, dir: dirOf(locale), app: 'marketing.brand' }
46
+ },
47
+ head: ({ match }) => ({
48
+ meta: [
49
+ { charSet: 'utf-8' },
50
+ { name: 'viewport', content: 'width=device-width, initial-scale=1' },
51
+ // `_notFound` is the router's mark on the root match it draws the not-found for.
52
+ ...documentHead(match.context, match._notFound ? (t) => t('admin.notFoundTitle', 'Page not found') : undefined).meta,
53
+ ],
54
+ links: [{ rel: 'stylesheet', href: appCss }, { rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' }],
55
+ }),
56
+ // Without it the router draws a development-only placeholder and warns.
57
+ notFoundComponent: NotFound,
58
+ component: RootDocument,
59
+ })
60
+
61
+ export const useCopy = () => {
62
+ const { dir, locale } = Route.useRouteContext()
63
+ return { dir, locale, t: translator(locale) }
64
+ }
65
+
66
+ function NotFound() {
67
+ const { t } = useCopy()
68
+ return (
69
+ <main className="flex min-h-screen flex-col items-center justify-center gap-4 px-4 text-center">
70
+ <h1 className="text-2xl font-bold tracking-tight">{t('admin.notFoundTitle', 'Page not found')}</h1>
71
+ <p className="text-muted-foreground text-sm">{t('admin.notFound', 'That page does not exist.')}</p>
72
+ {/* An anchor, because `/` is no route here. */}
73
+ <a className="text-foreground text-sm underline underline-offset-4" href="/">
74
+ {t('admin.goHome', 'Go to the home page')}
75
+ </a>
76
+ </main>
77
+ )
78
+ }
79
+
80
+ function RootDocument() {
81
+ const { dir, locale } = useCopy()
82
+ return (
83
+ <html lang={locale} dir={dir} suppressHydrationWarning>
84
+ <head>
85
+ <HeadContent />
86
+ {/* Safe to write raw: the string is this module's own and carries no value from a request. */}
87
+ <script dangerouslySetInnerHTML={{ __html: theme }} />
88
+ </head>
89
+ <body>
90
+ <DirectionProvider direction={dir}>
91
+ <ThemeProvider defaultTheme="system" storageKey={themeStorageKey}>
92
+ <Outlet />
93
+ </ThemeProvider>
94
+ </DirectionProvider>
95
+ <Scripts />
96
+ </body>
97
+ </html>
98
+ )
99
+ }
@@ -0,0 +1,11 @@
1
+ // `server` is a module augmentation `@tanstack/start-client-core` declares; the erased type-only
2
+ // import below loads it, so this app still ships `@tanstack/react-router` alone.
3
+ import { createFileRoute } from '@tanstack/react-router'
4
+ import type {} from '@tanstack/start-client-core'
5
+ import { asas } from 'asas'
6
+
7
+ // `/api/$` because one handler owns all of `/api`. The generated `routeTree.gen.ts` sits under
8
+ // `src/` so the root `tsc` sees the `FileRoutesByPath` augmentation that types this path.
9
+ export const Route = createFileRoute('/api/$')({
10
+ server: { handlers: { ANY: ({ request }) => asas.handler(request) } },
11
+ })
@@ -0,0 +1,108 @@
1
+ import { useEffect, useState } from 'react'
2
+ import { Link, createFileRoute, useNavigate } from '@tanstack/react-router'
3
+ import { Button } from '@/components/ui/button'
4
+ import { authArm } from '../api.ts'
5
+ import { documentHead } from '../copy.ts'
6
+ import { AuthFrame, authGate, useHomeLabel, useRefusal } from '../auth-frame.tsx'
7
+ import { useCopy } from './__root.tsx'
8
+ import { landing } from '../redirect.ts'
9
+
10
+ // Accepting is a POST the person presses, so following a link alone joins nobody to anything.
11
+ // The provider decides whether the invitation is theirs.
12
+
13
+ export const Route = createFileRoute('/auth/accept-invitation')({
14
+ head: ({ match }) => documentHead(match.context, (t) => t('auth.invitation')),
15
+ validateSearch: (search: Record<string, unknown>): { id?: string } => (typeof search.id === 'string' && search.id ? { id: search.id } : {}),
16
+ beforeLoad: () => authGate(),
17
+ component: AcceptInvitation,
18
+ })
19
+
20
+ function AcceptInvitation() {
21
+ const { t, locale } = useCopy()
22
+ const navigate = useNavigate()
23
+ const refused = useRefusal()
24
+ const { id } = Route.useSearch()
25
+ const { signedIn, home } = Route.useRouteContext()
26
+ const homeLabel = useHomeLabel()
27
+ const [organization, setOrganization] = useState<string>()
28
+ const [gone, setGone] = useState<string>()
29
+ const [pending, setPending] = useState(false)
30
+ const [refusal, setRefusal] = useState<string>()
31
+ const title = t('auth.invitation', 'Invitation')
32
+
33
+ useEffect(() => {
34
+ if (!signedIn || !id) return
35
+ fetch(`/api/auth/user/organization/get-invitation?id=${encodeURIComponent(id)}`)
36
+ .then(async (response) => {
37
+ const body = (await response.json().catch(() => null)) as { organizationName?: string; organizationNames?: Record<string, string>; code?: unknown } | null
38
+ if (response.ok) return setOrganization(body?.organizationNames?.[locale] || body?.organizationName || '')
39
+ // Better Auth answers a missing, spent or expired invitation 400 with no code.
40
+ setGone(refused({ status: response.status, body }, t('auth.invitationGone', 'This invitation has expired, was cancelled or was already accepted. Ask whoever invited you to send a new one.')))
41
+ })
42
+ .catch(() => setGone(t('auth.requestFailed', 'That request did not work.')))
43
+ }, [signedIn, id, locale])
44
+
45
+ const accept = async () => {
46
+ setPending(true)
47
+ setRefusal(undefined)
48
+ const answered = await authArm('organization/accept-invitation', { invitationId: id })
49
+ if (!answered.ok) {
50
+ setPending(false)
51
+ return setRefusal(refused(answered, t('auth.invitationAcceptFailed', 'That invitation could not be accepted.')))
52
+ }
53
+ // The provider made the organization active as it accepted, so the first screen is read as a member.
54
+ await navigate({ href: landing({ asked: undefined, screens: (await authGate()).screens }) })
55
+ }
56
+
57
+ if (!id) return <AuthFrame title={title} description={t('auth.invitationMissing', 'This link names no invitation.')}>{null}</AuthFrame>
58
+ if (!signedIn) {
59
+ const here = `/auth/accept-invitation?id=${encodeURIComponent(id)}`
60
+ return (
61
+ <AuthFrame title={title} description={t('auth.invitationSignedOut', 'Sign in, or make an account with the email address the invitation was sent to, to accept it.')}>
62
+ <div className="grid gap-2">
63
+ <Button nativeButton={false} render={<Link to="/auth/sign-in" search={{ redirect: here }} />}>
64
+ {t('auth.signIn', 'Sign in')}
65
+ </Button>
66
+ <Button variant="outline" nativeButton={false} render={<Link to="/auth/sign-up" search={{ redirect: here }} />}>
67
+ {t('auth.signUp', 'Sign up')}
68
+ </Button>
69
+ </div>
70
+ </AuthFrame>
71
+ )
72
+ }
73
+ if (gone) {
74
+ return (
75
+ <AuthFrame title={title}>
76
+ <div className="grid gap-4">
77
+ <p role="alert" data-invitation-gone="" className="text-sm">
78
+ {gone}
79
+ </p>
80
+ {/* A spent invitation is no dead end for someone already in: the way to their own first screen. An anchor,
81
+ because the path is the screen's own data, not a route the router knows. */}
82
+ <Button nativeButton={false} render={<a href={home.path} data-home="" />}>
83
+ {homeLabel(home, 'marketing.openScreen')}
84
+ </Button>
85
+ {/* The invitation may be another address's, which signing out and in again reaches. */}
86
+ <Button type="button" variant="outline" onClick={() => void authArm('sign-out').then(() => window.location.reload())}>
87
+ {t('admin.signOut', 'Sign out')}
88
+ </Button>
89
+ </div>
90
+ </AuthFrame>
91
+ )
92
+ }
93
+ if (organization === undefined) return <AuthFrame title={title} description={t('auth.invitationReading', 'Reading the invitation…')}>{null}</AuthFrame>
94
+ return (
95
+ <AuthFrame title={title} description={t('auth.invitationJoin', 'You are invited to join {organization}.')!.replace('{organization}', () => organization)}>
96
+ <div className="grid gap-4">
97
+ {refusal ? (
98
+ <p className="text-destructive text-sm" role="alert">
99
+ {refusal}
100
+ </p>
101
+ ) : null}
102
+ <Button type="button" data-button="accept-invitation" disabled={pending} onClick={() => void accept()}>
103
+ {pending ? t('auth.invitationAccepting', 'Accepting…') : t('auth.invitationAccept', 'Accept the invitation')}
104
+ </Button>
105
+ </div>
106
+ </AuthFrame>
107
+ )
108
+ }
@@ -0,0 +1,114 @@
1
+ import { useState, type ComponentProps } from 'react'
2
+ import { createFileRoute, redirect, useNavigate } from '@tanstack/react-router'
3
+ import { Button } from '@/components/ui/button'
4
+ import { authArm } from '../api.ts'
5
+ import { AuthBox, AuthFrame, authGate, notAnEmail, useRefusal } from '../auth-frame.tsx'
6
+ import { documentHead } from '../copy.ts'
7
+ import { firstRun } from '../first-run.ts'
8
+ import { useCopy } from './__root.tsx'
9
+
10
+ // The first owner of an app of one organization, and the organization itself: open once,
11
+ // to the holder of the setup code, and closed for good once the organization exists. The
12
+ // organization's name lives in code (`organizationName` in `organizations.ts`), so none is asked.
13
+ export const Route = createFileRoute('/auth/first-run')({
14
+ head: ({ match }) => documentHead(match.context, (t) => t('auth.firstRun')),
15
+ beforeLoad: async () => {
16
+ const state = await firstRun()
17
+ // `href`, not `to`: sign-in redirects here too, and two routes typed by each other's paths infer nothing.
18
+ if (!state.open) throw redirect({ href: '/auth/sign-in' })
19
+ return { code: state.code, password: (await authGate()).password }
20
+ },
21
+ component: FirstRun,
22
+ })
23
+
24
+ function FirstRun() {
25
+ const navigate = useNavigate()
26
+ const { t } = useCopy()
27
+ const refused = useRefusal()
28
+ const { code: asked, password: bounds } = Route.useRouteContext()
29
+ const [name, setName] = useState('')
30
+ const [email, setEmail] = useState('')
31
+ const [password, setPassword] = useState('')
32
+ const [confirmation, setConfirmation] = useState('')
33
+ const [code, setCode] = useState('')
34
+ const [pending, setPending] = useState(false)
35
+ const [refusal, setRefusal] = useState<string | null>(null)
36
+ const [left, setLeft] = useState<ReadonlySet<string>>(() => new Set())
37
+ const required = t('auth.required', 'This field is required.')
38
+ const wrong = {
39
+ name: () => (name.trim() ? undefined : required),
40
+ email: () => (!email ? required : notAnEmail(email) ? t('auth.invalidEmail', 'Invalid email') : undefined),
41
+ password: () =>
42
+ !password ? required
43
+ : password.length < bounds.min ? t('auth.passwordTooShort', 'Password too short')
44
+ : password.length > bounds.max ? t('auth.passwordTooLong', 'Password too long')
45
+ : undefined,
46
+ confirmation: () => (!confirmation ? required : confirmation === password ? undefined : t('auth.passwordsDiffer', 'The two passwords are not the same.')),
47
+ code: () => (asked !== 'required' || code ? undefined : required),
48
+ }
49
+ const errorOf = (box: keyof typeof wrong) => (left.has(box) ? wrong[box]() : undefined)
50
+ const box = (id: keyof typeof wrong, label: string | undefined, props: ComponentProps<'input'> & { description?: string }) => (
51
+ <AuthBox id={id} label={label} error={errorOf(id)} onLeave={() => setLeft((already) => new Set(already).add(id))} {...props} />
52
+ )
53
+
54
+ const submit = async (event: React.FormEvent) => {
55
+ event.preventDefault()
56
+ setLeft(new Set(Object.keys(wrong)))
57
+ if (Object.values(wrong).some((check) => check())) return
58
+ setPending(true)
59
+ setRefusal(null)
60
+ const made = await authArm('first-run', { name, email, password, ...(asked === 'required' && { code }) })
61
+ setPending(false)
62
+ if (!made.ok) return setRefusal(refused(made, t('auth.firstRunFailed', 'That owner could not be made.')))
63
+ await navigate({ to: '/auth/sign-in' })
64
+ }
65
+
66
+ if (asked === 'unset') {
67
+ return (
68
+ <AuthFrame title={t('auth.firstRun', 'Set up this app')}>
69
+ <p className="text-sm" role="alert">
70
+ {t('auth.firstRunCodeUnset')}
71
+ </p>
72
+ </AuthFrame>
73
+ )
74
+ }
75
+ return (
76
+ <AuthFrame title={t('auth.firstRun', 'Set up this app')} description={t('auth.firstRunDescription', 'Nobody owns this app yet. The account made here becomes its owner and first administrator.')}>
77
+ <form className="grid gap-4" noValidate onSubmit={submit}>
78
+ {box('name', t('auth.name', 'Name'), { autoComplete: 'name', onChange: (event) => setName(event.target.value), type: 'text', value: name })}
79
+ {box('email', t('auth.email', 'Email'), { autoComplete: 'email', onChange: (event) => setEmail(event.target.value), type: 'email', value: email })}
80
+ {box('password', t('auth.password', 'Password'), {
81
+ autoComplete: 'new-password',
82
+ description: t('auth.passwordRule', 'At least {count} characters.')!.replace('{count}', String(bounds.min)),
83
+ onChange: (event) => setPassword(event.target.value),
84
+ type: 'password',
85
+ value: password,
86
+ })}
87
+ {box('confirmation', t('auth.confirmPassword', 'Confirm the password'), {
88
+ autoComplete: 'new-password',
89
+ onChange: (event) => setConfirmation(event.target.value),
90
+ type: 'password',
91
+ value: confirmation,
92
+ })}
93
+ {asked === 'required'
94
+ ? box('code', t('auth.setupCode', 'Setup code'), {
95
+ autoComplete: 'off',
96
+ description: t('auth.setupCodeDescription', 'The SETUP_CODE this server was started with.'),
97
+ onChange: (event) => setCode(event.target.value),
98
+ type: 'password',
99
+ value: code,
100
+ })
101
+ : null}
102
+ {/* `role="alert"` so a refusal is spoken when it appears; nothing moves the focus to it. */}
103
+ {refusal && (
104
+ <p className="text-destructive text-sm" role="alert">
105
+ {refusal}
106
+ </p>
107
+ )}
108
+ <Button disabled={pending} type="submit">
109
+ {pending ? t('auth.firstRunPending', 'Setting up…') : t('auth.firstRunSubmit', 'Set up the app')}
110
+ </Button>
111
+ </form>
112
+ </AuthFrame>
113
+ )
114
+ }
@@ -0,0 +1,94 @@
1
+ import { useState } from 'react'
2
+ import { Link, createFileRoute } from '@tanstack/react-router'
3
+ import { Button } from '@/components/ui/button'
4
+ import { authArm } from '../api.ts'
5
+ import { documentHead } from '../copy.ts'
6
+ import { AlreadySignedIn, AuthBox, AuthFrame, authGate, notAnEmail, redirectSearch, useRefusal } from '../auth-frame.tsx'
7
+ import { useCopy } from './__root.tsx'
8
+
9
+ // The sentence after a request is the same whether or not the address has an account, so
10
+ // nothing on this page says which it was.
11
+
12
+ export const Route = createFileRoute('/auth/forgot-password')({
13
+ head: ({ match }) => documentHead(match.context, (t) => t('auth.forgotPassword')),
14
+ validateSearch: redirectSearch,
15
+ beforeLoad: () => authGate(),
16
+ component: ForgotPassword,
17
+ })
18
+
19
+ function ForgotPassword() {
20
+ const { t } = useCopy()
21
+ const search = Route.useSearch()
22
+ const { signedIn, home, screens } = Route.useRouteContext()
23
+ const refused = useRefusal()
24
+ const [email, setEmail] = useState('')
25
+ const [left, setLeft] = useState(false)
26
+ const [pending, setPending] = useState(false)
27
+ const [sent, setSent] = useState(false)
28
+ const [refusal, setRefusal] = useState<string | null>(null)
29
+ const wrong = () => (!email ? t('auth.required', 'This field is required.') : notAnEmail(email) ? t('auth.invalidEmail', 'Invalid email') : undefined)
30
+ const back = (
31
+ <p className="mt-4 text-center text-sm">
32
+ <Link className="text-primary underline underline-offset-4" to="/auth/sign-in" search={{ redirect: search.redirect }}>
33
+ {t('auth.backToSignIn', 'Back to login')}
34
+ </Link>
35
+ </p>
36
+ )
37
+
38
+ const submit = async (event: React.FormEvent) => {
39
+ event.preventDefault()
40
+ setLeft(true)
41
+ if (wrong()) return
42
+ setPending(true)
43
+ setRefusal(null)
44
+ const answered = await authArm('request-password-reset', { email })
45
+ setPending(false)
46
+ if (!answered.ok) return setRefusal(refused(answered, t('auth.requestFailed', 'That request did not work.')))
47
+ setSent(true)
48
+ }
49
+
50
+ if (signedIn) return <AlreadySignedIn redirect={search.redirect} home={home} screens={screens} />
51
+ if (sent) {
52
+ return (
53
+ <AuthFrame
54
+ title={t('auth.emailSent', 'Email sent')}
55
+ description={t('auth.checkYourEmail', "If the email address is associated with an account, you will receive instructions to reset your password shortly. Please check your spam or junk mail folder if you don't see the email in your inbox.")}
56
+ >
57
+ {import.meta.env?.DEV ? (
58
+ <p data-no-mailer="" className="text-muted-foreground rounded-md border px-3 py-2 text-sm">
59
+ {t('auth.noMailer', 'No mailer is configured, so no email was sent. In development the reset link is written to the server log.')}
60
+ </p>
61
+ ) : null}
62
+ {back}
63
+ </AuthFrame>
64
+ )
65
+ }
66
+ return (
67
+ <AuthFrame
68
+ title={t('auth.forgotPassword', 'Forgot password')}
69
+ description={t('auth.forgotPasswordInstructions', 'Please enter your email below. You will receive an email message with instructions on how to reset your password.')}
70
+ >
71
+ <form className="grid gap-4" noValidate onSubmit={submit}>
72
+ <AuthBox
73
+ autoComplete="email"
74
+ error={left ? wrong() : undefined}
75
+ id="email"
76
+ label={t('auth.email', 'Email')}
77
+ onChange={(event) => setEmail(event.target.value)}
78
+ onLeave={() => setLeft(true)}
79
+ type="email"
80
+ value={email}
81
+ />
82
+ {refusal && (
83
+ <p className="text-destructive text-sm" role="alert">
84
+ {refusal}
85
+ </p>
86
+ )}
87
+ <Button disabled={pending} type="submit">
88
+ {t('auth.submit', 'Submit')}
89
+ </Button>
90
+ </form>
91
+ {back}
92
+ </AuthFrame>
93
+ )
94
+ }
@@ -0,0 +1,39 @@
1
+ // Trades the backoffice's one-time impersonation token for the session through the plugin's own
2
+ // verify, then lands on the first screen the impersonated person may enter. The access report is
3
+ // read with only the session the verify set, so the landing grants nothing. A spent, expired or
4
+ // unknown token sets nothing and lands on sign-in.
5
+ import { createFileRoute } from '@tanstack/react-router'
6
+ import type {} from '@tanstack/start-client-core'
7
+
8
+ const firstScreen = (targets: unknown, listed: readonly string[], backoffice: string) => {
9
+ const screens = targets as Record<string, { capability?: string; client?: { path: string } } | undefined>
10
+ const name = listed.find((one) => one !== backoffice && screens[one]?.capability === 'screen')
11
+ return name ? screens[name]!.client!.path : '/unauthorized'
12
+ }
13
+
14
+ export const Route = createFileRoute('/auth/handoff')({
15
+ server: {
16
+ handlers: {
17
+ GET: async ({ request }) => {
18
+ const { asas, tenantURL, BACKOFFICE_SCREEN } = await import('asas')
19
+ // The deployment's own origin, never the request's, whose host a caller writes.
20
+ const own = new URL(tenantURL).origin
21
+ const verified = await asas.handler(
22
+ new Request(new URL('/api/auth/user/one-time-token/verify', own), {
23
+ method: 'POST',
24
+ headers: { 'content-type': 'application/json', origin: own },
25
+ body: JSON.stringify({ token: new URL(request.url).searchParams.get('token') ?? '' }),
26
+ }),
27
+ )
28
+ const cookies = verified.headers.getSetCookie()
29
+ const session = cookies.map((cookie) => cookie.split(';')[0]).join('; ')
30
+ const report = verified.ok
31
+ ? ((await (await asas.handler(new Request(new URL('/api/access', own), { headers: { cookie: session } }))).json().catch(() => ({}))) as { screens?: string[] })
32
+ : undefined
33
+ const headers = new Headers({ location: report ? firstScreen(asas.targets, report.screens ?? [], BACKOFFICE_SCREEN) : '/auth/sign-in' })
34
+ for (const cookie of cookies) headers.append('set-cookie', cookie)
35
+ return new Response(null, { status: 302, headers })
36
+ },
37
+ },
38
+ },
39
+ })
@@ -0,0 +1,16 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ import { documentHead } from '../copy.ts'
3
+ import { ChoosePassword, passwordGate, tokenSearch } from '../choose-password.tsx'
4
+
5
+ // Where a forgotten password's link lands. A first password, chosen from a sign-up's or an added person's link, has a
6
+ // page of its own, `/auth/set-password`, with the same form.
7
+ export const Route = createFileRoute('/auth/reset-password')({
8
+ head: ({ match }) => documentHead(match.context, (t) => t('auth.resetPassword')),
9
+ validateSearch: tokenSearch,
10
+ beforeLoad: ({ search }: { search: { token?: string } }) => passwordGate(search),
11
+ component: ResetPassword,
12
+ })
13
+
14
+ function ResetPassword() {
15
+ return <ChoosePassword {...Route.useSearch()} {...Route.useRouteContext()} first={false} />
16
+ }
@@ -0,0 +1,16 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ import { documentHead } from '../copy.ts'
3
+ import { ChoosePassword, passwordGate, tokenSearch } from '../choose-password.tsx'
4
+
5
+ // Where a sign-up's or an added person's link lands: a brand-new account chooses its first password here, under a
6
+ // title that says so, and never reads that it is resetting one.
7
+ export const Route = createFileRoute('/auth/set-password')({
8
+ head: ({ match }) => documentHead(match.context, (t) => t('auth.setPassword')),
9
+ validateSearch: tokenSearch,
10
+ beforeLoad: ({ search }: { search: { token?: string } }) => passwordGate(search),
11
+ component: SetPassword,
12
+ })
13
+
14
+ function SetPassword() {
15
+ return <ChoosePassword {...Route.useSearch()} {...Route.useRouteContext()} first />
16
+ }