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,25 @@
1
+ [
2
+ { "type": "rename", "kind": "column", "from": ["public", "tags", "name"], "to": ["public", "tags", "name_en"] },
3
+ { "type": "create", "kind": "column", "entity": ["public", "tags", "name_ar"] },
4
+ { "type": "rename", "kind": "index", "from": ["public", "tags", "tags_name_index"], "to": ["public", "tags", "tags_name_en_index"] },
5
+ { "type": "create", "kind": "index", "entity": ["public", "tags", "tags_name_ar_index"] },
6
+ { "type": "rename", "kind": "column", "from": ["public", "audit_log", "tenant_id"], "to": ["public", "audit_log", "organization_id"] },
7
+ { "type": "rename", "kind": "column", "from": ["public", "courses", "tenant_id"], "to": ["public", "courses", "organization_id"] },
8
+ { "type": "rename", "kind": "column", "from": ["public", "enrollments", "tenant_id"], "to": ["public", "enrollments", "organization_id"] },
9
+ { "type": "rename", "kind": "column", "from": ["public", "media", "tenant_id"], "to": ["public", "media", "organization_id"] },
10
+ { "type": "rename", "kind": "column", "from": ["public", "member_roles", "tenant_id"], "to": ["public", "member_roles", "organization_id"] },
11
+ { "type": "rename", "kind": "column", "from": ["public", "roles", "tenant_id"], "to": ["public", "roles", "organization_id"] },
12
+ { "type": "rename", "kind": "column", "from": ["public", "course_sessions", "tenant_id"], "to": ["public", "course_sessions", "organization_id"] },
13
+ { "type": "rename", "kind": "column", "from": ["public", "settings", "tenant_id"], "to": ["public", "settings", "organization_id"] },
14
+ { "type": "rename", "kind": "column", "from": ["public", "tags", "tenant_id"], "to": ["public", "tags", "organization_id"] },
15
+ { "type": "rename", "kind": "unique", "from": ["public", "settings", "settings_tenant_id_key"], "to": ["public", "settings", "settings_organization_id_key"] },
16
+ { "type": "rename", "kind": "foreign key", "from": ["public", "audit_log", "audit_log_tenant_id_organization_id_fkey"], "to": ["public", "audit_log", "audit_log_organization_id_organization_id_fkey"] },
17
+ { "type": "rename", "kind": "foreign key", "from": ["public", "courses", "courses_tenant_id_organization_id_fkey"], "to": ["public", "courses", "courses_organization_id_organization_id_fkey"] },
18
+ { "type": "rename", "kind": "foreign key", "from": ["public", "enrollments", "enrollments_tenant_id_organization_id_fkey"], "to": ["public", "enrollments", "enrollments_organization_id_organization_id_fkey"] },
19
+ { "type": "rename", "kind": "foreign key", "from": ["public", "media", "media_tenant_id_organization_id_fkey"], "to": ["public", "media", "media_organization_id_organization_id_fkey"] },
20
+ { "type": "rename", "kind": "foreign key", "from": ["public", "member_roles", "member_roles_tenant_id_organization_id_fkey"], "to": ["public", "member_roles", "member_roles_organization_id_organization_id_fkey"] },
21
+ { "type": "rename", "kind": "foreign key", "from": ["public", "roles", "roles_tenant_id_organization_id_fkey"], "to": ["public", "roles", "roles_organization_id_organization_id_fkey"] },
22
+ { "type": "rename", "kind": "foreign key", "from": ["public", "course_sessions", "course_sessions_tenant_id_organization_id_fkey"], "to": ["public", "course_sessions", "course_sessions_organization_id_organization_id_fkey"] },
23
+ { "type": "rename", "kind": "foreign key", "from": ["public", "settings", "settings_tenant_id_organization_id_fkey"], "to": ["public", "settings", "settings_organization_id_organization_id_fkey"] },
24
+ { "type": "rename", "kind": "foreign key", "from": ["public", "tags", "tags_tenant_id_organization_id_fkey"], "to": ["public", "tags", "tags_organization_id_organization_id_fkey"] }
25
+ ]
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "asas",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev:setup": "node --env-file-if-exists=.env src/dev-setup.ts",
8
+ "seed": "node --env-file-if-exists=.env src/seed.ts"
9
+ },
10
+ "exports": {
11
+ ".": "./src/asas.ts",
12
+ "./ui-dashboard/*": "./src/ui-dashboard/*",
13
+ "./messages/*": "./src/messages/*",
14
+ "./organizations": "./src/organizations.ts",
15
+ "./package.json": "./package.json"
16
+ },
17
+ "dependencies": {
18
+ "@asas/audit-log": "*",
19
+ "@asas/auth": "*",
20
+ "@asas/courses": "*",
21
+ "@asas/media": "*",
22
+ "@asas/mithaq": "*",
23
+ "@asas/roles": "*",
24
+ "@asas/settings": "*",
25
+ "@asas/tags": "*",
26
+ "@lexical/extension": "0.52.0",
27
+ "@lexical/link": "0.52.0",
28
+ "@lexical/list": "0.52.0",
29
+ "@lexical/rich-text": "0.52.0",
30
+ "@orpc/openapi": "1.15.1",
31
+ "@orpc/server": "1.15.1",
32
+ "aws4fetch": "1.0.20",
33
+ "better-auth": "1.7.4",
34
+ "drizzle-orm": "1.0.0-rc.4",
35
+ "file-type": "22.0.1",
36
+ "image-dimensions": "2.5.1",
37
+ "jose": "6.2.12",
38
+ "lexical": "0.52.0",
39
+ "mithaq": "*",
40
+ "mithaq-admin": "*",
41
+ "pg-connection-string": "2.14.0",
42
+ "zod": "^4.6.5"
43
+ },
44
+ "devDependencies": {
45
+ "@tanstack/react-query": "^5.104.0",
46
+ "@types/pg": "^8.15.6",
47
+ "@types/react": "^19.3.0",
48
+ "lucide-react": "^1.16.0",
49
+ "react": "^19.3.0",
50
+ "recharts": "3.8.0",
51
+ "typescript": "^7.0.2"
52
+ }
53
+ }
@@ -0,0 +1,118 @@
1
+ import { providerStatements } from '@asas/mithaq/auth'
2
+ // Each feature is read through its index alone, as a namespace, so its list
3
+ // (`courses.kayan`) never collides with a kayan of the same name.
4
+ // add-on: audit-log
5
+ import * as auditLog from '@asas/audit-log'
6
+ // end add-on: audit-log
7
+ import * as auth from '@asas/auth'
8
+ // add-on: courses
9
+ import * as courses from '@asas/courses'
10
+ // end add-on: courses
11
+ // add-on: media
12
+ import * as media from '@asas/media'
13
+ // end add-on: media
14
+ // add-on: roles
15
+ import * as roles from '@asas/roles'
16
+ // end add-on: roles
17
+ // add-on: settings
18
+ import * as settings from '@asas/settings'
19
+ // end add-on: settings
20
+ import * as tags from '@asas/tags'
21
+ import { defaultMaxBodyBytes, fetchHttp, Notice } from 'mithaq'
22
+ import { orpcHttp } from 'mithaq/orpc'
23
+ import { buildAsas, requestContext } from '@asas/mithaq'
24
+ import { backofficeHttp } from './backoffice-http.ts'
25
+ import { projectStatements } from '@asas/mithaq/roles/statements.ts'
26
+ import { superAdmin } from '@asas/mithaq/roles/tenant-access.ts'
27
+
28
+ /**
29
+ * Where a fault the build answers 500 to, or a strategy that threw, is reported: this project's
30
+ * log, the console. A project with an error tracker names it here. A `Notice` is expected, such
31
+ * as the build of a local auth kayan with no mailer, and prints as its one line; anything else
32
+ * prints with its stack.
33
+ */
34
+ const report = (error: unknown) => console.error(error instanceof Notice ? error.message : error)
35
+
36
+ export const asas = buildAsas({
37
+ kayan: [
38
+ ...auth.kayan,
39
+ // add-on: roles
40
+ ...roles.kayan,
41
+ // end add-on: roles
42
+ ...tags.kayan,
43
+ // add-on: media
44
+ // An image is read by a holder of `media.create`, and otherwise through
45
+ // what the features pointing at images say of it.
46
+ ...media.kayan({
47
+ // add-on: courses
48
+ reads: [courses.coverReads],
49
+ // end add-on: courses
50
+ // without add-on: courses
51
+ // reads: [],
52
+ // end without add-on: courses
53
+ }),
54
+ // end add-on: media
55
+ // add-on: courses
56
+ ...courses.kayan,
57
+ // end add-on: courses
58
+ // add-on: audit-log
59
+ ...auditLog.kayan,
60
+ // end add-on: audit-log
61
+ ],
62
+ global: [
63
+ // add-on: settings
64
+ ...settings.global,
65
+ // end add-on: settings
66
+ ],
67
+ // The HTTP layer, a list the handler asks in order. `orpcHttp` is first
68
+ // because it owns `/api/rpc`, where a typed client would reach the build, and
69
+ // because `asas.router` is the first plug's. It also claims `/api`, so the
70
+ // plain plug behind it answers only what oRPC's own OpenAPI arm matched
71
+ // nothing for; it is listed so a build that drops the typed client keeps plain
72
+ // HTTP by deleting one line. Core picks no plug of its own, so a build that
73
+ // leaves `http` out is refused.
74
+ // Each plug is handed the same context function, so a step reads the same
75
+ // bag whichever plug served it.
76
+ // The backoffice's rename of an organization sits between them, before the
77
+ // plain plug would read `/api/backoffice/*` as a kayan's route.
78
+ // A fault either plug answers 500 is handed to `report`; core reports none of its own.
79
+ // A body past `maxBodyBytes` is answered 413 before it is read whole.
80
+ http: [
81
+ orpcHttp({ context: (request, actor) => ({ actor, ...requestContext(request) }), onError: report, maxBodyBytes: defaultMaxBodyBytes }),
82
+ backofficeHttp(),
83
+ fetchHttp({ context: requestContext, onError: report, maxBodyBytes: defaultMaxBodyBytes }),
84
+ ],
85
+ // Better Auth's own actions, which no declaration of this project derives.
86
+ // They join the catalog under the resources they name, so a role holds
87
+ // `invitation.cancel` or `user.impersonate` exactly as it holds
88
+ // `tags.find`, and the hooks in @asas/mithaq/auth ask for them under whichever
89
+ // store this project turned on. `organization.rename` is this project's own,
90
+ // the key the backoffice's rename route asks, which is neither the
91
+ // provider's `organization.update` nor the managed kayan's, both of which
92
+ // share that name. The project's own keys beside a kayan's operations are
93
+ // listed with the role form that offers them.
94
+ statements: { ...providerStatements, organization: [...providerStatements.organization, 'rename'], ...projectStatements },
95
+ // `onError` hears a strategy that threw and an access target that could not
96
+ // answer, which the build reports nowhere of its own.
97
+ //
98
+ // `admin.access` is the answer `/api/access` reports as `canAccessAdmin`:
99
+ // `true` for a super admin in the backoffice app, `false` for every other
100
+ // actor. The local `unlock` endpoint asks the same answer of the caller local
101
+ // auth alone resolves, which is never a Better Auth super admin, so it stays
102
+ // closed.
103
+ //
104
+ // The administrator is a super admin in the backoffice app and nobody else: the
105
+ // one who verifies a local sign-up. A kayan with no callback of its own falls
106
+ // to this answer, so every table Better Auth alone writes closes itself.
107
+ auth: { admin: { kayan: auth.user, access: superAdmin }, onError: report, maxBodyBytes: defaultMaxBodyBytes },
108
+ })
109
+
110
+ // The backoffice slug, the screen the backoffice app serves, the reader that
111
+ // answers the backoffice roles, and the tenant app's link to the backoffice, for a
112
+ // host.
113
+ export { BACKOFFICE, BACKOFFICE_SCREEN, assignedRoles, backofficeEntry } from '@asas/mithaq/roles/tenant-access.ts'
114
+ // What the backoffice app wraps every request it serves in, whether a request is
115
+ // one of those, and where each app is served: re-exported here so a host reaches
116
+ // the one instance of the store behind it through the same module the build reads.
117
+ export { asBackoffice, onBackoffice, backofficeURL, tenantURL } from '@asas/mithaq/auth'
118
+ export type { Language } from '@asas/mithaq'
@@ -0,0 +1,117 @@
1
+ // The backoffice screen's one action no managed operation carries: renaming an
2
+ // organization and its slug. An organization has no admin route for staff:
3
+ // every route of Better Auth's organization plugin acts as a member of that
4
+ // organization, which staff are not, and the managed `organization` kayan
5
+ // writes nothing, because Better Auth owns the table and the tenant's `'*'`
6
+ // row, which every backoffice role takes too, names no update. So the rename is
7
+ // this one route of the project's own, refused without `organization.rename`,
8
+ // a key of the project's own that only `super_admin` holds, and audited in the
9
+ // line every admin route of the backoffice writes.
10
+ //
11
+ // Deleting an organization, removing a member and changing a member's
12
+ // governance role stay out: each is irreversible or passes the plugin's own
13
+ // owner rules by, and staff do each by impersonating a member, which is
14
+ // audited under the real person's name.
15
+ import { AsasError, actorPermissions, conflict, denied, invalid, missing, retryConflict, type HttpPlug } from 'mithaq'
16
+ import { namesOf, oneLine, backofficeRoles } from '@asas/mithaq/auth'
17
+ import { organizations as mode } from '@asas/mithaq/auth/organizations'
18
+ import { targets } from '@asas/mithaq'
19
+ import { assignedRoles, type OrganizationActor } from '@asas/mithaq/roles/tenant-access.ts'
20
+
21
+ const path = '/api/backoffice/organizations/update'
22
+ /**
23
+ * The project's own key, not `organization.update`: that name is both the
24
+ * provider's action and the managed kayan's update, which the rows refuse to
25
+ * everyone.
26
+ */
27
+ const key = 'organization.rename'
28
+
29
+ const slugShape = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
30
+
31
+ /**
32
+ * The longest name and slug a rename writes. The columns are unbounded `text`,
33
+ * so the caps are this route's own: a name as long as the auth setup lets a
34
+ * founder write one, and a slug that fits in a URL segment.
35
+ */
36
+ const most = { name: 100, slug: 64 }
37
+
38
+ /** The names `written` holds as the auth setup keeps them, or `null` where it refuses them. */
39
+ const namedOrNull = (written: unknown) => {
40
+ try {
41
+ return namesOf(written)
42
+ } catch {
43
+ return null
44
+ }
45
+ }
46
+
47
+ /** The body the shipped plain plug answers every refusal with. */
48
+ const codes: Record<number, string> = { 400: 'BAD_REQUEST', 403: 'FORBIDDEN', 404: 'NOT_FOUND', 409: 'CONFLICT' }
49
+ const refusal = (thrown: AsasError) =>
50
+ Response.json({ code: codes[thrown.status], data: { key: thrown.key, params: thrown.params }, message: thrown.message }, { status: thrown.status })
51
+
52
+ /**
53
+ * Answers the route only for an actor the template's reader gave a backoffice
54
+ * role holding `organization.rename`, which it does in the backoffice app alone:
55
+ * `'*'` inside a tenant is a tenant's and renames nothing here. The body is
56
+ * `{ id, names, slug }`, a name per locale with the default locale's required,
57
+ * or `{ id, name, slug }`, the default locale's alone; a slug another
58
+ * organization holds is the store's own 409. With one organization its name
59
+ * lives in code, so the route renames nothing: `organizations` is the project's
60
+ * mode, which a test sets.
61
+ */
62
+ export const backofficeHttp = ({ organizations }: { organizations: 'one' | 'many' } = { organizations: mode }) => (): HttpPlug => ({
63
+ name: 'backoffice',
64
+ router: undefined,
65
+ handle: async (request, who) => {
66
+ if (request.method !== 'POST' || new URL(request.url).pathname !== path) return null
67
+ try {
68
+ const actor = (await who()) as (OrganizationActor & { id?: string }) | null
69
+ const held = actorPermissions(actor)
70
+ const staff = !!actor && assignedRoles(actor).some((slug) => Object.hasOwn(backofficeRoles, slug))
71
+ if (!staff || !(held === true || held?.has(key))) {
72
+ throw denied({ message: `Forbidden: renaming an organization is the backoffice's, and it asks "${key}".`, key: 'error.forbidden.permission', params: { permission: key } })
73
+ }
74
+ // JSON alone, so a form another site posts, which a browser sends with
75
+ // no preflight, is never read as one.
76
+ const body = request.headers.get('content-type')?.startsWith('application/json') ? ((await request.json().catch(() => null)) as Record<string, unknown> | null) : null
77
+ if (organizations === 'one') {
78
+ throw conflict({ message: 'With one organization its name lives in code: organizationName in src/mithaq/auth/organizations.ts. Change it there and deploy.', key: 'error.conflict.organization_name_in_code' })
79
+ }
80
+ const id = body?.id
81
+ const slug = typeof body?.slug === 'string' ? body.slug.trim() : ''
82
+ // Each locale's name is the auth setup's own rule, so a rename keeps what a founder may write and no more.
83
+ const names = body?.names === undefined ? undefined : namedOrNull(body.names)
84
+ const name = names ? names.name : typeof body?.name === 'string' ? body.name.trim() : ''
85
+ // A name is one line: mail bodies carry it, where a break would be a line of the writer's own.
86
+ if (typeof id !== 'string' || !id || names === null || !name || name.length > most.name || new RegExp(oneLine.source, 'u').test(name) || !slugShape.test(slug) || slug.length > most.slug) {
87
+ throw invalid({
88
+ message: `An organization is renamed with its id, a name of one line and at most ${most.name} characters, and a slug of at most ${most.slug} lowercase letters, digits and hyphens.`,
89
+ key: 'error.invalid.value',
90
+ })
91
+ }
92
+ const store = targets.drizzle
93
+ // The row is found and written under one lock, so a delete between the
94
+ // two answers 404 rather than failing the write. `FOR UPDATE`, not `FOR NO
95
+ // KEY UPDATE`: the slug is unique, and the lock is taken before we know whether it changes.
96
+ await store.transaction(async (tx) => {
97
+ const found = await store.find({ kayan: 'organization', args: { limit: 1, lock: 'update', page: 1, sort: { field: 'id', direction: 'asc' }, where: { id: { equals: id } } }, tx })
98
+ if (!found.docs.length) throw missing({ message: `No organization "${id}".`, key: 'error.not_found.row', params: { kayan: 'organization' } })
99
+ await store.update({ tx, kayan: 'organization', id, data: { name, ...(names && { names: names.names }), slug, updatedAt: new Date() } })
100
+ })
101
+ // The line `backofficeAudit` writes for an admin route, under this route's own name.
102
+ console.log(JSON.stringify({
103
+ audit: 'backoffice',
104
+ action: '/backoffice/organizations/update',
105
+ actor: actor.id ?? null,
106
+ impersonatedBy: actor.impersonatedBy ?? null,
107
+ target: id,
108
+ at: new Date().toISOString(),
109
+ }))
110
+ return Response.json({ id, name, slug })
111
+ } catch (thrown) {
112
+ thrown = retryConflict(thrown)
113
+ if (thrown instanceof AsasError) return refusal(thrown)
114
+ throw thrown
115
+ }
116
+ },
117
+ })
@@ -0,0 +1,112 @@
1
+ // Makes a fresh development database usable in one command: pushes the schema,
2
+ // makes the dev admin that DEV_ADMIN_EMAIL and DEV_ADMIN_PASSWORD name, and
3
+ // with --seed fills its organization.
4
+ //
5
+ // bun run dev:setup [--seed]
6
+ //
7
+ // The dev admin owns an organization and, where the project serves the backoffice
8
+ // console, is its super_admin, with a proved address, so it signs in to both
9
+ // apps at once. A development tool only: it refuses production, a database on
10
+ // another machine, and a database that already holds a user.
11
+ import { spawnSync } from 'node:child_process'
12
+ import { fileURLToPath } from 'node:url'
13
+ import { parseArgs } from 'node:util'
14
+ import { getTableConfig, type PgTable } from 'drizzle-orm/pg-core'
15
+ import { organizations as mode } from '@asas/mithaq/auth/organizations'
16
+ import { databaseOf } from './local-database.ts'
17
+
18
+ function refuse(reason: string): never {
19
+ console.error(`Refusing dev:setup: ${reason}`)
20
+ process.exit(1)
21
+ }
22
+
23
+ const { values } = parseArgs({ options: { seed: { type: 'boolean' } } })
24
+ // Checked before the build is imported, since the import is what reads them.
25
+ if (!process.env.DATABASE_URL) refuse('set DATABASE_URL to the development database to set up.')
26
+ if (process.env.NODE_ENV && process.env.NODE_ENV !== 'development') refuse(`NODE_ENV is ${process.env.NODE_ENV}, and it sets up a development database only.`)
27
+ // The account it makes signs in with a password every project's .env.example
28
+ // holds, so it is made on this machine's database alone.
29
+ const at = databaseOf(process.env.DATABASE_URL)
30
+ if (!at) refuse('DATABASE_URL is not a connection string pg reads.')
31
+ const { database, local } = at
32
+ if (!local) refuse(`DATABASE_URL names ${database}, which is not this machine.`)
33
+ const { DEV_ADMIN_EMAIL: email, DEV_ADMIN_PASSWORD: password } = process.env
34
+ if (!email || !password) refuse('set DEV_ADMIN_EMAIL and DEV_ADMIN_PASSWORD in .env, as .env.example does.')
35
+ // Unset is development here, and the auth setup reads it: on this machine
36
+ // first-run then asks no setup code, as it does under `vite dev`.
37
+ Object.assign(process.env, { NODE_ENV: 'development' })
38
+
39
+ const { asas } = await import('./asas.ts')
40
+ const { isLocalHost, backofficeApp, backofficeURL, tenantURL } = await import('@asas/mithaq/auth')
41
+ // A deploy run with plain `node` sets no NODE_ENV and may hold its database on this machine; its address is what
42
+ // tells it apart, as the auth setup reads it, so an app served elsewhere gets no dev admin.
43
+ for (const [variable, url] of [['BETTER_AUTH_URL', tenantURL], ...(backofficeApp ? [['BACKOFFICE_URL', backofficeURL]] : [])] as const) {
44
+ if (!isLocalHost(url)) refuse(`${variable} names ${url}, which is not this machine.`)
45
+ }
46
+ const signIn = `Sign in at ${tenantURL}${backofficeApp ? ` or ${backofficeURL}` : ''}`
47
+ type Row = Record<string, unknown> & { id: string }
48
+ const { db, tables } = asas.targets.drizzle as unknown as { db: { $client: { query(text: string, values?: unknown[]): Promise<{ rows: Record<string, unknown>[] }> } }; tables: Record<'user' | 'organization', PgTable> }
49
+ const client = db.$client
50
+ /** A table of the build's own, as SQL names it. */
51
+ const named = (table: PgTable) => {
52
+ const { schema = 'public', name } = getTableConfig(table)
53
+ return [schema, name].map((part) => `"${part.replaceAll('"', '""')}"`).join('.')
54
+ }
55
+ // The provider's own routes and adapter: the generated auth kayan answer no
56
+ // operation to anyone.
57
+ const server = (asas.auth.strategies.find((strategy) => strategy.name === 'user') as unknown as {
58
+ server: {
59
+ $context: Promise<{
60
+ password: { hash(password: string): Promise<string>; config: { minPasswordLength: number; maxPasswordLength: number } }
61
+ internalAdapter: {
62
+ createUser(user: Record<string, unknown>): Promise<Row>
63
+ linkAccount(account: { userId: string; providerId: string; accountId: string; password: string }): Promise<unknown>
64
+ }
65
+ }>
66
+ api: {
67
+ firstRun(input: { body: Record<string, string> }): Promise<{ organization: { id: string } }>
68
+ createOrganization(input: { body: { name: string; slug: string; userId: string } }): Promise<{ id: string }>
69
+ }
70
+ }
71
+ }).server
72
+ const name = 'Dev admin'
73
+ const workspace = `${name}'s workspace`
74
+ try {
75
+ // Asked before the push, so a database someone already uses is never changed by it.
76
+ const users = named(tables.user)
77
+ const [table] = (await client.query('select to_regclass($1) is not null as held', [users])).rows
78
+ if (table!.held && (await client.query(`select exists (select 1 from ${users}) as held`)).rows[0]!.held) {
79
+ refuse(`the database already holds a user. ${signIn} as DEV_ADMIN_EMAIL with DEV_ADMIN_PASSWORD from .env, or as the account you made.`)
80
+ }
81
+ const auth = await server.$context
82
+ // First-run asks the same of its owner; the path with many makes the account itself, so it asks here.
83
+ const { minPasswordLength: min, maxPasswordLength: max } = auth.password.config
84
+ if (password.length < min || password.length > max) refuse(`DEV_ADMIN_PASSWORD holds ${min} to ${max} characters.`)
85
+ await asas.connect()
86
+ let organization: string
87
+ if (mode === 'one') {
88
+ // First-run's own path: the owner, the one organization and, with the backoffice console, its super_admin.
89
+ organization = (await server.api.firstRun({ body: { name, email, password } })).organization.id
90
+ } else {
91
+ // With many, an account proves its address by a mailed link and then founds its organization; the dev admin is
92
+ // made proved, as first-run makes its owner, and its workspace made for it, as the page of someone in none would.
93
+ const user = await auth.internalAdapter.createUser({ name, email, emailVerified: true, ...(backofficeApp && { role: 'super_admin' }) })
94
+ await auth.internalAdapter.linkAccount({ userId: user.id, providerId: 'credential', accountId: user.id, password: await auth.password.hash(password) })
95
+ organization = (await server.api.createOrganization({ body: { name: workspace, slug: 'dev-admin-workspace', userId: user.id } })).id
96
+ }
97
+ // Read back, since first-run names the organization from code (`organizationName`).
98
+ const [made] = (await client.query(`select name from ${named(tables.organization)} where id = $1`, [organization])).rows
99
+ console.log(`Made the dev admin, owner of "${String(made!.name)}"${backofficeApp ? ' and backoffice super_admin' : ''}. ${signIn} with DEV_ADMIN_EMAIL and DEV_ADMIN_PASSWORD from .env.`)
100
+ } finally {
101
+ await asas.destroy()
102
+ }
103
+
104
+ if (values.seed) {
105
+ // The seed names whose organization it fills by address, which stays in .env, in any case: the provider stores it
106
+ // lowercased.
107
+ const seeded = spawnSync(process.execPath, [fileURLToPath(new URL('./seed.ts', import.meta.url))], { env: { ...process.env, SEED_EMAIL: email }, stdio: ['inherit', 'pipe', 'pipe'], encoding: 'utf8' })
108
+ const address = new RegExp(email.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi')
109
+ process.stdout.write(seeded.stdout.replace(address, 'DEV_ADMIN_EMAIL'))
110
+ process.stderr.write(seeded.stderr.replace(address, 'DEV_ADMIN_EMAIL'))
111
+ process.exitCode = seeded.status ?? 1
112
+ }
@@ -0,0 +1,22 @@
1
+ import { parse } from 'pg-connection-string'
2
+
3
+ /**
4
+ * The database `url` names, as `host:port/name`, and whether it is on this machine, for the scripts that write to a
5
+ * development database; nothing when pg cannot read it. Read by pg's own parser, so the guard and the connection never
6
+ * disagree on the host, and a host left out is pg's default. Nothing of the URL is repeated on a failure, since it
7
+ * holds the password.
8
+ */
9
+ export const databaseOf = (url: string) => {
10
+ let config: ReturnType<typeof parse>
11
+ try {
12
+ config = parse(url)
13
+ } catch {
14
+ return undefined
15
+ }
16
+ const host = config.host || process.env.PGHOST || 'localhost'
17
+ return {
18
+ database: `${host}:${config.port || process.env.PGPORT || '5432'}/${config.database ?? ''}`,
19
+ // A path is a socket on this machine.
20
+ local: host.startsWith('/') || ['localhost', '127.0.0.1', '::1', '[::1]'].includes(host),
21
+ }
22
+ }
@@ -0,0 +1,132 @@
1
+ {
2
+ "accessToken": {
3
+ "invalid": {
4
+ "ar": "هذا ليس نصا.",
5
+ "en": "This is not text."
6
+ },
7
+ "required": {
8
+ "ar": "هذا الحقل مطلوب.",
9
+ "en": "This field is required."
10
+ }
11
+ },
12
+ "accessTokenExpiresAt": {
13
+ "invalid": {
14
+ "ar": "هذا ليس تاريخا ووقتا.",
15
+ "en": "This is not a date and time."
16
+ },
17
+ "required": {
18
+ "ar": "هذا الحقل مطلوب.",
19
+ "en": "This field is required."
20
+ }
21
+ },
22
+ "accountId": {
23
+ "invalid": {
24
+ "ar": "هذا ليس نصا.",
25
+ "en": "This is not text."
26
+ },
27
+ "required": {
28
+ "ar": "هذا الحقل مطلوب.",
29
+ "en": "This field is required."
30
+ }
31
+ },
32
+ "createdAt": {
33
+ "invalid": {
34
+ "ar": "هذا ليس تاريخا ووقتا.",
35
+ "en": "This is not a date and time."
36
+ },
37
+ "required": {
38
+ "ar": "هذا الحقل مطلوب.",
39
+ "en": "This field is required."
40
+ }
41
+ },
42
+ "id": {
43
+ "invalid": {
44
+ "ar": "هذا ليس نصا.",
45
+ "en": "This is not text."
46
+ },
47
+ "required": {
48
+ "ar": "هذا الحقل مطلوب.",
49
+ "en": "This field is required."
50
+ }
51
+ },
52
+ "idToken": {
53
+ "invalid": {
54
+ "ar": "هذا ليس نصا.",
55
+ "en": "This is not text."
56
+ },
57
+ "required": {
58
+ "ar": "هذا الحقل مطلوب.",
59
+ "en": "This field is required."
60
+ }
61
+ },
62
+ "password": {
63
+ "invalid": {
64
+ "ar": "هذا ليس نصا.",
65
+ "en": "This is not text."
66
+ },
67
+ "required": {
68
+ "ar": "هذا الحقل مطلوب.",
69
+ "en": "This field is required."
70
+ }
71
+ },
72
+ "providerId": {
73
+ "invalid": {
74
+ "ar": "هذا ليس نصا.",
75
+ "en": "This is not text."
76
+ },
77
+ "required": {
78
+ "ar": "هذا الحقل مطلوب.",
79
+ "en": "This field is required."
80
+ }
81
+ },
82
+ "refreshToken": {
83
+ "invalid": {
84
+ "ar": "هذا ليس نصا.",
85
+ "en": "This is not text."
86
+ },
87
+ "required": {
88
+ "ar": "هذا الحقل مطلوب.",
89
+ "en": "This field is required."
90
+ }
91
+ },
92
+ "refreshTokenExpiresAt": {
93
+ "invalid": {
94
+ "ar": "هذا ليس تاريخا ووقتا.",
95
+ "en": "This is not a date and time."
96
+ },
97
+ "required": {
98
+ "ar": "هذا الحقل مطلوب.",
99
+ "en": "This field is required."
100
+ }
101
+ },
102
+ "scope": {
103
+ "invalid": {
104
+ "ar": "هذا ليس نصا.",
105
+ "en": "This is not text."
106
+ },
107
+ "required": {
108
+ "ar": "هذا الحقل مطلوب.",
109
+ "en": "This field is required."
110
+ }
111
+ },
112
+ "updatedAt": {
113
+ "invalid": {
114
+ "ar": "هذا ليس تاريخا ووقتا.",
115
+ "en": "This is not a date and time."
116
+ },
117
+ "required": {
118
+ "ar": "هذا الحقل مطلوب.",
119
+ "en": "This field is required."
120
+ }
121
+ },
122
+ "userId": {
123
+ "invalid": {
124
+ "ar": "اختر عنصرًا من القائمة.",
125
+ "en": "Pick one of the listed entries."
126
+ },
127
+ "required": {
128
+ "ar": "هذا الحقل مطلوب.",
129
+ "en": "This field is required."
130
+ }
131
+ }
132
+ }