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
@@ -1,215 +0,0 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
- import { Drawer as DrawerPrimitive } from "@base-ui/react/drawer"
5
- import { cn } from "@/lib/utils"
6
-
7
- type DrawerContextProps = {
8
- hasSnapPoints: boolean
9
- modal: DrawerPrimitive.Root.Props["modal"]
10
- showSwipeHandle: boolean
11
- swipeDirection: NonNullable<DrawerPrimitive.Root.Props["swipeDirection"]>
12
- }
13
-
14
- const DrawerContext = React.createContext<DrawerContextProps | null>(null)
15
-
16
- function useDrawer() {
17
- const context = React.useContext(DrawerContext)
18
-
19
- if (!context) {
20
- throw new Error("useDrawer must be used within a Drawer.")
21
- }
22
-
23
- return context
24
- }
25
-
26
- function Drawer({
27
- modal = true,
28
- showSwipeHandle = false,
29
- snapPoints,
30
- swipeDirection = "down",
31
- ...props
32
- }: DrawerPrimitive.Root.Props & {
33
- showSwipeHandle?: boolean
34
- }) {
35
- const hasSnapPoints = snapPoints != null && snapPoints.length > 0
36
- const contextValue = React.useMemo(
37
- () => ({ hasSnapPoints, modal, showSwipeHandle, swipeDirection }),
38
- [hasSnapPoints, modal, showSwipeHandle, swipeDirection]
39
- )
40
-
41
- return (
42
- <DrawerContext.Provider value={contextValue}>
43
- <DrawerPrimitive.Root
44
- data-slot="drawer"
45
- modal={modal}
46
- snapPoints={snapPoints}
47
- swipeDirection={swipeDirection}
48
- {...props}
49
- />
50
- </DrawerContext.Provider>
51
- )
52
- }
53
-
54
- function DrawerTrigger({ ...props }: DrawerPrimitive.Trigger.Props) {
55
- return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
56
- }
57
-
58
- function DrawerPortal({ ...props }: DrawerPrimitive.Portal.Props) {
59
- return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
60
- }
61
-
62
- function DrawerClose({ ...props }: DrawerPrimitive.Close.Props) {
63
- return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
64
- }
65
-
66
- function DrawerOverlay({
67
- className,
68
- ...props
69
- }: DrawerPrimitive.Backdrop.Props) {
70
- return (
71
- <DrawerPrimitive.Backdrop
72
- data-slot="drawer-overlay"
73
- className={cn(
74
- "fixed inset-0 z-50 min-h-dvh bg-black/80 opacity-[max(var(--drawer-overlay-min-opacity,0),calc(1-var(--drawer-swipe-progress)))] transition-opacity duration-450 ease-[cubic-bezier(0.32,0.72,0,1)] select-none data-ending-style:pointer-events-none data-ending-style:opacity-0 data-ending-style:duration-[calc(var(--drawer-swipe-strength)*400ms)] data-snap-points:[--drawer-overlay-min-opacity:0.5] data-starting-style:opacity-0 data-swiping:duration-0 supports-backdrop-filter:backdrop-blur-xs supports-[-webkit-touch-callout:none]:absolute",
75
- className
76
- )}
77
- {...props}
78
- />
79
- )
80
- }
81
-
82
- function DrawerSwipeHandle({
83
- className,
84
- ...props
85
- }: React.ComponentProps<"div">) {
86
- return (
87
- <div
88
- data-slot="drawer-swipe-handle"
89
- aria-hidden="true"
90
- className={cn(
91
- "relative z-10 flex shrink-0 cursor-grab transition-opacity duration-200 group-data-nested-drawer-open/drawer-popup:opacity-0 group-data-nested-drawer-swiping/drawer-popup:opacity-100 group-data-[swipe-axis=x]/drawer-popup:h-full group-data-[swipe-axis=x]/drawer-popup:w-3 group-data-[swipe-axis=x]/drawer-popup:items-center group-data-[swipe-axis=y]/drawer-popup:h-3 group-data-[swipe-axis=y]/drawer-popup:w-full group-data-[swipe-axis=y]/drawer-popup:justify-center group-data-[swipe-direction=down]/drawer-popup:items-end group-data-[swipe-direction=left]/drawer-popup:order-last group-data-[swipe-direction=left]/drawer-popup:justify-start group-data-[swipe-direction=right]/drawer-popup:justify-end group-data-[swipe-direction=up]/drawer-popup:order-last group-data-[swipe-direction=up]/drawer-popup:items-start after:block after:shrink-0 after:rounded-full after:bg-muted group-data-[swipe-axis=x]/drawer-popup:after:h-[100px] group-data-[swipe-axis=x]/drawer-popup:after:w-1.5 group-data-[swipe-axis=y]/drawer-popup:after:h-1.5 group-data-[swipe-axis=y]/drawer-popup:after:w-[100px] active:cursor-grabbing",
92
- className
93
- )}
94
- {...props}
95
- />
96
- )
97
- }
98
-
99
- function DrawerContent({
100
- className,
101
- children,
102
- ...props
103
- }: DrawerPrimitive.Popup.Props) {
104
- const { hasSnapPoints, modal, showSwipeHandle, swipeDirection } = useDrawer()
105
- const swipeAxis =
106
- swipeDirection === "down" || swipeDirection === "up" ? "y" : "x"
107
-
108
- return (
109
- <DrawerPortal data-slot="drawer-portal">
110
- {modal === true && (
111
- <DrawerOverlay data-snap-points={hasSnapPoints ? "" : undefined} />
112
- )}
113
- <DrawerPrimitive.Viewport
114
- data-slot="drawer-viewport"
115
- data-modal={modal}
116
- className="pointer-events-none fixed inset-0 z-50 select-none data-[modal=true]:pointer-events-auto"
117
- >
118
- <DrawerPrimitive.Popup
119
- data-slot="drawer-popup"
120
- data-swipe-axis={swipeAxis}
121
- data-snap-points={hasSnapPoints ? "" : undefined}
122
- className={cn(
123
- "group/drawer-popup pointer-events-auto fixed z-50 m-(--drawer-inset,0px) flex h-(--drawer-content-height) max-h-(--drawer-content-max-height,none) min-h-0 w-(--drawer-content-width,auto) transform-[translate3d(var(--translate-x,0px),var(--translate-y,0px),0)_scale(var(--stack-scale))] flex-col rounded-4xl border border-popover bg-popover text-sm text-popover-foreground transition-[transform,height,opacity,filter] duration-450 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-transform outline-none select-none [--drawer-bleed-background:transparent] [--drawer-inset:--spacing(2)] [interpolate-size:allow-keywords] dark:border-border",
124
- "data-nested-drawer-open:overflow-hidden data-nested-drawer-open:brightness-95",
125
- "after:pointer-events-none after:absolute after:bg-(--drawer-bleed-background,var(--color-popover)) data-[swipe-axis=x]:after:inset-y-0 data-[swipe-axis=x]:after:w-(--bleed) data-[swipe-axis=y]:after:inset-x-0 data-[swipe-axis=y]:after:h-(--bleed) data-[swipe-direction=down]:after:top-full data-[swipe-direction=left]:after:end-full data-[swipe-direction=right]:after:start-full data-[swipe-direction=up]:after:bottom-full",
126
- "[--drawer-content-height:var(--drawer-height,auto)] data-[swipe-axis=x]:[--drawer-content-width:75%] data-[swipe-axis=y]:[--drawer-content-max-height:calc(100dvh-6rem)] data-[swipe-axis=y]:data-snap-points:[--drawer-content-height:100dvh] data-[swipe-axis=x]:sm:[--drawer-content-width:24rem]",
127
- "[--bleed:3rem] [--peek:1rem] [--stack-height:var(--drawer-frontmost-height,var(--drawer-height,0px))] [--stack-peek-offset:max(0px,calc((var(--nested-drawers)-var(--stack-progress))*var(--peek)))] [--stack-progress:clamp(0,var(--drawer-swipe-progress),1)] [--stack-scale-base:max(0,calc(1-(var(--nested-drawers)*var(--stack-step))))] [--stack-scale:clamp(0,calc(var(--stack-scale-base)+(var(--stack-step)*var(--stack-progress))),1)] [--stack-shrink:calc(1-var(--stack-scale))] [--stack-step:0.05]",
128
- "data-ending-style:transform-(--closed-transform) data-ending-style:opacity-[0.9999] data-ending-style:duration-[calc(var(--drawer-swipe-strength)*400ms)] data-nested-drawer-swiping:duration-0 data-ending-style:data-nested-drawer-swiping:duration-[calc(var(--drawer-swipe-strength)*400ms)] data-starting-style:transform-(--closed-transform) data-swiping:duration-0 data-ending-style:data-swiping:duration-[calc(var(--drawer-swipe-strength)*400ms)]",
129
- "data-[swipe-axis=y]:inset-x-0 data-[swipe-axis=y]:data-nested-drawer-open:h-(--stack-height)",
130
- "data-[swipe-axis=x]:inset-y-0 data-[swipe-axis=x]:flex-row",
131
- "data-[swipe-direction=down]:bottom-0 data-[swipe-direction=down]:origin-bottom data-[swipe-direction=down]:[--closed-transform:translate3d(0,calc(100%+var(--drawer-inset,0px)+2px),0)] data-[swipe-direction=down]:[--translate-y:calc(var(--drawer-snap-point-offset,0px)+var(--drawer-swipe-movement-y)-var(--stack-peek-offset)-(var(--stack-shrink)*var(--stack-height)))]",
132
- "data-[swipe-direction=up]:top-0 data-[swipe-direction=up]:origin-top data-[swipe-direction=up]:[--closed-transform:translate3d(0,calc(-100%-var(--drawer-inset,0px)-2px),0)] data-[swipe-direction=up]:[--translate-y:calc(var(--drawer-snap-point-offset,0px)+var(--drawer-swipe-movement-y)+var(--stack-peek-offset)+(var(--stack-shrink)*var(--stack-height)))]",
133
- "data-[swipe-direction=left]:start-0 data-[swipe-direction=left]:origin-start data-[swipe-direction=left]:[--closed-transform:translate3d(calc(-100%-var(--drawer-inset,0px)-2px),0,0)] data-[swipe-direction=left]:[--translate-x:calc(var(--drawer-swipe-movement-x)+var(--stack-peek-offset)+(var(--stack-shrink)*100%))]",
134
- "data-[swipe-direction=right]:end-0 data-[swipe-direction=right]:origin-end data-[swipe-direction=right]:[--closed-transform:translate3d(calc(100%+var(--drawer-inset,0px)+2px),0,0)] data-[swipe-direction=right]:[--translate-x:calc(var(--drawer-swipe-movement-x)-var(--stack-peek-offset)-(var(--stack-shrink)*100%))]",
135
- className
136
- )}
137
- {...props}
138
- >
139
- {showSwipeHandle && <DrawerSwipeHandle />}
140
- <DrawerPrimitive.Content
141
- data-slot="drawer-content"
142
- className={cn(
143
- "flex min-h-0 flex-1 flex-col overflow-hidden overscroll-contain rounded-[inherit] transition-opacity duration-300 ease-[cubic-bezier(0.45,1.005,0,1.005)] select-text group-data-nested-drawer-open/drawer-popup:opacity-0 group-data-nested-drawer-swiping/drawer-popup:opacity-100 group-data-swiping/drawer-popup:select-none"
144
- )}
145
- >
146
- {children}
147
- </DrawerPrimitive.Content>
148
- </DrawerPrimitive.Popup>
149
- </DrawerPrimitive.Viewport>
150
- </DrawerPortal>
151
- )
152
- }
153
-
154
- function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
155
- return (
156
- <div
157
- data-slot="drawer-header"
158
- className={cn(
159
- "flex shrink-0 flex-col gap-0.5 p-4 pb-0 group-data-[swipe-axis=y]/drawer-popup:text-center md:gap-1.5 md:text-start",
160
- className
161
- )}
162
- {...props}
163
- />
164
- )
165
- }
166
-
167
- function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
168
- return (
169
- <div
170
- data-slot="drawer-footer"
171
- className={cn("mt-auto flex shrink-0 flex-col gap-2 p-4 pt-0", className)}
172
- {...props}
173
- />
174
- )
175
- }
176
-
177
- function DrawerTitle({ className, ...props }: DrawerPrimitive.Title.Props) {
178
- return (
179
- <DrawerPrimitive.Title
180
- data-slot="drawer-title"
181
- className={cn(
182
- "text-base font-medium text-foreground",
183
- className
184
- )}
185
- {...props}
186
- />
187
- )
188
- }
189
-
190
- function DrawerDescription({
191
- className,
192
- ...props
193
- }: DrawerPrimitive.Description.Props) {
194
- return (
195
- <DrawerPrimitive.Description
196
- data-slot="drawer-description"
197
- className={cn("text-sm text-balance text-muted-foreground", className)}
198
- {...props}
199
- />
200
- )
201
- }
202
-
203
- export {
204
- Drawer,
205
- DrawerPortal,
206
- DrawerOverlay,
207
- DrawerSwipeHandle,
208
- DrawerTrigger,
209
- DrawerClose,
210
- DrawerContent,
211
- DrawerHeader,
212
- DrawerFooter,
213
- DrawerTitle,
214
- DrawerDescription,
215
- }
@@ -1,187 +0,0 @@
1
- import { actorPermissions, count, defaultFields, find, findOne, invalid, type Ctx, type Held } from 'mithaq'
2
- import { defineKayan, rows, organization } from '../mithaq.ts'
3
- import { platformClosed } from '../tenant-access.ts'
4
- import { auditLogName, type Change } from './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
- if (!entry || typeof entry !== 'object' || typeof (entry as { changes?: unknown }).changes !== 'string') return entry
52
- const { kayan, changes } = entry as { kayan: string; changes: string }
53
- const all = JSON.parse(changes) as Record<string, Change>
54
- const kept = Object.fromEntries(Object.entries(all).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 actor = (entry as { actor?: unknown }).actor
57
- const named = !readsPeople(held) && typeof actor === 'string' ? { actor: commandActor.exec(actor)?.[1] ?? actor } : {}
58
- return { ...entry, ...named, changes: JSON.stringify(kept) }
59
- }
60
- const data: unknown = ctx.data
61
- const page = data && typeof data === 'object' && Array.isArray((data as { docs?: unknown }).docs) ? (data as { docs: unknown[] }) : undefined
62
- return { ...ctx, data: (page ? { ...page, docs: page.docs.map(trim) } : trim(data)) as Record<string, unknown> }
63
- }
64
-
65
- /**
66
- * The columns the log is read by. Nothing else filters or orders it, `changes`
67
- * above all: a `contains` over its text would read a value out of a column the
68
- * reader holds no key for, before `visibleChanges` trims the page. The
69
- * organization is core's own filter and `impersonatedBy` names staff.
70
- */
71
- const queryable = new Set(['kayan', 'record', 'createdAt', 'operation', 'actor'])
72
-
73
- const namedIn = (where: unknown): string[] => {
74
- if (!where || typeof where !== 'object') return []
75
- return Object.entries(where).flatMap(([key, value]) =>
76
- (key === 'and' || key === 'or') && Array.isArray(value) ? value.flatMap(namedIn) : [key])
77
- }
78
-
79
- /**
80
- * The step before the read: a `where` or a `sort` naming any column but those
81
- * is refused 400, before the store is asked, on every read the log declares.
82
- * The runner's own checks still run after it on what it lets through.
83
- */
84
- const queriedByQueryable = (ctx: Ctx): Ctx => {
85
- const { where, sort } = ctx.data as { where?: unknown; sort?: unknown }
86
- const field = namedIn(where).find((name) => !queryable.has(name))
87
- if (field !== undefined) {
88
- throw invalid({ message: `Kayan "auditLog" is filtered by ${[...queryable].join(', ')} alone, and not by "${field}".`, key: 'error.invalid.where', params: { kayan: 'auditLog', field } })
89
- }
90
- // A reader of no people names an actor in no filter and no sort: a pattern, a range, an order or
91
- // even an equality over the string would let it confirm a guessed operating-system user of a
92
- // command's actor.
93
- if (!readsPeople(actorPermissions(ctx.actor)) && (namedIn(where).includes('actor') || (typeof sort === 'string' && sort.replace(/^-/, '') === 'actor'))) {
94
- 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' } })
95
- }
96
- if (sort !== undefined && !(typeof sort === 'string' && queryable.has(sort.replace(/^-/, '')))) {
97
- throw invalid({ message: `Kayan "auditLog" is sorted by ${[...queryable].join(', ')} alone.`, key: 'error.invalid.sort_field', params: { kayan: 'auditLog', field: String(sort) } })
98
- }
99
- return ctx
100
- }
101
-
102
- /**
103
- * The audit log's rows: an organization reads its own entries, and nothing writes
104
- * one through the log, a caller of any role included, since the service
105
- * target's sink writes each entry through the store inside the write it
106
- * records. Every platform role is closed: the platform lists no entry, and a
107
- * staff member's own impersonated writes are the entries it would read.
108
- */
109
- const auditLogAccess = rows({
110
- '*': { create: false, read: true, update: false, delete: false },
111
- ...platformClosed,
112
- })
113
-
114
- /**
115
- * The audit log: one row per row a write changed, written by the service
116
- * target's audit sink inside that write's own transaction and by nothing else.
117
- * It declares no operation that writes, so no caller creates, edits or deletes
118
- * an entry, and each of its rows answers `false` for every write besides.
119
- *
120
- * It is its organization's own, and a record's entries are read on that record's edit
121
- * page. The dashboard lists it for its access report alone, which is what the
122
- * edit pages' panel is drawn by, and draws no page of it. The platform lists
123
- * none of it and reads none of it.
124
- */
125
- export const auditLog = defineKayan({
126
- name: auditLogName,
127
- dbName: 'audit_log',
128
- messages: {
129
- singular: { en: 'Audit entry', ar: 'سجل تدقيق' },
130
- plural: { en: 'Audit log', ar: 'سجل التدقيق' },
131
- // What the activity panel draws beside the field labels.
132
- activity: { en: 'Activity', ar: 'النشاط' },
133
- loading: { en: 'Loading activity…', ar: 'جارٍ تحميل النشاط…' },
134
- empty: { en: 'Nothing has changed here yet.', ar: 'لم يتغير شيء هنا بعد.' },
135
- failed: { en: 'The activity could not be read.', ar: 'تعذرت قراءة النشاط.' },
136
- more: { en: 'Show older', ar: 'عرض الأقدم' },
137
- system: { en: 'The system', ar: 'النظام' },
138
- event: { en: '{action} by {actor}', ar: '{action} بواسطة {actor}' },
139
- // Staff writing as a member: the real person first, as the member they wrote as.
140
- impersonated: { en: '{staff} as {member}', ar: '{staff} بصفة {member}' },
141
- // One per operation a tenant kayan writes through, which is every entry the sink records.
142
- action: {
143
- create: { en: 'Created', ar: 'أُنشئ' },
144
- update: { en: 'Updated', ar: 'حُدِّث' },
145
- delete: { en: 'Deleted', ar: 'حُذف' },
146
- updateMany: { en: 'Updated', ar: 'حُدِّث' },
147
- deleteMany: { en: 'Deleted', ar: 'حُذف' },
148
- restoreVersion: { en: 'Restored a version', ar: 'استُعيد إصدار' },
149
- archive: { en: 'Archived', ar: 'أُرشف' },
150
- archiveMany: { en: 'Archived', ar: 'أُرشف' },
151
- },
152
- someone: { en: 'Someone', ar: 'شخص ما' },
153
- // A command line write, named for a reader of the people: the command and
154
- // the operating-system user who ran it.
155
- command: { en: 'the command {command} ({user})', ar: 'الأمر {command} ({user})' },
156
- // The viewer's own entry, which needs no read of the people to name. Its
157
- // sentence is its own, since Arabic joins the person to "by" in one word,
158
- // "بواسطتك", and not as "بواسطة أنت".
159
- you: { en: 'You', ar: 'أنت' },
160
- eventYou: { en: '{action} by You', ar: '{action} بواسطتك' },
161
- none: { en: 'No field you may read changed.', ar: 'لم يتغير حقل يمكنك قراءته.' },
162
- },
163
- access: { ...auditLogAccess, read: readableKayans },
164
- targets: {
165
- dashboard: {},
166
- service: {
167
- operations: {
168
- find: { steps: [queriedByQueryable, find, visibleChanges] },
169
- findByID: { steps: [queriedByQueryable, findOne, visibleChanges] },
170
- count: { steps: [queriedByQueryable, count] },
171
- },
172
- },
173
- },
174
- fields: {
175
- id: defaultFields.id,
176
- createdAt: defaultFields.createdAt,
177
- organization,
178
- kayan: { kind: 'text', label: { en: 'Kayan', ar: 'الكيان' }, rules: { required: true, maxLength: 60 } },
179
- record: { kind: 'text', label: { en: 'Record', ar: 'السجل' }, rules: { required: true, index: true } },
180
- operation: { kind: 'text', label: { en: 'Operation', ar: 'العملية' }, rules: { required: true, maxLength: 60 } },
181
- actor: { kind: 'text', label: { en: 'Actor', ar: 'المنفذ' } },
182
- impersonatedBy: { kind: 'text', label: { en: 'Impersonated by', ar: 'انتحل الصفة' } },
183
- // Never an order, which the runner refuses by name for every read, and never
184
- // a filter or a search, which the dashboard offers only where a field opts in.
185
- changes: { kind: 'text', label: { en: 'Changes', ar: 'التغييرات' }, rules: { required: true }, targets: { dashboard: { sortable: false } } },
186
- },
187
- })
@@ -1,243 +0,0 @@
1
- // The entries are read in the browser, as the viewer, from the log's own `find`,
2
- // which answers only this tenant's entries of kayan the viewer lists and only the
3
- // changes of columns it reads. The page holds only the fields this viewer reads
4
- // as well, so a change the server let through for a column the page left out is
5
- // not drawn either.
6
- import { useEffect, useMemo, useState, type ReactNode } from 'react'
7
- import type { ClientField, ClientKayan, ComponentProps, Row } from 'mithaq/admin'
8
- import { useAccess, useClient, useKayan, useLocale, useRecord, useScreen, useUser } from 'mithaq/admin/react'
9
- import { empty } from '../sink.ts'
10
- import { Avatar, AvatarFallback } from '@/components/ui/avatar'
11
- import { Button } from '@/components/ui/button'
12
- import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
13
- import { cellFor } from '@/components/mithaq/cells.tsx'
14
- import { formatDate, labelFor, operationLabelFor, type FindApi, type FoundPage, type Translate } from '@/components/mithaq/controls.tsx'
15
- import { useRelationshipTitles, type RelationshipTitles } from '@/components/mithaq/relationship-cell.tsx'
16
-
17
- /** One entry as the log's `find` answers it. */
18
- export type Entry = {
19
- id: string
20
- createdAt: string | Date
21
- operation: string
22
- actor?: string | null
23
- impersonatedBy?: string | null
24
- changes: string
25
- }
26
-
27
- const limit = 10
28
-
29
- /** The columns every kayan of this project carries and no person edits, which the panel leaves out. */
30
- const technical = new Set(['id', 'organization', 'createdAt', 'updatedAt'])
31
-
32
- /**
33
- * Who a write ran as, as a relationship to the provider's people, so the panel
34
- * names them through the same read a relationship cell does: the active
35
- * organization's members by their name, and anyone that read does not answer as
36
- * someone.
37
- */
38
- const actorField = { name: 'actor', kind: 'relationship', relationTo: 'user', label: {}, rules: {}, component: 'relationship', cell: 'relationship' } as unknown as ClientField
39
-
40
- /**
41
- * What the sink writes for a command: the command, then the operating-system user
42
- * in parentheses. A user's id holds no space, so no person is read as one.
43
- */
44
- const commandOf = /^(.+?) \((.+)\)$/
45
-
46
- const changesOf = (entry: Entry): [string, { from: unknown; to: unknown }][] => {
47
- try {
48
- return Object.entries(JSON.parse(entry.changes) as Record<string, { from: unknown; to: unknown }>)
49
- } catch {
50
- return []
51
- }
52
- }
53
-
54
- const units: [Intl.RelativeTimeFormatUnit, number][] = [['year', 31_536_000], ['month', 2_592_000], ['week', 604_800], ['day', 86_400], ['hour', 3_600], ['minute', 60], ['second', 1]]
55
-
56
- /** How long ago, in the viewer's own locale, in the largest unit that is at least one. */
57
- export const relative = ({ at, locale, now = Date.now() }: { at: Date; locale: string; now?: number }) => {
58
- const seconds = Math.round((at.getTime() - now) / 1000)
59
- const [unit, size] = units.find(([, size]) => Math.abs(seconds) >= size) ?? units.at(-1)!
60
- return new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }).format(Math.round(seconds / size), unit)
61
- }
62
-
63
- type EntriesProps = {
64
- entries: readonly Entry[]
65
- /** The viewer's own id, whom an entry that names them calls "You", without a read of the people. */
66
- viewer?: string
67
- /** Whether the viewer reads the provider's people, which is what lets a command's operating-system user be named. */
68
- people?: boolean
69
- kayan: ClientKayan
70
- titles?: RelationshipTitles
71
- t: Translate
72
- locale: string
73
- }
74
-
75
- /** The entries drawn, with nothing read: what the panel shows once a page has come back. */
76
- export function ActivityEntries({ entries, kayan, titles, t, locale, viewer, people }: EntriesProps) {
77
- const copy = { t, locale }
78
- // A person the title read did not name, one the reader may not read among
79
- // them, is someone, never the raw id.
80
- const named = (id: string | null | undefined) => {
81
- if (!id) return t('auditLog.system', 'The system') ?? 'The system'
82
- if (id === viewer) return t('auditLog.you', 'You') ?? 'You'
83
- // A command's actor, which the sink writes as one string, and which no
84
- // people read names: said in the viewer's locale to a reader of the
85
- // people, with each part isolated so the Arabic sentence keeps the
86
- // command's own direction.
87
- const command = people ? commandOf.exec(id) : null
88
- if (command) return (t('auditLog.command', 'the command {command} ({user})') ?? '').replace('{command}', () => `\u2068${command[1]}\u2069`).replace('{user}', () => `\u2068${command[2]}\u2069`)
89
- const title = titles?.get(`user:${id}`)
90
- return typeof title === 'string' ? title : (t('auditLog.someone', 'Someone') ?? 'Someone')
91
- }
92
- const drawn = (field: ClientField, value: unknown): ReactNode =>
93
- empty(value) ? '—' : cellFor(field)({ kayan, field, value, t, locale, titles })
94
- return (
95
- <ol className="grid gap-4 text-sm">
96
- {entries.map((entry) => {
97
- const at = new Date(entry.createdAt)
98
- // Every operation has its words in the log's own scope, in the past
99
- // tense the sentence reads in, and one a project adds before it
100
- // writes them falls back to the operation's own label.
101
- const action = t(`auditLog.action.${entry.operation}`, operationLabelFor({ kayan, operation: { name: entry.operation }, copy })) ?? entry.operation
102
- // Staff writing inside a member's session is named first, as the member.
103
- const actor = entry.impersonatedBy
104
- ? (t('auditLog.impersonated', '{staff} as {member}') ?? '{staff} as {member}').replace('{staff}', () => named(entry.impersonatedBy)).replace('{member}', () => named(entry.actor))
105
- : named(entry.actor)
106
- const shown = changesOf(entry).flatMap(([name, change]) => {
107
- const field = technical.has(name) ? undefined : kayan.fields.find((one) => one.name === name)
108
- return field ? [{ field, change }] : []
109
- })
110
- return (
111
- <li key={entry.id} data-entry={entry.operation} className="grid grid-cols-[2rem_minmax(0,1fr)] gap-3">
112
- <Avatar className="mt-0.5 size-8">
113
- <AvatarFallback>{(named(entry.impersonatedBy ?? entry.actor).trim()[0] ?? '?').toUpperCase()}</AvatarFallback>
114
- </Avatar>
115
- <div className="min-w-0">
116
- <div className="flex flex-wrap items-baseline justify-between gap-2">
117
- <p data-event className="font-medium">
118
- {(viewer && entry.actor === viewer && !entry.impersonatedBy ? (t('auditLog.eventYou', '{action} by You') ?? '{action} by You') : (t('auditLog.event', '{action} by {actor}') ?? '{action} by {actor}')).replace('{action}', () => action).replace('{actor}', () => actor)}
119
- </p>
120
- <time dateTime={at.toISOString()} className="text-muted-foreground shrink-0 text-xs">
121
- {relative({ at, locale })}
122
- </time>
123
- </div>
124
- <p className="text-muted-foreground text-xs">
125
- {formatDate({ value: at, locale })}
126
- </p>
127
- {shown.length ? (
128
- <ul className="text-muted-foreground mt-1.5 grid gap-1">
129
- {shown.map(({ field, change }) => (
130
- <li key={field.name} data-change={field.name} className="break-words">
131
- <span className="text-foreground">{labelFor({ kayan, field, copy })}</span>
132
- {': '}
133
- <span data-from>{drawn(field, change.from)}</span>
134
- <span aria-hidden className="mx-1 inline-block rtl:rotate-180">→</span>
135
- <span data-to>{drawn(field, change.to)}</span>
136
- </li>
137
- ))}
138
- </ul>
139
- ) : (
140
- <p className="text-muted-foreground mt-1.5">{t('auditLog.none', 'No field you may read changed.')}</p>
141
- )}
142
- </div>
143
- </li>
144
- )
145
- })}
146
- </ol>
147
- )
148
- }
149
-
150
- /**
151
- * The panel a page names as a component node. It reads the row the page
152
- * draws and the kayan it belongs to off the screen, so one file serves every
153
- * kayan's edit page.
154
- */
155
- export function Activity(_: ComponentProps) {
156
- const { t, locale } = useLocale()
157
- const kayan = useKayan()
158
- const record = useRecord()
159
- const client = useClient()
160
- const screen = useScreen()
161
- const viewer = useUser().id
162
- const people = useAccess().collections?.user?.read === true
163
- const id = record?.id === undefined ? undefined : String(record.id)
164
- // The row as this page last drew it: a save moves its `updatedAt`, and the
165
- // panel reads its entries again from the first page, the new one on top.
166
- const seen = `${id}@${String(record?.updatedAt ?? '')}`
167
- // What has been read, and how many pages were asked for, each held with the
168
- // row it is for, so a page that moves to another row, or the same row
169
- // saved, starts at its first page rather than drawing the last one's.
170
- const [read, setRead] = useState<{ seen: string; docs: Entry[]; more: boolean } | { seen: string; failed: true }>()
171
- const [asked, setAsked] = useState({ seen, page: 1 })
172
- const page = asked.seen === seen ? asked.page : 1
173
-
174
- // The client is left out of the dependencies: the host makes a new one on
175
- // every render of the page, and each posts to the same route, so following it
176
- // would read the same page again on every render.
177
- useEffect(() => {
178
- if (!id) return
179
- let live = true
180
- const where = { and: [{ kayan: { equals: kayan.name } }, { record: { equals: id } }] }
181
- ;(client({ kayan: 'auditLog', operation: 'find', input: { where, sort: '-createdAt', limit, page, depth: 0 } }) as Promise<FoundPage>).then(
182
- (found) => {
183
- if (!live) return
184
- // The pages before this one, as far as they go, and this one after
185
- // them, less any entry already drawn: a write that lands between two
186
- // pages moves every older entry down one, so the next page opens on
187
- // the one the last page ended with.
188
- setRead((held) => {
189
- const before = held?.seen === seen && 'docs' in held ? held.docs.slice(0, (page - 1) * limit) : []
190
- const drawn = new Set(before.map((entry) => entry.id))
191
- return { seen, docs: [...before, ...(found.docs as Entry[]).filter((entry) => !drawn.has(entry.id))], more: found.hasNextPage }
192
- })
193
- },
194
- () => live && setRead({ seen, failed: true }),
195
- )
196
- return () => {
197
- live = false
198
- }
199
- }, [kayan.name, seen, page])
200
-
201
- // Every kayan this screen crosses, read through the screen's own client, so
202
- // a relationship's value and the actor are named the way a list names them.
203
- const kayans = useMemo(() => Object.values(screen.kayan), [screen])
204
- const api = useMemo<FindApi>(
205
- () => Object.fromEntries(kayans.map((one) => [one.name, { find: (input: Row) => client({ kayan: one.name, operation: 'find', input }) as Promise<FoundPage> }])),
206
- [kayans],
207
- )
208
- const shown = read?.seen === seen ? read : undefined
209
- const entries = shown && 'docs' in shown ? shown.docs : []
210
- // Each entry's actor, both people of an impersonation, and every value a
211
- // change holds, as rows the title read groups by the kayan each points at.
212
- const docs = entries.flatMap((entry) => [
213
- { actor: [entry.actor, entry.impersonatedBy].filter(Boolean) },
214
- ...changesOf(entry).flatMap(([name, change]) => [{ [name]: change.from }, { [name]: change.to }]),
215
- ])
216
- const titles = useRelationshipTitles({ fields: [...kayan.fields, actorField], docs, context: { api, client: kayans, locale } })
217
-
218
- return (
219
- <Card data-panel="activity">
220
- <CardHeader>
221
- <CardTitle>{t('auditLog.activity', 'Activity')}</CardTitle>
222
- </CardHeader>
223
- <CardContent>
224
- {shown === undefined ? (
225
- <p className="text-muted-foreground text-sm">{t('auditLog.loading', 'Loading activity…')}</p>
226
- ) : 'failed' in shown ? (
227
- <p className="text-destructive text-sm">{t('auditLog.failed', 'The activity could not be read.')}</p>
228
- ) : !entries.length ? (
229
- <p className="text-muted-foreground text-sm">{t('auditLog.empty', 'Nothing has changed here yet.')}</p>
230
- ) : (
231
- <div className="grid gap-4">
232
- <ActivityEntries entries={entries} kayan={kayan} titles={titles} t={t} locale={locale} viewer={viewer} people={people} />
233
- {shown.more ? (
234
- <Button type="button" variant="outline" size="sm" onClick={() => setAsked({ seen, page: page + 1 })}>
235
- {t('auditLog.more', 'Show older')}
236
- </Button>
237
- ) : null}
238
- </div>
239
- )}
240
- </CardContent>
241
- </Card>
242
- )
243
- }