create-mithaq 0.1.1 → 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/LICENSES/openstatus-time-picker.txt +21 -0
- package/index.js +457 -63
- package/package.json +1 -1
- package/template/apps/{asas-platform → asas-backoffice}/package.json +5 -3
- package/template/apps/{asas-platform → asas-backoffice}/src/api.ts +1 -1
- package/template/apps/{asas-platform → asas-backoffice}/src/auth-frame.tsx +12 -12
- package/template/apps/asas-backoffice/src/components.ts +35 -0
- package/template/apps/asas-backoffice/src/first-user.ts +14 -0
- package/template/apps/{asas-platform → asas-backoffice}/src/links.ts +1 -1
- package/template/apps/{asas-platform → asas-backoffice}/src/locale-switch.tsx +7 -5
- 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-platform → asas-backoffice}/src/request-headers.ts +1 -1
- 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-platform/src/routes/(screens)/platform → asas-backoffice/src/routes/(screens)/backoffice}/screen.json +426 -64
- package/template/apps/{asas-platform/src/routes/(screens)/platform → asas-backoffice/src/routes/(screens)/backoffice}/types.ts +18 -0
- package/template/apps/{asas-platform/src/routes/(screens)/platform.$.tsx → asas-backoffice/src/routes/(screens)/backoffice.$.tsx} +4 -4
- package/template/apps/{asas-platform/src/routes/(screens)/platform.index.tsx → asas-backoffice/src/routes/(screens)/backoffice.index.tsx} +3 -3
- package/template/apps/{asas-platform/src/routes/(screens)/platform.tsx → asas-backoffice/src/routes/(screens)/backoffice.tsx} +3 -3
- package/template/apps/{asas-platform → asas-backoffice}/src/routes/__root.tsx +2 -2
- 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-platform → asas-backoffice}/src/routes/auth.sign-in.tsx +9 -4
- package/template/apps/{asas-platform → asas-backoffice}/src/routes/unauthorized.tsx +24 -12
- package/template/apps/{asas-platform → asas-backoffice}/src/screen-form.tsx +5 -3
- package/template/apps/{asas-platform → asas-backoffice}/src/screen-list.tsx +6 -5
- package/template/apps/{asas-platform → asas-backoffice}/src/screen-names.ts +1 -1
- package/template/apps/{asas-platform → asas-backoffice}/src/screens.tsx +46 -26
- package/template/apps/asas-backoffice/src/server.ts +30 -0
- package/template/apps/{asas-platform → asas-backoffice}/src/slide.ts +1 -1
- package/template/apps/{asas-platform → asas-backoffice}/src/start.ts +1 -1
- package/template/apps/{asas-platform → asas-backoffice}/src/styles.css +9 -7
- package/template/apps/{asas-platform → asas-backoffice}/tsconfig.json +2 -2
- package/template/apps/{asas-platform → asas-backoffice}/vite.config.ts +7 -1
- package/template/apps/asas-tanstack/package.json +4 -2
- package/template/apps/asas-tanstack/src/api.ts +1 -1
- package/template/apps/asas-tanstack/src/auth-frame.tsx +12 -12
- package/template/apps/asas-tanstack/src/choose-password.tsx +150 -0
- package/template/apps/asas-tanstack/src/components.ts +35 -0
- package/template/apps/asas-tanstack/src/copy.ts +416 -79
- package/template/apps/asas-tanstack/src/first-run.ts +16 -0
- package/template/apps/asas-tanstack/src/links.ts +1 -1
- package/template/apps/asas-tanstack/src/locale-switch.tsx +7 -5
- package/template/apps/asas-tanstack/src/no-organization.tsx +108 -0
- package/template/apps/asas-tanstack/src/redirect.ts +20 -1
- package/template/apps/asas-tanstack/src/request-headers.ts +1 -1
- package/template/apps/asas-tanstack/src/routeTree.gen.ts +42 -0
- package/template/apps/asas-tanstack/src/router.tsx +12 -1
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/files.ts +9 -2
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/screen.json +475 -116
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/types.ts +4 -2
- package/template/apps/asas-tanstack/src/routes/(screens)/portal/files.ts +7 -2
- package/template/apps/asas-tanstack/src/routes/(screens)/portal/screen.json +83 -319
- package/template/apps/asas-tanstack/src/routes/(screens)/portal/types.ts +3 -1
- package/template/apps/asas-tanstack/src/routes/__root.tsx +1 -1
- package/template/apps/asas-tanstack/src/routes/auth.accept-invitation.tsx +14 -7
- package/template/apps/asas-tanstack/src/routes/auth.first-run.tsx +114 -0
- package/template/apps/asas-tanstack/src/routes/auth.forgot-password.tsx +2 -2
- package/template/apps/asas-tanstack/src/routes/auth.handoff.ts +5 -5
- package/template/apps/asas-tanstack/src/routes/auth.reset-password.tsx +7 -100
- package/template/apps/asas-tanstack/src/routes/auth.set-password.tsx +16 -0
- package/template/apps/asas-tanstack/src/routes/auth.sign-in.tsx +27 -15
- package/template/apps/asas-tanstack/src/routes/auth.sign-up.tsx +59 -38
- package/template/apps/asas-tanstack/src/routes/index.tsx +18 -14
- package/template/apps/asas-tanstack/src/routes/unauthorized.tsx +24 -12
- package/template/apps/asas-tanstack/src/screen-form.tsx +5 -3
- package/template/apps/asas-tanstack/src/screen-list.tsx +6 -5
- package/template/apps/asas-tanstack/src/screen-names.ts +1 -1
- package/template/apps/asas-tanstack/src/screens.tsx +46 -26
- package/template/apps/asas-tanstack/src/server.ts +10 -0
- package/template/apps/asas-tanstack/src/slide.ts +1 -1
- package/template/apps/asas-tanstack/src/styles.css +7 -3
- package/template/apps/asas-tanstack/tsconfig.json +2 -2
- package/template/apps/asas-tanstack/vite.config.ts +7 -1
- package/template/package.json +3 -3
- 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 +22 -5
- 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/package.json +19 -4
- package/template/templates/asas/src/asas.ts +81 -56
- package/template/templates/asas/src/{platform-http.ts → backoffice-http.ts} +48 -27
- 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 +129 -103
- package/template/templates/asas/src/messages/auditLog.json +223 -105
- package/template/templates/asas/src/messages/backofficeLog.json +132 -0
- package/template/templates/asas/src/messages/courses.json +256 -214
- package/template/templates/asas/src/messages/enrollments.json +45 -27
- package/template/templates/asas/src/messages/invitation.json +72 -56
- package/template/templates/asas/src/messages/localUsers.json +34 -28
- package/template/templates/asas/src/messages/media.json +31 -21
- package/template/templates/asas/src/messages/member.json +59 -39
- package/template/templates/asas/src/messages/memberRoles.json +37 -31
- package/template/templates/asas/src/messages/mithaq.json +206 -162
- package/template/templates/asas/src/messages/organization.json +63 -41
- package/template/templates/asas/src/messages/organizationRole.json +59 -47
- package/template/templates/asas/src/messages/roles.json +59 -45
- package/template/templates/asas/src/messages/session.json +99 -79
- package/template/templates/asas/src/messages/sessions.json +110 -80
- package/template/templates/asas/src/messages/settings.json +51 -53
- package/template/templates/asas/src/messages/tags.json +31 -21
- package/template/templates/asas/src/messages/user.json +109 -87
- package/template/templates/asas/src/messages/verification.json +59 -47
- 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/schema.ts +1 -0
- package/template/templates/asas/src/mithaq.ts +4 -406
- package/template/templates/asas/src/organizations.ts +3 -0
- package/template/templates/asas/src/seed.ts +147 -45
- package/template/templates/asas/src/{ui-platform → ui-backoffice}/home.tsx +9 -9
- package/template/templates/asas/src/ui-backoffice/organization-actions.tsx +121 -0
- package/template/templates/asas/src/{ui-platform → ui-backoffice}/record-page.tsx +4 -4
- package/template/templates/asas/src/{ui-platform → ui-backoffice}/role-cells.tsx +13 -6
- package/template/templates/asas/src/{ui-platform → ui-backoffice}/user-actions.tsx +46 -48
- package/template/templates/asas/src/ui-dashboard/dashboard.tsx +10 -4
- 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 +637 -438
- package/template/templates/asas/src/ui-dashboard/versions.tsx +22 -8
- package/template/templates/asas/src/ui-portal/courses-home.tsx +96 -0
- package/template/templates/asas/src/ui-portal/home.tsx +9 -13
- 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/{asas/src → features/auth}/local-users/local-users.ts +7 -7
- package/template/templates/{auth → features/auth}/package.json +2 -3
- package/template/templates/{asas/src/kayan → features}/auth/user/account.access.ts +2 -2
- package/template/templates/{asas/src/kayan → features}/auth/user/account.ts +2 -2
- package/template/templates/{asas/src/kayan → features}/auth/user/invitation.access.ts +1 -1
- package/template/templates/{asas/src/kayan → features}/auth/user/invitation.ts +3 -3
- package/template/templates/{asas/src/kayan → features}/auth/user/member.access.ts +4 -4
- package/template/templates/{asas/src/kayan → features}/auth/user/member.screens.ts +5 -5
- package/template/templates/{asas/src/kayan → features}/auth/user/member.ts +4 -3
- package/template/templates/{asas/src/kayan → features}/auth/user/organization.access.ts +6 -6
- package/template/templates/{asas/src/kayan → features}/auth/user/organization.screens.ts +6 -6
- package/template/templates/{asas/src/kayan → features}/auth/user/organization.ts +2 -1
- package/template/templates/{asas/src/kayan → features}/auth/user/organizationRole.access.ts +2 -2
- package/template/templates/{asas/src/kayan → features}/auth/user/organizationRole.ts +3 -3
- package/template/templates/{asas/src/kayan → features}/auth/user/session.access.ts +2 -2
- package/template/templates/{asas/src/kayan → features}/auth/user/session.ts +2 -2
- package/template/templates/{asas/src/kayan → features}/auth/user/user.access.ts +8 -7
- package/template/templates/{asas/src/kayan → features}/auth/user/user.screens.ts +9 -9
- package/template/templates/{asas/src/kayan → features}/auth/user/user.ts +2 -2
- package/template/templates/{asas/src/kayan → features}/auth/user/verification.access.ts +2 -2
- package/template/templates/{asas/src/kayan → features}/auth/user/verification.ts +2 -2
- package/template/templates/{asas/src → features/courses}/courses/courses.ts +122 -133
- 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/{asas/src → features/courses}/courses/ui/fee-cell.tsx +1 -1
- package/template/templates/{asas/src → features/courses}/courses/ui/status-cell.tsx +5 -3
- 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/{asas/src → features/courses}/enrollments/ui/session-cell.tsx +17 -7
- package/template/templates/{asas/src → features/courses/enrollments}/ui/session-labels.tsx +3 -3
- 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/{asas/src → features/courses}/sessions/sessions.ts +45 -14
- package/template/templates/features/media/index.ts +10 -0
- package/template/templates/{asas/src → features/media}/media/media.ts +34 -40
- package/template/templates/{asas/src → features/media}/media/ui/preview.tsx +2 -2
- package/template/templates/features/media/package.json +19 -0
- package/template/templates/features/roles/index.ts +4 -0
- package/template/templates/{asas/src → features/roles}/member-roles/member-roles.ts +15 -15
- package/template/templates/features/roles/package.json +21 -0
- package/template/templates/{asas/src → features/roles}/roles/roles.ts +23 -23
- package/template/templates/{asas/src → features/roles}/roles/ui/role-form.tsx +65 -24
- package/template/templates/{asas/src → features/roles}/roles/ui/roles.tsx +49 -25
- package/template/templates/features/settings/index.ts +3 -0
- package/template/templates/features/settings/package.json +13 -0
- package/template/templates/{asas/src → features/settings}/settings/settings.ts +12 -17
- package/template/templates/features/tags/index.ts +3 -0
- package/template/templates/features/tags/package.json +14 -0
- package/template/templates/{asas/src → features/tags}/tags/tags.ts +15 -15
- 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/{asas/src/audit-log/sink.ts → mithaq/src/mithaq/audit-sink.ts} +31 -9
- package/template/templates/{auth/src/platform-roles.ts → mithaq/src/mithaq/auth/backoffice-roles.ts} +44 -23
- 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/{asas/src → mithaq/src/mithaq}/pages.ts +16 -7
- 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/{asas → mithaq}/src/roles/grants.ts +1 -1
- package/template/templates/mithaq/src/roles/member-access.ts +11 -0
- package/template/templates/{asas/src → mithaq/src/roles}/statements.ts +6 -4
- package/template/templates/mithaq/src/roles/system-roles.ts +16 -0
- package/template/templates/{asas/src → mithaq/src/roles}/tenant-access.ts +96 -57
- package/template/templates/{asas/src → mithaq/src/roles}/tenant-roles.ts +84 -25
- package/template/templates/{asas → mithaq}/src/steps/expected-updated-at.ts +2 -2
- package/template/templates/{asas → mithaq}/src/storage/index.ts +3 -1
- package/template/templates/mithaq/src/ui/activity-page.tsx +33 -0
- package/template/templates/mithaq/src/ui/activity.tsx +527 -0
- package/template/templates/{asas → mithaq}/src/ui/app-shell.tsx +27 -19
- package/template/templates/mithaq/src/ui/rich-text-nodes.tsx +33 -0
- package/template/templates/{asas → mithaq}/src/ui/screen-nav.tsx +1 -1
- package/template/templates/{asas → mithaq}/src/ui/top-shell.tsx +8 -8
- package/template/apps/asas-platform/src/components/mithaq/cells.tsx +0 -112
- package/template/apps/asas-platform/src/components/mithaq/controls.tsx +0 -650
- package/template/apps/asas-platform/src/components/mithaq/field-shell.tsx +0 -53
- package/template/apps/asas-platform/src/components/mithaq/filter-operators.ts +0 -108
- package/template/apps/asas-platform/src/components/mithaq/form.tsx +0 -1467
- package/template/apps/asas-platform/src/components/mithaq/list-columns.tsx +0 -108
- package/template/apps/asas-platform/src/components/mithaq/list-filters.tsx +0 -368
- package/template/apps/asas-platform/src/components/mithaq/list-pagination.tsx +0 -153
- package/template/apps/asas-platform/src/components/mithaq/list-query.ts +0 -432
- package/template/apps/asas-platform/src/components/mithaq/list-selection.tsx +0 -74
- package/template/apps/asas-platform/src/components/mithaq/list.tsx +0 -865
- package/template/apps/asas-platform/src/components/mithaq/nav.tsx +0 -871
- package/template/apps/asas-platform/src/components/mithaq/nodes.tsx +0 -1024
- package/template/apps/asas-platform/src/components/mithaq/relationship-cell.tsx +0 -127
- package/template/apps/asas-platform/src/components/mithaq/relationship-control.tsx +0 -400
- package/template/apps/asas-platform/src/components/mithaq/rich-text-nodes.ts +0 -6
- package/template/apps/asas-platform/src/components/mithaq/rich-text.tsx +0 -1083
- package/template/apps/asas-platform/src/components/mithaq/upload.tsx +0 -686
- package/template/apps/asas-platform/src/components/ui/drawer.tsx +0 -216
- package/template/apps/asas-platform/src/redirect.ts +0 -1
- package/template/apps/asas-platform/src/routeTree.gen.ts +0 -181
- package/template/apps/asas-platform/src/router.tsx +0 -7
- package/template/apps/asas-platform/src/routes/(screens)/platform/files.ts +0 -4
- package/template/apps/asas-platform/src/routes/api/$.ts +0 -30
- package/template/apps/asas-platform/src/server.ts +0 -20
- package/template/apps/asas-tanstack/src/components/mithaq/cells.tsx +0 -112
- package/template/apps/asas-tanstack/src/components/mithaq/controls.tsx +0 -650
- package/template/apps/asas-tanstack/src/components/mithaq/field-shell.tsx +0 -53
- package/template/apps/asas-tanstack/src/components/mithaq/filter-operators.ts +0 -108
- package/template/apps/asas-tanstack/src/components/mithaq/form.tsx +0 -1467
- package/template/apps/asas-tanstack/src/components/mithaq/list-columns.tsx +0 -108
- package/template/apps/asas-tanstack/src/components/mithaq/list-filters.tsx +0 -368
- package/template/apps/asas-tanstack/src/components/mithaq/list-pagination.tsx +0 -153
- package/template/apps/asas-tanstack/src/components/mithaq/list-query.ts +0 -432
- package/template/apps/asas-tanstack/src/components/mithaq/list-selection.tsx +0 -74
- package/template/apps/asas-tanstack/src/components/mithaq/list.tsx +0 -865
- package/template/apps/asas-tanstack/src/components/mithaq/nav.tsx +0 -871
- package/template/apps/asas-tanstack/src/components/mithaq/nodes.tsx +0 -1024
- package/template/apps/asas-tanstack/src/components/mithaq/relationship-cell.tsx +0 -127
- package/template/apps/asas-tanstack/src/components/mithaq/relationship-control.tsx +0 -400
- package/template/apps/asas-tanstack/src/components/mithaq/rich-text-nodes.ts +0 -6
- package/template/apps/asas-tanstack/src/components/mithaq/rich-text.tsx +0 -1083
- package/template/apps/asas-tanstack/src/components/mithaq/upload.tsx +0 -686
- package/template/apps/asas-tanstack/src/components/ui/drawer.tsx +0 -215
- package/template/templates/asas/src/audit-log/audit-log.ts +0 -187
- package/template/templates/asas/src/audit-log/ui/activity.tsx +0 -243
- package/template/templates/asas/src/courses/ui/archive-button.tsx +0 -25
- package/template/templates/asas/src/courses/ui/no-courses-on-offer.tsx +0 -11
- package/template/templates/asas/src/courses/ui/on-offer.ts +0 -6
- package/template/templates/asas/src/enrollments/enrollments.ts +0 -137
- package/template/templates/asas/src/rich-text-nodes.ts +0 -11
- package/template/templates/asas/src/roles.ts +0 -16
- package/template/templates/asas/src/ui/rich-text-nodes.tsx +0 -15
- package/template/templates/asas/src/ui-platform/organization-actions.tsx +0 -114
- package/template/templates/auth/src/index.ts +0 -690
- /package/template/apps/{asas-platform → asas-backoffice}/components.json +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/gitignore +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/public/favicon.svg +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/accordion.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/alert-dialog.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/avatar.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/badge.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/breadcrumb.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/button-group.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/button.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/calendar.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/card.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/chart.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/checkbox.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/collapsible.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/combobox.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/dialog.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/direction.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/directional-icon.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/dropdown-menu.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/field.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/input-group.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/input-otp.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/input.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/label.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/popover.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/radio-group.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/resizable.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/scroll-area.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/select.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/separator.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/sheet.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/sidebar.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/skeleton.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/slider.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/spinner.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/switch.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/table.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/tabs.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/textarea.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/toggle-group.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/toggle.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/tooltip.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/copy.ts +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/hooks/use-mobile.ts +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/lib/utils.ts +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/theme.ts +0 -0
- /package/template/templates/{asas/src/kayan → features}/auth/user/index.ts +0 -0
- /package/template/templates/{asas/src → features/courses}/courses/ui/no-courses.tsx +0 -0
- /package/template/templates/{asas → mithaq}/components.json +0 -0
- /package/template/templates/{asas → mithaq}/src/storage/defaults.ts +0 -0
- /package/template/templates/{asas → mithaq}/src/storage/local.ts +0 -0
- /package/template/templates/{asas → mithaq}/src/storage/r2.ts +0 -0
- /package/template/templates/{asas → mithaq}/src/storage/s3.ts +0 -0
- /package/template/templates/{asas → mithaq}/src/storage/uploadthing.ts +0 -0
- /package/template/templates/{asas → mithaq}/src/ui/kpi-card.tsx +0 -0
- /package/template/templates/{asas → mithaq}/src/ui/record-page.tsx +0 -0
- /package/template/templates/{asas → mithaq}/src/ui/table-page.tsx +0 -0
|
@@ -7,17 +7,27 @@ import { ScreenContext, useScreen, type ScreenState } from 'mithaq/admin/react'
|
|
|
7
7
|
import { actingAs, arm, authArm, caused, clientOf, readsOf, takesKey } from './api.ts'
|
|
8
8
|
import { clampedHref, routedHref } from './links.ts'
|
|
9
9
|
import { mergedHeaders } from './request-headers.ts'
|
|
10
|
+
import { nameIn } from 'asas/organizations'
|
|
10
11
|
import { documentHead, localeOf, locales, translator, type Locale } from './copy.ts'
|
|
11
|
-
import { FieldValue
|
|
12
|
-
import type {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
12
|
+
import { FieldValue } from 'mithaq-admin/field-value.tsx'
|
|
13
|
+
import type { FormClient, FormProps } from 'mithaq-admin/form-types.tsx'
|
|
14
|
+
import type { Page } from 'mithaq-admin/list-types.tsx'
|
|
15
|
+
import { QueryRefused, findOf, listRaw, prefixOf, readQuery, rulesOf, scopedTo, withList, type RawQuery } from 'mithaq-admin/list-query.ts'
|
|
16
|
+
import { newTitleOf, titleOf } from 'mithaq-admin/copy.ts'
|
|
17
|
+
import { createsIn } from 'mithaq-admin/field-values.ts'
|
|
18
|
+
import { NavLinks } from 'mithaq-admin/nav-links.tsx'
|
|
19
|
+
import { NodeButton, OwnButton, pageButtonOf } from 'mithaq-admin/node-buttons.tsx'
|
|
20
|
+
import { formIn, listIn, nodesOf, readsAsValues, relatedIn, scopeOf, type Draw } from 'mithaq-admin/node-tree.ts'
|
|
21
|
+
import { frameOf, pageOf } from 'mithaq-admin/nodes.tsx'
|
|
22
|
+
import { RecordFrame, holdFocus } from 'mithaq-admin/record-frame.tsx'
|
|
23
|
+
import { rowTitle } from 'mithaq-admin/row-title.tsx'
|
|
24
|
+
import { refOf, type Refs, type TreePage } from 'mithaq-admin/screen-refs.ts'
|
|
25
|
+
import { idsOf } from 'mithaq-admin/relationship-control.tsx'
|
|
17
26
|
import { ScreenForm } from './screen-form.tsx'
|
|
18
27
|
import { ScreenList, askedFor, readsDrafts, rowReadsDrafts, rowsKey } from './screen-list.tsx'
|
|
19
28
|
import { screenNamed } from './screen-names.ts'
|
|
20
29
|
import { slide } from './slide.ts'
|
|
30
|
+
import { Toaster } from 'mithaq-admin/toast.tsx'
|
|
21
31
|
import { useCopy } from './routes/__root.tsx'
|
|
22
32
|
|
|
23
33
|
/**
|
|
@@ -29,13 +39,13 @@ export const getAsas = createServerOnlyFn(async () => (await import('asas')).asa
|
|
|
29
39
|
type InProcess = Record<string, { operations: Record<string, (input: object, options: object) => Promise<unknown>> }>
|
|
30
40
|
|
|
31
41
|
/**
|
|
32
|
-
* Each kayan's operations in process. In the
|
|
33
|
-
* which the runner grants
|
|
42
|
+
* Each kayan's operations in process. In the backoffice app a read of a kayan asks `tenant: 'all'`,
|
|
43
|
+
* which the runner grants backoffice staff there alone, so staff read every organization's rows; a
|
|
34
44
|
* global is one organization's row and asks for none, and a write never asks.
|
|
35
45
|
*/
|
|
36
46
|
const inProcess = async (asas: Awaited<ReturnType<typeof getAsas>>): Promise<InProcess> => {
|
|
37
47
|
const kayan = asas.kayan as unknown as InProcess
|
|
38
|
-
if (!(await import('asas')).
|
|
48
|
+
if (!(await import('asas')).onBackoffice()) return kayan
|
|
39
49
|
const reads = new Set(asas.operations.filter((one) => one.effect === 'read' && !one.single).map((one) => `${one.kayan}/${one.name}`))
|
|
40
50
|
return Object.fromEntries(Object.entries(kayan).map(([name, one]) => [name, {
|
|
41
51
|
operations: Object.fromEntries(Object.entries(one.operations).map(([operation, run]) =>
|
|
@@ -117,7 +127,7 @@ type Actor = {
|
|
|
117
127
|
readonly email?: string | null
|
|
118
128
|
readonly impersonatedBy?: unknown
|
|
119
129
|
readonly activeOrganizationId?: string | null
|
|
120
|
-
readonly organizations?: readonly { readonly id: string; readonly name: string; readonly slug?: string }[]
|
|
130
|
+
readonly organizations?: readonly { readonly id: string; readonly name: string; readonly slug?: string; readonly names?: unknown }[]
|
|
121
131
|
}
|
|
122
132
|
|
|
123
133
|
// `'unauthorized'` is answered before the projection is read, so a refused screen sends no kayan, field, row or page;
|
|
@@ -140,15 +150,15 @@ const resolveScreen = async (asked: unknown) => {
|
|
|
140
150
|
.filter(([one]) => report.collections?.[one]?.read === true)
|
|
141
151
|
.map(([one, value]) => [one, { ...value, fields: value.fields.filter((field) => report.collections?.[one]?.fields?.[field.name]?.read === true) }]),
|
|
142
152
|
)
|
|
143
|
-
const {
|
|
144
|
-
const
|
|
145
|
-
// The other screens of this app the actor may enter. The
|
|
146
|
-
// is left out of a tenant app's list, so the one link to it is `
|
|
153
|
+
const { onBackoffice, backofficeEntry, BACKOFFICE_SCREEN } = await import('asas')
|
|
154
|
+
const backoffice = backofficeEntry(actor)
|
|
155
|
+
// The other screens of this app the actor may enter. The backoffice's own name
|
|
156
|
+
// is left out of a tenant app's list, so the one link to it is `backofficeEntry`'s alone.
|
|
147
157
|
const served = screensOf(asas.targets) as Record<string, { client: ScreenClient }>
|
|
148
158
|
const siblings = (report.screens ?? [])
|
|
149
|
-
.filter((one) => one !== name && Object.hasOwn(served, one) && (one ===
|
|
159
|
+
.filter((one) => one !== name && Object.hasOwn(served, one) && (one === BACKOFFICE_SCREEN) === onBackoffice())
|
|
150
160
|
.map((one) => ({ name: one, path: served[one]!.client.path }))
|
|
151
|
-
const others = [...siblings, ...(
|
|
161
|
+
const others = [...siblings, ...(backoffice ? [{ name: BACKOFFICE_SCREEN, path: backoffice }] : [])]
|
|
152
162
|
return {
|
|
153
163
|
asas,
|
|
154
164
|
request,
|
|
@@ -164,7 +174,8 @@ const resolveScreen = async (asked: unknown) => {
|
|
|
164
174
|
...(actor.impersonatedBy ? { impersonating: true } : {}),
|
|
165
175
|
...(others.length ? { screens: others } : {}),
|
|
166
176
|
...(actor.organizations && {
|
|
167
|
-
|
|
177
|
+
// Every locale's, because the frame is read before the viewer's locale is known here; the shell picks one.
|
|
178
|
+
organizations: actor.organizations.map(({ id, name, slug, names }) => ({ id, name, slug, ...(typeof names === 'string' && { names }) })),
|
|
168
179
|
activeOrganizationId: actor.activeOrganizationId ?? null,
|
|
169
180
|
}),
|
|
170
181
|
},
|
|
@@ -206,14 +217,14 @@ export const loadEnterable = createServerFn({ method: 'GET' })
|
|
|
206
217
|
others?: { id: string; name: string; slug?: string }[]
|
|
207
218
|
impersonating?: string
|
|
208
219
|
} | null> => {
|
|
209
|
-
const [{ getRequest }, asas, {
|
|
220
|
+
const [{ getRequest }, asas, { onBackoffice, BACKOFFICE_SCREEN }] = await Promise.all([import('@tanstack/react-start/server'), getAsas(), import('asas')])
|
|
210
221
|
const request = getRequest()
|
|
211
222
|
const actor = (await asas.auth.actor(request.headers)) as Actor | null
|
|
212
223
|
if (!actor?.email) return null
|
|
213
224
|
const report = (await (await asas.handler(new Request(new URL('/api/access', request.url), { headers: request.headers }))).json()) as AccessReport
|
|
214
225
|
const screens = screensOf(asas.targets) as Record<string, { client: ScreenClient }>
|
|
215
226
|
const entered = (report.screens ?? [])
|
|
216
|
-
.filter((name) => Object.hasOwn(screens, name) && (name ===
|
|
227
|
+
.filter((name) => Object.hasOwn(screens, name) && (name === BACKOFFICE_SCREEN) === onBackoffice())
|
|
217
228
|
.map((name) => ({ name, path: screens[name]!.client.path }))
|
|
218
229
|
const impersonating = actor.impersonatedBy ? { impersonating: actor.name ?? actor.email } : {}
|
|
219
230
|
if (entered.length) {
|
|
@@ -226,11 +237,14 @@ export const loadEnterable = createServerFn({ method: 'GET' })
|
|
|
226
237
|
}
|
|
227
238
|
const memberships = actor.organizations ?? []
|
|
228
239
|
const active = memberships.find((one) => one.id === actor.activeOrganizationId)
|
|
240
|
+
// Named here, where the locale is known: the page draws what it is handed, and a name the viewer cannot read
|
|
241
|
+
// in the default locale would be no name to them.
|
|
242
|
+
const named = ({ id, name, slug, names }: NonNullable<Actor['organizations']>[number]) => ({ id, name: nameIn({ name, names }, data.locale), slug })
|
|
229
243
|
return {
|
|
230
244
|
screens: entered,
|
|
231
245
|
email: actor.email,
|
|
232
|
-
organizations: (active ? [active] : memberships).map(
|
|
233
|
-
others:
|
|
246
|
+
organizations: (active ? [active] : memberships).map(named),
|
|
247
|
+
others: onBackoffice() ? [] : memberships.filter((one) => one !== active).map(named),
|
|
234
248
|
...impersonating,
|
|
235
249
|
}
|
|
236
250
|
})
|
|
@@ -473,7 +487,8 @@ const nameOfPage = ({ screen, name, match, row, titledBy, copy }: {
|
|
|
473
487
|
if (match.kayan === undefined) return screen.nav.find((entry) => !entry.kayan && entry.page === match.page)?.label?.[copy.locale] ?? copy.t(`screens.${name}`, name) ?? name
|
|
474
488
|
const kayan = screen.kayan[match.kayan]!
|
|
475
489
|
const page = kayan.pages[match.page]
|
|
476
|
-
|
|
490
|
+
// A list page draws every row, whether a tree's list node or a component drawing it whole.
|
|
491
|
+
const many = match.page === 'list' || (!!page && 'page' in page && listIn(nodesOf(page)) !== undefined)
|
|
477
492
|
if (match.page === 'create') return newTitleOf({ kayan, copy })
|
|
478
493
|
const field = kayan.fields.find((one) => one.name === kayan.useAsTitle)
|
|
479
494
|
const target = field?.kind === 'relationship' ? screen.kayan[field.relationTo ?? ''] : undefined
|
|
@@ -500,7 +515,7 @@ const useScreenState = (data: ScreenData): ScreenState => {
|
|
|
500
515
|
|
|
501
516
|
/**
|
|
502
517
|
* The bar that says the session is an impersonation, with the way out. Stopping
|
|
503
|
-
* is a sign-out: the staff member's own session lives in the
|
|
518
|
+
* is a sign-out: the staff member's own session lives in the backoffice app under
|
|
504
519
|
* that app's cookie, and the sign-out answers where they go next.
|
|
505
520
|
*/
|
|
506
521
|
export function ImpersonationBar({ name }: { name: string }): ReactElement {
|
|
@@ -544,6 +559,7 @@ function ScreenBar({ user }: { user: ScreenData['user'] }): ReactElement | null
|
|
|
544
559
|
|
|
545
560
|
/** The screen's own frame: its providers, outermost first, around the frame's bar and the page. */
|
|
546
561
|
export function ScreenFrame({ files, data, children }: { files: Files; data: ScreenData; children: ReactNode }): ReactElement {
|
|
562
|
+
const { t } = useCopy()
|
|
547
563
|
const refs = refsOf(files, data.screen)
|
|
548
564
|
// One per frame: made per request on the server, so no row one actor read
|
|
549
565
|
// reaches another's render; held for the frame's life in the browser.
|
|
@@ -590,6 +606,8 @@ export function ScreenFrame({ files, data, children }: { files: Files; data: Scr
|
|
|
590
606
|
<>
|
|
591
607
|
<ScreenBar user={data.user} />
|
|
592
608
|
{children}
|
|
609
|
+
{/* The one place every write's toast is drawn, kept across the screen's pages. */}
|
|
610
|
+
<Toaster closeLabel={t('admin.close', 'Close') ?? 'Close'} />
|
|
593
611
|
</>,
|
|
594
612
|
)}
|
|
595
613
|
</Suspense>
|
|
@@ -666,7 +684,7 @@ function PageBody({ files, data, query, onQuery }: PageViewProps): ReactElement
|
|
|
666
684
|
return path ? `${base}${id ? path.replace(':id', encodeURIComponent(id)) : path}` : undefined
|
|
667
685
|
}
|
|
668
686
|
// A row opens its kayan's edit page, else the one page whose path draws a
|
|
669
|
-
// row (how the
|
|
687
|
+
// row (how the backoffice opens a record it only reads).
|
|
670
688
|
const rowPage = (target: ClientKayan) => (target.pages.edit ? 'edit' : Object.keys(target.pages).find((one) => target.pages[one]!.path.includes(':id')))
|
|
671
689
|
const related = relatedOf(data.screen, kayan)
|
|
672
690
|
const go = (href: string | undefined) => void (href ? router.navigate({ href }) : router.invalidate())
|
|
@@ -844,7 +862,9 @@ function PageBody({ files, data, query, onQuery }: PageViewProps): ReactElement
|
|
|
844
862
|
api={readsOf(related)}
|
|
845
863
|
operations={data.operations}
|
|
846
864
|
related={draw(page).list}
|
|
847
|
-
|
|
865
|
+
// A create opened over its list closes back to it once it lands, the new row in the list, as Payload's drawer does;
|
|
866
|
+
// one whose kayan autosaves its drafts moves to the row's page instead, since its first autosave is a create too.
|
|
867
|
+
edit={(id) => (page.mode && listHref && !data.drafts?.autosave ? listHref : hrefOf(kayan, 'edit', id))}
|
|
848
868
|
list={listHref}
|
|
849
869
|
drafts={data.drafts}
|
|
850
870
|
expectedUpdatedAt={data.expectedUpdatedAt}
|
|
@@ -865,4 +885,4 @@ function PageBody({ files, data, query, onQuery }: PageViewProps): ReactElement
|
|
|
865
885
|
}
|
|
866
886
|
return form
|
|
867
887
|
}
|
|
868
|
-
// mithaq-admin
|
|
888
|
+
// mithaq-admin 0.0.0 da2e742262e16a7f9f8dab050c2a4201517478795101af68736907464a0d52ce
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import handler, { createServerEntry } from '@tanstack/react-start/server-entry'
|
|
2
|
+
import type { ScreenClient } from 'mithaq/admin'
|
|
3
|
+
import { checkComponents } from 'mithaq-admin/components.tsx'
|
|
4
|
+
import { components } from './components.ts'
|
|
5
|
+
import { withQuietAbort } from '../../asas-tanstack/src/quiet-abort.ts'
|
|
6
|
+
|
|
7
|
+
// Every screen this app serves, checked against its components map as it starts, so a field whose kind has no
|
|
8
|
+
// control fails the start, naming the kind and the field, and never reaches a page. Read on the server alone,
|
|
9
|
+
// since a screen.json names every field of the screen.
|
|
10
|
+
for (const client of Object.values(import.meta.glob<ScreenClient>('./routes/(screens)/*/screen.json', { eager: true, import: 'default' }))) {
|
|
11
|
+
checkComponents({ client, components })
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// A refused start (a migration not applied) exits 1 rather than leave a server up that cannot serve.
|
|
15
|
+
const { env } = process
|
|
16
|
+
if (env.NODE_ENV === 'production') {
|
|
17
|
+
await (await import('asas')).asas.connect().catch((error: unknown) => {
|
|
18
|
+
console.error(error)
|
|
19
|
+
process.exit(1)
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Every request runs as the backoffice; nothing a request carries decides it, and the tenant app never does.
|
|
24
|
+
// `asas` is imported per request so it is the same module every route reads.
|
|
25
|
+
export default createServerEntry({
|
|
26
|
+
fetch: async (request: Request) => {
|
|
27
|
+
const { asBackoffice } = await import('asas')
|
|
28
|
+
return asBackoffice(() => handler.fetch(withQuietAbort(request)))
|
|
29
|
+
},
|
|
30
|
+
})
|
|
@@ -19,4 +19,4 @@ export const slide = async ({
|
|
|
19
19
|
// Set once with every strategy's cookies: a second set replaces the first.
|
|
20
20
|
if (cookies.length) setResponseHeader('set-cookie', cookies)
|
|
21
21
|
}
|
|
22
|
-
// mithaq-admin
|
|
22
|
+
// mithaq-admin 2d3ab739c92c6c2b397041b76bf4b23b2902b7a2 bc0221e1088103f8638e0333b84aafb6b326e3991a187ced198bce2d090ecbe9
|
|
@@ -21,7 +21,7 @@ const csrf = createCsrfMiddleware({ filter: (ctx) => ctx.handlerType === 'server
|
|
|
21
21
|
|
|
22
22
|
// The location is a path of this origin, so nothing the request's host header says reaches it.
|
|
23
23
|
const home = createMiddleware({ type: 'request' }).server(({ request, next }) =>
|
|
24
|
-
new URL(request.url).pathname === '/' ? new Response(null, { status: 307, headers: { location: '/
|
|
24
|
+
new URL(request.url).pathname === '/' ? new Response(null, { status: 307, headers: { location: '/backoffice' } }) : next(),
|
|
25
25
|
)
|
|
26
26
|
|
|
27
27
|
// Only this origin may frame the pages, so no other site can lay this app under its own page and steer a click.
|
|
@@ -10,12 +10,14 @@
|
|
|
10
10
|
for it, listed first in `--font-sans` below so an Arabic page reads in it and
|
|
11
11
|
a Latin one falls through to the preset's. */
|
|
12
12
|
@import "@fontsource/ibm-plex-sans-arabic";
|
|
13
|
+
/* The admin's pieces in node_modules, which Tailwind never scans on its own. */
|
|
14
|
+
@import "mithaq-admin/source.css";
|
|
13
15
|
|
|
14
|
-
/* The project's own shells, pages and components, which the
|
|
15
|
-
loads
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@source "
|
|
16
|
+
/* The project's own shells, pages and components, which the backoffice screen
|
|
17
|
+
loads, as the tenant app lists them (`apps/asas-tanstack/src/styles.css`).
|
|
18
|
+
Tailwind finds this app's own files, an ejected `src/components/mithaq`
|
|
19
|
+
among them, without one. */
|
|
20
|
+
@source "../../../templates";
|
|
19
21
|
|
|
20
22
|
@custom-variant dark (&:is(.dark *));
|
|
21
23
|
|
|
@@ -143,7 +145,7 @@
|
|
|
143
145
|
@apply font-sans;
|
|
144
146
|
}
|
|
145
147
|
}
|
|
146
|
-
/* What the
|
|
148
|
+
/* What the backoffice draws beyond the preset, each a rule over a marker the
|
|
147
149
|
shared pieces already carry, so the tenant app, which does not load this
|
|
148
150
|
file, is unchanged. */
|
|
149
151
|
|
|
@@ -188,7 +190,7 @@ a[data-row-link]:empty::before,
|
|
|
188
190
|
color: var(--muted-foreground);
|
|
189
191
|
}
|
|
190
192
|
|
|
191
|
-
/* The
|
|
193
|
+
/* The backoffice's cards are square, as its buttons, inputs and home cards are:
|
|
192
194
|
the section and form cards the registry draws with a rounded class of their
|
|
193
195
|
own, the frame a list's table sits in, the filter panel above it and the
|
|
194
196
|
danger region of a record page. */
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"allowImportingTsExtensions": true,
|
|
14
14
|
"resolveJsonModule": true,
|
|
15
15
|
"types": ["node", "vite/client"],
|
|
16
|
-
"paths": { "@/*": ["./src/*"] }
|
|
16
|
+
"paths": { "@/*": ["./src/*"], "mithaq-admin/*": ["./src/components/mithaq/*", "./node_modules/mithaq-admin/registry/*"] }
|
|
17
17
|
},
|
|
18
|
-
"include": ["src/**/*.ts", "src/**/*.tsx", "vite.config.ts", "../../templates/
|
|
18
|
+
"include": ["src/**/*.ts", "src/**/*.tsx", "vite.config.ts", "../../templates/mithaq/src/ui/*.tsx", "../../templates/asas/src/ui-*/*.tsx"]
|
|
19
19
|
}
|
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
import tailwindcss from '@tailwindcss/vite'
|
|
2
2
|
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
|
|
3
3
|
import viteReact from '@vitejs/plugin-react'
|
|
4
|
+
import { mithaqAdmin } from 'mithaq/vite'
|
|
4
5
|
import { defineConfig } from 'vite'
|
|
5
6
|
import { fileURLToPath } from 'node:url'
|
|
6
7
|
|
|
7
8
|
export default defineConfig({
|
|
8
|
-
// An alias rather than `resolve.tsconfigPaths`: the shells in `
|
|
9
|
+
// An alias rather than `resolve.tsconfigPaths`: the shells in the language package's `src/ui/` fall under a tsconfig
|
|
9
10
|
// with no `@/` and still import it. `^@/` leaves `@tanstack/*` alone.
|
|
10
11
|
resolve: { alias: [{ find: /^@\//, replacement: fileURLToPath(new URL('./src/', import.meta.url)) }] },
|
|
11
12
|
plugins: [
|
|
12
13
|
tailwindcss(),
|
|
14
|
+
// A file under this folder takes the place of mithaq-admin's file of the same name, for every importer.
|
|
15
|
+
mithaqAdmin(fileURLToPath(new URL('./src/components/mithaq/', import.meta.url))),
|
|
13
16
|
// `mithaq generate screens` writes `files.ts` and `types.ts` beside the route files; neither is a route.
|
|
14
17
|
tanstackStart({ router: { routeFileIgnorePattern: '^(files|types)\\.ts$' } }),
|
|
15
18
|
viteReact(),
|
|
16
19
|
],
|
|
20
|
+
// Behind the proxy that routes by Host: bound to the loopback address it targets, and answering
|
|
21
|
+
// the public name in BACKOFFICE_URL, which Vite refuses in a Host header unless named here.
|
|
22
|
+
preview: { host: '127.0.0.1', allowedHosts: process.env.BACKOFFICE_URL ? [new URL(process.env.BACKOFFICE_URL).hostname] : [] },
|
|
17
23
|
environments: {
|
|
18
24
|
ssr: {
|
|
19
25
|
// `asas` and `mithaq` stay external: Vite bundles linked workspace packages by default, and bundling
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"@base-ui/react": "^1.5.0",
|
|
13
13
|
"@fontsource-variable/raleway": "^5.2.8",
|
|
14
14
|
"@fontsource/ibm-plex-sans-arabic": "^5.2.8",
|
|
15
|
-
"@hugeicons/core-free-icons": "
|
|
15
|
+
"@hugeicons/core-free-icons": "4.3.5",
|
|
16
16
|
"@hugeicons/react": "^1.1.6",
|
|
17
17
|
"@lexical/extension": "0.52.0",
|
|
18
18
|
"@lexical/link": "0.52.0",
|
|
@@ -34,9 +34,11 @@
|
|
|
34
34
|
"lexical": "0.52.0",
|
|
35
35
|
"lucide-react": "^1.16.0",
|
|
36
36
|
"mithaq": "*",
|
|
37
|
+
"mithaq-admin": "*",
|
|
37
38
|
"react": "^19.3.0",
|
|
38
39
|
"react-day-picker": "^10.0.1",
|
|
39
40
|
"react-dom": "^19.3.0",
|
|
41
|
+
"react-is": "^19.3.0",
|
|
40
42
|
"react-resizable-panels": "^4.13.3",
|
|
41
43
|
"recharts": "3.8.0",
|
|
42
44
|
"tailwind-merge": "^3.6.0",
|
|
@@ -48,9 +50,9 @@
|
|
|
48
50
|
"@types/react": "^19.3.0",
|
|
49
51
|
"@types/react-dom": "^19.3.0",
|
|
50
52
|
"@vitejs/plugin-react": "^6.0.2",
|
|
51
|
-
"mithaq-admin": "*",
|
|
52
53
|
"shadcn": "4.18.0",
|
|
53
54
|
"tailwindcss": "^4.3.0",
|
|
55
|
+
"typescript": "^7.0.2",
|
|
54
56
|
"vite": "^8.0.13"
|
|
55
57
|
}
|
|
56
58
|
}
|
|
@@ -144,4 +144,4 @@ export const authArm = async (path: string, body: unknown = {}) => {
|
|
|
144
144
|
// The status too, because a rate limit's refusal carries no code to read.
|
|
145
145
|
return { ok: response.ok, status: response.status, message: answered?.message, body: answered }
|
|
146
146
|
}
|
|
147
|
-
// mithaq-admin
|
|
147
|
+
// mithaq-admin 2d3ab739c92c6c2b397041b76bf4b23b2902b7a2 94db2fc49ad4391c0d1c2183cb41001dd08d62017c76af86cc5f611488c22c68
|
|
@@ -7,10 +7,10 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
|
|
7
7
|
import { Input } from '@/components/ui/input'
|
|
8
8
|
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from '@/components/ui/input-group'
|
|
9
9
|
import { authArm } from './api.ts'
|
|
10
|
-
import { FieldShell, describedBy } from '
|
|
10
|
+
import { FieldShell, describedBy } from 'mithaq-admin/field-shell.tsx'
|
|
11
11
|
import { refusalKey } from './copy.ts'
|
|
12
12
|
import { LocaleSwitch } from './locale-switch.tsx'
|
|
13
|
-
import {
|
|
13
|
+
import { landing } from './redirect.ts'
|
|
14
14
|
import { useCopy } from './routes/__root.tsx'
|
|
15
15
|
|
|
16
16
|
/** The one search key the auth pages read, off the raw search. */
|
|
@@ -21,8 +21,8 @@ export const redirectSearch = (search: Record<string, unknown>): { redirect?: st
|
|
|
21
21
|
export const signedInAs = async (actor: Promise<unknown>) => Boolean(((await actor.catch(() => null)) as { email?: unknown } | null)?.email)
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* Whether this request carries a session,
|
|
25
|
-
* No auth page sends a referrer, since the reset page's address carries its token.
|
|
24
|
+
* Whether this request carries a session, the screens it opens, the first of them as `home`, which a link back names,
|
|
25
|
+
* and the provider's password bounds. No auth page sends a referrer, since the reset page's address carries its token.
|
|
26
26
|
*/
|
|
27
27
|
export const authGate = createServerFn({ method: 'GET' }).handler(async () => {
|
|
28
28
|
// Imported inside the server half so no auth page ships the screen host to the browser.
|
|
@@ -31,9 +31,9 @@ export const authGate = createServerFn({ method: 'GET' }).handler(async () => {
|
|
|
31
31
|
type Provider = { name: string; server: { $context: Promise<{ password: { config: { minPasswordLength: number; maxPasswordLength: number } } }> } }
|
|
32
32
|
const provider = (asas.auth.strategies as unknown as Provider[]).find((one) => one.name === 'user')!
|
|
33
33
|
const [signedIn, { password }] = await Promise.all([signedInAs(asas.auth.actor(getRequest().headers)), provider.server.$context])
|
|
34
|
-
const
|
|
35
|
-
const home: { path: string; name?: string } =
|
|
36
|
-
return { signedIn, home, password: { min: password.config.minPasswordLength, max: password.config.maxPasswordLength } }
|
|
34
|
+
const entered = signedIn ? ((await loadEnterable())?.screens ?? []) : []
|
|
35
|
+
const home: { path: string; name?: string } = entered[0] ?? { path: '/unauthorized' }
|
|
36
|
+
return { signedIn, home, screens: entered, password: { min: password.config.minPasswordLength, max: password.config.maxPasswordLength } }
|
|
37
37
|
})
|
|
38
38
|
|
|
39
39
|
/**
|
|
@@ -42,8 +42,8 @@ export const authGate = createServerFn({ method: 'GET' }).handler(async () => {
|
|
|
42
42
|
* route context.
|
|
43
43
|
*/
|
|
44
44
|
export const gate = async ({ search }: { search: { redirect?: string } }) => {
|
|
45
|
-
const { signedIn,
|
|
46
|
-
if (signedIn) throw redirect({ href:
|
|
45
|
+
const { signedIn, screens, password } = await authGate()
|
|
46
|
+
if (signedIn) throw redirect({ href: landing({ asked: search.redirect, screens }) })
|
|
47
47
|
return { password }
|
|
48
48
|
}
|
|
49
49
|
|
|
@@ -57,10 +57,10 @@ export const useHomeLabel = () => {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/** What the forgot and reset pages draw for someone already signed in: the way on, and a sign-out. */
|
|
60
|
-
export function AlreadySignedIn({ redirect: asked, home }: { redirect?: string; home: { path: string; name?: string } }) {
|
|
60
|
+
export function AlreadySignedIn({ redirect: asked, home, screens }: { redirect?: string; home: { path: string; name?: string }; screens: readonly { name: string; path: string }[] }) {
|
|
61
61
|
const { t } = useCopy()
|
|
62
62
|
const label = useHomeLabel()
|
|
63
|
-
const to =
|
|
63
|
+
const to = landing({ asked, screens })
|
|
64
64
|
return (
|
|
65
65
|
<AuthFrame title={t('auth.alreadySignedIn', 'You are already signed in')}>
|
|
66
66
|
<div className="grid gap-4">
|
|
@@ -147,4 +147,4 @@ export function AuthBox({
|
|
|
147
147
|
</FieldShell>
|
|
148
148
|
)
|
|
149
149
|
}
|
|
150
|
-
// mithaq-admin
|
|
150
|
+
// mithaq-admin 0.0.0 e498423cf4a61adb8cf6358d043d31ae92d853de4c6939a9c0fe138c848b7211
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { useState, type ComponentProps } from 'react'
|
|
2
|
+
import { Link, useNavigate } from '@tanstack/react-router'
|
|
3
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
4
|
+
import { Button } from '@/components/ui/button'
|
|
5
|
+
import { authArm } from './api.ts'
|
|
6
|
+
import { AlreadySignedIn, AuthBox, AuthFrame, authGate, redirectSearch, useRefusal } from './auth-frame.tsx'
|
|
7
|
+
import { useCopy } from './routes/__root.tsx'
|
|
8
|
+
|
|
9
|
+
/** The search keys of a page a mailed link opens: its token, and where to go after. */
|
|
10
|
+
export const tokenSearch = (search: Record<string, unknown>): { redirect?: string; token?: string } => ({
|
|
11
|
+
...redirectSearch(search),
|
|
12
|
+
...(typeof search.token === 'string' && search.token ? { token: search.token } : {}),
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The address of the account a live link chooses the password of, or `null` for a spent, expired or made-up one: the
|
|
17
|
+
* provider's own answer, read in process. A POST, so the token rides in no further URL.
|
|
18
|
+
*/
|
|
19
|
+
const passwordLink = createServerFn({ method: 'POST' })
|
|
20
|
+
.validator((token: unknown) => (typeof token === 'string' ? token : ''))
|
|
21
|
+
.handler(async ({ data }) => {
|
|
22
|
+
// Imported inside the server half so no page ships the provider to the browser.
|
|
23
|
+
const { asas } = await import('asas')
|
|
24
|
+
type Provider = { name: string; server: { api: { setPasswordState(args: { query: { token: string } }): Promise<{ email: string }> } } }
|
|
25
|
+
const provider = (asas.auth.strategies as unknown as Provider[]).find((one) => one.name === 'user')!
|
|
26
|
+
return data ? provider.server.api.setPasswordState({ query: { token: data } }).then(({ email }) => email, () => null) : null
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
/** The `beforeLoad` of a page a mailed link opens: the auth gate's answer, and the account the link is for. */
|
|
30
|
+
export const passwordGate = async ({ token }: { token?: string }) => ({ ...(await authGate()), email: token ? await passwordLink({ data: token }) : null })
|
|
31
|
+
|
|
32
|
+
type Props = {
|
|
33
|
+
/** A first password, chosen from a sign-up's or an added person's link, rather than a forgotten one replaced. */
|
|
34
|
+
first: boolean
|
|
35
|
+
token?: string
|
|
36
|
+
redirect?: string
|
|
37
|
+
signedIn: boolean
|
|
38
|
+
home: { path: string; name?: string }
|
|
39
|
+
/** The screens a signed-in person opens. */
|
|
40
|
+
screens: readonly { name: string; path: string }[]
|
|
41
|
+
password: { min: number; max: number }
|
|
42
|
+
/** The account the link is for; `null` for a link that names none, `undefined` while unread. */
|
|
43
|
+
email?: string | null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The page a mailed link opens to choose an account's password: a first one, or one forgotten. It shows the account
|
|
48
|
+
* it is for and signs the person in with the password just chosen, through the ordinary sign-in, as Payload's reset
|
|
49
|
+
* does: every rule a sign-in keeps still holds, and nothing is known that the person did not just type.
|
|
50
|
+
*/
|
|
51
|
+
export function ChoosePassword({ first, token, redirect, signedIn, home, screens, password: bounds, email }: Props) {
|
|
52
|
+
const { t } = useCopy()
|
|
53
|
+
const navigate = useNavigate()
|
|
54
|
+
const refused = useRefusal()
|
|
55
|
+
const [password, setPassword] = useState('')
|
|
56
|
+
const [confirmation, setConfirmation] = useState('')
|
|
57
|
+
const [pending, setPending] = useState(false)
|
|
58
|
+
const [done, setDone] = useState(false)
|
|
59
|
+
// The answer, not its sentence, so a language switch says it again in the language chosen.
|
|
60
|
+
const [refusal, setRefusal] = useState<Parameters<typeof refused>[0] | null>(null)
|
|
61
|
+
const [left, setLeft] = useState<ReadonlySet<string>>(() => new Set())
|
|
62
|
+
const title = first ? t('auth.setPassword', 'Choose your password') : t('auth.resetPassword', 'Reset password')
|
|
63
|
+
const wrong = {
|
|
64
|
+
password: () =>
|
|
65
|
+
!password ? t('auth.required', 'This field is required.')
|
|
66
|
+
: password.length < bounds.min ? t('auth.passwordTooShort', 'Password too short')
|
|
67
|
+
: password.length > bounds.max ? t('auth.passwordTooLong', 'Password too long')
|
|
68
|
+
: undefined,
|
|
69
|
+
confirmation: () => (!confirmation ? t('auth.required', 'This field is required.') : confirmation === password ? undefined : t('auth.passwordsDiffer', 'The two passwords are not the same.')),
|
|
70
|
+
}
|
|
71
|
+
const errorOf = (box: keyof typeof wrong) => (left.has(box) ? wrong[box]() : undefined)
|
|
72
|
+
const box = (id: keyof typeof wrong, label: string | undefined, props: ComponentProps<'input'> & { description?: string }) => (
|
|
73
|
+
<AuthBox id={id} label={label} error={errorOf(id)} onLeave={() => setLeft((already) => new Set(already).add(id))} {...props} />
|
|
74
|
+
)
|
|
75
|
+
const signIn = (
|
|
76
|
+
<p className="mt-4 text-center text-sm">
|
|
77
|
+
<Link className="text-primary underline underline-offset-4" to="/auth/sign-in">
|
|
78
|
+
{t('auth.signIn', 'Sign in')}
|
|
79
|
+
</Link>
|
|
80
|
+
</p>
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const submit = async (event: React.FormEvent) => {
|
|
84
|
+
event.preventDefault()
|
|
85
|
+
setLeft(new Set(Object.keys(wrong)))
|
|
86
|
+
if (Object.values(wrong).some((check) => check())) return
|
|
87
|
+
setPending(true)
|
|
88
|
+
setRefusal(null)
|
|
89
|
+
const answered = await authArm('reset-password', { newPassword: password, token })
|
|
90
|
+
if (!answered.ok) {
|
|
91
|
+
setPending(false)
|
|
92
|
+
return setRefusal(answered)
|
|
93
|
+
}
|
|
94
|
+
// A sign-in this refuses, a banned account's, is left to the sign-in page, which says why.
|
|
95
|
+
// Signed in, the sign-in page's own gate sends the person on to `redirect` or their first screen.
|
|
96
|
+
if (email && (await authArm('sign-in/email', { email, password })).ok) return navigate({ to: '/auth/sign-in', search: { redirect } })
|
|
97
|
+
setPending(false)
|
|
98
|
+
setDone(true)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (signedIn) return <AlreadySignedIn redirect={redirect} home={home} screens={screens} />
|
|
102
|
+
if (!token || email === null) {
|
|
103
|
+
return (
|
|
104
|
+
<AuthFrame title={title} description={t('auth.tokenInvalid', 'Token is either invalid or has expired.')}>
|
|
105
|
+
<p className="text-center text-sm">
|
|
106
|
+
<Link className="text-primary underline underline-offset-4" to="/auth/forgot-password">
|
|
107
|
+
{t('auth.forgotPassword', 'Forgot password')}
|
|
108
|
+
</Link>
|
|
109
|
+
</p>
|
|
110
|
+
</AuthFrame>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
if (done) {
|
|
114
|
+
return (
|
|
115
|
+
<AuthFrame title={title} description={first ? t('auth.passwordChosen', 'Your address is confirmed and your password is set. Sign in with it.') : t('auth.passwordReset', 'Password reset successfully.')}>
|
|
116
|
+
{signIn}
|
|
117
|
+
</AuthFrame>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
return (
|
|
121
|
+
<AuthFrame title={title}>
|
|
122
|
+
<form className="grid gap-4" noValidate onSubmit={submit}>
|
|
123
|
+
{/* The account the password is for, which a password manager saves it under. */}
|
|
124
|
+
{email ? <AuthBox id="email" label={t('auth.email', 'Email')} type="email" autoComplete="username" readOnly value={email} onLeave={() => {}} /> : null}
|
|
125
|
+
{box('password', t('auth.newPassword', 'New password'), {
|
|
126
|
+
autoComplete: 'new-password',
|
|
127
|
+
description: t('auth.passwordRule', 'At least {count} characters.')!.replace('{count}', String(bounds.min)),
|
|
128
|
+
onChange: (event) => setPassword(event.target.value),
|
|
129
|
+
type: 'password',
|
|
130
|
+
value: password,
|
|
131
|
+
})}
|
|
132
|
+
{box('confirmation', t('auth.confirmPassword', 'Confirm the password'), {
|
|
133
|
+
autoComplete: 'new-password',
|
|
134
|
+
onChange: (event) => setConfirmation(event.target.value),
|
|
135
|
+
type: 'password',
|
|
136
|
+
value: confirmation,
|
|
137
|
+
})}
|
|
138
|
+
{refusal && (
|
|
139
|
+
<p className="text-destructive text-sm" role="alert">
|
|
140
|
+
{refused(refusal, t('auth.resetFailed', 'That reset did not work.'))}
|
|
141
|
+
</p>
|
|
142
|
+
)}
|
|
143
|
+
<Button disabled={pending} type="submit">
|
|
144
|
+
{first ? t('auth.setPasswordSubmit', 'Confirm and set password') : t('auth.resetPassword', 'Reset password')}
|
|
145
|
+
</Button>
|
|
146
|
+
</form>
|
|
147
|
+
{signIn}
|
|
148
|
+
</AuthFrame>
|
|
149
|
+
)
|
|
150
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Language } from 'asas'
|
|
2
|
+
import type { ComponentsOf } from 'mithaq-admin/components.tsx'
|
|
3
|
+
import { defaultComponents } from 'mithaq-admin/default-components.tsx'
|
|
4
|
+
// add-on: rich-text
|
|
5
|
+
import { richTextComponents } from 'mithaq-admin/rich-text.tsx'
|
|
6
|
+
// end add-on: rich-text
|
|
7
|
+
// add-on: uploads
|
|
8
|
+
import { uploadComponents } from 'mithaq-admin/upload.tsx'
|
|
9
|
+
// end add-on: uploads
|
|
10
|
+
|
|
11
|
+
// Typed against the language, so a kind of the project's own with no control or cell here is a type error, and so
|
|
12
|
+
// is leaving out the rich text or files add-on while the language holds its kind.
|
|
13
|
+
// Spread defaultComponents.controls or defaultComponents.cells to add or replace one.
|
|
14
|
+
// A file of its own, so the server entry reads the map without evaluating the router and its route tree.
|
|
15
|
+
export const components: ComponentsOf<Language> = {
|
|
16
|
+
...defaultComponents,
|
|
17
|
+
controls: {
|
|
18
|
+
...defaultComponents.controls,
|
|
19
|
+
// add-on: rich-text
|
|
20
|
+
...richTextComponents.controls,
|
|
21
|
+
// end add-on: rich-text
|
|
22
|
+
// add-on: uploads
|
|
23
|
+
...uploadComponents.controls,
|
|
24
|
+
// end add-on: uploads
|
|
25
|
+
},
|
|
26
|
+
cells: {
|
|
27
|
+
...defaultComponents.cells,
|
|
28
|
+
// add-on: rich-text
|
|
29
|
+
...richTextComponents.cells,
|
|
30
|
+
// end add-on: rich-text
|
|
31
|
+
// add-on: uploads
|
|
32
|
+
...uploadComponents.cells,
|
|
33
|
+
// end add-on: uploads
|
|
34
|
+
},
|
|
35
|
+
}
|