create-mithaq 0.0.0-stage → 0.1.1
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/payload.txt +22 -0
- package/LICENSES/ponytail.txt +21 -0
- package/index.js +127 -0
- package/package.json +19 -4
- package/template/apps/asas-platform/components.json +25 -0
- package/template/apps/asas-platform/gitignore +3 -0
- package/template/apps/asas-platform/package.json +55 -0
- package/template/apps/asas-platform/public/favicon.svg +1 -0
- package/template/apps/asas-platform/src/api.ts +147 -0
- package/template/apps/asas-platform/src/auth-frame.tsx +150 -0
- package/template/apps/asas-platform/src/components/mithaq/cells.tsx +112 -0
- package/template/apps/asas-platform/src/components/mithaq/controls.tsx +650 -0
- package/template/apps/asas-platform/src/components/mithaq/field-shell.tsx +53 -0
- package/template/apps/asas-platform/src/components/mithaq/filter-operators.ts +108 -0
- package/template/apps/asas-platform/src/components/mithaq/form.tsx +1467 -0
- package/template/apps/asas-platform/src/components/mithaq/list-columns.tsx +108 -0
- package/template/apps/asas-platform/src/components/mithaq/list-filters.tsx +368 -0
- package/template/apps/asas-platform/src/components/mithaq/list-pagination.tsx +153 -0
- package/template/apps/asas-platform/src/components/mithaq/list-query.ts +432 -0
- package/template/apps/asas-platform/src/components/mithaq/list-selection.tsx +74 -0
- package/template/apps/asas-platform/src/components/mithaq/list.tsx +865 -0
- package/template/apps/asas-platform/src/components/mithaq/nav.tsx +871 -0
- package/template/apps/asas-platform/src/components/mithaq/nodes.tsx +1024 -0
- package/template/apps/asas-platform/src/components/mithaq/relationship-cell.tsx +127 -0
- package/template/apps/asas-platform/src/components/mithaq/relationship-control.tsx +400 -0
- package/template/apps/asas-platform/src/components/mithaq/rich-text-nodes.ts +6 -0
- package/template/apps/asas-platform/src/components/mithaq/rich-text.tsx +1083 -0
- package/template/apps/asas-platform/src/components/mithaq/upload.tsx +686 -0
- package/template/apps/asas-platform/src/components/ui/accordion.tsx +74 -0
- package/template/apps/asas-platform/src/components/ui/alert-dialog.tsx +185 -0
- package/template/apps/asas-platform/src/components/ui/avatar.tsx +106 -0
- package/template/apps/asas-platform/src/components/ui/badge.tsx +48 -0
- package/template/apps/asas-platform/src/components/ui/breadcrumb.tsx +124 -0
- package/template/apps/asas-platform/src/components/ui/button-group.tsx +87 -0
- package/template/apps/asas-platform/src/components/ui/button.tsx +55 -0
- package/template/apps/asas-platform/src/components/ui/calendar.tsx +223 -0
- package/template/apps/asas-platform/src/components/ui/card.tsx +102 -0
- package/template/apps/asas-platform/src/components/ui/chart.tsx +371 -0
- package/template/apps/asas-platform/src/components/ui/checkbox.tsx +28 -0
- package/template/apps/asas-platform/src/components/ui/collapsible.tsx +19 -0
- package/template/apps/asas-platform/src/components/ui/combobox.tsx +308 -0
- package/template/apps/asas-platform/src/components/ui/dialog.tsx +158 -0
- package/template/apps/asas-platform/src/components/ui/direction.tsx +6 -0
- package/template/apps/asas-platform/src/components/ui/directional-icon.tsx +20 -0
- package/template/apps/asas-platform/src/components/ui/drawer.tsx +216 -0
- package/template/apps/asas-platform/src/components/ui/dropdown-menu.tsx +267 -0
- package/template/apps/asas-platform/src/components/ui/field.tsx +236 -0
- package/template/apps/asas-platform/src/components/ui/input-group.tsx +155 -0
- package/template/apps/asas-platform/src/components/ui/input-otp.tsx +84 -0
- package/template/apps/asas-platform/src/components/ui/input.tsx +19 -0
- package/template/apps/asas-platform/src/components/ui/label.tsx +19 -0
- package/template/apps/asas-platform/src/components/ui/popover.tsx +90 -0
- package/template/apps/asas-platform/src/components/ui/radio-group.tsx +37 -0
- package/template/apps/asas-platform/src/components/ui/resizable.tsx +47 -0
- package/template/apps/asas-platform/src/components/ui/scroll-area.tsx +54 -0
- package/template/apps/asas-platform/src/components/ui/select.tsx +204 -0
- package/template/apps/asas-platform/src/components/ui/separator.tsx +24 -0
- package/template/apps/asas-platform/src/components/ui/sheet.tsx +139 -0
- package/template/apps/asas-platform/src/components/ui/sidebar.tsx +714 -0
- package/template/apps/asas-platform/src/components/ui/skeleton.tsx +13 -0
- package/template/apps/asas-platform/src/components/ui/slider.tsx +55 -0
- package/template/apps/asas-platform/src/components/ui/spinner.tsx +10 -0
- package/template/apps/asas-platform/src/components/ui/switch.tsx +29 -0
- package/template/apps/asas-platform/src/components/ui/table.tsx +115 -0
- package/template/apps/asas-platform/src/components/ui/tabs.tsx +79 -0
- package/template/apps/asas-platform/src/components/ui/textarea.tsx +17 -0
- package/template/apps/asas-platform/src/components/ui/toggle-group.tsx +89 -0
- package/template/apps/asas-platform/src/components/ui/toggle.tsx +44 -0
- package/template/apps/asas-platform/src/components/ui/tooltip.tsx +63 -0
- package/template/apps/asas-platform/src/copy.ts +1 -0
- package/template/apps/asas-platform/src/hooks/use-mobile.ts +19 -0
- package/template/apps/asas-platform/src/lib/utils.ts +1 -0
- package/template/apps/asas-platform/src/links.ts +38 -0
- package/template/apps/asas-platform/src/locale-switch.tsx +26 -0
- package/template/apps/asas-platform/src/redirect.ts +1 -0
- package/template/apps/asas-platform/src/request-headers.ts +11 -0
- package/template/apps/asas-platform/src/routeTree.gen.ts +181 -0
- package/template/apps/asas-platform/src/router.tsx +7 -0
- package/template/apps/asas-platform/src/routes/(screens)/platform/files.ts +4 -0
- package/template/apps/asas-platform/src/routes/(screens)/platform/screen.json +3609 -0
- package/template/apps/asas-platform/src/routes/(screens)/platform/types.ts +145 -0
- package/template/apps/asas-platform/src/routes/(screens)/platform.$.tsx +17 -0
- package/template/apps/asas-platform/src/routes/(screens)/platform.index.tsx +10 -0
- package/template/apps/asas-platform/src/routes/(screens)/platform.tsx +13 -0
- package/template/apps/asas-platform/src/routes/__root.tsx +95 -0
- package/template/apps/asas-platform/src/routes/api/$.ts +30 -0
- package/template/apps/asas-platform/src/routes/auth.sign-in.tsx +72 -0
- package/template/apps/asas-platform/src/routes/unauthorized.tsx +122 -0
- package/template/apps/asas-platform/src/screen-form.tsx +34 -0
- package/template/apps/asas-platform/src/screen-list.tsx +139 -0
- package/template/apps/asas-platform/src/screen-names.ts +10 -0
- package/template/apps/asas-platform/src/screens.tsx +868 -0
- package/template/apps/asas-platform/src/server.ts +20 -0
- package/template/apps/asas-platform/src/slide.ts +22 -0
- package/template/apps/asas-platform/src/start.ts +42 -0
- package/template/apps/asas-platform/src/styles.css +200 -0
- package/template/apps/asas-platform/src/theme.ts +2 -0
- package/template/apps/asas-platform/tsconfig.json +19 -0
- package/template/apps/asas-platform/vite.config.ts +26 -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 +56 -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/components/mithaq/cells.tsx +112 -0
- package/template/apps/asas-tanstack/src/components/mithaq/controls.tsx +650 -0
- package/template/apps/asas-tanstack/src/components/mithaq/field-shell.tsx +53 -0
- package/template/apps/asas-tanstack/src/components/mithaq/filter-operators.ts +108 -0
- package/template/apps/asas-tanstack/src/components/mithaq/form.tsx +1467 -0
- package/template/apps/asas-tanstack/src/components/mithaq/list-columns.tsx +108 -0
- package/template/apps/asas-tanstack/src/components/mithaq/list-filters.tsx +368 -0
- package/template/apps/asas-tanstack/src/components/mithaq/list-pagination.tsx +153 -0
- package/template/apps/asas-tanstack/src/components/mithaq/list-query.ts +432 -0
- package/template/apps/asas-tanstack/src/components/mithaq/list-selection.tsx +74 -0
- package/template/apps/asas-tanstack/src/components/mithaq/list.tsx +865 -0
- package/template/apps/asas-tanstack/src/components/mithaq/nav.tsx +871 -0
- package/template/apps/asas-tanstack/src/components/mithaq/nodes.tsx +1024 -0
- package/template/apps/asas-tanstack/src/components/mithaq/relationship-cell.tsx +127 -0
- package/template/apps/asas-tanstack/src/components/mithaq/relationship-control.tsx +400 -0
- package/template/apps/asas-tanstack/src/components/mithaq/rich-text-nodes.ts +6 -0
- package/template/apps/asas-tanstack/src/components/mithaq/rich-text.tsx +1083 -0
- package/template/apps/asas-tanstack/src/components/mithaq/upload.tsx +686 -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/drawer.tsx +215 -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/copy.ts +870 -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 +26 -0
- package/template/apps/asas-tanstack/src/quiet-abort.ts +25 -0
- package/template/apps/asas-tanstack/src/redirect.ts +20 -0
- package/template/apps/asas-tanstack/src/request-headers.ts +11 -0
- package/template/apps/asas-tanstack/src/routeTree.gen.ts +382 -0
- package/template/apps/asas-tanstack/src/router.tsx +7 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/files.ts +4 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/screen.json +3883 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/types.ts +158 -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 +4 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/portal/screen.json +1800 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/portal/types.ts +92 -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 +101 -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 +109 -0
- package/template/apps/asas-tanstack/src/routes/auth.sign-in.tsx +86 -0
- package/template/apps/asas-tanstack/src/routes/auth.sign-up.tsx +122 -0
- package/template/apps/asas-tanstack/src/routes/index.tsx +142 -0
- package/template/apps/asas-tanstack/src/routes/unauthorized.tsx +122 -0
- package/template/apps/asas-tanstack/src/screen-form.tsx +34 -0
- package/template/apps/asas-tanstack/src/screen-list.tsx +139 -0
- package/template/apps/asas-tanstack/src/screen-names.ts +10 -0
- package/template/apps/asas-tanstack/src/screens.tsx +868 -0
- package/template/apps/asas-tanstack/src/server.ts +16 -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 +159 -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 +28 -0
- package/template/package.json +16 -0
- package/template/templates/asas/.env.example +16 -0
- package/template/templates/asas/components.json +22 -0
- package/template/templates/asas/mithaq.hints.json +25 -0
- package/template/templates/asas/package.json +38 -0
- package/template/templates/asas/src/asas.ts +93 -0
- package/template/templates/asas/src/audit-log/audit-log.ts +187 -0
- package/template/templates/asas/src/audit-log/sink.ts +89 -0
- package/template/templates/asas/src/audit-log/ui/activity.tsx +243 -0
- package/template/templates/asas/src/courses/courses.ts +612 -0
- package/template/templates/asas/src/courses/ui/archive-button.tsx +25 -0
- package/template/templates/asas/src/courses/ui/fee-cell.tsx +19 -0
- package/template/templates/asas/src/courses/ui/no-courses-on-offer.tsx +11 -0
- package/template/templates/asas/src/courses/ui/no-courses.tsx +13 -0
- package/template/templates/asas/src/courses/ui/on-offer.ts +6 -0
- package/template/templates/asas/src/courses/ui/status-cell.tsx +31 -0
- package/template/templates/asas/src/enrollments/enrollments.ts +137 -0
- package/template/templates/asas/src/enrollments/ui/session-cell.tsx +65 -0
- package/template/templates/asas/src/kayan/auth/user/account.access.ts +8 -0
- package/template/templates/asas/src/kayan/auth/user/account.ts +31 -0
- package/template/templates/asas/src/kayan/auth/user/index.ts +18 -0
- package/template/templates/asas/src/kayan/auth/user/invitation.access.ts +4 -0
- package/template/templates/asas/src/kayan/auth/user/invitation.ts +27 -0
- package/template/templates/asas/src/kayan/auth/user/member.access.ts +10 -0
- package/template/templates/asas/src/kayan/auth/user/member.screens.ts +37 -0
- package/template/templates/asas/src/kayan/auth/user/member.ts +25 -0
- package/template/templates/asas/src/kayan/auth/user/organization.access.ts +20 -0
- package/template/templates/asas/src/kayan/auth/user/organization.screens.ts +37 -0
- package/template/templates/asas/src/kayan/auth/user/organization.ts +24 -0
- package/template/templates/asas/src/kayan/auth/user/organizationRole.access.ts +8 -0
- package/template/templates/asas/src/kayan/auth/user/organizationRole.ts +25 -0
- package/template/templates/asas/src/kayan/auth/user/session.access.ts +8 -0
- package/template/templates/asas/src/kayan/auth/user/session.ts +28 -0
- package/template/templates/asas/src/kayan/auth/user/user.access.ts +29 -0
- package/template/templates/asas/src/kayan/auth/user/user.screens.ts +45 -0
- package/template/templates/asas/src/kayan/auth/user/user.ts +31 -0
- package/template/templates/asas/src/kayan/auth/user/verification.access.ts +8 -0
- package/template/templates/asas/src/kayan/auth/user/verification.ts +23 -0
- package/template/templates/asas/src/local-users/local-users.ts +143 -0
- package/template/templates/asas/src/media/media.ts +137 -0
- package/template/templates/asas/src/media/ui/preview.tsx +143 -0
- package/template/templates/asas/src/member-roles/member-roles.ts +207 -0
- package/template/templates/asas/src/messages/account.json +106 -0
- package/template/templates/asas/src/messages/auditLog.json +186 -0
- package/template/templates/asas/src/messages/courses.json +270 -0
- package/template/templates/asas/src/messages/enrollments.json +42 -0
- package/template/templates/asas/src/messages/invitation.json +66 -0
- package/template/templates/asas/src/messages/localUsers.json +42 -0
- package/template/templates/asas/src/messages/media.json +66 -0
- package/template/templates/asas/src/messages/member.json +42 -0
- package/template/templates/asas/src/messages/memberRoles.json +42 -0
- package/template/templates/asas/src/messages/mithaq.json +170 -0
- package/template/templates/asas/src/messages/organization.json +50 -0
- package/template/templates/asas/src/messages/organizationRole.json +50 -0
- package/template/templates/asas/src/messages/roles.json +58 -0
- package/template/templates/asas/src/messages/session.json +82 -0
- package/template/templates/asas/src/messages/sessions.json +106 -0
- package/template/templates/asas/src/messages/settings.json +66 -0
- package/template/templates/asas/src/messages/tags.json +38 -0
- package/template/templates/asas/src/messages/user.json +90 -0
- package/template/templates/asas/src/messages/verification.json +50 -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/drizzle.config.ts +11 -0
- package/template/templates/asas/src/migrations/schema.ts +29 -0
- package/template/templates/asas/src/mithaq.ts +406 -0
- package/template/templates/asas/src/pages.ts +74 -0
- package/template/templates/asas/src/platform-http.ts +96 -0
- package/template/templates/asas/src/rich-text-nodes.ts +11 -0
- package/template/templates/asas/src/roles/grants.ts +47 -0
- package/template/templates/asas/src/roles/roles.ts +184 -0
- package/template/templates/asas/src/roles/ui/role-form.tsx +226 -0
- package/template/templates/asas/src/roles/ui/roles.tsx +166 -0
- package/template/templates/asas/src/roles.ts +16 -0
- package/template/templates/asas/src/seed.ts +325 -0
- package/template/templates/asas/src/sessions/sessions.ts +155 -0
- package/template/templates/asas/src/settings/settings.ts +69 -0
- package/template/templates/asas/src/statements.ts +11 -0
- package/template/templates/asas/src/steps/expected-updated-at.ts +41 -0
- package/template/templates/asas/src/storage/defaults.ts +156 -0
- package/template/templates/asas/src/storage/index.ts +95 -0
- package/template/templates/asas/src/storage/local.ts +143 -0
- package/template/templates/asas/src/storage/r2.ts +5 -0
- package/template/templates/asas/src/storage/s3.ts +91 -0
- package/template/templates/asas/src/storage/uploadthing.ts +59 -0
- package/template/templates/asas/src/tags/tags.ts +80 -0
- package/template/templates/asas/src/tenant-access.ts +221 -0
- package/template/templates/asas/src/tenant-roles.ts +262 -0
- package/template/templates/asas/src/ui/app-shell.tsx +260 -0
- package/template/templates/asas/src/ui/kpi-card.tsx +25 -0
- package/template/templates/asas/src/ui/record-page.tsx +50 -0
- package/template/templates/asas/src/ui/rich-text-nodes.tsx +15 -0
- package/template/templates/asas/src/ui/screen-nav.tsx +28 -0
- package/template/templates/asas/src/ui/session-labels.tsx +36 -0
- package/template/templates/asas/src/ui/table-page.tsx +9 -0
- package/template/templates/asas/src/ui/top-shell.tsx +60 -0
- package/template/templates/asas/src/ui-dashboard/dashboard.tsx +294 -0
- package/template/templates/asas/src/ui-dashboard/members.tsx +630 -0
- package/template/templates/asas/src/ui-dashboard/theme-provider.tsx +10 -0
- package/template/templates/asas/src/ui-dashboard/versions.tsx +432 -0
- package/template/templates/asas/src/ui-platform/home.tsx +63 -0
- package/template/templates/asas/src/ui-platform/organization-actions.tsx +114 -0
- package/template/templates/asas/src/ui-platform/record-page.tsx +30 -0
- package/template/templates/asas/src/ui-platform/role-cells.tsx +52 -0
- package/template/templates/asas/src/ui-platform/user-actions.tsx +251 -0
- package/template/templates/asas/src/ui-portal/home.tsx +77 -0
- package/template/templates/asas/src/ui-portal/page.tsx +11 -0
- package/template/templates/auth/package.json +14 -0
- package/template/templates/auth/src/index.ts +690 -0
- package/template/templates/auth/src/platform-roles.ts +87 -0
- package/README.md +0 -3
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
}
|
|
28
|
+
export type EnrollmentsValues = {
|
|
29
|
+
id: string
|
|
30
|
+
updatedAt: Date
|
|
31
|
+
createdAt: Date
|
|
32
|
+
organization: unknown
|
|
33
|
+
session: string
|
|
34
|
+
student: string
|
|
35
|
+
deletedAt: Date | null
|
|
36
|
+
}
|
|
37
|
+
export type SessionsValues = {
|
|
38
|
+
id: string
|
|
39
|
+
updatedAt: Date
|
|
40
|
+
createdAt: Date
|
|
41
|
+
organization: unknown
|
|
42
|
+
course: string
|
|
43
|
+
startsAt: Date
|
|
44
|
+
capacity: number | null
|
|
45
|
+
venue: 'online' | 'campus' | null
|
|
46
|
+
format: 'workshop' | 'lecture' | null
|
|
47
|
+
archived: boolean | null
|
|
48
|
+
details: { root: Record<string, unknown> } | null
|
|
49
|
+
enrollments: Values['enrollments'][]
|
|
50
|
+
}
|
|
51
|
+
export type MediaValues = {
|
|
52
|
+
id: string
|
|
53
|
+
updatedAt: Date
|
|
54
|
+
createdAt: Date
|
|
55
|
+
organization: unknown
|
|
56
|
+
filename: string
|
|
57
|
+
mimeType: string
|
|
58
|
+
filesize: number
|
|
59
|
+
width: number | null
|
|
60
|
+
height: number | null
|
|
61
|
+
key: string
|
|
62
|
+
storage: string
|
|
63
|
+
alt: Record<'ar' | 'en', string>
|
|
64
|
+
}
|
|
65
|
+
export type UserValues = {
|
|
66
|
+
id: string
|
|
67
|
+
updatedAt: Date
|
|
68
|
+
createdAt: Date
|
|
69
|
+
name: string
|
|
70
|
+
email: string
|
|
71
|
+
emailVerified: boolean
|
|
72
|
+
image: string | null
|
|
73
|
+
role: string | null
|
|
74
|
+
banned: boolean | null
|
|
75
|
+
banReason: string | null
|
|
76
|
+
banExpires: Date | null
|
|
77
|
+
}
|
|
78
|
+
export type Values = {
|
|
79
|
+
courses: CoursesValues
|
|
80
|
+
enrollments: EnrollmentsValues
|
|
81
|
+
sessions: SessionsValues
|
|
82
|
+
media: MediaValues
|
|
83
|
+
user: UserValues
|
|
84
|
+
}
|
|
85
|
+
export type Pages = {
|
|
86
|
+
courses: 'list' | 'edit'
|
|
87
|
+
enrollments: 'list' | 'create' | 'edit'
|
|
88
|
+
sessions: never
|
|
89
|
+
media: never
|
|
90
|
+
user: never
|
|
91
|
+
}
|
|
92
|
+
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 '../components/mithaq/nav.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,101 @@
|
|
|
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, useRefusal } from '../auth-frame.tsx'
|
|
7
|
+
import { useCopy } from './__root.tsx'
|
|
8
|
+
|
|
9
|
+
// Accepting is a POST the person presses, so following a link alone joins nobody to anything.
|
|
10
|
+
// The provider decides whether the invitation is theirs.
|
|
11
|
+
|
|
12
|
+
export const Route = createFileRoute('/auth/accept-invitation')({
|
|
13
|
+
head: ({ match }) => documentHead(match.context, (t) => t('auth.invitation')),
|
|
14
|
+
validateSearch: (search: Record<string, unknown>): { id?: string } => (typeof search.id === 'string' && search.id ? { id: search.id } : {}),
|
|
15
|
+
beforeLoad: () => authGate(),
|
|
16
|
+
component: AcceptInvitation,
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
function AcceptInvitation() {
|
|
20
|
+
const { t } = useCopy()
|
|
21
|
+
const navigate = useNavigate()
|
|
22
|
+
const refused = useRefusal()
|
|
23
|
+
const { id } = Route.useSearch()
|
|
24
|
+
const { signedIn } = Route.useRouteContext()
|
|
25
|
+
const [organization, setOrganization] = useState<string>()
|
|
26
|
+
const [gone, setGone] = useState<string>()
|
|
27
|
+
const [pending, setPending] = useState(false)
|
|
28
|
+
const [refusal, setRefusal] = useState<string>()
|
|
29
|
+
const title = t('auth.invitation', 'Invitation')
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!signedIn || !id) return
|
|
33
|
+
fetch(`/api/auth/user/organization/get-invitation?id=${encodeURIComponent(id)}`)
|
|
34
|
+
.then(async (response) => {
|
|
35
|
+
const body = (await response.json().catch(() => null)) as { organizationName?: string; code?: unknown } | null
|
|
36
|
+
if (response.ok) return setOrganization(body?.organizationName ?? '')
|
|
37
|
+
// Better Auth answers a missing, spent or expired invitation 400 with no code.
|
|
38
|
+
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.')))
|
|
39
|
+
})
|
|
40
|
+
.catch(() => setGone(t('auth.requestFailed', 'That request did not work.')))
|
|
41
|
+
}, [signedIn, id])
|
|
42
|
+
|
|
43
|
+
const accept = async () => {
|
|
44
|
+
setPending(true)
|
|
45
|
+
setRefusal(undefined)
|
|
46
|
+
const answered = await authArm('organization/accept-invitation', { invitationId: id })
|
|
47
|
+
if (!answered.ok) {
|
|
48
|
+
setPending(false)
|
|
49
|
+
return setRefusal(refused(answered, t('auth.invitationAcceptFailed', 'That invitation could not be accepted.')))
|
|
50
|
+
}
|
|
51
|
+
// The provider made the organization active as it accepted, so the first screen is read as a member.
|
|
52
|
+
await navigate({ to: (await authGate()).home.path })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!id) return <AuthFrame title={title} description={t('auth.invitationMissing', 'This link names no invitation.')}>{null}</AuthFrame>
|
|
56
|
+
if (!signedIn) {
|
|
57
|
+
const here = `/auth/accept-invitation?id=${encodeURIComponent(id)}`
|
|
58
|
+
return (
|
|
59
|
+
<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.')}>
|
|
60
|
+
<div className="grid gap-2">
|
|
61
|
+
<Button nativeButton={false} render={<Link to="/auth/sign-in" search={{ redirect: here }} />}>
|
|
62
|
+
{t('auth.signIn', 'Sign in')}
|
|
63
|
+
</Button>
|
|
64
|
+
<Button variant="outline" nativeButton={false} render={<Link to="/auth/sign-up" search={{ redirect: here }} />}>
|
|
65
|
+
{t('auth.signUp', 'Sign up')}
|
|
66
|
+
</Button>
|
|
67
|
+
</div>
|
|
68
|
+
</AuthFrame>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
if (gone) {
|
|
72
|
+
return (
|
|
73
|
+
<AuthFrame title={title}>
|
|
74
|
+
<div className="grid gap-4">
|
|
75
|
+
<p role="alert" data-invitation-gone="" className="text-sm">
|
|
76
|
+
{gone}
|
|
77
|
+
</p>
|
|
78
|
+
{/* The invitation may be another address's, which signing out and in again reaches. */}
|
|
79
|
+
<Button type="button" variant="outline" onClick={() => void authArm('sign-out').then(() => window.location.reload())}>
|
|
80
|
+
{t('admin.signOut', 'Sign out')}
|
|
81
|
+
</Button>
|
|
82
|
+
</div>
|
|
83
|
+
</AuthFrame>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
if (organization === undefined) return <AuthFrame title={title} description={t('auth.invitationReading', 'Reading the invitation…')}>{null}</AuthFrame>
|
|
87
|
+
return (
|
|
88
|
+
<AuthFrame title={title} description={t('auth.invitationJoin', 'You are invited to join {organization}.')!.replace('{organization}', () => organization)}>
|
|
89
|
+
<div className="grid gap-4">
|
|
90
|
+
{refusal ? (
|
|
91
|
+
<p className="text-destructive text-sm" role="alert">
|
|
92
|
+
{refusal}
|
|
93
|
+
</p>
|
|
94
|
+
) : null}
|
|
95
|
+
<Button type="button" data-button="accept-invitation" disabled={pending} onClick={() => void accept()}>
|
|
96
|
+
{pending ? t('auth.invitationAccepting', 'Accepting…') : t('auth.invitationAccept', 'Accept the invitation')}
|
|
97
|
+
</Button>
|
|
98
|
+
</div>
|
|
99
|
+
</AuthFrame>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
@@ -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 } = 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} />
|
|
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 platform'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[], platform: string) => {
|
|
9
|
+
const screens = targets as Record<string, { capability?: string; client?: { path: string } } | undefined>
|
|
10
|
+
const name = listed.find((one) => one !== platform && 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, PLATFORM_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 ?? [], PLATFORM_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,109 @@
|
|
|
1
|
+
import { useState, type ComponentProps } 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, redirectSearch, useRefusal } from '../auth-frame.tsx'
|
|
7
|
+
import { useCopy } from './__root.tsx'
|
|
8
|
+
|
|
9
|
+
export const Route = createFileRoute('/auth/reset-password')({
|
|
10
|
+
head: ({ match }) => documentHead(match.context, (t) => t('auth.resetPassword')),
|
|
11
|
+
validateSearch: (search: Record<string, unknown>): { redirect?: string; token?: string } => ({
|
|
12
|
+
...redirectSearch(search),
|
|
13
|
+
...(typeof search.token === 'string' && search.token ? { token: search.token } : {}),
|
|
14
|
+
}),
|
|
15
|
+
beforeLoad: () => authGate(),
|
|
16
|
+
component: ResetPassword,
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
function ResetPassword() {
|
|
20
|
+
const { t } = useCopy()
|
|
21
|
+
const { token, redirect } = Route.useSearch()
|
|
22
|
+
const { password: bounds, signedIn, home } = Route.useRouteContext()
|
|
23
|
+
const refused = useRefusal()
|
|
24
|
+
const [password, setPassword] = useState('')
|
|
25
|
+
const [confirmation, setConfirmation] = useState('')
|
|
26
|
+
const [pending, setPending] = useState(false)
|
|
27
|
+
const [done, setDone] = useState(false)
|
|
28
|
+
const [refusal, setRefusal] = useState<string | null>(null)
|
|
29
|
+
const [left, setLeft] = useState<ReadonlySet<string>>(() => new Set())
|
|
30
|
+
const wrong = {
|
|
31
|
+
password: () =>
|
|
32
|
+
!password ? t('auth.required', 'This field is required.')
|
|
33
|
+
: password.length < bounds.min ? t('auth.passwordTooShort', 'Password too short')
|
|
34
|
+
: password.length > bounds.max ? t('auth.passwordTooLong', 'Password too long')
|
|
35
|
+
: undefined,
|
|
36
|
+
confirmation: () => (!confirmation ? t('auth.required', 'This field is required.') : confirmation === password ? undefined : t('auth.passwordsDiffer', 'The two passwords are not the same.')),
|
|
37
|
+
}
|
|
38
|
+
const errorOf = (box: keyof typeof wrong) => (left.has(box) ? wrong[box]() : undefined)
|
|
39
|
+
const box = (id: keyof typeof wrong, label: string | undefined, props: ComponentProps<'input'> & { description?: string }) => (
|
|
40
|
+
<AuthBox id={id} label={label} error={errorOf(id)} onLeave={() => setLeft((already) => new Set(already).add(id))} {...props} />
|
|
41
|
+
)
|
|
42
|
+
const signIn = (
|
|
43
|
+
<p className="mt-4 text-center text-sm">
|
|
44
|
+
<Link className="text-primary underline underline-offset-4" to="/auth/sign-in">
|
|
45
|
+
{t('auth.signIn', 'Sign in')}
|
|
46
|
+
</Link>
|
|
47
|
+
</p>
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
const submit = async (event: React.FormEvent) => {
|
|
51
|
+
event.preventDefault()
|
|
52
|
+
setLeft(new Set(Object.keys(wrong)))
|
|
53
|
+
if (Object.values(wrong).some((check) => check())) return
|
|
54
|
+
setPending(true)
|
|
55
|
+
setRefusal(null)
|
|
56
|
+
const answered = await authArm('reset-password', { newPassword: password, token })
|
|
57
|
+
setPending(false)
|
|
58
|
+
if (!answered.ok) return setRefusal(refused(answered, t('auth.resetFailed', 'That reset did not work.')))
|
|
59
|
+
setDone(true)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (signedIn) return <AlreadySignedIn redirect={redirect} home={home} />
|
|
63
|
+
if (!token) {
|
|
64
|
+
return (
|
|
65
|
+
<AuthFrame title={t('auth.resetPassword', 'Reset password')} description={t('auth.tokenInvalid', 'Token is either invalid or has expired.')}>
|
|
66
|
+
<p className="text-center text-sm">
|
|
67
|
+
<Link className="text-primary underline underline-offset-4" to="/auth/forgot-password">
|
|
68
|
+
{t('auth.forgotPassword', 'Forgot password')}
|
|
69
|
+
</Link>
|
|
70
|
+
</p>
|
|
71
|
+
</AuthFrame>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
if (done) {
|
|
75
|
+
return (
|
|
76
|
+
<AuthFrame title={t('auth.resetPassword', 'Reset password')} description={t('auth.passwordReset', 'Password reset successfully.')}>
|
|
77
|
+
{signIn}
|
|
78
|
+
</AuthFrame>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
return (
|
|
82
|
+
<AuthFrame title={t('auth.resetPassword', 'Reset password')}>
|
|
83
|
+
<form className="grid gap-4" noValidate onSubmit={submit}>
|
|
84
|
+
{box('password', t('auth.newPassword', 'New password'), {
|
|
85
|
+
autoComplete: 'new-password',
|
|
86
|
+
description: t('auth.passwordRule', 'At least {count} characters.')!.replace('{count}', String(bounds.min)),
|
|
87
|
+
onChange: (event) => setPassword(event.target.value),
|
|
88
|
+
type: 'password',
|
|
89
|
+
value: password,
|
|
90
|
+
})}
|
|
91
|
+
{box('confirmation', t('auth.confirmPassword', 'Confirm the password'), {
|
|
92
|
+
autoComplete: 'new-password',
|
|
93
|
+
onChange: (event) => setConfirmation(event.target.value),
|
|
94
|
+
type: 'password',
|
|
95
|
+
value: confirmation,
|
|
96
|
+
})}
|
|
97
|
+
{refusal && (
|
|
98
|
+
<p className="text-destructive text-sm" role="alert">
|
|
99
|
+
{refusal}
|
|
100
|
+
</p>
|
|
101
|
+
)}
|
|
102
|
+
<Button disabled={pending} type="submit">
|
|
103
|
+
{t('auth.resetPassword', 'Reset password')}
|
|
104
|
+
</Button>
|
|
105
|
+
</form>
|
|
106
|
+
{signIn}
|
|
107
|
+
</AuthFrame>
|
|
108
|
+
)
|
|
109
|
+
}
|