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
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
import { useContext, useEffect, useMemo, useState } from 'react'
|
|
7
7
|
import { same, type ClientField, type ComponentProps, type Row } from 'mithaq/admin'
|
|
8
8
|
import { FormContext, useAccess, useClient, useKayan, useLocale, useRecord, useScreen } from 'mithaq/admin/react'
|
|
9
|
-
import { empty } from '../audit-log/sink.ts'
|
|
10
9
|
import {
|
|
11
10
|
AlertDialog,
|
|
12
11
|
AlertDialogAction,
|
|
@@ -24,11 +23,24 @@ import { Label } from '@/components/ui/label'
|
|
|
24
23
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
|
25
24
|
import { Sheet, SheetClose, SheetContent, SheetFooter, SheetHeader, SheetTitle } from '@/components/ui/sheet'
|
|
26
25
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
|
27
|
-
import { blocksOf,
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
26
|
+
import { blocksOf, richTextPlainText } from 'mithaq-admin/rich-text-line.ts'
|
|
27
|
+
import { cellFor, useComponents } from 'mithaq-admin/components.tsx'
|
|
28
|
+
import type { FindApi, FoundPage } from 'mithaq-admin/control-props.ts'
|
|
29
|
+
import { labelFor, titleOf } from 'mithaq-admin/copy.ts'
|
|
30
|
+
import { formatDate } from 'mithaq-admin/format.tsx'
|
|
31
|
+
import { doneWords } from 'mithaq-admin/form-rules.tsx'
|
|
32
|
+
import { carry } from 'mithaq-admin/toast.tsx'
|
|
33
|
+
import { refusalKeyOf } from 'mithaq-admin/refusal.ts'
|
|
34
|
+
import { endonym } from 'mithaq-admin/locale-menu.ts'
|
|
35
|
+
import { CloseButton } from 'mithaq-admin/record-frame.tsx'
|
|
36
|
+
import { useRelationshipTitles } from 'mithaq-admin/relationship-cell.tsx'
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* What a version's column draws as a dash: absent, `null`, an empty string or
|
|
40
|
+
* an empty list, as the audit log's activity panel reads it. Its own here, so a
|
|
41
|
+
* project without the audit log keeps its versions.
|
|
42
|
+
*/
|
|
43
|
+
const empty = (value: unknown) => value === undefined || value === null || value === '' || (Array.isArray(value) && !value.length)
|
|
32
44
|
|
|
33
45
|
/** One version as `findVersions` answers it. */
|
|
34
46
|
export type Version = {
|
|
@@ -257,6 +269,7 @@ function Compare({ version, versions, parent, onClose }: { version: Version; ver
|
|
|
257
269
|
[kayans],
|
|
258
270
|
)
|
|
259
271
|
const titles = useRelationshipTitles({ fields: kayan.fields, docs: [version.version, from?.version ?? {}], context: { api, client: kayans, locale } })
|
|
272
|
+
const { cells } = useComponents()
|
|
260
273
|
const copy = { t, locale }
|
|
261
274
|
// A rich text value whole, a block to a line, where its list cell draws the
|
|
262
275
|
// first block alone: a change further down would read as no change. A block the
|
|
@@ -265,7 +278,7 @@ function Compare({ version, versions, parent, onClose }: { version: Version; ver
|
|
|
265
278
|
// as well as tinted, so the mark does not rest on colour alone.
|
|
266
279
|
const drawn = (field: ClientField, value: unknown, other: unknown, mark: 'del' | 'ins') => {
|
|
267
280
|
if (empty(value)) return '—'
|
|
268
|
-
if (field.kind !== 'richText') return cellFor(field)({ kayan, field, value, t, locale, titles })
|
|
281
|
+
if (field.kind !== 'richText') return cellFor({ cells, field })({ kayan, field, value, t, locale, titles })
|
|
269
282
|
const blocks = changedBlocks({ side: value, other })
|
|
270
283
|
// A value of images or empty blocks alone has no words, and reads as empty.
|
|
271
284
|
if (!blocks.length) return '—'
|
|
@@ -301,6 +314,7 @@ function Compare({ version, versions, parent, onClose }: { version: Version; ver
|
|
|
301
314
|
// since is refused 409 rather than burying it.
|
|
302
315
|
const read = takes ? versions.find((one) => one.latest)?.version.updatedAt : undefined
|
|
303
316
|
await client({ kayan: kayan.name, operation: 'restoreVersion', input: { id: version.id, ...(read !== undefined && { expectedUpdatedAt: read }) }, draft })
|
|
317
|
+
carry(doneWords({ kayan, copy: { t, locale }, operation: { name: 'restoreVersion' } }))
|
|
304
318
|
window.location.reload()
|
|
305
319
|
} catch (thrown) {
|
|
306
320
|
setRestoring(false)
|
|
@@ -349,7 +363,7 @@ function Compare({ version, versions, parent, onClose }: { version: Version; ver
|
|
|
349
363
|
) : 'failed' in prior && !against ? (
|
|
350
364
|
<p role="alert" className="text-destructive text-sm">{t('version.failed', 'The versions could not be read.')}</p>
|
|
351
365
|
) : !rows.length ? (
|
|
352
|
-
<p className="text-muted-foreground text-sm">{t('
|
|
366
|
+
<p className="text-muted-foreground text-sm">{t('version.noReadableChange', 'No field you may read changed.')}</p>
|
|
353
367
|
) : (
|
|
354
368
|
<ul className="grid gap-3 text-sm">
|
|
355
369
|
{/* Which side is which, over the two columns, since on a phone
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// What `/portal` itself draws for the courses example, in the portal's own look: a greeting, the viewer's next
|
|
2
|
+
// session, and a few courses on offer it holds no seat in yet. It is the asas's own, since a screen's root page is
|
|
3
|
+
// drawn from the project, and draws with the courses feature's portal pieces.
|
|
4
|
+
import { useSyncExternalStore } from 'react'
|
|
5
|
+
import { CalendarDays, MapPin } from 'lucide-react'
|
|
6
|
+
import { useQuery } from '@tanstack/react-query'
|
|
7
|
+
import { useClient, useLocale, useScreen, useUser } from 'mithaq/admin/react'
|
|
8
|
+
import { buttonVariants } from '@/components/ui/button'
|
|
9
|
+
import { Skeleton } from '@/components/ui/skeleton'
|
|
10
|
+
import { PortalShell, display } from '@asas/courses/portal/ui/shell.tsx'
|
|
11
|
+
import { CardsLoading, Cover, CourseCard, ReadFailed, usePlace } from '@asas/courses/portal/ui/parts.tsx'
|
|
12
|
+
import { byWhen, partOfDay, startsIn, whenOf } from '@asas/courses/portal/ui/rules.ts'
|
|
13
|
+
import { catalogueOf, readSeats } from '@asas/courses/portal/ui/reads.ts'
|
|
14
|
+
|
|
15
|
+
/** How many courses the home offers before its link to the rest. */
|
|
16
|
+
const offered = 3
|
|
17
|
+
|
|
18
|
+
/** The hour on the viewer's own clock, and none on the server, which knows neither the clock nor the zone. */
|
|
19
|
+
const useHour = () => useSyncExternalStore(() => () => {}, () => new Date().getHours(), () => undefined)
|
|
20
|
+
|
|
21
|
+
export function Home() {
|
|
22
|
+
const { locale, t } = useLocale()
|
|
23
|
+
const client = useClient()
|
|
24
|
+
const screen = useScreen()
|
|
25
|
+
const user = useUser()
|
|
26
|
+
const place = usePlace()
|
|
27
|
+
const hour = useHour()
|
|
28
|
+
const now = Date.now()
|
|
29
|
+
// The viewer's own seats, whoever it is: an instructor reads every seat, and its next session is still its own.
|
|
30
|
+
const mine = useQuery({
|
|
31
|
+
queryKey: ['portal-mine', user.id],
|
|
32
|
+
queryFn: () => readSeats({ client, maxLimit: screen.maxLimit, where: { student: { equals: user.id } } }),
|
|
33
|
+
enabled: !!user.id,
|
|
34
|
+
})
|
|
35
|
+
const catalogue = useQuery({ queryKey: ['portal-catalogue'], queryFn: () => catalogueOf({ client, maxLimit: screen.maxLimit, now }), staleTime: 30_000 })
|
|
36
|
+
const next = mine.data && byWhen(mine.data, now).upcoming[0]
|
|
37
|
+
const held = new Set(mine.data?.map((one) => String(one.course?.id)))
|
|
38
|
+
// A course the student may enroll itself in, with a session to come, and no seat of its own in it yet.
|
|
39
|
+
const open = catalogue.data?.courses.filter((course) => course.selfEnroll === true && !held.has(String(course.id)) && catalogue.data.next.has(String(course.id))).slice(0, offered)
|
|
40
|
+
// One sentence per locale, so each says the name where its grammar puts it.
|
|
41
|
+
const greeting = (hour === undefined ? t('portal.welcome', 'Welcome, {name}') : t(`portal.greeting.${partOfDay(hour)}`, 'Hello, {name}'))!.replace('{name}', () => user.name)
|
|
42
|
+
return (
|
|
43
|
+
<PortalShell
|
|
44
|
+
regions={{
|
|
45
|
+
header: <h1 className={`${display} text-3xl leading-tight sm:text-4xl`}>{greeting}</h1>,
|
|
46
|
+
main: (
|
|
47
|
+
<div data-dashboard="portal" className="grid gap-8">
|
|
48
|
+
{mine.isPending && user.id ? <Skeleton className="h-52 rounded-3xl" /> : next ? (
|
|
49
|
+
<section aria-labelledby="next" className="bg-primary text-primary-foreground grid overflow-hidden rounded-3xl md:grid-cols-[3fr_2fr]">
|
|
50
|
+
<div className="grid content-start gap-3.5 p-5 sm:p-8">
|
|
51
|
+
<h2 id="next" className="text-sm opacity-85">{t('portal.yourNext', 'Your next session')}</h2>
|
|
52
|
+
<p className={`${display} text-2xl leading-tight sm:text-3xl`}>{String(next.course?.title ?? '')}</p>
|
|
53
|
+
<div className="grid gap-1.5 text-[15px]">
|
|
54
|
+
<span className="flex items-center gap-2"><CalendarDays aria-hidden className="size-4.5 shrink-0" />{whenOf({ value: next.startsAt, locale })}</span>
|
|
55
|
+
{place(next.session) ? <span className="flex items-center gap-2"><MapPin aria-hidden className="size-4.5 shrink-0" />{place(next.session)}</span> : null}
|
|
56
|
+
</div>
|
|
57
|
+
<div className="flex flex-wrap items-center justify-between gap-3 pt-1">
|
|
58
|
+
<span className="text-sm opacity-85">{t('portal.startsIn', 'Starts {when}')!.replace('{when}', () => startsIn({ value: next.startsAt, now, locale }) ?? '')}</span>
|
|
59
|
+
{next.course ? (
|
|
60
|
+
<a href={`${screen.path}/courses/${encodeURIComponent(String(next.course.id))}`} className={buttonVariants({ variant: 'secondary', className: 'bg-card text-primary h-11 rounded-full px-5 font-semibold' })}>
|
|
61
|
+
{t('portal.viewCourse', 'View course')}
|
|
62
|
+
</a>
|
|
63
|
+
) : null}
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
<Cover course={next.course} cover={next.cover} className="hidden h-full min-h-60 w-full md:block" />
|
|
67
|
+
</section>
|
|
68
|
+
) : null}
|
|
69
|
+
<section aria-labelledby="join" className="grid gap-4">
|
|
70
|
+
<div className="flex items-baseline justify-between gap-3">
|
|
71
|
+
<h2 id="join" className={`${display} text-xl sm:text-2xl`}>{t('portal.canJoin', 'Courses you can join')}</h2>
|
|
72
|
+
<a href={`${screen.path}/courses`} className="text-primary text-[15px] font-medium underline-offset-4 hover:underline">{t('portal.seeAll', 'See all courses')}</a>
|
|
73
|
+
</div>
|
|
74
|
+
{catalogue.isError ? <ReadFailed retry={() => void catalogue.refetch()} /> : !open ? <CardsLoading /> : open.length ? (
|
|
75
|
+
<div className="grid grid-cols-[repeat(auto-fill,minmax(16rem,1fr))] gap-5">
|
|
76
|
+
{open.map((course) => (
|
|
77
|
+
<CourseCard
|
|
78
|
+
key={String(course.id)}
|
|
79
|
+
course={course}
|
|
80
|
+
cover={catalogue.data!.covers.get(String(course.cover))}
|
|
81
|
+
next={catalogue.data!.next.get(String(course.id))}
|
|
82
|
+
tags={catalogue.data!.tags}
|
|
83
|
+
href={`${screen.path}/courses/${encodeURIComponent(String(course.id))}`}
|
|
84
|
+
/>
|
|
85
|
+
))}
|
|
86
|
+
</div>
|
|
87
|
+
) : (
|
|
88
|
+
<p className="text-muted-foreground text-sm">{t('portal.noneToJoin', 'No course has a session coming up just now.')}</p>
|
|
89
|
+
)}
|
|
90
|
+
</section>
|
|
91
|
+
</div>
|
|
92
|
+
),
|
|
93
|
+
}}
|
|
94
|
+
/>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
@@ -1,23 +1,19 @@
|
|
|
1
|
-
// What `/portal` itself draws
|
|
2
|
-
//
|
|
3
|
-
// many rows of it the viewer may read and a link
|
|
4
|
-
//
|
|
1
|
+
// What `/portal` itself draws in a project without the courses example, whose
|
|
2
|
+
// portal draws its own home: a greeting, then the dashboard home's figure card
|
|
3
|
+
// for each list below, with how many rows of it the viewer may read and a link
|
|
4
|
+
// to it. A list the nav does not offer the viewer has no card, and a list whose
|
|
5
5
|
// count the viewer may not read, or whose count failed, shows a dash in place
|
|
6
6
|
// of the figure.
|
|
7
7
|
import type { ReactNode } from 'react'
|
|
8
|
-
import { BookOpen, ClipboardList } from 'lucide-react'
|
|
9
8
|
import type { LoaderTools, PageProps } from 'mithaq/admin'
|
|
10
9
|
import { useLocale, useScreen, useUser } from 'mithaq/admin/react'
|
|
11
|
-
import { KpiCard } from '
|
|
12
|
-
import { ScreenNav, navLabel } from '
|
|
13
|
-
import { PortalTopShell } from '
|
|
14
|
-
import { onOffer } from '../courses/ui/on-offer.ts'
|
|
10
|
+
import { KpiCard } from '@asas/mithaq/ui/kpi-card.tsx'
|
|
11
|
+
import { ScreenNav, navLabel } from '@asas/mithaq/ui/screen-nav.tsx'
|
|
12
|
+
import { PortalTopShell } from '@asas/mithaq/ui/top-shell.tsx'
|
|
15
13
|
|
|
16
14
|
/** The lists the home counts, each with its card's icon and the rows its list is held to. */
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
['enrollments', <ClipboardList className="size-4" />],
|
|
20
|
-
]
|
|
15
|
+
// A list of the project's own goes here, as `['tags', <Tags className="size-4" />]`.
|
|
16
|
+
const figures: readonly [kayan: string, icon: ReactNode, where?: object][] = []
|
|
21
17
|
|
|
22
18
|
/**
|
|
23
19
|
* How many rows of each list the viewer may count, read as the viewer, keyed
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { actorPermissions, count, defaultFields, defaultJsonMaxBytes, denied, find, findOne, invalid, type Ctx, type Held } from 'mithaq'
|
|
2
|
+
import { defineKayan, rows, organization } from '@asas/mithaq'
|
|
3
|
+
import { backofficeClosed } from '@asas/mithaq/roles/tenant-access.ts'
|
|
4
|
+
import { auditLogName, type Change } from '@asas/mithaq/mithaq/audit-sink.ts'
|
|
5
|
+
|
|
6
|
+
/** The kayan whose `find` this actor holds, which are the kayan whose entries it reads. */
|
|
7
|
+
const heldKayans = (held: Exclude<Held, true | undefined>) =>
|
|
8
|
+
[...held].filter((key) => key.endsWith('.find') && !key.includes('.fields.')).map((key) => key.slice(0, -'.find'.length))
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The read the log's rows are ANDed with, so it narrows and never widens: an
|
|
12
|
+
* entry of a kayan the actor may not list is no entry it reads. An actor
|
|
13
|
+
* nothing grades reads none, which is the closed direction.
|
|
14
|
+
*/
|
|
15
|
+
const readableKayans = ({ user }: { user: unknown }) => {
|
|
16
|
+
const held = actorPermissions(user)
|
|
17
|
+
if (held === true) return true
|
|
18
|
+
const kayans = held ? heldKayans(held) : []
|
|
19
|
+
return kayans.length ? { kayan: { in: kayans } } : false
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Whether this actor reads one column of the kayan an entry names. The access
|
|
24
|
+
* target grades every column of this project by its key.
|
|
25
|
+
*/
|
|
26
|
+
const readsField = (held: Held, kayan: string, field: string) => held === true || held?.has(`${kayan}.fields.${field}`) === true
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Whether this actor reads the provider's people, by the key of their `find`.
|
|
30
|
+
* A command's operating-system user is the one thing in the log that names a
|
|
31
|
+
* person no row of the people does, so only such a reader sees or asks for it.
|
|
32
|
+
*/
|
|
33
|
+
const readsPeople = (held: Held) => held === true || held?.has('user.find') === true
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* What the sink writes for a command: the command, then the operating-system
|
|
37
|
+
* user in parentheses. The command is the shortest part before ` (`, so a user
|
|
38
|
+
* holding a parenthesis is never left in it.
|
|
39
|
+
*/
|
|
40
|
+
const commandActor = /^(.+?) \(.+\)$/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The step after the read: each entry keeps the changes of the columns the
|
|
44
|
+
* actor may read and drops the rest whole, so a column it may not read shows
|
|
45
|
+
* neither value nor that it changed. The server does it rather than the page,
|
|
46
|
+
* because the log is read over HTTP as well as drawn.
|
|
47
|
+
*/
|
|
48
|
+
const visibleChanges = (ctx: Ctx): Ctx => {
|
|
49
|
+
const held = actorPermissions(ctx.actor)
|
|
50
|
+
const trim = (entry: unknown) => {
|
|
51
|
+
const changes = (entry as { changes?: unknown } | null)?.changes
|
|
52
|
+
if (!changes || typeof changes !== 'object') return entry
|
|
53
|
+
const { kayan, actor } = entry as { kayan: string; actor?: unknown }
|
|
54
|
+
const kept = Object.fromEntries(Object.entries(changes as Record<string, Change>).filter(([field]) => readsField(held, kayan, field)))
|
|
55
|
+
// A command's actor is the command alone to a reader of no people, its operating-system user blanked.
|
|
56
|
+
const named = !readsPeople(held) && typeof actor === 'string' ? { actor: commandActor.exec(actor)?.[1] ?? actor } : {}
|
|
57
|
+
return { ...(entry as object), ...named, changes: kept }
|
|
58
|
+
}
|
|
59
|
+
const data: unknown = ctx.data
|
|
60
|
+
const page = data && typeof data === 'object' && Array.isArray((data as { docs?: unknown }).docs) ? (data as { docs: unknown[] }) : undefined
|
|
61
|
+
return { ...ctx, data: (page ? { ...page, docs: page.docs.map(trim) } : trim(data)) as Record<string, unknown> }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The columns the log is filtered and ordered by. The organization is core's
|
|
66
|
+
* own filter and `impersonatedBy` names staff.
|
|
67
|
+
*/
|
|
68
|
+
const queryable = new Set(['kayan', 'record', 'createdAt', 'operation', 'actor'])
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* What the log is searched by besides those, never ordered by: `changes`, the
|
|
72
|
+
* whole of each change, or a path into it, `changes.<column>` and deeper. Each
|
|
73
|
+
* search takes any of the json kind's operators and reaches only the kayan whose
|
|
74
|
+
* columns it may tell of (`reachOf`), because a match in a column the reader
|
|
75
|
+
* holds no key for would say what that column held before `visibleChanges`
|
|
76
|
+
* trims the page.
|
|
77
|
+
*/
|
|
78
|
+
const searched = 'changes'
|
|
79
|
+
|
|
80
|
+
/** The field a `where` key names: `changes.title.to` names `changes`. */
|
|
81
|
+
const fieldOf = (key: string) => key.split('.')[0]!
|
|
82
|
+
|
|
83
|
+
const namedIn = (where: unknown): string[] => {
|
|
84
|
+
if (!where || typeof where !== 'object') return []
|
|
85
|
+
return Object.entries(where).flatMap(([key, value]) =>
|
|
86
|
+
(key === 'and' || key === 'or') && Array.isArray(value) ? value.flatMap(namedIn) : [key])
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Every column key the catalog holds, by the kayan it belongs to. */
|
|
90
|
+
const columnsOf = (catalog: readonly string[]) => {
|
|
91
|
+
const columns = new Map<string, string[]>()
|
|
92
|
+
for (const key of catalog) {
|
|
93
|
+
const at = key.indexOf('.fields.')
|
|
94
|
+
if (at > 0) columns.set(key.slice(0, at), [...(columns.get(key.slice(0, at)) ?? []), key])
|
|
95
|
+
}
|
|
96
|
+
return columns
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ponytail: the reach here and `visibleChanges` grade by keys alone. A tenant role that a row
|
|
100
|
+
// table narrows further, by a `fields` list or a row filter on its read, would be told of columns
|
|
101
|
+
// and rows its own reads hide; grade each entry through the kayan's own field access and read
|
|
102
|
+
// `where` before a project adds one.
|
|
103
|
+
/**
|
|
104
|
+
* The kayan one search of a change reaches for this actor, or `true` for every
|
|
105
|
+
* one. The whole change reaches a kayan whose every column the actor reads, since
|
|
106
|
+
* it matches any of them; a path into one column, `changes.<column>`, a kayan
|
|
107
|
+
* whose column of that name it reads, since it matches inside that column alone.
|
|
108
|
+
*/
|
|
109
|
+
const reachOf = ({ held, catalog }: { held: Held; catalog: readonly string[] }) => (key: string): true | string[] => {
|
|
110
|
+
if (held === true) return true
|
|
111
|
+
const column = key.split('.')[1]
|
|
112
|
+
return [...columnsOf(catalog)]
|
|
113
|
+
.filter(([kayan, keys]) => held?.has(`${kayan}.find`) && (column === undefined ? keys.every((one) => held.has(one)) : held.has(`${kayan}.fields.${column}`)))
|
|
114
|
+
.map(([kayan]) => kayan)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The `where` with each search of a change ANDed, where it stands, with the kayan
|
|
119
|
+
* it reaches, so an `or` around it still means what it said, and a negation, a
|
|
120
|
+
* `not_equals` or an `exists: false`, still answers no entry past its reach.
|
|
121
|
+
*/
|
|
122
|
+
const narrowed = (where: unknown, reach: (key: string) => true | string[]): unknown => {
|
|
123
|
+
if (!where || typeof where !== 'object') return where
|
|
124
|
+
const parts = Object.entries(where).map(([key, value]) => {
|
|
125
|
+
if ((key === 'and' || key === 'or') && Array.isArray(value)) return { [key]: value.map((one) => narrowed(one, reach)) }
|
|
126
|
+
if (fieldOf(key) !== searched) return { [key]: value }
|
|
127
|
+
const kayans = reach(key)
|
|
128
|
+
return kayans === true ? { [key]: value } : { and: [{ [key]: value }, { kayan: { in: kayans } }] }
|
|
129
|
+
})
|
|
130
|
+
return parts.length === 1 ? parts[0] : { and: parts }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The step before the read: a `where` naming any column but those above, or a
|
|
135
|
+
* `sort` by any but the queryable ones, is refused 400 before the store is
|
|
136
|
+
* asked, on every read the log declares, and each search of a change is narrowed
|
|
137
|
+
* to the kayan it reaches. The runner's own checks still run after it on what it
|
|
138
|
+
* lets through.
|
|
139
|
+
*/
|
|
140
|
+
const queriedByQueryable = (ctx: Ctx, tools: { catalog: readonly string[] }): Ctx => {
|
|
141
|
+
// The runner names the organization a read is scoped to only where the read is not widened.
|
|
142
|
+
// `reachOf` and `visibleChanges` grade by the active organization's keys, so a read across every
|
|
143
|
+
// membership (`tenant: 'mine'`) would grade another organization's entries by this one's: it is
|
|
144
|
+
// refused before anything else of it is judged.
|
|
145
|
+
if (ctx.tenant === undefined) {
|
|
146
|
+
throw denied({ message: 'Kayan "auditLog" is read in the active organization alone.', key: 'error.forbidden.read', params: {} })
|
|
147
|
+
}
|
|
148
|
+
const { where, sort } = ctx.data as { where?: unknown; sort?: unknown }
|
|
149
|
+
const field = namedIn(where).find((name) => !queryable.has(name) && fieldOf(name) !== searched)
|
|
150
|
+
if (field !== undefined) {
|
|
151
|
+
throw invalid({ message: `Kayan "auditLog" is filtered by ${[...queryable, searched].join(', ')} alone, and not by "${field}".`, key: 'error.invalid.where', params: { kayan: 'auditLog', field } })
|
|
152
|
+
}
|
|
153
|
+
const held = actorPermissions(ctx.actor)
|
|
154
|
+
// A reader of no people names an actor in no filter and no sort: a pattern, a range, an order or
|
|
155
|
+
// even an equality over the string would let it confirm a guessed operating-system user of a
|
|
156
|
+
// command's actor.
|
|
157
|
+
if (!readsPeople(held) && (namedIn(where).includes('actor') || (typeof sort === 'string' && sort.replace(/^-/, '') === 'actor'))) {
|
|
158
|
+
throw invalid({ message: 'Kayan "auditLog" is neither filtered nor sorted by "actor" for a reader of no people.', key: 'error.invalid.where', params: { kayan: 'auditLog', field: 'actor' } })
|
|
159
|
+
}
|
|
160
|
+
if (sort !== undefined && !(typeof sort === 'string' && queryable.has(sort.replace(/^-/, '')))) {
|
|
161
|
+
throw invalid({ message: `Kayan "auditLog" is sorted by ${[...queryable].join(', ')} alone.`, key: 'error.invalid.sort_field', params: { kayan: 'auditLog', field: String(sort) } })
|
|
162
|
+
}
|
|
163
|
+
return where === undefined ? ctx : { ...ctx, data: { ...ctx.data, where: narrowed(where, reachOf({ held, catalog: tools.catalog })) } }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The audit log's rows: an organization reads its own entries, and nothing writes
|
|
168
|
+
* one through the log, a caller of any role included, since the service
|
|
169
|
+
* target's sink writes each entry through the store inside the write it
|
|
170
|
+
* records. Every backoffice role is closed: the backoffice lists no entry, and a
|
|
171
|
+
* staff member's own impersonated writes are the entries it would read.
|
|
172
|
+
*/
|
|
173
|
+
const auditLogAccess = rows({
|
|
174
|
+
'*': { create: false, read: true, update: false, delete: false },
|
|
175
|
+
...backofficeClosed,
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The audit log: one row per row a write changed, written by the service
|
|
180
|
+
* target's audit sink inside that write's own transaction and by nothing else.
|
|
181
|
+
* It declares no operation that writes, so no caller creates, edits or deletes
|
|
182
|
+
* an entry, and each of its rows answers `false` for every write besides.
|
|
183
|
+
*
|
|
184
|
+
* It is its organization's own. A record's last entries are read in its edit
|
|
185
|
+
* page's side column, all of them in its Activity tab, and the organization's on
|
|
186
|
+
* the dashboard's Activity page, a page of the screen's own rather than of this
|
|
187
|
+
* kayan, which draws none. The backoffice lists none of it and reads none of
|
|
188
|
+
* it: staff's own actions are its staff history, `backofficeLog`.
|
|
189
|
+
*/
|
|
190
|
+
export const auditLog = defineKayan({
|
|
191
|
+
name: auditLogName,
|
|
192
|
+
dbName: 'audit_log',
|
|
193
|
+
messages: {
|
|
194
|
+
singular: { en: 'Audit entry', ar: 'سجل تدقيق' },
|
|
195
|
+
plural: { en: 'Audit log', ar: 'سجل التدقيق' },
|
|
196
|
+
// What the activity panel draws beside the field labels.
|
|
197
|
+
activity: { en: 'Activity', ar: 'النشاط' },
|
|
198
|
+
loading: { en: 'Loading activity…', ar: 'جارٍ تحميل النشاط…' },
|
|
199
|
+
empty: { en: 'Nothing has changed here yet.', ar: 'لم يتغير شيء هنا بعد.' },
|
|
200
|
+
failed: { en: 'The activity could not be read.', ar: 'تعذرت قراءة النشاط.' },
|
|
201
|
+
more: { en: 'Show older', ar: 'عرض الأقدم' },
|
|
202
|
+
system: { en: 'The system', ar: 'النظام' },
|
|
203
|
+
event: { en: '{action} by {actor}', ar: '{action} بواسطة {actor}' },
|
|
204
|
+
// Staff writing as a member: the real person first, as the member they wrote as.
|
|
205
|
+
impersonated: { en: '{staff} as {member}', ar: '{staff} بصفة {member}' },
|
|
206
|
+
// One per operation a tenant kayan writes through, which is every entry the sink records.
|
|
207
|
+
action: {
|
|
208
|
+
create: { en: 'Created', ar: 'أُنشئ' },
|
|
209
|
+
update: { en: 'Updated', ar: 'حُدِّث' },
|
|
210
|
+
delete: { en: 'Deleted', ar: 'حُذف' },
|
|
211
|
+
updateMany: { en: 'Updated', ar: 'حُدِّث' },
|
|
212
|
+
deleteMany: { en: 'Deleted', ar: 'حُذف' },
|
|
213
|
+
restoreVersion: { en: 'Restored a version', ar: 'استُعيد إصدار' },
|
|
214
|
+
archive: { en: 'Archived', ar: 'أُرشف' },
|
|
215
|
+
archiveMany: { en: 'Archived', ar: 'أُرشف' },
|
|
216
|
+
unarchive: { en: 'Unarchived', ar: 'أُلغيت أرشفته' },
|
|
217
|
+
unpublish: { en: 'Unpublished', ar: 'أُلغي نشره' },
|
|
218
|
+
// An update that published a draft, which the sink names apart from a plain update.
|
|
219
|
+
publish: { en: 'Published', ar: 'نُشر' },
|
|
220
|
+
},
|
|
221
|
+
someone: { en: 'Someone', ar: 'شخص ما' },
|
|
222
|
+
// A command line write, named for a reader of the people: the command and
|
|
223
|
+
// the operating-system user who ran it.
|
|
224
|
+
command: { en: 'the command {command} ({user})', ar: 'الأمر {command} ({user})' },
|
|
225
|
+
// The viewer's own entry, which needs no read of the people to name. Its
|
|
226
|
+
// sentence is its own, since Arabic joins the person to "by" in one word,
|
|
227
|
+
// "بواسطتك", and not as "بواسطة أنت".
|
|
228
|
+
you: { en: 'You', ar: 'أنت' },
|
|
229
|
+
eventYou: { en: '{action} by You', ar: '{action} بواسطتك' },
|
|
230
|
+
none: { en: 'No field you may read changed.', ar: 'لم يتغير حقل يمكنك قراءته.' },
|
|
231
|
+
// Beside a change the sink cut to the start of its JSON, which a large json value is.
|
|
232
|
+
truncated: { en: 'Too long to keep whole; its start is shown.', ar: 'أطول من أن يُحفظ كاملا؛ تظهر بدايته.' },
|
|
233
|
+
// The panel's link to the rest of a record's entries, and the Activity page and tab.
|
|
234
|
+
all: { en: 'All activity', ar: 'كل النشاط' },
|
|
235
|
+
about: { en: 'Every change in this organization, newest first.', ar: 'كل تغيير في هذه المؤسسة، الأحدث أولا.' },
|
|
236
|
+
filters: { en: 'Filter the activity', ar: 'تصفية النشاط' },
|
|
237
|
+
// What the search box names as what it searches, in the sentence "Search changed values".
|
|
238
|
+
searchChanges: { en: 'changed values', ar: 'القيم المتغيرة' },
|
|
239
|
+
person: { en: 'Person', ar: 'الشخص' },
|
|
240
|
+
type: { en: 'Record type', ar: 'نوع السجل' },
|
|
241
|
+
field: { en: 'Field', ar: 'الحقل' },
|
|
242
|
+
actionFilter: { en: 'Action', ar: 'الإجراء' },
|
|
243
|
+
from: { en: 'From', ar: 'من' },
|
|
244
|
+
to: { en: 'To', ar: 'إلى' },
|
|
245
|
+
any: { en: 'Any', ar: 'الكل' },
|
|
246
|
+
filter: {
|
|
247
|
+
created: { en: 'Created', ar: 'أُنشئ' },
|
|
248
|
+
updated: { en: 'Updated', ar: 'حُدِّث' },
|
|
249
|
+
deleted: { en: 'Deleted', ar: 'حُذف' },
|
|
250
|
+
published: { en: 'Published', ar: 'نُشر' },
|
|
251
|
+
restored: { en: 'Restored', ar: 'استُعيد' },
|
|
252
|
+
},
|
|
253
|
+
clear: { en: 'Clear filters', ar: 'مسح عوامل التصفية' },
|
|
254
|
+
nothing: { en: 'No activity matches these filters.', ar: 'لا نشاط يطابق عوامل التصفية هذه.' },
|
|
255
|
+
oneRecord: { en: 'One record\'s activity.', ar: 'نشاط سجل واحد.' },
|
|
256
|
+
found: { en: 'Entries found: {count}', ar: 'السجلات المطابقة: {count}' },
|
|
257
|
+
searchLimited: { en: 'The search covers the records whose every field you may read.', ar: 'يشمل البحث السجلات التي يمكنك قراءة كل حقولها.' },
|
|
258
|
+
},
|
|
259
|
+
access: { ...auditLogAccess, read: readableKayans },
|
|
260
|
+
targets: {
|
|
261
|
+
dashboard: {},
|
|
262
|
+
service: {
|
|
263
|
+
operations: {
|
|
264
|
+
find: { steps: [queriedByQueryable, find, visibleChanges] },
|
|
265
|
+
findByID: { steps: [queriedByQueryable, findOne, visibleChanges] },
|
|
266
|
+
count: { steps: [queriedByQueryable, count] },
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
fields: {
|
|
271
|
+
id: defaultFields.id,
|
|
272
|
+
createdAt: defaultFields.createdAt,
|
|
273
|
+
organization,
|
|
274
|
+
kayan: { kind: 'text', label: { en: 'Kayan', ar: 'الكيان' }, rules: { required: true, maxLength: 60 } },
|
|
275
|
+
record: { kind: 'text', label: { en: 'Record', ar: 'السجل' }, rules: { required: true, index: true } },
|
|
276
|
+
operation: { kind: 'text', label: { en: 'Operation', ar: 'العملية' }, rules: { required: true, maxLength: 60 } },
|
|
277
|
+
actor: { kind: 'text', label: { en: 'Actor', ar: 'المنفذ' } },
|
|
278
|
+
impersonatedBy: { kind: 'text', label: { en: 'Impersonated by', ar: 'انتحل الصفة' } },
|
|
279
|
+
// Each changed column by name, so a search finds the change of one column by
|
|
280
|
+
// the path `changes.<column>`, which `queriedByQueryable` grades by that
|
|
281
|
+
// column's key. Never an order, as no json field is.
|
|
282
|
+
changes: { kind: 'json', label: { en: 'Changes', ar: 'التغييرات' }, maxBytes: defaultJsonMaxBytes, rules: { required: true } },
|
|
283
|
+
},
|
|
284
|
+
})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { defaultFields, defaultOperations } from 'mithaq'
|
|
2
|
+
import { defineKayan, rows } from '@asas/mithaq'
|
|
3
|
+
import { backofficeList } from '@asas/mithaq/mithaq/pages.ts'
|
|
4
|
+
import { backofficeReads } from '@asas/mithaq/roles/tenant-access.ts'
|
|
5
|
+
|
|
6
|
+
/** Who did what to whom on the backoffice, as `action` names it. */
|
|
7
|
+
const actions = [
|
|
8
|
+
{ value: 'impersonate', label: { en: 'Started impersonating', ar: 'بدأ انتحال الصفة' } },
|
|
9
|
+
{ value: 'stopImpersonating', label: { en: 'Stopped impersonating', ar: 'أنهى انتحال الصفة' } },
|
|
10
|
+
{ value: 'setRole', label: { en: 'Set the backoffice role', ar: 'عيّن دور الإدارة الخلفية' } },
|
|
11
|
+
{ value: 'ban', label: { en: 'Banned', ar: 'حظر' } },
|
|
12
|
+
{ value: 'unban', label: { en: 'Lifted the ban', ar: 'رفع الحظر' } },
|
|
13
|
+
] as const
|
|
14
|
+
|
|
15
|
+
const text = (label: { en: string; ar: string }) => ({ kind: 'text', label }) as const
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The staff history: one row per impersonation started or stopped, backoffice
|
|
19
|
+
* role set and ban set or lifted, written by the auth setup's backoffice hook
|
|
20
|
+
* once the provider's route succeeded, and by nothing else. It belongs to no
|
|
21
|
+
* organization, since staff act above every tenant, and it declares no
|
|
22
|
+
* operation that writes, so no caller makes, edits or removes a row.
|
|
23
|
+
*
|
|
24
|
+
* Only a backoffice role reads it, in the backoffice app alone, and only one
|
|
25
|
+
* holding its keys: the row table names no `'*'` row, so a tenant's
|
|
26
|
+
* administrator, who holds every key of the project, takes no row of it and is
|
|
27
|
+
* refused. The tenant app's screens list none of it.
|
|
28
|
+
*
|
|
29
|
+
* Each person is kept by id and by the address they had then, so the history
|
|
30
|
+
* still says who once a person is renamed or removed.
|
|
31
|
+
*/
|
|
32
|
+
export const backofficeLog = defineKayan({
|
|
33
|
+
name: 'backofficeLog',
|
|
34
|
+
dbName: 'backoffice_log',
|
|
35
|
+
messages: {
|
|
36
|
+
singular: { en: 'Staff action', ar: 'إجراء طاقم' },
|
|
37
|
+
plural: { en: 'Staff history', ar: 'سجل الطاقم' },
|
|
38
|
+
},
|
|
39
|
+
access: rows({ ...backofficeReads }),
|
|
40
|
+
targets: {
|
|
41
|
+
service: {
|
|
42
|
+
operations: { find: defaultOperations.find, findByID: defaultOperations.findByID, count: defaultOperations.count },
|
|
43
|
+
},
|
|
44
|
+
backoffice: {
|
|
45
|
+
pages: {
|
|
46
|
+
...backofficeList('/history', {
|
|
47
|
+
columns: ['createdAt', 'action', 'actorEmail', 'targetEmail', 'previousRole', 'role', 'banReason'],
|
|
48
|
+
filters: ['action', 'actorEmail', 'targetEmail', 'createdAt'],
|
|
49
|
+
search: ['actorEmail', 'targetEmail'],
|
|
50
|
+
sort: '-createdAt',
|
|
51
|
+
}),
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
fields: {
|
|
56
|
+
id: defaultFields.id,
|
|
57
|
+
createdAt: defaultFields.createdAt,
|
|
58
|
+
action: { kind: 'text', label: { en: 'Action', ar: 'الإجراء' }, options: actions, rules: { required: true } },
|
|
59
|
+
actor: text({ en: 'Staff member', ar: 'عضو الطاقم' }),
|
|
60
|
+
actorEmail: text({ en: 'Staff member\'s email', ar: 'بريد عضو الطاقم' }),
|
|
61
|
+
target: text({ en: 'Person', ar: 'الشخص' }),
|
|
62
|
+
targetEmail: text({ en: 'Person\'s email', ar: 'بريد الشخص' }),
|
|
63
|
+
previousRole: text({ en: 'Role before', ar: 'الدور السابق' }),
|
|
64
|
+
role: text({ en: 'Role after', ar: 'الدور الجديد' }),
|
|
65
|
+
banReason: text({ en: 'Ban reason', ar: 'سبب الحظر' }),
|
|
66
|
+
},
|
|
67
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { localUsers } from './local-users/local-users.ts'
|
|
2
|
+
// Written by `mithaq generate auth`: one key per plugin the vocabulary names,
|
|
3
|
+
// and `core` for the four Better Auth ships without one. Spreading the keys is
|
|
4
|
+
// what keeps this list from naming eight files, and a project that drops the
|
|
5
|
+
// organization plugin drops one spread.
|
|
6
|
+
import { betterAuth } from './user/index.ts'
|
|
7
|
+
|
|
8
|
+
// The account other features point at, and the asas names its administrator by.
|
|
9
|
+
export { user } from './user/user.ts'
|
|
10
|
+
|
|
11
|
+
export const kayan = [...betterAuth.core, ...betterAuth.organization, localUsers] as const
|
|
@@ -9,9 +9,9 @@ import {
|
|
|
9
9
|
defaultOperations,
|
|
10
10
|
type Ctx,
|
|
11
11
|
} from 'mithaq'
|
|
12
|
-
import { defineKayan, requestContext } from '
|
|
13
|
-
import { appPage,
|
|
14
|
-
import { superAdmin } from '
|
|
12
|
+
import { defineKayan, requestContext } from '@asas/mithaq'
|
|
13
|
+
import { appPage, backofficeList } from '@asas/mithaq/mithaq/pages.ts'
|
|
14
|
+
import { superAdmin } from '@asas/mithaq/roles/tenant-access.ts'
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* The trusted-header names this deployment's own proxy overwrites with the real client address, read
|
|
@@ -62,7 +62,7 @@ export const localUsers = defineKayan({
|
|
|
62
62
|
// would share one count; there, take the entry at the depth of the trusted proxies. A project served
|
|
63
63
|
// bare passes its server's own address instead, such as Bun's `server.requestIP(request)`. No header
|
|
64
64
|
// answers null, which leaves the request uncounted.
|
|
65
|
-
// No `email` is passed, so no mail is sent: a super admin verifies each sign-up in the
|
|
65
|
+
// No `email` is passed, so no mail is sent: a super admin verifies each sign-up in the backoffice app.
|
|
66
66
|
auth: local({
|
|
67
67
|
secret,
|
|
68
68
|
withoutSecret: 'Local sign-in is off on this server: LOCAL_AUTH_SECRET is not set. Set it to a secret of 32 characters at least and restart the server.',
|
|
@@ -91,10 +91,10 @@ export const localUsers = defineKayan({
|
|
|
91
91
|
* The form sends only what changed, so an unchanged toggle writes nothing and leaves `updatedAt`
|
|
92
92
|
* alone. The protected columns are named by no page, so no list offers one as a column or a filter.
|
|
93
93
|
*/
|
|
94
|
-
|
|
94
|
+
backoffice: {
|
|
95
95
|
useAsTitle: 'email',
|
|
96
96
|
pages: {
|
|
97
|
-
...
|
|
97
|
+
...backofficeList('/local-users', {
|
|
98
98
|
columns: ['email', '_verified', 'createdAt'],
|
|
99
99
|
filters: ['_verified'],
|
|
100
100
|
search: ['email'],
|
|
@@ -121,7 +121,7 @@ export const localUsers = defineKayan({
|
|
|
121
121
|
},
|
|
122
122
|
},
|
|
123
123
|
},
|
|
124
|
-
// Public sign-up stays open. A super admin, in the
|
|
124
|
+
// Public sign-up stays open. A super admin, in the backoffice app alone, verifies an account by writing
|
|
125
125
|
// `_verified: true`, which the field's own write admits from the administrator and nobody else. The
|
|
126
126
|
// super admin writes `_verified` and nothing else: a password or an email it set would be an account
|
|
127
127
|
// it could then log in as, and no audit entry records a write to a kayan with no tenant. An
|