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,94 @@
|
|
|
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 CoursesValues = {
|
|
5
|
+
id: string
|
|
6
|
+
updatedAt: Date
|
|
7
|
+
createdAt: Date
|
|
8
|
+
organization: unknown
|
|
9
|
+
title: string
|
|
10
|
+
summary: string | null
|
|
11
|
+
fee: number | null
|
|
12
|
+
currency: 'SAR' | 'USD' | null
|
|
13
|
+
level: number | null
|
|
14
|
+
notifications: boolean | null
|
|
15
|
+
delivery: 'live' | 'recorded' | null
|
|
16
|
+
audience: 'beginner' | 'advanced' | null
|
|
17
|
+
accessCode: string | null
|
|
18
|
+
archived: boolean | null
|
|
19
|
+
topics: string[] | null
|
|
20
|
+
opensOn: string | null
|
|
21
|
+
archiveReason: string | null
|
|
22
|
+
tags: unknown[] | null
|
|
23
|
+
scheduledSessions: Values['sessions'][]
|
|
24
|
+
_status: 'draft' | 'published' | null
|
|
25
|
+
details: { root: Record<string, unknown> } | null
|
|
26
|
+
cover: string | null
|
|
27
|
+
selfEnroll: boolean | null
|
|
28
|
+
}
|
|
29
|
+
export type EnrollmentsValues = {
|
|
30
|
+
id: string
|
|
31
|
+
updatedAt: Date
|
|
32
|
+
createdAt: Date
|
|
33
|
+
organization: unknown
|
|
34
|
+
session: string
|
|
35
|
+
student: string
|
|
36
|
+
deletedAt: Date | null
|
|
37
|
+
}
|
|
38
|
+
export type SessionsValues = {
|
|
39
|
+
id: string
|
|
40
|
+
updatedAt: Date
|
|
41
|
+
createdAt: Date
|
|
42
|
+
organization: unknown
|
|
43
|
+
course: string
|
|
44
|
+
startsAt: Date
|
|
45
|
+
capacity: number | null
|
|
46
|
+
venue: 'online' | 'campus' | null
|
|
47
|
+
format: 'workshop' | 'lecture' | null
|
|
48
|
+
archived: boolean | null
|
|
49
|
+
details: { root: Record<string, unknown> } | null
|
|
50
|
+
enrollments: Values['enrollments'][]
|
|
51
|
+
archiveReason: string | null
|
|
52
|
+
}
|
|
53
|
+
export type MediaValues = {
|
|
54
|
+
id: string
|
|
55
|
+
updatedAt: Date
|
|
56
|
+
createdAt: Date
|
|
57
|
+
organization: unknown
|
|
58
|
+
filename: string
|
|
59
|
+
mimeType: string
|
|
60
|
+
filesize: number
|
|
61
|
+
width: number | null
|
|
62
|
+
height: number | null
|
|
63
|
+
key: string
|
|
64
|
+
storage: string
|
|
65
|
+
alt: Record<'ar' | 'en', string>
|
|
66
|
+
}
|
|
67
|
+
export type UserValues = {
|
|
68
|
+
id: string
|
|
69
|
+
updatedAt: Date
|
|
70
|
+
createdAt: Date
|
|
71
|
+
name: string
|
|
72
|
+
email: string
|
|
73
|
+
emailVerified: boolean
|
|
74
|
+
image: string | null
|
|
75
|
+
role: string | null
|
|
76
|
+
banned: boolean | null
|
|
77
|
+
banReason: string | null
|
|
78
|
+
banExpires: Date | null
|
|
79
|
+
}
|
|
80
|
+
export type Values = {
|
|
81
|
+
courses: CoursesValues
|
|
82
|
+
enrollments: EnrollmentsValues
|
|
83
|
+
sessions: SessionsValues
|
|
84
|
+
media: MediaValues
|
|
85
|
+
user: UserValues
|
|
86
|
+
}
|
|
87
|
+
export type Pages = {
|
|
88
|
+
courses: 'list' | 'view'
|
|
89
|
+
enrollments: 'list' | 'create' | 'edit'
|
|
90
|
+
sessions: never
|
|
91
|
+
media: never
|
|
92
|
+
user: never
|
|
93
|
+
}
|
|
94
|
+
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 './portal/files.ts'
|
|
5
|
+
|
|
6
|
+
export const Route = createFileRoute('/(screens)/portal/$')({
|
|
7
|
+
validateSearch: askedFor,
|
|
8
|
+
loader: ({ location, params }) => pageLoader({ screen: 'portal', files, path: params._splat ?? '', query: location.search, href: location.href }),
|
|
9
|
+
head: ({ loaderData, match }) => pageHead(loaderData, match.context),
|
|
10
|
+
notFoundComponent: () => <ScreenNotFound files={files} screen={'portal'} />,
|
|
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 './portal/files.ts'
|
|
5
|
+
|
|
6
|
+
export const Route = createFileRoute('/(screens)/portal/')({
|
|
7
|
+
loader: ({ location }) => pageLoader({ screen: 'portal', 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 './portal/files.ts'
|
|
5
|
+
|
|
6
|
+
export const Route = createFileRoute('/(screens)/portal')({
|
|
7
|
+
loader: ({ location }) => frameLoader({ screen: 'portal', files, href: location.href }),
|
|
8
|
+
component: () => (
|
|
9
|
+
<ScreenFrame files={files} data={Route.useLoaderData()}>
|
|
10
|
+
<Outlet />
|
|
11
|
+
</ScreenFrame>
|
|
12
|
+
),
|
|
13
|
+
})
|
|
@@ -0,0 +1,99 @@
|
|
|
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
|
+
/**
|
|
10
|
+
* Puts the chosen theme on the document before the first paint, which React cannot do: it
|
|
11
|
+
* reads `localStorage` only after mount. Also reloads a page restored from the back-forward
|
|
12
|
+
* cache, so Back after signing out asks the server again instead of showing the signed-in page.
|
|
13
|
+
*/
|
|
14
|
+
const theme = `
|
|
15
|
+
(function() {
|
|
16
|
+
var storageKey = ${JSON.stringify(themeStorageKey)};
|
|
17
|
+
var defaultTheme = "system";
|
|
18
|
+
try {
|
|
19
|
+
var theme = localStorage.getItem(storageKey) || defaultTheme;
|
|
20
|
+
var systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
21
|
+
var root = document.documentElement;
|
|
22
|
+
root.classList.remove("light", "dark");
|
|
23
|
+
root.classList.add(theme === "system" ? systemTheme : theme);
|
|
24
|
+
} catch (e) {}
|
|
25
|
+
window.addEventListener("pageshow", function (event) {
|
|
26
|
+
if (event.persisted) location.reload();
|
|
27
|
+
});
|
|
28
|
+
})()
|
|
29
|
+
`
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* An unregistered request context is `never` in the router's types, so the shape is named
|
|
33
|
+
* here and read through a cast; `localeOf` turns it back into a declared locale.
|
|
34
|
+
*/
|
|
35
|
+
type FromRequest = { serverContext?: { locale?: unknown } }
|
|
36
|
+
|
|
37
|
+
export const Route = createRootRoute({
|
|
38
|
+
// On the client the locale is the `lang` the server wrote on `<html>`, so the hydrated tree matches
|
|
39
|
+
// the one sent. `localeOf` on both sides refuses a hand-edited `lang` or cookie.
|
|
40
|
+
beforeLoad: (ctx) => {
|
|
41
|
+
const locale =
|
|
42
|
+
typeof document === 'undefined'
|
|
43
|
+
? localeOf((ctx as unknown as FromRequest).serverContext?.locale as string | undefined)
|
|
44
|
+
: localeOf(document.documentElement.lang)
|
|
45
|
+
return { locale, dir: dirOf(locale), app: 'marketing.brand' }
|
|
46
|
+
},
|
|
47
|
+
head: ({ match }) => ({
|
|
48
|
+
meta: [
|
|
49
|
+
{ charSet: 'utf-8' },
|
|
50
|
+
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
|
51
|
+
// `_notFound` is the router's mark on the root match it draws the not-found for.
|
|
52
|
+
...documentHead(match.context, match._notFound ? (t) => t('admin.notFoundTitle', 'Page not found') : undefined).meta,
|
|
53
|
+
],
|
|
54
|
+
links: [{ rel: 'stylesheet', href: appCss }, { rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' }],
|
|
55
|
+
}),
|
|
56
|
+
// Without it the router draws a development-only placeholder and warns.
|
|
57
|
+
notFoundComponent: NotFound,
|
|
58
|
+
component: RootDocument,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
export const useCopy = () => {
|
|
62
|
+
const { dir, locale } = Route.useRouteContext()
|
|
63
|
+
return { dir, locale, t: translator(locale) }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function NotFound() {
|
|
67
|
+
const { t } = useCopy()
|
|
68
|
+
return (
|
|
69
|
+
<main className="flex min-h-screen flex-col items-center justify-center gap-4 px-4 text-center">
|
|
70
|
+
<h1 className="text-2xl font-bold tracking-tight">{t('admin.notFoundTitle', 'Page not found')}</h1>
|
|
71
|
+
<p className="text-muted-foreground text-sm">{t('admin.notFound', 'That page does not exist.')}</p>
|
|
72
|
+
{/* An anchor, because `/` is no route here. */}
|
|
73
|
+
<a className="text-foreground text-sm underline underline-offset-4" href="/">
|
|
74
|
+
{t('admin.goHome', 'Go to the home page')}
|
|
75
|
+
</a>
|
|
76
|
+
</main>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function RootDocument() {
|
|
81
|
+
const { dir, locale } = useCopy()
|
|
82
|
+
return (
|
|
83
|
+
<html lang={locale} dir={dir} suppressHydrationWarning>
|
|
84
|
+
<head>
|
|
85
|
+
<HeadContent />
|
|
86
|
+
{/* Safe to write raw: the string is this module's own and carries no value from a request. */}
|
|
87
|
+
<script dangerouslySetInnerHTML={{ __html: theme }} />
|
|
88
|
+
</head>
|
|
89
|
+
<body>
|
|
90
|
+
<DirectionProvider direction={dir}>
|
|
91
|
+
<ThemeProvider defaultTheme="system" storageKey={themeStorageKey}>
|
|
92
|
+
<Outlet />
|
|
93
|
+
</ThemeProvider>
|
|
94
|
+
</DirectionProvider>
|
|
95
|
+
<Scripts />
|
|
96
|
+
</body>
|
|
97
|
+
</html>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// `server` is a module augmentation `@tanstack/start-client-core` declares; the erased type-only
|
|
2
|
+
// import below loads it, so this app still ships `@tanstack/react-router` alone.
|
|
3
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
4
|
+
import type {} from '@tanstack/start-client-core'
|
|
5
|
+
import { asas } from 'asas'
|
|
6
|
+
|
|
7
|
+
// `/api/$` because one handler owns all of `/api`. The generated `routeTree.gen.ts` sits under
|
|
8
|
+
// `src/` so the root `tsc` sees the `FileRoutesByPath` augmentation that types this path.
|
|
9
|
+
export const Route = createFileRoute('/api/$')({
|
|
10
|
+
server: { handlers: { ANY: ({ request }) => asas.handler(request) } },
|
|
11
|
+
})
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import { Link, createFileRoute, useNavigate } from '@tanstack/react-router'
|
|
3
|
+
import { Button } from '@/components/ui/button'
|
|
4
|
+
import { authArm } from '../api.ts'
|
|
5
|
+
import { documentHead } from '../copy.ts'
|
|
6
|
+
import { AuthFrame, authGate, useHomeLabel, useRefusal } from '../auth-frame.tsx'
|
|
7
|
+
import { useCopy } from './__root.tsx'
|
|
8
|
+
import { landing } from '../redirect.ts'
|
|
9
|
+
|
|
10
|
+
// Accepting is a POST the person presses, so following a link alone joins nobody to anything.
|
|
11
|
+
// The provider decides whether the invitation is theirs.
|
|
12
|
+
|
|
13
|
+
export const Route = createFileRoute('/auth/accept-invitation')({
|
|
14
|
+
head: ({ match }) => documentHead(match.context, (t) => t('auth.invitation')),
|
|
15
|
+
validateSearch: (search: Record<string, unknown>): { id?: string } => (typeof search.id === 'string' && search.id ? { id: search.id } : {}),
|
|
16
|
+
beforeLoad: () => authGate(),
|
|
17
|
+
component: AcceptInvitation,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
function AcceptInvitation() {
|
|
21
|
+
const { t, locale } = useCopy()
|
|
22
|
+
const navigate = useNavigate()
|
|
23
|
+
const refused = useRefusal()
|
|
24
|
+
const { id } = Route.useSearch()
|
|
25
|
+
const { signedIn, home } = Route.useRouteContext()
|
|
26
|
+
const homeLabel = useHomeLabel()
|
|
27
|
+
const [organization, setOrganization] = useState<string>()
|
|
28
|
+
const [gone, setGone] = useState<string>()
|
|
29
|
+
const [pending, setPending] = useState(false)
|
|
30
|
+
const [refusal, setRefusal] = useState<string>()
|
|
31
|
+
const title = t('auth.invitation', 'Invitation')
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!signedIn || !id) return
|
|
35
|
+
fetch(`/api/auth/user/organization/get-invitation?id=${encodeURIComponent(id)}`)
|
|
36
|
+
.then(async (response) => {
|
|
37
|
+
const body = (await response.json().catch(() => null)) as { organizationName?: string; organizationNames?: Record<string, string>; code?: unknown } | null
|
|
38
|
+
if (response.ok) return setOrganization(body?.organizationNames?.[locale] || body?.organizationName || '')
|
|
39
|
+
// Better Auth answers a missing, spent or expired invitation 400 with no code.
|
|
40
|
+
setGone(refused({ status: response.status, body }, t('auth.invitationGone', 'This invitation has expired, was cancelled or was already accepted. Ask whoever invited you to send a new one.')))
|
|
41
|
+
})
|
|
42
|
+
.catch(() => setGone(t('auth.requestFailed', 'That request did not work.')))
|
|
43
|
+
}, [signedIn, id, locale])
|
|
44
|
+
|
|
45
|
+
const accept = async () => {
|
|
46
|
+
setPending(true)
|
|
47
|
+
setRefusal(undefined)
|
|
48
|
+
const answered = await authArm('organization/accept-invitation', { invitationId: id })
|
|
49
|
+
if (!answered.ok) {
|
|
50
|
+
setPending(false)
|
|
51
|
+
return setRefusal(refused(answered, t('auth.invitationAcceptFailed', 'That invitation could not be accepted.')))
|
|
52
|
+
}
|
|
53
|
+
// The provider made the organization active as it accepted, so the first screen is read as a member.
|
|
54
|
+
await navigate({ href: landing({ asked: undefined, screens: (await authGate()).screens }) })
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!id) return <AuthFrame title={title} description={t('auth.invitationMissing', 'This link names no invitation.')}>{null}</AuthFrame>
|
|
58
|
+
if (!signedIn) {
|
|
59
|
+
const here = `/auth/accept-invitation?id=${encodeURIComponent(id)}`
|
|
60
|
+
return (
|
|
61
|
+
<AuthFrame title={title} description={t('auth.invitationSignedOut', 'Sign in, or make an account with the email address the invitation was sent to, to accept it.')}>
|
|
62
|
+
<div className="grid gap-2">
|
|
63
|
+
<Button nativeButton={false} render={<Link to="/auth/sign-in" search={{ redirect: here }} />}>
|
|
64
|
+
{t('auth.signIn', 'Sign in')}
|
|
65
|
+
</Button>
|
|
66
|
+
<Button variant="outline" nativeButton={false} render={<Link to="/auth/sign-up" search={{ redirect: here }} />}>
|
|
67
|
+
{t('auth.signUp', 'Sign up')}
|
|
68
|
+
</Button>
|
|
69
|
+
</div>
|
|
70
|
+
</AuthFrame>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
if (gone) {
|
|
74
|
+
return (
|
|
75
|
+
<AuthFrame title={title}>
|
|
76
|
+
<div className="grid gap-4">
|
|
77
|
+
<p role="alert" data-invitation-gone="" className="text-sm">
|
|
78
|
+
{gone}
|
|
79
|
+
</p>
|
|
80
|
+
{/* A spent invitation is no dead end for someone already in: the way to their own first screen. An anchor,
|
|
81
|
+
because the path is the screen's own data, not a route the router knows. */}
|
|
82
|
+
<Button nativeButton={false} render={<a href={home.path} data-home="" />}>
|
|
83
|
+
{homeLabel(home, 'marketing.openScreen')}
|
|
84
|
+
</Button>
|
|
85
|
+
{/* The invitation may be another address's, which signing out and in again reaches. */}
|
|
86
|
+
<Button type="button" variant="outline" onClick={() => void authArm('sign-out').then(() => window.location.reload())}>
|
|
87
|
+
{t('admin.signOut', 'Sign out')}
|
|
88
|
+
</Button>
|
|
89
|
+
</div>
|
|
90
|
+
</AuthFrame>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
if (organization === undefined) return <AuthFrame title={title} description={t('auth.invitationReading', 'Reading the invitation…')}>{null}</AuthFrame>
|
|
94
|
+
return (
|
|
95
|
+
<AuthFrame title={title} description={t('auth.invitationJoin', 'You are invited to join {organization}.')!.replace('{organization}', () => organization)}>
|
|
96
|
+
<div className="grid gap-4">
|
|
97
|
+
{refusal ? (
|
|
98
|
+
<p className="text-destructive text-sm" role="alert">
|
|
99
|
+
{refusal}
|
|
100
|
+
</p>
|
|
101
|
+
) : null}
|
|
102
|
+
<Button type="button" data-button="accept-invitation" disabled={pending} onClick={() => void accept()}>
|
|
103
|
+
{pending ? t('auth.invitationAccepting', 'Accepting…') : t('auth.invitationAccept', 'Accept the invitation')}
|
|
104
|
+
</Button>
|
|
105
|
+
</div>
|
|
106
|
+
</AuthFrame>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
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 { firstRun } from '../first-run.ts'
|
|
8
|
+
import { useCopy } from './__root.tsx'
|
|
9
|
+
|
|
10
|
+
// The first owner of an app of one organization, and the organization itself: open once,
|
|
11
|
+
// to the holder of the setup code, and closed for good once the organization exists. The
|
|
12
|
+
// organization's name lives in code (`organizationName` in `organizations.ts`), so none is asked.
|
|
13
|
+
export const Route = createFileRoute('/auth/first-run')({
|
|
14
|
+
head: ({ match }) => documentHead(match.context, (t) => t('auth.firstRun')),
|
|
15
|
+
beforeLoad: async () => {
|
|
16
|
+
const state = await firstRun()
|
|
17
|
+
// `href`, not `to`: sign-in redirects here too, and two routes typed by each other's paths infer nothing.
|
|
18
|
+
if (!state.open) throw redirect({ href: '/auth/sign-in' })
|
|
19
|
+
return { code: state.code, password: (await authGate()).password }
|
|
20
|
+
},
|
|
21
|
+
component: FirstRun,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
function FirstRun() {
|
|
25
|
+
const navigate = useNavigate()
|
|
26
|
+
const { t } = useCopy()
|
|
27
|
+
const refused = useRefusal()
|
|
28
|
+
const { code: asked, password: bounds } = Route.useRouteContext()
|
|
29
|
+
const [name, setName] = useState('')
|
|
30
|
+
const [email, setEmail] = useState('')
|
|
31
|
+
const [password, setPassword] = useState('')
|
|
32
|
+
const [confirmation, setConfirmation] = useState('')
|
|
33
|
+
const [code, setCode] = useState('')
|
|
34
|
+
const [pending, setPending] = useState(false)
|
|
35
|
+
const [refusal, setRefusal] = useState<string | null>(null)
|
|
36
|
+
const [left, setLeft] = useState<ReadonlySet<string>>(() => new Set())
|
|
37
|
+
const required = t('auth.required', 'This field is required.')
|
|
38
|
+
const wrong = {
|
|
39
|
+
name: () => (name.trim() ? undefined : required),
|
|
40
|
+
email: () => (!email ? required : notAnEmail(email) ? t('auth.invalidEmail', 'Invalid email') : undefined),
|
|
41
|
+
password: () =>
|
|
42
|
+
!password ? required
|
|
43
|
+
: password.length < bounds.min ? t('auth.passwordTooShort', 'Password too short')
|
|
44
|
+
: password.length > bounds.max ? t('auth.passwordTooLong', 'Password too long')
|
|
45
|
+
: undefined,
|
|
46
|
+
confirmation: () => (!confirmation ? required : confirmation === password ? undefined : t('auth.passwordsDiffer', 'The two passwords are not the same.')),
|
|
47
|
+
code: () => (asked !== 'required' || code ? undefined : required),
|
|
48
|
+
}
|
|
49
|
+
const errorOf = (box: keyof typeof wrong) => (left.has(box) ? wrong[box]() : undefined)
|
|
50
|
+
const box = (id: keyof typeof wrong, label: string | undefined, props: ComponentProps<'input'> & { description?: string }) => (
|
|
51
|
+
<AuthBox id={id} label={label} error={errorOf(id)} onLeave={() => setLeft((already) => new Set(already).add(id))} {...props} />
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
const submit = async (event: React.FormEvent) => {
|
|
55
|
+
event.preventDefault()
|
|
56
|
+
setLeft(new Set(Object.keys(wrong)))
|
|
57
|
+
if (Object.values(wrong).some((check) => check())) return
|
|
58
|
+
setPending(true)
|
|
59
|
+
setRefusal(null)
|
|
60
|
+
const made = await authArm('first-run', { name, email, password, ...(asked === 'required' && { code }) })
|
|
61
|
+
setPending(false)
|
|
62
|
+
if (!made.ok) return setRefusal(refused(made, t('auth.firstRunFailed', 'That owner could not be made.')))
|
|
63
|
+
await navigate({ to: '/auth/sign-in' })
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (asked === 'unset') {
|
|
67
|
+
return (
|
|
68
|
+
<AuthFrame title={t('auth.firstRun', 'Set up this app')}>
|
|
69
|
+
<p className="text-sm" role="alert">
|
|
70
|
+
{t('auth.firstRunCodeUnset')}
|
|
71
|
+
</p>
|
|
72
|
+
</AuthFrame>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
return (
|
|
76
|
+
<AuthFrame title={t('auth.firstRun', 'Set up this app')} description={t('auth.firstRunDescription', 'Nobody owns this app yet. The account made here becomes its owner and first administrator.')}>
|
|
77
|
+
<form className="grid gap-4" noValidate onSubmit={submit}>
|
|
78
|
+
{box('name', t('auth.name', 'Name'), { autoComplete: 'name', onChange: (event) => setName(event.target.value), type: 'text', value: name })}
|
|
79
|
+
{box('email', t('auth.email', 'Email'), { autoComplete: 'email', onChange: (event) => setEmail(event.target.value), type: 'email', value: email })}
|
|
80
|
+
{box('password', t('auth.password', 'Password'), {
|
|
81
|
+
autoComplete: 'new-password',
|
|
82
|
+
description: t('auth.passwordRule', 'At least {count} characters.')!.replace('{count}', String(bounds.min)),
|
|
83
|
+
onChange: (event) => setPassword(event.target.value),
|
|
84
|
+
type: 'password',
|
|
85
|
+
value: password,
|
|
86
|
+
})}
|
|
87
|
+
{box('confirmation', t('auth.confirmPassword', 'Confirm the password'), {
|
|
88
|
+
autoComplete: 'new-password',
|
|
89
|
+
onChange: (event) => setConfirmation(event.target.value),
|
|
90
|
+
type: 'password',
|
|
91
|
+
value: confirmation,
|
|
92
|
+
})}
|
|
93
|
+
{asked === 'required'
|
|
94
|
+
? box('code', t('auth.setupCode', 'Setup code'), {
|
|
95
|
+
autoComplete: 'off',
|
|
96
|
+
description: t('auth.setupCodeDescription', 'The SETUP_CODE this server was started with.'),
|
|
97
|
+
onChange: (event) => setCode(event.target.value),
|
|
98
|
+
type: 'password',
|
|
99
|
+
value: code,
|
|
100
|
+
})
|
|
101
|
+
: null}
|
|
102
|
+
{/* `role="alert"` so a refusal is spoken when it appears; nothing moves the focus to it. */}
|
|
103
|
+
{refusal && (
|
|
104
|
+
<p className="text-destructive text-sm" role="alert">
|
|
105
|
+
{refusal}
|
|
106
|
+
</p>
|
|
107
|
+
)}
|
|
108
|
+
<Button disabled={pending} type="submit">
|
|
109
|
+
{pending ? t('auth.firstRunPending', 'Setting up…') : t('auth.firstRunSubmit', 'Set up the app')}
|
|
110
|
+
</Button>
|
|
111
|
+
</form>
|
|
112
|
+
</AuthFrame>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { Link, createFileRoute } from '@tanstack/react-router'
|
|
3
|
+
import { Button } from '@/components/ui/button'
|
|
4
|
+
import { authArm } from '../api.ts'
|
|
5
|
+
import { documentHead } from '../copy.ts'
|
|
6
|
+
import { AlreadySignedIn, AuthBox, AuthFrame, authGate, notAnEmail, redirectSearch, useRefusal } from '../auth-frame.tsx'
|
|
7
|
+
import { useCopy } from './__root.tsx'
|
|
8
|
+
|
|
9
|
+
// The sentence after a request is the same whether or not the address has an account, so
|
|
10
|
+
// nothing on this page says which it was.
|
|
11
|
+
|
|
12
|
+
export const Route = createFileRoute('/auth/forgot-password')({
|
|
13
|
+
head: ({ match }) => documentHead(match.context, (t) => t('auth.forgotPassword')),
|
|
14
|
+
validateSearch: redirectSearch,
|
|
15
|
+
beforeLoad: () => authGate(),
|
|
16
|
+
component: ForgotPassword,
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
function ForgotPassword() {
|
|
20
|
+
const { t } = useCopy()
|
|
21
|
+
const search = Route.useSearch()
|
|
22
|
+
const { signedIn, home, screens } = Route.useRouteContext()
|
|
23
|
+
const refused = useRefusal()
|
|
24
|
+
const [email, setEmail] = useState('')
|
|
25
|
+
const [left, setLeft] = useState(false)
|
|
26
|
+
const [pending, setPending] = useState(false)
|
|
27
|
+
const [sent, setSent] = useState(false)
|
|
28
|
+
const [refusal, setRefusal] = useState<string | null>(null)
|
|
29
|
+
const wrong = () => (!email ? t('auth.required', 'This field is required.') : notAnEmail(email) ? t('auth.invalidEmail', 'Invalid email') : undefined)
|
|
30
|
+
const back = (
|
|
31
|
+
<p className="mt-4 text-center text-sm">
|
|
32
|
+
<Link className="text-primary underline underline-offset-4" to="/auth/sign-in" search={{ redirect: search.redirect }}>
|
|
33
|
+
{t('auth.backToSignIn', 'Back to login')}
|
|
34
|
+
</Link>
|
|
35
|
+
</p>
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
const submit = async (event: React.FormEvent) => {
|
|
39
|
+
event.preventDefault()
|
|
40
|
+
setLeft(true)
|
|
41
|
+
if (wrong()) return
|
|
42
|
+
setPending(true)
|
|
43
|
+
setRefusal(null)
|
|
44
|
+
const answered = await authArm('request-password-reset', { email })
|
|
45
|
+
setPending(false)
|
|
46
|
+
if (!answered.ok) return setRefusal(refused(answered, t('auth.requestFailed', 'That request did not work.')))
|
|
47
|
+
setSent(true)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (signedIn) return <AlreadySignedIn redirect={search.redirect} home={home} screens={screens} />
|
|
51
|
+
if (sent) {
|
|
52
|
+
return (
|
|
53
|
+
<AuthFrame
|
|
54
|
+
title={t('auth.emailSent', 'Email sent')}
|
|
55
|
+
description={t('auth.checkYourEmail', "If the email address is associated with an account, you will receive instructions to reset your password shortly. Please check your spam or junk mail folder if you don't see the email in your inbox.")}
|
|
56
|
+
>
|
|
57
|
+
{import.meta.env?.DEV ? (
|
|
58
|
+
<p data-no-mailer="" className="text-muted-foreground rounded-md border px-3 py-2 text-sm">
|
|
59
|
+
{t('auth.noMailer', 'No mailer is configured, so no email was sent. In development the reset link is written to the server log.')}
|
|
60
|
+
</p>
|
|
61
|
+
) : null}
|
|
62
|
+
{back}
|
|
63
|
+
</AuthFrame>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
return (
|
|
67
|
+
<AuthFrame
|
|
68
|
+
title={t('auth.forgotPassword', 'Forgot password')}
|
|
69
|
+
description={t('auth.forgotPasswordInstructions', 'Please enter your email below. You will receive an email message with instructions on how to reset your password.')}
|
|
70
|
+
>
|
|
71
|
+
<form className="grid gap-4" noValidate onSubmit={submit}>
|
|
72
|
+
<AuthBox
|
|
73
|
+
autoComplete="email"
|
|
74
|
+
error={left ? wrong() : undefined}
|
|
75
|
+
id="email"
|
|
76
|
+
label={t('auth.email', 'Email')}
|
|
77
|
+
onChange={(event) => setEmail(event.target.value)}
|
|
78
|
+
onLeave={() => setLeft(true)}
|
|
79
|
+
type="email"
|
|
80
|
+
value={email}
|
|
81
|
+
/>
|
|
82
|
+
{refusal && (
|
|
83
|
+
<p className="text-destructive text-sm" role="alert">
|
|
84
|
+
{refusal}
|
|
85
|
+
</p>
|
|
86
|
+
)}
|
|
87
|
+
<Button disabled={pending} type="submit">
|
|
88
|
+
{t('auth.submit', 'Submit')}
|
|
89
|
+
</Button>
|
|
90
|
+
</form>
|
|
91
|
+
{back}
|
|
92
|
+
</AuthFrame>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Trades the backoffice's one-time impersonation token for the session through the plugin's own
|
|
2
|
+
// verify, then lands on the first screen the impersonated person may enter. The access report is
|
|
3
|
+
// read with only the session the verify set, so the landing grants nothing. A spent, expired or
|
|
4
|
+
// unknown token sets nothing and lands on sign-in.
|
|
5
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
6
|
+
import type {} from '@tanstack/start-client-core'
|
|
7
|
+
|
|
8
|
+
const firstScreen = (targets: unknown, listed: readonly string[], backoffice: string) => {
|
|
9
|
+
const screens = targets as Record<string, { capability?: string; client?: { path: string } } | undefined>
|
|
10
|
+
const name = listed.find((one) => one !== backoffice && screens[one]?.capability === 'screen')
|
|
11
|
+
return name ? screens[name]!.client!.path : '/unauthorized'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const Route = createFileRoute('/auth/handoff')({
|
|
15
|
+
server: {
|
|
16
|
+
handlers: {
|
|
17
|
+
GET: async ({ request }) => {
|
|
18
|
+
const { asas, tenantURL, BACKOFFICE_SCREEN } = await import('asas')
|
|
19
|
+
// The deployment's own origin, never the request's, whose host a caller writes.
|
|
20
|
+
const own = new URL(tenantURL).origin
|
|
21
|
+
const verified = await asas.handler(
|
|
22
|
+
new Request(new URL('/api/auth/user/one-time-token/verify', own), {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
headers: { 'content-type': 'application/json', origin: own },
|
|
25
|
+
body: JSON.stringify({ token: new URL(request.url).searchParams.get('token') ?? '' }),
|
|
26
|
+
}),
|
|
27
|
+
)
|
|
28
|
+
const cookies = verified.headers.getSetCookie()
|
|
29
|
+
const session = cookies.map((cookie) => cookie.split(';')[0]).join('; ')
|
|
30
|
+
const report = verified.ok
|
|
31
|
+
? ((await (await asas.handler(new Request(new URL('/api/access', own), { headers: { cookie: session } }))).json().catch(() => ({}))) as { screens?: string[] })
|
|
32
|
+
: undefined
|
|
33
|
+
const headers = new Headers({ location: report ? firstScreen(asas.targets, report.screens ?? [], BACKOFFICE_SCREEN) : '/auth/sign-in' })
|
|
34
|
+
for (const cookie of cookies) headers.append('set-cookie', cookie)
|
|
35
|
+
return new Response(null, { status: 302, headers })
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
import { documentHead } from '../copy.ts'
|
|
3
|
+
import { ChoosePassword, passwordGate, tokenSearch } from '../choose-password.tsx'
|
|
4
|
+
|
|
5
|
+
// Where a forgotten password's link lands. A first password, chosen from a sign-up's or an added person's link, has a
|
|
6
|
+
// page of its own, `/auth/set-password`, with the same form.
|
|
7
|
+
export const Route = createFileRoute('/auth/reset-password')({
|
|
8
|
+
head: ({ match }) => documentHead(match.context, (t) => t('auth.resetPassword')),
|
|
9
|
+
validateSearch: tokenSearch,
|
|
10
|
+
beforeLoad: ({ search }: { search: { token?: string } }) => passwordGate(search),
|
|
11
|
+
component: ResetPassword,
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
function ResetPassword() {
|
|
15
|
+
return <ChoosePassword {...Route.useSearch()} {...Route.useRouteContext()} first={false} />
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
import { documentHead } from '../copy.ts'
|
|
3
|
+
import { ChoosePassword, passwordGate, tokenSearch } from '../choose-password.tsx'
|
|
4
|
+
|
|
5
|
+
// Where a sign-up's or an added person's link lands: a brand-new account chooses its first password here, under a
|
|
6
|
+
// title that says so, and never reads that it is resetting one.
|
|
7
|
+
export const Route = createFileRoute('/auth/set-password')({
|
|
8
|
+
head: ({ match }) => documentHead(match.context, (t) => t('auth.setPassword')),
|
|
9
|
+
validateSearch: tokenSearch,
|
|
10
|
+
beforeLoad: ({ search }: { search: { token?: string } }) => passwordGate(search),
|
|
11
|
+
component: SetPassword,
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
function SetPassword() {
|
|
15
|
+
return <ChoosePassword {...Route.useSearch()} {...Route.useRouteContext()} first />
|
|
16
|
+
}
|