create-mithaq 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/LICENSES/openstatus-time-picker.txt +21 -0
- package/LICENSES/payload.txt +22 -0
- package/LICENSES/ponytail.txt +21 -0
- package/index.js +521 -0
- package/package.json +19 -4
- package/template/apps/asas-backoffice/components.json +25 -0
- package/template/apps/asas-backoffice/gitignore +3 -0
- package/template/apps/asas-backoffice/package.json +57 -0
- package/template/apps/asas-backoffice/public/favicon.svg +1 -0
- package/template/apps/asas-backoffice/src/api.ts +147 -0
- package/template/apps/asas-backoffice/src/auth-frame.tsx +150 -0
- package/template/apps/asas-backoffice/src/components/ui/accordion.tsx +74 -0
- package/template/apps/asas-backoffice/src/components/ui/alert-dialog.tsx +185 -0
- package/template/apps/asas-backoffice/src/components/ui/avatar.tsx +106 -0
- package/template/apps/asas-backoffice/src/components/ui/badge.tsx +48 -0
- package/template/apps/asas-backoffice/src/components/ui/breadcrumb.tsx +124 -0
- package/template/apps/asas-backoffice/src/components/ui/button-group.tsx +87 -0
- package/template/apps/asas-backoffice/src/components/ui/button.tsx +55 -0
- package/template/apps/asas-backoffice/src/components/ui/calendar.tsx +223 -0
- package/template/apps/asas-backoffice/src/components/ui/card.tsx +102 -0
- package/template/apps/asas-backoffice/src/components/ui/chart.tsx +371 -0
- package/template/apps/asas-backoffice/src/components/ui/checkbox.tsx +28 -0
- package/template/apps/asas-backoffice/src/components/ui/collapsible.tsx +19 -0
- package/template/apps/asas-backoffice/src/components/ui/combobox.tsx +308 -0
- package/template/apps/asas-backoffice/src/components/ui/dialog.tsx +158 -0
- package/template/apps/asas-backoffice/src/components/ui/direction.tsx +6 -0
- package/template/apps/asas-backoffice/src/components/ui/directional-icon.tsx +20 -0
- package/template/apps/asas-backoffice/src/components/ui/dropdown-menu.tsx +267 -0
- package/template/apps/asas-backoffice/src/components/ui/field.tsx +236 -0
- package/template/apps/asas-backoffice/src/components/ui/input-group.tsx +155 -0
- package/template/apps/asas-backoffice/src/components/ui/input-otp.tsx +84 -0
- package/template/apps/asas-backoffice/src/components/ui/input.tsx +19 -0
- package/template/apps/asas-backoffice/src/components/ui/label.tsx +19 -0
- package/template/apps/asas-backoffice/src/components/ui/popover.tsx +90 -0
- package/template/apps/asas-backoffice/src/components/ui/radio-group.tsx +37 -0
- package/template/apps/asas-backoffice/src/components/ui/resizable.tsx +47 -0
- package/template/apps/asas-backoffice/src/components/ui/scroll-area.tsx +54 -0
- package/template/apps/asas-backoffice/src/components/ui/select.tsx +204 -0
- package/template/apps/asas-backoffice/src/components/ui/separator.tsx +24 -0
- package/template/apps/asas-backoffice/src/components/ui/sheet.tsx +139 -0
- package/template/apps/asas-backoffice/src/components/ui/sidebar.tsx +714 -0
- package/template/apps/asas-backoffice/src/components/ui/skeleton.tsx +13 -0
- package/template/apps/asas-backoffice/src/components/ui/slider.tsx +55 -0
- package/template/apps/asas-backoffice/src/components/ui/spinner.tsx +10 -0
- package/template/apps/asas-backoffice/src/components/ui/switch.tsx +29 -0
- package/template/apps/asas-backoffice/src/components/ui/table.tsx +115 -0
- package/template/apps/asas-backoffice/src/components/ui/tabs.tsx +79 -0
- package/template/apps/asas-backoffice/src/components/ui/textarea.tsx +17 -0
- package/template/apps/asas-backoffice/src/components/ui/toggle-group.tsx +89 -0
- package/template/apps/asas-backoffice/src/components/ui/toggle.tsx +44 -0
- package/template/apps/asas-backoffice/src/components/ui/tooltip.tsx +63 -0
- package/template/apps/asas-backoffice/src/components.ts +35 -0
- package/template/apps/asas-backoffice/src/copy.ts +1 -0
- package/template/apps/asas-backoffice/src/first-user.ts +14 -0
- package/template/apps/asas-backoffice/src/hooks/use-mobile.ts +19 -0
- package/template/apps/asas-backoffice/src/lib/utils.ts +1 -0
- package/template/apps/asas-backoffice/src/links.ts +38 -0
- package/template/apps/asas-backoffice/src/locale-switch.tsx +28 -0
- package/template/apps/asas-backoffice/src/no-organization.tsx +5 -0
- package/template/apps/asas-backoffice/src/redirect.ts +1 -0
- package/template/apps/asas-backoffice/src/request-headers.ts +11 -0
- package/template/apps/asas-backoffice/src/routeTree.gen.ts +206 -0
- package/template/apps/asas-backoffice/src/router.tsx +18 -0
- package/template/apps/asas-backoffice/src/routes/(screens)/backoffice/files.ts +10 -0
- package/template/apps/asas-backoffice/src/routes/(screens)/backoffice/screen.json +3971 -0
- package/template/apps/asas-backoffice/src/routes/(screens)/backoffice/types.ts +163 -0
- package/template/apps/asas-backoffice/src/routes/(screens)/backoffice.$.tsx +17 -0
- package/template/apps/asas-backoffice/src/routes/(screens)/backoffice.index.tsx +10 -0
- package/template/apps/asas-backoffice/src/routes/(screens)/backoffice.tsx +13 -0
- package/template/apps/asas-backoffice/src/routes/__root.tsx +95 -0
- package/template/apps/asas-backoffice/src/routes/api/$.ts +34 -0
- package/template/apps/asas-backoffice/src/routes/auth.create-first-user.tsx +113 -0
- package/template/apps/asas-backoffice/src/routes/auth.sign-in.tsx +77 -0
- package/template/apps/asas-backoffice/src/routes/unauthorized.tsx +134 -0
- package/template/apps/asas-backoffice/src/screen-form.tsx +36 -0
- package/template/apps/asas-backoffice/src/screen-list.tsx +140 -0
- package/template/apps/asas-backoffice/src/screen-names.ts +10 -0
- package/template/apps/asas-backoffice/src/screens.tsx +888 -0
- package/template/apps/asas-backoffice/src/server.ts +30 -0
- package/template/apps/asas-backoffice/src/slide.ts +22 -0
- package/template/apps/asas-backoffice/src/start.ts +42 -0
- package/template/apps/asas-backoffice/src/styles.css +202 -0
- package/template/apps/asas-backoffice/src/theme.ts +2 -0
- package/template/apps/asas-backoffice/tsconfig.json +19 -0
- package/template/apps/asas-backoffice/vite.config.ts +32 -0
- package/template/apps/asas-tanstack/components.json +25 -0
- package/template/apps/asas-tanstack/gitignore +3 -0
- package/template/apps/asas-tanstack/package.json +58 -0
- package/template/apps/asas-tanstack/public/favicon.svg +1 -0
- package/template/apps/asas-tanstack/src/api.ts +147 -0
- package/template/apps/asas-tanstack/src/auth-frame.tsx +150 -0
- package/template/apps/asas-tanstack/src/choose-password.tsx +150 -0
- package/template/apps/asas-tanstack/src/components/ui/accordion.tsx +10 -0
- package/template/apps/asas-tanstack/src/components/ui/alert-dialog.tsx +173 -0
- package/template/apps/asas-tanstack/src/components/ui/avatar.tsx +104 -0
- package/template/apps/asas-tanstack/src/components/ui/badge.tsx +48 -0
- package/template/apps/asas-tanstack/src/components/ui/breadcrumb.tsx +125 -0
- package/template/apps/asas-tanstack/src/components/ui/button-group.tsx +87 -0
- package/template/apps/asas-tanstack/src/components/ui/button.tsx +56 -0
- package/template/apps/asas-tanstack/src/components/ui/calendar.tsx +223 -0
- package/template/apps/asas-tanstack/src/components/ui/card.tsx +94 -0
- package/template/apps/asas-tanstack/src/components/ui/chart.tsx +369 -0
- package/template/apps/asas-tanstack/src/components/ui/checkbox.tsx +27 -0
- package/template/apps/asas-tanstack/src/components/ui/collapsible.tsx +19 -0
- package/template/apps/asas-tanstack/src/components/ui/combobox.tsx +300 -0
- package/template/apps/asas-tanstack/src/components/ui/dialog.tsx +149 -0
- package/template/apps/asas-tanstack/src/components/ui/direction.tsx +5 -0
- package/template/apps/asas-tanstack/src/components/ui/directional-icon.tsx +20 -0
- package/template/apps/asas-tanstack/src/components/ui/dropdown-menu.tsx +267 -0
- package/template/apps/asas-tanstack/src/components/ui/field.tsx +225 -0
- package/template/apps/asas-tanstack/src/components/ui/input-group.tsx +147 -0
- package/template/apps/asas-tanstack/src/components/ui/input-otp.tsx +20 -0
- package/template/apps/asas-tanstack/src/components/ui/input.tsx +20 -0
- package/template/apps/asas-tanstack/src/components/ui/label.tsx +18 -0
- package/template/apps/asas-tanstack/src/components/ui/popover.tsx +50 -0
- package/template/apps/asas-tanstack/src/components/ui/radio-group.tsx +10 -0
- package/template/apps/asas-tanstack/src/components/ui/resizable.tsx +47 -0
- package/template/apps/asas-tanstack/src/components/ui/scroll-area.tsx +52 -0
- package/template/apps/asas-tanstack/src/components/ui/select.tsx +190 -0
- package/template/apps/asas-tanstack/src/components/ui/separator.tsx +23 -0
- package/template/apps/asas-tanstack/src/components/ui/sheet.tsx +127 -0
- package/template/apps/asas-tanstack/src/components/ui/sidebar.tsx +716 -0
- package/template/apps/asas-tanstack/src/components/ui/skeleton.tsx +13 -0
- package/template/apps/asas-tanstack/src/components/ui/slider.tsx +13 -0
- package/template/apps/asas-tanstack/src/components/ui/spinner.tsx +11 -0
- package/template/apps/asas-tanstack/src/components/ui/switch.tsx +8 -0
- package/template/apps/asas-tanstack/src/components/ui/table.tsx +119 -0
- package/template/apps/asas-tanstack/src/components/ui/tabs.tsx +80 -0
- package/template/apps/asas-tanstack/src/components/ui/textarea.tsx +18 -0
- package/template/apps/asas-tanstack/src/components/ui/toggle-group.tsx +8 -0
- package/template/apps/asas-tanstack/src/components/ui/toggle.tsx +44 -0
- package/template/apps/asas-tanstack/src/components/ui/tooltip.tsx +68 -0
- package/template/apps/asas-tanstack/src/components.ts +35 -0
- package/template/apps/asas-tanstack/src/copy.ts +1207 -0
- package/template/apps/asas-tanstack/src/first-run.ts +16 -0
- package/template/apps/asas-tanstack/src/hooks/use-mobile.ts +19 -0
- package/template/apps/asas-tanstack/src/lib/utils.ts +6 -0
- package/template/apps/asas-tanstack/src/links.ts +38 -0
- package/template/apps/asas-tanstack/src/locale-switch.tsx +28 -0
- package/template/apps/asas-tanstack/src/no-organization.tsx +108 -0
- package/template/apps/asas-tanstack/src/quiet-abort.ts +25 -0
- package/template/apps/asas-tanstack/src/redirect.ts +39 -0
- package/template/apps/asas-tanstack/src/request-headers.ts +11 -0
- package/template/apps/asas-tanstack/src/routeTree.gen.ts +424 -0
- package/template/apps/asas-tanstack/src/router.tsx +18 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/files.ts +11 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/screen.json +4242 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/types.ts +160 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard.$.tsx +17 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard.index.tsx +10 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard.tsx +13 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/portal/files.ts +9 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/portal/screen.json +1564 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/portal/types.ts +94 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/portal.$.tsx +17 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/portal.index.tsx +10 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/portal.tsx +13 -0
- package/template/apps/asas-tanstack/src/routes/__root.tsx +99 -0
- package/template/apps/asas-tanstack/src/routes/api/$.ts +11 -0
- package/template/apps/asas-tanstack/src/routes/auth.accept-invitation.tsx +108 -0
- package/template/apps/asas-tanstack/src/routes/auth.first-run.tsx +114 -0
- package/template/apps/asas-tanstack/src/routes/auth.forgot-password.tsx +94 -0
- package/template/apps/asas-tanstack/src/routes/auth.handoff.ts +39 -0
- package/template/apps/asas-tanstack/src/routes/auth.reset-password.tsx +16 -0
- package/template/apps/asas-tanstack/src/routes/auth.set-password.tsx +16 -0
- package/template/apps/asas-tanstack/src/routes/auth.sign-in.tsx +98 -0
- package/template/apps/asas-tanstack/src/routes/auth.sign-up.tsx +143 -0
- package/template/apps/asas-tanstack/src/routes/index.tsx +146 -0
- package/template/apps/asas-tanstack/src/routes/unauthorized.tsx +134 -0
- package/template/apps/asas-tanstack/src/screen-form.tsx +36 -0
- package/template/apps/asas-tanstack/src/screen-list.tsx +140 -0
- package/template/apps/asas-tanstack/src/screen-names.ts +10 -0
- package/template/apps/asas-tanstack/src/screens.tsx +888 -0
- package/template/apps/asas-tanstack/src/server.ts +26 -0
- package/template/apps/asas-tanstack/src/slide.ts +22 -0
- package/template/apps/asas-tanstack/src/start.ts +57 -0
- package/template/apps/asas-tanstack/src/styles.css +163 -0
- package/template/apps/asas-tanstack/src/theme.ts +5 -0
- package/template/apps/asas-tanstack/tsconfig.json +19 -0
- package/template/apps/asas-tanstack/vite.config.ts +34 -0
- package/template/package.json +16 -0
- package/template/templates/asas/.claude/skills/add-field/SKILL.md +27 -0
- package/template/templates/asas/.claude/skills/add-kayan/SKILL.md +26 -0
- package/template/templates/asas/.claude/skills/eject-admin-piece/SKILL.md +28 -0
- package/template/templates/asas/.claude/skills/upgrade-mithaq/SKILL.md +35 -0
- package/template/templates/asas/.env.example +33 -0
- package/template/templates/asas/AGENTS.md +101 -0
- package/template/templates/asas/CLAUDE.md +1 -0
- package/template/templates/asas/README.md +1056 -0
- package/template/templates/asas/mithaq.hints.json +25 -0
- package/template/templates/asas/package.json +53 -0
- package/template/templates/asas/src/asas.ts +118 -0
- package/template/templates/asas/src/backoffice-http.ts +117 -0
- package/template/templates/asas/src/dev-setup.ts +112 -0
- package/template/templates/asas/src/local-database.ts +22 -0
- package/template/templates/asas/src/messages/account.json +132 -0
- package/template/templates/asas/src/messages/auditLog.json +304 -0
- package/template/templates/asas/src/messages/backofficeLog.json +132 -0
- package/template/templates/asas/src/messages/courses.json +312 -0
- package/template/templates/asas/src/messages/enrollments.json +60 -0
- package/template/templates/asas/src/messages/invitation.json +82 -0
- package/template/templates/asas/src/messages/localUsers.json +48 -0
- package/template/templates/asas/src/messages/media.json +76 -0
- package/template/templates/asas/src/messages/member.json +62 -0
- package/template/templates/asas/src/messages/memberRoles.json +48 -0
- package/template/templates/asas/src/messages/mithaq.json +214 -0
- package/template/templates/asas/src/messages/organization.json +72 -0
- package/template/templates/asas/src/messages/organizationRole.json +62 -0
- package/template/templates/asas/src/messages/roles.json +72 -0
- package/template/templates/asas/src/messages/session.json +102 -0
- package/template/templates/asas/src/messages/sessions.json +136 -0
- package/template/templates/asas/src/messages/settings.json +64 -0
- package/template/templates/asas/src/messages/tags.json +48 -0
- package/template/templates/asas/src/messages/user.json +112 -0
- package/template/templates/asas/src/messages/verification.json +62 -0
- package/template/templates/asas/src/migrations/20260920080701_parched_tombstone/migration.sql +177 -0
- package/template/templates/asas/src/migrations/20260920080701_parched_tombstone/snapshot.json +2169 -0
- package/template/templates/asas/src/migrations/20260920230505_zippy_ben_urich/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20260920230505_zippy_ben_urich/snapshot.json +2182 -0
- package/template/templates/asas/src/migrations/20260920231219_slim_retro_girl/migration.sql +20 -0
- package/template/templates/asas/src/migrations/20260920231219_slim_retro_girl/snapshot.json +2409 -0
- package/template/templates/asas/src/migrations/20260922082945_military_ironclad/migration.sql +15 -0
- package/template/templates/asas/src/migrations/20260922082945_military_ironclad/snapshot.json +2627 -0
- package/template/templates/asas/src/migrations/20260922112630_bitter_chat/migration.sql +5 -0
- package/template/templates/asas/src/migrations/20260922112630_bitter_chat/snapshot.json +2691 -0
- package/template/templates/asas/src/migrations/20260923235915_foamy_rachel_grey/migration.sql +8 -0
- package/template/templates/asas/src/migrations/20260923235915_foamy_rachel_grey/snapshot.json +2795 -0
- package/template/templates/asas/src/migrations/20260926145709_clean_the_initiative/migration.sql +3 -0
- package/template/templates/asas/src/migrations/20260926145709_clean_the_initiative/snapshot.json +2834 -0
- package/template/templates/asas/src/migrations/20260927212525_bent_prowler/migration.sql +9 -0
- package/template/templates/asas/src/migrations/20260927212525_bent_prowler/snapshot.json +2870 -0
- package/template/templates/asas/src/migrations/20260927223441_tranquil_bromley/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20260927223441_tranquil_bromley/snapshot.json +2881 -0
- package/template/templates/asas/src/migrations/20260928030047_outgoing_wraith/migration.sql +15 -0
- package/template/templates/asas/src/migrations/20260928030047_outgoing_wraith/snapshot.json +3073 -0
- package/template/templates/asas/src/migrations/20260929024407_grey_morlun/migration.sql +21 -0
- package/template/templates/asas/src/migrations/20260929024407_grey_morlun/snapshot.json +3098 -0
- package/template/templates/asas/src/migrations/20260929130530_bored_jackal/migration.sql +21 -0
- package/template/templates/asas/src/migrations/20260929130530_bored_jackal/snapshot.json +3124 -0
- package/template/templates/asas/src/migrations/20260930102924_material_newton_destine/migration.sql +32 -0
- package/template/templates/asas/src/migrations/20260930102924_material_newton_destine/snapshot.json +3324 -0
- package/template/templates/asas/src/migrations/20261003022353_clumsy_taskmaster/migration.sql +2 -0
- package/template/templates/asas/src/migrations/20261003022353_clumsy_taskmaster/snapshot.json +3350 -0
- package/template/templates/asas/src/migrations/20261003143249_premium_red_shift/migration.sql +21 -0
- package/template/templates/asas/src/migrations/20261003143249_premium_red_shift/snapshot.json +3635 -0
- package/template/templates/asas/src/migrations/20261008215349_square_norrin_radd/migration.sql +8 -0
- package/template/templates/asas/src/migrations/20261008215349_square_norrin_radd/snapshot.json +3635 -0
- package/template/templates/asas/src/migrations/20261008230204_many_scarecrow/migration.sql +18 -0
- package/template/templates/asas/src/migrations/20261008230204_many_scarecrow/snapshot.json +3635 -0
- package/template/templates/asas/src/migrations/20261010005521_elite_giant_girl/migration.sql +18 -0
- package/template/templates/asas/src/migrations/20261010005521_elite_giant_girl/snapshot.json +3635 -0
- package/template/templates/asas/src/migrations/20261010120822_needy_thunderbolt/migration.sql +11 -0
- package/template/templates/asas/src/migrations/20261010120822_needy_thunderbolt/snapshot.json +3667 -0
- package/template/templates/asas/src/migrations/20261012000752_organization_ownership/migration.sql +48 -0
- package/template/templates/asas/src/migrations/20261012000752_organization_ownership/snapshot.json +3891 -0
- package/template/templates/asas/src/migrations/20261014221355_tags_restrict/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261014221355_tags_restrict/snapshot.json +3881 -0
- package/template/templates/asas/src/migrations/20261014222218_one_seat_per_session/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261014222218_one_seat_per_session/snapshot.json +3894 -0
- package/template/templates/asas/src/migrations/20261014223522_session_archive_reason/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261014223522_session_archive_reason/snapshot.json +3907 -0
- package/template/templates/asas/src/migrations/20261014225728_course_archive_by_operation/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261014225728_course_archive_by_operation/snapshot.json +3907 -0
- package/template/templates/asas/src/migrations/20261015042444_organization_names/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261015042444_organization_names/snapshot.json +3920 -0
- package/template/templates/asas/src/migrations/20261015053212_member_last_seen/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261015053212_member_last_seen/snapshot.json +3933 -0
- package/template/templates/asas/src/migrations/20261015093203_activity_and_staff_history/migration.sql +15 -0
- package/template/templates/asas/src/migrations/20261015093203_activity_and_staff_history/snapshot.json +4113 -0
- package/template/templates/asas/src/migrations/20261015095752_portal/migration.sql +2 -0
- package/template/templates/asas/src/migrations/20261015095752_portal/snapshot.json +4113 -0
- package/template/templates/asas/src/migrations/20261015135529_better_auth_indexes/migration.sql +9 -0
- package/template/templates/asas/src/migrations/20261015135529_better_auth_indexes/snapshot.json +4302 -0
- package/template/templates/asas/src/migrations/20261015162438_audit_changes_json/migration.sql +27 -0
- package/template/templates/asas/src/migrations/20261015162438_audit_changes_json/snapshot.json +4289 -0
- package/template/templates/asas/src/migrations/drizzle.config.ts +11 -0
- package/template/templates/asas/src/migrations/schema.ts +30 -0
- package/template/templates/asas/src/mithaq.ts +4 -0
- package/template/templates/asas/src/organizations.ts +3 -0
- package/template/templates/asas/src/seed.ts +427 -0
- package/template/templates/asas/src/ui-backoffice/home.tsx +63 -0
- package/template/templates/asas/src/ui-backoffice/organization-actions.tsx +121 -0
- package/template/templates/asas/src/ui-backoffice/record-page.tsx +30 -0
- package/template/templates/asas/src/ui-backoffice/role-cells.tsx +59 -0
- package/template/templates/asas/src/ui-backoffice/user-actions.tsx +249 -0
- package/template/templates/asas/src/ui-dashboard/dashboard.tsx +300 -0
- package/template/templates/asas/src/ui-dashboard/home.tsx +73 -0
- package/template/templates/asas/src/ui-dashboard/members-card.tsx +147 -0
- package/template/templates/asas/src/ui-dashboard/members.tsx +829 -0
- package/template/templates/asas/src/ui-dashboard/theme-provider.tsx +10 -0
- package/template/templates/asas/src/ui-dashboard/versions.tsx +446 -0
- package/template/templates/asas/src/ui-portal/courses-home.tsx +96 -0
- package/template/templates/asas/src/ui-portal/home.tsx +73 -0
- package/template/templates/asas/src/ui-portal/page.tsx +11 -0
- package/template/templates/features/audit-log/audit-log/audit-log.ts +284 -0
- package/template/templates/features/audit-log/backoffice-log/backoffice-log.ts +67 -0
- package/template/templates/features/audit-log/index.ts +4 -0
- package/template/templates/features/audit-log/package.json +13 -0
- package/template/templates/features/auth/index.ts +11 -0
- package/template/templates/features/auth/local-users/local-users.ts +143 -0
- package/template/templates/features/auth/package.json +13 -0
- package/template/templates/features/auth/user/account.access.ts +8 -0
- package/template/templates/features/auth/user/account.ts +31 -0
- package/template/templates/features/auth/user/index.ts +18 -0
- package/template/templates/features/auth/user/invitation.access.ts +4 -0
- package/template/templates/features/auth/user/invitation.ts +27 -0
- package/template/templates/features/auth/user/member.access.ts +10 -0
- package/template/templates/features/auth/user/member.screens.ts +37 -0
- package/template/templates/features/auth/user/member.ts +26 -0
- package/template/templates/features/auth/user/organization.access.ts +20 -0
- package/template/templates/features/auth/user/organization.screens.ts +37 -0
- package/template/templates/features/auth/user/organization.ts +25 -0
- package/template/templates/features/auth/user/organizationRole.access.ts +8 -0
- package/template/templates/features/auth/user/organizationRole.ts +25 -0
- package/template/templates/features/auth/user/session.access.ts +8 -0
- package/template/templates/features/auth/user/session.ts +28 -0
- package/template/templates/features/auth/user/user.access.ts +30 -0
- package/template/templates/features/auth/user/user.screens.ts +45 -0
- package/template/templates/features/auth/user/user.ts +31 -0
- package/template/templates/features/auth/user/verification.access.ts +8 -0
- package/template/templates/features/auth/user/verification.ts +23 -0
- package/template/templates/features/courses/courses/courses.ts +601 -0
- package/template/templates/features/courses/courses/cover-reads.ts +33 -0
- package/template/templates/features/courses/courses/ui/archived-badge.tsx +9 -0
- package/template/templates/features/courses/courses/ui/fee-cell.tsx +19 -0
- package/template/templates/features/courses/courses/ui/no-courses.tsx +13 -0
- package/template/templates/features/courses/courses/ui/status-cell.tsx +33 -0
- package/template/templates/features/courses/courses/ui/unpublish-button.tsx +15 -0
- package/template/templates/features/courses/domain/archive.ts +39 -0
- package/template/templates/features/courses/domain/on-offer.ts +6 -0
- package/template/templates/features/courses/enrollments/enrollments.ts +309 -0
- package/template/templates/features/courses/enrollments/ui/session-cell.tsx +75 -0
- package/template/templates/features/courses/enrollments/ui/session-labels.tsx +36 -0
- package/template/templates/features/courses/index.ts +8 -0
- package/template/templates/features/courses/package.json +24 -0
- package/template/templates/features/courses/portal/ui/catalogue.tsx +86 -0
- package/template/templates/features/courses/portal/ui/course.tsx +176 -0
- package/template/templates/features/courses/portal/ui/enrollments.tsx +170 -0
- package/template/templates/features/courses/portal/ui/parts.tsx +95 -0
- package/template/templates/features/courses/portal/ui/reads.ts +76 -0
- package/template/templates/features/courses/portal/ui/rules.ts +87 -0
- package/template/templates/features/courses/portal/ui/shell.tsx +194 -0
- package/template/templates/features/courses/sessions/seats.ts +42 -0
- package/template/templates/features/courses/sessions/sessions.ts +186 -0
- package/template/templates/features/media/index.ts +10 -0
- package/template/templates/features/media/media/media.ts +131 -0
- package/template/templates/features/media/media/ui/preview.tsx +143 -0
- package/template/templates/features/media/package.json +19 -0
- package/template/templates/features/roles/index.ts +4 -0
- package/template/templates/features/roles/member-roles/member-roles.ts +207 -0
- package/template/templates/features/roles/package.json +21 -0
- package/template/templates/features/roles/roles/roles.ts +184 -0
- package/template/templates/features/roles/roles/ui/role-form.tsx +267 -0
- package/template/templates/features/roles/roles/ui/roles.tsx +190 -0
- package/template/templates/features/settings/index.ts +3 -0
- package/template/templates/features/settings/package.json +13 -0
- package/template/templates/features/settings/settings/settings.ts +64 -0
- package/template/templates/features/tags/index.ts +3 -0
- package/template/templates/features/tags/package.json +14 -0
- package/template/templates/features/tags/tags/tags.ts +80 -0
- package/template/templates/mithaq/components.json +22 -0
- package/template/templates/mithaq/package.json +33 -0
- package/template/templates/mithaq/src/mail/index.ts +32 -0
- package/template/templates/mithaq/src/mail/mailers.ts +36 -0
- package/template/templates/mithaq/src/mithaq/audit-sink.ts +111 -0
- package/template/templates/mithaq/src/mithaq/auth/backoffice-roles.ts +108 -0
- package/template/templates/mithaq/src/mithaq/auth/index.ts +1859 -0
- package/template/templates/mithaq/src/mithaq/auth/organizations.ts +80 -0
- package/template/templates/mithaq/src/mithaq/pages.ts +83 -0
- package/template/templates/mithaq/src/mithaq/rich-text-nodes.ts +19 -0
- package/template/templates/mithaq/src/mithaq.ts +593 -0
- package/template/templates/mithaq/src/roles/default-member-roles.ts +29 -0
- package/template/templates/mithaq/src/roles/grants.ts +47 -0
- package/template/templates/mithaq/src/roles/member-access.ts +11 -0
- package/template/templates/mithaq/src/roles/statements.ts +13 -0
- package/template/templates/mithaq/src/roles/system-roles.ts +16 -0
- package/template/templates/mithaq/src/roles/tenant-access.ts +260 -0
- package/template/templates/mithaq/src/roles/tenant-roles.ts +321 -0
- package/template/templates/mithaq/src/steps/expected-updated-at.ts +41 -0
- package/template/templates/mithaq/src/storage/defaults.ts +156 -0
- package/template/templates/mithaq/src/storage/index.ts +97 -0
- package/template/templates/mithaq/src/storage/local.ts +143 -0
- package/template/templates/mithaq/src/storage/r2.ts +5 -0
- package/template/templates/mithaq/src/storage/s3.ts +91 -0
- package/template/templates/mithaq/src/storage/uploadthing.ts +59 -0
- package/template/templates/mithaq/src/ui/activity-page.tsx +33 -0
- package/template/templates/mithaq/src/ui/activity.tsx +527 -0
- package/template/templates/mithaq/src/ui/app-shell.tsx +268 -0
- package/template/templates/mithaq/src/ui/kpi-card.tsx +25 -0
- package/template/templates/mithaq/src/ui/record-page.tsx +50 -0
- package/template/templates/mithaq/src/ui/rich-text-nodes.tsx +33 -0
- package/template/templates/mithaq/src/ui/screen-nav.tsx +28 -0
- package/template/templates/mithaq/src/ui/table-page.tsx +9 -0
- package/template/templates/mithaq/src/ui/top-shell.tsx +60 -0
- package/README.md +0 -3
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
import {
|
|
2
|
+
// add-on: audit-log
|
|
3
|
+
audit,
|
|
4
|
+
// end add-on: audit-log
|
|
5
|
+
base,
|
|
6
|
+
betterAuthTenant,
|
|
7
|
+
defaultAuthorize,
|
|
8
|
+
defaultBulkLimit,
|
|
9
|
+
defaultDepth,
|
|
10
|
+
defaultJoinLimit,
|
|
11
|
+
defaultLimit,
|
|
12
|
+
defaultMaxLimit,
|
|
13
|
+
screenKinds,
|
|
14
|
+
defineMithaq,
|
|
15
|
+
drizzleAdapter,
|
|
16
|
+
drizzleKinds, drizzleRules, drizzleIdKinds,
|
|
17
|
+
folderOf,
|
|
18
|
+
// add-on: roles
|
|
19
|
+
rbacTarget,
|
|
20
|
+
// end add-on: roles
|
|
21
|
+
Refusal,
|
|
22
|
+
screenTarget,
|
|
23
|
+
serviceTarget,
|
|
24
|
+
// add-on: uploads
|
|
25
|
+
storage,
|
|
26
|
+
// end add-on: uploads
|
|
27
|
+
versions,
|
|
28
|
+
zodKinds, zodRules, zodIdKinds, zodTarget,
|
|
29
|
+
type CommandInput,
|
|
30
|
+
} from 'mithaq'
|
|
31
|
+
// add-on: rich-text
|
|
32
|
+
import { richText } from 'mithaq/rich-text'
|
|
33
|
+
// end add-on: rich-text
|
|
34
|
+
import { fileURLToPath } from 'node:url'
|
|
35
|
+
// add-on: audit-log
|
|
36
|
+
import { auditSink } from './mithaq/audit-sink.ts'
|
|
37
|
+
// end add-on: audit-log
|
|
38
|
+
// add-on: rich-text
|
|
39
|
+
import { richTextNodes } from './mithaq/rich-text-nodes.ts'
|
|
40
|
+
// end add-on: rich-text
|
|
41
|
+
import { systemRoles } from './roles/system-roles.ts'
|
|
42
|
+
// add-on: uploads
|
|
43
|
+
import { storageConfig } from './storage/index.ts'
|
|
44
|
+
// end add-on: uploads
|
|
45
|
+
import { onBackoffice, organizationNameProblem } from './mithaq/auth/index.ts'
|
|
46
|
+
import { organizationName, organizations, showOrganization } from './mithaq/auth/organizations.ts'
|
|
47
|
+
import { assignedRoles, backofficeRolesOf, tenantAccess } from './roles/tenant-access.ts'
|
|
48
|
+
// without add-on: roles
|
|
49
|
+
// import { accessTarget } from './roles/member-access.ts'
|
|
50
|
+
// end without add-on: roles
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A local PostgreSQL on its default port, which `vite dev` falls back to when
|
|
54
|
+
* no DATABASE_URL is set; it is also the URL `.env.example` names. A
|
|
55
|
+
* deployment passes its own. `testDatabase` refuses an unset DATABASE_URL and
|
|
56
|
+
* this URL's port, so a test never reaches it.
|
|
57
|
+
*/
|
|
58
|
+
export const devDatabaseUrl = 'postgres://postgres:postgres@localhost:5432/mithaq'
|
|
59
|
+
/**
|
|
60
|
+
* Production refuses the dev fallback, whose password sits in this file. This
|
|
61
|
+
* runs on import, before anything connects.
|
|
62
|
+
*/
|
|
63
|
+
if (process.env.NODE_ENV === 'production' && !process.env.DATABASE_URL) {
|
|
64
|
+
throw new Error('A production boot needs DATABASE_URL, the database this deployment uses. Nothing here falls back to the dev database in production.')
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* `node --test` sets `NODE_TEST_CONTEXT` on every process it runs, so a test
|
|
68
|
+
* that forgot DATABASE_URL never reaches the dev database.
|
|
69
|
+
*/
|
|
70
|
+
if (process.env.NODE_TEST_CONTEXT && !process.env.DATABASE_URL) {
|
|
71
|
+
throw new Error('A test run needs DATABASE_URL, a database of its own: set DATABASE_URL. Nothing under node --test falls back to the dev database.')
|
|
72
|
+
}
|
|
73
|
+
const connectionString = process.env.DATABASE_URL ?? devDatabaseUrl
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Refuses a command that would open a real connection with no DATABASE_URL
|
|
77
|
+
* named. `vite dev` never calls it and uses the dev fallback.
|
|
78
|
+
*/
|
|
79
|
+
const requireDatabaseUrl = () => {
|
|
80
|
+
if (!process.env.DATABASE_URL) {
|
|
81
|
+
throw new Refusal(
|
|
82
|
+
'Refusing to connect: set DATABASE_URL to the database this command should use. Nothing here falls back to the dev database.',
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The asas's own source folder, which holds the migrations and each screen's
|
|
89
|
+
* own pieces. The language names it, and a feature's folder, by path and
|
|
90
|
+
* imports neither, so the packages depend one way: the asas on the language.
|
|
91
|
+
*/
|
|
92
|
+
const asasSource = new URL('../../asas/src/', import.meta.url)
|
|
93
|
+
const featuresFolder = new URL('../../features/', import.meta.url)
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* A production `connect()` refuses to start while migrations are unapplied, so
|
|
97
|
+
* a deploy changes the database only when someone runs
|
|
98
|
+
* `drizzle-kit migrate --config src/migrations/drizzle.config.ts`; `'run'`
|
|
99
|
+
* applies them on boot instead. The folder comes from this file because each
|
|
100
|
+
* app calls `connect()` from its own folder.
|
|
101
|
+
*/
|
|
102
|
+
const drizzleStore = drizzleAdapter({
|
|
103
|
+
connectionString,
|
|
104
|
+
/** The column builders per kind, mithaq's default. */
|
|
105
|
+
kinds: drizzleKinds,
|
|
106
|
+
/** The column rules, mithaq's default. */
|
|
107
|
+
rules: drizzleRules,
|
|
108
|
+
/** The kinds an id may use, mithaq's default. */
|
|
109
|
+
idKinds: drizzleIdKinds,
|
|
110
|
+
prodMigrations: { migrationsFolder: fileURLToPath(new URL('migrations/', asasSource)), pending: 'refuse' },
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* A drizzle store whose `connect()` (the schema push) and `migrate` command are
|
|
115
|
+
* refused without DATABASE_URL, since either could silently reach the dev
|
|
116
|
+
* database. `init` returns a fresh store per build, so it is guarded too.
|
|
117
|
+
*/
|
|
118
|
+
const guarded = (store: typeof drizzleStore): typeof drizzleStore => ({
|
|
119
|
+
...store,
|
|
120
|
+
init: (kayan) => guarded((store.init(kayan) ?? store) as typeof drizzleStore) as never,
|
|
121
|
+
connect: async (args?: { readOnly?: boolean }) => {
|
|
122
|
+
requireDatabaseUrl()
|
|
123
|
+
await store.connect(args)
|
|
124
|
+
},
|
|
125
|
+
commands: {
|
|
126
|
+
migrate: async (input: CommandInput) => {
|
|
127
|
+
requireDatabaseUrl()
|
|
128
|
+
return store.commands!.migrate!(input)
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
})
|
|
132
|
+
// add-on: roles
|
|
133
|
+
// A build has one `access` target. Swapping this line for `betterAuthTarget` moves
|
|
134
|
+
// custom roles into the provider's own rows, but the assignment guard
|
|
135
|
+
// `assignableRole` stays bound to this shape: it reads the `roles` kayan by a
|
|
136
|
+
// `name` filter and a `permissions` list, where the provider keys rows on `role`
|
|
137
|
+
// and holds a `permission` object. It fails closed (an unreadable role is every
|
|
138
|
+
// key of the project), so a project that swaps this must teach the guard the
|
|
139
|
+
// provider's table and columns too.
|
|
140
|
+
const accessOptions = {
|
|
141
|
+
/** The roles this project ships, and the names a row may be keyed on. */
|
|
142
|
+
roles: systemRoles,
|
|
143
|
+
/** How an actor's assigned slugs are read, for every row table. */
|
|
144
|
+
rolesOf: assignedRoles,
|
|
145
|
+
// The rows every kayan placing `tenant.field` answers from. A kayan drops them
|
|
146
|
+
// with `access: { tenant: false }`; a shared one never had them, which is how
|
|
147
|
+
// the generated auth kayan keep their default deny, except the two whose
|
|
148
|
+
// `<kayan>.access.ts` the generator imports.
|
|
149
|
+
access: tenantAccess,
|
|
150
|
+
/** Where the target seeds the system roles and reads the assignments. */
|
|
151
|
+
kayan: { roles: 'roles', memberRoles: 'memberRoles' },
|
|
152
|
+
} as const
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The access target over these roles: `rbacTarget` whole, except `assign`,
|
|
156
|
+
* which writes and is refused without DATABASE_URL unless run with `--check`.
|
|
157
|
+
*/
|
|
158
|
+
export const accessTarget = (roles: typeof systemRoles = systemRoles) => {
|
|
159
|
+
const target = rbacTarget({ ...accessOptions, roles })
|
|
160
|
+
return {
|
|
161
|
+
...target,
|
|
162
|
+
commands: {
|
|
163
|
+
assign: async (input: CommandInput) => {
|
|
164
|
+
if (!input.flags.includes('--check')) requireDatabaseUrl()
|
|
165
|
+
return target.commands!.assign!(input)
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// end add-on: roles
|
|
171
|
+
// without add-on: roles
|
|
172
|
+
// export { accessTarget }
|
|
173
|
+
// end without add-on: roles
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Every target but the screens. A screen names the kayan it draws, so a build
|
|
177
|
+
* holding only some of this project's kayan, as a test's does, is refused by it
|
|
178
|
+
* and uses a language whose screen draws only what it holds.
|
|
179
|
+
*/
|
|
180
|
+
export const serverTargets = {
|
|
181
|
+
drizzle: guarded(drizzleStore),
|
|
182
|
+
zod: zodTarget({
|
|
183
|
+
/** The schema builders per kind, mithaq's default. */
|
|
184
|
+
kinds: zodKinds,
|
|
185
|
+
/** The schema rules, mithaq's default. */
|
|
186
|
+
rules: zodRules,
|
|
187
|
+
/** The kinds an id may use, mithaq's default. */
|
|
188
|
+
idKinds: zodIdKinds,
|
|
189
|
+
}),
|
|
190
|
+
rbac: accessTarget(),
|
|
191
|
+
// Every kayan placing `organization` belongs to one organization,
|
|
192
|
+
// the session's active one. `restrict`: an organization that still holds a row
|
|
193
|
+
// is not deleted, so no row is left behind with no tenant to read it.
|
|
194
|
+
tenant: betterAuthTenant({
|
|
195
|
+
relationTo: 'organization',
|
|
196
|
+
onDelete: 'restrict',
|
|
197
|
+
// New rows take the active organization, mithaq's tenant fill rule.
|
|
198
|
+
newRows: 'active',
|
|
199
|
+
// How many organizations this project holds; in one a start over more is refused.
|
|
200
|
+
organizations,
|
|
201
|
+
// Staff read every organization with `tenant: 'all'` in the backoffice app
|
|
202
|
+
// alone, which is the app's own fact and never the request's; their rows
|
|
203
|
+
// there write nothing.
|
|
204
|
+
backoffice: ({ user }) => onBackoffice() && backofficeRolesOf(user).length > 0,
|
|
205
|
+
}),
|
|
206
|
+
// The runner is a target too (`capability: 'service'`); a build has exactly
|
|
207
|
+
// one, under this name. It holds no operations: each kayan spreads
|
|
208
|
+
// `defaultOperations` into its own `targets.service.operations` and leaves
|
|
209
|
+
// out any it does not want, such as `count`.
|
|
210
|
+
service: serviceTarget({
|
|
211
|
+
// One of the three lists every write runs around its own: a step here runs
|
|
212
|
+
// last inside the write's transaction, as the audit log's `audit` does,
|
|
213
|
+
// writing one `auditLog` row in it. A precondition goes in `beforeSteps`, and
|
|
214
|
+
// a webhook or email after a durable write in `afterCommit`.
|
|
215
|
+
afterSteps: [
|
|
216
|
+
// add-on: audit-log
|
|
217
|
+
audit,
|
|
218
|
+
// end add-on: audit-log
|
|
219
|
+
],
|
|
220
|
+
// add-on: audit-log
|
|
221
|
+
/** Where the `audit` step hands each write: the template's audit log. */
|
|
222
|
+
audit: auditSink,
|
|
223
|
+
// end add-on: audit-log
|
|
224
|
+
/** The access evaluator every operation runs before its first step. */
|
|
225
|
+
authorize: defaultAuthorize,
|
|
226
|
+
/** The deepest a read or a write may nest another before it is refused, eight. */
|
|
227
|
+
depth: defaultDepth,
|
|
228
|
+
/**
|
|
229
|
+
* The page a `find` or a `findVersions` answers when its caller names no
|
|
230
|
+
* `limit`, ten, and the most rows a served one may ask for, a hundred.
|
|
231
|
+
*/
|
|
232
|
+
defaultLimit,
|
|
233
|
+
maxLimit: defaultMaxLimit,
|
|
234
|
+
/** How many child rows a populated join carries where its field names none, ten. */
|
|
235
|
+
joinLimit: defaultJoinLimit,
|
|
236
|
+
/** The most rows one bulk write may match, a thousand. */
|
|
237
|
+
bulkLimit: defaultBulkLimit,
|
|
238
|
+
// add-on: courses
|
|
239
|
+
// The field that marks a row deleted. A kayan takes soft delete by declaring
|
|
240
|
+
// a field of this name, as `enrollments` does. A build where no kayan
|
|
241
|
+
// declares it is refused, because every delete in it would remove rows for
|
|
242
|
+
// good. Spreading the field into every kayan is refused where a permission
|
|
243
|
+
// store reads the kayan, since a role row the delete marked would still grant
|
|
244
|
+
// its keys.
|
|
245
|
+
softDelete: 'deletedAt',
|
|
246
|
+
// end add-on: courses
|
|
247
|
+
/** The database a kayan writes through when it names no other. */
|
|
248
|
+
store: 'drizzle',
|
|
249
|
+
/** The target a write is validated against when a kayan names no other. */
|
|
250
|
+
schema: 'zod',
|
|
251
|
+
}),
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// add-on: uploads
|
|
255
|
+
/**
|
|
256
|
+
* Files: the `upload` kind and the storage target, from the storage rukn over
|
|
257
|
+
* this project's config. `mithaq storage sweep` reads every row, so it is
|
|
258
|
+
* refused without DATABASE_URL like `migrate`.
|
|
259
|
+
*/
|
|
260
|
+
const files = storage(storageConfig)
|
|
261
|
+
const guardedFiles = {
|
|
262
|
+
...files,
|
|
263
|
+
targets: {
|
|
264
|
+
...files.targets,
|
|
265
|
+
storage: {
|
|
266
|
+
...files.targets.storage,
|
|
267
|
+
commands: {
|
|
268
|
+
...files.targets.storage.commands,
|
|
269
|
+
sweep: async (input: CommandInput) => {
|
|
270
|
+
requireDatabaseUrl()
|
|
271
|
+
return files.targets.storage.commands.sweep(input)
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
}
|
|
277
|
+
// end add-on: uploads
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* The pillars the language's kinds, rules and built targets come from: `base`
|
|
281
|
+
* brings the shipped kinds and rules, and `versions` the target a kayan keeps
|
|
282
|
+
* its past and drafts through by writing `targets.versions`. One row keeps its
|
|
283
|
+
* last hundred versions unless its kayan names another window. A rukn of the
|
|
284
|
+
* project's own goes beside them.
|
|
285
|
+
*/
|
|
286
|
+
export const arkan = [
|
|
287
|
+
base,
|
|
288
|
+
// add-on: rich-text
|
|
289
|
+
// The rich text kind over this project's node list.
|
|
290
|
+
richText({ nodes: richTextNodes }),
|
|
291
|
+
// end add-on: rich-text
|
|
292
|
+
// add-on: uploads
|
|
293
|
+
// The `upload` kind and the storage target.
|
|
294
|
+
guardedFiles,
|
|
295
|
+
// end add-on: uploads
|
|
296
|
+
versions({ max: 100 }),
|
|
297
|
+
] as const
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* The context every served call and first registration hands its steps, built
|
|
301
|
+
* here from the request: mithaq reads no header and no body key into it. A
|
|
302
|
+
* fresh id per request, so nothing a client sends names it. A step reads the
|
|
303
|
+
* key untyped unless the runner declares `context`, for example
|
|
304
|
+
* `context: {} as { requestId: string }`, and then every call in the app passes
|
|
305
|
+
* one too.
|
|
306
|
+
*/
|
|
307
|
+
export const requestContext = (_request: Request) => ({ requestId: crypto.randomUUID() })
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Each screen file is named `ui('file.tsx#Export')` off a folder URL taken from
|
|
311
|
+
* this file, so the build reads it wherever it runs and refuses a missing file
|
|
312
|
+
* or export. `ui/` holds what several screens draw with, each screen's own
|
|
313
|
+
* folder what only it does.
|
|
314
|
+
*/
|
|
315
|
+
export const ui = folderOf(new URL('./ui/', import.meta.url))
|
|
316
|
+
export const dashboardUi = folderOf(new URL('ui-dashboard/', asasSource))
|
|
317
|
+
export const portalUi = folderOf(new URL('ui-portal/', asasSource))
|
|
318
|
+
export const backofficeUi = folderOf(new URL('ui-backoffice/', asasSource))
|
|
319
|
+
// add-on: courses
|
|
320
|
+
/** Every screen's provider of course titles for a session's label, which the courses feature draws. */
|
|
321
|
+
const sessionLabels = folderOf(new URL('courses/enrollments/ui/', featuresFolder))('session-labels.tsx#SessionLabels')
|
|
322
|
+
/** The portal's own look, which the courses feature draws for its students. */
|
|
323
|
+
const coursesPortal = folderOf(new URL('courses/portal/ui/', featuresFolder))
|
|
324
|
+
// end add-on: courses
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Written once and spread into each screen: what draws each kind and the
|
|
328
|
+
* shared layouts. A piece only one screen draws is written on that screen.
|
|
329
|
+
*/
|
|
330
|
+
const shared = {
|
|
331
|
+
controls: screenKinds,
|
|
332
|
+
layouts: {
|
|
333
|
+
app: ui('app-shell.tsx#AppShell'),
|
|
334
|
+
record: ui('record-page.tsx#RecordPage'),
|
|
335
|
+
table: ui('table-page.tsx#TablePage'),
|
|
336
|
+
},
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** The portal screen, the students' and instructors' own. The test language narrows it to the kayan a test builds. */
|
|
340
|
+
export const portalScreen = screenTarget({
|
|
341
|
+
...shared,
|
|
342
|
+
path: '/portal',
|
|
343
|
+
kayan: [
|
|
344
|
+
// add-on: courses
|
|
345
|
+
// `sessions` is listed for its title alone, which an enrollment's session
|
|
346
|
+
// reads by; it has no page here and so no nav entry.
|
|
347
|
+
'courses',
|
|
348
|
+
'enrollments',
|
|
349
|
+
'sessions',
|
|
350
|
+
// end add-on: courses
|
|
351
|
+
// add-on: media
|
|
352
|
+
// `media` is listed for an image's title alone, such as a course's cover,
|
|
353
|
+
// which a course page draws read-only; it has no page here.
|
|
354
|
+
'media',
|
|
355
|
+
// end add-on: media
|
|
356
|
+
// `user` is listed for its title alone, which a relationship to a person reads by.
|
|
357
|
+
'user',
|
|
358
|
+
],
|
|
359
|
+
providers: [
|
|
360
|
+
// add-on: rich-text
|
|
361
|
+
ui('rich-text-nodes.tsx#RichTextNodes'),
|
|
362
|
+
// end add-on: rich-text
|
|
363
|
+
// add-on: courses
|
|
364
|
+
sessionLabels,
|
|
365
|
+
// end add-on: courses
|
|
366
|
+
],
|
|
367
|
+
pages: {
|
|
368
|
+
// add-on: courses
|
|
369
|
+
// A greeting, the viewer's next session, and courses it can join.
|
|
370
|
+
home: { path: '/', component: portalUi('courses-home.tsx#Home') },
|
|
371
|
+
// end add-on: courses
|
|
372
|
+
// without add-on: courses
|
|
373
|
+
// home: { path: '/', component: portalUi('home.tsx#Home') },
|
|
374
|
+
// end without add-on: courses
|
|
375
|
+
},
|
|
376
|
+
// Its own shell, which draws no link to the backoffice app: staff reach it from
|
|
377
|
+
// the dashboard alone. Every page a tree draws is that shell over the portal's
|
|
378
|
+
// single column; the courses example's pages draw it whole.
|
|
379
|
+
layouts: {
|
|
380
|
+
// add-on: courses
|
|
381
|
+
top: coursesPortal('shell.tsx#PortalShell'),
|
|
382
|
+
// end add-on: courses
|
|
383
|
+
// without add-on: courses
|
|
384
|
+
// top: ui('top-shell.tsx#PortalTopShell'),
|
|
385
|
+
// end without add-on: courses
|
|
386
|
+
page: portalUi('page.tsx#Page'),
|
|
387
|
+
},
|
|
388
|
+
components: {},
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
/** The dashboard screen, the tenant back office. The test language narrows it to the kayan a test builds. */
|
|
392
|
+
export const dashboardScreen = screenTarget({
|
|
393
|
+
...shared,
|
|
394
|
+
// The top-bar shell too, which the courses board page is rooted at.
|
|
395
|
+
layouts: { ...shared.layouts, top: ui('top-shell.tsx#TopShell') },
|
|
396
|
+
path: '/dashboard',
|
|
397
|
+
kayan: [
|
|
398
|
+
// add-on: courses
|
|
399
|
+
'courses',
|
|
400
|
+
'sessions',
|
|
401
|
+
'enrollments',
|
|
402
|
+
// end add-on: courses
|
|
403
|
+
'tags',
|
|
404
|
+
// add-on: media
|
|
405
|
+
'media',
|
|
406
|
+
// end add-on: media
|
|
407
|
+
// add-on: roles
|
|
408
|
+
// `memberRoles` is listed for its access report alone, which the members
|
|
409
|
+
// page's sheet is drawn by; it has no page of its own.
|
|
410
|
+
'roles',
|
|
411
|
+
'memberRoles',
|
|
412
|
+
// end add-on: roles
|
|
413
|
+
// add-on: settings
|
|
414
|
+
'settings',
|
|
415
|
+
// end add-on: settings
|
|
416
|
+
// `user` and `organization` are listed for their titles alone: they have no
|
|
417
|
+
// page and so no nav entry, and a relationship to either shows the row's name.
|
|
418
|
+
'user',
|
|
419
|
+
'organization',
|
|
420
|
+
// add-on: audit-log
|
|
421
|
+
// `auditLog` is listed for its access report, which the edit pages' activity
|
|
422
|
+
// and the Activity page are drawn by, and for the person filter's own field.
|
|
423
|
+
'auditLog',
|
|
424
|
+
// end add-on: audit-log
|
|
425
|
+
],
|
|
426
|
+
providers: [
|
|
427
|
+
dashboardUi('theme-provider.tsx#ThemeProvider'),
|
|
428
|
+
// add-on: rich-text
|
|
429
|
+
ui('rich-text-nodes.tsx#RichTextNodes'),
|
|
430
|
+
// end add-on: rich-text
|
|
431
|
+
// add-on: courses
|
|
432
|
+
sessionLabels,
|
|
433
|
+
// end add-on: courses
|
|
434
|
+
],
|
|
435
|
+
pages: {
|
|
436
|
+
// add-on: courses
|
|
437
|
+
// The page at `/`, served only to an actor who reads the courses it counts.
|
|
438
|
+
dashboard: { path: '/', component: dashboardUi('dashboard.tsx#Dashboard'), permission: 'courses.find' },
|
|
439
|
+
// end add-on: courses
|
|
440
|
+
// without add-on: courses
|
|
441
|
+
// // The page at `/`: a figure for each list the nav offers the viewer.
|
|
442
|
+
// dashboard: { path: '/', component: dashboardUi('home.tsx#Home') },
|
|
443
|
+
// end without add-on: courses
|
|
444
|
+
// add-on: roles
|
|
445
|
+
// A page that belongs to no kayan, listed in the nav under its own label
|
|
446
|
+
// and served only to an actor holding the key it names: `member.find`, the
|
|
447
|
+
// key the auth setup asks before the provider lists members.
|
|
448
|
+
members: {
|
|
449
|
+
path: '/members',
|
|
450
|
+
component: dashboardUi('members.tsx#Members'),
|
|
451
|
+
// With no organization shown, its members are the app's users.
|
|
452
|
+
nav: { label: showOrganization ? { en: 'Members', ar: 'الأعضاء' } : { en: 'Users', ar: 'المستخدمون' } },
|
|
453
|
+
permission: 'member.find',
|
|
454
|
+
},
|
|
455
|
+
// end add-on: roles
|
|
456
|
+
// add-on: audit-log
|
|
457
|
+
// Every change in the organization, filtered and paged, for an actor holding the log's key,
|
|
458
|
+
// which reads the entries of the kayan it lists alone. Read-only, as the log is.
|
|
459
|
+
activity: {
|
|
460
|
+
path: '/activity',
|
|
461
|
+
component: ui('activity-page.tsx#ActivityPage'),
|
|
462
|
+
nav: { label: { en: 'Activity', ar: 'النشاط' } },
|
|
463
|
+
permission: 'auditLog.find',
|
|
464
|
+
},
|
|
465
|
+
// end add-on: audit-log
|
|
466
|
+
},
|
|
467
|
+
// A piece only one kayan draws is named by `ui()` in that kayan's file; these
|
|
468
|
+
// are drawn by name from many.
|
|
469
|
+
components: {
|
|
470
|
+
// add-on: audit-log
|
|
471
|
+
// A record's own audit entries, which each edit page draws in its side column.
|
|
472
|
+
activity: ui('activity.tsx#Activity'),
|
|
473
|
+
// A record's whole history, filtered and paged, which an edit page draws in a tab beside Versions.
|
|
474
|
+
activityLog: ui('activity.tsx#ActivityTab'),
|
|
475
|
+
// end add-on: audit-log
|
|
476
|
+
// A row's versions, with their restore, which the edit page of a kayan that
|
|
477
|
+
// keeps drafts draws in a tab of its own.
|
|
478
|
+
versions: dashboardUi('versions.tsx#Versions'),
|
|
479
|
+
},
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* The backoffice screen: staff above every tenant, entered by `screen.backoffice`,
|
|
484
|
+
* which only a backoffice role holds, and served by its own app,
|
|
485
|
+
* `apps/asas-backoffice`, whose requests alone get the backoffice roles from the
|
|
486
|
+
* template's role reader. It lists the provider's generated kayan and every
|
|
487
|
+
* tenant kayan across organizations, read-only: a backoffice role reads every
|
|
488
|
+
* tenant's rows by its keys and writes none. Local accounts are listed for a
|
|
489
|
+
* super admin alone, who verifies a sign-up there. The settings global is not
|
|
490
|
+
* listed: a list across tenants reads by `find`, which a global declares none
|
|
491
|
+
* of.
|
|
492
|
+
*/
|
|
493
|
+
export const backofficeScreen = screenTarget({
|
|
494
|
+
...shared,
|
|
495
|
+
// The dashboard's frame with no organization switcher, which staff have no
|
|
496
|
+
// use for, and a record page with a link back to its list.
|
|
497
|
+
layouts: { ...shared.layouts, app: ui('app-shell.tsx#BackofficeShell'), record: backofficeUi('record-page.tsx#BackofficeRecordPage') },
|
|
498
|
+
path: '/backoffice',
|
|
499
|
+
kayan: [
|
|
500
|
+
'user',
|
|
501
|
+
'organization',
|
|
502
|
+
'member',
|
|
503
|
+
// add-on: courses
|
|
504
|
+
'courses',
|
|
505
|
+
'sessions',
|
|
506
|
+
'enrollments',
|
|
507
|
+
// end add-on: courses
|
|
508
|
+
'tags',
|
|
509
|
+
// add-on: roles
|
|
510
|
+
'roles',
|
|
511
|
+
'memberRoles',
|
|
512
|
+
// end add-on: roles
|
|
513
|
+
'localUsers',
|
|
514
|
+
// add-on: audit-log
|
|
515
|
+
// Who impersonated whom, set whose backoffice role and banned whom, for the roles that read it.
|
|
516
|
+
'backofficeLog',
|
|
517
|
+
// end add-on: audit-log
|
|
518
|
+
],
|
|
519
|
+
providers: [
|
|
520
|
+
dashboardUi('theme-provider.tsx#ThemeProvider'),
|
|
521
|
+
// add-on: rich-text
|
|
522
|
+
ui('rich-text-nodes.tsx#RichTextNodes'),
|
|
523
|
+
// end add-on: rich-text
|
|
524
|
+
// add-on: courses
|
|
525
|
+
sessionLabels,
|
|
526
|
+
// end add-on: courses
|
|
527
|
+
],
|
|
528
|
+
pages: { home: { path: '/', component: backofficeUi('home.tsx#Home') } },
|
|
529
|
+
components: {},
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
const language = defineMithaq({
|
|
533
|
+
/** Every locale this project's copy is written in, so a missing one is a type error. */
|
|
534
|
+
locales: ['ar', 'en'] as const,
|
|
535
|
+
arkan,
|
|
536
|
+
// A project's own target spreads in here beside the shipped ones and is
|
|
537
|
+
// checked exactly like them.
|
|
538
|
+
targets: {
|
|
539
|
+
...serverTargets,
|
|
540
|
+
// The three screens, each a target of capability `screen` under its own key.
|
|
541
|
+
// A kayan says what each screen draws of it under `targets.<screen>`, and
|
|
542
|
+
// `mithaq generate screens` writes each screen's `screen.json`, `types.ts`,
|
|
543
|
+
// glob and route files under `generate.screens`.
|
|
544
|
+
dashboard: dashboardScreen,
|
|
545
|
+
portal: portalScreen,
|
|
546
|
+
backoffice: backofficeScreen,
|
|
547
|
+
},
|
|
548
|
+
// Not a key the language knows: the project's own, passed through typed, and
|
|
549
|
+
// read by `mithaq generate auth`, `messages` and `screens` to find where each
|
|
550
|
+
// writes. `providers` is the module `generate auth` reads the Better Auth
|
|
551
|
+
// instances from and writes into each generated kayan's import, and `language`
|
|
552
|
+
// the module each of them imports `defineKayan` from. `screens` is
|
|
553
|
+
// the host app's routes folder, one per screen because more than one app
|
|
554
|
+
// serves them: the tenant's two by `apps/asas-tanstack`, the backoffice by
|
|
555
|
+
// `apps/asas-backoffice`.
|
|
556
|
+
// Each path is from the asas's folder, where every `mithaq` command runs.
|
|
557
|
+
generate: {
|
|
558
|
+
language: '@asas/mithaq',
|
|
559
|
+
features: '../features/',
|
|
560
|
+
auth: '../features/auth/',
|
|
561
|
+
providers: '@asas/mithaq/auth',
|
|
562
|
+
messages: 'src/messages/',
|
|
563
|
+
migrations: 'src/migrations/',
|
|
564
|
+
screens: {
|
|
565
|
+
dashboard: '../../apps/asas-tanstack/src/routes/(screens)/',
|
|
566
|
+
portal: '../../apps/asas-tanstack/src/routes/(screens)/',
|
|
567
|
+
// add-on: backoffice
|
|
568
|
+
backoffice: '../../apps/asas-backoffice/src/routes/(screens)/',
|
|
569
|
+
// end add-on: backoffice
|
|
570
|
+
// without add-on: backoffice
|
|
571
|
+
// backoffice: false,
|
|
572
|
+
// end without add-on: backoffice
|
|
573
|
+
},
|
|
574
|
+
},
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
// `targets` is the language's own, which `mithaq generate auth` reads beside
|
|
578
|
+
// `generate` to title a provider kayan on each screen that lists it.
|
|
579
|
+
export const { defineKayan, defineGlobal, buildAsas, rows, user, generate, locales, targets, targets: { tenant } } = language
|
|
580
|
+
/** The language's type, which the apps check their admin components map against: a kind of this project's own needs a control there. */
|
|
581
|
+
export type Language = typeof language
|
|
582
|
+
|
|
583
|
+
// Every mode holds an organization's names to `organizationName`'s locales and falls back to its first, so a locale it
|
|
584
|
+
// leaves out would be one no organization could be named in.
|
|
585
|
+
const unnamed = organizationNameProblem({ names: organizationName, locales })
|
|
586
|
+
if (unnamed) throw new Error(`src/mithaq/auth/organizations.ts: ${unnamed}`)
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* The field every tenant-owned kayan places, as `organization`: the tenant target's own, under the
|
|
590
|
+
* backoffice's word for a tenant. Its copy is all a kayan may change of it, and a kayan that places
|
|
591
|
+
* none is shared.
|
|
592
|
+
*/
|
|
593
|
+
export const organization = { ...tenant.field, label: { en: 'Organization', ar: 'المؤسسة' } }
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { SystemRoles } from 'mithaq'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Better Auth's own organization roles, `owner`, `admin` and `member`, as the
|
|
5
|
+
* roles a project without the roles add-on ships: the access target answers a
|
|
6
|
+
* member the ones its `member.role` names in that organization. An owner and an
|
|
7
|
+
* admin hold every key of the project, and a member reads the tags and changes
|
|
8
|
+
* nothing. A kayan added later is the owner's and the admin's alone until its
|
|
9
|
+
* keys are listed under `member` here; the roles add-on brings roles a tenant
|
|
10
|
+
* writes itself.
|
|
11
|
+
*/
|
|
12
|
+
export const defaultMemberRoles = {
|
|
13
|
+
owner: { label: { ar: 'مالك', en: 'Owner' }, permissions: '*' },
|
|
14
|
+
admin: { label: { ar: 'مشرف', en: 'Admin' }, permissions: '*' },
|
|
15
|
+
member: {
|
|
16
|
+
label: { ar: 'عضو', en: 'Member' },
|
|
17
|
+
permissions: [
|
|
18
|
+
'screen.dashboard',
|
|
19
|
+
'tags.find',
|
|
20
|
+
'tags.count',
|
|
21
|
+
'tags.findByID',
|
|
22
|
+
'tags.fields.id',
|
|
23
|
+
'tags.fields.createdAt',
|
|
24
|
+
'tags.fields.updatedAt',
|
|
25
|
+
'tags.fields.organization',
|
|
26
|
+
'tags.fields.name',
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
} as const satisfies SystemRoles
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// What a page asks before it offers a role or a key, measured the way the
|
|
2
|
+
// server measures the write it would make. It imports nothing at runtime, so a
|
|
3
|
+
// page's chunk takes no server module with it, and the server's own guards
|
|
4
|
+
// import the same functions.
|
|
5
|
+
import type { AccessReport } from 'mithaq/admin'
|
|
6
|
+
import type { tenantRoles } from './system-roles.ts'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Whether the viewer may hand over these keys, as a role or one at a time,
|
|
10
|
+
* measured the way the server's guards measure it: a root granter anything,
|
|
11
|
+
* anyone else only keys it holds, and `'*'` only a root granter. Keys the
|
|
12
|
+
* viewer may not read are not grantable. The server refuses a wrong answer here
|
|
13
|
+
* either way.
|
|
14
|
+
*/
|
|
15
|
+
export const grantable = (access: AccessReport, keys: readonly string[] | undefined): boolean => {
|
|
16
|
+
const held = access.permissions
|
|
17
|
+
if (held === '*') return true
|
|
18
|
+
return held !== undefined && keys !== undefined && keys.every((key) => key !== '*' && held.includes(key))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The keys that let their holder change who holds which role, or what a role holds. */
|
|
22
|
+
const managing = ['*', 'roles.create', 'roles.update', 'roles.delete', 'memberRoles.create', 'memberRoles.update', 'memberRoles.delete']
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Whether a role holding these keys is what lets its holder manage roles. The
|
|
26
|
+
* Members page's sheet disables a member's own such role, and the assignment
|
|
27
|
+
* kayan refuses its removal, so the box and the refusal are one rule.
|
|
28
|
+
*/
|
|
29
|
+
export const managesRoles = (keys: '*' | readonly string[]): boolean => keys === '*' || keys.some((key) => managing.includes(key))
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Better Auth's creator role, the organization's owner: `creatorRole`, which
|
|
33
|
+
* this project leaves at the provider's default. The provider keeps it already:
|
|
34
|
+
* nobody but an owner removes or demotes one, and the last one never leaves.
|
|
35
|
+
*/
|
|
36
|
+
export const OWNER = 'owner'
|
|
37
|
+
|
|
38
|
+
/** The role of this project's that an owner always holds in its tenant. */
|
|
39
|
+
export const OWNER_ROLE = 'administrator' satisfies keyof typeof tenantRoles
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Whether a Better Auth `member.role` names the owner. Several roles are one
|
|
43
|
+
* column, comma-joined, and the name is matched exactly, untrimmed, the way the
|
|
44
|
+
* provider's own owner checks match it: a spaced or cased name the provider does
|
|
45
|
+
* not treat as an owner is no owner here either.
|
|
46
|
+
*/
|
|
47
|
+
export const isOwner = (memberRole: unknown): boolean => typeof memberRole === 'string' && memberRole.split(',').includes(OWNER)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { memberRoleTarget } from 'mithaq'
|
|
2
|
+
import { systemRoles } from './system-roles.ts'
|
|
3
|
+
import { assignedRoles, tenantAccess } from './tenant-access.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The access target of a project without the roles add-on, under which a member holds what
|
|
7
|
+
* Better Auth's own `member.role` names in the organization, owner, admin or
|
|
8
|
+
* member, and no custom role or assignment table exists. The roles add-on
|
|
9
|
+
* swaps it for `rbacTarget` over its `roles` and `memberRoles` kayan.
|
|
10
|
+
*/
|
|
11
|
+
export const accessTarget = (roles: typeof systemRoles = systemRoles) => memberRoleTarget({ roles, rolesOf: assignedRoles, access: tenantAccess })
|