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