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,284 @@
|
|
|
1
|
+
import { actorPermissions, count, defaultFields, defaultJsonMaxBytes, denied, find, findOne, invalid, type Ctx, type Held } from 'mithaq'
|
|
2
|
+
import { defineKayan, rows, organization } from '@asas/mithaq'
|
|
3
|
+
import { backofficeClosed } from '@asas/mithaq/roles/tenant-access.ts'
|
|
4
|
+
import { auditLogName, type Change } from '@asas/mithaq/mithaq/audit-sink.ts'
|
|
5
|
+
|
|
6
|
+
/** The kayan whose `find` this actor holds, which are the kayan whose entries it reads. */
|
|
7
|
+
const heldKayans = (held: Exclude<Held, true | undefined>) =>
|
|
8
|
+
[...held].filter((key) => key.endsWith('.find') && !key.includes('.fields.')).map((key) => key.slice(0, -'.find'.length))
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The read the log's rows are ANDed with, so it narrows and never widens: an
|
|
12
|
+
* entry of a kayan the actor may not list is no entry it reads. An actor
|
|
13
|
+
* nothing grades reads none, which is the closed direction.
|
|
14
|
+
*/
|
|
15
|
+
const readableKayans = ({ user }: { user: unknown }) => {
|
|
16
|
+
const held = actorPermissions(user)
|
|
17
|
+
if (held === true) return true
|
|
18
|
+
const kayans = held ? heldKayans(held) : []
|
|
19
|
+
return kayans.length ? { kayan: { in: kayans } } : false
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Whether this actor reads one column of the kayan an entry names. The access
|
|
24
|
+
* target grades every column of this project by its key.
|
|
25
|
+
*/
|
|
26
|
+
const readsField = (held: Held, kayan: string, field: string) => held === true || held?.has(`${kayan}.fields.${field}`) === true
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Whether this actor reads the provider's people, by the key of their `find`.
|
|
30
|
+
* A command's operating-system user is the one thing in the log that names a
|
|
31
|
+
* person no row of the people does, so only such a reader sees or asks for it.
|
|
32
|
+
*/
|
|
33
|
+
const readsPeople = (held: Held) => held === true || held?.has('user.find') === true
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* What the sink writes for a command: the command, then the operating-system
|
|
37
|
+
* user in parentheses. The command is the shortest part before ` (`, so a user
|
|
38
|
+
* holding a parenthesis is never left in it.
|
|
39
|
+
*/
|
|
40
|
+
const commandActor = /^(.+?) \(.+\)$/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The step after the read: each entry keeps the changes of the columns the
|
|
44
|
+
* actor may read and drops the rest whole, so a column it may not read shows
|
|
45
|
+
* neither value nor that it changed. The server does it rather than the page,
|
|
46
|
+
* because the log is read over HTTP as well as drawn.
|
|
47
|
+
*/
|
|
48
|
+
const visibleChanges = (ctx: Ctx): Ctx => {
|
|
49
|
+
const held = actorPermissions(ctx.actor)
|
|
50
|
+
const trim = (entry: unknown) => {
|
|
51
|
+
const changes = (entry as { changes?: unknown } | null)?.changes
|
|
52
|
+
if (!changes || typeof changes !== 'object') return entry
|
|
53
|
+
const { kayan, actor } = entry as { kayan: string; actor?: unknown }
|
|
54
|
+
const kept = Object.fromEntries(Object.entries(changes as Record<string, Change>).filter(([field]) => readsField(held, kayan, field)))
|
|
55
|
+
// A command's actor is the command alone to a reader of no people, its operating-system user blanked.
|
|
56
|
+
const named = !readsPeople(held) && typeof actor === 'string' ? { actor: commandActor.exec(actor)?.[1] ?? actor } : {}
|
|
57
|
+
return { ...(entry as object), ...named, changes: kept }
|
|
58
|
+
}
|
|
59
|
+
const data: unknown = ctx.data
|
|
60
|
+
const page = data && typeof data === 'object' && Array.isArray((data as { docs?: unknown }).docs) ? (data as { docs: unknown[] }) : undefined
|
|
61
|
+
return { ...ctx, data: (page ? { ...page, docs: page.docs.map(trim) } : trim(data)) as Record<string, unknown> }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The columns the log is filtered and ordered by. The organization is core's
|
|
66
|
+
* own filter and `impersonatedBy` names staff.
|
|
67
|
+
*/
|
|
68
|
+
const queryable = new Set(['kayan', 'record', 'createdAt', 'operation', 'actor'])
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* What the log is searched by besides those, never ordered by: `changes`, the
|
|
72
|
+
* whole of each change, or a path into it, `changes.<column>` and deeper. Each
|
|
73
|
+
* search takes any of the json kind's operators and reaches only the kayan whose
|
|
74
|
+
* columns it may tell of (`reachOf`), because a match in a column the reader
|
|
75
|
+
* holds no key for would say what that column held before `visibleChanges`
|
|
76
|
+
* trims the page.
|
|
77
|
+
*/
|
|
78
|
+
const searched = 'changes'
|
|
79
|
+
|
|
80
|
+
/** The field a `where` key names: `changes.title.to` names `changes`. */
|
|
81
|
+
const fieldOf = (key: string) => key.split('.')[0]!
|
|
82
|
+
|
|
83
|
+
const namedIn = (where: unknown): string[] => {
|
|
84
|
+
if (!where || typeof where !== 'object') return []
|
|
85
|
+
return Object.entries(where).flatMap(([key, value]) =>
|
|
86
|
+
(key === 'and' || key === 'or') && Array.isArray(value) ? value.flatMap(namedIn) : [key])
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Every column key the catalog holds, by the kayan it belongs to. */
|
|
90
|
+
const columnsOf = (catalog: readonly string[]) => {
|
|
91
|
+
const columns = new Map<string, string[]>()
|
|
92
|
+
for (const key of catalog) {
|
|
93
|
+
const at = key.indexOf('.fields.')
|
|
94
|
+
if (at > 0) columns.set(key.slice(0, at), [...(columns.get(key.slice(0, at)) ?? []), key])
|
|
95
|
+
}
|
|
96
|
+
return columns
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ponytail: the reach here and `visibleChanges` grade by keys alone. A tenant role that a row
|
|
100
|
+
// table narrows further, by a `fields` list or a row filter on its read, would be told of columns
|
|
101
|
+
// and rows its own reads hide; grade each entry through the kayan's own field access and read
|
|
102
|
+
// `where` before a project adds one.
|
|
103
|
+
/**
|
|
104
|
+
* The kayan one search of a change reaches for this actor, or `true` for every
|
|
105
|
+
* one. The whole change reaches a kayan whose every column the actor reads, since
|
|
106
|
+
* it matches any of them; a path into one column, `changes.<column>`, a kayan
|
|
107
|
+
* whose column of that name it reads, since it matches inside that column alone.
|
|
108
|
+
*/
|
|
109
|
+
const reachOf = ({ held, catalog }: { held: Held; catalog: readonly string[] }) => (key: string): true | string[] => {
|
|
110
|
+
if (held === true) return true
|
|
111
|
+
const column = key.split('.')[1]
|
|
112
|
+
return [...columnsOf(catalog)]
|
|
113
|
+
.filter(([kayan, keys]) => held?.has(`${kayan}.find`) && (column === undefined ? keys.every((one) => held.has(one)) : held.has(`${kayan}.fields.${column}`)))
|
|
114
|
+
.map(([kayan]) => kayan)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The `where` with each search of a change ANDed, where it stands, with the kayan
|
|
119
|
+
* it reaches, so an `or` around it still means what it said, and a negation, a
|
|
120
|
+
* `not_equals` or an `exists: false`, still answers no entry past its reach.
|
|
121
|
+
*/
|
|
122
|
+
const narrowed = (where: unknown, reach: (key: string) => true | string[]): unknown => {
|
|
123
|
+
if (!where || typeof where !== 'object') return where
|
|
124
|
+
const parts = Object.entries(where).map(([key, value]) => {
|
|
125
|
+
if ((key === 'and' || key === 'or') && Array.isArray(value)) return { [key]: value.map((one) => narrowed(one, reach)) }
|
|
126
|
+
if (fieldOf(key) !== searched) return { [key]: value }
|
|
127
|
+
const kayans = reach(key)
|
|
128
|
+
return kayans === true ? { [key]: value } : { and: [{ [key]: value }, { kayan: { in: kayans } }] }
|
|
129
|
+
})
|
|
130
|
+
return parts.length === 1 ? parts[0] : { and: parts }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The step before the read: a `where` naming any column but those above, or a
|
|
135
|
+
* `sort` by any but the queryable ones, is refused 400 before the store is
|
|
136
|
+
* asked, on every read the log declares, and each search of a change is narrowed
|
|
137
|
+
* to the kayan it reaches. The runner's own checks still run after it on what it
|
|
138
|
+
* lets through.
|
|
139
|
+
*/
|
|
140
|
+
const queriedByQueryable = (ctx: Ctx, tools: { catalog: readonly string[] }): Ctx => {
|
|
141
|
+
// The runner names the organization a read is scoped to only where the read is not widened.
|
|
142
|
+
// `reachOf` and `visibleChanges` grade by the active organization's keys, so a read across every
|
|
143
|
+
// membership (`tenant: 'mine'`) would grade another organization's entries by this one's: it is
|
|
144
|
+
// refused before anything else of it is judged.
|
|
145
|
+
if (ctx.tenant === undefined) {
|
|
146
|
+
throw denied({ message: 'Kayan "auditLog" is read in the active organization alone.', key: 'error.forbidden.read', params: {} })
|
|
147
|
+
}
|
|
148
|
+
const { where, sort } = ctx.data as { where?: unknown; sort?: unknown }
|
|
149
|
+
const field = namedIn(where).find((name) => !queryable.has(name) && fieldOf(name) !== searched)
|
|
150
|
+
if (field !== undefined) {
|
|
151
|
+
throw invalid({ message: `Kayan "auditLog" is filtered by ${[...queryable, searched].join(', ')} alone, and not by "${field}".`, key: 'error.invalid.where', params: { kayan: 'auditLog', field } })
|
|
152
|
+
}
|
|
153
|
+
const held = actorPermissions(ctx.actor)
|
|
154
|
+
// A reader of no people names an actor in no filter and no sort: a pattern, a range, an order or
|
|
155
|
+
// even an equality over the string would let it confirm a guessed operating-system user of a
|
|
156
|
+
// command's actor.
|
|
157
|
+
if (!readsPeople(held) && (namedIn(where).includes('actor') || (typeof sort === 'string' && sort.replace(/^-/, '') === 'actor'))) {
|
|
158
|
+
throw invalid({ message: 'Kayan "auditLog" is neither filtered nor sorted by "actor" for a reader of no people.', key: 'error.invalid.where', params: { kayan: 'auditLog', field: 'actor' } })
|
|
159
|
+
}
|
|
160
|
+
if (sort !== undefined && !(typeof sort === 'string' && queryable.has(sort.replace(/^-/, '')))) {
|
|
161
|
+
throw invalid({ message: `Kayan "auditLog" is sorted by ${[...queryable].join(', ')} alone.`, key: 'error.invalid.sort_field', params: { kayan: 'auditLog', field: String(sort) } })
|
|
162
|
+
}
|
|
163
|
+
return where === undefined ? ctx : { ...ctx, data: { ...ctx.data, where: narrowed(where, reachOf({ held, catalog: tools.catalog })) } }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The audit log's rows: an organization reads its own entries, and nothing writes
|
|
168
|
+
* one through the log, a caller of any role included, since the service
|
|
169
|
+
* target's sink writes each entry through the store inside the write it
|
|
170
|
+
* records. Every backoffice role is closed: the backoffice lists no entry, and a
|
|
171
|
+
* staff member's own impersonated writes are the entries it would read.
|
|
172
|
+
*/
|
|
173
|
+
const auditLogAccess = rows({
|
|
174
|
+
'*': { create: false, read: true, update: false, delete: false },
|
|
175
|
+
...backofficeClosed,
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The audit log: one row per row a write changed, written by the service
|
|
180
|
+
* target's audit sink inside that write's own transaction and by nothing else.
|
|
181
|
+
* It declares no operation that writes, so no caller creates, edits or deletes
|
|
182
|
+
* an entry, and each of its rows answers `false` for every write besides.
|
|
183
|
+
*
|
|
184
|
+
* It is its organization's own. A record's last entries are read in its edit
|
|
185
|
+
* page's side column, all of them in its Activity tab, and the organization's on
|
|
186
|
+
* the dashboard's Activity page, a page of the screen's own rather than of this
|
|
187
|
+
* kayan, which draws none. The backoffice lists none of it and reads none of
|
|
188
|
+
* it: staff's own actions are its staff history, `backofficeLog`.
|
|
189
|
+
*/
|
|
190
|
+
export const auditLog = defineKayan({
|
|
191
|
+
name: auditLogName,
|
|
192
|
+
dbName: 'audit_log',
|
|
193
|
+
messages: {
|
|
194
|
+
singular: { en: 'Audit entry', ar: 'سجل تدقيق' },
|
|
195
|
+
plural: { en: 'Audit log', ar: 'سجل التدقيق' },
|
|
196
|
+
// What the activity panel draws beside the field labels.
|
|
197
|
+
activity: { en: 'Activity', ar: 'النشاط' },
|
|
198
|
+
loading: { en: 'Loading activity…', ar: 'جارٍ تحميل النشاط…' },
|
|
199
|
+
empty: { en: 'Nothing has changed here yet.', ar: 'لم يتغير شيء هنا بعد.' },
|
|
200
|
+
failed: { en: 'The activity could not be read.', ar: 'تعذرت قراءة النشاط.' },
|
|
201
|
+
more: { en: 'Show older', ar: 'عرض الأقدم' },
|
|
202
|
+
system: { en: 'The system', ar: 'النظام' },
|
|
203
|
+
event: { en: '{action} by {actor}', ar: '{action} بواسطة {actor}' },
|
|
204
|
+
// Staff writing as a member: the real person first, as the member they wrote as.
|
|
205
|
+
impersonated: { en: '{staff} as {member}', ar: '{staff} بصفة {member}' },
|
|
206
|
+
// One per operation a tenant kayan writes through, which is every entry the sink records.
|
|
207
|
+
action: {
|
|
208
|
+
create: { en: 'Created', ar: 'أُنشئ' },
|
|
209
|
+
update: { en: 'Updated', ar: 'حُدِّث' },
|
|
210
|
+
delete: { en: 'Deleted', ar: 'حُذف' },
|
|
211
|
+
updateMany: { en: 'Updated', ar: 'حُدِّث' },
|
|
212
|
+
deleteMany: { en: 'Deleted', ar: 'حُذف' },
|
|
213
|
+
restoreVersion: { en: 'Restored a version', ar: 'استُعيد إصدار' },
|
|
214
|
+
archive: { en: 'Archived', ar: 'أُرشف' },
|
|
215
|
+
archiveMany: { en: 'Archived', ar: 'أُرشف' },
|
|
216
|
+
unarchive: { en: 'Unarchived', ar: 'أُلغيت أرشفته' },
|
|
217
|
+
unpublish: { en: 'Unpublished', ar: 'أُلغي نشره' },
|
|
218
|
+
// An update that published a draft, which the sink names apart from a plain update.
|
|
219
|
+
publish: { en: 'Published', ar: 'نُشر' },
|
|
220
|
+
},
|
|
221
|
+
someone: { en: 'Someone', ar: 'شخص ما' },
|
|
222
|
+
// A command line write, named for a reader of the people: the command and
|
|
223
|
+
// the operating-system user who ran it.
|
|
224
|
+
command: { en: 'the command {command} ({user})', ar: 'الأمر {command} ({user})' },
|
|
225
|
+
// The viewer's own entry, which needs no read of the people to name. Its
|
|
226
|
+
// sentence is its own, since Arabic joins the person to "by" in one word,
|
|
227
|
+
// "بواسطتك", and not as "بواسطة أنت".
|
|
228
|
+
you: { en: 'You', ar: 'أنت' },
|
|
229
|
+
eventYou: { en: '{action} by You', ar: '{action} بواسطتك' },
|
|
230
|
+
none: { en: 'No field you may read changed.', ar: 'لم يتغير حقل يمكنك قراءته.' },
|
|
231
|
+
// Beside a change the sink cut to the start of its JSON, which a large json value is.
|
|
232
|
+
truncated: { en: 'Too long to keep whole; its start is shown.', ar: 'أطول من أن يُحفظ كاملا؛ تظهر بدايته.' },
|
|
233
|
+
// The panel's link to the rest of a record's entries, and the Activity page and tab.
|
|
234
|
+
all: { en: 'All activity', ar: 'كل النشاط' },
|
|
235
|
+
about: { en: 'Every change in this organization, newest first.', ar: 'كل تغيير في هذه المؤسسة، الأحدث أولا.' },
|
|
236
|
+
filters: { en: 'Filter the activity', ar: 'تصفية النشاط' },
|
|
237
|
+
// What the search box names as what it searches, in the sentence "Search changed values".
|
|
238
|
+
searchChanges: { en: 'changed values', ar: 'القيم المتغيرة' },
|
|
239
|
+
person: { en: 'Person', ar: 'الشخص' },
|
|
240
|
+
type: { en: 'Record type', ar: 'نوع السجل' },
|
|
241
|
+
field: { en: 'Field', ar: 'الحقل' },
|
|
242
|
+
actionFilter: { en: 'Action', ar: 'الإجراء' },
|
|
243
|
+
from: { en: 'From', ar: 'من' },
|
|
244
|
+
to: { en: 'To', ar: 'إلى' },
|
|
245
|
+
any: { en: 'Any', ar: 'الكل' },
|
|
246
|
+
filter: {
|
|
247
|
+
created: { en: 'Created', ar: 'أُنشئ' },
|
|
248
|
+
updated: { en: 'Updated', ar: 'حُدِّث' },
|
|
249
|
+
deleted: { en: 'Deleted', ar: 'حُذف' },
|
|
250
|
+
published: { en: 'Published', ar: 'نُشر' },
|
|
251
|
+
restored: { en: 'Restored', ar: 'استُعيد' },
|
|
252
|
+
},
|
|
253
|
+
clear: { en: 'Clear filters', ar: 'مسح عوامل التصفية' },
|
|
254
|
+
nothing: { en: 'No activity matches these filters.', ar: 'لا نشاط يطابق عوامل التصفية هذه.' },
|
|
255
|
+
oneRecord: { en: 'One record\'s activity.', ar: 'نشاط سجل واحد.' },
|
|
256
|
+
found: { en: 'Entries found: {count}', ar: 'السجلات المطابقة: {count}' },
|
|
257
|
+
searchLimited: { en: 'The search covers the records whose every field you may read.', ar: 'يشمل البحث السجلات التي يمكنك قراءة كل حقولها.' },
|
|
258
|
+
},
|
|
259
|
+
access: { ...auditLogAccess, read: readableKayans },
|
|
260
|
+
targets: {
|
|
261
|
+
dashboard: {},
|
|
262
|
+
service: {
|
|
263
|
+
operations: {
|
|
264
|
+
find: { steps: [queriedByQueryable, find, visibleChanges] },
|
|
265
|
+
findByID: { steps: [queriedByQueryable, findOne, visibleChanges] },
|
|
266
|
+
count: { steps: [queriedByQueryable, count] },
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
fields: {
|
|
271
|
+
id: defaultFields.id,
|
|
272
|
+
createdAt: defaultFields.createdAt,
|
|
273
|
+
organization,
|
|
274
|
+
kayan: { kind: 'text', label: { en: 'Kayan', ar: 'الكيان' }, rules: { required: true, maxLength: 60 } },
|
|
275
|
+
record: { kind: 'text', label: { en: 'Record', ar: 'السجل' }, rules: { required: true, index: true } },
|
|
276
|
+
operation: { kind: 'text', label: { en: 'Operation', ar: 'العملية' }, rules: { required: true, maxLength: 60 } },
|
|
277
|
+
actor: { kind: 'text', label: { en: 'Actor', ar: 'المنفذ' } },
|
|
278
|
+
impersonatedBy: { kind: 'text', label: { en: 'Impersonated by', ar: 'انتحل الصفة' } },
|
|
279
|
+
// Each changed column by name, so a search finds the change of one column by
|
|
280
|
+
// the path `changes.<column>`, which `queriedByQueryable` grades by that
|
|
281
|
+
// column's key. Never an order, as no json field is.
|
|
282
|
+
changes: { kind: 'json', label: { en: 'Changes', ar: 'التغييرات' }, maxBytes: defaultJsonMaxBytes, rules: { required: true } },
|
|
283
|
+
},
|
|
284
|
+
})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { defaultFields, defaultOperations } from 'mithaq'
|
|
2
|
+
import { defineKayan, rows } from '@asas/mithaq'
|
|
3
|
+
import { backofficeList } from '@asas/mithaq/mithaq/pages.ts'
|
|
4
|
+
import { backofficeReads } from '@asas/mithaq/roles/tenant-access.ts'
|
|
5
|
+
|
|
6
|
+
/** Who did what to whom on the backoffice, as `action` names it. */
|
|
7
|
+
const actions = [
|
|
8
|
+
{ value: 'impersonate', label: { en: 'Started impersonating', ar: 'بدأ انتحال الصفة' } },
|
|
9
|
+
{ value: 'stopImpersonating', label: { en: 'Stopped impersonating', ar: 'أنهى انتحال الصفة' } },
|
|
10
|
+
{ value: 'setRole', label: { en: 'Set the backoffice role', ar: 'عيّن دور الإدارة الخلفية' } },
|
|
11
|
+
{ value: 'ban', label: { en: 'Banned', ar: 'حظر' } },
|
|
12
|
+
{ value: 'unban', label: { en: 'Lifted the ban', ar: 'رفع الحظر' } },
|
|
13
|
+
] as const
|
|
14
|
+
|
|
15
|
+
const text = (label: { en: string; ar: string }) => ({ kind: 'text', label }) as const
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The staff history: one row per impersonation started or stopped, backoffice
|
|
19
|
+
* role set and ban set or lifted, written by the auth setup's backoffice hook
|
|
20
|
+
* once the provider's route succeeded, and by nothing else. It belongs to no
|
|
21
|
+
* organization, since staff act above every tenant, and it declares no
|
|
22
|
+
* operation that writes, so no caller makes, edits or removes a row.
|
|
23
|
+
*
|
|
24
|
+
* Only a backoffice role reads it, in the backoffice app alone, and only one
|
|
25
|
+
* holding its keys: the row table names no `'*'` row, so a tenant's
|
|
26
|
+
* administrator, who holds every key of the project, takes no row of it and is
|
|
27
|
+
* refused. The tenant app's screens list none of it.
|
|
28
|
+
*
|
|
29
|
+
* Each person is kept by id and by the address they had then, so the history
|
|
30
|
+
* still says who once a person is renamed or removed.
|
|
31
|
+
*/
|
|
32
|
+
export const backofficeLog = defineKayan({
|
|
33
|
+
name: 'backofficeLog',
|
|
34
|
+
dbName: 'backoffice_log',
|
|
35
|
+
messages: {
|
|
36
|
+
singular: { en: 'Staff action', ar: 'إجراء طاقم' },
|
|
37
|
+
plural: { en: 'Staff history', ar: 'سجل الطاقم' },
|
|
38
|
+
},
|
|
39
|
+
access: rows({ ...backofficeReads }),
|
|
40
|
+
targets: {
|
|
41
|
+
service: {
|
|
42
|
+
operations: { find: defaultOperations.find, findByID: defaultOperations.findByID, count: defaultOperations.count },
|
|
43
|
+
},
|
|
44
|
+
backoffice: {
|
|
45
|
+
pages: {
|
|
46
|
+
...backofficeList('/history', {
|
|
47
|
+
columns: ['createdAt', 'action', 'actorEmail', 'targetEmail', 'previousRole', 'role', 'banReason'],
|
|
48
|
+
filters: ['action', 'actorEmail', 'targetEmail', 'createdAt'],
|
|
49
|
+
search: ['actorEmail', 'targetEmail'],
|
|
50
|
+
sort: '-createdAt',
|
|
51
|
+
}),
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
fields: {
|
|
56
|
+
id: defaultFields.id,
|
|
57
|
+
createdAt: defaultFields.createdAt,
|
|
58
|
+
action: { kind: 'text', label: { en: 'Action', ar: 'الإجراء' }, options: actions, rules: { required: true } },
|
|
59
|
+
actor: text({ en: 'Staff member', ar: 'عضو الطاقم' }),
|
|
60
|
+
actorEmail: text({ en: 'Staff member\'s email', ar: 'بريد عضو الطاقم' }),
|
|
61
|
+
target: text({ en: 'Person', ar: 'الشخص' }),
|
|
62
|
+
targetEmail: text({ en: 'Person\'s email', ar: 'بريد الشخص' }),
|
|
63
|
+
previousRole: text({ en: 'Role before', ar: 'الدور السابق' }),
|
|
64
|
+
role: text({ en: 'Role after', ar: 'الدور الجديد' }),
|
|
65
|
+
banReason: text({ en: 'Ban reason', ar: 'سبب الحظر' }),
|
|
66
|
+
},
|
|
67
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { localUsers } from './local-users/local-users.ts'
|
|
2
|
+
// Written by `mithaq generate auth`: one key per plugin the vocabulary names,
|
|
3
|
+
// and `core` for the four Better Auth ships without one. Spreading the keys is
|
|
4
|
+
// what keeps this list from naming eight files, and a project that drops the
|
|
5
|
+
// organization plugin drops one spread.
|
|
6
|
+
import { betterAuth } from './user/index.ts'
|
|
7
|
+
|
|
8
|
+
// The account other features point at, and the asas names its administrator by.
|
|
9
|
+
export { user } from './user/user.ts'
|
|
10
|
+
|
|
11
|
+
export const kayan = [...betterAuth.core, ...betterAuth.organization, localUsers] as const
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defaultFields,
|
|
3
|
+
defaultLockTime,
|
|
4
|
+
defaultMaxLoginAttempts,
|
|
5
|
+
defaultTokenExpiration,
|
|
6
|
+
local,
|
|
7
|
+
localAuthFields,
|
|
8
|
+
localAuthOperations,
|
|
9
|
+
defaultOperations,
|
|
10
|
+
type Ctx,
|
|
11
|
+
} from 'mithaq'
|
|
12
|
+
import { defineKayan, requestContext } from '@asas/mithaq'
|
|
13
|
+
import { appPage, backofficeList } from '@asas/mithaq/mithaq/pages.ts'
|
|
14
|
+
import { superAdmin } from '@asas/mithaq/roles/tenant-access.ts'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The trusted-header names this deployment's own proxy overwrites with the real client address, read
|
|
18
|
+
* from the same `TRUSTED_IP_HEADER` variable Better Auth's `advanced.ipAddress.ipAddressHeaders` is
|
|
19
|
+
* fed from, so both limiters key on the same address. Unset, it falls back to `x-forwarded-for`.
|
|
20
|
+
*/
|
|
21
|
+
export const ip = (request: Request): string | null => {
|
|
22
|
+
const headers = process.env.TRUSTED_IP_HEADER?.split(',').map((header) => header.trim()).filter(Boolean) ?? [
|
|
23
|
+
'x-forwarded-for',
|
|
24
|
+
]
|
|
25
|
+
for (const header of headers) {
|
|
26
|
+
const value = request.headers.get(header)?.split(',').at(-1)?.trim()
|
|
27
|
+
if (value) return value
|
|
28
|
+
}
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The secret local auth signs its tokens with, 32 characters at least, from the environment and
|
|
34
|
+
* never from this file: a secret written here is one anyone reading the repository forges a token
|
|
35
|
+
* with. Unset or empty, local auth is off as a whole: every POST to `/api/auth/local` answers
|
|
36
|
+
* `withoutSecret` below with a 503, and the public sign-up with a 403. The kayan keeps its provider,
|
|
37
|
+
* fields and operations, so the build and every file generated from it are the same in every
|
|
38
|
+
* environment.
|
|
39
|
+
*/
|
|
40
|
+
const secret = process.env.LOCAL_AUTH_SECRET || undefined
|
|
41
|
+
|
|
42
|
+
const markVerified = (ctx: Ctx): Ctx => ({ ...ctx, data: { ...ctx.data, _verified: true } })
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The kayan the local auth provider writes. It spreads the shipped `localAuthFields` and
|
|
46
|
+
* `localAuthOperations`, so every column and step it has is written here, and the build refuses a
|
|
47
|
+
* protected column left readable.
|
|
48
|
+
*/
|
|
49
|
+
export const localUsers = defineKayan({
|
|
50
|
+
name: 'localUsers',
|
|
51
|
+
dbName: 'local_users',
|
|
52
|
+
messages: {
|
|
53
|
+
singular: { en: 'Local account', ar: 'حساب محلي' },
|
|
54
|
+
plural: { en: 'Local accounts', ar: 'الحسابات المحلية' },
|
|
55
|
+
},
|
|
56
|
+
// `context` is what first registration hands its steps.
|
|
57
|
+
//
|
|
58
|
+
// `ip` reads the last entry of whichever header `TRUSTED_IP_HEADER` names, the one the trusted proxy
|
|
59
|
+
// in front of it appended, because no host this template is served by hands the socket address to
|
|
60
|
+
// the `Request`. That is only safe behind exactly one such proxy: without one any caller writes the
|
|
61
|
+
// header, and behind more than one the last entry is the nearest proxy's own address, so every user
|
|
62
|
+
// would share one count; there, take the entry at the depth of the trusted proxies. A project served
|
|
63
|
+
// bare passes its server's own address instead, such as Bun's `server.requestIP(request)`. No header
|
|
64
|
+
// answers null, which leaves the request uncounted.
|
|
65
|
+
// No `email` is passed, so no mail is sent: a super admin verifies each sign-up in the backoffice app.
|
|
66
|
+
auth: local({
|
|
67
|
+
secret,
|
|
68
|
+
withoutSecret: 'Local sign-in is off on this server: LOCAL_AUTH_SECRET is not set. Set it to a secret of 32 characters at least and restart the server.',
|
|
69
|
+
context: requestContext,
|
|
70
|
+
ip,
|
|
71
|
+
tokenExpiration: defaultTokenExpiration,
|
|
72
|
+
maxLoginAttempts: defaultMaxLoginAttempts,
|
|
73
|
+
lockTime: defaultLockTime,
|
|
74
|
+
}),
|
|
75
|
+
targets: {
|
|
76
|
+
service: {
|
|
77
|
+
operations: {
|
|
78
|
+
...defaultOperations,
|
|
79
|
+
...localAuthOperations,
|
|
80
|
+
// The provider's own update with one step before it that writes `_verified: true` over whatever
|
|
81
|
+
// the call sent, so it can only verify; the access below judges it as an update with that one key
|
|
82
|
+
// as its data.
|
|
83
|
+
verify: { ...localAuthOperations.update, steps: [markVerified, ...localAuthOperations.update.steps] },
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
/**
|
|
87
|
+
* Where a super admin verifies a sign-up: the accounts by email, verified state and the date each
|
|
88
|
+
* was made, and an edit page whose form is the verified toggle alone. Its fields are written here,
|
|
89
|
+
* never read off `/api/access`, which reports `update` for `email` because the kayan's own `update`
|
|
90
|
+
* asks the data and the report has none; the access above still refuses any write but `_verified`.
|
|
91
|
+
* The form sends only what changed, so an unchanged toggle writes nothing and leaves `updatedAt`
|
|
92
|
+
* alone. The protected columns are named by no page, so no list offers one as a column or a filter.
|
|
93
|
+
*/
|
|
94
|
+
backoffice: {
|
|
95
|
+
useAsTitle: 'email',
|
|
96
|
+
pages: {
|
|
97
|
+
...backofficeList('/local-users', {
|
|
98
|
+
columns: ['email', '_verified', 'createdAt'],
|
|
99
|
+
filters: ['_verified'],
|
|
100
|
+
search: ['email'],
|
|
101
|
+
row: [{
|
|
102
|
+
button: 'verify',
|
|
103
|
+
when: { or: [{ 'record._verified': { exists: false } }, { 'record._verified': { equals: false } }] },
|
|
104
|
+
label: { en: 'Verify', ar: 'توثيق' },
|
|
105
|
+
confirm: {
|
|
106
|
+
title: { en: 'Verify this account?', ar: 'توثيق هذا الحساب؟' },
|
|
107
|
+
body: { en: 'Its owner can sign in from now on.', ar: 'يستطيع صاحبه تسجيل الدخول من الآن.' },
|
|
108
|
+
},
|
|
109
|
+
}],
|
|
110
|
+
}),
|
|
111
|
+
// Read-only above the toggle: a read-only leaf is drawn and never sent, so a save still writes
|
|
112
|
+
// `_verified` alone.
|
|
113
|
+
edit: {
|
|
114
|
+
path: '/local-users/:id',
|
|
115
|
+
page: appPage([{ layout: 'record', main: [{ form: [{ section: 'verification', label: { en: 'Verification', ar: 'التحقق' }, fields: [
|
|
116
|
+
{ field: 'email', readOnly: true },
|
|
117
|
+
{ field: 'createdAt', readOnly: true },
|
|
118
|
+
'_verified',
|
|
119
|
+
] }] }] }]),
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
// Public sign-up stays open. A super admin, in the backoffice app alone, verifies an account by writing
|
|
125
|
+
// `_verified: true`, which the field's own write admits from the administrator and nobody else. The
|
|
126
|
+
// super admin writes `_verified` and nothing else: a password or an email it set would be an account
|
|
127
|
+
// it could then log in as, and no audit entry records a write to a kayan with no tenant. An
|
|
128
|
+
// `updateMany` is asked this again for each row it writes, with that row's values. Everyone else, a
|
|
129
|
+
// local user on its own row included, reads and writes none.
|
|
130
|
+
access: {
|
|
131
|
+
create: () => true,
|
|
132
|
+
read: superAdmin,
|
|
133
|
+
update: ({ user, data }) => superAdmin({ user }) && Object.keys(data ?? {}).every((key) => key === '_verified'),
|
|
134
|
+
delete: () => false,
|
|
135
|
+
},
|
|
136
|
+
// A label leaves the shipped access in place, which the build compares by identity.
|
|
137
|
+
fields: {
|
|
138
|
+
...defaultFields,
|
|
139
|
+
...localAuthFields,
|
|
140
|
+
email: { ...localAuthFields.email, label: { en: 'Email', ar: 'البريد الإلكتروني' } },
|
|
141
|
+
_verified: { ...localAuthFields._verified, label: { en: 'Verified', ar: 'موثق' } },
|
|
142
|
+
},
|
|
143
|
+
})
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// This project's access for the generated `account` kayan, which `mithaq generate
|
|
2
|
+
// auth` imports into `./account.ts` because this file sits beside it.
|
|
3
|
+
//
|
|
4
|
+
// Better Auth alone writes and reads this table, through its own adapter, so
|
|
5
|
+
// every managed operation on it is refused to every actor. Written out rather
|
|
6
|
+
// than left to the administrator's default, which a super admin passes in the
|
|
7
|
+
// backoffice app.
|
|
8
|
+
export { providerOnly as access } from '@asas/mithaq/roles/tenant-access.ts'
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Written by `mithaq generate auth` from the Better Auth options generate.providers names. Do not edit.
|
|
2
|
+
// This kayan's access and what each screen draws of it are the project's:
|
|
3
|
+
// `account.access.ts` exporting `access` and `account.screens.ts` exporting
|
|
4
|
+
// `screens`, beside this file, are imported here on the next run. A kayan
|
|
5
|
+
// built under this one's name elsewhere is refused by every relationship to it.
|
|
6
|
+
import { defaultFields, defaultOperations } from 'mithaq'
|
|
7
|
+
import { defineKayan } from '@asas/mithaq'
|
|
8
|
+
import { user } from './user.ts'
|
|
9
|
+
import { access } from './account.access.ts'
|
|
10
|
+
|
|
11
|
+
export const account = defineKayan({
|
|
12
|
+
name: 'account',
|
|
13
|
+
targets: { service: { operations: defaultOperations } },
|
|
14
|
+
access,
|
|
15
|
+
fields: {
|
|
16
|
+
...defaultFields,
|
|
17
|
+
id: { kind: 'text' },
|
|
18
|
+
accountId: { kind: 'text', rules: { required: true } },
|
|
19
|
+
providerId: { kind: 'text', rules: { required: true } },
|
|
20
|
+
userId: { kind: 'relationship', relationTo: () => user, dbName: 'user_id', rules: { required: true, index: true, onDelete: 'cascade' } },
|
|
21
|
+
accessToken: { kind: 'text' },
|
|
22
|
+
refreshToken: { kind: 'text' },
|
|
23
|
+
idToken: { kind: 'text' },
|
|
24
|
+
accessTokenExpiresAt: { kind: 'datetime' },
|
|
25
|
+
refreshTokenExpiresAt: { kind: 'datetime' },
|
|
26
|
+
scope: { kind: 'text' },
|
|
27
|
+
password: { kind: 'text' },
|
|
28
|
+
createdAt: { kind: 'datetime', rules: { required: true } },
|
|
29
|
+
updatedAt: { kind: 'datetime', rules: { required: true, nowOnUpdate: true, providedBy: { update: 'application' } } },
|
|
30
|
+
},
|
|
31
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Written by `mithaq generate auth` from the Better Auth options generate.providers names. Do not edit.
|
|
2
|
+
// One key per plugin that brought tables of its own, and `core` for the tables
|
|
3
|
+
// Better Auth ships without one, so a project spreads these lists into
|
|
4
|
+
// `buildAsas` instead of naming every file. A plugin that leaves takes its key
|
|
5
|
+
// with it, and `mithaq generate auth --check` reports the drift.
|
|
6
|
+
import { account } from './account.ts'
|
|
7
|
+
import { invitation } from './invitation.ts'
|
|
8
|
+
import { member } from './member.ts'
|
|
9
|
+
import { organization } from './organization.ts'
|
|
10
|
+
import { organizationRole } from './organizationRole.ts'
|
|
11
|
+
import { session } from './session.ts'
|
|
12
|
+
import { user } from './user.ts'
|
|
13
|
+
import { verification } from './verification.ts'
|
|
14
|
+
|
|
15
|
+
export const betterAuth = {
|
|
16
|
+
core: [user, session, account, verification],
|
|
17
|
+
organization: [organization, organizationRole, member, invitation],
|
|
18
|
+
} as const
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Better Auth alone writes and reads this table, through its own adapter, so every managed
|
|
2
|
+
// operation on it is refused to every actor. Written out rather than left to the administrator's
|
|
3
|
+
// default, which a super admin passes.
|
|
4
|
+
export { providerOnly as access } from '@asas/mithaq/roles/tenant-access.ts'
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Written by `mithaq generate auth` from the Better Auth options generate.providers names. Do not edit.
|
|
2
|
+
// This kayan's access and what each screen draws of it are the project's:
|
|
3
|
+
// `invitation.access.ts` exporting `access` and `invitation.screens.ts` exporting
|
|
4
|
+
// `screens`, beside this file, are imported here on the next run. A kayan
|
|
5
|
+
// built under this one's name elsewhere is refused by every relationship to it.
|
|
6
|
+
import { defaultFields, defaultOperations } from 'mithaq'
|
|
7
|
+
import { defineKayan } from '@asas/mithaq'
|
|
8
|
+
import { organization } from './organization.ts'
|
|
9
|
+
import { user } from './user.ts'
|
|
10
|
+
import { access } from './invitation.access.ts'
|
|
11
|
+
|
|
12
|
+
export const invitation = defineKayan({
|
|
13
|
+
name: 'invitation',
|
|
14
|
+
targets: { service: { operations: defaultOperations } },
|
|
15
|
+
access,
|
|
16
|
+
fields: {
|
|
17
|
+
...defaultFields,
|
|
18
|
+
id: { kind: 'text' },
|
|
19
|
+
organizationId: { kind: 'relationship', relationTo: () => organization, dbName: 'organization_id', rules: { required: true, index: true, onDelete: 'cascade' } },
|
|
20
|
+
email: { kind: 'text', rules: { required: true, index: true } },
|
|
21
|
+
role: { kind: 'text' },
|
|
22
|
+
status: { kind: 'text', rules: { required: true } },
|
|
23
|
+
expiresAt: { kind: 'datetime', rules: { required: true } },
|
|
24
|
+
createdAt: { kind: 'datetime', rules: { required: true } },
|
|
25
|
+
inviterId: { kind: 'relationship', relationTo: () => user, dbName: 'inviter_id', rules: { required: true, onDelete: 'cascade' } },
|
|
26
|
+
},
|
|
27
|
+
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// This project's access for the generated `member` kayan, which `mithaq
|
|
2
|
+
// generate auth` imports into `./member.ts` because this file sits beside it. A
|
|
3
|
+
// backoffice role reads every organization's members in the backoffice app, by the
|
|
4
|
+
// keys it holds, and no one writes through it. There is no `'*'` row, so a
|
|
5
|
+
// tenant's actor takes no row here and reads nothing: a tenant lists its
|
|
6
|
+
// members through the provider's own `list-members`.
|
|
7
|
+
import { rows } from '@asas/mithaq'
|
|
8
|
+
import { backofficeReads } from '@asas/mithaq/roles/tenant-access.ts'
|
|
9
|
+
|
|
10
|
+
export const access = rows(backofficeReads)
|