create-mithaq 0.1.1 → 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 (348) hide show
  1. package/LICENSES/openstatus-time-picker.txt +21 -0
  2. package/index.js +457 -63
  3. package/package.json +1 -1
  4. package/template/apps/{asas-platform → asas-backoffice}/package.json +5 -3
  5. package/template/apps/{asas-platform → asas-backoffice}/src/api.ts +1 -1
  6. package/template/apps/{asas-platform → asas-backoffice}/src/auth-frame.tsx +12 -12
  7. package/template/apps/asas-backoffice/src/components.ts +35 -0
  8. package/template/apps/asas-backoffice/src/first-user.ts +14 -0
  9. package/template/apps/{asas-platform → asas-backoffice}/src/links.ts +1 -1
  10. package/template/apps/{asas-platform → asas-backoffice}/src/locale-switch.tsx +7 -5
  11. package/template/apps/asas-backoffice/src/no-organization.tsx +5 -0
  12. package/template/apps/asas-backoffice/src/redirect.ts +1 -0
  13. package/template/apps/{asas-platform → asas-backoffice}/src/request-headers.ts +1 -1
  14. package/template/apps/asas-backoffice/src/routeTree.gen.ts +206 -0
  15. package/template/apps/asas-backoffice/src/router.tsx +18 -0
  16. package/template/apps/asas-backoffice/src/routes/(screens)/backoffice/files.ts +10 -0
  17. package/template/apps/{asas-platform/src/routes/(screens)/platform → asas-backoffice/src/routes/(screens)/backoffice}/screen.json +426 -64
  18. package/template/apps/{asas-platform/src/routes/(screens)/platform → asas-backoffice/src/routes/(screens)/backoffice}/types.ts +18 -0
  19. package/template/apps/{asas-platform/src/routes/(screens)/platform.$.tsx → asas-backoffice/src/routes/(screens)/backoffice.$.tsx} +4 -4
  20. package/template/apps/{asas-platform/src/routes/(screens)/platform.index.tsx → asas-backoffice/src/routes/(screens)/backoffice.index.tsx} +3 -3
  21. package/template/apps/{asas-platform/src/routes/(screens)/platform.tsx → asas-backoffice/src/routes/(screens)/backoffice.tsx} +3 -3
  22. package/template/apps/{asas-platform → asas-backoffice}/src/routes/__root.tsx +2 -2
  23. package/template/apps/asas-backoffice/src/routes/api/$.ts +34 -0
  24. package/template/apps/asas-backoffice/src/routes/auth.create-first-user.tsx +113 -0
  25. package/template/apps/{asas-platform → asas-backoffice}/src/routes/auth.sign-in.tsx +9 -4
  26. package/template/apps/{asas-platform → asas-backoffice}/src/routes/unauthorized.tsx +24 -12
  27. package/template/apps/{asas-platform → asas-backoffice}/src/screen-form.tsx +5 -3
  28. package/template/apps/{asas-platform → asas-backoffice}/src/screen-list.tsx +6 -5
  29. package/template/apps/{asas-platform → asas-backoffice}/src/screen-names.ts +1 -1
  30. package/template/apps/{asas-platform → asas-backoffice}/src/screens.tsx +46 -26
  31. package/template/apps/asas-backoffice/src/server.ts +30 -0
  32. package/template/apps/{asas-platform → asas-backoffice}/src/slide.ts +1 -1
  33. package/template/apps/{asas-platform → asas-backoffice}/src/start.ts +1 -1
  34. package/template/apps/{asas-platform → asas-backoffice}/src/styles.css +9 -7
  35. package/template/apps/{asas-platform → asas-backoffice}/tsconfig.json +2 -2
  36. package/template/apps/{asas-platform → asas-backoffice}/vite.config.ts +7 -1
  37. package/template/apps/asas-tanstack/package.json +4 -2
  38. package/template/apps/asas-tanstack/src/api.ts +1 -1
  39. package/template/apps/asas-tanstack/src/auth-frame.tsx +12 -12
  40. package/template/apps/asas-tanstack/src/choose-password.tsx +150 -0
  41. package/template/apps/asas-tanstack/src/components.ts +35 -0
  42. package/template/apps/asas-tanstack/src/copy.ts +416 -79
  43. package/template/apps/asas-tanstack/src/first-run.ts +16 -0
  44. package/template/apps/asas-tanstack/src/links.ts +1 -1
  45. package/template/apps/asas-tanstack/src/locale-switch.tsx +7 -5
  46. package/template/apps/asas-tanstack/src/no-organization.tsx +108 -0
  47. package/template/apps/asas-tanstack/src/redirect.ts +20 -1
  48. package/template/apps/asas-tanstack/src/request-headers.ts +1 -1
  49. package/template/apps/asas-tanstack/src/routeTree.gen.ts +42 -0
  50. package/template/apps/asas-tanstack/src/router.tsx +12 -1
  51. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/files.ts +9 -2
  52. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/screen.json +475 -116
  53. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/types.ts +4 -2
  54. package/template/apps/asas-tanstack/src/routes/(screens)/portal/files.ts +7 -2
  55. package/template/apps/asas-tanstack/src/routes/(screens)/portal/screen.json +83 -319
  56. package/template/apps/asas-tanstack/src/routes/(screens)/portal/types.ts +3 -1
  57. package/template/apps/asas-tanstack/src/routes/__root.tsx +1 -1
  58. package/template/apps/asas-tanstack/src/routes/auth.accept-invitation.tsx +14 -7
  59. package/template/apps/asas-tanstack/src/routes/auth.first-run.tsx +114 -0
  60. package/template/apps/asas-tanstack/src/routes/auth.forgot-password.tsx +2 -2
  61. package/template/apps/asas-tanstack/src/routes/auth.handoff.ts +5 -5
  62. package/template/apps/asas-tanstack/src/routes/auth.reset-password.tsx +7 -100
  63. package/template/apps/asas-tanstack/src/routes/auth.set-password.tsx +16 -0
  64. package/template/apps/asas-tanstack/src/routes/auth.sign-in.tsx +27 -15
  65. package/template/apps/asas-tanstack/src/routes/auth.sign-up.tsx +59 -38
  66. package/template/apps/asas-tanstack/src/routes/index.tsx +18 -14
  67. package/template/apps/asas-tanstack/src/routes/unauthorized.tsx +24 -12
  68. package/template/apps/asas-tanstack/src/screen-form.tsx +5 -3
  69. package/template/apps/asas-tanstack/src/screen-list.tsx +6 -5
  70. package/template/apps/asas-tanstack/src/screen-names.ts +1 -1
  71. package/template/apps/asas-tanstack/src/screens.tsx +46 -26
  72. package/template/apps/asas-tanstack/src/server.ts +10 -0
  73. package/template/apps/asas-tanstack/src/slide.ts +1 -1
  74. package/template/apps/asas-tanstack/src/styles.css +7 -3
  75. package/template/apps/asas-tanstack/tsconfig.json +2 -2
  76. package/template/apps/asas-tanstack/vite.config.ts +7 -1
  77. package/template/package.json +3 -3
  78. package/template/templates/asas/.claude/skills/add-field/SKILL.md +27 -0
  79. package/template/templates/asas/.claude/skills/add-kayan/SKILL.md +26 -0
  80. package/template/templates/asas/.claude/skills/eject-admin-piece/SKILL.md +28 -0
  81. package/template/templates/asas/.claude/skills/upgrade-mithaq/SKILL.md +35 -0
  82. package/template/templates/asas/.env.example +22 -5
  83. package/template/templates/asas/AGENTS.md +101 -0
  84. package/template/templates/asas/CLAUDE.md +1 -0
  85. package/template/templates/asas/README.md +1056 -0
  86. package/template/templates/asas/package.json +19 -4
  87. package/template/templates/asas/src/asas.ts +81 -56
  88. package/template/templates/asas/src/{platform-http.ts → backoffice-http.ts} +48 -27
  89. package/template/templates/asas/src/dev-setup.ts +112 -0
  90. package/template/templates/asas/src/local-database.ts +22 -0
  91. package/template/templates/asas/src/messages/account.json +129 -103
  92. package/template/templates/asas/src/messages/auditLog.json +223 -105
  93. package/template/templates/asas/src/messages/backofficeLog.json +132 -0
  94. package/template/templates/asas/src/messages/courses.json +256 -214
  95. package/template/templates/asas/src/messages/enrollments.json +45 -27
  96. package/template/templates/asas/src/messages/invitation.json +72 -56
  97. package/template/templates/asas/src/messages/localUsers.json +34 -28
  98. package/template/templates/asas/src/messages/media.json +31 -21
  99. package/template/templates/asas/src/messages/member.json +59 -39
  100. package/template/templates/asas/src/messages/memberRoles.json +37 -31
  101. package/template/templates/asas/src/messages/mithaq.json +206 -162
  102. package/template/templates/asas/src/messages/organization.json +63 -41
  103. package/template/templates/asas/src/messages/organizationRole.json +59 -47
  104. package/template/templates/asas/src/messages/roles.json +59 -45
  105. package/template/templates/asas/src/messages/session.json +99 -79
  106. package/template/templates/asas/src/messages/sessions.json +110 -80
  107. package/template/templates/asas/src/messages/settings.json +51 -53
  108. package/template/templates/asas/src/messages/tags.json +31 -21
  109. package/template/templates/asas/src/messages/user.json +109 -87
  110. package/template/templates/asas/src/messages/verification.json +59 -47
  111. package/template/templates/asas/src/migrations/20261014221355_tags_restrict/migration.sql +1 -0
  112. package/template/templates/asas/src/migrations/20261014221355_tags_restrict/snapshot.json +3881 -0
  113. package/template/templates/asas/src/migrations/20261014222218_one_seat_per_session/migration.sql +1 -0
  114. package/template/templates/asas/src/migrations/20261014222218_one_seat_per_session/snapshot.json +3894 -0
  115. package/template/templates/asas/src/migrations/20261014223522_session_archive_reason/migration.sql +1 -0
  116. package/template/templates/asas/src/migrations/20261014223522_session_archive_reason/snapshot.json +3907 -0
  117. package/template/templates/asas/src/migrations/20261014225728_course_archive_by_operation/migration.sql +1 -0
  118. package/template/templates/asas/src/migrations/20261014225728_course_archive_by_operation/snapshot.json +3907 -0
  119. package/template/templates/asas/src/migrations/20261015042444_organization_names/migration.sql +1 -0
  120. package/template/templates/asas/src/migrations/20261015042444_organization_names/snapshot.json +3920 -0
  121. package/template/templates/asas/src/migrations/20261015053212_member_last_seen/migration.sql +1 -0
  122. package/template/templates/asas/src/migrations/20261015053212_member_last_seen/snapshot.json +3933 -0
  123. package/template/templates/asas/src/migrations/20261015093203_activity_and_staff_history/migration.sql +15 -0
  124. package/template/templates/asas/src/migrations/20261015093203_activity_and_staff_history/snapshot.json +4113 -0
  125. package/template/templates/asas/src/migrations/20261015095752_portal/migration.sql +2 -0
  126. package/template/templates/asas/src/migrations/20261015095752_portal/snapshot.json +4113 -0
  127. package/template/templates/asas/src/migrations/20261015135529_better_auth_indexes/migration.sql +9 -0
  128. package/template/templates/asas/src/migrations/20261015135529_better_auth_indexes/snapshot.json +4302 -0
  129. package/template/templates/asas/src/migrations/20261015162438_audit_changes_json/migration.sql +27 -0
  130. package/template/templates/asas/src/migrations/20261015162438_audit_changes_json/snapshot.json +4289 -0
  131. package/template/templates/asas/src/migrations/schema.ts +1 -0
  132. package/template/templates/asas/src/mithaq.ts +4 -406
  133. package/template/templates/asas/src/organizations.ts +3 -0
  134. package/template/templates/asas/src/seed.ts +147 -45
  135. package/template/templates/asas/src/{ui-platform → ui-backoffice}/home.tsx +9 -9
  136. package/template/templates/asas/src/ui-backoffice/organization-actions.tsx +121 -0
  137. package/template/templates/asas/src/{ui-platform → ui-backoffice}/record-page.tsx +4 -4
  138. package/template/templates/asas/src/{ui-platform → ui-backoffice}/role-cells.tsx +13 -6
  139. package/template/templates/asas/src/{ui-platform → ui-backoffice}/user-actions.tsx +46 -48
  140. package/template/templates/asas/src/ui-dashboard/dashboard.tsx +10 -4
  141. package/template/templates/asas/src/ui-dashboard/home.tsx +73 -0
  142. package/template/templates/asas/src/ui-dashboard/members-card.tsx +147 -0
  143. package/template/templates/asas/src/ui-dashboard/members.tsx +637 -438
  144. package/template/templates/asas/src/ui-dashboard/versions.tsx +22 -8
  145. package/template/templates/asas/src/ui-portal/courses-home.tsx +96 -0
  146. package/template/templates/asas/src/ui-portal/home.tsx +9 -13
  147. package/template/templates/features/audit-log/audit-log/audit-log.ts +284 -0
  148. package/template/templates/features/audit-log/backoffice-log/backoffice-log.ts +67 -0
  149. package/template/templates/features/audit-log/index.ts +4 -0
  150. package/template/templates/features/audit-log/package.json +13 -0
  151. package/template/templates/features/auth/index.ts +11 -0
  152. package/template/templates/{asas/src → features/auth}/local-users/local-users.ts +7 -7
  153. package/template/templates/{auth → features/auth}/package.json +2 -3
  154. package/template/templates/{asas/src/kayan → features}/auth/user/account.access.ts +2 -2
  155. package/template/templates/{asas/src/kayan → features}/auth/user/account.ts +2 -2
  156. package/template/templates/{asas/src/kayan → features}/auth/user/invitation.access.ts +1 -1
  157. package/template/templates/{asas/src/kayan → features}/auth/user/invitation.ts +3 -3
  158. package/template/templates/{asas/src/kayan → features}/auth/user/member.access.ts +4 -4
  159. package/template/templates/{asas/src/kayan → features}/auth/user/member.screens.ts +5 -5
  160. package/template/templates/{asas/src/kayan → features}/auth/user/member.ts +4 -3
  161. package/template/templates/{asas/src/kayan → features}/auth/user/organization.access.ts +6 -6
  162. package/template/templates/{asas/src/kayan → features}/auth/user/organization.screens.ts +6 -6
  163. package/template/templates/{asas/src/kayan → features}/auth/user/organization.ts +2 -1
  164. package/template/templates/{asas/src/kayan → features}/auth/user/organizationRole.access.ts +2 -2
  165. package/template/templates/{asas/src/kayan → features}/auth/user/organizationRole.ts +3 -3
  166. package/template/templates/{asas/src/kayan → features}/auth/user/session.access.ts +2 -2
  167. package/template/templates/{asas/src/kayan → features}/auth/user/session.ts +2 -2
  168. package/template/templates/{asas/src/kayan → features}/auth/user/user.access.ts +8 -7
  169. package/template/templates/{asas/src/kayan → features}/auth/user/user.screens.ts +9 -9
  170. package/template/templates/{asas/src/kayan → features}/auth/user/user.ts +2 -2
  171. package/template/templates/{asas/src/kayan → features}/auth/user/verification.access.ts +2 -2
  172. package/template/templates/{asas/src/kayan → features}/auth/user/verification.ts +2 -2
  173. package/template/templates/{asas/src → features/courses}/courses/courses.ts +122 -133
  174. package/template/templates/features/courses/courses/cover-reads.ts +33 -0
  175. package/template/templates/features/courses/courses/ui/archived-badge.tsx +9 -0
  176. package/template/templates/{asas/src → features/courses}/courses/ui/fee-cell.tsx +1 -1
  177. package/template/templates/{asas/src → features/courses}/courses/ui/status-cell.tsx +5 -3
  178. package/template/templates/features/courses/courses/ui/unpublish-button.tsx +15 -0
  179. package/template/templates/features/courses/domain/archive.ts +39 -0
  180. package/template/templates/features/courses/domain/on-offer.ts +6 -0
  181. package/template/templates/features/courses/enrollments/enrollments.ts +309 -0
  182. package/template/templates/{asas/src → features/courses}/enrollments/ui/session-cell.tsx +17 -7
  183. package/template/templates/{asas/src → features/courses/enrollments}/ui/session-labels.tsx +3 -3
  184. package/template/templates/features/courses/index.ts +8 -0
  185. package/template/templates/features/courses/package.json +24 -0
  186. package/template/templates/features/courses/portal/ui/catalogue.tsx +86 -0
  187. package/template/templates/features/courses/portal/ui/course.tsx +176 -0
  188. package/template/templates/features/courses/portal/ui/enrollments.tsx +170 -0
  189. package/template/templates/features/courses/portal/ui/parts.tsx +95 -0
  190. package/template/templates/features/courses/portal/ui/reads.ts +76 -0
  191. package/template/templates/features/courses/portal/ui/rules.ts +87 -0
  192. package/template/templates/features/courses/portal/ui/shell.tsx +194 -0
  193. package/template/templates/features/courses/sessions/seats.ts +42 -0
  194. package/template/templates/{asas/src → features/courses}/sessions/sessions.ts +45 -14
  195. package/template/templates/features/media/index.ts +10 -0
  196. package/template/templates/{asas/src → features/media}/media/media.ts +34 -40
  197. package/template/templates/{asas/src → features/media}/media/ui/preview.tsx +2 -2
  198. package/template/templates/features/media/package.json +19 -0
  199. package/template/templates/features/roles/index.ts +4 -0
  200. package/template/templates/{asas/src → features/roles}/member-roles/member-roles.ts +15 -15
  201. package/template/templates/features/roles/package.json +21 -0
  202. package/template/templates/{asas/src → features/roles}/roles/roles.ts +23 -23
  203. package/template/templates/{asas/src → features/roles}/roles/ui/role-form.tsx +65 -24
  204. package/template/templates/{asas/src → features/roles}/roles/ui/roles.tsx +49 -25
  205. package/template/templates/features/settings/index.ts +3 -0
  206. package/template/templates/features/settings/package.json +13 -0
  207. package/template/templates/{asas/src → features/settings}/settings/settings.ts +12 -17
  208. package/template/templates/features/tags/index.ts +3 -0
  209. package/template/templates/features/tags/package.json +14 -0
  210. package/template/templates/{asas/src → features/tags}/tags/tags.ts +15 -15
  211. package/template/templates/mithaq/package.json +33 -0
  212. package/template/templates/mithaq/src/mail/index.ts +32 -0
  213. package/template/templates/mithaq/src/mail/mailers.ts +36 -0
  214. package/template/templates/{asas/src/audit-log/sink.ts → mithaq/src/mithaq/audit-sink.ts} +31 -9
  215. package/template/templates/{auth/src/platform-roles.ts → mithaq/src/mithaq/auth/backoffice-roles.ts} +44 -23
  216. package/template/templates/mithaq/src/mithaq/auth/index.ts +1859 -0
  217. package/template/templates/mithaq/src/mithaq/auth/organizations.ts +80 -0
  218. package/template/templates/{asas/src → mithaq/src/mithaq}/pages.ts +16 -7
  219. package/template/templates/mithaq/src/mithaq/rich-text-nodes.ts +19 -0
  220. package/template/templates/mithaq/src/mithaq.ts +593 -0
  221. package/template/templates/mithaq/src/roles/default-member-roles.ts +29 -0
  222. package/template/templates/{asas → mithaq}/src/roles/grants.ts +1 -1
  223. package/template/templates/mithaq/src/roles/member-access.ts +11 -0
  224. package/template/templates/{asas/src → mithaq/src/roles}/statements.ts +6 -4
  225. package/template/templates/mithaq/src/roles/system-roles.ts +16 -0
  226. package/template/templates/{asas/src → mithaq/src/roles}/tenant-access.ts +96 -57
  227. package/template/templates/{asas/src → mithaq/src/roles}/tenant-roles.ts +84 -25
  228. package/template/templates/{asas → mithaq}/src/steps/expected-updated-at.ts +2 -2
  229. package/template/templates/{asas → mithaq}/src/storage/index.ts +3 -1
  230. package/template/templates/mithaq/src/ui/activity-page.tsx +33 -0
  231. package/template/templates/mithaq/src/ui/activity.tsx +527 -0
  232. package/template/templates/{asas → mithaq}/src/ui/app-shell.tsx +27 -19
  233. package/template/templates/mithaq/src/ui/rich-text-nodes.tsx +33 -0
  234. package/template/templates/{asas → mithaq}/src/ui/screen-nav.tsx +1 -1
  235. package/template/templates/{asas → mithaq}/src/ui/top-shell.tsx +8 -8
  236. package/template/apps/asas-platform/src/components/mithaq/cells.tsx +0 -112
  237. package/template/apps/asas-platform/src/components/mithaq/controls.tsx +0 -650
  238. package/template/apps/asas-platform/src/components/mithaq/field-shell.tsx +0 -53
  239. package/template/apps/asas-platform/src/components/mithaq/filter-operators.ts +0 -108
  240. package/template/apps/asas-platform/src/components/mithaq/form.tsx +0 -1467
  241. package/template/apps/asas-platform/src/components/mithaq/list-columns.tsx +0 -108
  242. package/template/apps/asas-platform/src/components/mithaq/list-filters.tsx +0 -368
  243. package/template/apps/asas-platform/src/components/mithaq/list-pagination.tsx +0 -153
  244. package/template/apps/asas-platform/src/components/mithaq/list-query.ts +0 -432
  245. package/template/apps/asas-platform/src/components/mithaq/list-selection.tsx +0 -74
  246. package/template/apps/asas-platform/src/components/mithaq/list.tsx +0 -865
  247. package/template/apps/asas-platform/src/components/mithaq/nav.tsx +0 -871
  248. package/template/apps/asas-platform/src/components/mithaq/nodes.tsx +0 -1024
  249. package/template/apps/asas-platform/src/components/mithaq/relationship-cell.tsx +0 -127
  250. package/template/apps/asas-platform/src/components/mithaq/relationship-control.tsx +0 -400
  251. package/template/apps/asas-platform/src/components/mithaq/rich-text-nodes.ts +0 -6
  252. package/template/apps/asas-platform/src/components/mithaq/rich-text.tsx +0 -1083
  253. package/template/apps/asas-platform/src/components/mithaq/upload.tsx +0 -686
  254. package/template/apps/asas-platform/src/components/ui/drawer.tsx +0 -216
  255. package/template/apps/asas-platform/src/redirect.ts +0 -1
  256. package/template/apps/asas-platform/src/routeTree.gen.ts +0 -181
  257. package/template/apps/asas-platform/src/router.tsx +0 -7
  258. package/template/apps/asas-platform/src/routes/(screens)/platform/files.ts +0 -4
  259. package/template/apps/asas-platform/src/routes/api/$.ts +0 -30
  260. package/template/apps/asas-platform/src/server.ts +0 -20
  261. package/template/apps/asas-tanstack/src/components/mithaq/cells.tsx +0 -112
  262. package/template/apps/asas-tanstack/src/components/mithaq/controls.tsx +0 -650
  263. package/template/apps/asas-tanstack/src/components/mithaq/field-shell.tsx +0 -53
  264. package/template/apps/asas-tanstack/src/components/mithaq/filter-operators.ts +0 -108
  265. package/template/apps/asas-tanstack/src/components/mithaq/form.tsx +0 -1467
  266. package/template/apps/asas-tanstack/src/components/mithaq/list-columns.tsx +0 -108
  267. package/template/apps/asas-tanstack/src/components/mithaq/list-filters.tsx +0 -368
  268. package/template/apps/asas-tanstack/src/components/mithaq/list-pagination.tsx +0 -153
  269. package/template/apps/asas-tanstack/src/components/mithaq/list-query.ts +0 -432
  270. package/template/apps/asas-tanstack/src/components/mithaq/list-selection.tsx +0 -74
  271. package/template/apps/asas-tanstack/src/components/mithaq/list.tsx +0 -865
  272. package/template/apps/asas-tanstack/src/components/mithaq/nav.tsx +0 -871
  273. package/template/apps/asas-tanstack/src/components/mithaq/nodes.tsx +0 -1024
  274. package/template/apps/asas-tanstack/src/components/mithaq/relationship-cell.tsx +0 -127
  275. package/template/apps/asas-tanstack/src/components/mithaq/relationship-control.tsx +0 -400
  276. package/template/apps/asas-tanstack/src/components/mithaq/rich-text-nodes.ts +0 -6
  277. package/template/apps/asas-tanstack/src/components/mithaq/rich-text.tsx +0 -1083
  278. package/template/apps/asas-tanstack/src/components/mithaq/upload.tsx +0 -686
  279. package/template/apps/asas-tanstack/src/components/ui/drawer.tsx +0 -215
  280. package/template/templates/asas/src/audit-log/audit-log.ts +0 -187
  281. package/template/templates/asas/src/audit-log/ui/activity.tsx +0 -243
  282. package/template/templates/asas/src/courses/ui/archive-button.tsx +0 -25
  283. package/template/templates/asas/src/courses/ui/no-courses-on-offer.tsx +0 -11
  284. package/template/templates/asas/src/courses/ui/on-offer.ts +0 -6
  285. package/template/templates/asas/src/enrollments/enrollments.ts +0 -137
  286. package/template/templates/asas/src/rich-text-nodes.ts +0 -11
  287. package/template/templates/asas/src/roles.ts +0 -16
  288. package/template/templates/asas/src/ui/rich-text-nodes.tsx +0 -15
  289. package/template/templates/asas/src/ui-platform/organization-actions.tsx +0 -114
  290. package/template/templates/auth/src/index.ts +0 -690
  291. /package/template/apps/{asas-platform → asas-backoffice}/components.json +0 -0
  292. /package/template/apps/{asas-platform → asas-backoffice}/gitignore +0 -0
  293. /package/template/apps/{asas-platform → asas-backoffice}/public/favicon.svg +0 -0
  294. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/accordion.tsx +0 -0
  295. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/alert-dialog.tsx +0 -0
  296. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/avatar.tsx +0 -0
  297. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/badge.tsx +0 -0
  298. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/breadcrumb.tsx +0 -0
  299. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/button-group.tsx +0 -0
  300. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/button.tsx +0 -0
  301. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/calendar.tsx +0 -0
  302. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/card.tsx +0 -0
  303. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/chart.tsx +0 -0
  304. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/checkbox.tsx +0 -0
  305. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/collapsible.tsx +0 -0
  306. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/combobox.tsx +0 -0
  307. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/dialog.tsx +0 -0
  308. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/direction.tsx +0 -0
  309. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/directional-icon.tsx +0 -0
  310. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/dropdown-menu.tsx +0 -0
  311. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/field.tsx +0 -0
  312. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/input-group.tsx +0 -0
  313. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/input-otp.tsx +0 -0
  314. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/input.tsx +0 -0
  315. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/label.tsx +0 -0
  316. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/popover.tsx +0 -0
  317. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/radio-group.tsx +0 -0
  318. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/resizable.tsx +0 -0
  319. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/scroll-area.tsx +0 -0
  320. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/select.tsx +0 -0
  321. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/separator.tsx +0 -0
  322. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/sheet.tsx +0 -0
  323. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/sidebar.tsx +0 -0
  324. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/skeleton.tsx +0 -0
  325. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/slider.tsx +0 -0
  326. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/spinner.tsx +0 -0
  327. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/switch.tsx +0 -0
  328. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/table.tsx +0 -0
  329. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/tabs.tsx +0 -0
  330. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/textarea.tsx +0 -0
  331. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/toggle-group.tsx +0 -0
  332. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/toggle.tsx +0 -0
  333. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/tooltip.tsx +0 -0
  334. /package/template/apps/{asas-platform → asas-backoffice}/src/copy.ts +0 -0
  335. /package/template/apps/{asas-platform → asas-backoffice}/src/hooks/use-mobile.ts +0 -0
  336. /package/template/apps/{asas-platform → asas-backoffice}/src/lib/utils.ts +0 -0
  337. /package/template/apps/{asas-platform → asas-backoffice}/src/theme.ts +0 -0
  338. /package/template/templates/{asas/src/kayan → features}/auth/user/index.ts +0 -0
  339. /package/template/templates/{asas/src → features/courses}/courses/ui/no-courses.tsx +0 -0
  340. /package/template/templates/{asas → mithaq}/components.json +0 -0
  341. /package/template/templates/{asas → mithaq}/src/storage/defaults.ts +0 -0
  342. /package/template/templates/{asas → mithaq}/src/storage/local.ts +0 -0
  343. /package/template/templates/{asas → mithaq}/src/storage/r2.ts +0 -0
  344. /package/template/templates/{asas → mithaq}/src/storage/s3.ts +0 -0
  345. /package/template/templates/{asas → mithaq}/src/storage/uploadthing.ts +0 -0
  346. /package/template/templates/{asas → mithaq}/src/ui/kpi-card.tsx +0 -0
  347. /package/template/templates/{asas → mithaq}/src/ui/record-page.tsx +0 -0
  348. /package/template/templates/{asas → mithaq}/src/ui/table-page.tsx +0 -0
@@ -1,7 +1,9 @@
1
- import { onPlatform, platformRoles, platformURL } from '@asas/auth'
2
- import { secret } from '@asas/auth/platform-roles'
1
+ import { onBackoffice, backofficeApp, backofficeRoles, backofficeURL } from '../mithaq/auth/index.ts'
2
+ import { secret } from '../mithaq/auth/backoffice-roles.ts'
3
3
  import { actorPermissions, actorRoles, authActor, userRefs, type Held, type RowTable, type Store } from 'mithaq'
4
- import { isOwner, OWNER_ROLE } from './roles/grants.ts'
4
+ // add-on: roles
5
+ import { isOwner, OWNER_ROLE } from './grants.ts'
6
+ // end add-on: roles
5
7
 
6
8
  /**
7
9
  * What a row may name on the actor: `user.<key>` in any kayan's row is checked
@@ -24,28 +26,28 @@ export type OrganizationActor = {
24
26
  }
25
27
 
26
28
  /**
27
- * The platform administrator's slug. Platform roles live on Better Auth's
28
- * `user.role`, never in a tenant's `memberRoles`: `assignedRoles` drops any such
29
- * assignment, because the access target holds a slug's keys only where this
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
30
32
  * reader answers it.
31
33
  */
32
- export const PLATFORM = 'super_admin'
34
+ export const BACKOFFICE = 'super_admin'
33
35
 
34
36
  /**
35
- * The platform screen, the only place the platform roles answer. The app decides
37
+ * The backoffice screen, the only place the backoffice roles answer. The app decides
36
38
  * this, never the request.
37
39
  */
38
- export const PLATFORM_SCREEN = 'platform'
40
+ export const BACKOFFICE_SCREEN = 'backoffice'
39
41
 
40
42
  /**
41
- * The kayan whose session carries the platform column, the one `auth.admin`
43
+ * The kayan whose session carries the backoffice column, the one `auth.admin`
42
44
  * names. A build may run several Better Auth instances, and only this one's
43
45
  * `user.role` is the layer above every tenant.
44
46
  */
45
- const PLATFORM_KAYAN = 'user'
47
+ const BACKOFFICE_KAYAN = 'user'
46
48
 
47
49
  /**
48
- * Platform roles read every tenant's rows and write none; staff change a
50
+ * Backoffice roles read every tenant's rows and write none; staff change a
49
51
  * tenant's data only by impersonating a member, which is audited. Every write is
50
52
  * an explicit `false`, because a slug a table leaves out still takes the `'*'`
51
53
  * row, which opens every write. The row opens the door and the keys decide who
@@ -53,19 +55,19 @@ const PLATFORM_KAYAN = 'user'
53
55
  */
54
56
  const readOnly = { create: false, read: true, update: false, delete: false } as const
55
57
 
56
- /** The read-only row under every platform role, which every row table of this project spreads in. */
57
- export const platformReads = Object.fromEntries(Object.keys(platformRoles).map((slug) => [slug, readOnly])) as {
58
- readonly [Slug in keyof typeof platformRoles]: typeof readOnly
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
59
61
  }
60
62
 
61
63
  /**
62
- * Every operation refused under every platform role, spread in place of
63
- * `platformReads` by tables the platform does not read, such as the settings
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
64
66
  * global and the audit log.
65
67
  */
66
68
  const closed = { create: false, read: false, update: false, delete: false } as const
67
- export const platformClosed = Object.fromEntries(Object.keys(platformRoles).map((slug) => [slug, closed])) as {
68
- readonly [Slug in keyof typeof platformRoles]: typeof closed
69
+ export const backofficeClosed = Object.fromEntries(Object.keys(backofficeRoles).map((slug) => [slug, closed])) as {
70
+ readonly [Slug in keyof typeof backofficeRoles]: typeof closed
69
71
  }
70
72
 
71
73
  /**
@@ -89,13 +91,13 @@ export const currentOrganization = (user: unknown) => {
89
91
  }
90
92
 
91
93
  /**
92
- * The platform rows of the tenant-owned kayan: `platformReads` minus the
94
+ * The backoffice rows of the tenant-owned kayan: `backofficeReads` minus the
93
95
  * columns `secret` names. A row's field list closes a column whatever keys the
94
96
  * actor holds, and a column an actor may not read is no filter either.
95
97
  */
96
98
  const readsNoSecret = { ...readOnly, fields: ['*', ...secret.map((key) => `!${key.slice(key.indexOf('.fields.') + '.fields.'.length)}`)] as readonly string[] }
97
- const secretClosed = Object.fromEntries(Object.keys(platformRoles).map((slug) => [slug, readsNoSecret])) as {
98
- readonly [Slug in keyof typeof platformRoles]: typeof readsNoSecret
99
+ const secretClosed = Object.fromEntries(Object.keys(backofficeRoles).map((slug) => [slug, readsNoSecret])) as {
100
+ readonly [Slug in keyof typeof backofficeRoles]: typeof readsNoSecret
99
101
  }
100
102
 
101
103
  /**
@@ -110,39 +112,46 @@ const secretClosed = Object.fromEntries(Object.keys(platformRoles).map((slug) =>
110
112
  */
111
113
  export const tenantAccess = {
112
114
  '*': { create: true, read: true, update: true, delete: true },
115
+ // add-on: courses
113
116
  /**
114
117
  * The one row keyed on a system slug, and it narrows: an instructor teaches and
115
118
  * never removes a row. Every operation is named in full, because a missing one
116
119
  * reads as `false` and a `false` in any applicable row wins.
117
120
  */
118
121
  instructor: { create: true, read: true, update: true, delete: false },
122
+ // end add-on: courses
119
123
  ...secretClosed,
120
124
  } as const satisfies RowTable
121
125
 
122
126
  /**
123
127
  * The roles a kayan's rows are read through; its parameter also types `user` for
124
- * every row this project writes. Slugs come from the `memberRoles` kayan for the
128
+ * every row this project writes. Slugs come from the access target for the
125
129
  * active organization, off the trusted snapshot, so an object naming its own
126
- * roles is no one. `member.role` grants one thing only: an owner always holds
127
- * `administrator` in its tenant, so the role that manages roles is never lost.
130
+ * roles is no one.
128
131
  */
129
132
  export const assignedRoles = (actor: OrganizationActor): readonly string[] => {
130
133
  if (!authActor(actor)) return []
131
- // The platform app answers the platform roles alone and the tenant app the
134
+ // The backoffice app answers the backoffice roles alone and the tenant app the
132
135
  // membership's roles alone, never unioned, so neither adds keys on the other's
133
136
  // screens. Which app serves the request is the app's own fact, never the request's.
134
- if (onPlatform()) return platformRolesOf(actor)
137
+ if (onBackoffice()) return backofficeRolesOf(actor)
135
138
  if (!currentOrganization(actor)) return []
136
- const assigned = (actorRoles(actor) ?? []).filter((slug) => !Object.hasOwn(platformRoles, slug))
137
- return isOwner(authActor(actor)?.memberRole) && !assigned.includes(OWNER_ROLE) ? [...assigned, OWNER_ROLE] : assigned
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
138
147
  }
139
148
 
140
149
  /**
141
150
  * Whether `assignedRoles` answers this actor `super_admin`, which only the
142
- * platform app does. It is the administrator of `auth.admin`, the one answer
151
+ * backoffice app does. It is the administrator of `auth.admin`, the one answer
143
152
  * local auth's `_verified` writes ask, and the reader and writer of local accounts.
144
153
  */
145
- export const superAdmin = ({ user }: { user: unknown }) => assignedRoles(user as OrganizationActor).includes(PLATFORM)
154
+ export const superAdmin = ({ user }: { user: unknown }) => assignedRoles(user as OrganizationActor).includes(BACKOFFICE)
146
155
 
147
156
  /**
148
157
  * Every operation refused to every actor, the administrator included, for the
@@ -153,69 +162,99 @@ export const superAdmin = ({ user }: { user: unknown }) => assignedRoles(user as
153
162
  export const providerOnly = { create: () => false, read: () => false, update: () => false, delete: () => false } as const
154
163
 
155
164
  /**
156
- * The platform roles this actor holds on Better Auth's `user.role`, whichever app
157
- * asks. Several are comma-separated, and only a platform role's name counts.
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.
158
167
  *
159
168
  * The column is read off the trusted snapshot, and only from the admin kayan's
160
169
  * session on a Better Auth strategy: a build may run several instances that all
161
170
  * write `user.role`, and local auth spreads its kayan's row onto the actor, so a
162
- * `role` column there would otherwise hand someone a platform role.
171
+ * `role` column there would otherwise hand someone a backoffice role.
163
172
  */
164
- export const platformRolesOf = (actor: unknown): string[] => {
173
+ export const backofficeRolesOf = (actor: unknown): string[] => {
165
174
  const trusted = authActor(actor)
166
- if (!trusted || trusted.strategy !== 'better-auth' || trusted.collection !== PLATFORM_KAYAN || typeof trusted.role !== 'string') return []
167
- return [...new Set(trusted.role.split(',').map((slug) => slug.trim()).filter((slug) => Object.hasOwn(platformRoles, slug)))]
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)))]
168
177
  }
169
178
 
170
179
  /**
171
- * Where the tenant app's header links the platform app, for an actor whose
172
- * platform roles hold `screen.platform`; nothing otherwise. It is a link and
173
- * grants nothing: the platform app's own server decides entry.
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.
174
184
  */
175
- export const platformEntry = (actor: unknown): string | undefined => {
176
- if (onPlatform()) return undefined
177
- const enters = platformRolesOf(actor).some((slug) => {
178
- const held = platformRoles[slug as keyof typeof platformRoles].permissions
179
- return held === '*' || (held as readonly string[]).includes(`screen.${PLATFORM_SCREEN}`)
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}`)
180
190
  })
181
- return enters ? new URL(`/${PLATFORM_SCREEN}`, platformURL).href : undefined
191
+ return enters ? new URL(`/${BACKOFFICE_SCREEN}`, backofficeURL).href : undefined
182
192
  }
183
193
 
184
194
  /**
185
195
  * The keys a provider kayan's columns are graded by, which `rows()` takes as its
186
196
  * `holds`: in the tenant app the id and the name alone, and only where the actor
187
- * holds their keys; in the platform app the actor's own keys. An actor holding no
197
+ * holds their keys; in the backoffice app the actor's own keys. An actor holding no
188
198
  * key reads no column, since an empty set grades every column closed where
189
199
  * `undefined` would grade none.
190
200
  */
191
201
  export const namesInTenant = (kayan: string) => (actor: unknown): Held => {
192
202
  const held = actorPermissions(actor)
193
- if (onPlatform()) return held
203
+ if (onBackoffice()) return held
194
204
  return new Set(['id', 'name'].map((field) => `${kayan}.fields.${field}`).filter((key) => held === true || held?.has(key) === true))
195
205
  }
196
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
+
197
220
  /**
198
221
  * The read the person's own rows are ANDed with, so it narrows and never widens:
199
222
  * the members of the organization the actor's session has active, read off the
200
- * store. The organization is the trusted snapshot's, so a claim is no one's.
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.
201
226
  */
202
227
  export const membersOf = (store: Pick<Store, 'find'>) => async ({ user: actor }: { user: unknown }) => {
203
- // A platform role reads every person in the platform app; the rows answer who holds one.
204
- if (onPlatform()) return true
228
+ // A backoffice role reads every person in the backoffice app; the rows answer who holds one.
229
+ if (onBackoffice()) return true
205
230
  const active = currentOrganization(actor)
206
231
  if (!active) return false
207
- const members = await store.find({ kayan: 'member', args: { limit: 0, page: 1, sort: { field: 'id', direction: 'asc' }, where: { organizationId: { equals: active } } } })
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 } } } })
208
234
  const ids = members.docs.map((row) => row.userId).filter((id): id is string => typeof id === 'string')
209
- return ids.length ? { id: { in: ids } } : false
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
210
249
  }
211
250
 
212
251
  /**
213
252
  * Whether an actor reads past what a kayan's own read narrows for roles not
214
- * holding `key`: in the platform app, whose staff read every tenant's rows whole,
215
- * or in a tenant, holding that key. Archived courses, sessions of unreadable
216
- * courses, covers of unreadable courses and other people's seats narrow this way.
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.
217
256
  */
218
257
  export const readsPast = (actor: unknown, key: string) => {
219
258
  const held = actorPermissions(actor)
220
- return onPlatform() || held === true || held?.has(key) === true
259
+ return onBackoffice() || held === true || held?.has(key) === true
221
260
  }
@@ -1,5 +1,9 @@
1
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
2
5
 
6
+ // add-on: courses
3
7
  /** A session's columns but its `enrollments`, which the registrar and the instructor read alike. */
4
8
  const sessionColumns = [
5
9
  'sessions.fields.id',
@@ -12,9 +16,29 @@ const sessionColumns = [
12
16
  'sessions.fields.venue',
13
17
  'sessions.fields.format',
14
18
  'sessions.fields.archived',
19
+ 'sessions.fields.archiveReason',
15
20
  'sessions.fields.details',
16
21
  ] as const
17
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
+
18
42
  /** A course's cover, read and never changed, which the portal's roles read alike. */
19
43
  const coverReads = [
20
44
  'media.find',
@@ -32,41 +56,43 @@ const coverReads = [
32
56
  'media.fields.updatedAt',
33
57
  'media.fields.width',
34
58
  ] as const
59
+ // end add-on: courses
35
60
 
36
61
  /**
37
62
  * The roles this project ships to every tenant, written in code and never a
38
- * row: the access target takes them whole and reads what each holds from here,
39
- * and the `roles` kayan holds a tenant's own roles alone. A page lists them from
40
- * here too, which is why this file imports nothing.
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.
41
66
  *
42
67
  * A slug here is the whole vocabulary of role names: a row table may be keyed on
43
68
  * one of them or on `'*'` and on nothing else, and `buildAsas` refuses a key
44
69
  * that is not in this project's catalog exactly as it does for a row's own.
45
70
  *
46
- * **These are business roles and never governance.** Better Auth's own
47
- * `member.role`, which holds `owner`, `admin` or `member`, stays what the
48
- * provider uses for settings, members and invitations, and nothing here reads
49
- * it. Sharing one comma-joined column would inherit the provider's own open
50
- * comma-in-a-role-name bug.
51
- *
52
- * A new organization's creator is assigned `administrator` while the
53
- * organization is being created, by the hook the template wires into the
54
- * provider, so a fresh tenant has a root granter from its first moment. That
55
- * row is written straight through the store, because a managed create is graded
56
- * against the actor making it and the creator is in no organization until that
57
- * route puts it in one.
58
- *
59
- * The platform roles, `super_admin` among them, are not here: they are staff
71
+ * The backoffice roles, `super_admin` among them, are not here: they are staff
60
72
  * above every tenant, held on Better Auth's `user.role`, and the access target
61
- * holds their keys in the platform app alone. Inside a tenant every actor, a
73
+ * holds their keys in the backoffice app alone. Inside a tenant every actor, a
62
74
  * `super_admin` included, holds what its membership's roles hold and nothing
63
- * more. These are the roles an assignment may name.
75
+ * more.
64
76
  */
65
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.
66
92
  /**
67
93
  * The tenant's root granter: every key of the project, which is what lets it
68
94
  * author a custom role at all. `'*'` holds every screen's key too, so it
69
- * enters the dashboard and the portal alike. It never reaches the platform
95
+ * enters the dashboard and the portal alike. It never reaches the backoffice
70
96
  * app, where the reader answers no membership role at all.
71
97
  * `grantedPermissions` returns early on a granter holding everything, so this
72
98
  * role and no other may hand out a key it will never use itself.
@@ -80,11 +106,15 @@ export const tenantRoles = {
80
106
  label: { ar: 'مسجل', en: 'Registrar' },
81
107
  permissions: [
82
108
  'screen.dashboard',
109
+ // add-on: courses
83
110
  'courses.create',
84
111
  'courses.find',
85
112
  'courses.count',
86
113
  'courses.findByID',
87
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',
88
118
  'courses.fields.archived',
89
119
  'courses.fields.archiveReason',
90
120
  'courses.fields.createdAt',
@@ -97,6 +127,7 @@ export const tenantRoles = {
97
127
  'courses.fields.organization',
98
128
  'courses.fields.title',
99
129
  'courses.fields.updatedAt',
130
+ 'courses.fields.selfEnroll',
100
131
  // Whether a course is a draft, its versions and the restore of one: the
101
132
  // registrar edits the catalogue, so it reads a course's history and puts
102
133
  // an older copy back.
@@ -113,6 +144,7 @@ export const tenantRoles = {
113
144
  // read of it is refused for its fields and its pages draw nothing. Not a
114
145
  // session's `enrollments`, which reads a kayan this role does not.
115
146
  ...sessionColumns,
147
+ // end add-on: courses
116
148
  'tags.find',
117
149
  'tags.count',
118
150
  'tags.findByID',
@@ -121,6 +153,7 @@ export const tenantRoles = {
121
153
  'tags.fields.updatedAt',
122
154
  'tags.fields.organization',
123
155
  'tags.fields.name',
156
+ // add-on: media
124
157
  // The images a course's cover is picked from, which it uploads,
125
158
  // re-describes and deletes once no course points at one.
126
159
  'media.create',
@@ -141,6 +174,8 @@ export const tenantRoles = {
141
174
  'media.fields.organization',
142
175
  'media.fields.updatedAt',
143
176
  'media.fields.width',
177
+ // end add-on: media
178
+ // add-on: audit-log
144
179
  // The activity panel on each edit page it opens: the entries of the
145
180
  // kayan it lists, each change graded by the column's own key above.
146
181
  'auditLog.find',
@@ -152,8 +187,10 @@ export const tenantRoles = {
152
187
  'auditLog.fields.actor',
153
188
  'auditLog.fields.impersonatedBy',
154
189
  'auditLog.fields.changes',
190
+ // end add-on: audit-log
155
191
  ],
156
192
  },
193
+ // add-on: courses
157
194
  /**
158
195
  * Teaches: reads the catalogue and enrols. It holds no `courses.archive`. It
159
196
  * works in the portal, which draws the courses and the enrolments, and never
@@ -171,10 +208,14 @@ export const tenantRoles = {
171
208
  'courses.fields.cover',
172
209
  'courses.fields.details',
173
210
  'courses.fields.id',
174
- 'courses.fields.tags',
175
211
  'courses.fields.organization',
176
212
  'courses.fields.title',
177
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',
178
219
  // It reads published courses alone, and whether one is published with them.
179
220
  'courses.fields._status',
180
221
  'enrollments.create',
@@ -195,15 +236,19 @@ export const tenantRoles = {
195
236
  'sessions.count',
196
237
  'sessions.findByID',
197
238
  ...sessionColumns,
198
- // The people of its organization by their id and name alone, which the
239
+ // The students of its organization by their id and name alone, which the
199
240
  // enrollment drawer's student picker lists and an enrollment's student is
200
- // titled by.
241
+ // titled by: `peopleAudience` in `tenant-access.ts` names whom it reads.
201
242
  'user.find',
202
243
  'user.count',
203
244
  'user.findByID',
204
245
  'user.fields.id',
205
246
  'user.fields.name',
206
247
  ...coverReads,
248
+ ...catalogueReads,
249
+ // add-on: settings
250
+ ...portalSettings,
251
+ // end add-on: settings
207
252
  ],
208
253
  },
209
254
  /**
@@ -216,8 +261,9 @@ export const tenantRoles = {
216
261
  * alone would hand a student every other student's seat. The enrollments'
217
262
  * own access narrows them to the student's own seats, since it holds no
218
263
  * `enrollments.others`, and the sessions' to those of the courses it reads.
219
- * It reads no seat's `student`, which is itself on every row it reads, and
220
- * writes no enrollment.
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.
221
267
  */
222
268
  student: {
223
269
  label: { ar: 'طالب', en: 'Student' },
@@ -245,11 +291,15 @@ export const tenantRoles = {
245
291
  'enrollments.find',
246
292
  'enrollments.count',
247
293
  'enrollments.findByID',
294
+ 'enrollments.create',
295
+ 'enrollments.delete',
248
296
  'enrollments.fields.id',
249
297
  'enrollments.fields.createdAt',
250
298
  'enrollments.fields.updatedAt',
251
299
  'enrollments.fields.organization',
252
300
  'enrollments.fields.session',
301
+ // Itself, which its own create fills and every seat it reads names.
302
+ 'enrollments.fields.student',
253
303
  // The sessions its seats name, which the enrollment's session cell draws
254
304
  // by course and start.
255
305
  'sessions.find',
@@ -257,6 +307,15 @@ export const tenantRoles = {
257
307
  'sessions.findByID',
258
308
  ...sessionColumns,
259
309
  ...coverReads,
310
+ ...catalogueReads,
311
+ // add-on: settings
312
+ ...portalSettings,
313
+ // end add-on: settings
260
314
  ],
261
315
  },
316
+ // end add-on: courses
317
+ // end add-on: roles
318
+ // without add-on: roles
319
+ // ...defaultMemberRoles,
320
+ // end without add-on: roles
262
321
  } as const satisfies SystemRoles
@@ -10,7 +10,7 @@
10
10
  // Listing the step without widening the input protects nothing, since the strict update
11
11
  // shape refuses the key by name.
12
12
  //
13
- // The compare is against `ctx.dataBefore`, loaded with `lock: 'update'` inside the update's
13
+ // The compare is against `ctx.dataBefore`, loaded under the update's own row lock inside its
14
14
  // transaction, so on a store that honours the lock a second writer waits and no write lands
15
15
  // between the compare and the store step. A store that ignores the lock lets both through.
16
16
  //
@@ -38,4 +38,4 @@ export const expectedUpdatedAt = (ctx: Ctx<{ expectedUpdatedAt?: Date | string }
38
38
  }
39
39
  return ctx
40
40
  }
41
- // mithaq-admin b06d8d335d306c07556f8f2b81fba20ba7ded198 dad3f2c08ca33c2d63db0e3e11df5a2ba9d8acbba71ba77599ab76d625f2189f
41
+ // mithaq-admin 0.0.0 10cc4a57db299d48c4fd53314ed42e02fc04b0464a7fa8a0c512cefc7ccb2b82
@@ -8,7 +8,7 @@
8
8
  // `mithaq-storage` registry item and owned by the project: `mithaq upgrade
9
9
  // --check` reports one that was edited, and an edit is merged by hand.
10
10
  import { randomBytes } from 'node:crypto'
11
- import { defaultFileSize, type StorageConfig } from 'mithaq'
11
+ import { defaultFileSize, defaultMaxBodyBytes, type StorageConfig } from 'mithaq'
12
12
  import {
13
13
  defaultDisposition,
14
14
  defaultKey,
@@ -81,6 +81,8 @@ export const storageConfig: StorageConfig = {
81
81
  adapter,
82
82
  // The largest file a kayan holds when its targets.storage names no fileSize.
83
83
  fileSize: defaultFileSize,
84
+ // The largest instructions request read; files themselves never come this way.
85
+ maxBodyBytes: defaultMaxBodyBytes,
84
86
  key: defaultKey,
85
87
  verify: defaultVerify,
86
88
  disposition: defaultDisposition,
@@ -0,0 +1,33 @@
1
+ // The dashboard's Activity page: every change in the organization, read as the
2
+ // viewer from the log's `find`, which answers this tenant's entries alone, of
3
+ // the kayan the viewer lists. A file of its own beside `activity.tsx`, since the
4
+ // shell it draws in holds the app's own pieces.
5
+ import { useEffect, useState } from 'react'
6
+ import { useAccess, useLocale } from 'mithaq/admin/react'
7
+ import { ActivityLog, filtersOf, type ActivityQuery } from './activity.tsx'
8
+ import { AppShell } from './app-shell.tsx'
9
+
10
+ /**
11
+ * The page, filtered at first by what its link names, such as one record's
12
+ * entries from that record's panel. The link is read once the page is in the
13
+ * browser, since the server renders no location.
14
+ */
15
+ export function ActivityPage() {
16
+ const { t } = useLocale()
17
+ const people = useAccess().collections?.user?.read === true
18
+ const [query, setQuery] = useState<ActivityQuery>()
19
+ useEffect(() => setQuery(filtersOf({ search: window.location.search, people })), [])
20
+ return (
21
+ <AppShell
22
+ regions={{
23
+ header: <h1>{t('auditLog.activity', 'Activity')}</h1>,
24
+ main: (
25
+ <section data-dashboard="activity" className="grid gap-4">
26
+ <p className="text-muted-foreground">{t('auditLog.about', 'Every change in this organization, newest first.')}</p>
27
+ {query ? <ActivityLog query={query} onQuery={setQuery} /> : <p className="text-muted-foreground text-sm">{t('auditLog.loading', 'Loading activity…')}</p>}
28
+ </section>
29
+ ),
30
+ }}
31
+ />
32
+ )
33
+ }