create-mithaq 0.1.1 → 0.2.1

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 +1094 -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
@@ -6,7 +6,6 @@
6
6
  import { useContext, useEffect, useMemo, useState } from 'react'
7
7
  import { same, type ClientField, type ComponentProps, type Row } from 'mithaq/admin'
8
8
  import { FormContext, useAccess, useClient, useKayan, useLocale, useRecord, useScreen } from 'mithaq/admin/react'
9
- import { empty } from '../audit-log/sink.ts'
10
9
  import {
11
10
  AlertDialog,
12
11
  AlertDialogAction,
@@ -24,11 +23,24 @@ import { Label } from '@/components/ui/label'
24
23
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
25
24
  import { Sheet, SheetClose, SheetContent, SheetFooter, SheetHeader, SheetTitle } from '@/components/ui/sheet'
26
25
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
27
- import { blocksOf, cellFor, richTextPlainText } from '@/components/mithaq/cells.tsx'
28
- import { formatDate, labelFor, refusalKeyOf, titleOf, type FindApi, type FoundPage } from '@/components/mithaq/controls.tsx'
29
- import { endonym } from '@/components/mithaq/nav.tsx'
30
- import { CloseButton } from '@/components/mithaq/nodes.tsx'
31
- import { useRelationshipTitles } from '@/components/mithaq/relationship-cell.tsx'
26
+ import { blocksOf, richTextPlainText } from 'mithaq-admin/rich-text-line.ts'
27
+ import { cellFor, useComponents } from 'mithaq-admin/components.tsx'
28
+ import type { FindApi, FoundPage } from 'mithaq-admin/control-props.ts'
29
+ import { labelFor, titleOf } from 'mithaq-admin/copy.ts'
30
+ import { formatDate } from 'mithaq-admin/format.tsx'
31
+ import { doneWords } from 'mithaq-admin/form-rules.tsx'
32
+ import { carry } from 'mithaq-admin/toast.tsx'
33
+ import { refusalKeyOf } from 'mithaq-admin/refusal.ts'
34
+ import { endonym } from 'mithaq-admin/locale-menu.ts'
35
+ import { CloseButton } from 'mithaq-admin/record-frame.tsx'
36
+ import { useRelationshipTitles } from 'mithaq-admin/relationship-cell.tsx'
37
+
38
+ /**
39
+ * What a version's column draws as a dash: absent, `null`, an empty string or
40
+ * an empty list, as the audit log's activity panel reads it. Its own here, so a
41
+ * project without the audit log keeps its versions.
42
+ */
43
+ const empty = (value: unknown) => value === undefined || value === null || value === '' || (Array.isArray(value) && !value.length)
32
44
 
33
45
  /** One version as `findVersions` answers it. */
34
46
  export type Version = {
@@ -257,6 +269,7 @@ function Compare({ version, versions, parent, onClose }: { version: Version; ver
257
269
  [kayans],
258
270
  )
259
271
  const titles = useRelationshipTitles({ fields: kayan.fields, docs: [version.version, from?.version ?? {}], context: { api, client: kayans, locale } })
272
+ const { cells } = useComponents()
260
273
  const copy = { t, locale }
261
274
  // A rich text value whole, a block to a line, where its list cell draws the
262
275
  // first block alone: a change further down would read as no change. A block the
@@ -265,7 +278,7 @@ function Compare({ version, versions, parent, onClose }: { version: Version; ver
265
278
  // as well as tinted, so the mark does not rest on colour alone.
266
279
  const drawn = (field: ClientField, value: unknown, other: unknown, mark: 'del' | 'ins') => {
267
280
  if (empty(value)) return '—'
268
- if (field.kind !== 'richText') return cellFor(field)({ kayan, field, value, t, locale, titles })
281
+ if (field.kind !== 'richText') return cellFor({ cells, field })({ kayan, field, value, t, locale, titles })
269
282
  const blocks = changedBlocks({ side: value, other })
270
283
  // A value of images or empty blocks alone has no words, and reads as empty.
271
284
  if (!blocks.length) return '—'
@@ -301,6 +314,7 @@ function Compare({ version, versions, parent, onClose }: { version: Version; ver
301
314
  // since is refused 409 rather than burying it.
302
315
  const read = takes ? versions.find((one) => one.latest)?.version.updatedAt : undefined
303
316
  await client({ kayan: kayan.name, operation: 'restoreVersion', input: { id: version.id, ...(read !== undefined && { expectedUpdatedAt: read }) }, draft })
317
+ carry(doneWords({ kayan, copy: { t, locale }, operation: { name: 'restoreVersion' } }))
304
318
  window.location.reload()
305
319
  } catch (thrown) {
306
320
  setRestoring(false)
@@ -349,7 +363,7 @@ function Compare({ version, versions, parent, onClose }: { version: Version; ver
349
363
  ) : 'failed' in prior && !against ? (
350
364
  <p role="alert" className="text-destructive text-sm">{t('version.failed', 'The versions could not be read.')}</p>
351
365
  ) : !rows.length ? (
352
- <p className="text-muted-foreground text-sm">{t('auditLog.none', 'No field you may read changed.')}</p>
366
+ <p className="text-muted-foreground text-sm">{t('version.noReadableChange', 'No field you may read changed.')}</p>
353
367
  ) : (
354
368
  <ul className="grid gap-3 text-sm">
355
369
  {/* Which side is which, over the two columns, since on a phone
@@ -0,0 +1,96 @@
1
+ // What `/portal` itself draws for the courses example, in the portal's own look: a greeting, the viewer's next
2
+ // session, and a few courses on offer it holds no seat in yet. It is the asas's own, since a screen's root page is
3
+ // drawn from the project, and draws with the courses feature's portal pieces.
4
+ import { useSyncExternalStore } from 'react'
5
+ import { CalendarDays, MapPin } from 'lucide-react'
6
+ import { useQuery } from '@tanstack/react-query'
7
+ import { useClient, useLocale, useScreen, useUser } from 'mithaq/admin/react'
8
+ import { buttonVariants } from '@/components/ui/button'
9
+ import { Skeleton } from '@/components/ui/skeleton'
10
+ import { PortalShell, display } from '@asas/courses/portal/ui/shell.tsx'
11
+ import { CardsLoading, Cover, CourseCard, ReadFailed, usePlace } from '@asas/courses/portal/ui/parts.tsx'
12
+ import { byWhen, partOfDay, startsIn, whenOf } from '@asas/courses/portal/ui/rules.ts'
13
+ import { catalogueOf, readSeats } from '@asas/courses/portal/ui/reads.ts'
14
+
15
+ /** How many courses the home offers before its link to the rest. */
16
+ const offered = 3
17
+
18
+ /** The hour on the viewer's own clock, and none on the server, which knows neither the clock nor the zone. */
19
+ const useHour = () => useSyncExternalStore(() => () => {}, () => new Date().getHours(), () => undefined)
20
+
21
+ export function Home() {
22
+ const { locale, t } = useLocale()
23
+ const client = useClient()
24
+ const screen = useScreen()
25
+ const user = useUser()
26
+ const place = usePlace()
27
+ const hour = useHour()
28
+ const now = Date.now()
29
+ // The viewer's own seats, whoever it is: an instructor reads every seat, and its next session is still its own.
30
+ const mine = useQuery({
31
+ queryKey: ['portal-mine', user.id],
32
+ queryFn: () => readSeats({ client, maxLimit: screen.maxLimit, where: { student: { equals: user.id } } }),
33
+ enabled: !!user.id,
34
+ })
35
+ const catalogue = useQuery({ queryKey: ['portal-catalogue'], queryFn: () => catalogueOf({ client, maxLimit: screen.maxLimit, now }), staleTime: 30_000 })
36
+ const next = mine.data && byWhen(mine.data, now).upcoming[0]
37
+ const held = new Set(mine.data?.map((one) => String(one.course?.id)))
38
+ // A course the student may enroll itself in, with a session to come, and no seat of its own in it yet.
39
+ const open = catalogue.data?.courses.filter((course) => course.selfEnroll === true && !held.has(String(course.id)) && catalogue.data.next.has(String(course.id))).slice(0, offered)
40
+ // One sentence per locale, so each says the name where its grammar puts it.
41
+ const greeting = (hour === undefined ? t('portal.welcome', 'Welcome, {name}') : t(`portal.greeting.${partOfDay(hour)}`, 'Hello, {name}'))!.replace('{name}', () => user.name)
42
+ return (
43
+ <PortalShell
44
+ regions={{
45
+ header: <h1 className={`${display} text-3xl leading-tight sm:text-4xl`}>{greeting}</h1>,
46
+ main: (
47
+ <div data-dashboard="portal" className="grid gap-8">
48
+ {mine.isPending && user.id ? <Skeleton className="h-52 rounded-3xl" /> : next ? (
49
+ <section aria-labelledby="next" className="bg-primary text-primary-foreground grid overflow-hidden rounded-3xl md:grid-cols-[3fr_2fr]">
50
+ <div className="grid content-start gap-3.5 p-5 sm:p-8">
51
+ <h2 id="next" className="text-sm opacity-85">{t('portal.yourNext', 'Your next session')}</h2>
52
+ <p className={`${display} text-2xl leading-tight sm:text-3xl`}>{String(next.course?.title ?? '')}</p>
53
+ <div className="grid gap-1.5 text-[15px]">
54
+ <span className="flex items-center gap-2"><CalendarDays aria-hidden className="size-4.5 shrink-0" />{whenOf({ value: next.startsAt, locale })}</span>
55
+ {place(next.session) ? <span className="flex items-center gap-2"><MapPin aria-hidden className="size-4.5 shrink-0" />{place(next.session)}</span> : null}
56
+ </div>
57
+ <div className="flex flex-wrap items-center justify-between gap-3 pt-1">
58
+ <span className="text-sm opacity-85">{t('portal.startsIn', 'Starts {when}')!.replace('{when}', () => startsIn({ value: next.startsAt, now, locale }) ?? '')}</span>
59
+ {next.course ? (
60
+ <a href={`${screen.path}/courses/${encodeURIComponent(String(next.course.id))}`} className={buttonVariants({ variant: 'secondary', className: 'bg-card text-primary h-11 rounded-full px-5 font-semibold' })}>
61
+ {t('portal.viewCourse', 'View course')}
62
+ </a>
63
+ ) : null}
64
+ </div>
65
+ </div>
66
+ <Cover course={next.course} cover={next.cover} className="hidden h-full min-h-60 w-full md:block" />
67
+ </section>
68
+ ) : null}
69
+ <section aria-labelledby="join" className="grid gap-4">
70
+ <div className="flex items-baseline justify-between gap-3">
71
+ <h2 id="join" className={`${display} text-xl sm:text-2xl`}>{t('portal.canJoin', 'Courses you can join')}</h2>
72
+ <a href={`${screen.path}/courses`} className="text-primary text-[15px] font-medium underline-offset-4 hover:underline">{t('portal.seeAll', 'See all courses')}</a>
73
+ </div>
74
+ {catalogue.isError ? <ReadFailed retry={() => void catalogue.refetch()} /> : !open ? <CardsLoading /> : open.length ? (
75
+ <div className="grid grid-cols-[repeat(auto-fill,minmax(16rem,1fr))] gap-5">
76
+ {open.map((course) => (
77
+ <CourseCard
78
+ key={String(course.id)}
79
+ course={course}
80
+ cover={catalogue.data!.covers.get(String(course.cover))}
81
+ next={catalogue.data!.next.get(String(course.id))}
82
+ tags={catalogue.data!.tags}
83
+ href={`${screen.path}/courses/${encodeURIComponent(String(course.id))}`}
84
+ />
85
+ ))}
86
+ </div>
87
+ ) : (
88
+ <p className="text-muted-foreground text-sm">{t('portal.noneToJoin', 'No course has a session coming up just now.')}</p>
89
+ )}
90
+ </section>
91
+ </div>
92
+ ),
93
+ }}
94
+ />
95
+ )
96
+ }
@@ -1,23 +1,19 @@
1
- // What `/portal` itself draws, inside the portal's top shell: a greeting, then
2
- // the dashboard home's figure card for each of the portal's two lists, with how
3
- // many rows of it the viewer may read and a link to it, the courses on offer
4
- // alone. A list the nav does not offer the viewer has no card, and a list whose
1
+ // What `/portal` itself draws in a project without the courses example, whose
2
+ // portal draws its own home: a greeting, then the dashboard home's figure card
3
+ // for each list below, with how many rows of it the viewer may read and a link
4
+ // to it. A list the nav does not offer the viewer has no card, and a list whose
5
5
  // count the viewer may not read, or whose count failed, shows a dash in place
6
6
  // of the figure.
7
7
  import type { ReactNode } from 'react'
8
- import { BookOpen, ClipboardList } from 'lucide-react'
9
8
  import type { LoaderTools, PageProps } from 'mithaq/admin'
10
9
  import { useLocale, useScreen, useUser } from 'mithaq/admin/react'
11
- import { KpiCard } from '../ui/kpi-card.tsx'
12
- import { ScreenNav, navLabel } from '../ui/screen-nav.tsx'
13
- import { PortalTopShell } from '../ui/top-shell.tsx'
14
- import { onOffer } from '../courses/ui/on-offer.ts'
10
+ import { KpiCard } from '@asas/mithaq/ui/kpi-card.tsx'
11
+ import { ScreenNav, navLabel } from '@asas/mithaq/ui/screen-nav.tsx'
12
+ import { PortalTopShell } from '@asas/mithaq/ui/top-shell.tsx'
15
13
 
16
14
  /** The lists the home counts, each with its card's icon and the rows its list is held to. */
17
- const figures: readonly [kayan: string, icon: ReactNode, where?: object][] = [
18
- ['courses', <BookOpen className="size-4" />, onOffer],
19
- ['enrollments', <ClipboardList className="size-4" />],
20
- ]
15
+ // A list of the project's own goes here, as `['tags', <Tags className="size-4" />]`.
16
+ const figures: readonly [kayan: string, icon: ReactNode, where?: object][] = []
21
17
 
22
18
  /**
23
19
  * How many rows of each list the viewer may count, read as the viewer, keyed
@@ -0,0 +1,284 @@
1
+ import { actorPermissions, count, defaultFields, defaultJsonMaxBytes, denied, find, findOne, invalid, type Ctx, type Held } from 'mithaq'
2
+ import { defineKayan, rows, organization } from '@asas/mithaq'
3
+ import { backofficeClosed } from '@asas/mithaq/roles/tenant-access.ts'
4
+ import { auditLogName, type Change } from '@asas/mithaq/mithaq/audit-sink.ts'
5
+
6
+ /** The kayan whose `find` this actor holds, which are the kayan whose entries it reads. */
7
+ const heldKayans = (held: Exclude<Held, true | undefined>) =>
8
+ [...held].filter((key) => key.endsWith('.find') && !key.includes('.fields.')).map((key) => key.slice(0, -'.find'.length))
9
+
10
+ /**
11
+ * The read the log's rows are ANDed with, so it narrows and never widens: an
12
+ * entry of a kayan the actor may not list is no entry it reads. An actor
13
+ * nothing grades reads none, which is the closed direction.
14
+ */
15
+ const readableKayans = ({ user }: { user: unknown }) => {
16
+ const held = actorPermissions(user)
17
+ if (held === true) return true
18
+ const kayans = held ? heldKayans(held) : []
19
+ return kayans.length ? { kayan: { in: kayans } } : false
20
+ }
21
+
22
+ /**
23
+ * Whether this actor reads one column of the kayan an entry names. The access
24
+ * target grades every column of this project by its key.
25
+ */
26
+ const readsField = (held: Held, kayan: string, field: string) => held === true || held?.has(`${kayan}.fields.${field}`) === true
27
+
28
+ /**
29
+ * Whether this actor reads the provider's people, by the key of their `find`.
30
+ * A command's operating-system user is the one thing in the log that names a
31
+ * person no row of the people does, so only such a reader sees or asks for it.
32
+ */
33
+ const readsPeople = (held: Held) => held === true || held?.has('user.find') === true
34
+
35
+ /**
36
+ * What the sink writes for a command: the command, then the operating-system
37
+ * user in parentheses. The command is the shortest part before ` (`, so a user
38
+ * holding a parenthesis is never left in it.
39
+ */
40
+ const commandActor = /^(.+?) \(.+\)$/
41
+
42
+ /**
43
+ * The step after the read: each entry keeps the changes of the columns the
44
+ * actor may read and drops the rest whole, so a column it may not read shows
45
+ * neither value nor that it changed. The server does it rather than the page,
46
+ * because the log is read over HTTP as well as drawn.
47
+ */
48
+ const visibleChanges = (ctx: Ctx): Ctx => {
49
+ const held = actorPermissions(ctx.actor)
50
+ const trim = (entry: unknown) => {
51
+ const changes = (entry as { changes?: unknown } | null)?.changes
52
+ if (!changes || typeof changes !== 'object') return entry
53
+ const { kayan, actor } = entry as { kayan: string; actor?: unknown }
54
+ const kept = Object.fromEntries(Object.entries(changes as Record<string, Change>).filter(([field]) => readsField(held, kayan, field)))
55
+ // A command's actor is the command alone to a reader of no people, its operating-system user blanked.
56
+ const named = !readsPeople(held) && typeof actor === 'string' ? { actor: commandActor.exec(actor)?.[1] ?? actor } : {}
57
+ return { ...(entry as object), ...named, changes: kept }
58
+ }
59
+ const data: unknown = ctx.data
60
+ const page = data && typeof data === 'object' && Array.isArray((data as { docs?: unknown }).docs) ? (data as { docs: unknown[] }) : undefined
61
+ return { ...ctx, data: (page ? { ...page, docs: page.docs.map(trim) } : trim(data)) as Record<string, unknown> }
62
+ }
63
+
64
+ /**
65
+ * The columns the log is filtered and ordered by. The organization is core's
66
+ * own filter and `impersonatedBy` names staff.
67
+ */
68
+ const queryable = new Set(['kayan', 'record', 'createdAt', 'operation', 'actor'])
69
+
70
+ /**
71
+ * What the log is searched by besides those, never ordered by: `changes`, the
72
+ * whole of each change, or a path into it, `changes.<column>` and deeper. Each
73
+ * search takes any of the json kind's operators and reaches only the kayan whose
74
+ * columns it may tell of (`reachOf`), because a match in a column the reader
75
+ * holds no key for would say what that column held before `visibleChanges`
76
+ * trims the page.
77
+ */
78
+ const searched = 'changes'
79
+
80
+ /** The field a `where` key names: `changes.title.to` names `changes`. */
81
+ const fieldOf = (key: string) => key.split('.')[0]!
82
+
83
+ const namedIn = (where: unknown): string[] => {
84
+ if (!where || typeof where !== 'object') return []
85
+ return Object.entries(where).flatMap(([key, value]) =>
86
+ (key === 'and' || key === 'or') && Array.isArray(value) ? value.flatMap(namedIn) : [key])
87
+ }
88
+
89
+ /** Every column key the catalog holds, by the kayan it belongs to. */
90
+ const columnsOf = (catalog: readonly string[]) => {
91
+ const columns = new Map<string, string[]>()
92
+ for (const key of catalog) {
93
+ const at = key.indexOf('.fields.')
94
+ if (at > 0) columns.set(key.slice(0, at), [...(columns.get(key.slice(0, at)) ?? []), key])
95
+ }
96
+ return columns
97
+ }
98
+
99
+ // ponytail: the reach here and `visibleChanges` grade by keys alone. A tenant role that a row
100
+ // table narrows further, by a `fields` list or a row filter on its read, would be told of columns
101
+ // and rows its own reads hide; grade each entry through the kayan's own field access and read
102
+ // `where` before a project adds one.
103
+ /**
104
+ * The kayan one search of a change reaches for this actor, or `true` for every
105
+ * one. The whole change reaches a kayan whose every column the actor reads, since
106
+ * it matches any of them; a path into one column, `changes.<column>`, a kayan
107
+ * whose column of that name it reads, since it matches inside that column alone.
108
+ */
109
+ const reachOf = ({ held, catalog }: { held: Held; catalog: readonly string[] }) => (key: string): true | string[] => {
110
+ if (held === true) return true
111
+ const column = key.split('.')[1]
112
+ return [...columnsOf(catalog)]
113
+ .filter(([kayan, keys]) => held?.has(`${kayan}.find`) && (column === undefined ? keys.every((one) => held.has(one)) : held.has(`${kayan}.fields.${column}`)))
114
+ .map(([kayan]) => kayan)
115
+ }
116
+
117
+ /**
118
+ * The `where` with each search of a change ANDed, where it stands, with the kayan
119
+ * it reaches, so an `or` around it still means what it said, and a negation, a
120
+ * `not_equals` or an `exists: false`, still answers no entry past its reach.
121
+ */
122
+ const narrowed = (where: unknown, reach: (key: string) => true | string[]): unknown => {
123
+ if (!where || typeof where !== 'object') return where
124
+ const parts = Object.entries(where).map(([key, value]) => {
125
+ if ((key === 'and' || key === 'or') && Array.isArray(value)) return { [key]: value.map((one) => narrowed(one, reach)) }
126
+ if (fieldOf(key) !== searched) return { [key]: value }
127
+ const kayans = reach(key)
128
+ return kayans === true ? { [key]: value } : { and: [{ [key]: value }, { kayan: { in: kayans } }] }
129
+ })
130
+ return parts.length === 1 ? parts[0] : { and: parts }
131
+ }
132
+
133
+ /**
134
+ * The step before the read: a `where` naming any column but those above, or a
135
+ * `sort` by any but the queryable ones, is refused 400 before the store is
136
+ * asked, on every read the log declares, and each search of a change is narrowed
137
+ * to the kayan it reaches. The runner's own checks still run after it on what it
138
+ * lets through.
139
+ */
140
+ const queriedByQueryable = (ctx: Ctx, tools: { catalog: readonly string[] }): Ctx => {
141
+ // The runner names the organization a read is scoped to only where the read is not widened.
142
+ // `reachOf` and `visibleChanges` grade by the active organization's keys, so a read across every
143
+ // membership (`tenant: 'mine'`) would grade another organization's entries by this one's: it is
144
+ // refused before anything else of it is judged.
145
+ if (ctx.tenant === undefined) {
146
+ throw denied({ message: 'Kayan "auditLog" is read in the active organization alone.', key: 'error.forbidden.read', params: {} })
147
+ }
148
+ const { where, sort } = ctx.data as { where?: unknown; sort?: unknown }
149
+ const field = namedIn(where).find((name) => !queryable.has(name) && fieldOf(name) !== searched)
150
+ if (field !== undefined) {
151
+ throw invalid({ message: `Kayan "auditLog" is filtered by ${[...queryable, searched].join(', ')} alone, and not by "${field}".`, key: 'error.invalid.where', params: { kayan: 'auditLog', field } })
152
+ }
153
+ const held = actorPermissions(ctx.actor)
154
+ // A reader of no people names an actor in no filter and no sort: a pattern, a range, an order or
155
+ // even an equality over the string would let it confirm a guessed operating-system user of a
156
+ // command's actor.
157
+ if (!readsPeople(held) && (namedIn(where).includes('actor') || (typeof sort === 'string' && sort.replace(/^-/, '') === 'actor'))) {
158
+ throw invalid({ message: 'Kayan "auditLog" is neither filtered nor sorted by "actor" for a reader of no people.', key: 'error.invalid.where', params: { kayan: 'auditLog', field: 'actor' } })
159
+ }
160
+ if (sort !== undefined && !(typeof sort === 'string' && queryable.has(sort.replace(/^-/, '')))) {
161
+ throw invalid({ message: `Kayan "auditLog" is sorted by ${[...queryable].join(', ')} alone.`, key: 'error.invalid.sort_field', params: { kayan: 'auditLog', field: String(sort) } })
162
+ }
163
+ return where === undefined ? ctx : { ...ctx, data: { ...ctx.data, where: narrowed(where, reachOf({ held, catalog: tools.catalog })) } }
164
+ }
165
+
166
+ /**
167
+ * The audit log's rows: an organization reads its own entries, and nothing writes
168
+ * one through the log, a caller of any role included, since the service
169
+ * target's sink writes each entry through the store inside the write it
170
+ * records. Every backoffice role is closed: the backoffice lists no entry, and a
171
+ * staff member's own impersonated writes are the entries it would read.
172
+ */
173
+ const auditLogAccess = rows({
174
+ '*': { create: false, read: true, update: false, delete: false },
175
+ ...backofficeClosed,
176
+ })
177
+
178
+ /**
179
+ * The audit log: one row per row a write changed, written by the service
180
+ * target's audit sink inside that write's own transaction and by nothing else.
181
+ * It declares no operation that writes, so no caller creates, edits or deletes
182
+ * an entry, and each of its rows answers `false` for every write besides.
183
+ *
184
+ * It is its organization's own. A record's last entries are read in its edit
185
+ * page's side column, all of them in its Activity tab, and the organization's on
186
+ * the dashboard's Activity page, a page of the screen's own rather than of this
187
+ * kayan, which draws none. The backoffice lists none of it and reads none of
188
+ * it: staff's own actions are its staff history, `backofficeLog`.
189
+ */
190
+ export const auditLog = defineKayan({
191
+ name: auditLogName,
192
+ dbName: 'audit_log',
193
+ messages: {
194
+ singular: { en: 'Audit entry', ar: 'سجل تدقيق' },
195
+ plural: { en: 'Audit log', ar: 'سجل التدقيق' },
196
+ // What the activity panel draws beside the field labels.
197
+ activity: { en: 'Activity', ar: 'النشاط' },
198
+ loading: { en: 'Loading activity…', ar: 'جارٍ تحميل النشاط…' },
199
+ empty: { en: 'Nothing has changed here yet.', ar: 'لم يتغير شيء هنا بعد.' },
200
+ failed: { en: 'The activity could not be read.', ar: 'تعذرت قراءة النشاط.' },
201
+ more: { en: 'Show older', ar: 'عرض الأقدم' },
202
+ system: { en: 'The system', ar: 'النظام' },
203
+ event: { en: '{action} by {actor}', ar: '{action} بواسطة {actor}' },
204
+ // Staff writing as a member: the real person first, as the member they wrote as.
205
+ impersonated: { en: '{staff} as {member}', ar: '{staff} بصفة {member}' },
206
+ // One per operation a tenant kayan writes through, which is every entry the sink records.
207
+ action: {
208
+ create: { en: 'Created', ar: 'أُنشئ' },
209
+ update: { en: 'Updated', ar: 'حُدِّث' },
210
+ delete: { en: 'Deleted', ar: 'حُذف' },
211
+ updateMany: { en: 'Updated', ar: 'حُدِّث' },
212
+ deleteMany: { en: 'Deleted', ar: 'حُذف' },
213
+ restoreVersion: { en: 'Restored a version', ar: 'استُعيد إصدار' },
214
+ archive: { en: 'Archived', ar: 'أُرشف' },
215
+ archiveMany: { en: 'Archived', ar: 'أُرشف' },
216
+ unarchive: { en: 'Unarchived', ar: 'أُلغيت أرشفته' },
217
+ unpublish: { en: 'Unpublished', ar: 'أُلغي نشره' },
218
+ // An update that published a draft, which the sink names apart from a plain update.
219
+ publish: { en: 'Published', ar: 'نُشر' },
220
+ },
221
+ someone: { en: 'Someone', ar: 'شخص ما' },
222
+ // A command line write, named for a reader of the people: the command and
223
+ // the operating-system user who ran it.
224
+ command: { en: 'the command {command} ({user})', ar: 'الأمر {command} ({user})' },
225
+ // The viewer's own entry, which needs no read of the people to name. Its
226
+ // sentence is its own, since Arabic joins the person to "by" in one word,
227
+ // "بواسطتك", and not as "بواسطة أنت".
228
+ you: { en: 'You', ar: 'أنت' },
229
+ eventYou: { en: '{action} by You', ar: '{action} بواسطتك' },
230
+ none: { en: 'No field you may read changed.', ar: 'لم يتغير حقل يمكنك قراءته.' },
231
+ // Beside a change the sink cut to the start of its JSON, which a large json value is.
232
+ truncated: { en: 'Too long to keep whole; its start is shown.', ar: 'أطول من أن يُحفظ كاملا؛ تظهر بدايته.' },
233
+ // The panel's link to the rest of a record's entries, and the Activity page and tab.
234
+ all: { en: 'All activity', ar: 'كل النشاط' },
235
+ about: { en: 'Every change in this organization, newest first.', ar: 'كل تغيير في هذه المؤسسة، الأحدث أولا.' },
236
+ filters: { en: 'Filter the activity', ar: 'تصفية النشاط' },
237
+ // What the search box names as what it searches, in the sentence "Search changed values".
238
+ searchChanges: { en: 'changed values', ar: 'القيم المتغيرة' },
239
+ person: { en: 'Person', ar: 'الشخص' },
240
+ type: { en: 'Record type', ar: 'نوع السجل' },
241
+ field: { en: 'Field', ar: 'الحقل' },
242
+ actionFilter: { en: 'Action', ar: 'الإجراء' },
243
+ from: { en: 'From', ar: 'من' },
244
+ to: { en: 'To', ar: 'إلى' },
245
+ any: { en: 'Any', ar: 'الكل' },
246
+ filter: {
247
+ created: { en: 'Created', ar: 'أُنشئ' },
248
+ updated: { en: 'Updated', ar: 'حُدِّث' },
249
+ deleted: { en: 'Deleted', ar: 'حُذف' },
250
+ published: { en: 'Published', ar: 'نُشر' },
251
+ restored: { en: 'Restored', ar: 'استُعيد' },
252
+ },
253
+ clear: { en: 'Clear filters', ar: 'مسح عوامل التصفية' },
254
+ nothing: { en: 'No activity matches these filters.', ar: 'لا نشاط يطابق عوامل التصفية هذه.' },
255
+ oneRecord: { en: 'One record\'s activity.', ar: 'نشاط سجل واحد.' },
256
+ found: { en: 'Entries found: {count}', ar: 'السجلات المطابقة: {count}' },
257
+ searchLimited: { en: 'The search covers the records whose every field you may read.', ar: 'يشمل البحث السجلات التي يمكنك قراءة كل حقولها.' },
258
+ },
259
+ access: { ...auditLogAccess, read: readableKayans },
260
+ targets: {
261
+ dashboard: {},
262
+ service: {
263
+ operations: {
264
+ find: { steps: [queriedByQueryable, find, visibleChanges] },
265
+ findByID: { steps: [queriedByQueryable, findOne, visibleChanges] },
266
+ count: { steps: [queriedByQueryable, count] },
267
+ },
268
+ },
269
+ },
270
+ fields: {
271
+ id: defaultFields.id,
272
+ createdAt: defaultFields.createdAt,
273
+ organization,
274
+ kayan: { kind: 'text', label: { en: 'Kayan', ar: 'الكيان' }, rules: { required: true, maxLength: 60 } },
275
+ record: { kind: 'text', label: { en: 'Record', ar: 'السجل' }, rules: { required: true, index: true } },
276
+ operation: { kind: 'text', label: { en: 'Operation', ar: 'العملية' }, rules: { required: true, maxLength: 60 } },
277
+ actor: { kind: 'text', label: { en: 'Actor', ar: 'المنفذ' } },
278
+ impersonatedBy: { kind: 'text', label: { en: 'Impersonated by', ar: 'انتحل الصفة' } },
279
+ // Each changed column by name, so a search finds the change of one column by
280
+ // the path `changes.<column>`, which `queriedByQueryable` grades by that
281
+ // column's key. Never an order, as no json field is.
282
+ changes: { kind: 'json', label: { en: 'Changes', ar: 'التغييرات' }, maxBytes: defaultJsonMaxBytes, rules: { required: true } },
283
+ },
284
+ })
@@ -0,0 +1,67 @@
1
+ import { defaultFields, defaultOperations } from 'mithaq'
2
+ import { defineKayan, rows } from '@asas/mithaq'
3
+ import { backofficeList } from '@asas/mithaq/mithaq/pages.ts'
4
+ import { backofficeReads } from '@asas/mithaq/roles/tenant-access.ts'
5
+
6
+ /** Who did what to whom on the backoffice, as `action` names it. */
7
+ const actions = [
8
+ { value: 'impersonate', label: { en: 'Started impersonating', ar: 'بدأ انتحال الصفة' } },
9
+ { value: 'stopImpersonating', label: { en: 'Stopped impersonating', ar: 'أنهى انتحال الصفة' } },
10
+ { value: 'setRole', label: { en: 'Set the backoffice role', ar: 'عيّن دور الإدارة الخلفية' } },
11
+ { value: 'ban', label: { en: 'Banned', ar: 'حظر' } },
12
+ { value: 'unban', label: { en: 'Lifted the ban', ar: 'رفع الحظر' } },
13
+ ] as const
14
+
15
+ const text = (label: { en: string; ar: string }) => ({ kind: 'text', label }) as const
16
+
17
+ /**
18
+ * The staff history: one row per impersonation started or stopped, backoffice
19
+ * role set and ban set or lifted, written by the auth setup's backoffice hook
20
+ * once the provider's route succeeded, and by nothing else. It belongs to no
21
+ * organization, since staff act above every tenant, and it declares no
22
+ * operation that writes, so no caller makes, edits or removes a row.
23
+ *
24
+ * Only a backoffice role reads it, in the backoffice app alone, and only one
25
+ * holding its keys: the row table names no `'*'` row, so a tenant's
26
+ * administrator, who holds every key of the project, takes no row of it and is
27
+ * refused. The tenant app's screens list none of it.
28
+ *
29
+ * Each person is kept by id and by the address they had then, so the history
30
+ * still says who once a person is renamed or removed.
31
+ */
32
+ export const backofficeLog = defineKayan({
33
+ name: 'backofficeLog',
34
+ dbName: 'backoffice_log',
35
+ messages: {
36
+ singular: { en: 'Staff action', ar: 'إجراء طاقم' },
37
+ plural: { en: 'Staff history', ar: 'سجل الطاقم' },
38
+ },
39
+ access: rows({ ...backofficeReads }),
40
+ targets: {
41
+ service: {
42
+ operations: { find: defaultOperations.find, findByID: defaultOperations.findByID, count: defaultOperations.count },
43
+ },
44
+ backoffice: {
45
+ pages: {
46
+ ...backofficeList('/history', {
47
+ columns: ['createdAt', 'action', 'actorEmail', 'targetEmail', 'previousRole', 'role', 'banReason'],
48
+ filters: ['action', 'actorEmail', 'targetEmail', 'createdAt'],
49
+ search: ['actorEmail', 'targetEmail'],
50
+ sort: '-createdAt',
51
+ }),
52
+ },
53
+ },
54
+ },
55
+ fields: {
56
+ id: defaultFields.id,
57
+ createdAt: defaultFields.createdAt,
58
+ action: { kind: 'text', label: { en: 'Action', ar: 'الإجراء' }, options: actions, rules: { required: true } },
59
+ actor: text({ en: 'Staff member', ar: 'عضو الطاقم' }),
60
+ actorEmail: text({ en: 'Staff member\'s email', ar: 'بريد عضو الطاقم' }),
61
+ target: text({ en: 'Person', ar: 'الشخص' }),
62
+ targetEmail: text({ en: 'Person\'s email', ar: 'بريد الشخص' }),
63
+ previousRole: text({ en: 'Role before', ar: 'الدور السابق' }),
64
+ role: text({ en: 'Role after', ar: 'الدور الجديد' }),
65
+ banReason: text({ en: 'Ban reason', ar: 'سبب الحظر' }),
66
+ },
67
+ })
@@ -0,0 +1,4 @@
1
+ import { auditLog } from './audit-log/audit-log.ts'
2
+ import { backofficeLog } from './backoffice-log/backoffice-log.ts'
3
+
4
+ export const kayan = [auditLog, backofficeLog] as const
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "@asas/audit-log",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./index.ts"
8
+ },
9
+ "dependencies": {
10
+ "@asas/mithaq": "*",
11
+ "mithaq": "*"
12
+ }
13
+ }
@@ -0,0 +1,11 @@
1
+ import { localUsers } from './local-users/local-users.ts'
2
+ // Written by `mithaq generate auth`: one key per plugin the vocabulary names,
3
+ // and `core` for the four Better Auth ships without one. Spreading the keys is
4
+ // what keeps this list from naming eight files, and a project that drops the
5
+ // organization plugin drops one spread.
6
+ import { betterAuth } from './user/index.ts'
7
+
8
+ // The account other features point at, and the asas names its administrator by.
9
+ export { user } from './user/user.ts'
10
+
11
+ export const kayan = [...betterAuth.core, ...betterAuth.organization, localUsers] as const
@@ -9,9 +9,9 @@ import {
9
9
  defaultOperations,
10
10
  type Ctx,
11
11
  } from 'mithaq'
12
- import { defineKayan, requestContext } from '../mithaq.ts'
13
- import { appPage, platformList } from '../pages.ts'
14
- import { superAdmin } from '../tenant-access.ts'
12
+ import { defineKayan, requestContext } from '@asas/mithaq'
13
+ import { appPage, backofficeList } from '@asas/mithaq/mithaq/pages.ts'
14
+ import { superAdmin } from '@asas/mithaq/roles/tenant-access.ts'
15
15
 
16
16
  /**
17
17
  * The trusted-header names this deployment's own proxy overwrites with the real client address, read
@@ -62,7 +62,7 @@ export const localUsers = defineKayan({
62
62
  // would share one count; there, take the entry at the depth of the trusted proxies. A project served
63
63
  // bare passes its server's own address instead, such as Bun's `server.requestIP(request)`. No header
64
64
  // answers null, which leaves the request uncounted.
65
- // No `email` is passed, so no mail is sent: a super admin verifies each sign-up in the platform app.
65
+ // No `email` is passed, so no mail is sent: a super admin verifies each sign-up in the backoffice app.
66
66
  auth: local({
67
67
  secret,
68
68
  withoutSecret: 'Local sign-in is off on this server: LOCAL_AUTH_SECRET is not set. Set it to a secret of 32 characters at least and restart the server.',
@@ -91,10 +91,10 @@ export const localUsers = defineKayan({
91
91
  * The form sends only what changed, so an unchanged toggle writes nothing and leaves `updatedAt`
92
92
  * alone. The protected columns are named by no page, so no list offers one as a column or a filter.
93
93
  */
94
- platform: {
94
+ backoffice: {
95
95
  useAsTitle: 'email',
96
96
  pages: {
97
- ...platformList('/local-users', {
97
+ ...backofficeList('/local-users', {
98
98
  columns: ['email', '_verified', 'createdAt'],
99
99
  filters: ['_verified'],
100
100
  search: ['email'],
@@ -121,7 +121,7 @@ export const localUsers = defineKayan({
121
121
  },
122
122
  },
123
123
  },
124
- // Public sign-up stays open. A super admin, in the platform app alone, verifies an account by writing
124
+ // Public sign-up stays open. A super admin, in the backoffice app alone, verifies an account by writing
125
125
  // `_verified: true`, which the field's own write admits from the administrator and nobody else. The
126
126
  // super admin writes `_verified` and nothing else: a password or an email it set would be an account
127
127
  // it could then log in as, and no audit entry records a write to a kayan with no tenant. An