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,13 @@
1
+ /**
2
+ * The keys this project adds beside a kayan's own operations, by kayan. Each
3
+ * joins the catalog as `<kayan>.<key>`, held by a role like any operation's key,
4
+ * and the dashboard's role form offers it among that kayan's operations,
5
+ * labelled by the kayan's copy under `operations.<key>`. It is plain data,
6
+ * imported by the server and by the role form alike.
7
+ */
8
+ export const projectStatements = {
9
+ // add-on: courses
10
+ // Every seat of the organization rather than the actor's own.
11
+ enrollments: ['others'],
12
+ // end add-on: courses
13
+ } as const satisfies Record<string, readonly string[]>
@@ -0,0 +1,16 @@
1
+ // The subpath, so a page piece reading the fixed roles' labels loads no server code.
2
+ import { backofficeRoles } from '../mithaq/auth/backoffice-roles.ts'
3
+ import type { SystemRoles } from 'mithaq'
4
+ import { tenantRoles } from './tenant-roles.ts'
5
+
6
+ export { tenantRoles }
7
+
8
+ /**
9
+ * Every role this project ships, the backoffice's and the tenants', which is what
10
+ * the access target takes whole: it holds a slug's keys wherever the project's
11
+ * reader answers it. The backoffice roles are the auth setup's, because the column
12
+ * they live on is Better Auth's own `user.role` and the admin plugin's roles are
13
+ * built from the same lists. The reader answers them in the backoffice app alone,
14
+ * and an assignment never names one.
15
+ */
16
+ export const systemRoles = { ...backofficeRoles, ...tenantRoles } as const satisfies SystemRoles
@@ -0,0 +1,260 @@
1
+ import { onBackoffice, backofficeApp, backofficeRoles, backofficeURL } from '../mithaq/auth/index.ts'
2
+ import { secret } from '../mithaq/auth/backoffice-roles.ts'
3
+ import { actorPermissions, actorRoles, authActor, userRefs, type Held, type RowTable, type Store } from 'mithaq'
4
+ // add-on: roles
5
+ import { isOwner, OWNER_ROLE } from './grants.ts'
6
+ // end add-on: roles
7
+
8
+ /**
9
+ * What a row may name on the actor: `user.<key>` in any kayan's row is checked
10
+ * against these keys. The active organization is a string because a row that
11
+ * resolves it to anything else throws rather than filtering.
12
+ */
13
+ export type OrganizationActor = {
14
+ activeOrganizationId: string
15
+ /** The person's own id, which a field defaults to when the row is the actor's own. */
16
+ id: string
17
+ /**
18
+ * Better Auth's own `member.role`: governance only (settings, members,
19
+ * invitations, the provider's `owner` override). Optional because a local-auth
20
+ * actor is no organization's member.
21
+ */
22
+ memberRole?: string
23
+ /** The real person behind an impersonated session, so the audit handler names who really made the write. */
24
+ impersonatedBy?: string
25
+ organizationIds?: readonly string[]
26
+ }
27
+
28
+ /**
29
+ * The backoffice administrator's slug. Backoffice roles live on Better Auth's
30
+ * `user.role`, never in a tenant's assignment: `assignedRoles` drops any such
31
+ * slug there, because the access target holds a slug's keys only where this
32
+ * reader answers it.
33
+ */
34
+ export const BACKOFFICE = 'super_admin'
35
+
36
+ /**
37
+ * The backoffice screen, the only place the backoffice roles answer. The app decides
38
+ * this, never the request.
39
+ */
40
+ export const BACKOFFICE_SCREEN = 'backoffice'
41
+
42
+ /**
43
+ * The kayan whose session carries the backoffice column, the one `auth.admin`
44
+ * names. A build may run several Better Auth instances, and only this one's
45
+ * `user.role` is the layer above every tenant.
46
+ */
47
+ const BACKOFFICE_KAYAN = 'user'
48
+
49
+ /**
50
+ * Backoffice roles read every tenant's rows and write none; staff change a
51
+ * tenant's data only by impersonating a member, which is audited. Every write is
52
+ * an explicit `false`, because a slug a table leaves out still takes the `'*'`
53
+ * row, which opens every write. The row opens the door and the keys decide who
54
+ * walks through it.
55
+ */
56
+ const readOnly = { create: false, read: true, update: false, delete: false } as const
57
+
58
+ /** The read-only row under every backoffice role, which every row table of this project spreads in. */
59
+ export const backofficeReads = Object.fromEntries(Object.keys(backofficeRoles).map((slug) => [slug, readOnly])) as {
60
+ readonly [Slug in keyof typeof backofficeRoles]: typeof readOnly
61
+ }
62
+
63
+ /**
64
+ * Every operation refused under every backoffice role, spread in place of
65
+ * `backofficeReads` by tables the backoffice does not read, such as the settings
66
+ * global and the audit log.
67
+ */
68
+ const closed = { create: false, read: false, update: false, delete: false } as const
69
+ export const backofficeClosed = Object.fromEntries(Object.keys(backofficeRoles).map((slug) => [slug, closed])) as {
70
+ readonly [Slug in keyof typeof backofficeRoles]: typeof closed
71
+ }
72
+
73
+ /**
74
+ * The accessor every row of this project is written with. The actor is typed
75
+ * here once because a kayan's table is written before the language is imported
76
+ * into its file.
77
+ */
78
+ export const user = userRefs<OrganizationActor>()
79
+
80
+ /**
81
+ * The organization the actor's session has active, read off the trusted
82
+ * snapshot and checked against the provider's own list, or `null`.
83
+ */
84
+ export const currentOrganization = (user: unknown) => {
85
+ const trusted = authActor(user)
86
+ if (!trusted || trusted.strategy !== 'better-auth') return null
87
+ const active = trusted.activeOrganizationId
88
+ const organizations = trusted.organizations
89
+ if (typeof active !== 'string' || !Array.isArray(organizations)) return null
90
+ return organizations.some((organization) => organization.id === active) ? active : null
91
+ }
92
+
93
+ /**
94
+ * The backoffice rows of the tenant-owned kayan: `backofficeReads` minus the
95
+ * columns `secret` names. A row's field list closes a column whatever keys the
96
+ * actor holds, and a column an actor may not read is no filter either.
97
+ */
98
+ const readsNoSecret = { ...readOnly, fields: ['*', ...secret.map((key) => `!${key.slice(key.indexOf('.fields.') + '.fields.'.length)}`)] as readonly string[] }
99
+ const secretClosed = Object.fromEntries(Object.keys(backofficeRoles).map((slug) => [slug, readsNoSecret])) as {
100
+ readonly [Slug in keyof typeof backofficeRoles]: typeof readsNoSecret
101
+ }
102
+
103
+ /**
104
+ * The rows every tenant-owned kayan inherits. The organization is core's: a
105
+ * read, a write and a reference reach the session's active organization alone,
106
+ * whatever a row here answers. `'*'` is the row every actor assigned a role
107
+ * takes beside its roles' own, so a custom role's keys decide what it reaches;
108
+ * an actor assigned no role takes no row and is denied.
109
+ *
110
+ * No row here names `permissions`: what a role holds is the access target's
111
+ * business, and a second answer here would let a role row widen.
112
+ */
113
+ export const tenantAccess = {
114
+ '*': { create: true, read: true, update: true, delete: true },
115
+ // add-on: courses
116
+ /**
117
+ * The one row keyed on a system slug, and it narrows: an instructor teaches and
118
+ * never removes a row. Every operation is named in full, because a missing one
119
+ * reads as `false` and a `false` in any applicable row wins.
120
+ */
121
+ instructor: { create: true, read: true, update: true, delete: false },
122
+ // end add-on: courses
123
+ ...secretClosed,
124
+ } as const satisfies RowTable
125
+
126
+ /**
127
+ * The roles a kayan's rows are read through; its parameter also types `user` for
128
+ * every row this project writes. Slugs come from the access target for the
129
+ * active organization, off the trusted snapshot, so an object naming its own
130
+ * roles is no one.
131
+ */
132
+ export const assignedRoles = (actor: OrganizationActor): readonly string[] => {
133
+ if (!authActor(actor)) return []
134
+ // The backoffice app answers the backoffice roles alone and the tenant app the
135
+ // membership's roles alone, never unioned, so neither adds keys on the other's
136
+ // screens. Which app serves the request is the app's own fact, never the request's.
137
+ if (onBackoffice()) return backofficeRolesOf(actor)
138
+ if (!currentOrganization(actor)) return []
139
+ const assigned = (actorRoles(actor) ?? []).filter((slug) => !Object.hasOwn(backofficeRoles, slug))
140
+ // add-on: roles
141
+ // The slugs are the `memberRoles` kayan's, and `member.role` grants one thing
142
+ // only: an owner always holds `administrator` in its tenant, so the role that
143
+ // manages roles is never lost.
144
+ if (isOwner(authActor(actor)?.memberRole) && !assigned.includes(OWNER_ROLE)) return [...assigned, OWNER_ROLE]
145
+ // end add-on: roles
146
+ return assigned
147
+ }
148
+
149
+ /**
150
+ * Whether `assignedRoles` answers this actor `super_admin`, which only the
151
+ * backoffice app does. It is the administrator of `auth.admin`, the one answer
152
+ * local auth's `_verified` writes ask, and the reader and writer of local accounts.
153
+ */
154
+ export const superAdmin = ({ user }: { user: unknown }) => assignedRoles(user as OrganizationActor).includes(BACKOFFICE)
155
+
156
+ /**
157
+ * Every operation refused to every actor, the administrator included, for the
158
+ * tables Better Auth alone reads and writes through its adapter. Without it those
159
+ * kayan would fall to the administrator's default access, and a super admin would
160
+ * read every session token and password hash.
161
+ */
162
+ export const providerOnly = { create: () => false, read: () => false, update: () => false, delete: () => false } as const
163
+
164
+ /**
165
+ * The backoffice roles this actor holds on Better Auth's `user.role`, whichever app
166
+ * asks. Several are comma-separated, and only a backoffice role's name counts.
167
+ *
168
+ * The column is read off the trusted snapshot, and only from the admin kayan's
169
+ * session on a Better Auth strategy: a build may run several instances that all
170
+ * write `user.role`, and local auth spreads its kayan's row onto the actor, so a
171
+ * `role` column there would otherwise hand someone a backoffice role.
172
+ */
173
+ export const backofficeRolesOf = (actor: unknown): string[] => {
174
+ const trusted = authActor(actor)
175
+ if (!trusted || trusted.strategy !== 'better-auth' || trusted.collection !== BACKOFFICE_KAYAN || typeof trusted.role !== 'string') return []
176
+ return [...new Set(trusted.role.split(',').map((slug) => slug.trim()).filter((slug) => Object.hasOwn(backofficeRoles, slug)))]
177
+ }
178
+
179
+ /**
180
+ * Where the tenant app's header links the backoffice app, for an actor whose
181
+ * backoffice roles hold `screen.backoffice`; nothing otherwise. It is a link and
182
+ * grants nothing: the backoffice app's own server decides entry. A project that
183
+ * serves no backoffice app links nowhere.
184
+ */
185
+ export const backofficeEntry = (actor: unknown): string | undefined => {
186
+ if (onBackoffice() || !backofficeApp) return undefined
187
+ const enters = backofficeRolesOf(actor).some((slug) => {
188
+ const held = backofficeRoles[slug as keyof typeof backofficeRoles].permissions
189
+ return held === '*' || (held as readonly string[]).includes(`screen.${BACKOFFICE_SCREEN}`)
190
+ })
191
+ return enters ? new URL(`/${BACKOFFICE_SCREEN}`, backofficeURL).href : undefined
192
+ }
193
+
194
+ /**
195
+ * The keys a provider kayan's columns are graded by, which `rows()` takes as its
196
+ * `holds`: in the tenant app the id and the name alone, and only where the actor
197
+ * holds their keys; in the backoffice app the actor's own keys. An actor holding no
198
+ * key reads no column, since an empty set grades every column closed where
199
+ * `undefined` would grade none.
200
+ */
201
+ export const namesInTenant = (kayan: string) => (actor: unknown): Held => {
202
+ const held = actorPermissions(actor)
203
+ if (onBackoffice()) return held
204
+ return new Set(['id', 'name'].map((field) => `${kayan}.fields.${field}`).filter((key) => held === true || held?.has(key) === true))
205
+ }
206
+
207
+ /**
208
+ * Whom a role reads among its organization's people when it holds no
209
+ * `member.find`: the members assigned one of the roles named here, beside
210
+ * itself. A role left out, a custom role included, reads itself alone, so a
211
+ * key to read people never by itself opens the staff to a portal role.
212
+ */
213
+ export const peopleAudience: Readonly<Record<string, readonly string[]>> = {
214
+ // add-on: courses
215
+ // The students it enrols, never the staff.
216
+ instructor: ['student'],
217
+ // end add-on: courses
218
+ }
219
+
220
+ /**
221
+ * The read the person's own rows are ANDed with, so it narrows and never widens:
222
+ * the members of the organization the actor's session has active, read off the
223
+ * store, all of them to a holder of `member.find`, who lists them through the
224
+ * provider anyway, and otherwise the actor and its roles' `peopleAudience`. The
225
+ * organization is the trusted snapshot's, so a claim is no one's.
226
+ */
227
+ export const membersOf = (store: Pick<Store, 'find'>) => async ({ user: actor }: { user: unknown }) => {
228
+ // A backoffice role reads every person in the backoffice app; the rows answer who holds one.
229
+ if (onBackoffice()) return true
230
+ const active = currentOrganization(actor)
231
+ if (!active) return false
232
+ const all = { limit: 0, page: 1, sort: { field: 'id', direction: 'asc' } } as const
233
+ const members = await store.find({ kayan: 'member', args: { ...all, where: { organizationId: { equals: active } } } })
234
+ const ids = members.docs.map((row) => row.userId).filter((id): id is string => typeof id === 'string')
235
+ const held = actorPermissions(actor)
236
+ if (held === true || held?.has('member.find')) return ids.length ? { id: { in: ids } } : false
237
+ const seen = new Set([authActor(actor)?.id])
238
+ // add-on: roles
239
+ // A custom role's name is a tenant's to write, so one named `constructor` reads no inherited key.
240
+ const audience =[...new Set(assignedRoles(actor as OrganizationActor).flatMap((slug) => (Object.hasOwn(peopleAudience, slug) ? peopleAudience[slug]! : [])))]
241
+ if (audience.length) {
242
+ const assigned = await store.find({ kayan: 'memberRoles', args: { ...all, where: { organization: { equals: active }, role: { in: audience } } } })
243
+ for (const row of assigned.docs) seen.add(row.user)
244
+ }
245
+ // end add-on: roles
246
+ // An assignment can outlive its membership, so only a current member is seen.
247
+ const reached = ids.filter((id) => seen.has(id))
248
+ return reached.length ? { id: { in: reached } } : false
249
+ }
250
+
251
+ /**
252
+ * Whether an actor reads past what a kayan's own read narrows for roles not
253
+ * holding `key`: in the backoffice app, whose staff read every tenant's rows whole,
254
+ * or in a tenant, holding that key. The courses add-on narrows archived courses,
255
+ * sessions and covers of unreadable courses and other people's seats this way.
256
+ */
257
+ export const readsPast = (actor: unknown, key: string) => {
258
+ const held = actorPermissions(actor)
259
+ return onBackoffice() || held === true || held?.has(key) === true
260
+ }
@@ -0,0 +1,321 @@
1
+ import type { SystemRoles } from 'mithaq'
2
+ // without add-on: roles
3
+ // import { defaultMemberRoles } from './default-member-roles.ts'
4
+ // end without add-on: roles
5
+
6
+ // add-on: courses
7
+ /** A session's columns but its `enrollments`, which the registrar and the instructor read alike. */
8
+ const sessionColumns = [
9
+ 'sessions.fields.id',
10
+ 'sessions.fields.createdAt',
11
+ 'sessions.fields.updatedAt',
12
+ 'sessions.fields.organization',
13
+ 'sessions.fields.course',
14
+ 'sessions.fields.startsAt',
15
+ 'sessions.fields.capacity',
16
+ 'sessions.fields.venue',
17
+ 'sessions.fields.format',
18
+ 'sessions.fields.archived',
19
+ 'sessions.fields.archiveReason',
20
+ 'sessions.fields.details',
21
+ ] as const
22
+
23
+ /**
24
+ * What the portal's catalogue and course page read beside a course's own columns, for its roles alike: the
25
+ * sessions with the seats each has left, and the names of the tags it carries.
26
+ */
27
+ const catalogueReads = [
28
+ 'courses.findWithSessions',
29
+ 'courses.fields.tags',
30
+ 'courses.fields.selfEnroll',
31
+ 'tags.find',
32
+ 'tags.findByID',
33
+ 'tags.fields.id',
34
+ 'tags.fields.name',
35
+ ] as const
36
+
37
+ // add-on: settings
38
+ /** Whether the portal is closed for maintenance, and where to write, which the portal's foot reads for its roles alike. */
39
+ const portalSettings = ['settings.findOne', 'settings.fields.maintenance', 'settings.fields.contactEmail'] as const
40
+ // end add-on: settings
41
+
42
+ /** A course's cover, read and never changed, which the portal's roles read alike. */
43
+ const coverReads = [
44
+ 'media.find',
45
+ 'media.findByID',
46
+ 'media.fields.alt',
47
+ 'media.fields.createdAt',
48
+ 'media.fields.filename',
49
+ 'media.fields.filesize',
50
+ 'media.fields.height',
51
+ 'media.fields.id',
52
+ 'media.fields.key',
53
+ 'media.fields.mimeType',
54
+ 'media.fields.storage',
55
+ 'media.fields.organization',
56
+ 'media.fields.updatedAt',
57
+ 'media.fields.width',
58
+ ] as const
59
+ // end add-on: courses
60
+
61
+ /**
62
+ * The roles this project ships to every tenant, written in code and never a
63
+ * row: the access target takes them whole and reads what each holds from here.
64
+ * A page lists them from here too, which is why this file imports no server
65
+ * code.
66
+ *
67
+ * A slug here is the whole vocabulary of role names: a row table may be keyed on
68
+ * one of them or on `'*'` and on nothing else, and `buildAsas` refuses a key
69
+ * that is not in this project's catalog exactly as it does for a row's own.
70
+ *
71
+ * The backoffice roles, `super_admin` among them, are not here: they are staff
72
+ * above every tenant, held on Better Auth's `user.role`, and the access target
73
+ * holds their keys in the backoffice app alone. Inside a tenant every actor, a
74
+ * `super_admin` included, holds what its membership's roles hold and nothing
75
+ * more.
76
+ */
77
+ export const tenantRoles = {
78
+ // add-on: roles
79
+ // These are business roles and never governance, and the `roles` kayan holds
80
+ // a tenant's own roles beside them; an assignment names one of either. Better
81
+ // Auth's own `member.role`, which holds `owner`, `admin` or `member`, stays
82
+ // what the provider uses for settings, members and invitations, and nothing
83
+ // here reads it. Sharing one comma-joined column would inherit the provider's
84
+ // own open comma-in-a-role-name bug.
85
+ //
86
+ // A new organization's creator is assigned `administrator` while the
87
+ // organization is being created, by the hook the template wires into the
88
+ // provider, so a fresh tenant has a root granter from its first moment. That
89
+ // row is written straight through the store, because a managed create is
90
+ // graded against the actor making it and the creator is in no organization
91
+ // until that route puts it in one.
92
+ /**
93
+ * The tenant's root granter: every key of the project, which is what lets it
94
+ * author a custom role at all. `'*'` holds every screen's key too, so it
95
+ * enters the dashboard and the portal alike. It never reaches the backoffice
96
+ * app, where the reader answers no membership role at all.
97
+ * `grantedPermissions` returns early on a granter holding everything, so this
98
+ * role and no other may hand out a key it will never use itself.
99
+ */
100
+ administrator: {
101
+ label: { ar: 'مدير', en: 'Administrator' },
102
+ permissions: '*',
103
+ },
104
+ /** Runs the catalogue in the dashboard: the courses, their sessions and the tags on them. */
105
+ registrar: {
106
+ label: { ar: 'مسجل', en: 'Registrar' },
107
+ permissions: [
108
+ 'screen.dashboard',
109
+ // add-on: courses
110
+ 'courses.create',
111
+ 'courses.find',
112
+ 'courses.count',
113
+ 'courses.findByID',
114
+ 'courses.update',
115
+ // A course leaves the catalogue and comes back through its own operations, each with its reason.
116
+ 'courses.archive',
117
+ 'courses.unarchive',
118
+ 'courses.fields.archived',
119
+ 'courses.fields.archiveReason',
120
+ 'courses.fields.createdAt',
121
+ 'courses.fields.cover',
122
+ 'courses.fields.currency',
123
+ 'courses.fields.details',
124
+ 'courses.fields.fee',
125
+ 'courses.fields.id',
126
+ 'courses.fields.tags',
127
+ 'courses.fields.organization',
128
+ 'courses.fields.title',
129
+ 'courses.fields.updatedAt',
130
+ 'courses.fields.selfEnroll',
131
+ // Whether a course is a draft, its versions and the restore of one: the
132
+ // registrar edits the catalogue, so it reads a course's history and puts
133
+ // an older copy back.
134
+ 'courses.fields._status',
135
+ 'courses.findVersions',
136
+ 'courses.findVersionByID',
137
+ 'courses.restoreVersion',
138
+ 'sessions.create',
139
+ 'sessions.find',
140
+ 'sessions.count',
141
+ 'sessions.findByID',
142
+ 'sessions.update',
143
+ // The columns of each kayan it holds an operation of, without which every
144
+ // read of it is refused for its fields and its pages draw nothing. Not a
145
+ // session's `enrollments`, which reads a kayan this role does not.
146
+ ...sessionColumns,
147
+ // end add-on: courses
148
+ 'tags.find',
149
+ 'tags.count',
150
+ 'tags.findByID',
151
+ 'tags.fields.id',
152
+ 'tags.fields.createdAt',
153
+ 'tags.fields.updatedAt',
154
+ 'tags.fields.organization',
155
+ 'tags.fields.name',
156
+ // add-on: media
157
+ // The images a course's cover is picked from, which it uploads,
158
+ // re-describes and deletes once no course points at one.
159
+ 'media.create',
160
+ 'media.find',
161
+ 'media.count',
162
+ 'media.findByID',
163
+ 'media.update',
164
+ 'media.delete',
165
+ 'media.fields.alt',
166
+ 'media.fields.createdAt',
167
+ 'media.fields.filename',
168
+ 'media.fields.filesize',
169
+ 'media.fields.height',
170
+ 'media.fields.id',
171
+ 'media.fields.key',
172
+ 'media.fields.mimeType',
173
+ 'media.fields.storage',
174
+ 'media.fields.organization',
175
+ 'media.fields.updatedAt',
176
+ 'media.fields.width',
177
+ // end add-on: media
178
+ // add-on: audit-log
179
+ // The activity panel on each edit page it opens: the entries of the
180
+ // kayan it lists, each change graded by the column's own key above.
181
+ 'auditLog.find',
182
+ 'auditLog.fields.id',
183
+ 'auditLog.fields.createdAt',
184
+ 'auditLog.fields.kayan',
185
+ 'auditLog.fields.record',
186
+ 'auditLog.fields.operation',
187
+ 'auditLog.fields.actor',
188
+ 'auditLog.fields.impersonatedBy',
189
+ 'auditLog.fields.changes',
190
+ // end add-on: audit-log
191
+ ],
192
+ },
193
+ // add-on: courses
194
+ /**
195
+ * Teaches: reads the catalogue and enrols. It holds no `courses.archive`. It
196
+ * works in the portal, which draws the courses and the enrolments, and never
197
+ * enters the dashboard.
198
+ */
199
+ instructor: {
200
+ label: { ar: 'مدرس', en: 'Instructor' },
201
+ permissions: [
202
+ 'screen.portal',
203
+ 'courses.find',
204
+ 'courses.count',
205
+ 'courses.findByID',
206
+ 'courses.fields.archived',
207
+ 'courses.fields.createdAt',
208
+ 'courses.fields.cover',
209
+ 'courses.fields.details',
210
+ 'courses.fields.id',
211
+ 'courses.fields.organization',
212
+ 'courses.fields.title',
213
+ 'courses.fields.updatedAt',
214
+ // What the portal's catalogue draws on a course's card and its page.
215
+ 'courses.fields.summary',
216
+ 'courses.fields.fee',
217
+ 'courses.fields.currency',
218
+ 'courses.fields.delivery',
219
+ // It reads published courses alone, and whether one is published with them.
220
+ 'courses.fields._status',
221
+ 'enrollments.create',
222
+ 'enrollments.find',
223
+ 'enrollments.count',
224
+ 'enrollments.findByID',
225
+ // Every seat of its organization, not only its own
226
+ 'enrollments.others',
227
+ // An enrollment's columns but the soft-delete mark, which the runner
228
+ // writes, and a session's, which the enrollment's picker and cell read
229
+ 'enrollments.fields.id',
230
+ 'enrollments.fields.createdAt',
231
+ 'enrollments.fields.updatedAt',
232
+ 'enrollments.fields.organization',
233
+ 'enrollments.fields.session',
234
+ 'enrollments.fields.student',
235
+ 'sessions.find',
236
+ 'sessions.count',
237
+ 'sessions.findByID',
238
+ ...sessionColumns,
239
+ // The students of its organization by their id and name alone, which the
240
+ // enrollment drawer's student picker lists and an enrollment's student is
241
+ // titled by: `peopleAudience` in `tenant-access.ts` names whom it reads.
242
+ 'user.find',
243
+ 'user.count',
244
+ 'user.findByID',
245
+ 'user.fields.id',
246
+ 'user.fields.name',
247
+ ...coverReads,
248
+ ...catalogueReads,
249
+ // add-on: settings
250
+ ...portalSettings,
251
+ // end add-on: settings
252
+ ],
253
+ },
254
+ /**
255
+ * Takes the courses: reads the catalogue in the portal, a course's cover
256
+ * with it, and its own enrollments with the sessions they name. Not the
257
+ * access code, which a course hands out to whoever it chooses, nor the
258
+ * archive reason or the notifications, which are staff's.
259
+ *
260
+ * A role's keys reach every row of the organization, so the enrollment keys
261
+ * alone would hand a student every other student's seat. The enrollments'
262
+ * own access narrows them to the student's own seats, since it holds no
263
+ * `enrollments.others`, and the sessions' to those of the courses it reads.
264
+ * It takes a seat for itself alone, in a course that lets its students enroll
265
+ * themselves and before the session starts, and gives one back before then:
266
+ * the enrollments' own steps hold it to that. It changes no seat.
267
+ */
268
+ student: {
269
+ label: { ar: 'طالب', en: 'Student' },
270
+ permissions: [
271
+ 'screen.portal',
272
+ 'courses.find',
273
+ 'courses.count',
274
+ 'courses.findByID',
275
+ 'courses.fields.id',
276
+ 'courses.fields.createdAt',
277
+ 'courses.fields.updatedAt',
278
+ 'courses.fields.organization',
279
+ 'courses.fields.title',
280
+ 'courses.fields.summary',
281
+ 'courses.fields.details',
282
+ 'courses.fields.cover',
283
+ 'courses.fields.fee',
284
+ 'courses.fields.currency',
285
+ 'courses.fields.level',
286
+ 'courses.fields.delivery',
287
+ 'courses.fields.audience',
288
+ 'courses.fields.opensOn',
289
+ 'courses.fields.archived',
290
+ 'courses.fields._status',
291
+ 'enrollments.find',
292
+ 'enrollments.count',
293
+ 'enrollments.findByID',
294
+ 'enrollments.create',
295
+ 'enrollments.delete',
296
+ 'enrollments.fields.id',
297
+ 'enrollments.fields.createdAt',
298
+ 'enrollments.fields.updatedAt',
299
+ 'enrollments.fields.organization',
300
+ 'enrollments.fields.session',
301
+ // Itself, which its own create fills and every seat it reads names.
302
+ 'enrollments.fields.student',
303
+ // The sessions its seats name, which the enrollment's session cell draws
304
+ // by course and start.
305
+ 'sessions.find',
306
+ 'sessions.count',
307
+ 'sessions.findByID',
308
+ ...sessionColumns,
309
+ ...coverReads,
310
+ ...catalogueReads,
311
+ // add-on: settings
312
+ ...portalSettings,
313
+ // end add-on: settings
314
+ ],
315
+ },
316
+ // end add-on: courses
317
+ // end add-on: roles
318
+ // without add-on: roles
319
+ // ...defaultMemberRoles,
320
+ // end without add-on: roles
321
+ } as const satisfies SystemRoles
@@ -0,0 +1,41 @@
1
+ // Refuses an update when the row changed after the caller read it. A kayan opts in by
2
+ // widening its update input and listing the step before the store step:
3
+ //
4
+ // update: {
5
+ // input: ({ update }) => update.extend({ expectedUpdatedAt: z.date() }),
6
+ // steps: ({ steps }) => [expectedUpdatedAt, steps.update],
7
+ // },
8
+ //
9
+ // Use `z.date()`: a string or `null` would not be a moment and would answer a misleading 409.
10
+ // Listing the step without widening the input protects nothing, since the strict update
11
+ // shape refuses the key by name.
12
+ //
13
+ // The compare is against `ctx.dataBefore`, loaded under the update's own row lock inside its
14
+ // transaction, so on a store that honours the lock a second writer waits and no write lands
15
+ // between the compare and the store step. A store that ignores the lock lets both through.
16
+ //
17
+ // updateMany runs this once per row with no instant per row: a kayan whose input requires the
18
+ // key refuses every updateMany, and one that leaves it optional runs the bulk unguarded.
19
+ //
20
+ // A kayan with no `updatedAt` field has nothing to compare, so listing the step on it throws.
21
+ //
22
+ // A project that copies this step adds the `error.conflict.stale` copy key in every locale.
23
+ import { conflict, type Ctx } from 'mithaq'
24
+
25
+ export const expectedUpdatedAt = (ctx: Ctx<{ expectedUpdatedAt?: Date | string }>): Ctx => {
26
+ if (!('updatedAt' in ctx.kayan.fields)) {
27
+ throw new Error(`Kayan "${ctx.kayan.name}" lists expectedUpdatedAt but has no updatedAt field, so the step cannot guard it. Add defaultTimestampFields or remove the step.`)
28
+ }
29
+ const { expectedUpdatedAt: expected } = ctx.data
30
+ const held = ctx.dataBefore?.updatedAt
31
+ if (expected === undefined || held === undefined) return ctx
32
+ if (new Date(expected).getTime() !== new Date(held as Date).getTime()) {
33
+ throw conflict({
34
+ message: `Kayan "${ctx.kayan.name}" row changed after it was read, so it is not updated. Read it again and send the change on the row as it is.`,
35
+ key: 'error.conflict.stale',
36
+ params: { kayan: ctx.kayan.name },
37
+ })
38
+ }
39
+ return ctx
40
+ }
41
+ // mithaq-admin 0.0.0 10cc4a57db299d48c4fd53314ed42e02fc04b0464a7fa8a0c512cefc7ccb2b82