create-mithaq 0.1.1 → 0.2.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/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 +1094 -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
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { secret } from '
|
|
1
|
+
import { onBackoffice, backofficeApp, backofficeRoles, backofficeURL } from '../mithaq/auth/index.ts'
|
|
2
|
+
import { secret } from '../mithaq/auth/backoffice-roles.ts'
|
|
3
3
|
import { actorPermissions, actorRoles, authActor, userRefs, type Held, type RowTable, type Store } from 'mithaq'
|
|
4
|
-
|
|
4
|
+
// add-on: roles
|
|
5
|
+
import { isOwner, OWNER_ROLE } from './grants.ts'
|
|
6
|
+
// end add-on: roles
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* What a row may name on the actor: `user.<key>` in any kayan's row is checked
|
|
@@ -24,28 +26,28 @@ export type OrganizationActor = {
|
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
|
-
* The
|
|
28
|
-
* `user.role`, never in a tenant's
|
|
29
|
-
*
|
|
29
|
+
* The backoffice administrator's slug. Backoffice roles live on Better Auth's
|
|
30
|
+
* `user.role`, never in a tenant's assignment: `assignedRoles` drops any such
|
|
31
|
+
* slug there, because the access target holds a slug's keys only where this
|
|
30
32
|
* reader answers it.
|
|
31
33
|
*/
|
|
32
|
-
export const
|
|
34
|
+
export const BACKOFFICE = 'super_admin'
|
|
33
35
|
|
|
34
36
|
/**
|
|
35
|
-
* The
|
|
37
|
+
* The backoffice screen, the only place the backoffice roles answer. The app decides
|
|
36
38
|
* this, never the request.
|
|
37
39
|
*/
|
|
38
|
-
export const
|
|
40
|
+
export const BACKOFFICE_SCREEN = 'backoffice'
|
|
39
41
|
|
|
40
42
|
/**
|
|
41
|
-
* The kayan whose session carries the
|
|
43
|
+
* The kayan whose session carries the backoffice column, the one `auth.admin`
|
|
42
44
|
* names. A build may run several Better Auth instances, and only this one's
|
|
43
45
|
* `user.role` is the layer above every tenant.
|
|
44
46
|
*/
|
|
45
|
-
const
|
|
47
|
+
const BACKOFFICE_KAYAN = 'user'
|
|
46
48
|
|
|
47
49
|
/**
|
|
48
|
-
*
|
|
50
|
+
* Backoffice roles read every tenant's rows and write none; staff change a
|
|
49
51
|
* tenant's data only by impersonating a member, which is audited. Every write is
|
|
50
52
|
* an explicit `false`, because a slug a table leaves out still takes the `'*'`
|
|
51
53
|
* row, which opens every write. The row opens the door and the keys decide who
|
|
@@ -53,19 +55,19 @@ const PLATFORM_KAYAN = 'user'
|
|
|
53
55
|
*/
|
|
54
56
|
const readOnly = { create: false, read: true, update: false, delete: false } as const
|
|
55
57
|
|
|
56
|
-
/** The read-only row under every
|
|
57
|
-
export const
|
|
58
|
-
readonly [Slug in keyof typeof
|
|
58
|
+
/** The read-only row under every backoffice role, which every row table of this project spreads in. */
|
|
59
|
+
export const backofficeReads = Object.fromEntries(Object.keys(backofficeRoles).map((slug) => [slug, readOnly])) as {
|
|
60
|
+
readonly [Slug in keyof typeof backofficeRoles]: typeof readOnly
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
/**
|
|
62
|
-
* Every operation refused under every
|
|
63
|
-
* `
|
|
64
|
+
* Every operation refused under every backoffice role, spread in place of
|
|
65
|
+
* `backofficeReads` by tables the backoffice does not read, such as the settings
|
|
64
66
|
* global and the audit log.
|
|
65
67
|
*/
|
|
66
68
|
const closed = { create: false, read: false, update: false, delete: false } as const
|
|
67
|
-
export const
|
|
68
|
-
readonly [Slug in keyof typeof
|
|
69
|
+
export const backofficeClosed = Object.fromEntries(Object.keys(backofficeRoles).map((slug) => [slug, closed])) as {
|
|
70
|
+
readonly [Slug in keyof typeof backofficeRoles]: typeof closed
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
/**
|
|
@@ -89,13 +91,13 @@ export const currentOrganization = (user: unknown) => {
|
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
/**
|
|
92
|
-
* The
|
|
94
|
+
* The backoffice rows of the tenant-owned kayan: `backofficeReads` minus the
|
|
93
95
|
* columns `secret` names. A row's field list closes a column whatever keys the
|
|
94
96
|
* actor holds, and a column an actor may not read is no filter either.
|
|
95
97
|
*/
|
|
96
98
|
const readsNoSecret = { ...readOnly, fields: ['*', ...secret.map((key) => `!${key.slice(key.indexOf('.fields.') + '.fields.'.length)}`)] as readonly string[] }
|
|
97
|
-
const secretClosed = Object.fromEntries(Object.keys(
|
|
98
|
-
readonly [Slug in keyof typeof
|
|
99
|
+
const secretClosed = Object.fromEntries(Object.keys(backofficeRoles).map((slug) => [slug, readsNoSecret])) as {
|
|
100
|
+
readonly [Slug in keyof typeof backofficeRoles]: typeof readsNoSecret
|
|
99
101
|
}
|
|
100
102
|
|
|
101
103
|
/**
|
|
@@ -110,39 +112,46 @@ const secretClosed = Object.fromEntries(Object.keys(platformRoles).map((slug) =>
|
|
|
110
112
|
*/
|
|
111
113
|
export const tenantAccess = {
|
|
112
114
|
'*': { create: true, read: true, update: true, delete: true },
|
|
115
|
+
// add-on: courses
|
|
113
116
|
/**
|
|
114
117
|
* The one row keyed on a system slug, and it narrows: an instructor teaches and
|
|
115
118
|
* never removes a row. Every operation is named in full, because a missing one
|
|
116
119
|
* reads as `false` and a `false` in any applicable row wins.
|
|
117
120
|
*/
|
|
118
121
|
instructor: { create: true, read: true, update: true, delete: false },
|
|
122
|
+
// end add-on: courses
|
|
119
123
|
...secretClosed,
|
|
120
124
|
} as const satisfies RowTable
|
|
121
125
|
|
|
122
126
|
/**
|
|
123
127
|
* The roles a kayan's rows are read through; its parameter also types `user` for
|
|
124
|
-
* every row this project writes. Slugs come from the
|
|
128
|
+
* every row this project writes. Slugs come from the access target for the
|
|
125
129
|
* active organization, off the trusted snapshot, so an object naming its own
|
|
126
|
-
* roles is no one.
|
|
127
|
-
* `administrator` in its tenant, so the role that manages roles is never lost.
|
|
130
|
+
* roles is no one.
|
|
128
131
|
*/
|
|
129
132
|
export const assignedRoles = (actor: OrganizationActor): readonly string[] => {
|
|
130
133
|
if (!authActor(actor)) return []
|
|
131
|
-
// The
|
|
134
|
+
// The backoffice app answers the backoffice roles alone and the tenant app the
|
|
132
135
|
// membership's roles alone, never unioned, so neither adds keys on the other's
|
|
133
136
|
// screens. Which app serves the request is the app's own fact, never the request's.
|
|
134
|
-
if (
|
|
137
|
+
if (onBackoffice()) return backofficeRolesOf(actor)
|
|
135
138
|
if (!currentOrganization(actor)) return []
|
|
136
|
-
const assigned = (actorRoles(actor) ?? []).filter((slug) => !Object.hasOwn(
|
|
137
|
-
|
|
139
|
+
const assigned = (actorRoles(actor) ?? []).filter((slug) => !Object.hasOwn(backofficeRoles, slug))
|
|
140
|
+
// add-on: roles
|
|
141
|
+
// The slugs are the `memberRoles` kayan's, and `member.role` grants one thing
|
|
142
|
+
// only: an owner always holds `administrator` in its tenant, so the role that
|
|
143
|
+
// manages roles is never lost.
|
|
144
|
+
if (isOwner(authActor(actor)?.memberRole) && !assigned.includes(OWNER_ROLE)) return [...assigned, OWNER_ROLE]
|
|
145
|
+
// end add-on: roles
|
|
146
|
+
return assigned
|
|
138
147
|
}
|
|
139
148
|
|
|
140
149
|
/**
|
|
141
150
|
* Whether `assignedRoles` answers this actor `super_admin`, which only the
|
|
142
|
-
*
|
|
151
|
+
* backoffice app does. It is the administrator of `auth.admin`, the one answer
|
|
143
152
|
* local auth's `_verified` writes ask, and the reader and writer of local accounts.
|
|
144
153
|
*/
|
|
145
|
-
export const superAdmin = ({ user }: { user: unknown }) => assignedRoles(user as OrganizationActor).includes(
|
|
154
|
+
export const superAdmin = ({ user }: { user: unknown }) => assignedRoles(user as OrganizationActor).includes(BACKOFFICE)
|
|
146
155
|
|
|
147
156
|
/**
|
|
148
157
|
* Every operation refused to every actor, the administrator included, for the
|
|
@@ -153,69 +162,99 @@ export const superAdmin = ({ user }: { user: unknown }) => assignedRoles(user as
|
|
|
153
162
|
export const providerOnly = { create: () => false, read: () => false, update: () => false, delete: () => false } as const
|
|
154
163
|
|
|
155
164
|
/**
|
|
156
|
-
* The
|
|
157
|
-
* asks. Several are comma-separated, and only a
|
|
165
|
+
* The backoffice roles this actor holds on Better Auth's `user.role`, whichever app
|
|
166
|
+
* asks. Several are comma-separated, and only a backoffice role's name counts.
|
|
158
167
|
*
|
|
159
168
|
* The column is read off the trusted snapshot, and only from the admin kayan's
|
|
160
169
|
* session on a Better Auth strategy: a build may run several instances that all
|
|
161
170
|
* write `user.role`, and local auth spreads its kayan's row onto the actor, so a
|
|
162
|
-
* `role` column there would otherwise hand someone a
|
|
171
|
+
* `role` column there would otherwise hand someone a backoffice role.
|
|
163
172
|
*/
|
|
164
|
-
export const
|
|
173
|
+
export const backofficeRolesOf = (actor: unknown): string[] => {
|
|
165
174
|
const trusted = authActor(actor)
|
|
166
|
-
if (!trusted || trusted.strategy !== 'better-auth' || trusted.collection !==
|
|
167
|
-
return [...new Set(trusted.role.split(',').map((slug) => slug.trim()).filter((slug) => Object.hasOwn(
|
|
175
|
+
if (!trusted || trusted.strategy !== 'better-auth' || trusted.collection !== BACKOFFICE_KAYAN || typeof trusted.role !== 'string') return []
|
|
176
|
+
return [...new Set(trusted.role.split(',').map((slug) => slug.trim()).filter((slug) => Object.hasOwn(backofficeRoles, slug)))]
|
|
168
177
|
}
|
|
169
178
|
|
|
170
179
|
/**
|
|
171
|
-
* Where the tenant app's header links the
|
|
172
|
-
*
|
|
173
|
-
* grants nothing: the
|
|
180
|
+
* Where the tenant app's header links the backoffice app, for an actor whose
|
|
181
|
+
* backoffice roles hold `screen.backoffice`; nothing otherwise. It is a link and
|
|
182
|
+
* grants nothing: the backoffice app's own server decides entry. A project that
|
|
183
|
+
* serves no backoffice app links nowhere.
|
|
174
184
|
*/
|
|
175
|
-
export const
|
|
176
|
-
if (
|
|
177
|
-
const enters =
|
|
178
|
-
const held =
|
|
179
|
-
return held === '*' || (held as readonly string[]).includes(`screen.${
|
|
185
|
+
export const backofficeEntry = (actor: unknown): string | undefined => {
|
|
186
|
+
if (onBackoffice() || !backofficeApp) return undefined
|
|
187
|
+
const enters = backofficeRolesOf(actor).some((slug) => {
|
|
188
|
+
const held = backofficeRoles[slug as keyof typeof backofficeRoles].permissions
|
|
189
|
+
return held === '*' || (held as readonly string[]).includes(`screen.${BACKOFFICE_SCREEN}`)
|
|
180
190
|
})
|
|
181
|
-
return enters ? new URL(`/${
|
|
191
|
+
return enters ? new URL(`/${BACKOFFICE_SCREEN}`, backofficeURL).href : undefined
|
|
182
192
|
}
|
|
183
193
|
|
|
184
194
|
/**
|
|
185
195
|
* The keys a provider kayan's columns are graded by, which `rows()` takes as its
|
|
186
196
|
* `holds`: in the tenant app the id and the name alone, and only where the actor
|
|
187
|
-
* holds their keys; in the
|
|
197
|
+
* holds their keys; in the backoffice app the actor's own keys. An actor holding no
|
|
188
198
|
* key reads no column, since an empty set grades every column closed where
|
|
189
199
|
* `undefined` would grade none.
|
|
190
200
|
*/
|
|
191
201
|
export const namesInTenant = (kayan: string) => (actor: unknown): Held => {
|
|
192
202
|
const held = actorPermissions(actor)
|
|
193
|
-
if (
|
|
203
|
+
if (onBackoffice()) return held
|
|
194
204
|
return new Set(['id', 'name'].map((field) => `${kayan}.fields.${field}`).filter((key) => held === true || held?.has(key) === true))
|
|
195
205
|
}
|
|
196
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Whom a role reads among its organization's people when it holds no
|
|
209
|
+
* `member.find`: the members assigned one of the roles named here, beside
|
|
210
|
+
* itself. A role left out, a custom role included, reads itself alone, so a
|
|
211
|
+
* key to read people never by itself opens the staff to a portal role.
|
|
212
|
+
*/
|
|
213
|
+
export const peopleAudience: Readonly<Record<string, readonly string[]>> = {
|
|
214
|
+
// add-on: courses
|
|
215
|
+
// The students it enrols, never the staff.
|
|
216
|
+
instructor: ['student'],
|
|
217
|
+
// end add-on: courses
|
|
218
|
+
}
|
|
219
|
+
|
|
197
220
|
/**
|
|
198
221
|
* The read the person's own rows are ANDed with, so it narrows and never widens:
|
|
199
222
|
* the members of the organization the actor's session has active, read off the
|
|
200
|
-
* store
|
|
223
|
+
* store, all of them to a holder of `member.find`, who lists them through the
|
|
224
|
+
* provider anyway, and otherwise the actor and its roles' `peopleAudience`. The
|
|
225
|
+
* organization is the trusted snapshot's, so a claim is no one's.
|
|
201
226
|
*/
|
|
202
227
|
export const membersOf = (store: Pick<Store, 'find'>) => async ({ user: actor }: { user: unknown }) => {
|
|
203
|
-
// A
|
|
204
|
-
if (
|
|
228
|
+
// A backoffice role reads every person in the backoffice app; the rows answer who holds one.
|
|
229
|
+
if (onBackoffice()) return true
|
|
205
230
|
const active = currentOrganization(actor)
|
|
206
231
|
if (!active) return false
|
|
207
|
-
const
|
|
232
|
+
const all = { limit: 0, page: 1, sort: { field: 'id', direction: 'asc' } } as const
|
|
233
|
+
const members = await store.find({ kayan: 'member', args: { ...all, where: { organizationId: { equals: active } } } })
|
|
208
234
|
const ids = members.docs.map((row) => row.userId).filter((id): id is string => typeof id === 'string')
|
|
209
|
-
|
|
235
|
+
const held = actorPermissions(actor)
|
|
236
|
+
if (held === true || held?.has('member.find')) return ids.length ? { id: { in: ids } } : false
|
|
237
|
+
const seen = new Set([authActor(actor)?.id])
|
|
238
|
+
// add-on: roles
|
|
239
|
+
// A custom role's name is a tenant's to write, so one named `constructor` reads no inherited key.
|
|
240
|
+
const audience =[...new Set(assignedRoles(actor as OrganizationActor).flatMap((slug) => (Object.hasOwn(peopleAudience, slug) ? peopleAudience[slug]! : [])))]
|
|
241
|
+
if (audience.length) {
|
|
242
|
+
const assigned = await store.find({ kayan: 'memberRoles', args: { ...all, where: { organization: { equals: active }, role: { in: audience } } } })
|
|
243
|
+
for (const row of assigned.docs) seen.add(row.user)
|
|
244
|
+
}
|
|
245
|
+
// end add-on: roles
|
|
246
|
+
// An assignment can outlive its membership, so only a current member is seen.
|
|
247
|
+
const reached = ids.filter((id) => seen.has(id))
|
|
248
|
+
return reached.length ? { id: { in: reached } } : false
|
|
210
249
|
}
|
|
211
250
|
|
|
212
251
|
/**
|
|
213
252
|
* Whether an actor reads past what a kayan's own read narrows for roles not
|
|
214
|
-
* holding `key`: in the
|
|
215
|
-
* or in a tenant, holding that key.
|
|
216
|
-
*
|
|
253
|
+
* holding `key`: in the backoffice app, whose staff read every tenant's rows whole,
|
|
254
|
+
* or in a tenant, holding that key. The courses add-on narrows archived courses,
|
|
255
|
+
* sessions and covers of unreadable courses and other people's seats this way.
|
|
217
256
|
*/
|
|
218
257
|
export const readsPast = (actor: unknown, key: string) => {
|
|
219
258
|
const held = actorPermissions(actor)
|
|
220
|
-
return
|
|
259
|
+
return onBackoffice() || held === true || held?.has(key) === true
|
|
221
260
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { SystemRoles } from 'mithaq'
|
|
2
|
+
// without add-on: roles
|
|
3
|
+
// import { defaultMemberRoles } from './default-member-roles.ts'
|
|
4
|
+
// end without add-on: roles
|
|
2
5
|
|
|
6
|
+
// add-on: courses
|
|
3
7
|
/** A session's columns but its `enrollments`, which the registrar and the instructor read alike. */
|
|
4
8
|
const sessionColumns = [
|
|
5
9
|
'sessions.fields.id',
|
|
@@ -12,9 +16,29 @@ const sessionColumns = [
|
|
|
12
16
|
'sessions.fields.venue',
|
|
13
17
|
'sessions.fields.format',
|
|
14
18
|
'sessions.fields.archived',
|
|
19
|
+
'sessions.fields.archiveReason',
|
|
15
20
|
'sessions.fields.details',
|
|
16
21
|
] as const
|
|
17
22
|
|
|
23
|
+
/**
|
|
24
|
+
* What the portal's catalogue and course page read beside a course's own columns, for its roles alike: the
|
|
25
|
+
* sessions with the seats each has left, and the names of the tags it carries.
|
|
26
|
+
*/
|
|
27
|
+
const catalogueReads = [
|
|
28
|
+
'courses.findWithSessions',
|
|
29
|
+
'courses.fields.tags',
|
|
30
|
+
'courses.fields.selfEnroll',
|
|
31
|
+
'tags.find',
|
|
32
|
+
'tags.findByID',
|
|
33
|
+
'tags.fields.id',
|
|
34
|
+
'tags.fields.name',
|
|
35
|
+
] as const
|
|
36
|
+
|
|
37
|
+
// add-on: settings
|
|
38
|
+
/** Whether the portal is closed for maintenance, and where to write, which the portal's foot reads for its roles alike. */
|
|
39
|
+
const portalSettings = ['settings.findOne', 'settings.fields.maintenance', 'settings.fields.contactEmail'] as const
|
|
40
|
+
// end add-on: settings
|
|
41
|
+
|
|
18
42
|
/** A course's cover, read and never changed, which the portal's roles read alike. */
|
|
19
43
|
const coverReads = [
|
|
20
44
|
'media.find',
|
|
@@ -32,41 +56,43 @@ const coverReads = [
|
|
|
32
56
|
'media.fields.updatedAt',
|
|
33
57
|
'media.fields.width',
|
|
34
58
|
] as const
|
|
59
|
+
// end add-on: courses
|
|
35
60
|
|
|
36
61
|
/**
|
|
37
62
|
* The roles this project ships to every tenant, written in code and never a
|
|
38
|
-
* row: the access target takes them whole and reads what each holds from here
|
|
39
|
-
*
|
|
40
|
-
*
|
|
63
|
+
* row: the access target takes them whole and reads what each holds from here.
|
|
64
|
+
* A page lists them from here too, which is why this file imports no server
|
|
65
|
+
* code.
|
|
41
66
|
*
|
|
42
67
|
* A slug here is the whole vocabulary of role names: a row table may be keyed on
|
|
43
68
|
* one of them or on `'*'` and on nothing else, and `buildAsas` refuses a key
|
|
44
69
|
* that is not in this project's catalog exactly as it does for a row's own.
|
|
45
70
|
*
|
|
46
|
-
*
|
|
47
|
-
* `member.role`, which holds `owner`, `admin` or `member`, stays what the
|
|
48
|
-
* provider uses for settings, members and invitations, and nothing here reads
|
|
49
|
-
* it. Sharing one comma-joined column would inherit the provider's own open
|
|
50
|
-
* comma-in-a-role-name bug.
|
|
51
|
-
*
|
|
52
|
-
* A new organization's creator is assigned `administrator` while the
|
|
53
|
-
* organization is being created, by the hook the template wires into the
|
|
54
|
-
* provider, so a fresh tenant has a root granter from its first moment. That
|
|
55
|
-
* row is written straight through the store, because a managed create is graded
|
|
56
|
-
* against the actor making it and the creator is in no organization until that
|
|
57
|
-
* route puts it in one.
|
|
58
|
-
*
|
|
59
|
-
* The platform roles, `super_admin` among them, are not here: they are staff
|
|
71
|
+
* The backoffice roles, `super_admin` among them, are not here: they are staff
|
|
60
72
|
* above every tenant, held on Better Auth's `user.role`, and the access target
|
|
61
|
-
* holds their keys in the
|
|
73
|
+
* holds their keys in the backoffice app alone. Inside a tenant every actor, a
|
|
62
74
|
* `super_admin` included, holds what its membership's roles hold and nothing
|
|
63
|
-
* more.
|
|
75
|
+
* more.
|
|
64
76
|
*/
|
|
65
77
|
export const tenantRoles = {
|
|
78
|
+
// add-on: roles
|
|
79
|
+
// These are business roles and never governance, and the `roles` kayan holds
|
|
80
|
+
// a tenant's own roles beside them; an assignment names one of either. Better
|
|
81
|
+
// Auth's own `member.role`, which holds `owner`, `admin` or `member`, stays
|
|
82
|
+
// what the provider uses for settings, members and invitations, and nothing
|
|
83
|
+
// here reads it. Sharing one comma-joined column would inherit the provider's
|
|
84
|
+
// own open comma-in-a-role-name bug.
|
|
85
|
+
//
|
|
86
|
+
// A new organization's creator is assigned `administrator` while the
|
|
87
|
+
// organization is being created, by the hook the template wires into the
|
|
88
|
+
// provider, so a fresh tenant has a root granter from its first moment. That
|
|
89
|
+
// row is written straight through the store, because a managed create is
|
|
90
|
+
// graded against the actor making it and the creator is in no organization
|
|
91
|
+
// until that route puts it in one.
|
|
66
92
|
/**
|
|
67
93
|
* The tenant's root granter: every key of the project, which is what lets it
|
|
68
94
|
* author a custom role at all. `'*'` holds every screen's key too, so it
|
|
69
|
-
* enters the dashboard and the portal alike. It never reaches the
|
|
95
|
+
* enters the dashboard and the portal alike. It never reaches the backoffice
|
|
70
96
|
* app, where the reader answers no membership role at all.
|
|
71
97
|
* `grantedPermissions` returns early on a granter holding everything, so this
|
|
72
98
|
* role and no other may hand out a key it will never use itself.
|
|
@@ -80,11 +106,15 @@ export const tenantRoles = {
|
|
|
80
106
|
label: { ar: 'مسجل', en: 'Registrar' },
|
|
81
107
|
permissions: [
|
|
82
108
|
'screen.dashboard',
|
|
109
|
+
// add-on: courses
|
|
83
110
|
'courses.create',
|
|
84
111
|
'courses.find',
|
|
85
112
|
'courses.count',
|
|
86
113
|
'courses.findByID',
|
|
87
114
|
'courses.update',
|
|
115
|
+
// A course leaves the catalogue and comes back through its own operations, each with its reason.
|
|
116
|
+
'courses.archive',
|
|
117
|
+
'courses.unarchive',
|
|
88
118
|
'courses.fields.archived',
|
|
89
119
|
'courses.fields.archiveReason',
|
|
90
120
|
'courses.fields.createdAt',
|
|
@@ -97,6 +127,7 @@ export const tenantRoles = {
|
|
|
97
127
|
'courses.fields.organization',
|
|
98
128
|
'courses.fields.title',
|
|
99
129
|
'courses.fields.updatedAt',
|
|
130
|
+
'courses.fields.selfEnroll',
|
|
100
131
|
// Whether a course is a draft, its versions and the restore of one: the
|
|
101
132
|
// registrar edits the catalogue, so it reads a course's history and puts
|
|
102
133
|
// an older copy back.
|
|
@@ -113,6 +144,7 @@ export const tenantRoles = {
|
|
|
113
144
|
// read of it is refused for its fields and its pages draw nothing. Not a
|
|
114
145
|
// session's `enrollments`, which reads a kayan this role does not.
|
|
115
146
|
...sessionColumns,
|
|
147
|
+
// end add-on: courses
|
|
116
148
|
'tags.find',
|
|
117
149
|
'tags.count',
|
|
118
150
|
'tags.findByID',
|
|
@@ -121,6 +153,7 @@ export const tenantRoles = {
|
|
|
121
153
|
'tags.fields.updatedAt',
|
|
122
154
|
'tags.fields.organization',
|
|
123
155
|
'tags.fields.name',
|
|
156
|
+
// add-on: media
|
|
124
157
|
// The images a course's cover is picked from, which it uploads,
|
|
125
158
|
// re-describes and deletes once no course points at one.
|
|
126
159
|
'media.create',
|
|
@@ -141,6 +174,8 @@ export const tenantRoles = {
|
|
|
141
174
|
'media.fields.organization',
|
|
142
175
|
'media.fields.updatedAt',
|
|
143
176
|
'media.fields.width',
|
|
177
|
+
// end add-on: media
|
|
178
|
+
// add-on: audit-log
|
|
144
179
|
// The activity panel on each edit page it opens: the entries of the
|
|
145
180
|
// kayan it lists, each change graded by the column's own key above.
|
|
146
181
|
'auditLog.find',
|
|
@@ -152,8 +187,10 @@ export const tenantRoles = {
|
|
|
152
187
|
'auditLog.fields.actor',
|
|
153
188
|
'auditLog.fields.impersonatedBy',
|
|
154
189
|
'auditLog.fields.changes',
|
|
190
|
+
// end add-on: audit-log
|
|
155
191
|
],
|
|
156
192
|
},
|
|
193
|
+
// add-on: courses
|
|
157
194
|
/**
|
|
158
195
|
* Teaches: reads the catalogue and enrols. It holds no `courses.archive`. It
|
|
159
196
|
* works in the portal, which draws the courses and the enrolments, and never
|
|
@@ -171,10 +208,14 @@ export const tenantRoles = {
|
|
|
171
208
|
'courses.fields.cover',
|
|
172
209
|
'courses.fields.details',
|
|
173
210
|
'courses.fields.id',
|
|
174
|
-
'courses.fields.tags',
|
|
175
211
|
'courses.fields.organization',
|
|
176
212
|
'courses.fields.title',
|
|
177
213
|
'courses.fields.updatedAt',
|
|
214
|
+
// What the portal's catalogue draws on a course's card and its page.
|
|
215
|
+
'courses.fields.summary',
|
|
216
|
+
'courses.fields.fee',
|
|
217
|
+
'courses.fields.currency',
|
|
218
|
+
'courses.fields.delivery',
|
|
178
219
|
// It reads published courses alone, and whether one is published with them.
|
|
179
220
|
'courses.fields._status',
|
|
180
221
|
'enrollments.create',
|
|
@@ -195,15 +236,19 @@ export const tenantRoles = {
|
|
|
195
236
|
'sessions.count',
|
|
196
237
|
'sessions.findByID',
|
|
197
238
|
...sessionColumns,
|
|
198
|
-
// The
|
|
239
|
+
// The students of its organization by their id and name alone, which the
|
|
199
240
|
// enrollment drawer's student picker lists and an enrollment's student is
|
|
200
|
-
// titled by.
|
|
241
|
+
// titled by: `peopleAudience` in `tenant-access.ts` names whom it reads.
|
|
201
242
|
'user.find',
|
|
202
243
|
'user.count',
|
|
203
244
|
'user.findByID',
|
|
204
245
|
'user.fields.id',
|
|
205
246
|
'user.fields.name',
|
|
206
247
|
...coverReads,
|
|
248
|
+
...catalogueReads,
|
|
249
|
+
// add-on: settings
|
|
250
|
+
...portalSettings,
|
|
251
|
+
// end add-on: settings
|
|
207
252
|
],
|
|
208
253
|
},
|
|
209
254
|
/**
|
|
@@ -216,8 +261,9 @@ export const tenantRoles = {
|
|
|
216
261
|
* alone would hand a student every other student's seat. The enrollments'
|
|
217
262
|
* own access narrows them to the student's own seats, since it holds no
|
|
218
263
|
* `enrollments.others`, and the sessions' to those of the courses it reads.
|
|
219
|
-
* It
|
|
220
|
-
*
|
|
264
|
+
* It takes a seat for itself alone, in a course that lets its students enroll
|
|
265
|
+
* themselves and before the session starts, and gives one back before then:
|
|
266
|
+
* the enrollments' own steps hold it to that. It changes no seat.
|
|
221
267
|
*/
|
|
222
268
|
student: {
|
|
223
269
|
label: { ar: 'طالب', en: 'Student' },
|
|
@@ -245,11 +291,15 @@ export const tenantRoles = {
|
|
|
245
291
|
'enrollments.find',
|
|
246
292
|
'enrollments.count',
|
|
247
293
|
'enrollments.findByID',
|
|
294
|
+
'enrollments.create',
|
|
295
|
+
'enrollments.delete',
|
|
248
296
|
'enrollments.fields.id',
|
|
249
297
|
'enrollments.fields.createdAt',
|
|
250
298
|
'enrollments.fields.updatedAt',
|
|
251
299
|
'enrollments.fields.organization',
|
|
252
300
|
'enrollments.fields.session',
|
|
301
|
+
// Itself, which its own create fills and every seat it reads names.
|
|
302
|
+
'enrollments.fields.student',
|
|
253
303
|
// The sessions its seats name, which the enrollment's session cell draws
|
|
254
304
|
// by course and start.
|
|
255
305
|
'sessions.find',
|
|
@@ -257,6 +307,15 @@ export const tenantRoles = {
|
|
|
257
307
|
'sessions.findByID',
|
|
258
308
|
...sessionColumns,
|
|
259
309
|
...coverReads,
|
|
310
|
+
...catalogueReads,
|
|
311
|
+
// add-on: settings
|
|
312
|
+
...portalSettings,
|
|
313
|
+
// end add-on: settings
|
|
260
314
|
],
|
|
261
315
|
},
|
|
316
|
+
// end add-on: courses
|
|
317
|
+
// end add-on: roles
|
|
318
|
+
// without add-on: roles
|
|
319
|
+
// ...defaultMemberRoles,
|
|
320
|
+
// end without add-on: roles
|
|
262
321
|
} as const satisfies SystemRoles
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// Listing the step without widening the input protects nothing, since the strict update
|
|
11
11
|
// shape refuses the key by name.
|
|
12
12
|
//
|
|
13
|
-
// The compare is against `ctx.dataBefore`, loaded
|
|
13
|
+
// The compare is against `ctx.dataBefore`, loaded under the update's own row lock inside its
|
|
14
14
|
// transaction, so on a store that honours the lock a second writer waits and no write lands
|
|
15
15
|
// between the compare and the store step. A store that ignores the lock lets both through.
|
|
16
16
|
//
|
|
@@ -38,4 +38,4 @@ export const expectedUpdatedAt = (ctx: Ctx<{ expectedUpdatedAt?: Date | string }
|
|
|
38
38
|
}
|
|
39
39
|
return ctx
|
|
40
40
|
}
|
|
41
|
-
// mithaq-admin
|
|
41
|
+
// mithaq-admin 0.0.0 10cc4a57db299d48c4fd53314ed42e02fc04b0464a7fa8a0c512cefc7ccb2b82
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// `mithaq-storage` registry item and owned by the project: `mithaq upgrade
|
|
9
9
|
// --check` reports one that was edited, and an edit is merged by hand.
|
|
10
10
|
import { randomBytes } from 'node:crypto'
|
|
11
|
-
import { defaultFileSize, type StorageConfig } from 'mithaq'
|
|
11
|
+
import { defaultFileSize, defaultMaxBodyBytes, type StorageConfig } from 'mithaq'
|
|
12
12
|
import {
|
|
13
13
|
defaultDisposition,
|
|
14
14
|
defaultKey,
|
|
@@ -81,6 +81,8 @@ export const storageConfig: StorageConfig = {
|
|
|
81
81
|
adapter,
|
|
82
82
|
// The largest file a kayan holds when its targets.storage names no fileSize.
|
|
83
83
|
fileSize: defaultFileSize,
|
|
84
|
+
// The largest instructions request read; files themselves never come this way.
|
|
85
|
+
maxBodyBytes: defaultMaxBodyBytes,
|
|
84
86
|
key: defaultKey,
|
|
85
87
|
verify: defaultVerify,
|
|
86
88
|
disposition: defaultDisposition,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// The dashboard's Activity page: every change in the organization, read as the
|
|
2
|
+
// viewer from the log's `find`, which answers this tenant's entries alone, of
|
|
3
|
+
// the kayan the viewer lists. A file of its own beside `activity.tsx`, since the
|
|
4
|
+
// shell it draws in holds the app's own pieces.
|
|
5
|
+
import { useEffect, useState } from 'react'
|
|
6
|
+
import { useAccess, useLocale } from 'mithaq/admin/react'
|
|
7
|
+
import { ActivityLog, filtersOf, type ActivityQuery } from './activity.tsx'
|
|
8
|
+
import { AppShell } from './app-shell.tsx'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The page, filtered at first by what its link names, such as one record's
|
|
12
|
+
* entries from that record's panel. The link is read once the page is in the
|
|
13
|
+
* browser, since the server renders no location.
|
|
14
|
+
*/
|
|
15
|
+
export function ActivityPage() {
|
|
16
|
+
const { t } = useLocale()
|
|
17
|
+
const people = useAccess().collections?.user?.read === true
|
|
18
|
+
const [query, setQuery] = useState<ActivityQuery>()
|
|
19
|
+
useEffect(() => setQuery(filtersOf({ search: window.location.search, people })), [])
|
|
20
|
+
return (
|
|
21
|
+
<AppShell
|
|
22
|
+
regions={{
|
|
23
|
+
header: <h1>{t('auditLog.activity', 'Activity')}</h1>,
|
|
24
|
+
main: (
|
|
25
|
+
<section data-dashboard="activity" className="grid gap-4">
|
|
26
|
+
<p className="text-muted-foreground">{t('auditLog.about', 'Every change in this organization, newest first.')}</p>
|
|
27
|
+
{query ? <ActivityLog query={query} onQuery={setQuery} /> : <p className="text-muted-foreground text-sm">{t('auditLog.loading', 'Loading activity…')}</p>}
|
|
28
|
+
</section>
|
|
29
|
+
),
|
|
30
|
+
}}
|
|
31
|
+
/>
|
|
32
|
+
)
|
|
33
|
+
}
|