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,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The keys this project adds beside a kayan's own operations, by kayan. Each
|
|
3
|
+
* joins the catalog as `<kayan>.<key>`, held by a role like any operation's key,
|
|
4
|
+
* and the dashboard's role form offers it among that kayan's operations,
|
|
5
|
+
* labelled by the kayan's copy under `operations.<key>`. It is plain data,
|
|
6
|
+
* imported by the server and by the role form alike.
|
|
7
|
+
*/
|
|
8
|
+
export const projectStatements = {
|
|
9
|
+
// add-on: courses
|
|
10
|
+
// Every seat of the organization rather than the actor's own.
|
|
11
|
+
enrollments: ['others'],
|
|
12
|
+
// end add-on: courses
|
|
13
|
+
} as const satisfies Record<string, readonly string[]>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// The subpath, so a page piece reading the fixed roles' labels loads no server code.
|
|
2
|
+
import { backofficeRoles } from '../mithaq/auth/backoffice-roles.ts'
|
|
3
|
+
import type { SystemRoles } from 'mithaq'
|
|
4
|
+
import { tenantRoles } from './tenant-roles.ts'
|
|
5
|
+
|
|
6
|
+
export { tenantRoles }
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Every role this project ships, the backoffice's and the tenants', which is what
|
|
10
|
+
* the access target takes whole: it holds a slug's keys wherever the project's
|
|
11
|
+
* reader answers it. The backoffice roles are the auth setup's, because the column
|
|
12
|
+
* they live on is Better Auth's own `user.role` and the admin plugin's roles are
|
|
13
|
+
* built from the same lists. The reader answers them in the backoffice app alone,
|
|
14
|
+
* and an assignment never names one.
|
|
15
|
+
*/
|
|
16
|
+
export const systemRoles = { ...backofficeRoles, ...tenantRoles } as const satisfies SystemRoles
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { onBackoffice, backofficeApp, backofficeRoles, backofficeURL } from '../mithaq/auth/index.ts'
|
|
2
|
+
import { secret } from '../mithaq/auth/backoffice-roles.ts'
|
|
3
|
+
import { actorPermissions, actorRoles, authActor, userRefs, type Held, type RowTable, type Store } from 'mithaq'
|
|
4
|
+
// add-on: roles
|
|
5
|
+
import { isOwner, OWNER_ROLE } from './grants.ts'
|
|
6
|
+
// end add-on: roles
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* What a row may name on the actor: `user.<key>` in any kayan's row is checked
|
|
10
|
+
* against these keys. The active organization is a string because a row that
|
|
11
|
+
* resolves it to anything else throws rather than filtering.
|
|
12
|
+
*/
|
|
13
|
+
export type OrganizationActor = {
|
|
14
|
+
activeOrganizationId: string
|
|
15
|
+
/** The person's own id, which a field defaults to when the row is the actor's own. */
|
|
16
|
+
id: string
|
|
17
|
+
/**
|
|
18
|
+
* Better Auth's own `member.role`: governance only (settings, members,
|
|
19
|
+
* invitations, the provider's `owner` override). Optional because a local-auth
|
|
20
|
+
* actor is no organization's member.
|
|
21
|
+
*/
|
|
22
|
+
memberRole?: string
|
|
23
|
+
/** The real person behind an impersonated session, so the audit handler names who really made the write. */
|
|
24
|
+
impersonatedBy?: string
|
|
25
|
+
organizationIds?: readonly string[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The backoffice administrator's slug. Backoffice roles live on Better Auth's
|
|
30
|
+
* `user.role`, never in a tenant's assignment: `assignedRoles` drops any such
|
|
31
|
+
* slug there, because the access target holds a slug's keys only where this
|
|
32
|
+
* reader answers it.
|
|
33
|
+
*/
|
|
34
|
+
export const BACKOFFICE = 'super_admin'
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The backoffice screen, the only place the backoffice roles answer. The app decides
|
|
38
|
+
* this, never the request.
|
|
39
|
+
*/
|
|
40
|
+
export const BACKOFFICE_SCREEN = 'backoffice'
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The kayan whose session carries the backoffice column, the one `auth.admin`
|
|
44
|
+
* names. A build may run several Better Auth instances, and only this one's
|
|
45
|
+
* `user.role` is the layer above every tenant.
|
|
46
|
+
*/
|
|
47
|
+
const BACKOFFICE_KAYAN = 'user'
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Backoffice roles read every tenant's rows and write none; staff change a
|
|
51
|
+
* tenant's data only by impersonating a member, which is audited. Every write is
|
|
52
|
+
* an explicit `false`, because a slug a table leaves out still takes the `'*'`
|
|
53
|
+
* row, which opens every write. The row opens the door and the keys decide who
|
|
54
|
+
* walks through it.
|
|
55
|
+
*/
|
|
56
|
+
const readOnly = { create: false, read: true, update: false, delete: false } as const
|
|
57
|
+
|
|
58
|
+
/** The read-only row under every backoffice role, which every row table of this project spreads in. */
|
|
59
|
+
export const backofficeReads = Object.fromEntries(Object.keys(backofficeRoles).map((slug) => [slug, readOnly])) as {
|
|
60
|
+
readonly [Slug in keyof typeof backofficeRoles]: typeof readOnly
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Every operation refused under every backoffice role, spread in place of
|
|
65
|
+
* `backofficeReads` by tables the backoffice does not read, such as the settings
|
|
66
|
+
* global and the audit log.
|
|
67
|
+
*/
|
|
68
|
+
const closed = { create: false, read: false, update: false, delete: false } as const
|
|
69
|
+
export const backofficeClosed = Object.fromEntries(Object.keys(backofficeRoles).map((slug) => [slug, closed])) as {
|
|
70
|
+
readonly [Slug in keyof typeof backofficeRoles]: typeof closed
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The accessor every row of this project is written with. The actor is typed
|
|
75
|
+
* here once because a kayan's table is written before the language is imported
|
|
76
|
+
* into its file.
|
|
77
|
+
*/
|
|
78
|
+
export const user = userRefs<OrganizationActor>()
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The organization the actor's session has active, read off the trusted
|
|
82
|
+
* snapshot and checked against the provider's own list, or `null`.
|
|
83
|
+
*/
|
|
84
|
+
export const currentOrganization = (user: unknown) => {
|
|
85
|
+
const trusted = authActor(user)
|
|
86
|
+
if (!trusted || trusted.strategy !== 'better-auth') return null
|
|
87
|
+
const active = trusted.activeOrganizationId
|
|
88
|
+
const organizations = trusted.organizations
|
|
89
|
+
if (typeof active !== 'string' || !Array.isArray(organizations)) return null
|
|
90
|
+
return organizations.some((organization) => organization.id === active) ? active : null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The backoffice rows of the tenant-owned kayan: `backofficeReads` minus the
|
|
95
|
+
* columns `secret` names. A row's field list closes a column whatever keys the
|
|
96
|
+
* actor holds, and a column an actor may not read is no filter either.
|
|
97
|
+
*/
|
|
98
|
+
const readsNoSecret = { ...readOnly, fields: ['*', ...secret.map((key) => `!${key.slice(key.indexOf('.fields.') + '.fields.'.length)}`)] as readonly string[] }
|
|
99
|
+
const secretClosed = Object.fromEntries(Object.keys(backofficeRoles).map((slug) => [slug, readsNoSecret])) as {
|
|
100
|
+
readonly [Slug in keyof typeof backofficeRoles]: typeof readsNoSecret
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The rows every tenant-owned kayan inherits. The organization is core's: a
|
|
105
|
+
* read, a write and a reference reach the session's active organization alone,
|
|
106
|
+
* whatever a row here answers. `'*'` is the row every actor assigned a role
|
|
107
|
+
* takes beside its roles' own, so a custom role's keys decide what it reaches;
|
|
108
|
+
* an actor assigned no role takes no row and is denied.
|
|
109
|
+
*
|
|
110
|
+
* No row here names `permissions`: what a role holds is the access target's
|
|
111
|
+
* business, and a second answer here would let a role row widen.
|
|
112
|
+
*/
|
|
113
|
+
export const tenantAccess = {
|
|
114
|
+
'*': { create: true, read: true, update: true, delete: true },
|
|
115
|
+
// add-on: courses
|
|
116
|
+
/**
|
|
117
|
+
* The one row keyed on a system slug, and it narrows: an instructor teaches and
|
|
118
|
+
* never removes a row. Every operation is named in full, because a missing one
|
|
119
|
+
* reads as `false` and a `false` in any applicable row wins.
|
|
120
|
+
*/
|
|
121
|
+
instructor: { create: true, read: true, update: true, delete: false },
|
|
122
|
+
// end add-on: courses
|
|
123
|
+
...secretClosed,
|
|
124
|
+
} as const satisfies RowTable
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The roles a kayan's rows are read through; its parameter also types `user` for
|
|
128
|
+
* every row this project writes. Slugs come from the access target for the
|
|
129
|
+
* active organization, off the trusted snapshot, so an object naming its own
|
|
130
|
+
* roles is no one.
|
|
131
|
+
*/
|
|
132
|
+
export const assignedRoles = (actor: OrganizationActor): readonly string[] => {
|
|
133
|
+
if (!authActor(actor)) return []
|
|
134
|
+
// The backoffice app answers the backoffice roles alone and the tenant app the
|
|
135
|
+
// membership's roles alone, never unioned, so neither adds keys on the other's
|
|
136
|
+
// screens. Which app serves the request is the app's own fact, never the request's.
|
|
137
|
+
if (onBackoffice()) return backofficeRolesOf(actor)
|
|
138
|
+
if (!currentOrganization(actor)) return []
|
|
139
|
+
const assigned = (actorRoles(actor) ?? []).filter((slug) => !Object.hasOwn(backofficeRoles, slug))
|
|
140
|
+
// add-on: roles
|
|
141
|
+
// The slugs are the `memberRoles` kayan's, and `member.role` grants one thing
|
|
142
|
+
// only: an owner always holds `administrator` in its tenant, so the role that
|
|
143
|
+
// manages roles is never lost.
|
|
144
|
+
if (isOwner(authActor(actor)?.memberRole) && !assigned.includes(OWNER_ROLE)) return [...assigned, OWNER_ROLE]
|
|
145
|
+
// end add-on: roles
|
|
146
|
+
return assigned
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Whether `assignedRoles` answers this actor `super_admin`, which only the
|
|
151
|
+
* backoffice app does. It is the administrator of `auth.admin`, the one answer
|
|
152
|
+
* local auth's `_verified` writes ask, and the reader and writer of local accounts.
|
|
153
|
+
*/
|
|
154
|
+
export const superAdmin = ({ user }: { user: unknown }) => assignedRoles(user as OrganizationActor).includes(BACKOFFICE)
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Every operation refused to every actor, the administrator included, for the
|
|
158
|
+
* tables Better Auth alone reads and writes through its adapter. Without it those
|
|
159
|
+
* kayan would fall to the administrator's default access, and a super admin would
|
|
160
|
+
* read every session token and password hash.
|
|
161
|
+
*/
|
|
162
|
+
export const providerOnly = { create: () => false, read: () => false, update: () => false, delete: () => false } as const
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The backoffice roles this actor holds on Better Auth's `user.role`, whichever app
|
|
166
|
+
* asks. Several are comma-separated, and only a backoffice role's name counts.
|
|
167
|
+
*
|
|
168
|
+
* The column is read off the trusted snapshot, and only from the admin kayan's
|
|
169
|
+
* session on a Better Auth strategy: a build may run several instances that all
|
|
170
|
+
* write `user.role`, and local auth spreads its kayan's row onto the actor, so a
|
|
171
|
+
* `role` column there would otherwise hand someone a backoffice role.
|
|
172
|
+
*/
|
|
173
|
+
export const backofficeRolesOf = (actor: unknown): string[] => {
|
|
174
|
+
const trusted = authActor(actor)
|
|
175
|
+
if (!trusted || trusted.strategy !== 'better-auth' || trusted.collection !== BACKOFFICE_KAYAN || typeof trusted.role !== 'string') return []
|
|
176
|
+
return [...new Set(trusted.role.split(',').map((slug) => slug.trim()).filter((slug) => Object.hasOwn(backofficeRoles, slug)))]
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Where the tenant app's header links the backoffice app, for an actor whose
|
|
181
|
+
* backoffice roles hold `screen.backoffice`; nothing otherwise. It is a link and
|
|
182
|
+
* grants nothing: the backoffice app's own server decides entry. A project that
|
|
183
|
+
* serves no backoffice app links nowhere.
|
|
184
|
+
*/
|
|
185
|
+
export const backofficeEntry = (actor: unknown): string | undefined => {
|
|
186
|
+
if (onBackoffice() || !backofficeApp) return undefined
|
|
187
|
+
const enters = backofficeRolesOf(actor).some((slug) => {
|
|
188
|
+
const held = backofficeRoles[slug as keyof typeof backofficeRoles].permissions
|
|
189
|
+
return held === '*' || (held as readonly string[]).includes(`screen.${BACKOFFICE_SCREEN}`)
|
|
190
|
+
})
|
|
191
|
+
return enters ? new URL(`/${BACKOFFICE_SCREEN}`, backofficeURL).href : undefined
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The keys a provider kayan's columns are graded by, which `rows()` takes as its
|
|
196
|
+
* `holds`: in the tenant app the id and the name alone, and only where the actor
|
|
197
|
+
* holds their keys; in the backoffice app the actor's own keys. An actor holding no
|
|
198
|
+
* key reads no column, since an empty set grades every column closed where
|
|
199
|
+
* `undefined` would grade none.
|
|
200
|
+
*/
|
|
201
|
+
export const namesInTenant = (kayan: string) => (actor: unknown): Held => {
|
|
202
|
+
const held = actorPermissions(actor)
|
|
203
|
+
if (onBackoffice()) return held
|
|
204
|
+
return new Set(['id', 'name'].map((field) => `${kayan}.fields.${field}`).filter((key) => held === true || held?.has(key) === true))
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Whom a role reads among its organization's people when it holds no
|
|
209
|
+
* `member.find`: the members assigned one of the roles named here, beside
|
|
210
|
+
* itself. A role left out, a custom role included, reads itself alone, so a
|
|
211
|
+
* key to read people never by itself opens the staff to a portal role.
|
|
212
|
+
*/
|
|
213
|
+
export const peopleAudience: Readonly<Record<string, readonly string[]>> = {
|
|
214
|
+
// add-on: courses
|
|
215
|
+
// The students it enrols, never the staff.
|
|
216
|
+
instructor: ['student'],
|
|
217
|
+
// end add-on: courses
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The read the person's own rows are ANDed with, so it narrows and never widens:
|
|
222
|
+
* the members of the organization the actor's session has active, read off the
|
|
223
|
+
* store, all of them to a holder of `member.find`, who lists them through the
|
|
224
|
+
* provider anyway, and otherwise the actor and its roles' `peopleAudience`. The
|
|
225
|
+
* organization is the trusted snapshot's, so a claim is no one's.
|
|
226
|
+
*/
|
|
227
|
+
export const membersOf = (store: Pick<Store, 'find'>) => async ({ user: actor }: { user: unknown }) => {
|
|
228
|
+
// A backoffice role reads every person in the backoffice app; the rows answer who holds one.
|
|
229
|
+
if (onBackoffice()) return true
|
|
230
|
+
const active = currentOrganization(actor)
|
|
231
|
+
if (!active) return false
|
|
232
|
+
const all = { limit: 0, page: 1, sort: { field: 'id', direction: 'asc' } } as const
|
|
233
|
+
const members = await store.find({ kayan: 'member', args: { ...all, where: { organizationId: { equals: active } } } })
|
|
234
|
+
const ids = members.docs.map((row) => row.userId).filter((id): id is string => typeof id === 'string')
|
|
235
|
+
const held = actorPermissions(actor)
|
|
236
|
+
if (held === true || held?.has('member.find')) return ids.length ? { id: { in: ids } } : false
|
|
237
|
+
const seen = new Set([authActor(actor)?.id])
|
|
238
|
+
// add-on: roles
|
|
239
|
+
// A custom role's name is a tenant's to write, so one named `constructor` reads no inherited key.
|
|
240
|
+
const audience =[...new Set(assignedRoles(actor as OrganizationActor).flatMap((slug) => (Object.hasOwn(peopleAudience, slug) ? peopleAudience[slug]! : [])))]
|
|
241
|
+
if (audience.length) {
|
|
242
|
+
const assigned = await store.find({ kayan: 'memberRoles', args: { ...all, where: { organization: { equals: active }, role: { in: audience } } } })
|
|
243
|
+
for (const row of assigned.docs) seen.add(row.user)
|
|
244
|
+
}
|
|
245
|
+
// end add-on: roles
|
|
246
|
+
// An assignment can outlive its membership, so only a current member is seen.
|
|
247
|
+
const reached = ids.filter((id) => seen.has(id))
|
|
248
|
+
return reached.length ? { id: { in: reached } } : false
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Whether an actor reads past what a kayan's own read narrows for roles not
|
|
253
|
+
* holding `key`: in the backoffice app, whose staff read every tenant's rows whole,
|
|
254
|
+
* or in a tenant, holding that key. The courses add-on narrows archived courses,
|
|
255
|
+
* sessions and covers of unreadable courses and other people's seats this way.
|
|
256
|
+
*/
|
|
257
|
+
export const readsPast = (actor: unknown, key: string) => {
|
|
258
|
+
const held = actorPermissions(actor)
|
|
259
|
+
return onBackoffice() || held === true || held?.has(key) === true
|
|
260
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import type { SystemRoles } from 'mithaq'
|
|
2
|
+
// without add-on: roles
|
|
3
|
+
// import { defaultMemberRoles } from './default-member-roles.ts'
|
|
4
|
+
// end without add-on: roles
|
|
5
|
+
|
|
6
|
+
// add-on: courses
|
|
7
|
+
/** A session's columns but its `enrollments`, which the registrar and the instructor read alike. */
|
|
8
|
+
const sessionColumns = [
|
|
9
|
+
'sessions.fields.id',
|
|
10
|
+
'sessions.fields.createdAt',
|
|
11
|
+
'sessions.fields.updatedAt',
|
|
12
|
+
'sessions.fields.organization',
|
|
13
|
+
'sessions.fields.course',
|
|
14
|
+
'sessions.fields.startsAt',
|
|
15
|
+
'sessions.fields.capacity',
|
|
16
|
+
'sessions.fields.venue',
|
|
17
|
+
'sessions.fields.format',
|
|
18
|
+
'sessions.fields.archived',
|
|
19
|
+
'sessions.fields.archiveReason',
|
|
20
|
+
'sessions.fields.details',
|
|
21
|
+
] as const
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* What the portal's catalogue and course page read beside a course's own columns, for its roles alike: the
|
|
25
|
+
* sessions with the seats each has left, and the names of the tags it carries.
|
|
26
|
+
*/
|
|
27
|
+
const catalogueReads = [
|
|
28
|
+
'courses.findWithSessions',
|
|
29
|
+
'courses.fields.tags',
|
|
30
|
+
'courses.fields.selfEnroll',
|
|
31
|
+
'tags.find',
|
|
32
|
+
'tags.findByID',
|
|
33
|
+
'tags.fields.id',
|
|
34
|
+
'tags.fields.name',
|
|
35
|
+
] as const
|
|
36
|
+
|
|
37
|
+
// add-on: settings
|
|
38
|
+
/** Whether the portal is closed for maintenance, and where to write, which the portal's foot reads for its roles alike. */
|
|
39
|
+
const portalSettings = ['settings.findOne', 'settings.fields.maintenance', 'settings.fields.contactEmail'] as const
|
|
40
|
+
// end add-on: settings
|
|
41
|
+
|
|
42
|
+
/** A course's cover, read and never changed, which the portal's roles read alike. */
|
|
43
|
+
const coverReads = [
|
|
44
|
+
'media.find',
|
|
45
|
+
'media.findByID',
|
|
46
|
+
'media.fields.alt',
|
|
47
|
+
'media.fields.createdAt',
|
|
48
|
+
'media.fields.filename',
|
|
49
|
+
'media.fields.filesize',
|
|
50
|
+
'media.fields.height',
|
|
51
|
+
'media.fields.id',
|
|
52
|
+
'media.fields.key',
|
|
53
|
+
'media.fields.mimeType',
|
|
54
|
+
'media.fields.storage',
|
|
55
|
+
'media.fields.organization',
|
|
56
|
+
'media.fields.updatedAt',
|
|
57
|
+
'media.fields.width',
|
|
58
|
+
] as const
|
|
59
|
+
// end add-on: courses
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The roles this project ships to every tenant, written in code and never a
|
|
63
|
+
* row: the access target takes them whole and reads what each holds from here.
|
|
64
|
+
* A page lists them from here too, which is why this file imports no server
|
|
65
|
+
* code.
|
|
66
|
+
*
|
|
67
|
+
* A slug here is the whole vocabulary of role names: a row table may be keyed on
|
|
68
|
+
* one of them or on `'*'` and on nothing else, and `buildAsas` refuses a key
|
|
69
|
+
* that is not in this project's catalog exactly as it does for a row's own.
|
|
70
|
+
*
|
|
71
|
+
* The backoffice roles, `super_admin` among them, are not here: they are staff
|
|
72
|
+
* above every tenant, held on Better Auth's `user.role`, and the access target
|
|
73
|
+
* holds their keys in the backoffice app alone. Inside a tenant every actor, a
|
|
74
|
+
* `super_admin` included, holds what its membership's roles hold and nothing
|
|
75
|
+
* more.
|
|
76
|
+
*/
|
|
77
|
+
export const tenantRoles = {
|
|
78
|
+
// add-on: roles
|
|
79
|
+
// These are business roles and never governance, and the `roles` kayan holds
|
|
80
|
+
// a tenant's own roles beside them; an assignment names one of either. Better
|
|
81
|
+
// Auth's own `member.role`, which holds `owner`, `admin` or `member`, stays
|
|
82
|
+
// what the provider uses for settings, members and invitations, and nothing
|
|
83
|
+
// here reads it. Sharing one comma-joined column would inherit the provider's
|
|
84
|
+
// own open comma-in-a-role-name bug.
|
|
85
|
+
//
|
|
86
|
+
// A new organization's creator is assigned `administrator` while the
|
|
87
|
+
// organization is being created, by the hook the template wires into the
|
|
88
|
+
// provider, so a fresh tenant has a root granter from its first moment. That
|
|
89
|
+
// row is written straight through the store, because a managed create is
|
|
90
|
+
// graded against the actor making it and the creator is in no organization
|
|
91
|
+
// until that route puts it in one.
|
|
92
|
+
/**
|
|
93
|
+
* The tenant's root granter: every key of the project, which is what lets it
|
|
94
|
+
* author a custom role at all. `'*'` holds every screen's key too, so it
|
|
95
|
+
* enters the dashboard and the portal alike. It never reaches the backoffice
|
|
96
|
+
* app, where the reader answers no membership role at all.
|
|
97
|
+
* `grantedPermissions` returns early on a granter holding everything, so this
|
|
98
|
+
* role and no other may hand out a key it will never use itself.
|
|
99
|
+
*/
|
|
100
|
+
administrator: {
|
|
101
|
+
label: { ar: 'مدير', en: 'Administrator' },
|
|
102
|
+
permissions: '*',
|
|
103
|
+
},
|
|
104
|
+
/** Runs the catalogue in the dashboard: the courses, their sessions and the tags on them. */
|
|
105
|
+
registrar: {
|
|
106
|
+
label: { ar: 'مسجل', en: 'Registrar' },
|
|
107
|
+
permissions: [
|
|
108
|
+
'screen.dashboard',
|
|
109
|
+
// add-on: courses
|
|
110
|
+
'courses.create',
|
|
111
|
+
'courses.find',
|
|
112
|
+
'courses.count',
|
|
113
|
+
'courses.findByID',
|
|
114
|
+
'courses.update',
|
|
115
|
+
// A course leaves the catalogue and comes back through its own operations, each with its reason.
|
|
116
|
+
'courses.archive',
|
|
117
|
+
'courses.unarchive',
|
|
118
|
+
'courses.fields.archived',
|
|
119
|
+
'courses.fields.archiveReason',
|
|
120
|
+
'courses.fields.createdAt',
|
|
121
|
+
'courses.fields.cover',
|
|
122
|
+
'courses.fields.currency',
|
|
123
|
+
'courses.fields.details',
|
|
124
|
+
'courses.fields.fee',
|
|
125
|
+
'courses.fields.id',
|
|
126
|
+
'courses.fields.tags',
|
|
127
|
+
'courses.fields.organization',
|
|
128
|
+
'courses.fields.title',
|
|
129
|
+
'courses.fields.updatedAt',
|
|
130
|
+
'courses.fields.selfEnroll',
|
|
131
|
+
// Whether a course is a draft, its versions and the restore of one: the
|
|
132
|
+
// registrar edits the catalogue, so it reads a course's history and puts
|
|
133
|
+
// an older copy back.
|
|
134
|
+
'courses.fields._status',
|
|
135
|
+
'courses.findVersions',
|
|
136
|
+
'courses.findVersionByID',
|
|
137
|
+
'courses.restoreVersion',
|
|
138
|
+
'sessions.create',
|
|
139
|
+
'sessions.find',
|
|
140
|
+
'sessions.count',
|
|
141
|
+
'sessions.findByID',
|
|
142
|
+
'sessions.update',
|
|
143
|
+
// The columns of each kayan it holds an operation of, without which every
|
|
144
|
+
// read of it is refused for its fields and its pages draw nothing. Not a
|
|
145
|
+
// session's `enrollments`, which reads a kayan this role does not.
|
|
146
|
+
...sessionColumns,
|
|
147
|
+
// end add-on: courses
|
|
148
|
+
'tags.find',
|
|
149
|
+
'tags.count',
|
|
150
|
+
'tags.findByID',
|
|
151
|
+
'tags.fields.id',
|
|
152
|
+
'tags.fields.createdAt',
|
|
153
|
+
'tags.fields.updatedAt',
|
|
154
|
+
'tags.fields.organization',
|
|
155
|
+
'tags.fields.name',
|
|
156
|
+
// add-on: media
|
|
157
|
+
// The images a course's cover is picked from, which it uploads,
|
|
158
|
+
// re-describes and deletes once no course points at one.
|
|
159
|
+
'media.create',
|
|
160
|
+
'media.find',
|
|
161
|
+
'media.count',
|
|
162
|
+
'media.findByID',
|
|
163
|
+
'media.update',
|
|
164
|
+
'media.delete',
|
|
165
|
+
'media.fields.alt',
|
|
166
|
+
'media.fields.createdAt',
|
|
167
|
+
'media.fields.filename',
|
|
168
|
+
'media.fields.filesize',
|
|
169
|
+
'media.fields.height',
|
|
170
|
+
'media.fields.id',
|
|
171
|
+
'media.fields.key',
|
|
172
|
+
'media.fields.mimeType',
|
|
173
|
+
'media.fields.storage',
|
|
174
|
+
'media.fields.organization',
|
|
175
|
+
'media.fields.updatedAt',
|
|
176
|
+
'media.fields.width',
|
|
177
|
+
// end add-on: media
|
|
178
|
+
// add-on: audit-log
|
|
179
|
+
// The activity panel on each edit page it opens: the entries of the
|
|
180
|
+
// kayan it lists, each change graded by the column's own key above.
|
|
181
|
+
'auditLog.find',
|
|
182
|
+
'auditLog.fields.id',
|
|
183
|
+
'auditLog.fields.createdAt',
|
|
184
|
+
'auditLog.fields.kayan',
|
|
185
|
+
'auditLog.fields.record',
|
|
186
|
+
'auditLog.fields.operation',
|
|
187
|
+
'auditLog.fields.actor',
|
|
188
|
+
'auditLog.fields.impersonatedBy',
|
|
189
|
+
'auditLog.fields.changes',
|
|
190
|
+
// end add-on: audit-log
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
// add-on: courses
|
|
194
|
+
/**
|
|
195
|
+
* Teaches: reads the catalogue and enrols. It holds no `courses.archive`. It
|
|
196
|
+
* works in the portal, which draws the courses and the enrolments, and never
|
|
197
|
+
* enters the dashboard.
|
|
198
|
+
*/
|
|
199
|
+
instructor: {
|
|
200
|
+
label: { ar: 'مدرس', en: 'Instructor' },
|
|
201
|
+
permissions: [
|
|
202
|
+
'screen.portal',
|
|
203
|
+
'courses.find',
|
|
204
|
+
'courses.count',
|
|
205
|
+
'courses.findByID',
|
|
206
|
+
'courses.fields.archived',
|
|
207
|
+
'courses.fields.createdAt',
|
|
208
|
+
'courses.fields.cover',
|
|
209
|
+
'courses.fields.details',
|
|
210
|
+
'courses.fields.id',
|
|
211
|
+
'courses.fields.organization',
|
|
212
|
+
'courses.fields.title',
|
|
213
|
+
'courses.fields.updatedAt',
|
|
214
|
+
// What the portal's catalogue draws on a course's card and its page.
|
|
215
|
+
'courses.fields.summary',
|
|
216
|
+
'courses.fields.fee',
|
|
217
|
+
'courses.fields.currency',
|
|
218
|
+
'courses.fields.delivery',
|
|
219
|
+
// It reads published courses alone, and whether one is published with them.
|
|
220
|
+
'courses.fields._status',
|
|
221
|
+
'enrollments.create',
|
|
222
|
+
'enrollments.find',
|
|
223
|
+
'enrollments.count',
|
|
224
|
+
'enrollments.findByID',
|
|
225
|
+
// Every seat of its organization, not only its own
|
|
226
|
+
'enrollments.others',
|
|
227
|
+
// An enrollment's columns but the soft-delete mark, which the runner
|
|
228
|
+
// writes, and a session's, which the enrollment's picker and cell read
|
|
229
|
+
'enrollments.fields.id',
|
|
230
|
+
'enrollments.fields.createdAt',
|
|
231
|
+
'enrollments.fields.updatedAt',
|
|
232
|
+
'enrollments.fields.organization',
|
|
233
|
+
'enrollments.fields.session',
|
|
234
|
+
'enrollments.fields.student',
|
|
235
|
+
'sessions.find',
|
|
236
|
+
'sessions.count',
|
|
237
|
+
'sessions.findByID',
|
|
238
|
+
...sessionColumns,
|
|
239
|
+
// The students of its organization by their id and name alone, which the
|
|
240
|
+
// enrollment drawer's student picker lists and an enrollment's student is
|
|
241
|
+
// titled by: `peopleAudience` in `tenant-access.ts` names whom it reads.
|
|
242
|
+
'user.find',
|
|
243
|
+
'user.count',
|
|
244
|
+
'user.findByID',
|
|
245
|
+
'user.fields.id',
|
|
246
|
+
'user.fields.name',
|
|
247
|
+
...coverReads,
|
|
248
|
+
...catalogueReads,
|
|
249
|
+
// add-on: settings
|
|
250
|
+
...portalSettings,
|
|
251
|
+
// end add-on: settings
|
|
252
|
+
],
|
|
253
|
+
},
|
|
254
|
+
/**
|
|
255
|
+
* Takes the courses: reads the catalogue in the portal, a course's cover
|
|
256
|
+
* with it, and its own enrollments with the sessions they name. Not the
|
|
257
|
+
* access code, which a course hands out to whoever it chooses, nor the
|
|
258
|
+
* archive reason or the notifications, which are staff's.
|
|
259
|
+
*
|
|
260
|
+
* A role's keys reach every row of the organization, so the enrollment keys
|
|
261
|
+
* alone would hand a student every other student's seat. The enrollments'
|
|
262
|
+
* own access narrows them to the student's own seats, since it holds no
|
|
263
|
+
* `enrollments.others`, and the sessions' to those of the courses it reads.
|
|
264
|
+
* It takes a seat for itself alone, in a course that lets its students enroll
|
|
265
|
+
* themselves and before the session starts, and gives one back before then:
|
|
266
|
+
* the enrollments' own steps hold it to that. It changes no seat.
|
|
267
|
+
*/
|
|
268
|
+
student: {
|
|
269
|
+
label: { ar: 'طالب', en: 'Student' },
|
|
270
|
+
permissions: [
|
|
271
|
+
'screen.portal',
|
|
272
|
+
'courses.find',
|
|
273
|
+
'courses.count',
|
|
274
|
+
'courses.findByID',
|
|
275
|
+
'courses.fields.id',
|
|
276
|
+
'courses.fields.createdAt',
|
|
277
|
+
'courses.fields.updatedAt',
|
|
278
|
+
'courses.fields.organization',
|
|
279
|
+
'courses.fields.title',
|
|
280
|
+
'courses.fields.summary',
|
|
281
|
+
'courses.fields.details',
|
|
282
|
+
'courses.fields.cover',
|
|
283
|
+
'courses.fields.fee',
|
|
284
|
+
'courses.fields.currency',
|
|
285
|
+
'courses.fields.level',
|
|
286
|
+
'courses.fields.delivery',
|
|
287
|
+
'courses.fields.audience',
|
|
288
|
+
'courses.fields.opensOn',
|
|
289
|
+
'courses.fields.archived',
|
|
290
|
+
'courses.fields._status',
|
|
291
|
+
'enrollments.find',
|
|
292
|
+
'enrollments.count',
|
|
293
|
+
'enrollments.findByID',
|
|
294
|
+
'enrollments.create',
|
|
295
|
+
'enrollments.delete',
|
|
296
|
+
'enrollments.fields.id',
|
|
297
|
+
'enrollments.fields.createdAt',
|
|
298
|
+
'enrollments.fields.updatedAt',
|
|
299
|
+
'enrollments.fields.organization',
|
|
300
|
+
'enrollments.fields.session',
|
|
301
|
+
// Itself, which its own create fills and every seat it reads names.
|
|
302
|
+
'enrollments.fields.student',
|
|
303
|
+
// The sessions its seats name, which the enrollment's session cell draws
|
|
304
|
+
// by course and start.
|
|
305
|
+
'sessions.find',
|
|
306
|
+
'sessions.count',
|
|
307
|
+
'sessions.findByID',
|
|
308
|
+
...sessionColumns,
|
|
309
|
+
...coverReads,
|
|
310
|
+
...catalogueReads,
|
|
311
|
+
// add-on: settings
|
|
312
|
+
...portalSettings,
|
|
313
|
+
// end add-on: settings
|
|
314
|
+
],
|
|
315
|
+
},
|
|
316
|
+
// end add-on: courses
|
|
317
|
+
// end add-on: roles
|
|
318
|
+
// without add-on: roles
|
|
319
|
+
// ...defaultMemberRoles,
|
|
320
|
+
// end without add-on: roles
|
|
321
|
+
} as const satisfies SystemRoles
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Refuses an update when the row changed after the caller read it. A kayan opts in by
|
|
2
|
+
// widening its update input and listing the step before the store step:
|
|
3
|
+
//
|
|
4
|
+
// update: {
|
|
5
|
+
// input: ({ update }) => update.extend({ expectedUpdatedAt: z.date() }),
|
|
6
|
+
// steps: ({ steps }) => [expectedUpdatedAt, steps.update],
|
|
7
|
+
// },
|
|
8
|
+
//
|
|
9
|
+
// Use `z.date()`: a string or `null` would not be a moment and would answer a misleading 409.
|
|
10
|
+
// Listing the step without widening the input protects nothing, since the strict update
|
|
11
|
+
// shape refuses the key by name.
|
|
12
|
+
//
|
|
13
|
+
// The compare is against `ctx.dataBefore`, loaded under the update's own row lock inside its
|
|
14
|
+
// transaction, so on a store that honours the lock a second writer waits and no write lands
|
|
15
|
+
// between the compare and the store step. A store that ignores the lock lets both through.
|
|
16
|
+
//
|
|
17
|
+
// updateMany runs this once per row with no instant per row: a kayan whose input requires the
|
|
18
|
+
// key refuses every updateMany, and one that leaves it optional runs the bulk unguarded.
|
|
19
|
+
//
|
|
20
|
+
// A kayan with no `updatedAt` field has nothing to compare, so listing the step on it throws.
|
|
21
|
+
//
|
|
22
|
+
// A project that copies this step adds the `error.conflict.stale` copy key in every locale.
|
|
23
|
+
import { conflict, type Ctx } from 'mithaq'
|
|
24
|
+
|
|
25
|
+
export const expectedUpdatedAt = (ctx: Ctx<{ expectedUpdatedAt?: Date | string }>): Ctx => {
|
|
26
|
+
if (!('updatedAt' in ctx.kayan.fields)) {
|
|
27
|
+
throw new Error(`Kayan "${ctx.kayan.name}" lists expectedUpdatedAt but has no updatedAt field, so the step cannot guard it. Add defaultTimestampFields or remove the step.`)
|
|
28
|
+
}
|
|
29
|
+
const { expectedUpdatedAt: expected } = ctx.data
|
|
30
|
+
const held = ctx.dataBefore?.updatedAt
|
|
31
|
+
if (expected === undefined || held === undefined) return ctx
|
|
32
|
+
if (new Date(expected).getTime() !== new Date(held as Date).getTime()) {
|
|
33
|
+
throw conflict({
|
|
34
|
+
message: `Kayan "${ctx.kayan.name}" row changed after it was read, so it is not updated. Read it again and send the change on the row as it is.`,
|
|
35
|
+
key: 'error.conflict.stale',
|
|
36
|
+
params: { kayan: ctx.kayan.name },
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
return ctx
|
|
40
|
+
}
|
|
41
|
+
// mithaq-admin 0.0.0 10cc4a57db299d48c4fd53314ed42e02fc04b0464a7fa8a0c512cefc7ccb2b82
|