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,163 @@
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 UserValues = {
5
+ id: string
6
+ updatedAt: Date
7
+ createdAt: Date
8
+ name: string
9
+ email: string
10
+ emailVerified: boolean
11
+ image: string | null
12
+ role: string | null
13
+ banned: boolean | null
14
+ banReason: string | null
15
+ banExpires: Date | null
16
+ }
17
+ export type OrganizationValues = {
18
+ id: string
19
+ updatedAt: Date
20
+ createdAt: Date
21
+ name: string
22
+ slug: string
23
+ logo: string | null
24
+ metadata: string | null
25
+ names: string | null
26
+ }
27
+ export type MemberValues = {
28
+ id: string
29
+ updatedAt: Date
30
+ createdAt: Date
31
+ organizationId: string
32
+ userId: string
33
+ role: string
34
+ lastSeenAt: Date | null
35
+ }
36
+ export type CoursesValues = {
37
+ id: string
38
+ updatedAt: Date
39
+ createdAt: Date
40
+ organization: string
41
+ title: string
42
+ summary: string | null
43
+ fee: number | null
44
+ currency: 'SAR' | 'USD' | null
45
+ level: number | null
46
+ notifications: boolean | null
47
+ delivery: 'live' | 'recorded' | null
48
+ audience: 'beginner' | 'advanced' | null
49
+ accessCode: string | null
50
+ archived: boolean | null
51
+ topics: string[] | null
52
+ opensOn: string | null
53
+ archiveReason: string | null
54
+ tags: string[] | null
55
+ scheduledSessions: Values['sessions'][]
56
+ _status: 'draft' | 'published' | null
57
+ details: { root: Record<string, unknown> } | null
58
+ cover: unknown
59
+ selfEnroll: boolean | null
60
+ }
61
+ export type SessionsValues = {
62
+ id: string
63
+ updatedAt: Date
64
+ createdAt: Date
65
+ organization: string
66
+ course: string
67
+ startsAt: Date
68
+ capacity: number | null
69
+ venue: 'online' | 'campus' | null
70
+ format: 'workshop' | 'lecture' | null
71
+ archived: boolean | null
72
+ details: { root: Record<string, unknown> } | null
73
+ enrollments: Values['enrollments'][]
74
+ archiveReason: string | null
75
+ }
76
+ export type EnrollmentsValues = {
77
+ id: string
78
+ updatedAt: Date
79
+ createdAt: Date
80
+ organization: string
81
+ session: string
82
+ student: string
83
+ deletedAt: Date | null
84
+ }
85
+ export type TagsValues = {
86
+ id: string
87
+ updatedAt: Date
88
+ createdAt: Date
89
+ organization: string
90
+ name: Record<'ar' | 'en', string>
91
+ }
92
+ export type RolesValues = {
93
+ id: string
94
+ updatedAt: Date
95
+ createdAt: Date
96
+ organization: string
97
+ name: string
98
+ label: Record<'ar' | 'en', string>
99
+ permissions: string[]
100
+ }
101
+ export type MemberRolesValues = {
102
+ id: string
103
+ updatedAt: Date
104
+ createdAt: Date
105
+ organization: string
106
+ user: string
107
+ role: string
108
+ }
109
+ export type LocalUsersValues = {
110
+ id: string
111
+ updatedAt: Date
112
+ createdAt: Date
113
+ email: string
114
+ hash: string
115
+ salt: string
116
+ sessions: string | null
117
+ loginAttempts: number | null
118
+ lockUntil: Date | null
119
+ resetPasswordToken: string | null
120
+ resetPasswordAttempts: number | null
121
+ resetPasswordExpiration: Date | null
122
+ _verified: boolean | null
123
+ _verificationToken: string | null
124
+ }
125
+ export type BackofficeLogValues = {
126
+ id: string
127
+ createdAt: Date
128
+ action: 'impersonate' | 'stopImpersonating' | 'setRole' | 'ban' | 'unban'
129
+ actor: string | null
130
+ actorEmail: string | null
131
+ target: string | null
132
+ targetEmail: string | null
133
+ previousRole: string | null
134
+ role: string | null
135
+ banReason: string | null
136
+ }
137
+ export type Values = {
138
+ user: UserValues
139
+ organization: OrganizationValues
140
+ member: MemberValues
141
+ courses: CoursesValues
142
+ sessions: SessionsValues
143
+ enrollments: EnrollmentsValues
144
+ tags: TagsValues
145
+ roles: RolesValues
146
+ memberRoles: MemberRolesValues
147
+ localUsers: LocalUsersValues
148
+ backofficeLog: BackofficeLogValues
149
+ }
150
+ export type Pages = {
151
+ user: 'list' | 'view'
152
+ organization: 'list' | 'view'
153
+ member: 'list' | 'view'
154
+ courses: 'list' | 'view'
155
+ sessions: 'list' | 'view'
156
+ enrollments: 'list' | 'view'
157
+ tags: 'list' | 'view'
158
+ roles: 'list' | 'view'
159
+ memberRoles: 'list' | 'view'
160
+ localUsers: 'list' | 'edit'
161
+ backofficeLog: 'list'
162
+ }
163
+ 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 './backoffice/files.ts'
5
+
6
+ export const Route = createFileRoute('/(screens)/backoffice/$')({
7
+ validateSearch: askedFor,
8
+ loader: ({ location, params }) => pageLoader({ screen: 'backoffice', files, path: params._splat ?? '', query: location.search, href: location.href }),
9
+ head: ({ loaderData, match }) => pageHead(loaderData, match.context),
10
+ notFoundComponent: () => <ScreenNotFound files={files} screen={'backoffice'} />,
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 './backoffice/files.ts'
5
+
6
+ export const Route = createFileRoute('/(screens)/backoffice/')({
7
+ loader: ({ location }) => pageLoader({ screen: 'backoffice', 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 './backoffice/files.ts'
5
+
6
+ export const Route = createFileRoute('/(screens)/backoffice')({
7
+ loader: ({ location }) => frameLoader({ screen: 'backoffice', files, href: location.href }),
8
+ component: () => (
9
+ <ScreenFrame files={files} data={Route.useLoaderData()}>
10
+ <Outlet />
11
+ </ScreenFrame>
12
+ ),
13
+ })
@@ -0,0 +1,95 @@
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
+ // Puts the chosen theme on the document before first paint, which the provider's effect cannot do.
10
+ // Also reloads a page restored from the back-forward cache, so Back after signing out asks the server again.
11
+ const theme = `
12
+ (function() {
13
+ var storageKey = ${JSON.stringify(themeStorageKey)};
14
+ var defaultTheme = "system";
15
+ try {
16
+ var theme = localStorage.getItem(storageKey) || defaultTheme;
17
+ var systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
18
+ var root = document.documentElement;
19
+ root.classList.remove("light", "dark");
20
+ root.classList.add(theme === "system" ? systemTheme : theme);
21
+ } catch (e) {}
22
+ window.addEventListener("pageshow", function (event) {
23
+ if (event.persisted) location.reload();
24
+ });
25
+ })()
26
+ `
27
+
28
+ // A request context nothing registered is `never` in the router's types, so the shape is named and read
29
+ // through a cast; `localeOf` turns it back into a declared locale.
30
+ type FromRequest = { serverContext?: { locale?: unknown } }
31
+
32
+ export const Route = createRootRoute({
33
+ // On the client the locale is read back off the `lang` the server wrote on `<html>`, so the hydrated tree
34
+ // matches the one sent. `localeOf` on both refuses a hand-edited `lang` like a hand-written cookie.
35
+ beforeLoad: (ctx) => {
36
+ const locale =
37
+ typeof document === 'undefined'
38
+ ? localeOf((ctx as unknown as FromRequest).serverContext?.locale as string | undefined)
39
+ : localeOf(document.documentElement.lang)
40
+ return { locale, dir: dirOf(locale), app: 'backoffice.brand' }
41
+ },
42
+ // The favicon stops the browser asking for a `/favicon.ico` nobody serves.
43
+ head: ({ match }) => ({
44
+ meta: [
45
+ { charSet: 'utf-8' },
46
+ { name: 'viewport', content: 'width=device-width, initial-scale=1' },
47
+ ...documentHead(match.context, match._notFound ? (t) => t('admin.notFoundTitle', 'Page not found') : undefined).meta,
48
+ ],
49
+ links: [{ rel: 'stylesheet', href: appCss }, { rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' }],
50
+ }),
51
+ // Without it the router draws a development-only placeholder and warns.
52
+ notFoundComponent: NotFound,
53
+ component: RootDocument,
54
+ })
55
+
56
+ /** The locale and direction of this request and the `t` resolved for it, read from the root route's context. */
57
+ export const useCopy = () => {
58
+ const { dir, locale } = Route.useRouteContext()
59
+ return { dir, locale, t: translator(locale) }
60
+ }
61
+
62
+ function NotFound() {
63
+ const { t } = useCopy()
64
+ return (
65
+ <main className="flex min-h-screen flex-col items-center justify-center gap-4 px-4 text-center">
66
+ <h1 className="text-2xl font-bold tracking-tight">{t('admin.notFoundTitle', 'Page not found')}</h1>
67
+ <p className="text-muted-foreground text-sm">{t('admin.notFound', 'That page does not exist.')}</p>
68
+ {/* An anchor, because `/` is no route here. */}
69
+ <a className="text-foreground text-sm underline underline-offset-4" href="/">
70
+ {t('admin.goHome', 'Go to the home page')}
71
+ </a>
72
+ </main>
73
+ )
74
+ }
75
+
76
+ function RootDocument() {
77
+ const { dir, locale } = useCopy()
78
+ return (
79
+ <html lang={locale} dir={dir} suppressHydrationWarning>
80
+ <head>
81
+ <HeadContent />
82
+ {/* The string is this module's own and carries no request value, so writing it raw is safe. */}
83
+ <script dangerouslySetInnerHTML={{ __html: theme }} />
84
+ </head>
85
+ <body>
86
+ <DirectionProvider direction={dir}>
87
+ <ThemeProvider defaultTheme="system" storageKey={themeStorageKey}>
88
+ <Outlet />
89
+ </ThemeProvider>
90
+ </DirectionProvider>
91
+ <Scripts />
92
+ </body>
93
+ </html>
94
+ )
95
+ }
@@ -0,0 +1,34 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ // Loads the types of the `server` route option; erased at build.
3
+ import type {} from '@tanstack/start-client-core'
4
+ import { asas, backofficeURL } from 'asas'
5
+
6
+ // The backoffice reads every organization: a read of a kayan asks `tenant: 'all'`, which the runner
7
+ // grants backoffice staff in this app alone and refuses anyone else. A global is one organization's row
8
+ // and asks for none.
9
+ const reads = new Set(asas.operations.filter((one) => one.effect === 'read' && !one.single).map((one) => `/api/${one.kayan}/${one.name}`))
10
+
11
+ // The body is handed on unread, so the plug that answers bounds it before any session is looked at.
12
+ const acrossTenants = (request: Request) => {
13
+ const url = new URL(request.url)
14
+ if (!reads.has(url.pathname)) return request
15
+ url.searchParams.set('tenant', 'all')
16
+ return new Request(url, { method: request.method, headers: request.headers, body: request.body, signal: request.signal, duplex: 'half' } as RequestInit)
17
+ }
18
+
19
+ // A write whose `Origin` is not BACKOFFICE_URL's is refused: the data plug checks no origin and leans on
20
+ // the session cookie's `SameSite=Lax`, which the customer app's sibling subdomain passes, so this check
21
+ // is the barrier between the two. Never the URL the request arrived on: behind a TLS proxy it is
22
+ // http://, and its host is whatever the Host header said.
23
+ const origin = new URL(backofficeURL).origin
24
+
25
+ export const Route = createFileRoute('/api/$')({
26
+ server: {
27
+ handlers: {
28
+ ANY: async ({ request }) =>
29
+ request.method === 'GET' || request.method === 'HEAD' || request.headers.get('origin') === origin
30
+ ? asas.handler(acrossTenants(request))
31
+ : new Response('Forbidden', { status: 403 }),
32
+ },
33
+ },
34
+ })
@@ -0,0 +1,113 @@
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 { firstUser } from '../first-user.ts'
8
+ import { useCopy } from './__root.tsx'
9
+
10
+ export const Route = createFileRoute('/auth/create-first-user')({
11
+ head: ({ match }) => documentHead(match.context, (t) => t('auth.createFirstUser')),
12
+ beforeLoad: async () => {
13
+ const state = await firstUser()
14
+ if (state.initialized) throw redirect({ to: '/auth/sign-in' })
15
+ return { code: state.code, password: (await authGate()).password }
16
+ },
17
+ component: CreateFirstUser,
18
+ })
19
+
20
+ function CreateFirstUser() {
21
+ const navigate = useNavigate()
22
+ const { t } = useCopy()
23
+ const refused = useRefusal()
24
+ const { code: asked, password: bounds } = Route.useRouteContext()
25
+ const [name, setName] = useState('')
26
+ const [email, setEmail] = useState('')
27
+ const [password, setPassword] = useState('')
28
+ const [confirmation, setConfirmation] = useState('')
29
+ const [code, setCode] = useState('')
30
+ const [pending, setPending] = useState(false)
31
+ const [refusal, setRefusal] = useState<string | null>(null)
32
+ const [left, setLeft] = useState<ReadonlySet<string>>(() => new Set())
33
+ const required = t('auth.required', 'This field is required.')
34
+ const wrong = {
35
+ name: () => (name.trim() ? undefined : required),
36
+ email: () => (!email ? required : notAnEmail(email) ? t('auth.invalidEmail', 'Invalid email') : undefined),
37
+ password: () =>
38
+ !password ? required
39
+ : password.length < bounds.min ? t('auth.passwordTooShort', 'Password too short')
40
+ : password.length > bounds.max ? t('auth.passwordTooLong', 'Password too long')
41
+ : undefined,
42
+ confirmation: () => (!confirmation ? required : confirmation === password ? undefined : t('auth.passwordsDiffer', 'The two passwords are not the same.')),
43
+ code: () => (asked !== 'required' || code ? undefined : required),
44
+ }
45
+ const errorOf = (box: keyof typeof wrong) => (left.has(box) ? wrong[box]() : undefined)
46
+ const box = (id: keyof typeof wrong, label: string | undefined, props: ComponentProps<'input'> & { description?: string }) => (
47
+ <AuthBox id={id} label={label} error={errorOf(id)} onLeave={() => setLeft((already) => new Set(already).add(id))} {...props} />
48
+ )
49
+
50
+ const submit = async (event: React.FormEvent) => {
51
+ event.preventDefault()
52
+ setLeft(new Set(Object.keys(wrong)))
53
+ if (Object.values(wrong).some((check) => check())) return
54
+ setPending(true)
55
+ setRefusal(null)
56
+ const made = await authArm('first-register', { name, email, password, ...(asked === 'required' && { code }) })
57
+ setPending(false)
58
+ if (!made.ok) return setRefusal(refused(made, t('auth.createFirstUserFailed', 'That administrator could not be created.')))
59
+ await navigate({ to: '/auth/sign-in' })
60
+ }
61
+
62
+ if (asked === 'unset') {
63
+ return (
64
+ <AuthFrame title={t('auth.createFirstUser', 'Create the first administrator')}>
65
+ <p className="text-sm" role="alert">
66
+ {t('auth.setupCodeUnset')}
67
+ </p>
68
+ </AuthFrame>
69
+ )
70
+ }
71
+ return (
72
+ <AuthFrame
73
+ title={t('auth.createFirstUser', 'Create the first administrator')}
74
+ description={t('auth.createFirstUserDescription', 'No one administers this backoffice yet. The account made here becomes its first administrator.')}
75
+ >
76
+ <form className="grid gap-4" noValidate onSubmit={submit}>
77
+ {box('name', t('auth.name', 'Name'), { autoComplete: 'name', onChange: (event) => setName(event.target.value), type: 'text', value: name })}
78
+ {box('email', t('auth.email', 'Email'), { autoComplete: 'email', onChange: (event) => setEmail(event.target.value), type: 'email', value: email })}
79
+ {box('password', t('auth.password', 'Password'), {
80
+ autoComplete: 'new-password',
81
+ description: t('auth.passwordRule', 'At least {count} characters.')!.replace('{count}', String(bounds.min)),
82
+ onChange: (event) => setPassword(event.target.value),
83
+ type: 'password',
84
+ value: password,
85
+ })}
86
+ {box('confirmation', t('auth.confirmPassword', 'Confirm the password'), {
87
+ autoComplete: 'new-password',
88
+ onChange: (event) => setConfirmation(event.target.value),
89
+ type: 'password',
90
+ value: confirmation,
91
+ })}
92
+ {asked === 'required'
93
+ ? box('code', t('auth.setupCode', 'Setup code'), {
94
+ autoComplete: 'off',
95
+ description: t('auth.setupCodeDescription', 'The SETUP_CODE this server was started with.'),
96
+ onChange: (event) => setCode(event.target.value),
97
+ type: 'password',
98
+ value: code,
99
+ })
100
+ : null}
101
+ {/* `role="alert"` so a refusal is spoken when it appears; nothing moves the focus to it. */}
102
+ {refusal && (
103
+ <p className="text-destructive text-sm" role="alert">
104
+ {refusal}
105
+ </p>
106
+ )}
107
+ <Button disabled={pending} type="submit">
108
+ {pending ? t('auth.createFirstUserPending', 'Creating…') : t('auth.createFirstUserSubmit', 'Create the administrator')}
109
+ </Button>
110
+ </form>
111
+ </AuthFrame>
112
+ )
113
+ }
@@ -0,0 +1,77 @@
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, redirectSearch, useRefusal } from '../auth-frame.tsx'
6
+ import { documentHead } from '../copy.ts'
7
+ import { firstUser } from '../first-user.ts'
8
+ import { useCopy } from './__root.tsx'
9
+ import { landing } from '../redirect.ts'
10
+
11
+ export const Route = createFileRoute('/auth/sign-in')({
12
+ head: ({ match }) => documentHead(match.context, (t) => t('auth.signIn')),
13
+ validateSearch: redirectSearch,
14
+ // Nobody can sign in to a backoffice no one administers yet.
15
+ beforeLoad: async () => {
16
+ if (!(await firstUser()).initialized) throw redirect({ href: '/auth/create-first-user' })
17
+ },
18
+ component: SignIn,
19
+ })
20
+
21
+ function SignIn() {
22
+ const navigate = useNavigate()
23
+ const { t } = useCopy()
24
+ const search = Route.useSearch()
25
+ const refused = useRefusal()
26
+ const [email, setEmail] = useState('')
27
+ const [password, setPassword] = useState('')
28
+ const [pending, setPending] = useState(false)
29
+ const [refusal, setRefusal] = useState<string | null>(null)
30
+ const [left, setLeft] = useState<ReadonlySet<string>>(() => new Set())
31
+ const required = t('auth.required', 'This field is required.')
32
+ const wrong = {
33
+ email: () => (!email ? required : notAnEmail(email) ? t('auth.invalidEmail', 'Invalid email') : undefined),
34
+ password: () => (password ? undefined : required),
35
+ }
36
+ const errorOf = (box: keyof typeof wrong) => (left.has(box) ? wrong[box]() : undefined)
37
+ const box = (id: keyof typeof wrong, label: string | undefined, props: ComponentProps<'input'>) => (
38
+ <AuthBox id={id} label={label} error={errorOf(id)} onLeave={() => setLeft((already) => new Set(already).add(id))} {...props} />
39
+ )
40
+
41
+ const submit = async (event: React.FormEvent) => {
42
+ event.preventDefault()
43
+ setLeft(new Set(Object.keys(wrong)))
44
+ if (Object.values(wrong).some((check) => check())) return
45
+ setPending(true)
46
+ setRefusal(null)
47
+ const answered = await authArm('sign-in/email', { email, password })
48
+ // Our own sentence for the provider's code, not its English `message`: a refusal is copy and carries a string per locale.
49
+ if (!answered.ok) {
50
+ setPending(false)
51
+ return setRefusal(refused(answered, t('auth.failed', 'That sign-in did not work.')))
52
+ }
53
+
54
+ setPending(false)
55
+
56
+ // A navigation, not a reload, so the screen's loader runs with the cookie just set.
57
+ await navigate({ href: landing({ asked: search.redirect, screens: (await authGate()).screens }) })
58
+ }
59
+
60
+ return (
61
+ <AuthFrame title={t('auth.signIn', 'Sign in')} description={t('auth.signInBackofficeDescription', 'Sign in to reach the backoffice.')}>
62
+ <form className="grid gap-4" noValidate onSubmit={submit}>
63
+ {box('email', t('auth.email', 'Email'), { autoComplete: 'email', onChange: (event) => setEmail(event.target.value), type: 'email', value: email })}
64
+ {box('password', t('auth.password', 'Password'), { autoComplete: 'current-password', onChange: (event) => setPassword(event.target.value), type: 'password', value: password })}
65
+ {/* `role="alert"` so a refusal is spoken when it appears; nothing moves the focus to it. */}
66
+ {refusal && (
67
+ <p className="text-destructive text-sm" role="alert">
68
+ {refusal}
69
+ </p>
70
+ )}
71
+ <Button disabled={pending} type="submit">
72
+ {pending ? t('auth.signInPending', 'Signing in…') : t('auth.signIn', 'Sign in')}
73
+ </Button>
74
+ </form>
75
+ </AuthFrame>
76
+ )
77
+ }
@@ -0,0 +1,134 @@
1
+ import { Fragment, useState, type ReactNode } from 'react'
2
+ import { createFileRoute, redirect } from '@tanstack/react-router'
3
+ import { Button } from '@/components/ui/button'
4
+ import { authArm } from '../api.ts'
5
+ import { AuthFrame, useRefusal } from '../auth-frame.tsx'
6
+ import { documentHead } from '../copy.ts'
7
+ import { NoOrganization } from '../no-organization.tsx'
8
+ import { ImpersonationBar, loadEnterable } from '../screens.tsx'
9
+ import { useCopy } from './__root.tsx'
10
+
11
+ /**
12
+ * Where someone sent here from a screen their role does not open goes without being asked: the one screen it does
13
+ * open. A page refused inside that screen is said instead, and so is an impersonation, whose bar is the way out.
14
+ */
15
+ export const onwardOf = ({ screens, refused, impersonating }: { screens: readonly { path: string }[]; refused?: unknown; impersonating?: unknown }) =>
16
+ !refused && !impersonating && screens.length === 1 ? screens[0]!.path : undefined
17
+
18
+ export const Route = createFileRoute('/unauthorized')({
19
+ // The path a screen refused; the server only matches it against the build's pages.
20
+ validateSearch: (search: Record<string, unknown>): { from?: string } => (typeof search.from === 'string' ? { from: search.from } : {}),
21
+ loaderDeps: ({ search }) => ({ from: search.from }),
22
+ loader: async ({ deps, context }) => {
23
+ const entered = await loadEnterable({ data: { from: deps.from, locale: context.locale } })
24
+ // Carries this page's own path so a visitor whose session expired lands back here after signing in.
25
+ if (!entered) throw redirect({ to: '/auth/sign-in', search: { redirect: '/unauthorized' } })
26
+ const onward = onwardOf(entered)
27
+ if (onward) throw redirect({ href: onward })
28
+ return entered
29
+ },
30
+ // After the loader, so its data's type is inferred before this reads it.
31
+ head: ({ match, loaderData }) => documentHead(match.context, (t) => (loaderData?.screens.length === 0 ? t('auth.noAccess') : t('auth.unauthorized'))),
32
+ component: Unauthorized,
33
+ })
34
+
35
+ function Unauthorized() {
36
+ const { t } = useCopy()
37
+ const { screens, refused, email, impersonating, organizations = [], others = [] } = Route.useLoaderData()
38
+ const refusal = useRefusal()
39
+ const [switching, setSwitching] = useState<string>()
40
+ const [failed, setFailed] = useState<string>()
41
+ // The slug sits beside the name where another membership shares the name.
42
+ const memberships = [...organizations, ...others]
43
+ const named = (one: { id: string; name: string; slug?: string }): ReactNode =>
44
+ one.slug && memberships.some((other) => other.id !== one.id && other.name === one.name) ? (
45
+ <>
46
+ {one.name} <span dir="ltr" data-slug="" className="text-muted-foreground text-xs font-normal">{one.slug}</span>
47
+ </>
48
+ ) : (
49
+ one.name
50
+ )
51
+ // Each name is isolated so an email or Latin name in an Arabic sentence keeps
52
+ // its order, and is one inline box that wraps whole, so a long address never
53
+ // runs past the card on a phone.
54
+ const whole = 'inline-block max-w-full [overflow-wrap:anywhere]'
55
+ const said = (text: string | undefined) =>
56
+ text!.split(/(\{email\}|\{organization\}|\{page\})/).map((part, at) =>
57
+ part === '{email}' ? <bdi key={at} className={whole}>{email}</bdi>
58
+ : part === '{organization}' ? <bdi key={at}>{organizations.map((one, index) => <Fragment key={one.id}>{index ? ', ' : ''}<span className={whole}>{named(one)}</span></Fragment>)}</bdi>
59
+ : part === '{page}' ? <bdi key={at}>{refused?.page}</bdi>
60
+ : part)
61
+ const link = 'text-foreground underline underline-offset-4'
62
+ // Someone who belongs to no organization is shown the ways into one, which the app draws.
63
+ const unplaced = !refused && !screens.length && !organizations.length && !others.length && !impersonating
64
+ const screenName = (name: string) => t(`screens.${name}`, name) ?? name
65
+ return (
66
+ <AuthFrame
67
+ title={screens.length ? t('auth.unauthorized', 'Choose where to go') : t('auth.noAccess', 'Your account has no access yet')}
68
+ description={
69
+ <span data-no-access={screens.length ? undefined : ''}>
70
+ {refused
71
+ ? said(t('auth.unauthorizedPage', 'Your role does not open that page: {page}.'))
72
+ : screens.length
73
+ ? t('auth.unauthorizedDescription', 'This page isn\'t part of your account. Where would you like to go?')
74
+ : organizations.length
75
+ ? said(t('auth.noAccessDescription', '{email} is a member of {organization} and holds no role that opens a screen here. Ask an administrator of {organization} to give you one.'))
76
+ : said(t('auth.noOrganization', '{email} belongs to no organization yet.'))}
77
+ </span>
78
+ }
79
+ >
80
+ <div className="grid gap-4">
81
+ {/* Stopping impersonation is the way out in place of Sign out. */}
82
+ {impersonating ? <ImpersonationBar name={impersonating} /> : null}
83
+ {refused ? (
84
+ <a className={link} href={refused.screen.path}>
85
+ {(t('auth.backTo', 'Back to {screen}') ?? 'Back to {screen}').replace('{screen}', () => screenName(refused.screen.name))}
86
+ </a>
87
+ ) : screens.length ? (
88
+ <div className="grid gap-2">
89
+ {screens.map((screen) => (
90
+ // An anchor: the path is the screen's own data, not a route the router knows.
91
+ <Button key={screen.name} nativeButton={false} className="max-sm:h-11" render={<a href={screen.path} />}>
92
+ {screenName(screen.name)}
93
+ </Button>
94
+ ))}
95
+ </div>
96
+ ) : unplaced ? (
97
+ <NoOrganization email={email} />
98
+ ) : null}
99
+ {/* So a switch into an organization where the actor holds no role is not a dead end. */}
100
+ {others.map((one) => (
101
+ <Button
102
+ key={one.id}
103
+ type="button"
104
+ variant="secondary"
105
+ data-switch-organization={one.id}
106
+ // Busy while it runs, so a second press sends nothing.
107
+ disabled={switching !== undefined}
108
+ aria-busy={switching === one.id || undefined}
109
+ onClick={() => {
110
+ setSwitching(one.id)
111
+ setFailed(undefined)
112
+ void authArm('organization/set-active', { organizationId: one.id }).then((answer) => {
113
+ if (answer.ok) return window.location.assign('/auth/sign-in')
114
+ setSwitching(undefined)
115
+ setFailed(refusal(answer, t('auth.requestFailed', 'That request did not work.')))
116
+ })
117
+ }}
118
+ >
119
+ {/* One box, so the button's gap does not open beside the name. */}
120
+ <span>{(t('auth.switchTo', 'Switch to {organization}') ?? 'Switch to {organization}').split(/(\{organization\})/).map((part, at) => (part === '{organization}' ? <bdi key={at}>{named(one)}</bdi> : part))}</span>
121
+ </Button>
122
+ ))}
123
+ {failed ? <p role="alert" className="text-destructive text-sm">{failed}</p> : null}
124
+ {/* `replace`, so Back does not return to the page signed out of. */}
125
+ {impersonating ? null : (
126
+ <Button type="button" variant="outline" className="max-sm:h-11" onClick={() => void authArm('sign-out').then(() => window.location.replace('/auth/sign-in'))}>
127
+ {t('admin.signOut', 'Sign out')}
128
+ </Button>
129
+ )}
130
+ </div>
131
+ </AuthFrame>
132
+ )
133
+ }
134
+ // mithaq-admin 0.0.0 81d25978cdacbb38254854b0f64b738946ee27baca06a952aed51d35e6fb10af