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,16 @@
|
|
|
1
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
2
|
+
|
|
3
|
+
type FirstRun = { open: false } | { open: true; code: 'none' | 'required' | 'unset' }
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Whether this app of one organization still waits for its first owner, and while it does,
|
|
7
|
+
* whether first-run asks the setup code: the provider's own state, read in process. With
|
|
8
|
+
* many organizations nobody waits: each sign-up founds one.
|
|
9
|
+
*/
|
|
10
|
+
export const firstRun = createServerFn({ method: 'GET' }).handler(async (): Promise<FirstRun> => {
|
|
11
|
+
// Imported inside the server half so no page ships the provider to the browser.
|
|
12
|
+
const [{ asas }, { organizations }] = await Promise.all([import('asas'), import('asas/organizations')])
|
|
13
|
+
if (organizations === 'many') return { open: false }
|
|
14
|
+
type Provider = { name: string; server: { api: { firstRunState(): Promise<FirstRun> } } }
|
|
15
|
+
return (asas.auth.strategies as unknown as Provider[]).find((one) => one.name === 'user')!.server.api.firstRunState()
|
|
16
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
const MOBILE_BREAKPOINT = 768
|
|
4
|
+
|
|
5
|
+
export function useIsMobile() {
|
|
6
|
+
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
|
7
|
+
|
|
8
|
+
React.useEffect(() => {
|
|
9
|
+
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
|
10
|
+
const onChange = () => {
|
|
11
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
12
|
+
}
|
|
13
|
+
mql.addEventListener("change", onChange)
|
|
14
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
15
|
+
return () => mql.removeEventListener("change", onChange)
|
|
16
|
+
}, [])
|
|
17
|
+
|
|
18
|
+
return !!isMobile
|
|
19
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The path a click on a plain `<a href>` is routed to inside the screen, or
|
|
3
|
+
* `undefined` for a click the browser keeps: a modified or handled click, another
|
|
4
|
+
* target, a download, another origin, or a path outside the screen's `base`.
|
|
5
|
+
*/
|
|
6
|
+
export const routedHref = ({
|
|
7
|
+
event,
|
|
8
|
+
origin,
|
|
9
|
+
base,
|
|
10
|
+
}: {
|
|
11
|
+
event: Pick<MouseEvent, 'button' | 'metaKey' | 'altKey' | 'ctrlKey' | 'shiftKey' | 'defaultPrevented' | 'target'>
|
|
12
|
+
origin: string
|
|
13
|
+
base: string
|
|
14
|
+
}): string | undefined => {
|
|
15
|
+
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) return undefined
|
|
16
|
+
const anchor = (event.target as Element | null)?.closest?.('a[href]')
|
|
17
|
+
const target = anchor?.getAttribute('target')
|
|
18
|
+
if (!anchor || (target && target !== '_self') || anchor.hasAttribute('download')) return undefined
|
|
19
|
+
// The browser's own resolution against the current page: against the origin
|
|
20
|
+
// alone, `href="edit"` on `/dashboard/courses/` would read as `/edit`.
|
|
21
|
+
const url = new URL((anchor as HTMLAnchorElement).href)
|
|
22
|
+
if (url.origin !== origin || (url.pathname !== base && !url.pathname.startsWith(`${base}/`))) return undefined
|
|
23
|
+
return `${url.pathname}${url.search}${url.hash}`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Where a list asked for a page past its last is sent instead: the same URL at
|
|
28
|
+
* the last page, or with no page where the last is the first. `undefined` for a
|
|
29
|
+
* page in range.
|
|
30
|
+
*/
|
|
31
|
+
export const clampedHref = ({ href, page, totalPages }: { href: string; page: number; totalPages: number }): string | undefined => {
|
|
32
|
+
if (page <= Math.max(1, totalPages)) return undefined
|
|
33
|
+
const url = new URL(href, 'http://local')
|
|
34
|
+
if (totalPages > 1) url.searchParams.set('page', String(totalPages))
|
|
35
|
+
else url.searchParams.delete('page')
|
|
36
|
+
return `${url.pathname}${url.search}${url.hash}`
|
|
37
|
+
}
|
|
38
|
+
// mithaq-admin 2d3ab739c92c6c2b397041b76bf4b23b2902b7a2 4754cbac64eeac528e7e037173ad56ffa8d07df3e526e1191bbe3be400db97a5
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useRouter } from '@tanstack/react-router'
|
|
2
|
+
import { Button } from '@/components/ui/button'
|
|
3
|
+
import { localeEntries } from 'mithaq-admin/locale-menu.ts'
|
|
4
|
+
import { locales, showLocale, type Locale } from './copy.ts'
|
|
5
|
+
|
|
6
|
+
/** The language switch of the auth and landing pages, which redraws the page in place, so a form keeps what was typed. */
|
|
7
|
+
export function LocaleSwitch({ locale }: { locale: Locale }) {
|
|
8
|
+
const router = useRouter()
|
|
9
|
+
return (
|
|
10
|
+
<div className="flex items-center gap-1">
|
|
11
|
+
{localeEntries(locales, locale).map((entry) => (
|
|
12
|
+
<Button
|
|
13
|
+
key={entry.locale}
|
|
14
|
+
type="button"
|
|
15
|
+
variant={entry.active ? 'secondary' : 'ghost'}
|
|
16
|
+
size="xs"
|
|
17
|
+
aria-current={entry.active}
|
|
18
|
+
aria-label={entry.label}
|
|
19
|
+
onClick={() => void showLocale({ to: entry.locale, router })}
|
|
20
|
+
>
|
|
21
|
+
<span className="hidden sm:inline">{entry.label}</span>
|
|
22
|
+
<span className="sm:hidden">{entry.locale.toUpperCase()}</span>
|
|
23
|
+
</Button>
|
|
24
|
+
))}
|
|
25
|
+
</div>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
// mithaq-admin 0.0.0 37e32a32766452cd88389e2e4cc6c246ccc3ccb40bf941f8c1e3835b4e377f98
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// What `/unauthorized` draws below its sentence for a person who belongs to no organization: the invitations sent to
|
|
2
|
+
// their address, and with many organizations a way to found one. The screen host imports it from the app, which knows
|
|
3
|
+
// the organization mode the host does not.
|
|
4
|
+
import { useEffect, useState } from 'react'
|
|
5
|
+
import { defaultLocale, organizations } from 'asas/organizations'
|
|
6
|
+
import { endonym } from 'mithaq-admin/locale-menu.ts'
|
|
7
|
+
import { Button } from '@/components/ui/button'
|
|
8
|
+
import { Input } from '@/components/ui/input'
|
|
9
|
+
import { authArm } from './api.ts'
|
|
10
|
+
import { useRefusal } from './auth-frame.tsx'
|
|
11
|
+
import { locales } from './copy.ts'
|
|
12
|
+
import { useCopy } from './routes/__root.tsx'
|
|
13
|
+
|
|
14
|
+
/** An invitation as the auth setup answers it: the organization's name in each locale beside the provider's one. */
|
|
15
|
+
type Invitation = { id: string; organizationName: string; organizationNames?: Record<string, string>; expiresAt: string }
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Better Auth requires a unique slug, so a random tail keeps same-named organizations apart.
|
|
19
|
+
* Padded before the cut, so the tail is always six characters and the slug never ends in `-`.
|
|
20
|
+
*/
|
|
21
|
+
const slugOf = (name: string) =>
|
|
22
|
+
`${name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 40) || 'organization'}-${Math.random().toString(36).slice(2).padEnd(6, '0').slice(0, 6)}`
|
|
23
|
+
|
|
24
|
+
/** Signing in again lands a signed-in person on their first screen, in the organization the session now names. */
|
|
25
|
+
const goIn = () => window.location.assign('/auth/sign-in')
|
|
26
|
+
|
|
27
|
+
export function NoOrganization({ email, founds = organizations === 'many' }: { email: string | undefined; founds?: boolean }) {
|
|
28
|
+
const { t, locale } = useCopy()
|
|
29
|
+
const refusal = useRefusal()
|
|
30
|
+
const [invitations, setInvitations] = useState<Invitation[]>([])
|
|
31
|
+
const [name, setName] = useState('')
|
|
32
|
+
// The organization's name in each other locale, optional: one left empty falls back to `name`.
|
|
33
|
+
const [names, setNames] = useState<Record<string, string>>({})
|
|
34
|
+
const others = locales.filter((one) => one !== defaultLocale)
|
|
35
|
+
const [busy, setBusy] = useState(false)
|
|
36
|
+
const [failed, setFailed] = useState<string>()
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
// A refusal, such as an address nobody proved, reads as no invitation.
|
|
39
|
+
void fetch('/api/auth/user/organization/list-user-invitations').then(async (answer) => {
|
|
40
|
+
if (answer.ok) setInvitations(((await answer.json()) as Invitation[]).filter((one) => new Date(one.expiresAt).getTime() > Date.now()))
|
|
41
|
+
})
|
|
42
|
+
if (founds) {
|
|
43
|
+
void fetch('/api/auth/user/get-session').then(async (answer) => {
|
|
44
|
+
const named = ((await answer.json().catch(() => null)) as { user?: { name?: string } } | null)?.user?.name
|
|
45
|
+
if (named) setName((typed) => typed || t('auth.workspaceOf', '{name}\'s workspace')!.replace('{name}', () => named))
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
}, [])
|
|
49
|
+
/** One request at a time, its refusal said in this app's words. */
|
|
50
|
+
const send = async ({ path, body, done }: { path: string; body: Record<string, unknown>; done: () => void }) => {
|
|
51
|
+
setBusy(true)
|
|
52
|
+
setFailed(undefined)
|
|
53
|
+
const answered = await authArm(path, body)
|
|
54
|
+
setBusy(false)
|
|
55
|
+
if (!answered.ok) return setFailed(refusal(answered, t('auth.requestFailed', 'That request did not work.')))
|
|
56
|
+
done()
|
|
57
|
+
}
|
|
58
|
+
return (
|
|
59
|
+
<div data-no-organization="" className="grid gap-4">
|
|
60
|
+
{invitations.length ? (
|
|
61
|
+
<section className="grid gap-2">
|
|
62
|
+
<h2 className="text-sm font-medium">{t('auth.yourInvitations', 'Your invitations')}</h2>
|
|
63
|
+
<ul className="grid gap-2">
|
|
64
|
+
{invitations.map((one) => (
|
|
65
|
+
<li key={one.id} data-invitation={one.id} className="flex flex-wrap items-center justify-between gap-2 rounded-md border px-3 py-2">
|
|
66
|
+
<bdi className="[overflow-wrap:anywhere]">{one.organizationNames?.[locale] || one.organizationName}</bdi>
|
|
67
|
+
<span className="flex gap-2">
|
|
68
|
+
<Button type="button" size="sm" data-accept={one.id} disabled={busy} onClick={() => void send({ path: 'organization/accept-invitation', body: { invitationId: one.id }, done: goIn })}>
|
|
69
|
+
{t('auth.invitationAccept', 'Accept the invitation')}
|
|
70
|
+
</Button>
|
|
71
|
+
<Button type="button" size="sm" variant="outline" data-decline={one.id} disabled={busy} onClick={() => void send({ path: 'organization/reject-invitation', body: { invitationId: one.id }, done: () => setInvitations((all) => all.filter((other) => other.id !== one.id)) })}>
|
|
72
|
+
{t('auth.invitationDecline', 'Decline')}
|
|
73
|
+
</Button>
|
|
74
|
+
</span>
|
|
75
|
+
</li>
|
|
76
|
+
))}
|
|
77
|
+
</ul>
|
|
78
|
+
</section>
|
|
79
|
+
) : null}
|
|
80
|
+
{founds ? (
|
|
81
|
+
<form
|
|
82
|
+
data-create-organization=""
|
|
83
|
+
className="grid gap-2"
|
|
84
|
+
noValidate
|
|
85
|
+
onSubmit={(event) => {
|
|
86
|
+
event.preventDefault()
|
|
87
|
+
const named = { [defaultLocale]: name.trim(), ...Object.fromEntries(others.map((one) => [one, names[one]?.trim() ?? '']).filter(([, one]) => one)) }
|
|
88
|
+
if (name.trim()) void send({ path: 'organization/create', body: { name: name.trim(), names: JSON.stringify(named), slug: slugOf(name.trim()) }, done: goIn })
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
<label htmlFor="new-organization" className="text-sm font-medium">{t('auth.createOrganization', 'Create an organization')}</label>
|
|
92
|
+
<Input id="new-organization" value={name} maxLength={100} autoComplete="organization" dir="auto" onChange={(event) => setName(event.target.value)} />
|
|
93
|
+
{others.map((one) => (
|
|
94
|
+
<div key={one} className="grid gap-1">
|
|
95
|
+
<label htmlFor={`new-organization-${one}`} className="text-muted-foreground text-sm">{t('auth.organizationNameIn', 'Its name in {language}, if it has one')!.replace('{language}', () => endonym(one))}</label>
|
|
96
|
+
<Input id={`new-organization-${one}`} lang={one} dir="auto" value={names[one] ?? ''} maxLength={100} onChange={(event) => setNames((held) => ({ ...held, [one]: event.target.value }))} />
|
|
97
|
+
</div>
|
|
98
|
+
))}
|
|
99
|
+
<Button type="submit" disabled={busy || !name.trim()}>{t('auth.createOrganizationSubmit', 'Create')}</Button>
|
|
100
|
+
</form>
|
|
101
|
+
) : null}
|
|
102
|
+
<p className="text-muted-foreground text-sm">
|
|
103
|
+
{t('auth.askToBeInvited', 'Ask an administrator to invite {email}.')!.split(/(\{email\})/).map((part, at) => (part === '{email}' ? <bdi key={at} className="[overflow-wrap:anywhere]">{email}</bdi> : part))}
|
|
104
|
+
</p>
|
|
105
|
+
{failed ? <p role="alert" className="text-destructive text-sm">{failed}</p> : null}
|
|
106
|
+
</div>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* h3 knows its own error by the name `HTTPError` alone. 499 is the status nginx logs for a client
|
|
3
|
+
* that closed its request.
|
|
4
|
+
*/
|
|
5
|
+
const gone = () =>
|
|
6
|
+
Object.assign(new Error('The client closed the request.'), {
|
|
7
|
+
name: 'HTTPError',
|
|
8
|
+
status: 499,
|
|
9
|
+
statusText: 'Client Closed Request',
|
|
10
|
+
toJSON: () => ({ status: 499, message: 'The client closed the request.' }),
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The request handed to TanStack Start, whose signal aborts with `gone` when the client's does.
|
|
15
|
+
* Start throws the signal's reason on abort and h3 logs any error that is not its own as an
|
|
16
|
+
* unhandled 500, so a closed tab would be logged as a fault. The request is rebuilt from its parts
|
|
17
|
+
* because the Node server's srvx request is a class `new Request(request)` cannot read.
|
|
18
|
+
*/
|
|
19
|
+
export const withQuietAbort = (request: Request): Request => {
|
|
20
|
+
const controller = new AbortController()
|
|
21
|
+
if (request.signal.aborted) controller.abort(gone())
|
|
22
|
+
else request.signal.addEventListener('abort', () => controller.abort(gone()), { once: true })
|
|
23
|
+
const { method, headers, body } = request
|
|
24
|
+
return new Request(request.url, { method, headers, body: method === 'GET' || method === 'HEAD' ? null : body, signal: controller.signal, duplex: 'half' } as RequestInit)
|
|
25
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where signing in lands: only a path of this origin, so a link cannot turn the sign-in page
|
|
3
|
+
* into a jump to another site; anything else lands on `home`.
|
|
4
|
+
*
|
|
5
|
+
* The origin is the URL parser's answer rather than a prefix test's, so `https://elsewhere`,
|
|
6
|
+
* `//elsewhere` and `javascript:` are refused however they are spelled. A backslash, a C0 control
|
|
7
|
+
* and DEL are refused before the parse, since the parser rewrites or strips them before reading
|
|
8
|
+
* the host. They are written as escapes so git and grep keep reading this file as text.
|
|
9
|
+
*
|
|
10
|
+
* The answer is the parser's own path, its dot segments resolved, plain or percent-encoded, so what
|
|
11
|
+
* a caller reads is where the browser goes. A path that resolves to `//` is refused, since a browser
|
|
12
|
+
* reads it as another origin: `/..//elsewhere` resolves to `//elsewhere`.
|
|
13
|
+
*/
|
|
14
|
+
const base = 'https://mithaq.invalid'
|
|
15
|
+
|
|
16
|
+
export const wanted = ({ asked, home }: { asked: string | undefined; home: string }): string => {
|
|
17
|
+
if (asked === undefined || !asked.startsWith('/') || /[\\\x00-\x1f\x7f]/.test(asked)) return home
|
|
18
|
+
try {
|
|
19
|
+
const url = new URL(asked, base)
|
|
20
|
+
return url.origin === base && !url.pathname.startsWith('//') ? url.pathname + url.search + url.hash : home
|
|
21
|
+
} catch {
|
|
22
|
+
// `new URL('//', base)` throws rather than resolving.
|
|
23
|
+
return home
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Where a signed-in person goes: the path asked for where it lies inside a screen they open, else the
|
|
29
|
+
* one screen they open, else the dashboard, since whoever opens several is staff, else `/unauthorized`,
|
|
30
|
+
* which asks which of several or says why there is none. The dashboard is found by its screen's name,
|
|
31
|
+
* never its place in the list. A path in a screen they do not open is not followed, since there they
|
|
32
|
+
* would only be refused.
|
|
33
|
+
*/
|
|
34
|
+
export const landing = ({ asked, screens }: { asked: string | undefined; screens: readonly { name: string; path: string }[] }): string => {
|
|
35
|
+
const home = (screens.length === 1 ? screens[0] : screens.find((one) => one.name === 'dashboard'))?.path ?? '/unauthorized'
|
|
36
|
+
const path = wanted({ asked, home })
|
|
37
|
+
const { pathname } = new URL(path, base)
|
|
38
|
+
return screens.some(({ path: screen }) => pathname === screen || pathname.startsWith(`${screen}/`)) ? path : home
|
|
39
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The headers a page's server-side `request` call sends: the session's, with
|
|
3
|
+
* the loader's own layered over them. The session's cookie always wins, so a
|
|
4
|
+
* loader's headers cannot act as another actor.
|
|
5
|
+
*/
|
|
6
|
+
export const mergedHeaders = (session: Headers, own?: HeadersInit): Headers => {
|
|
7
|
+
const headers = new Headers(session)
|
|
8
|
+
if (own) for (const [key, value] of new Headers(own)) if (key !== 'cookie') headers.set(key, value)
|
|
9
|
+
return headers
|
|
10
|
+
}
|
|
11
|
+
// mithaq-admin 2d3ab739c92c6c2b397041b76bf4b23b2902b7a2 2969a3dafbf201d6eb6074ecda0e8f17e341cbd1ca176100350844e74ec23623
|