create-mithaq 0.0.0-stage → 0.1.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 (357) hide show
  1. package/LICENSE +22 -0
  2. package/LICENSES/payload.txt +22 -0
  3. package/LICENSES/ponytail.txt +21 -0
  4. package/index.js +127 -0
  5. package/package.json +19 -4
  6. package/template/apps/asas-platform/components.json +25 -0
  7. package/template/apps/asas-platform/gitignore +3 -0
  8. package/template/apps/asas-platform/package.json +55 -0
  9. package/template/apps/asas-platform/public/favicon.svg +1 -0
  10. package/template/apps/asas-platform/src/api.ts +147 -0
  11. package/template/apps/asas-platform/src/auth-frame.tsx +150 -0
  12. package/template/apps/asas-platform/src/components/mithaq/cells.tsx +112 -0
  13. package/template/apps/asas-platform/src/components/mithaq/controls.tsx +650 -0
  14. package/template/apps/asas-platform/src/components/mithaq/field-shell.tsx +53 -0
  15. package/template/apps/asas-platform/src/components/mithaq/filter-operators.ts +108 -0
  16. package/template/apps/asas-platform/src/components/mithaq/form.tsx +1467 -0
  17. package/template/apps/asas-platform/src/components/mithaq/list-columns.tsx +108 -0
  18. package/template/apps/asas-platform/src/components/mithaq/list-filters.tsx +368 -0
  19. package/template/apps/asas-platform/src/components/mithaq/list-pagination.tsx +153 -0
  20. package/template/apps/asas-platform/src/components/mithaq/list-query.ts +432 -0
  21. package/template/apps/asas-platform/src/components/mithaq/list-selection.tsx +74 -0
  22. package/template/apps/asas-platform/src/components/mithaq/list.tsx +865 -0
  23. package/template/apps/asas-platform/src/components/mithaq/nav.tsx +871 -0
  24. package/template/apps/asas-platform/src/components/mithaq/nodes.tsx +1024 -0
  25. package/template/apps/asas-platform/src/components/mithaq/relationship-cell.tsx +127 -0
  26. package/template/apps/asas-platform/src/components/mithaq/relationship-control.tsx +400 -0
  27. package/template/apps/asas-platform/src/components/mithaq/rich-text-nodes.ts +6 -0
  28. package/template/apps/asas-platform/src/components/mithaq/rich-text.tsx +1083 -0
  29. package/template/apps/asas-platform/src/components/mithaq/upload.tsx +686 -0
  30. package/template/apps/asas-platform/src/components/ui/accordion.tsx +74 -0
  31. package/template/apps/asas-platform/src/components/ui/alert-dialog.tsx +185 -0
  32. package/template/apps/asas-platform/src/components/ui/avatar.tsx +106 -0
  33. package/template/apps/asas-platform/src/components/ui/badge.tsx +48 -0
  34. package/template/apps/asas-platform/src/components/ui/breadcrumb.tsx +124 -0
  35. package/template/apps/asas-platform/src/components/ui/button-group.tsx +87 -0
  36. package/template/apps/asas-platform/src/components/ui/button.tsx +55 -0
  37. package/template/apps/asas-platform/src/components/ui/calendar.tsx +223 -0
  38. package/template/apps/asas-platform/src/components/ui/card.tsx +102 -0
  39. package/template/apps/asas-platform/src/components/ui/chart.tsx +371 -0
  40. package/template/apps/asas-platform/src/components/ui/checkbox.tsx +28 -0
  41. package/template/apps/asas-platform/src/components/ui/collapsible.tsx +19 -0
  42. package/template/apps/asas-platform/src/components/ui/combobox.tsx +308 -0
  43. package/template/apps/asas-platform/src/components/ui/dialog.tsx +158 -0
  44. package/template/apps/asas-platform/src/components/ui/direction.tsx +6 -0
  45. package/template/apps/asas-platform/src/components/ui/directional-icon.tsx +20 -0
  46. package/template/apps/asas-platform/src/components/ui/drawer.tsx +216 -0
  47. package/template/apps/asas-platform/src/components/ui/dropdown-menu.tsx +267 -0
  48. package/template/apps/asas-platform/src/components/ui/field.tsx +236 -0
  49. package/template/apps/asas-platform/src/components/ui/input-group.tsx +155 -0
  50. package/template/apps/asas-platform/src/components/ui/input-otp.tsx +84 -0
  51. package/template/apps/asas-platform/src/components/ui/input.tsx +19 -0
  52. package/template/apps/asas-platform/src/components/ui/label.tsx +19 -0
  53. package/template/apps/asas-platform/src/components/ui/popover.tsx +90 -0
  54. package/template/apps/asas-platform/src/components/ui/radio-group.tsx +37 -0
  55. package/template/apps/asas-platform/src/components/ui/resizable.tsx +47 -0
  56. package/template/apps/asas-platform/src/components/ui/scroll-area.tsx +54 -0
  57. package/template/apps/asas-platform/src/components/ui/select.tsx +204 -0
  58. package/template/apps/asas-platform/src/components/ui/separator.tsx +24 -0
  59. package/template/apps/asas-platform/src/components/ui/sheet.tsx +139 -0
  60. package/template/apps/asas-platform/src/components/ui/sidebar.tsx +714 -0
  61. package/template/apps/asas-platform/src/components/ui/skeleton.tsx +13 -0
  62. package/template/apps/asas-platform/src/components/ui/slider.tsx +55 -0
  63. package/template/apps/asas-platform/src/components/ui/spinner.tsx +10 -0
  64. package/template/apps/asas-platform/src/components/ui/switch.tsx +29 -0
  65. package/template/apps/asas-platform/src/components/ui/table.tsx +115 -0
  66. package/template/apps/asas-platform/src/components/ui/tabs.tsx +79 -0
  67. package/template/apps/asas-platform/src/components/ui/textarea.tsx +17 -0
  68. package/template/apps/asas-platform/src/components/ui/toggle-group.tsx +89 -0
  69. package/template/apps/asas-platform/src/components/ui/toggle.tsx +44 -0
  70. package/template/apps/asas-platform/src/components/ui/tooltip.tsx +63 -0
  71. package/template/apps/asas-platform/src/copy.ts +1 -0
  72. package/template/apps/asas-platform/src/hooks/use-mobile.ts +19 -0
  73. package/template/apps/asas-platform/src/lib/utils.ts +1 -0
  74. package/template/apps/asas-platform/src/links.ts +38 -0
  75. package/template/apps/asas-platform/src/locale-switch.tsx +26 -0
  76. package/template/apps/asas-platform/src/redirect.ts +1 -0
  77. package/template/apps/asas-platform/src/request-headers.ts +11 -0
  78. package/template/apps/asas-platform/src/routeTree.gen.ts +181 -0
  79. package/template/apps/asas-platform/src/router.tsx +7 -0
  80. package/template/apps/asas-platform/src/routes/(screens)/platform/files.ts +4 -0
  81. package/template/apps/asas-platform/src/routes/(screens)/platform/screen.json +3609 -0
  82. package/template/apps/asas-platform/src/routes/(screens)/platform/types.ts +145 -0
  83. package/template/apps/asas-platform/src/routes/(screens)/platform.$.tsx +17 -0
  84. package/template/apps/asas-platform/src/routes/(screens)/platform.index.tsx +10 -0
  85. package/template/apps/asas-platform/src/routes/(screens)/platform.tsx +13 -0
  86. package/template/apps/asas-platform/src/routes/__root.tsx +95 -0
  87. package/template/apps/asas-platform/src/routes/api/$.ts +30 -0
  88. package/template/apps/asas-platform/src/routes/auth.sign-in.tsx +72 -0
  89. package/template/apps/asas-platform/src/routes/unauthorized.tsx +122 -0
  90. package/template/apps/asas-platform/src/screen-form.tsx +34 -0
  91. package/template/apps/asas-platform/src/screen-list.tsx +139 -0
  92. package/template/apps/asas-platform/src/screen-names.ts +10 -0
  93. package/template/apps/asas-platform/src/screens.tsx +868 -0
  94. package/template/apps/asas-platform/src/server.ts +20 -0
  95. package/template/apps/asas-platform/src/slide.ts +22 -0
  96. package/template/apps/asas-platform/src/start.ts +42 -0
  97. package/template/apps/asas-platform/src/styles.css +200 -0
  98. package/template/apps/asas-platform/src/theme.ts +2 -0
  99. package/template/apps/asas-platform/tsconfig.json +19 -0
  100. package/template/apps/asas-platform/vite.config.ts +26 -0
  101. package/template/apps/asas-tanstack/components.json +25 -0
  102. package/template/apps/asas-tanstack/gitignore +3 -0
  103. package/template/apps/asas-tanstack/package.json +56 -0
  104. package/template/apps/asas-tanstack/public/favicon.svg +1 -0
  105. package/template/apps/asas-tanstack/src/api.ts +147 -0
  106. package/template/apps/asas-tanstack/src/auth-frame.tsx +150 -0
  107. package/template/apps/asas-tanstack/src/components/mithaq/cells.tsx +112 -0
  108. package/template/apps/asas-tanstack/src/components/mithaq/controls.tsx +650 -0
  109. package/template/apps/asas-tanstack/src/components/mithaq/field-shell.tsx +53 -0
  110. package/template/apps/asas-tanstack/src/components/mithaq/filter-operators.ts +108 -0
  111. package/template/apps/asas-tanstack/src/components/mithaq/form.tsx +1467 -0
  112. package/template/apps/asas-tanstack/src/components/mithaq/list-columns.tsx +108 -0
  113. package/template/apps/asas-tanstack/src/components/mithaq/list-filters.tsx +368 -0
  114. package/template/apps/asas-tanstack/src/components/mithaq/list-pagination.tsx +153 -0
  115. package/template/apps/asas-tanstack/src/components/mithaq/list-query.ts +432 -0
  116. package/template/apps/asas-tanstack/src/components/mithaq/list-selection.tsx +74 -0
  117. package/template/apps/asas-tanstack/src/components/mithaq/list.tsx +865 -0
  118. package/template/apps/asas-tanstack/src/components/mithaq/nav.tsx +871 -0
  119. package/template/apps/asas-tanstack/src/components/mithaq/nodes.tsx +1024 -0
  120. package/template/apps/asas-tanstack/src/components/mithaq/relationship-cell.tsx +127 -0
  121. package/template/apps/asas-tanstack/src/components/mithaq/relationship-control.tsx +400 -0
  122. package/template/apps/asas-tanstack/src/components/mithaq/rich-text-nodes.ts +6 -0
  123. package/template/apps/asas-tanstack/src/components/mithaq/rich-text.tsx +1083 -0
  124. package/template/apps/asas-tanstack/src/components/mithaq/upload.tsx +686 -0
  125. package/template/apps/asas-tanstack/src/components/ui/accordion.tsx +10 -0
  126. package/template/apps/asas-tanstack/src/components/ui/alert-dialog.tsx +173 -0
  127. package/template/apps/asas-tanstack/src/components/ui/avatar.tsx +104 -0
  128. package/template/apps/asas-tanstack/src/components/ui/badge.tsx +48 -0
  129. package/template/apps/asas-tanstack/src/components/ui/breadcrumb.tsx +125 -0
  130. package/template/apps/asas-tanstack/src/components/ui/button-group.tsx +87 -0
  131. package/template/apps/asas-tanstack/src/components/ui/button.tsx +56 -0
  132. package/template/apps/asas-tanstack/src/components/ui/calendar.tsx +223 -0
  133. package/template/apps/asas-tanstack/src/components/ui/card.tsx +94 -0
  134. package/template/apps/asas-tanstack/src/components/ui/chart.tsx +369 -0
  135. package/template/apps/asas-tanstack/src/components/ui/checkbox.tsx +27 -0
  136. package/template/apps/asas-tanstack/src/components/ui/collapsible.tsx +19 -0
  137. package/template/apps/asas-tanstack/src/components/ui/combobox.tsx +300 -0
  138. package/template/apps/asas-tanstack/src/components/ui/dialog.tsx +149 -0
  139. package/template/apps/asas-tanstack/src/components/ui/direction.tsx +5 -0
  140. package/template/apps/asas-tanstack/src/components/ui/directional-icon.tsx +20 -0
  141. package/template/apps/asas-tanstack/src/components/ui/drawer.tsx +215 -0
  142. package/template/apps/asas-tanstack/src/components/ui/dropdown-menu.tsx +267 -0
  143. package/template/apps/asas-tanstack/src/components/ui/field.tsx +225 -0
  144. package/template/apps/asas-tanstack/src/components/ui/input-group.tsx +147 -0
  145. package/template/apps/asas-tanstack/src/components/ui/input-otp.tsx +20 -0
  146. package/template/apps/asas-tanstack/src/components/ui/input.tsx +20 -0
  147. package/template/apps/asas-tanstack/src/components/ui/label.tsx +18 -0
  148. package/template/apps/asas-tanstack/src/components/ui/popover.tsx +50 -0
  149. package/template/apps/asas-tanstack/src/components/ui/radio-group.tsx +10 -0
  150. package/template/apps/asas-tanstack/src/components/ui/resizable.tsx +47 -0
  151. package/template/apps/asas-tanstack/src/components/ui/scroll-area.tsx +52 -0
  152. package/template/apps/asas-tanstack/src/components/ui/select.tsx +190 -0
  153. package/template/apps/asas-tanstack/src/components/ui/separator.tsx +23 -0
  154. package/template/apps/asas-tanstack/src/components/ui/sheet.tsx +127 -0
  155. package/template/apps/asas-tanstack/src/components/ui/sidebar.tsx +716 -0
  156. package/template/apps/asas-tanstack/src/components/ui/skeleton.tsx +13 -0
  157. package/template/apps/asas-tanstack/src/components/ui/slider.tsx +13 -0
  158. package/template/apps/asas-tanstack/src/components/ui/spinner.tsx +11 -0
  159. package/template/apps/asas-tanstack/src/components/ui/switch.tsx +8 -0
  160. package/template/apps/asas-tanstack/src/components/ui/table.tsx +119 -0
  161. package/template/apps/asas-tanstack/src/components/ui/tabs.tsx +80 -0
  162. package/template/apps/asas-tanstack/src/components/ui/textarea.tsx +18 -0
  163. package/template/apps/asas-tanstack/src/components/ui/toggle-group.tsx +8 -0
  164. package/template/apps/asas-tanstack/src/components/ui/toggle.tsx +44 -0
  165. package/template/apps/asas-tanstack/src/components/ui/tooltip.tsx +68 -0
  166. package/template/apps/asas-tanstack/src/copy.ts +870 -0
  167. package/template/apps/asas-tanstack/src/hooks/use-mobile.ts +19 -0
  168. package/template/apps/asas-tanstack/src/lib/utils.ts +6 -0
  169. package/template/apps/asas-tanstack/src/links.ts +38 -0
  170. package/template/apps/asas-tanstack/src/locale-switch.tsx +26 -0
  171. package/template/apps/asas-tanstack/src/quiet-abort.ts +25 -0
  172. package/template/apps/asas-tanstack/src/redirect.ts +20 -0
  173. package/template/apps/asas-tanstack/src/request-headers.ts +11 -0
  174. package/template/apps/asas-tanstack/src/routeTree.gen.ts +382 -0
  175. package/template/apps/asas-tanstack/src/router.tsx +7 -0
  176. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/files.ts +4 -0
  177. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/screen.json +3883 -0
  178. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/types.ts +158 -0
  179. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard.$.tsx +17 -0
  180. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard.index.tsx +10 -0
  181. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard.tsx +13 -0
  182. package/template/apps/asas-tanstack/src/routes/(screens)/portal/files.ts +4 -0
  183. package/template/apps/asas-tanstack/src/routes/(screens)/portal/screen.json +1800 -0
  184. package/template/apps/asas-tanstack/src/routes/(screens)/portal/types.ts +92 -0
  185. package/template/apps/asas-tanstack/src/routes/(screens)/portal.$.tsx +17 -0
  186. package/template/apps/asas-tanstack/src/routes/(screens)/portal.index.tsx +10 -0
  187. package/template/apps/asas-tanstack/src/routes/(screens)/portal.tsx +13 -0
  188. package/template/apps/asas-tanstack/src/routes/__root.tsx +99 -0
  189. package/template/apps/asas-tanstack/src/routes/api/$.ts +11 -0
  190. package/template/apps/asas-tanstack/src/routes/auth.accept-invitation.tsx +101 -0
  191. package/template/apps/asas-tanstack/src/routes/auth.forgot-password.tsx +94 -0
  192. package/template/apps/asas-tanstack/src/routes/auth.handoff.ts +39 -0
  193. package/template/apps/asas-tanstack/src/routes/auth.reset-password.tsx +109 -0
  194. package/template/apps/asas-tanstack/src/routes/auth.sign-in.tsx +86 -0
  195. package/template/apps/asas-tanstack/src/routes/auth.sign-up.tsx +122 -0
  196. package/template/apps/asas-tanstack/src/routes/index.tsx +142 -0
  197. package/template/apps/asas-tanstack/src/routes/unauthorized.tsx +122 -0
  198. package/template/apps/asas-tanstack/src/screen-form.tsx +34 -0
  199. package/template/apps/asas-tanstack/src/screen-list.tsx +139 -0
  200. package/template/apps/asas-tanstack/src/screen-names.ts +10 -0
  201. package/template/apps/asas-tanstack/src/screens.tsx +868 -0
  202. package/template/apps/asas-tanstack/src/server.ts +16 -0
  203. package/template/apps/asas-tanstack/src/slide.ts +22 -0
  204. package/template/apps/asas-tanstack/src/start.ts +57 -0
  205. package/template/apps/asas-tanstack/src/styles.css +159 -0
  206. package/template/apps/asas-tanstack/src/theme.ts +5 -0
  207. package/template/apps/asas-tanstack/tsconfig.json +19 -0
  208. package/template/apps/asas-tanstack/vite.config.ts +28 -0
  209. package/template/package.json +16 -0
  210. package/template/templates/asas/.env.example +16 -0
  211. package/template/templates/asas/components.json +22 -0
  212. package/template/templates/asas/mithaq.hints.json +25 -0
  213. package/template/templates/asas/package.json +38 -0
  214. package/template/templates/asas/src/asas.ts +93 -0
  215. package/template/templates/asas/src/audit-log/audit-log.ts +187 -0
  216. package/template/templates/asas/src/audit-log/sink.ts +89 -0
  217. package/template/templates/asas/src/audit-log/ui/activity.tsx +243 -0
  218. package/template/templates/asas/src/courses/courses.ts +612 -0
  219. package/template/templates/asas/src/courses/ui/archive-button.tsx +25 -0
  220. package/template/templates/asas/src/courses/ui/fee-cell.tsx +19 -0
  221. package/template/templates/asas/src/courses/ui/no-courses-on-offer.tsx +11 -0
  222. package/template/templates/asas/src/courses/ui/no-courses.tsx +13 -0
  223. package/template/templates/asas/src/courses/ui/on-offer.ts +6 -0
  224. package/template/templates/asas/src/courses/ui/status-cell.tsx +31 -0
  225. package/template/templates/asas/src/enrollments/enrollments.ts +137 -0
  226. package/template/templates/asas/src/enrollments/ui/session-cell.tsx +65 -0
  227. package/template/templates/asas/src/kayan/auth/user/account.access.ts +8 -0
  228. package/template/templates/asas/src/kayan/auth/user/account.ts +31 -0
  229. package/template/templates/asas/src/kayan/auth/user/index.ts +18 -0
  230. package/template/templates/asas/src/kayan/auth/user/invitation.access.ts +4 -0
  231. package/template/templates/asas/src/kayan/auth/user/invitation.ts +27 -0
  232. package/template/templates/asas/src/kayan/auth/user/member.access.ts +10 -0
  233. package/template/templates/asas/src/kayan/auth/user/member.screens.ts +37 -0
  234. package/template/templates/asas/src/kayan/auth/user/member.ts +25 -0
  235. package/template/templates/asas/src/kayan/auth/user/organization.access.ts +20 -0
  236. package/template/templates/asas/src/kayan/auth/user/organization.screens.ts +37 -0
  237. package/template/templates/asas/src/kayan/auth/user/organization.ts +24 -0
  238. package/template/templates/asas/src/kayan/auth/user/organizationRole.access.ts +8 -0
  239. package/template/templates/asas/src/kayan/auth/user/organizationRole.ts +25 -0
  240. package/template/templates/asas/src/kayan/auth/user/session.access.ts +8 -0
  241. package/template/templates/asas/src/kayan/auth/user/session.ts +28 -0
  242. package/template/templates/asas/src/kayan/auth/user/user.access.ts +29 -0
  243. package/template/templates/asas/src/kayan/auth/user/user.screens.ts +45 -0
  244. package/template/templates/asas/src/kayan/auth/user/user.ts +31 -0
  245. package/template/templates/asas/src/kayan/auth/user/verification.access.ts +8 -0
  246. package/template/templates/asas/src/kayan/auth/user/verification.ts +23 -0
  247. package/template/templates/asas/src/local-users/local-users.ts +143 -0
  248. package/template/templates/asas/src/media/media.ts +137 -0
  249. package/template/templates/asas/src/media/ui/preview.tsx +143 -0
  250. package/template/templates/asas/src/member-roles/member-roles.ts +207 -0
  251. package/template/templates/asas/src/messages/account.json +106 -0
  252. package/template/templates/asas/src/messages/auditLog.json +186 -0
  253. package/template/templates/asas/src/messages/courses.json +270 -0
  254. package/template/templates/asas/src/messages/enrollments.json +42 -0
  255. package/template/templates/asas/src/messages/invitation.json +66 -0
  256. package/template/templates/asas/src/messages/localUsers.json +42 -0
  257. package/template/templates/asas/src/messages/media.json +66 -0
  258. package/template/templates/asas/src/messages/member.json +42 -0
  259. package/template/templates/asas/src/messages/memberRoles.json +42 -0
  260. package/template/templates/asas/src/messages/mithaq.json +170 -0
  261. package/template/templates/asas/src/messages/organization.json +50 -0
  262. package/template/templates/asas/src/messages/organizationRole.json +50 -0
  263. package/template/templates/asas/src/messages/roles.json +58 -0
  264. package/template/templates/asas/src/messages/session.json +82 -0
  265. package/template/templates/asas/src/messages/sessions.json +106 -0
  266. package/template/templates/asas/src/messages/settings.json +66 -0
  267. package/template/templates/asas/src/messages/tags.json +38 -0
  268. package/template/templates/asas/src/messages/user.json +90 -0
  269. package/template/templates/asas/src/messages/verification.json +50 -0
  270. package/template/templates/asas/src/migrations/20260920080701_parched_tombstone/migration.sql +177 -0
  271. package/template/templates/asas/src/migrations/20260920080701_parched_tombstone/snapshot.json +2169 -0
  272. package/template/templates/asas/src/migrations/20260920230505_zippy_ben_urich/migration.sql +1 -0
  273. package/template/templates/asas/src/migrations/20260920230505_zippy_ben_urich/snapshot.json +2182 -0
  274. package/template/templates/asas/src/migrations/20260920231219_slim_retro_girl/migration.sql +20 -0
  275. package/template/templates/asas/src/migrations/20260920231219_slim_retro_girl/snapshot.json +2409 -0
  276. package/template/templates/asas/src/migrations/20260922082945_military_ironclad/migration.sql +15 -0
  277. package/template/templates/asas/src/migrations/20260922082945_military_ironclad/snapshot.json +2627 -0
  278. package/template/templates/asas/src/migrations/20260922112630_bitter_chat/migration.sql +5 -0
  279. package/template/templates/asas/src/migrations/20260922112630_bitter_chat/snapshot.json +2691 -0
  280. package/template/templates/asas/src/migrations/20260923235915_foamy_rachel_grey/migration.sql +8 -0
  281. package/template/templates/asas/src/migrations/20260923235915_foamy_rachel_grey/snapshot.json +2795 -0
  282. package/template/templates/asas/src/migrations/20260926145709_clean_the_initiative/migration.sql +3 -0
  283. package/template/templates/asas/src/migrations/20260926145709_clean_the_initiative/snapshot.json +2834 -0
  284. package/template/templates/asas/src/migrations/20260927212525_bent_prowler/migration.sql +9 -0
  285. package/template/templates/asas/src/migrations/20260927212525_bent_prowler/snapshot.json +2870 -0
  286. package/template/templates/asas/src/migrations/20260927223441_tranquil_bromley/migration.sql +1 -0
  287. package/template/templates/asas/src/migrations/20260927223441_tranquil_bromley/snapshot.json +2881 -0
  288. package/template/templates/asas/src/migrations/20260928030047_outgoing_wraith/migration.sql +15 -0
  289. package/template/templates/asas/src/migrations/20260928030047_outgoing_wraith/snapshot.json +3073 -0
  290. package/template/templates/asas/src/migrations/20260929024407_grey_morlun/migration.sql +21 -0
  291. package/template/templates/asas/src/migrations/20260929024407_grey_morlun/snapshot.json +3098 -0
  292. package/template/templates/asas/src/migrations/20260929130530_bored_jackal/migration.sql +21 -0
  293. package/template/templates/asas/src/migrations/20260929130530_bored_jackal/snapshot.json +3124 -0
  294. package/template/templates/asas/src/migrations/20260930102924_material_newton_destine/migration.sql +32 -0
  295. package/template/templates/asas/src/migrations/20260930102924_material_newton_destine/snapshot.json +3324 -0
  296. package/template/templates/asas/src/migrations/20261003022353_clumsy_taskmaster/migration.sql +2 -0
  297. package/template/templates/asas/src/migrations/20261003022353_clumsy_taskmaster/snapshot.json +3350 -0
  298. package/template/templates/asas/src/migrations/20261003143249_premium_red_shift/migration.sql +21 -0
  299. package/template/templates/asas/src/migrations/20261003143249_premium_red_shift/snapshot.json +3635 -0
  300. package/template/templates/asas/src/migrations/20261008215349_square_norrin_radd/migration.sql +8 -0
  301. package/template/templates/asas/src/migrations/20261008215349_square_norrin_radd/snapshot.json +3635 -0
  302. package/template/templates/asas/src/migrations/20261008230204_many_scarecrow/migration.sql +18 -0
  303. package/template/templates/asas/src/migrations/20261008230204_many_scarecrow/snapshot.json +3635 -0
  304. package/template/templates/asas/src/migrations/20261010005521_elite_giant_girl/migration.sql +18 -0
  305. package/template/templates/asas/src/migrations/20261010005521_elite_giant_girl/snapshot.json +3635 -0
  306. package/template/templates/asas/src/migrations/20261010120822_needy_thunderbolt/migration.sql +11 -0
  307. package/template/templates/asas/src/migrations/20261010120822_needy_thunderbolt/snapshot.json +3667 -0
  308. package/template/templates/asas/src/migrations/20261012000752_organization_ownership/migration.sql +48 -0
  309. package/template/templates/asas/src/migrations/20261012000752_organization_ownership/snapshot.json +3891 -0
  310. package/template/templates/asas/src/migrations/drizzle.config.ts +11 -0
  311. package/template/templates/asas/src/migrations/schema.ts +29 -0
  312. package/template/templates/asas/src/mithaq.ts +406 -0
  313. package/template/templates/asas/src/pages.ts +74 -0
  314. package/template/templates/asas/src/platform-http.ts +96 -0
  315. package/template/templates/asas/src/rich-text-nodes.ts +11 -0
  316. package/template/templates/asas/src/roles/grants.ts +47 -0
  317. package/template/templates/asas/src/roles/roles.ts +184 -0
  318. package/template/templates/asas/src/roles/ui/role-form.tsx +226 -0
  319. package/template/templates/asas/src/roles/ui/roles.tsx +166 -0
  320. package/template/templates/asas/src/roles.ts +16 -0
  321. package/template/templates/asas/src/seed.ts +325 -0
  322. package/template/templates/asas/src/sessions/sessions.ts +155 -0
  323. package/template/templates/asas/src/settings/settings.ts +69 -0
  324. package/template/templates/asas/src/statements.ts +11 -0
  325. package/template/templates/asas/src/steps/expected-updated-at.ts +41 -0
  326. package/template/templates/asas/src/storage/defaults.ts +156 -0
  327. package/template/templates/asas/src/storage/index.ts +95 -0
  328. package/template/templates/asas/src/storage/local.ts +143 -0
  329. package/template/templates/asas/src/storage/r2.ts +5 -0
  330. package/template/templates/asas/src/storage/s3.ts +91 -0
  331. package/template/templates/asas/src/storage/uploadthing.ts +59 -0
  332. package/template/templates/asas/src/tags/tags.ts +80 -0
  333. package/template/templates/asas/src/tenant-access.ts +221 -0
  334. package/template/templates/asas/src/tenant-roles.ts +262 -0
  335. package/template/templates/asas/src/ui/app-shell.tsx +260 -0
  336. package/template/templates/asas/src/ui/kpi-card.tsx +25 -0
  337. package/template/templates/asas/src/ui/record-page.tsx +50 -0
  338. package/template/templates/asas/src/ui/rich-text-nodes.tsx +15 -0
  339. package/template/templates/asas/src/ui/screen-nav.tsx +28 -0
  340. package/template/templates/asas/src/ui/session-labels.tsx +36 -0
  341. package/template/templates/asas/src/ui/table-page.tsx +9 -0
  342. package/template/templates/asas/src/ui/top-shell.tsx +60 -0
  343. package/template/templates/asas/src/ui-dashboard/dashboard.tsx +294 -0
  344. package/template/templates/asas/src/ui-dashboard/members.tsx +630 -0
  345. package/template/templates/asas/src/ui-dashboard/theme-provider.tsx +10 -0
  346. package/template/templates/asas/src/ui-dashboard/versions.tsx +432 -0
  347. package/template/templates/asas/src/ui-platform/home.tsx +63 -0
  348. package/template/templates/asas/src/ui-platform/organization-actions.tsx +114 -0
  349. package/template/templates/asas/src/ui-platform/record-page.tsx +30 -0
  350. package/template/templates/asas/src/ui-platform/role-cells.tsx +52 -0
  351. package/template/templates/asas/src/ui-platform/user-actions.tsx +251 -0
  352. package/template/templates/asas/src/ui-portal/home.tsx +77 -0
  353. package/template/templates/asas/src/ui-portal/page.tsx +11 -0
  354. package/template/templates/auth/package.json +14 -0
  355. package/template/templates/auth/src/index.ts +690 -0
  356. package/template/templates/auth/src/platform-roles.ts +87 -0
  357. package/README.md +0 -3
@@ -0,0 +1,92 @@
1
+ // Written by `mithaq generate screens`. Do not edit.
2
+ import type { ScreenClient as Client } from 'mithaq/admin'
3
+ export type { PageClient } from 'mithaq/admin'
4
+ export type CoursesValues = {
5
+ id: string
6
+ updatedAt: Date
7
+ createdAt: Date
8
+ organization: unknown
9
+ title: string
10
+ summary: string | null
11
+ fee: number | null
12
+ currency: 'SAR' | 'USD' | null
13
+ level: number | null
14
+ notifications: boolean | null
15
+ delivery: 'live' | 'recorded' | null
16
+ audience: 'beginner' | 'advanced' | null
17
+ accessCode: string | null
18
+ archived: boolean | null
19
+ topics: string[] | null
20
+ opensOn: string | null
21
+ archiveReason: string | null
22
+ tags: unknown[] | null
23
+ scheduledSessions: Values['sessions'][]
24
+ _status: 'draft' | 'published' | null
25
+ details: { root: Record<string, unknown> } | null
26
+ cover: string | null
27
+ }
28
+ export type EnrollmentsValues = {
29
+ id: string
30
+ updatedAt: Date
31
+ createdAt: Date
32
+ organization: unknown
33
+ session: string
34
+ student: string
35
+ deletedAt: Date | null
36
+ }
37
+ export type SessionsValues = {
38
+ id: string
39
+ updatedAt: Date
40
+ createdAt: Date
41
+ organization: unknown
42
+ course: string
43
+ startsAt: Date
44
+ capacity: number | null
45
+ venue: 'online' | 'campus' | null
46
+ format: 'workshop' | 'lecture' | null
47
+ archived: boolean | null
48
+ details: { root: Record<string, unknown> } | null
49
+ enrollments: Values['enrollments'][]
50
+ }
51
+ export type MediaValues = {
52
+ id: string
53
+ updatedAt: Date
54
+ createdAt: Date
55
+ organization: unknown
56
+ filename: string
57
+ mimeType: string
58
+ filesize: number
59
+ width: number | null
60
+ height: number | null
61
+ key: string
62
+ storage: string
63
+ alt: Record<'ar' | 'en', string>
64
+ }
65
+ export type UserValues = {
66
+ id: string
67
+ updatedAt: Date
68
+ createdAt: Date
69
+ name: string
70
+ email: string
71
+ emailVerified: boolean
72
+ image: string | null
73
+ role: string | null
74
+ banned: boolean | null
75
+ banReason: string | null
76
+ banExpires: Date | null
77
+ }
78
+ export type Values = {
79
+ courses: CoursesValues
80
+ enrollments: EnrollmentsValues
81
+ sessions: SessionsValues
82
+ media: MediaValues
83
+ user: UserValues
84
+ }
85
+ export type Pages = {
86
+ courses: 'list' | 'edit'
87
+ enrollments: 'list' | 'create' | 'edit'
88
+ sessions: never
89
+ media: never
90
+ user: never
91
+ }
92
+ export type ScreenClient = Client<Pages>
@@ -0,0 +1,17 @@
1
+ // Written by `mithaq generate screens`. Do not edit.
2
+ import { createFileRoute } from '@tanstack/react-router'
3
+ import { ScreenNotFound, ScreenPageView, askedFor, pageHead, pageLoader } from '@/screens.tsx'
4
+ import { files } from './portal/files.ts'
5
+
6
+ export const Route = createFileRoute('/(screens)/portal/$')({
7
+ validateSearch: askedFor,
8
+ loader: ({ location, params }) => pageLoader({ screen: 'portal', files, path: params._splat ?? '', query: location.search, href: location.href }),
9
+ head: ({ loaderData, match }) => pageHead(loaderData, match.context),
10
+ notFoundComponent: () => <ScreenNotFound files={files} screen={'portal'} />,
11
+ component: Page,
12
+ })
13
+
14
+ function Page() {
15
+ const navigate = Route.useNavigate()
16
+ return <ScreenPageView files={files} data={Route.useLoaderData()} query={Route.useSearch()} onQuery={(search) => void navigate({ search })} />
17
+ }
@@ -0,0 +1,10 @@
1
+ // Written by `mithaq generate screens`. Do not edit.
2
+ import { createFileRoute } from '@tanstack/react-router'
3
+ import { ScreenPageView, askedFor, pageHead, pageLoader } from '@/screens.tsx'
4
+ import { files } from './portal/files.ts'
5
+
6
+ export const Route = createFileRoute('/(screens)/portal/')({
7
+ loader: ({ location }) => pageLoader({ screen: 'portal', files, path: '', query: {}, href: location.href }),
8
+ head: ({ loaderData, match }) => pageHead(loaderData, match.context),
9
+ component: () => <ScreenPageView files={files} data={Route.useLoaderData()} query={askedFor({})} onQuery={() => {}} />,
10
+ })
@@ -0,0 +1,13 @@
1
+ // Written by `mithaq generate screens`. Do not edit.
2
+ import { Outlet, createFileRoute } from '@tanstack/react-router'
3
+ import { ScreenFrame, frameLoader } from '@/screens.tsx'
4
+ import { files } from './portal/files.ts'
5
+
6
+ export const Route = createFileRoute('/(screens)/portal')({
7
+ loader: ({ location }) => frameLoader({ screen: 'portal', files, href: location.href }),
8
+ component: () => (
9
+ <ScreenFrame files={files} data={Route.useLoaderData()}>
10
+ <Outlet />
11
+ </ScreenFrame>
12
+ ),
13
+ })
@@ -0,0 +1,99 @@
1
+ import { HeadContent, Outlet, Scripts, createRootRoute } from '@tanstack/react-router'
2
+ import { DirectionProvider } from '@/components/ui/direction'
3
+ export { locales } from '../copy.ts'
4
+ import { dirOf, documentHead, localeOf, translator } from '../copy.ts'
5
+ import { ThemeProvider } from '../components/mithaq/nav.tsx'
6
+ import { themeStorageKey } from '../theme.ts'
7
+ import appCss from '../styles.css?url'
8
+
9
+ /**
10
+ * Puts the chosen theme on the document before the first paint, which React cannot do: it
11
+ * reads `localStorage` only after mount. Also reloads a page restored from the back-forward
12
+ * cache, so Back after signing out asks the server again instead of showing the signed-in page.
13
+ */
14
+ const theme = `
15
+ (function() {
16
+ var storageKey = ${JSON.stringify(themeStorageKey)};
17
+ var defaultTheme = "system";
18
+ try {
19
+ var theme = localStorage.getItem(storageKey) || defaultTheme;
20
+ var systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
21
+ var root = document.documentElement;
22
+ root.classList.remove("light", "dark");
23
+ root.classList.add(theme === "system" ? systemTheme : theme);
24
+ } catch (e) {}
25
+ window.addEventListener("pageshow", function (event) {
26
+ if (event.persisted) location.reload();
27
+ });
28
+ })()
29
+ `
30
+
31
+ /**
32
+ * An unregistered request context is `never` in the router's types, so the shape is named
33
+ * here and read through a cast; `localeOf` turns it back into a declared locale.
34
+ */
35
+ type FromRequest = { serverContext?: { locale?: unknown } }
36
+
37
+ export const Route = createRootRoute({
38
+ // On the client the locale is the `lang` the server wrote on `<html>`, so the hydrated tree matches
39
+ // the one sent. `localeOf` on both sides refuses a hand-edited `lang` or cookie.
40
+ beforeLoad: (ctx) => {
41
+ const locale =
42
+ typeof document === 'undefined'
43
+ ? localeOf((ctx as unknown as FromRequest).serverContext?.locale as string | undefined)
44
+ : localeOf(document.documentElement.lang)
45
+ return { locale, dir: dirOf(locale), app: 'marketing.brand' }
46
+ },
47
+ head: ({ match }) => ({
48
+ meta: [
49
+ { charSet: 'utf-8' },
50
+ { name: 'viewport', content: 'width=device-width, initial-scale=1' },
51
+ // `_notFound` is the router's mark on the root match it draws the not-found for.
52
+ ...documentHead(match.context, match._notFound ? (t) => t('admin.notFoundTitle', 'Page not found') : undefined).meta,
53
+ ],
54
+ links: [{ rel: 'stylesheet', href: appCss }, { rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' }],
55
+ }),
56
+ // Without it the router draws a development-only placeholder and warns.
57
+ notFoundComponent: NotFound,
58
+ component: RootDocument,
59
+ })
60
+
61
+ export const useCopy = () => {
62
+ const { dir, locale } = Route.useRouteContext()
63
+ return { dir, locale, t: translator(locale) }
64
+ }
65
+
66
+ function NotFound() {
67
+ const { t } = useCopy()
68
+ return (
69
+ <main className="flex min-h-screen flex-col items-center justify-center gap-4 px-4 text-center">
70
+ <h1 className="text-2xl font-bold tracking-tight">{t('admin.notFoundTitle', 'Page not found')}</h1>
71
+ <p className="text-muted-foreground text-sm">{t('admin.notFound', 'That page does not exist.')}</p>
72
+ {/* An anchor, because `/` is no route here. */}
73
+ <a className="text-foreground text-sm underline underline-offset-4" href="/">
74
+ {t('admin.goHome', 'Go to the home page')}
75
+ </a>
76
+ </main>
77
+ )
78
+ }
79
+
80
+ function RootDocument() {
81
+ const { dir, locale } = useCopy()
82
+ return (
83
+ <html lang={locale} dir={dir} suppressHydrationWarning>
84
+ <head>
85
+ <HeadContent />
86
+ {/* Safe to write raw: the string is this module's own and carries no value from a request. */}
87
+ <script dangerouslySetInnerHTML={{ __html: theme }} />
88
+ </head>
89
+ <body>
90
+ <DirectionProvider direction={dir}>
91
+ <ThemeProvider defaultTheme="system" storageKey={themeStorageKey}>
92
+ <Outlet />
93
+ </ThemeProvider>
94
+ </DirectionProvider>
95
+ <Scripts />
96
+ </body>
97
+ </html>
98
+ )
99
+ }
@@ -0,0 +1,11 @@
1
+ // `server` is a module augmentation `@tanstack/start-client-core` declares; the erased type-only
2
+ // import below loads it, so this app still ships `@tanstack/react-router` alone.
3
+ import { createFileRoute } from '@tanstack/react-router'
4
+ import type {} from '@tanstack/start-client-core'
5
+ import { asas } from 'asas'
6
+
7
+ // `/api/$` because one handler owns all of `/api`. The generated `routeTree.gen.ts` sits under
8
+ // `src/` so the root `tsc` sees the `FileRoutesByPath` augmentation that types this path.
9
+ export const Route = createFileRoute('/api/$')({
10
+ server: { handlers: { ANY: ({ request }) => asas.handler(request) } },
11
+ })
@@ -0,0 +1,101 @@
1
+ import { useEffect, useState } from 'react'
2
+ import { Link, createFileRoute, useNavigate } from '@tanstack/react-router'
3
+ import { Button } from '@/components/ui/button'
4
+ import { authArm } from '../api.ts'
5
+ import { documentHead } from '../copy.ts'
6
+ import { AuthFrame, authGate, useRefusal } from '../auth-frame.tsx'
7
+ import { useCopy } from './__root.tsx'
8
+
9
+ // Accepting is a POST the person presses, so following a link alone joins nobody to anything.
10
+ // The provider decides whether the invitation is theirs.
11
+
12
+ export const Route = createFileRoute('/auth/accept-invitation')({
13
+ head: ({ match }) => documentHead(match.context, (t) => t('auth.invitation')),
14
+ validateSearch: (search: Record<string, unknown>): { id?: string } => (typeof search.id === 'string' && search.id ? { id: search.id } : {}),
15
+ beforeLoad: () => authGate(),
16
+ component: AcceptInvitation,
17
+ })
18
+
19
+ function AcceptInvitation() {
20
+ const { t } = useCopy()
21
+ const navigate = useNavigate()
22
+ const refused = useRefusal()
23
+ const { id } = Route.useSearch()
24
+ const { signedIn } = Route.useRouteContext()
25
+ const [organization, setOrganization] = useState<string>()
26
+ const [gone, setGone] = useState<string>()
27
+ const [pending, setPending] = useState(false)
28
+ const [refusal, setRefusal] = useState<string>()
29
+ const title = t('auth.invitation', 'Invitation')
30
+
31
+ useEffect(() => {
32
+ if (!signedIn || !id) return
33
+ fetch(`/api/auth/user/organization/get-invitation?id=${encodeURIComponent(id)}`)
34
+ .then(async (response) => {
35
+ const body = (await response.json().catch(() => null)) as { organizationName?: string; code?: unknown } | null
36
+ if (response.ok) return setOrganization(body?.organizationName ?? '')
37
+ // Better Auth answers a missing, spent or expired invitation 400 with no code.
38
+ setGone(refused({ status: response.status, body }, t('auth.invitationGone', 'This invitation has expired, was cancelled or was already accepted. Ask whoever invited you to send a new one.')))
39
+ })
40
+ .catch(() => setGone(t('auth.requestFailed', 'That request did not work.')))
41
+ }, [signedIn, id])
42
+
43
+ const accept = async () => {
44
+ setPending(true)
45
+ setRefusal(undefined)
46
+ const answered = await authArm('organization/accept-invitation', { invitationId: id })
47
+ if (!answered.ok) {
48
+ setPending(false)
49
+ return setRefusal(refused(answered, t('auth.invitationAcceptFailed', 'That invitation could not be accepted.')))
50
+ }
51
+ // The provider made the organization active as it accepted, so the first screen is read as a member.
52
+ await navigate({ to: (await authGate()).home.path })
53
+ }
54
+
55
+ if (!id) return <AuthFrame title={title} description={t('auth.invitationMissing', 'This link names no invitation.')}>{null}</AuthFrame>
56
+ if (!signedIn) {
57
+ const here = `/auth/accept-invitation?id=${encodeURIComponent(id)}`
58
+ return (
59
+ <AuthFrame title={title} description={t('auth.invitationSignedOut', 'Sign in, or make an account with the email address the invitation was sent to, to accept it.')}>
60
+ <div className="grid gap-2">
61
+ <Button nativeButton={false} render={<Link to="/auth/sign-in" search={{ redirect: here }} />}>
62
+ {t('auth.signIn', 'Sign in')}
63
+ </Button>
64
+ <Button variant="outline" nativeButton={false} render={<Link to="/auth/sign-up" search={{ redirect: here }} />}>
65
+ {t('auth.signUp', 'Sign up')}
66
+ </Button>
67
+ </div>
68
+ </AuthFrame>
69
+ )
70
+ }
71
+ if (gone) {
72
+ return (
73
+ <AuthFrame title={title}>
74
+ <div className="grid gap-4">
75
+ <p role="alert" data-invitation-gone="" className="text-sm">
76
+ {gone}
77
+ </p>
78
+ {/* The invitation may be another address's, which signing out and in again reaches. */}
79
+ <Button type="button" variant="outline" onClick={() => void authArm('sign-out').then(() => window.location.reload())}>
80
+ {t('admin.signOut', 'Sign out')}
81
+ </Button>
82
+ </div>
83
+ </AuthFrame>
84
+ )
85
+ }
86
+ if (organization === undefined) return <AuthFrame title={title} description={t('auth.invitationReading', 'Reading the invitation…')}>{null}</AuthFrame>
87
+ return (
88
+ <AuthFrame title={title} description={t('auth.invitationJoin', 'You are invited to join {organization}.')!.replace('{organization}', () => organization)}>
89
+ <div className="grid gap-4">
90
+ {refusal ? (
91
+ <p className="text-destructive text-sm" role="alert">
92
+ {refusal}
93
+ </p>
94
+ ) : null}
95
+ <Button type="button" data-button="accept-invitation" disabled={pending} onClick={() => void accept()}>
96
+ {pending ? t('auth.invitationAccepting', 'Accepting…') : t('auth.invitationAccept', 'Accept the invitation')}
97
+ </Button>
98
+ </div>
99
+ </AuthFrame>
100
+ )
101
+ }
@@ -0,0 +1,94 @@
1
+ import { useState } from 'react'
2
+ import { Link, createFileRoute } from '@tanstack/react-router'
3
+ import { Button } from '@/components/ui/button'
4
+ import { authArm } from '../api.ts'
5
+ import { documentHead } from '../copy.ts'
6
+ import { AlreadySignedIn, AuthBox, AuthFrame, authGate, notAnEmail, redirectSearch, useRefusal } from '../auth-frame.tsx'
7
+ import { useCopy } from './__root.tsx'
8
+
9
+ // The sentence after a request is the same whether or not the address has an account, so
10
+ // nothing on this page says which it was.
11
+
12
+ export const Route = createFileRoute('/auth/forgot-password')({
13
+ head: ({ match }) => documentHead(match.context, (t) => t('auth.forgotPassword')),
14
+ validateSearch: redirectSearch,
15
+ beforeLoad: () => authGate(),
16
+ component: ForgotPassword,
17
+ })
18
+
19
+ function ForgotPassword() {
20
+ const { t } = useCopy()
21
+ const search = Route.useSearch()
22
+ const { signedIn, home } = Route.useRouteContext()
23
+ const refused = useRefusal()
24
+ const [email, setEmail] = useState('')
25
+ const [left, setLeft] = useState(false)
26
+ const [pending, setPending] = useState(false)
27
+ const [sent, setSent] = useState(false)
28
+ const [refusal, setRefusal] = useState<string | null>(null)
29
+ const wrong = () => (!email ? t('auth.required', 'This field is required.') : notAnEmail(email) ? t('auth.invalidEmail', 'Invalid email') : undefined)
30
+ const back = (
31
+ <p className="mt-4 text-center text-sm">
32
+ <Link className="text-primary underline underline-offset-4" to="/auth/sign-in" search={{ redirect: search.redirect }}>
33
+ {t('auth.backToSignIn', 'Back to login')}
34
+ </Link>
35
+ </p>
36
+ )
37
+
38
+ const submit = async (event: React.FormEvent) => {
39
+ event.preventDefault()
40
+ setLeft(true)
41
+ if (wrong()) return
42
+ setPending(true)
43
+ setRefusal(null)
44
+ const answered = await authArm('request-password-reset', { email })
45
+ setPending(false)
46
+ if (!answered.ok) return setRefusal(refused(answered, t('auth.requestFailed', 'That request did not work.')))
47
+ setSent(true)
48
+ }
49
+
50
+ if (signedIn) return <AlreadySignedIn redirect={search.redirect} home={home} />
51
+ if (sent) {
52
+ return (
53
+ <AuthFrame
54
+ title={t('auth.emailSent', 'Email sent')}
55
+ description={t('auth.checkYourEmail', "If the email address is associated with an account, you will receive instructions to reset your password shortly. Please check your spam or junk mail folder if you don't see the email in your inbox.")}
56
+ >
57
+ {import.meta.env?.DEV ? (
58
+ <p data-no-mailer="" className="text-muted-foreground rounded-md border px-3 py-2 text-sm">
59
+ {t('auth.noMailer', 'No mailer is configured, so no email was sent. In development the reset link is written to the server log.')}
60
+ </p>
61
+ ) : null}
62
+ {back}
63
+ </AuthFrame>
64
+ )
65
+ }
66
+ return (
67
+ <AuthFrame
68
+ title={t('auth.forgotPassword', 'Forgot password')}
69
+ description={t('auth.forgotPasswordInstructions', 'Please enter your email below. You will receive an email message with instructions on how to reset your password.')}
70
+ >
71
+ <form className="grid gap-4" noValidate onSubmit={submit}>
72
+ <AuthBox
73
+ autoComplete="email"
74
+ error={left ? wrong() : undefined}
75
+ id="email"
76
+ label={t('auth.email', 'Email')}
77
+ onChange={(event) => setEmail(event.target.value)}
78
+ onLeave={() => setLeft(true)}
79
+ type="email"
80
+ value={email}
81
+ />
82
+ {refusal && (
83
+ <p className="text-destructive text-sm" role="alert">
84
+ {refusal}
85
+ </p>
86
+ )}
87
+ <Button disabled={pending} type="submit">
88
+ {t('auth.submit', 'Submit')}
89
+ </Button>
90
+ </form>
91
+ {back}
92
+ </AuthFrame>
93
+ )
94
+ }
@@ -0,0 +1,39 @@
1
+ // Trades the platform's one-time impersonation token for the session through the plugin's own
2
+ // verify, then lands on the first screen the impersonated person may enter. The access report is
3
+ // read with only the session the verify set, so the landing grants nothing. A spent, expired or
4
+ // unknown token sets nothing and lands on sign-in.
5
+ import { createFileRoute } from '@tanstack/react-router'
6
+ import type {} from '@tanstack/start-client-core'
7
+
8
+ const firstScreen = (targets: unknown, listed: readonly string[], platform: string) => {
9
+ const screens = targets as Record<string, { capability?: string; client?: { path: string } } | undefined>
10
+ const name = listed.find((one) => one !== platform && screens[one]?.capability === 'screen')
11
+ return name ? screens[name]!.client!.path : '/unauthorized'
12
+ }
13
+
14
+ export const Route = createFileRoute('/auth/handoff')({
15
+ server: {
16
+ handlers: {
17
+ GET: async ({ request }) => {
18
+ const { asas, tenantURL, PLATFORM_SCREEN } = await import('asas')
19
+ // The deployment's own origin, never the request's, whose host a caller writes.
20
+ const own = new URL(tenantURL).origin
21
+ const verified = await asas.handler(
22
+ new Request(new URL('/api/auth/user/one-time-token/verify', own), {
23
+ method: 'POST',
24
+ headers: { 'content-type': 'application/json', origin: own },
25
+ body: JSON.stringify({ token: new URL(request.url).searchParams.get('token') ?? '' }),
26
+ }),
27
+ )
28
+ const cookies = verified.headers.getSetCookie()
29
+ const session = cookies.map((cookie) => cookie.split(';')[0]).join('; ')
30
+ const report = verified.ok
31
+ ? ((await (await asas.handler(new Request(new URL('/api/access', own), { headers: { cookie: session } }))).json().catch(() => ({}))) as { screens?: string[] })
32
+ : undefined
33
+ const headers = new Headers({ location: report ? firstScreen(asas.targets, report.screens ?? [], PLATFORM_SCREEN) : '/auth/sign-in' })
34
+ for (const cookie of cookies) headers.append('set-cookie', cookie)
35
+ return new Response(null, { status: 302, headers })
36
+ },
37
+ },
38
+ },
39
+ })
@@ -0,0 +1,109 @@
1
+ import { useState, type ComponentProps } from 'react'
2
+ import { Link, createFileRoute } from '@tanstack/react-router'
3
+ import { Button } from '@/components/ui/button'
4
+ import { authArm } from '../api.ts'
5
+ import { documentHead } from '../copy.ts'
6
+ import { AlreadySignedIn, AuthBox, AuthFrame, authGate, redirectSearch, useRefusal } from '../auth-frame.tsx'
7
+ import { useCopy } from './__root.tsx'
8
+
9
+ export const Route = createFileRoute('/auth/reset-password')({
10
+ head: ({ match }) => documentHead(match.context, (t) => t('auth.resetPassword')),
11
+ validateSearch: (search: Record<string, unknown>): { redirect?: string; token?: string } => ({
12
+ ...redirectSearch(search),
13
+ ...(typeof search.token === 'string' && search.token ? { token: search.token } : {}),
14
+ }),
15
+ beforeLoad: () => authGate(),
16
+ component: ResetPassword,
17
+ })
18
+
19
+ function ResetPassword() {
20
+ const { t } = useCopy()
21
+ const { token, redirect } = Route.useSearch()
22
+ const { password: bounds, signedIn, home } = Route.useRouteContext()
23
+ const refused = useRefusal()
24
+ const [password, setPassword] = useState('')
25
+ const [confirmation, setConfirmation] = useState('')
26
+ const [pending, setPending] = useState(false)
27
+ const [done, setDone] = useState(false)
28
+ const [refusal, setRefusal] = useState<string | null>(null)
29
+ const [left, setLeft] = useState<ReadonlySet<string>>(() => new Set())
30
+ const wrong = {
31
+ password: () =>
32
+ !password ? t('auth.required', 'This field is required.')
33
+ : password.length < bounds.min ? t('auth.passwordTooShort', 'Password too short')
34
+ : password.length > bounds.max ? t('auth.passwordTooLong', 'Password too long')
35
+ : undefined,
36
+ confirmation: () => (!confirmation ? t('auth.required', 'This field is required.') : confirmation === password ? undefined : t('auth.passwordsDiffer', 'The two passwords are not the same.')),
37
+ }
38
+ const errorOf = (box: keyof typeof wrong) => (left.has(box) ? wrong[box]() : undefined)
39
+ const box = (id: keyof typeof wrong, label: string | undefined, props: ComponentProps<'input'> & { description?: string }) => (
40
+ <AuthBox id={id} label={label} error={errorOf(id)} onLeave={() => setLeft((already) => new Set(already).add(id))} {...props} />
41
+ )
42
+ const signIn = (
43
+ <p className="mt-4 text-center text-sm">
44
+ <Link className="text-primary underline underline-offset-4" to="/auth/sign-in">
45
+ {t('auth.signIn', 'Sign in')}
46
+ </Link>
47
+ </p>
48
+ )
49
+
50
+ const submit = async (event: React.FormEvent) => {
51
+ event.preventDefault()
52
+ setLeft(new Set(Object.keys(wrong)))
53
+ if (Object.values(wrong).some((check) => check())) return
54
+ setPending(true)
55
+ setRefusal(null)
56
+ const answered = await authArm('reset-password', { newPassword: password, token })
57
+ setPending(false)
58
+ if (!answered.ok) return setRefusal(refused(answered, t('auth.resetFailed', 'That reset did not work.')))
59
+ setDone(true)
60
+ }
61
+
62
+ if (signedIn) return <AlreadySignedIn redirect={redirect} home={home} />
63
+ if (!token) {
64
+ return (
65
+ <AuthFrame title={t('auth.resetPassword', 'Reset password')} description={t('auth.tokenInvalid', 'Token is either invalid or has expired.')}>
66
+ <p className="text-center text-sm">
67
+ <Link className="text-primary underline underline-offset-4" to="/auth/forgot-password">
68
+ {t('auth.forgotPassword', 'Forgot password')}
69
+ </Link>
70
+ </p>
71
+ </AuthFrame>
72
+ )
73
+ }
74
+ if (done) {
75
+ return (
76
+ <AuthFrame title={t('auth.resetPassword', 'Reset password')} description={t('auth.passwordReset', 'Password reset successfully.')}>
77
+ {signIn}
78
+ </AuthFrame>
79
+ )
80
+ }
81
+ return (
82
+ <AuthFrame title={t('auth.resetPassword', 'Reset password')}>
83
+ <form className="grid gap-4" noValidate onSubmit={submit}>
84
+ {box('password', t('auth.newPassword', 'New password'), {
85
+ autoComplete: 'new-password',
86
+ description: t('auth.passwordRule', 'At least {count} characters.')!.replace('{count}', String(bounds.min)),
87
+ onChange: (event) => setPassword(event.target.value),
88
+ type: 'password',
89
+ value: password,
90
+ })}
91
+ {box('confirmation', t('auth.confirmPassword', 'Confirm the password'), {
92
+ autoComplete: 'new-password',
93
+ onChange: (event) => setConfirmation(event.target.value),
94
+ type: 'password',
95
+ value: confirmation,
96
+ })}
97
+ {refusal && (
98
+ <p className="text-destructive text-sm" role="alert">
99
+ {refusal}
100
+ </p>
101
+ )}
102
+ <Button disabled={pending} type="submit">
103
+ {t('auth.resetPassword', 'Reset password')}
104
+ </Button>
105
+ </form>
106
+ {signIn}
107
+ </AuthFrame>
108
+ )
109
+ }