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,163 @@
|
|
|
1
|
+
// Written by `mithaq generate screens`. Do not edit.
|
|
2
|
+
import type { ScreenClient as Client } from 'mithaq/admin'
|
|
3
|
+
export type { PageClient } from 'mithaq/admin'
|
|
4
|
+
export type UserValues = {
|
|
5
|
+
id: string
|
|
6
|
+
updatedAt: Date
|
|
7
|
+
createdAt: Date
|
|
8
|
+
name: string
|
|
9
|
+
email: string
|
|
10
|
+
emailVerified: boolean
|
|
11
|
+
image: string | null
|
|
12
|
+
role: string | null
|
|
13
|
+
banned: boolean | null
|
|
14
|
+
banReason: string | null
|
|
15
|
+
banExpires: Date | null
|
|
16
|
+
}
|
|
17
|
+
export type OrganizationValues = {
|
|
18
|
+
id: string
|
|
19
|
+
updatedAt: Date
|
|
20
|
+
createdAt: Date
|
|
21
|
+
name: string
|
|
22
|
+
slug: string
|
|
23
|
+
logo: string | null
|
|
24
|
+
metadata: string | null
|
|
25
|
+
names: string | null
|
|
26
|
+
}
|
|
27
|
+
export type MemberValues = {
|
|
28
|
+
id: string
|
|
29
|
+
updatedAt: Date
|
|
30
|
+
createdAt: Date
|
|
31
|
+
organizationId: string
|
|
32
|
+
userId: string
|
|
33
|
+
role: string
|
|
34
|
+
lastSeenAt: Date | null
|
|
35
|
+
}
|
|
36
|
+
export type CoursesValues = {
|
|
37
|
+
id: string
|
|
38
|
+
updatedAt: Date
|
|
39
|
+
createdAt: Date
|
|
40
|
+
organization: string
|
|
41
|
+
title: string
|
|
42
|
+
summary: string | null
|
|
43
|
+
fee: number | null
|
|
44
|
+
currency: 'SAR' | 'USD' | null
|
|
45
|
+
level: number | null
|
|
46
|
+
notifications: boolean | null
|
|
47
|
+
delivery: 'live' | 'recorded' | null
|
|
48
|
+
audience: 'beginner' | 'advanced' | null
|
|
49
|
+
accessCode: string | null
|
|
50
|
+
archived: boolean | null
|
|
51
|
+
topics: string[] | null
|
|
52
|
+
opensOn: string | null
|
|
53
|
+
archiveReason: string | null
|
|
54
|
+
tags: string[] | null
|
|
55
|
+
scheduledSessions: Values['sessions'][]
|
|
56
|
+
_status: 'draft' | 'published' | null
|
|
57
|
+
details: { root: Record<string, unknown> } | null
|
|
58
|
+
cover: unknown
|
|
59
|
+
selfEnroll: boolean | null
|
|
60
|
+
}
|
|
61
|
+
export type SessionsValues = {
|
|
62
|
+
id: string
|
|
63
|
+
updatedAt: Date
|
|
64
|
+
createdAt: Date
|
|
65
|
+
organization: string
|
|
66
|
+
course: string
|
|
67
|
+
startsAt: Date
|
|
68
|
+
capacity: number | null
|
|
69
|
+
venue: 'online' | 'campus' | null
|
|
70
|
+
format: 'workshop' | 'lecture' | null
|
|
71
|
+
archived: boolean | null
|
|
72
|
+
details: { root: Record<string, unknown> } | null
|
|
73
|
+
enrollments: Values['enrollments'][]
|
|
74
|
+
archiveReason: string | null
|
|
75
|
+
}
|
|
76
|
+
export type EnrollmentsValues = {
|
|
77
|
+
id: string
|
|
78
|
+
updatedAt: Date
|
|
79
|
+
createdAt: Date
|
|
80
|
+
organization: string
|
|
81
|
+
session: string
|
|
82
|
+
student: string
|
|
83
|
+
deletedAt: Date | null
|
|
84
|
+
}
|
|
85
|
+
export type TagsValues = {
|
|
86
|
+
id: string
|
|
87
|
+
updatedAt: Date
|
|
88
|
+
createdAt: Date
|
|
89
|
+
organization: string
|
|
90
|
+
name: Record<'ar' | 'en', string>
|
|
91
|
+
}
|
|
92
|
+
export type RolesValues = {
|
|
93
|
+
id: string
|
|
94
|
+
updatedAt: Date
|
|
95
|
+
createdAt: Date
|
|
96
|
+
organization: string
|
|
97
|
+
name: string
|
|
98
|
+
label: Record<'ar' | 'en', string>
|
|
99
|
+
permissions: string[]
|
|
100
|
+
}
|
|
101
|
+
export type MemberRolesValues = {
|
|
102
|
+
id: string
|
|
103
|
+
updatedAt: Date
|
|
104
|
+
createdAt: Date
|
|
105
|
+
organization: string
|
|
106
|
+
user: string
|
|
107
|
+
role: string
|
|
108
|
+
}
|
|
109
|
+
export type LocalUsersValues = {
|
|
110
|
+
id: string
|
|
111
|
+
updatedAt: Date
|
|
112
|
+
createdAt: Date
|
|
113
|
+
email: string
|
|
114
|
+
hash: string
|
|
115
|
+
salt: string
|
|
116
|
+
sessions: string | null
|
|
117
|
+
loginAttempts: number | null
|
|
118
|
+
lockUntil: Date | null
|
|
119
|
+
resetPasswordToken: string | null
|
|
120
|
+
resetPasswordAttempts: number | null
|
|
121
|
+
resetPasswordExpiration: Date | null
|
|
122
|
+
_verified: boolean | null
|
|
123
|
+
_verificationToken: string | null
|
|
124
|
+
}
|
|
125
|
+
export type BackofficeLogValues = {
|
|
126
|
+
id: string
|
|
127
|
+
createdAt: Date
|
|
128
|
+
action: 'impersonate' | 'stopImpersonating' | 'setRole' | 'ban' | 'unban'
|
|
129
|
+
actor: string | null
|
|
130
|
+
actorEmail: string | null
|
|
131
|
+
target: string | null
|
|
132
|
+
targetEmail: string | null
|
|
133
|
+
previousRole: string | null
|
|
134
|
+
role: string | null
|
|
135
|
+
banReason: string | null
|
|
136
|
+
}
|
|
137
|
+
export type Values = {
|
|
138
|
+
user: UserValues
|
|
139
|
+
organization: OrganizationValues
|
|
140
|
+
member: MemberValues
|
|
141
|
+
courses: CoursesValues
|
|
142
|
+
sessions: SessionsValues
|
|
143
|
+
enrollments: EnrollmentsValues
|
|
144
|
+
tags: TagsValues
|
|
145
|
+
roles: RolesValues
|
|
146
|
+
memberRoles: MemberRolesValues
|
|
147
|
+
localUsers: LocalUsersValues
|
|
148
|
+
backofficeLog: BackofficeLogValues
|
|
149
|
+
}
|
|
150
|
+
export type Pages = {
|
|
151
|
+
user: 'list' | 'view'
|
|
152
|
+
organization: 'list' | 'view'
|
|
153
|
+
member: 'list' | 'view'
|
|
154
|
+
courses: 'list' | 'view'
|
|
155
|
+
sessions: 'list' | 'view'
|
|
156
|
+
enrollments: 'list' | 'view'
|
|
157
|
+
tags: 'list' | 'view'
|
|
158
|
+
roles: 'list' | 'view'
|
|
159
|
+
memberRoles: 'list' | 'view'
|
|
160
|
+
localUsers: 'list' | 'edit'
|
|
161
|
+
backofficeLog: 'list'
|
|
162
|
+
}
|
|
163
|
+
export type ScreenClient = Client<Pages>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Written by `mithaq generate screens`. Do not edit.
|
|
2
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
3
|
+
import { ScreenNotFound, ScreenPageView, askedFor, pageHead, pageLoader } from '@/screens.tsx'
|
|
4
|
+
import { files } from './backoffice/files.ts'
|
|
5
|
+
|
|
6
|
+
export const Route = createFileRoute('/(screens)/backoffice/$')({
|
|
7
|
+
validateSearch: askedFor,
|
|
8
|
+
loader: ({ location, params }) => pageLoader({ screen: 'backoffice', files, path: params._splat ?? '', query: location.search, href: location.href }),
|
|
9
|
+
head: ({ loaderData, match }) => pageHead(loaderData, match.context),
|
|
10
|
+
notFoundComponent: () => <ScreenNotFound files={files} screen={'backoffice'} />,
|
|
11
|
+
component: Page,
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
function Page() {
|
|
15
|
+
const navigate = Route.useNavigate()
|
|
16
|
+
return <ScreenPageView files={files} data={Route.useLoaderData()} query={Route.useSearch()} onQuery={(search) => void navigate({ search })} />
|
|
17
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Written by `mithaq generate screens`. Do not edit.
|
|
2
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
3
|
+
import { ScreenPageView, askedFor, pageHead, pageLoader } from '@/screens.tsx'
|
|
4
|
+
import { files } from './backoffice/files.ts'
|
|
5
|
+
|
|
6
|
+
export const Route = createFileRoute('/(screens)/backoffice/')({
|
|
7
|
+
loader: ({ location }) => pageLoader({ screen: 'backoffice', files, path: '', query: {}, href: location.href }),
|
|
8
|
+
head: ({ loaderData, match }) => pageHead(loaderData, match.context),
|
|
9
|
+
component: () => <ScreenPageView files={files} data={Route.useLoaderData()} query={askedFor({})} onQuery={() => {}} />,
|
|
10
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Written by `mithaq generate screens`. Do not edit.
|
|
2
|
+
import { Outlet, createFileRoute } from '@tanstack/react-router'
|
|
3
|
+
import { ScreenFrame, frameLoader } from '@/screens.tsx'
|
|
4
|
+
import { files } from './backoffice/files.ts'
|
|
5
|
+
|
|
6
|
+
export const Route = createFileRoute('/(screens)/backoffice')({
|
|
7
|
+
loader: ({ location }) => frameLoader({ screen: 'backoffice', files, href: location.href }),
|
|
8
|
+
component: () => (
|
|
9
|
+
<ScreenFrame files={files} data={Route.useLoaderData()}>
|
|
10
|
+
<Outlet />
|
|
11
|
+
</ScreenFrame>
|
|
12
|
+
),
|
|
13
|
+
})
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { HeadContent, Outlet, Scripts, createRootRoute } from '@tanstack/react-router'
|
|
2
|
+
import { DirectionProvider } from '@/components/ui/direction'
|
|
3
|
+
export { locales } from '../copy.ts'
|
|
4
|
+
import { dirOf, documentHead, localeOf, translator } from '../copy.ts'
|
|
5
|
+
import { ThemeProvider } from 'mithaq-admin/theme.tsx'
|
|
6
|
+
import { themeStorageKey } from '../theme.ts'
|
|
7
|
+
import appCss from '../styles.css?url'
|
|
8
|
+
|
|
9
|
+
// Puts the chosen theme on the document before first paint, which the provider's effect cannot do.
|
|
10
|
+
// Also reloads a page restored from the back-forward cache, so Back after signing out asks the server again.
|
|
11
|
+
const theme = `
|
|
12
|
+
(function() {
|
|
13
|
+
var storageKey = ${JSON.stringify(themeStorageKey)};
|
|
14
|
+
var defaultTheme = "system";
|
|
15
|
+
try {
|
|
16
|
+
var theme = localStorage.getItem(storageKey) || defaultTheme;
|
|
17
|
+
var systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
18
|
+
var root = document.documentElement;
|
|
19
|
+
root.classList.remove("light", "dark");
|
|
20
|
+
root.classList.add(theme === "system" ? systemTheme : theme);
|
|
21
|
+
} catch (e) {}
|
|
22
|
+
window.addEventListener("pageshow", function (event) {
|
|
23
|
+
if (event.persisted) location.reload();
|
|
24
|
+
});
|
|
25
|
+
})()
|
|
26
|
+
`
|
|
27
|
+
|
|
28
|
+
// A request context nothing registered is `never` in the router's types, so the shape is named and read
|
|
29
|
+
// through a cast; `localeOf` turns it back into a declared locale.
|
|
30
|
+
type FromRequest = { serverContext?: { locale?: unknown } }
|
|
31
|
+
|
|
32
|
+
export const Route = createRootRoute({
|
|
33
|
+
// On the client the locale is read back off the `lang` the server wrote on `<html>`, so the hydrated tree
|
|
34
|
+
// matches the one sent. `localeOf` on both refuses a hand-edited `lang` like a hand-written cookie.
|
|
35
|
+
beforeLoad: (ctx) => {
|
|
36
|
+
const locale =
|
|
37
|
+
typeof document === 'undefined'
|
|
38
|
+
? localeOf((ctx as unknown as FromRequest).serverContext?.locale as string | undefined)
|
|
39
|
+
: localeOf(document.documentElement.lang)
|
|
40
|
+
return { locale, dir: dirOf(locale), app: 'backoffice.brand' }
|
|
41
|
+
},
|
|
42
|
+
// The favicon stops the browser asking for a `/favicon.ico` nobody serves.
|
|
43
|
+
head: ({ match }) => ({
|
|
44
|
+
meta: [
|
|
45
|
+
{ charSet: 'utf-8' },
|
|
46
|
+
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
|
47
|
+
...documentHead(match.context, match._notFound ? (t) => t('admin.notFoundTitle', 'Page not found') : undefined).meta,
|
|
48
|
+
],
|
|
49
|
+
links: [{ rel: 'stylesheet', href: appCss }, { rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' }],
|
|
50
|
+
}),
|
|
51
|
+
// Without it the router draws a development-only placeholder and warns.
|
|
52
|
+
notFoundComponent: NotFound,
|
|
53
|
+
component: RootDocument,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
/** The locale and direction of this request and the `t` resolved for it, read from the root route's context. */
|
|
57
|
+
export const useCopy = () => {
|
|
58
|
+
const { dir, locale } = Route.useRouteContext()
|
|
59
|
+
return { dir, locale, t: translator(locale) }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function NotFound() {
|
|
63
|
+
const { t } = useCopy()
|
|
64
|
+
return (
|
|
65
|
+
<main className="flex min-h-screen flex-col items-center justify-center gap-4 px-4 text-center">
|
|
66
|
+
<h1 className="text-2xl font-bold tracking-tight">{t('admin.notFoundTitle', 'Page not found')}</h1>
|
|
67
|
+
<p className="text-muted-foreground text-sm">{t('admin.notFound', 'That page does not exist.')}</p>
|
|
68
|
+
{/* An anchor, because `/` is no route here. */}
|
|
69
|
+
<a className="text-foreground text-sm underline underline-offset-4" href="/">
|
|
70
|
+
{t('admin.goHome', 'Go to the home page')}
|
|
71
|
+
</a>
|
|
72
|
+
</main>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function RootDocument() {
|
|
77
|
+
const { dir, locale } = useCopy()
|
|
78
|
+
return (
|
|
79
|
+
<html lang={locale} dir={dir} suppressHydrationWarning>
|
|
80
|
+
<head>
|
|
81
|
+
<HeadContent />
|
|
82
|
+
{/* The string is this module's own and carries no request value, so writing it raw is safe. */}
|
|
83
|
+
<script dangerouslySetInnerHTML={{ __html: theme }} />
|
|
84
|
+
</head>
|
|
85
|
+
<body>
|
|
86
|
+
<DirectionProvider direction={dir}>
|
|
87
|
+
<ThemeProvider defaultTheme="system" storageKey={themeStorageKey}>
|
|
88
|
+
<Outlet />
|
|
89
|
+
</ThemeProvider>
|
|
90
|
+
</DirectionProvider>
|
|
91
|
+
<Scripts />
|
|
92
|
+
</body>
|
|
93
|
+
</html>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
// Loads the types of the `server` route option; erased at build.
|
|
3
|
+
import type {} from '@tanstack/start-client-core'
|
|
4
|
+
import { asas, backofficeURL } from 'asas'
|
|
5
|
+
|
|
6
|
+
// The backoffice reads every organization: a read of a kayan asks `tenant: 'all'`, which the runner
|
|
7
|
+
// grants backoffice staff in this app alone and refuses anyone else. A global is one organization's row
|
|
8
|
+
// and asks for none.
|
|
9
|
+
const reads = new Set(asas.operations.filter((one) => one.effect === 'read' && !one.single).map((one) => `/api/${one.kayan}/${one.name}`))
|
|
10
|
+
|
|
11
|
+
// The body is handed on unread, so the plug that answers bounds it before any session is looked at.
|
|
12
|
+
const acrossTenants = (request: Request) => {
|
|
13
|
+
const url = new URL(request.url)
|
|
14
|
+
if (!reads.has(url.pathname)) return request
|
|
15
|
+
url.searchParams.set('tenant', 'all')
|
|
16
|
+
return new Request(url, { method: request.method, headers: request.headers, body: request.body, signal: request.signal, duplex: 'half' } as RequestInit)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// A write whose `Origin` is not BACKOFFICE_URL's is refused: the data plug checks no origin and leans on
|
|
20
|
+
// the session cookie's `SameSite=Lax`, which the customer app's sibling subdomain passes, so this check
|
|
21
|
+
// is the barrier between the two. Never the URL the request arrived on: behind a TLS proxy it is
|
|
22
|
+
// http://, and its host is whatever the Host header said.
|
|
23
|
+
const origin = new URL(backofficeURL).origin
|
|
24
|
+
|
|
25
|
+
export const Route = createFileRoute('/api/$')({
|
|
26
|
+
server: {
|
|
27
|
+
handlers: {
|
|
28
|
+
ANY: async ({ request }) =>
|
|
29
|
+
request.method === 'GET' || request.method === 'HEAD' || request.headers.get('origin') === origin
|
|
30
|
+
? asas.handler(acrossTenants(request))
|
|
31
|
+
: new Response('Forbidden', { status: 403 }),
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
})
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { useState, type ComponentProps } from 'react'
|
|
2
|
+
import { createFileRoute, redirect, useNavigate } from '@tanstack/react-router'
|
|
3
|
+
import { Button } from '@/components/ui/button'
|
|
4
|
+
import { authArm } from '../api.ts'
|
|
5
|
+
import { AuthBox, AuthFrame, authGate, notAnEmail, useRefusal } from '../auth-frame.tsx'
|
|
6
|
+
import { documentHead } from '../copy.ts'
|
|
7
|
+
import { firstUser } from '../first-user.ts'
|
|
8
|
+
import { useCopy } from './__root.tsx'
|
|
9
|
+
|
|
10
|
+
export const Route = createFileRoute('/auth/create-first-user')({
|
|
11
|
+
head: ({ match }) => documentHead(match.context, (t) => t('auth.createFirstUser')),
|
|
12
|
+
beforeLoad: async () => {
|
|
13
|
+
const state = await firstUser()
|
|
14
|
+
if (state.initialized) throw redirect({ to: '/auth/sign-in' })
|
|
15
|
+
return { code: state.code, password: (await authGate()).password }
|
|
16
|
+
},
|
|
17
|
+
component: CreateFirstUser,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
function CreateFirstUser() {
|
|
21
|
+
const navigate = useNavigate()
|
|
22
|
+
const { t } = useCopy()
|
|
23
|
+
const refused = useRefusal()
|
|
24
|
+
const { code: asked, password: bounds } = Route.useRouteContext()
|
|
25
|
+
const [name, setName] = useState('')
|
|
26
|
+
const [email, setEmail] = useState('')
|
|
27
|
+
const [password, setPassword] = useState('')
|
|
28
|
+
const [confirmation, setConfirmation] = useState('')
|
|
29
|
+
const [code, setCode] = useState('')
|
|
30
|
+
const [pending, setPending] = useState(false)
|
|
31
|
+
const [refusal, setRefusal] = useState<string | null>(null)
|
|
32
|
+
const [left, setLeft] = useState<ReadonlySet<string>>(() => new Set())
|
|
33
|
+
const required = t('auth.required', 'This field is required.')
|
|
34
|
+
const wrong = {
|
|
35
|
+
name: () => (name.trim() ? undefined : required),
|
|
36
|
+
email: () => (!email ? required : notAnEmail(email) ? t('auth.invalidEmail', 'Invalid email') : undefined),
|
|
37
|
+
password: () =>
|
|
38
|
+
!password ? required
|
|
39
|
+
: password.length < bounds.min ? t('auth.passwordTooShort', 'Password too short')
|
|
40
|
+
: password.length > bounds.max ? t('auth.passwordTooLong', 'Password too long')
|
|
41
|
+
: undefined,
|
|
42
|
+
confirmation: () => (!confirmation ? required : confirmation === password ? undefined : t('auth.passwordsDiffer', 'The two passwords are not the same.')),
|
|
43
|
+
code: () => (asked !== 'required' || code ? undefined : required),
|
|
44
|
+
}
|
|
45
|
+
const errorOf = (box: keyof typeof wrong) => (left.has(box) ? wrong[box]() : undefined)
|
|
46
|
+
const box = (id: keyof typeof wrong, label: string | undefined, props: ComponentProps<'input'> & { description?: string }) => (
|
|
47
|
+
<AuthBox id={id} label={label} error={errorOf(id)} onLeave={() => setLeft((already) => new Set(already).add(id))} {...props} />
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
const submit = async (event: React.FormEvent) => {
|
|
51
|
+
event.preventDefault()
|
|
52
|
+
setLeft(new Set(Object.keys(wrong)))
|
|
53
|
+
if (Object.values(wrong).some((check) => check())) return
|
|
54
|
+
setPending(true)
|
|
55
|
+
setRefusal(null)
|
|
56
|
+
const made = await authArm('first-register', { name, email, password, ...(asked === 'required' && { code }) })
|
|
57
|
+
setPending(false)
|
|
58
|
+
if (!made.ok) return setRefusal(refused(made, t('auth.createFirstUserFailed', 'That administrator could not be created.')))
|
|
59
|
+
await navigate({ to: '/auth/sign-in' })
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (asked === 'unset') {
|
|
63
|
+
return (
|
|
64
|
+
<AuthFrame title={t('auth.createFirstUser', 'Create the first administrator')}>
|
|
65
|
+
<p className="text-sm" role="alert">
|
|
66
|
+
{t('auth.setupCodeUnset')}
|
|
67
|
+
</p>
|
|
68
|
+
</AuthFrame>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
return (
|
|
72
|
+
<AuthFrame
|
|
73
|
+
title={t('auth.createFirstUser', 'Create the first administrator')}
|
|
74
|
+
description={t('auth.createFirstUserDescription', 'No one administers this backoffice yet. The account made here becomes its first administrator.')}
|
|
75
|
+
>
|
|
76
|
+
<form className="grid gap-4" noValidate onSubmit={submit}>
|
|
77
|
+
{box('name', t('auth.name', 'Name'), { autoComplete: 'name', onChange: (event) => setName(event.target.value), type: 'text', value: name })}
|
|
78
|
+
{box('email', t('auth.email', 'Email'), { autoComplete: 'email', onChange: (event) => setEmail(event.target.value), type: 'email', value: email })}
|
|
79
|
+
{box('password', t('auth.password', 'Password'), {
|
|
80
|
+
autoComplete: 'new-password',
|
|
81
|
+
description: t('auth.passwordRule', 'At least {count} characters.')!.replace('{count}', String(bounds.min)),
|
|
82
|
+
onChange: (event) => setPassword(event.target.value),
|
|
83
|
+
type: 'password',
|
|
84
|
+
value: password,
|
|
85
|
+
})}
|
|
86
|
+
{box('confirmation', t('auth.confirmPassword', 'Confirm the password'), {
|
|
87
|
+
autoComplete: 'new-password',
|
|
88
|
+
onChange: (event) => setConfirmation(event.target.value),
|
|
89
|
+
type: 'password',
|
|
90
|
+
value: confirmation,
|
|
91
|
+
})}
|
|
92
|
+
{asked === 'required'
|
|
93
|
+
? box('code', t('auth.setupCode', 'Setup code'), {
|
|
94
|
+
autoComplete: 'off',
|
|
95
|
+
description: t('auth.setupCodeDescription', 'The SETUP_CODE this server was started with.'),
|
|
96
|
+
onChange: (event) => setCode(event.target.value),
|
|
97
|
+
type: 'password',
|
|
98
|
+
value: code,
|
|
99
|
+
})
|
|
100
|
+
: null}
|
|
101
|
+
{/* `role="alert"` so a refusal is spoken when it appears; nothing moves the focus to it. */}
|
|
102
|
+
{refusal && (
|
|
103
|
+
<p className="text-destructive text-sm" role="alert">
|
|
104
|
+
{refusal}
|
|
105
|
+
</p>
|
|
106
|
+
)}
|
|
107
|
+
<Button disabled={pending} type="submit">
|
|
108
|
+
{pending ? t('auth.createFirstUserPending', 'Creating…') : t('auth.createFirstUserSubmit', 'Create the administrator')}
|
|
109
|
+
</Button>
|
|
110
|
+
</form>
|
|
111
|
+
</AuthFrame>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { useState, type ComponentProps } from 'react'
|
|
2
|
+
import { createFileRoute, redirect, useNavigate } from '@tanstack/react-router'
|
|
3
|
+
import { Button } from '@/components/ui/button'
|
|
4
|
+
import { authArm } from '../api.ts'
|
|
5
|
+
import { AuthBox, AuthFrame, authGate, notAnEmail, redirectSearch, useRefusal } from '../auth-frame.tsx'
|
|
6
|
+
import { documentHead } from '../copy.ts'
|
|
7
|
+
import { firstUser } from '../first-user.ts'
|
|
8
|
+
import { useCopy } from './__root.tsx'
|
|
9
|
+
import { landing } from '../redirect.ts'
|
|
10
|
+
|
|
11
|
+
export const Route = createFileRoute('/auth/sign-in')({
|
|
12
|
+
head: ({ match }) => documentHead(match.context, (t) => t('auth.signIn')),
|
|
13
|
+
validateSearch: redirectSearch,
|
|
14
|
+
// Nobody can sign in to a backoffice no one administers yet.
|
|
15
|
+
beforeLoad: async () => {
|
|
16
|
+
if (!(await firstUser()).initialized) throw redirect({ href: '/auth/create-first-user' })
|
|
17
|
+
},
|
|
18
|
+
component: SignIn,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
function SignIn() {
|
|
22
|
+
const navigate = useNavigate()
|
|
23
|
+
const { t } = useCopy()
|
|
24
|
+
const search = Route.useSearch()
|
|
25
|
+
const refused = useRefusal()
|
|
26
|
+
const [email, setEmail] = useState('')
|
|
27
|
+
const [password, setPassword] = useState('')
|
|
28
|
+
const [pending, setPending] = useState(false)
|
|
29
|
+
const [refusal, setRefusal] = useState<string | null>(null)
|
|
30
|
+
const [left, setLeft] = useState<ReadonlySet<string>>(() => new Set())
|
|
31
|
+
const required = t('auth.required', 'This field is required.')
|
|
32
|
+
const wrong = {
|
|
33
|
+
email: () => (!email ? required : notAnEmail(email) ? t('auth.invalidEmail', 'Invalid email') : undefined),
|
|
34
|
+
password: () => (password ? undefined : required),
|
|
35
|
+
}
|
|
36
|
+
const errorOf = (box: keyof typeof wrong) => (left.has(box) ? wrong[box]() : undefined)
|
|
37
|
+
const box = (id: keyof typeof wrong, label: string | undefined, props: ComponentProps<'input'>) => (
|
|
38
|
+
<AuthBox id={id} label={label} error={errorOf(id)} onLeave={() => setLeft((already) => new Set(already).add(id))} {...props} />
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const submit = async (event: React.FormEvent) => {
|
|
42
|
+
event.preventDefault()
|
|
43
|
+
setLeft(new Set(Object.keys(wrong)))
|
|
44
|
+
if (Object.values(wrong).some((check) => check())) return
|
|
45
|
+
setPending(true)
|
|
46
|
+
setRefusal(null)
|
|
47
|
+
const answered = await authArm('sign-in/email', { email, password })
|
|
48
|
+
// Our own sentence for the provider's code, not its English `message`: a refusal is copy and carries a string per locale.
|
|
49
|
+
if (!answered.ok) {
|
|
50
|
+
setPending(false)
|
|
51
|
+
return setRefusal(refused(answered, t('auth.failed', 'That sign-in did not work.')))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
setPending(false)
|
|
55
|
+
|
|
56
|
+
// A navigation, not a reload, so the screen's loader runs with the cookie just set.
|
|
57
|
+
await navigate({ href: landing({ asked: search.redirect, screens: (await authGate()).screens }) })
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<AuthFrame title={t('auth.signIn', 'Sign in')} description={t('auth.signInBackofficeDescription', 'Sign in to reach the backoffice.')}>
|
|
62
|
+
<form className="grid gap-4" noValidate onSubmit={submit}>
|
|
63
|
+
{box('email', t('auth.email', 'Email'), { autoComplete: 'email', onChange: (event) => setEmail(event.target.value), type: 'email', value: email })}
|
|
64
|
+
{box('password', t('auth.password', 'Password'), { autoComplete: 'current-password', onChange: (event) => setPassword(event.target.value), type: 'password', value: password })}
|
|
65
|
+
{/* `role="alert"` so a refusal is spoken when it appears; nothing moves the focus to it. */}
|
|
66
|
+
{refusal && (
|
|
67
|
+
<p className="text-destructive text-sm" role="alert">
|
|
68
|
+
{refusal}
|
|
69
|
+
</p>
|
|
70
|
+
)}
|
|
71
|
+
<Button disabled={pending} type="submit">
|
|
72
|
+
{pending ? t('auth.signInPending', 'Signing in…') : t('auth.signIn', 'Sign in')}
|
|
73
|
+
</Button>
|
|
74
|
+
</form>
|
|
75
|
+
</AuthFrame>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { Fragment, useState, type ReactNode } from 'react'
|
|
2
|
+
import { createFileRoute, redirect } from '@tanstack/react-router'
|
|
3
|
+
import { Button } from '@/components/ui/button'
|
|
4
|
+
import { authArm } from '../api.ts'
|
|
5
|
+
import { AuthFrame, useRefusal } from '../auth-frame.tsx'
|
|
6
|
+
import { documentHead } from '../copy.ts'
|
|
7
|
+
import { NoOrganization } from '../no-organization.tsx'
|
|
8
|
+
import { ImpersonationBar, loadEnterable } from '../screens.tsx'
|
|
9
|
+
import { useCopy } from './__root.tsx'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Where someone sent here from a screen their role does not open goes without being asked: the one screen it does
|
|
13
|
+
* open. A page refused inside that screen is said instead, and so is an impersonation, whose bar is the way out.
|
|
14
|
+
*/
|
|
15
|
+
export const onwardOf = ({ screens, refused, impersonating }: { screens: readonly { path: string }[]; refused?: unknown; impersonating?: unknown }) =>
|
|
16
|
+
!refused && !impersonating && screens.length === 1 ? screens[0]!.path : undefined
|
|
17
|
+
|
|
18
|
+
export const Route = createFileRoute('/unauthorized')({
|
|
19
|
+
// The path a screen refused; the server only matches it against the build's pages.
|
|
20
|
+
validateSearch: (search: Record<string, unknown>): { from?: string } => (typeof search.from === 'string' ? { from: search.from } : {}),
|
|
21
|
+
loaderDeps: ({ search }) => ({ from: search.from }),
|
|
22
|
+
loader: async ({ deps, context }) => {
|
|
23
|
+
const entered = await loadEnterable({ data: { from: deps.from, locale: context.locale } })
|
|
24
|
+
// Carries this page's own path so a visitor whose session expired lands back here after signing in.
|
|
25
|
+
if (!entered) throw redirect({ to: '/auth/sign-in', search: { redirect: '/unauthorized' } })
|
|
26
|
+
const onward = onwardOf(entered)
|
|
27
|
+
if (onward) throw redirect({ href: onward })
|
|
28
|
+
return entered
|
|
29
|
+
},
|
|
30
|
+
// After the loader, so its data's type is inferred before this reads it.
|
|
31
|
+
head: ({ match, loaderData }) => documentHead(match.context, (t) => (loaderData?.screens.length === 0 ? t('auth.noAccess') : t('auth.unauthorized'))),
|
|
32
|
+
component: Unauthorized,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
function Unauthorized() {
|
|
36
|
+
const { t } = useCopy()
|
|
37
|
+
const { screens, refused, email, impersonating, organizations = [], others = [] } = Route.useLoaderData()
|
|
38
|
+
const refusal = useRefusal()
|
|
39
|
+
const [switching, setSwitching] = useState<string>()
|
|
40
|
+
const [failed, setFailed] = useState<string>()
|
|
41
|
+
// The slug sits beside the name where another membership shares the name.
|
|
42
|
+
const memberships = [...organizations, ...others]
|
|
43
|
+
const named = (one: { id: string; name: string; slug?: string }): ReactNode =>
|
|
44
|
+
one.slug && memberships.some((other) => other.id !== one.id && other.name === one.name) ? (
|
|
45
|
+
<>
|
|
46
|
+
{one.name} <span dir="ltr" data-slug="" className="text-muted-foreground text-xs font-normal">{one.slug}</span>
|
|
47
|
+
</>
|
|
48
|
+
) : (
|
|
49
|
+
one.name
|
|
50
|
+
)
|
|
51
|
+
// Each name is isolated so an email or Latin name in an Arabic sentence keeps
|
|
52
|
+
// its order, and is one inline box that wraps whole, so a long address never
|
|
53
|
+
// runs past the card on a phone.
|
|
54
|
+
const whole = 'inline-block max-w-full [overflow-wrap:anywhere]'
|
|
55
|
+
const said = (text: string | undefined) =>
|
|
56
|
+
text!.split(/(\{email\}|\{organization\}|\{page\})/).map((part, at) =>
|
|
57
|
+
part === '{email}' ? <bdi key={at} className={whole}>{email}</bdi>
|
|
58
|
+
: part === '{organization}' ? <bdi key={at}>{organizations.map((one, index) => <Fragment key={one.id}>{index ? ', ' : ''}<span className={whole}>{named(one)}</span></Fragment>)}</bdi>
|
|
59
|
+
: part === '{page}' ? <bdi key={at}>{refused?.page}</bdi>
|
|
60
|
+
: part)
|
|
61
|
+
const link = 'text-foreground underline underline-offset-4'
|
|
62
|
+
// Someone who belongs to no organization is shown the ways into one, which the app draws.
|
|
63
|
+
const unplaced = !refused && !screens.length && !organizations.length && !others.length && !impersonating
|
|
64
|
+
const screenName = (name: string) => t(`screens.${name}`, name) ?? name
|
|
65
|
+
return (
|
|
66
|
+
<AuthFrame
|
|
67
|
+
title={screens.length ? t('auth.unauthorized', 'Choose where to go') : t('auth.noAccess', 'Your account has no access yet')}
|
|
68
|
+
description={
|
|
69
|
+
<span data-no-access={screens.length ? undefined : ''}>
|
|
70
|
+
{refused
|
|
71
|
+
? said(t('auth.unauthorizedPage', 'Your role does not open that page: {page}.'))
|
|
72
|
+
: screens.length
|
|
73
|
+
? t('auth.unauthorizedDescription', 'This page isn\'t part of your account. Where would you like to go?')
|
|
74
|
+
: organizations.length
|
|
75
|
+
? said(t('auth.noAccessDescription', '{email} is a member of {organization} and holds no role that opens a screen here. Ask an administrator of {organization} to give you one.'))
|
|
76
|
+
: said(t('auth.noOrganization', '{email} belongs to no organization yet.'))}
|
|
77
|
+
</span>
|
|
78
|
+
}
|
|
79
|
+
>
|
|
80
|
+
<div className="grid gap-4">
|
|
81
|
+
{/* Stopping impersonation is the way out in place of Sign out. */}
|
|
82
|
+
{impersonating ? <ImpersonationBar name={impersonating} /> : null}
|
|
83
|
+
{refused ? (
|
|
84
|
+
<a className={link} href={refused.screen.path}>
|
|
85
|
+
{(t('auth.backTo', 'Back to {screen}') ?? 'Back to {screen}').replace('{screen}', () => screenName(refused.screen.name))}
|
|
86
|
+
</a>
|
|
87
|
+
) : screens.length ? (
|
|
88
|
+
<div className="grid gap-2">
|
|
89
|
+
{screens.map((screen) => (
|
|
90
|
+
// An anchor: the path is the screen's own data, not a route the router knows.
|
|
91
|
+
<Button key={screen.name} nativeButton={false} className="max-sm:h-11" render={<a href={screen.path} />}>
|
|
92
|
+
{screenName(screen.name)}
|
|
93
|
+
</Button>
|
|
94
|
+
))}
|
|
95
|
+
</div>
|
|
96
|
+
) : unplaced ? (
|
|
97
|
+
<NoOrganization email={email} />
|
|
98
|
+
) : null}
|
|
99
|
+
{/* So a switch into an organization where the actor holds no role is not a dead end. */}
|
|
100
|
+
{others.map((one) => (
|
|
101
|
+
<Button
|
|
102
|
+
key={one.id}
|
|
103
|
+
type="button"
|
|
104
|
+
variant="secondary"
|
|
105
|
+
data-switch-organization={one.id}
|
|
106
|
+
// Busy while it runs, so a second press sends nothing.
|
|
107
|
+
disabled={switching !== undefined}
|
|
108
|
+
aria-busy={switching === one.id || undefined}
|
|
109
|
+
onClick={() => {
|
|
110
|
+
setSwitching(one.id)
|
|
111
|
+
setFailed(undefined)
|
|
112
|
+
void authArm('organization/set-active', { organizationId: one.id }).then((answer) => {
|
|
113
|
+
if (answer.ok) return window.location.assign('/auth/sign-in')
|
|
114
|
+
setSwitching(undefined)
|
|
115
|
+
setFailed(refusal(answer, t('auth.requestFailed', 'That request did not work.')))
|
|
116
|
+
})
|
|
117
|
+
}}
|
|
118
|
+
>
|
|
119
|
+
{/* One box, so the button's gap does not open beside the name. */}
|
|
120
|
+
<span>{(t('auth.switchTo', 'Switch to {organization}') ?? 'Switch to {organization}').split(/(\{organization\})/).map((part, at) => (part === '{organization}' ? <bdi key={at}>{named(one)}</bdi> : part))}</span>
|
|
121
|
+
</Button>
|
|
122
|
+
))}
|
|
123
|
+
{failed ? <p role="alert" className="text-destructive text-sm">{failed}</p> : null}
|
|
124
|
+
{/* `replace`, so Back does not return to the page signed out of. */}
|
|
125
|
+
{impersonating ? null : (
|
|
126
|
+
<Button type="button" variant="outline" className="max-sm:h-11" onClick={() => void authArm('sign-out').then(() => window.location.replace('/auth/sign-in'))}>
|
|
127
|
+
{t('admin.signOut', 'Sign out')}
|
|
128
|
+
</Button>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
</AuthFrame>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
// mithaq-admin 0.0.0 81d25978cdacbb38254854b0f64b738946ee27baca06a952aed51d35e6fb10af
|