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,11 +1,11 @@
|
|
|
1
1
|
import { actorRoles, conflict, defaultFields, denied, folderOf, grantedPermissions, writableRoleName, defaultOperations, type Ctx, type WriteTools } from 'mithaq'
|
|
2
|
-
import { user as users } from '
|
|
2
|
+
import { user as users } from '@asas/auth'
|
|
3
3
|
import { assignedCount, managerRemains, memberRoles } from '../member-roles/member-roles.ts'
|
|
4
|
-
import { managesRoles } from '
|
|
5
|
-
import { defineKayan, rows, organization } from '
|
|
6
|
-
import { appPage,
|
|
7
|
-
import { systemRoles } from '
|
|
8
|
-
import {
|
|
4
|
+
import { managesRoles } from '@asas/mithaq/roles/grants.ts'
|
|
5
|
+
import { defineKayan, rows, organization } from '@asas/mithaq'
|
|
6
|
+
import { appPage, backofficeList, backofficeRecord } from '@asas/mithaq/mithaq/pages.ts'
|
|
7
|
+
import { systemRoles } from '@asas/mithaq/roles/system-roles.ts'
|
|
8
|
+
import { backofficeReads } from '@asas/mithaq/roles/tenant-access.ts'
|
|
9
9
|
|
|
10
10
|
const rolesUi = folderOf(new URL('./ui/', import.meta.url))
|
|
11
11
|
|
|
@@ -14,17 +14,17 @@ const rolesUi = folderOf(new URL('./ui/', import.meta.url))
|
|
|
14
14
|
* a granter may not hand over a key it does not itself hold.
|
|
15
15
|
*/
|
|
16
16
|
/**
|
|
17
|
-
* A tenant's role never holds `screen.
|
|
17
|
+
* A tenant's role never holds `screen.backoffice`, which only a backoffice role holds: a row naming it would be a tenant claiming the backoffice.
|
|
18
18
|
* It is refused before the granter check, so a root granter holding `'*'` is refused too.
|
|
19
19
|
*/
|
|
20
|
-
const
|
|
20
|
+
const BACKOFFICE_KEY = 'screen.backoffice'
|
|
21
21
|
const tenantKeysOnly = (ctx: Ctx): Ctx => {
|
|
22
22
|
const written = (ctx.data as Record<string, unknown>).permissions
|
|
23
|
-
if (!Array.isArray(written) || !written.includes(
|
|
23
|
+
if (!Array.isArray(written) || !written.includes(BACKOFFICE_KEY)) return ctx
|
|
24
24
|
throw denied({
|
|
25
|
-
message: `Forbidden: a "${ctx.kayan.name}" row is a tenant's role, and "${
|
|
26
|
-
key: 'error.forbidden.
|
|
27
|
-
params: { kayan: ctx.kayan.name, permissions: [
|
|
25
|
+
message: `Forbidden: a "${ctx.kayan.name}" row is a tenant's role, and "${BACKOFFICE_KEY}" is held only by a backoffice role.`,
|
|
26
|
+
key: 'error.forbidden.backoffice_key',
|
|
27
|
+
params: { kayan: ctx.kayan.name, permissions: [BACKOFFICE_KEY] },
|
|
28
28
|
})
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -98,24 +98,24 @@ const keepsManagers = async (ctx: Ctx, tools: WriteTools): Promise<Ctx> => {
|
|
|
98
98
|
const rolesAccess = rows({
|
|
99
99
|
'*': { create: true, read: true, update: true, delete: true },
|
|
100
100
|
/**
|
|
101
|
-
* Every
|
|
101
|
+
* Every backoffice role reads the role rows of every tenant, and writes none. A
|
|
102
102
|
* tenant that lost its administrator is repaired through `mithaq rbac assign`,
|
|
103
|
-
* never from the
|
|
103
|
+
* never from the backoffice app.
|
|
104
104
|
*/
|
|
105
|
-
...
|
|
105
|
+
...backofficeReads,
|
|
106
106
|
})
|
|
107
107
|
|
|
108
108
|
export const roles = defineKayan({
|
|
109
109
|
name: 'roles',
|
|
110
110
|
targets: {
|
|
111
|
-
/** Every organization's rows on the
|
|
112
|
-
|
|
111
|
+
/** Every organization's rows on the backoffice, read-only. */
|
|
112
|
+
backoffice: {
|
|
113
113
|
useAsTitle: 'label',
|
|
114
114
|
pages: {
|
|
115
115
|
// A role by its label, never its slug, and its keys grouped as the
|
|
116
116
|
// dashboard draws a fixed role's.
|
|
117
|
-
...
|
|
118
|
-
...
|
|
117
|
+
...backofficeList('/roles', { columns: ['label', 'organization'], filters: ['organization'], search: ['label'] }),
|
|
118
|
+
...backofficeRecord({
|
|
119
119
|
path: '/roles/:id',
|
|
120
120
|
main: [{ section: 'role', label: { en: 'Role', ar: 'الدور' }, fields: ['label', { field: 'permissions', cell: rolesUi('roles.tsx#Permissions') }] }],
|
|
121
121
|
}),
|
|
@@ -142,15 +142,15 @@ export const roles = defineKayan({
|
|
|
142
142
|
ar: 'ما يمكن لكل دور في هذه المؤسسة أن يراه ويفعله.',
|
|
143
143
|
},
|
|
144
144
|
pages: {
|
|
145
|
-
list: { path: '/roles', page: appPage([{ layout: 'table', main: [{ list: { columns: ['label', 'name'], limit: 100 }, component: '
|
|
146
|
-
create: { path: '/roles/new', page: appPage([{ layout: 'record', main: [{ form: ['name', 'label', 'permissions'], component: '
|
|
145
|
+
list: { path: '/roles', page: appPage([{ layout: 'table', main: [{ list: { columns: ['label', 'name', 'permissions'], limit: 100 }, component: rolesUi('roles.tsx#RolesTable') }] }]) },
|
|
146
|
+
create: { path: '/roles/new', page: appPage([{ layout: 'record', main: [{ form: ['name', 'label', 'permissions'], component: rolesUi('role-form.tsx#RoleForm') }] }]) },
|
|
147
147
|
edit: {
|
|
148
148
|
path: '/roles/:id',
|
|
149
149
|
page: appPage([
|
|
150
150
|
{
|
|
151
151
|
layout: 'record',
|
|
152
|
-
main: [{ form: ['name', 'label', 'permissions'], component: '
|
|
153
|
-
danger: [{ button: 'delete', label: { en: 'Delete', ar: 'حذف' }, component: '
|
|
152
|
+
main: [{ form: ['name', 'label', 'permissions'], component: rolesUi('role-form.tsx#RoleForm') }],
|
|
153
|
+
danger: [{ button: 'delete', label: { en: 'Delete', ar: 'حذف' }, component: rolesUi('roles.tsx#DeleteRole') }],
|
|
154
154
|
},
|
|
155
155
|
]),
|
|
156
156
|
},
|
|
@@ -3,20 +3,22 @@
|
|
|
3
3
|
// values; this reads and writes them through `useForm()`, and reads each box's
|
|
4
4
|
// refusal through `useKayanField`, so a box says it is required, says what is
|
|
5
5
|
// wrong under itself once left or refused, and marks itself invalid.
|
|
6
|
-
import type { ReactNode } from 'react'
|
|
6
|
+
import type { ComponentProps as ElementProps, ReactNode } from 'react'
|
|
7
|
+
import { ChevronDown } from 'lucide-react'
|
|
7
8
|
import type { ComponentProps } from 'mithaq/admin'
|
|
8
9
|
import { useAccess, useForm, useKayan, useLocale, useScreen } from 'mithaq/admin/react'
|
|
9
10
|
import { Badge } from '@/components/ui/badge'
|
|
10
11
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
12
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
|
|
11
13
|
import { Input } from '@/components/ui/input'
|
|
12
|
-
import { ariaRequired } from '
|
|
13
|
-
import { FieldShell, describedBy } from '
|
|
14
|
-
import { useKayanField } from '
|
|
15
|
-
import { grantable } from '
|
|
16
|
-
import { projectStatements } from '
|
|
14
|
+
import { ariaRequired } from 'mithaq-admin/control-props.ts'
|
|
15
|
+
import { FieldShell, describedBy } from 'mithaq-admin/field-shell.tsx'
|
|
16
|
+
import { useKayanField } from 'mithaq-admin/form-context.tsx'
|
|
17
|
+
import { grantable } from '@asas/mithaq/roles/grants.ts'
|
|
18
|
+
import { projectStatements } from '@asas/mithaq/roles/statements.ts'
|
|
17
19
|
|
|
18
20
|
/** The key a tenant's role may never hold, which the role kayan refuses by name, so no badge offers it. */
|
|
19
|
-
const
|
|
21
|
+
const BACKOFFICE_KEY = 'screen.backoffice'
|
|
20
22
|
|
|
21
23
|
type Key = { key: string; label: string }
|
|
22
24
|
type Group = { name: string; label: string; operations: Key[]; fields: Key[] }
|
|
@@ -31,7 +33,7 @@ const useGroups = (): Group[] => {
|
|
|
31
33
|
const screen = useScreen()
|
|
32
34
|
const access = useAccess()
|
|
33
35
|
const { locale, t } = useLocale()
|
|
34
|
-
const screens = (access.screens ?? []).map((name) => `screen.${name}`).filter((key) => key !==
|
|
36
|
+
const screens = (access.screens ?? []).map((name) => `screen.${name}`).filter((key) => key !== BACKOFFICE_KEY)
|
|
35
37
|
return [
|
|
36
38
|
{
|
|
37
39
|
name: 'screen',
|
|
@@ -54,7 +56,45 @@ const useGroups = (): Group[] => {
|
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
/**
|
|
57
|
-
*
|
|
59
|
+
* A role's or a group's keys behind a row that names them and counts what they grant, closed at
|
|
60
|
+
* first so a page of roles reads as a list of names; `defaultOpen` where the keys are the page's
|
|
61
|
+
* work. A closed panel is `hidden="until-found"` once the page runs, so it is in the first paint
|
|
62
|
+
* and a find on the page opens it. `named` names a row that draws no label of its own, so a reader
|
|
63
|
+
* of the screen hears whose keys it opens. Every permission has nothing to open, so it is said.
|
|
64
|
+
*/
|
|
65
|
+
export function Grants({ label, named, held, heading, defaultOpen, children, ...root }: { label?: ReactNode; named?: string; held: readonly string[]; heading?: boolean; defaultOpen?: boolean; children: ReactNode } & ElementProps<'div'>) {
|
|
66
|
+
const { locale, t } = useLocale()
|
|
67
|
+
const count = held.length === 1
|
|
68
|
+
? t('dashboard.permissionsCountOne', '1 permission')
|
|
69
|
+
: (t('dashboard.permissionsCount', '{count} permissions') ?? '{count} permissions').replace('{count}', () => new Intl.NumberFormat(locale).format(held.length))
|
|
70
|
+
if (held.includes('*')) {
|
|
71
|
+
return (
|
|
72
|
+
<div {...root} className="flex items-center gap-2">
|
|
73
|
+
{/* Where a chevron stands on every other row, so the names line up. */}
|
|
74
|
+
<span aria-hidden="true" className="size-4 shrink-0" />
|
|
75
|
+
{label}
|
|
76
|
+
<span className="text-muted-foreground text-xs">{t('dashboard.everyPermission', 'Every permission')}</span>
|
|
77
|
+
</div>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
const trigger = (
|
|
81
|
+
<CollapsibleTrigger aria-label={named && `${named}: ${count}`} className="group flex w-full cursor-pointer items-center gap-2 text-start max-sm:min-h-11">
|
|
82
|
+
<ChevronDown aria-hidden="true" className="text-muted-foreground size-4 shrink-0 transition-transform group-data-[panel-open]:rotate-180" />
|
|
83
|
+
{label}
|
|
84
|
+
<span className="text-muted-foreground text-xs font-normal">{count}</span>
|
|
85
|
+
</CollapsibleTrigger>
|
|
86
|
+
)
|
|
87
|
+
return (
|
|
88
|
+
<Collapsible {...root} defaultOpen={defaultOpen}>
|
|
89
|
+
{heading ? <h3 className="text-sm font-medium">{trigger}</h3> : trigger}
|
|
90
|
+
<CollapsibleContent hiddenUntilFound className="ps-6 pt-2">{children}</CollapsibleContent>
|
|
91
|
+
</Collapsible>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The keys a role holds as badges, grouped by screen and kayan, each group behind its count: closed where they are
|
|
97
|
+
* read, open in the editor, whose work they are. With `onChange`
|
|
58
98
|
* each badge is a toggle, a key the viewer may not hand over disabled; without it
|
|
59
99
|
* only the keys held are drawn. A key no group names stays, under "Other", so a
|
|
60
100
|
* save never drops one.
|
|
@@ -63,7 +103,7 @@ export function PermissionBadges({ held, onChange, busy }: { held: readonly stri
|
|
|
63
103
|
const access = useAccess()
|
|
64
104
|
const { t } = useLocale()
|
|
65
105
|
// Read only, every screen the role enters is named, whichever the viewer
|
|
66
|
-
// enters: the
|
|
106
|
+
// enters: the backoffice reads a tenant's role, which holds `screen.dashboard`.
|
|
67
107
|
const groups = useGroups().map((group) =>
|
|
68
108
|
group.name === 'screen' && !onChange
|
|
69
109
|
? { ...group, operations: held.filter((key) => key.startsWith('screen.')).map((key) => ({ key, label: t(`screens.${key.slice('screen.'.length)}`) ?? key })) }
|
|
@@ -111,28 +151,29 @@ export function PermissionBadges({ held, onChange, busy }: { held: readonly stri
|
|
|
111
151
|
}
|
|
112
152
|
if (held.includes('*')) return <Badge variant="outline">{t('dashboard.everyPermission', 'Every permission')}</Badge>
|
|
113
153
|
return (
|
|
114
|
-
<div data-permissions="" className="grid gap-
|
|
154
|
+
<div data-permissions="" className="grid gap-2">
|
|
115
155
|
{groups.map((group) => {
|
|
116
156
|
const operations = badges(group.operations)
|
|
117
157
|
const fields = badges(group.fields)
|
|
158
|
+
const keys = [...group.operations, ...group.fields].map((one) => one.key)
|
|
118
159
|
return operations || fields ? (
|
|
119
|
-
<
|
|
120
|
-
<
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
160
|
+
<Grants key={group.name} data-group={group.name} heading defaultOpen={Boolean(onChange)} label={<span>{group.label}</span>} held={held.filter((key) => keys.includes(key))}>
|
|
161
|
+
<div className="grid gap-1.5">
|
|
162
|
+
{operations}
|
|
163
|
+
{fields ? (
|
|
164
|
+
<>
|
|
165
|
+
<p className="text-muted-foreground text-xs">{t('dashboard.permissionsFields', 'Fields')}</p>
|
|
166
|
+
{fields}
|
|
167
|
+
</>
|
|
168
|
+
) : null}
|
|
169
|
+
</div>
|
|
170
|
+
</Grants>
|
|
129
171
|
) : null
|
|
130
172
|
})}
|
|
131
173
|
{other.length ? (
|
|
132
|
-
<
|
|
133
|
-
<h3 className="text-muted-foreground text-xs font-medium">{t('dashboard.permissionsOther', 'Other')}</h3>
|
|
174
|
+
<Grants data-group="" heading defaultOpen={Boolean(onChange)} label={<span>{t('dashboard.permissionsOther', 'Other')}</span>} held={other}>
|
|
134
175
|
{badges(other.map((key) => ({ key, label: otherLabel(key) })))}
|
|
135
|
-
</
|
|
176
|
+
</Grants>
|
|
136
177
|
) : null}
|
|
137
178
|
</div>
|
|
138
179
|
)
|
|
@@ -4,8 +4,7 @@ import { useState } from 'react'
|
|
|
4
4
|
import { Lock } from 'lucide-react'
|
|
5
5
|
import type { ButtonProps, CellProps, Row } from 'mithaq/admin'
|
|
6
6
|
import { useClient, useList, useLocale, useRecord, useScreen } from 'mithaq/admin/react'
|
|
7
|
-
import
|
|
8
|
-
import { tenantRoles } from '../../tenant-roles.ts'
|
|
7
|
+
import { tenantRoles } from '@asas/mithaq/roles/tenant-roles.ts'
|
|
9
8
|
import {
|
|
10
9
|
AlertDialog,
|
|
11
10
|
AlertDialogAction,
|
|
@@ -19,7 +18,7 @@ import {
|
|
|
19
18
|
import { Badge } from '@/components/ui/badge'
|
|
20
19
|
import { Button } from '@/components/ui/button'
|
|
21
20
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
|
22
|
-
import { PermissionBadges } from './role-form.tsx'
|
|
21
|
+
import { Grants, PermissionBadges } from './role-form.tsx'
|
|
23
22
|
|
|
24
23
|
/** A role's label in the viewer's locale, and its slug only where a row carries none. */
|
|
25
24
|
export const labelOf = (row: Row | null | undefined, locale: string) => {
|
|
@@ -28,9 +27,9 @@ export const labelOf = (row: Row | null | undefined, locale: string) => {
|
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
/**
|
|
31
|
-
* The list node's component: the fixed roles first, from their code, each with a lock
|
|
32
|
-
*
|
|
33
|
-
* role's delete is its page's.
|
|
30
|
+
* The list node's component: the fixed roles first, from their code, each with a lock, then the
|
|
31
|
+
* organization's own. Each row names a role and counts what it grants, and opens to its keys and,
|
|
32
|
+
* for a custom role, a link to its page. It draws no row menu; a custom role's delete is its page's.
|
|
34
33
|
*/
|
|
35
34
|
export function RolesTable() {
|
|
36
35
|
const { page, creates, href, newHref } = useList()
|
|
@@ -56,33 +55,55 @@ export function RolesTable() {
|
|
|
56
55
|
<TableBody>
|
|
57
56
|
{Object.entries(tenantRoles).map(([name, role]) => (
|
|
58
57
|
<TableRow key={name} data-row={name} data-fixed="true">
|
|
59
|
-
<TableCell>
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<span
|
|
64
|
-
|
|
65
|
-
|
|
58
|
+
<TableCell className="whitespace-normal">
|
|
59
|
+
<Grants
|
|
60
|
+
data-fixed-role=""
|
|
61
|
+
label={
|
|
62
|
+
<span className="flex items-center gap-1.5 font-medium">
|
|
63
|
+
<Lock data-lock="" className="text-muted-foreground size-3.5" aria-hidden="true" />
|
|
64
|
+
<span>{(role.label as Readonly<Record<string, string>>)[locale] ?? name}</span>
|
|
65
|
+
</span>
|
|
66
|
+
}
|
|
67
|
+
held={role.permissions === '*' ? ['*'] : role.permissions}
|
|
68
|
+
>
|
|
69
|
+
<div className="grid gap-2">
|
|
66
70
|
<p className="text-muted-foreground text-sm">{t('dashboard.rolesFixedAbout', 'This role ships with the project. What it holds is code, so it cannot be edited or deleted here.')}</p>
|
|
67
71
|
<PermissionBadges held={role.permissions === '*' ? ['*'] : role.permissions} />
|
|
68
72
|
</div>
|
|
69
|
-
</
|
|
73
|
+
</Grants>
|
|
70
74
|
</TableCell>
|
|
71
|
-
<TableCell>
|
|
75
|
+
<TableCell className="align-top">
|
|
72
76
|
<Badge variant="secondary">{t('dashboard.rolesFixed', 'Fixed')}</Badge>
|
|
73
77
|
</TableCell>
|
|
74
78
|
</TableRow>
|
|
75
79
|
))}
|
|
76
80
|
{page.docs.map((row) => {
|
|
77
81
|
const id = String(row.id)
|
|
82
|
+
const label = labelOf(row, locale)
|
|
83
|
+
const open = (
|
|
84
|
+
<a href={href(id)} className="text-primary justify-self-start text-sm underline underline-offset-4 max-sm:inline-flex max-sm:min-h-11 max-sm:items-center">
|
|
85
|
+
{(t('dashboard.rolesOpen', 'Open {role}') ?? 'Open {role}').replace('{role}', () => label)}
|
|
86
|
+
</a>
|
|
87
|
+
)
|
|
78
88
|
return (
|
|
79
89
|
<TableRow key={id} data-row={id} data-fixed="false">
|
|
80
|
-
<TableCell>
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
90
|
+
<TableCell className="whitespace-normal">
|
|
91
|
+
{/* A role whose keys the viewer may not read comes without them, so it is named and linked and counts nothing. */}
|
|
92
|
+
{Array.isArray(row.permissions) ? (
|
|
93
|
+
<Grants label={<span className="font-medium">{label}</span>} held={row.permissions as string[]}>
|
|
94
|
+
<div className="grid gap-2">
|
|
95
|
+
{open}
|
|
96
|
+
<PermissionBadges held={row.permissions as string[]} />
|
|
97
|
+
</div>
|
|
98
|
+
</Grants>
|
|
99
|
+
) : (
|
|
100
|
+
<div className="grid gap-1">
|
|
101
|
+
<span className="font-medium">{label}</span>
|
|
102
|
+
{open}
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
84
105
|
</TableCell>
|
|
85
|
-
<TableCell>
|
|
106
|
+
<TableCell className="align-top">
|
|
86
107
|
<Badge variant="outline">{t('dashboard.rolesCustom', 'Custom')}</Badge>
|
|
87
108
|
</TableCell>
|
|
88
109
|
</TableRow>
|
|
@@ -103,6 +124,9 @@ export function Permissions({ value }: Pick<CellProps, 'value'>) {
|
|
|
103
124
|
return <PermissionBadges held={Array.isArray(value) ? (value as string[]) : []} />
|
|
104
125
|
}
|
|
105
126
|
|
|
127
|
+
/** A page of rows as the client answers it, read untyped since a feature never imports the asas it is built into. */
|
|
128
|
+
type Found = { docs?: Record<string, unknown>[] } | undefined
|
|
129
|
+
|
|
106
130
|
/**
|
|
107
131
|
* A custom role's delete, the danger zone's button. A role any member holds is not deleted, so the
|
|
108
132
|
* button reads who holds it first: while anyone does, the dialog names them, each linking to their
|
|
@@ -111,7 +135,7 @@ export function Permissions({ value }: Pick<CellProps, 'value'>) {
|
|
|
111
135
|
*/
|
|
112
136
|
export function DeleteRole({ run, busy }: ButtonProps) {
|
|
113
137
|
const record = useRecord()
|
|
114
|
-
const client = useClient
|
|
138
|
+
const client = useClient()
|
|
115
139
|
// Read at the build's most rows one served read answers, since a page past
|
|
116
140
|
// it is a 400 the catch below would read as no holders at all.
|
|
117
141
|
const { path, maxLimit } = useScreen()
|
|
@@ -119,12 +143,12 @@ export function DeleteRole({ run, busy }: ButtonProps) {
|
|
|
119
143
|
const [open, setOpen] = useState(false)
|
|
120
144
|
const [holders, setHolders] = useState<{ id: string; name: string }[]>([])
|
|
121
145
|
const ask = async () => {
|
|
122
|
-
const held = await client({ kayan: 'memberRoles', operation: 'find', input: { where: { role: { equals: record?.name as string } }, limit: maxLimit, depth: 0 } }).catch(() => undefined)
|
|
123
|
-
const ids = held?.docs?.map((row) => row.user) ?? []
|
|
124
|
-
const named = ids.length ? await client({ kayan: 'user', operation: 'find', input: { where: { id: { in: ids } }, limit: maxLimit, depth: 0 } }).catch(() => undefined) : undefined
|
|
146
|
+
const held = await client({ kayan: 'memberRoles', operation: 'find', input: { where: { role: { equals: record?.name as string } }, limit: maxLimit, depth: 0 } }).catch(() => undefined) as Found
|
|
147
|
+
const ids = held?.docs?.map((row) => String(row.user)) ?? []
|
|
148
|
+
const named = ids.length ? await client({ kayan: 'user', operation: 'find', input: { where: { id: { in: ids } }, limit: maxLimit, depth: 0 } }).catch(() => undefined) as Found : undefined
|
|
125
149
|
// A holder whose name the viewer may not read is "a member", never a raw id, and the link still
|
|
126
150
|
// opens their roles.
|
|
127
|
-
setHolders(ids.map((id) => ({ id, name: named?.docs?.find((one) => one.id === id)?.name ?? t('dashboard.rolesHolderUnnamed', 'A member') ?? '' })))
|
|
151
|
+
setHolders(ids.map((id) => ({ id, name: (named?.docs?.find((one) => one.id === id)?.name as string | undefined) ?? t('dashboard.rolesHolderUnnamed', 'A member') ?? '' })))
|
|
128
152
|
setOpen(true)
|
|
129
153
|
}
|
|
130
154
|
return (
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defaultFields, defaultGlobalFields, defaultGlobalOperations } from 'mithaq'
|
|
2
|
-
import { defineGlobal, organization, rows } from '
|
|
3
|
-
import {
|
|
4
|
-
import { appPage } from '
|
|
2
|
+
import { defineGlobal, organization, rows } from '@asas/mithaq'
|
|
3
|
+
import { backofficeClosed, tenantAccess } from '@asas/mithaq/roles/tenant-access.ts'
|
|
4
|
+
import { appPage } from '@asas/mithaq/mithaq/pages.ts'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* The project's settings, one row per tenant, because this global places
|
|
@@ -20,31 +20,26 @@ export const settings = defineGlobal({
|
|
|
20
20
|
fields: {
|
|
21
21
|
...defaultFields,
|
|
22
22
|
organization: { ...organization, rules: { ...organization.rules, unique: true } },
|
|
23
|
-
|
|
24
|
-
kind: 'text',
|
|
25
|
-
label: { en: 'Site title', ar: 'عنوان الموقع' },
|
|
26
|
-
// Trimmed, so a title of spaces is refused as required.
|
|
27
|
-
rules: { required: true, trim: true, maxLength: 120 },
|
|
28
|
-
},
|
|
23
|
+
// The site has no name of its own: it is the organization's, in every locale.
|
|
29
24
|
maintenance: {
|
|
30
25
|
kind: 'checkbox',
|
|
31
26
|
label: { en: 'Maintenance', ar: 'الصيانة' },
|
|
32
27
|
description: {
|
|
33
|
-
en: 'While this is on, the
|
|
34
|
-
ar: 'ما دام هذا مفعلا،
|
|
28
|
+
en: 'While this is on, the portal tells students it is closed for maintenance. Staff keep working.',
|
|
29
|
+
ar: 'ما دام هذا مفعلا، تخبر البوابة الطلاب بأنها مغلقة للصيانة. ويواصل الموظفون عملهم.',
|
|
35
30
|
},
|
|
36
31
|
},
|
|
37
|
-
// Where a
|
|
32
|
+
// Where a student writes to, the one address the portal shows: at its foot, and on its maintenance page.
|
|
38
33
|
contactEmail: { kind: 'email', label: { en: 'Contact email', ar: 'بريد التواصل' } },
|
|
39
34
|
// The column default is the one the table was made with, so the committed
|
|
40
35
|
// migrations still describe it.
|
|
41
36
|
globalType: { ...defaultGlobalFields.globalType, rules: { required: true, defaultValue: 'settings' } },
|
|
42
37
|
},
|
|
43
|
-
// Its own rows: the language's, and every
|
|
38
|
+
// Its own rows: the language's, and every backoffice role closed. The backoffice
|
|
44
39
|
// lists no global, since a list across tenants reads by `find` and a global
|
|
45
40
|
// declares none, so it reads none, 403 `error.forbidden.read`. A global takes
|
|
46
41
|
// read and update alone, and the other two the rows compile are never reached.
|
|
47
|
-
access: rows({ ...tenantAccess, ...
|
|
42
|
+
access: rows({ ...tenantAccess, ...backofficeClosed }) as Pick<ReturnType<typeof rows>, 'read' | 'update'>,
|
|
48
43
|
targets: {
|
|
49
44
|
service: { operations: defaultGlobalOperations },
|
|
50
45
|
// A global is one row, so its page is a custom one with no id: its form
|
|
@@ -52,14 +47,14 @@ export const settings = defineGlobal({
|
|
|
52
47
|
// yes or no setting a switch with its label beside it.
|
|
53
48
|
dashboard: {
|
|
54
49
|
description: {
|
|
55
|
-
en: '
|
|
56
|
-
ar: '
|
|
50
|
+
en: 'Where students reach the school, and whether the portal is open to them.',
|
|
51
|
+
ar: 'أين يتواصل الطلاب مع المدرسة، وهل البوابة مفتوحة لهم.',
|
|
57
52
|
},
|
|
58
53
|
pages: {
|
|
59
54
|
settings: {
|
|
60
55
|
path: '/settings',
|
|
61
56
|
page: appPage([{ layout: 'record', main: [{ form: [
|
|
62
|
-
{ section: '
|
|
57
|
+
{ section: 'contact', label: { en: 'Contact', ar: 'التواصل' }, fields: ['contactEmail'] },
|
|
63
58
|
{ section: 'availability', label: { en: 'Availability', ar: 'الإتاحة' }, fields: [{ field: 'maintenance', component: 'switch' }] },
|
|
64
59
|
] }] }]),
|
|
65
60
|
},
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { defaultFields
|
|
1
|
+
import { defaultFields } from 'mithaq'
|
|
2
2
|
import { z } from 'zod'
|
|
3
|
-
import { defineKayan, organization } from '
|
|
4
|
-
import { appPage, danger,
|
|
5
|
-
import { expectedUpdatedAt } from '
|
|
3
|
+
import { defineKayan, organization } from '@asas/mithaq'
|
|
4
|
+
import { appPage, danger, backofficeList, backofficeRecord } from '@asas/mithaq/mithaq/pages.ts'
|
|
5
|
+
import { expectedUpdatedAt } from '@asas/mithaq/steps/expected-updated-at.ts'
|
|
6
6
|
|
|
7
7
|
const tag = [{ section: 'tag', label: { en: 'Tag', ar: 'الوسم' }, fields: ['name'] }] as const
|
|
8
8
|
|
|
@@ -21,13 +21,13 @@ export const tags = defineKayan({
|
|
|
21
21
|
// What the picker on `courses.tags` shows for each row, and what its search
|
|
22
22
|
// runs a `like` over. Without it every tag is a uuid.
|
|
23
23
|
targets: {
|
|
24
|
-
// Every organization's rows on the
|
|
24
|
+
// Every organization's rows on the backoffice, read-only, and each row's
|
|
25
25
|
// record, which only reads.
|
|
26
|
-
|
|
26
|
+
backoffice: {
|
|
27
27
|
useAsTitle: 'name',
|
|
28
28
|
pages: {
|
|
29
|
-
...
|
|
30
|
-
...
|
|
29
|
+
...backofficeList('/tags', { columns: ['name', 'organization', 'createdAt'], filters: ['organization'], search: ['name'] }),
|
|
30
|
+
...backofficeRecord({ path: '/tags/:id', main: [{ section: 'tag', label: { en: 'Tag', ar: 'الوسم' }, fields: ['name'] }] }),
|
|
31
31
|
},
|
|
32
32
|
},
|
|
33
33
|
// A create opens as a sheet over the list. The name is copy a screen shows
|
|
@@ -36,20 +36,20 @@ export const tags = defineKayan({
|
|
|
36
36
|
dashboard: {
|
|
37
37
|
useAsTitle: 'name',
|
|
38
38
|
description: {
|
|
39
|
-
en: 'Labels
|
|
40
|
-
ar: 'تسميات تجمع
|
|
39
|
+
en: 'Labels the organization groups its records by.',
|
|
40
|
+
ar: 'تسميات تجمع بها المؤسسة سجلاتها.',
|
|
41
41
|
},
|
|
42
42
|
pages: {
|
|
43
43
|
list: { path: '/tags', page: appPage([{ layout: 'table', main: [{ list: { columns: ['name', 'createdAt'], search: ['name'] } }] }]) },
|
|
44
|
-
// The
|
|
44
|
+
// The drawer's title says it makes a tag, so its one box is drawn
|
|
45
45
|
// bare rather than under a second heading that says "Tag" again.
|
|
46
|
-
create: { path: '/tags/new', mode: '
|
|
46
|
+
create: { path: '/tags/new', mode: 'drawer', page: appPage([{ layout: 'record', main: [{ form: ['name'] }] }]) },
|
|
47
47
|
edit: { path: '/tags/:id', page: appPage([{ layout: 'record', main: [{ form: tag }], danger: danger() }]) },
|
|
48
48
|
},
|
|
49
49
|
},
|
|
50
50
|
service: {
|
|
51
|
-
operations: {
|
|
52
|
-
...
|
|
51
|
+
operations: ({ defaults }) => ({
|
|
52
|
+
...defaults,
|
|
53
53
|
/**
|
|
54
54
|
* The shipped update list with the `expectedUpdatedAt` step before it and
|
|
55
55
|
* its input widened by the key the step reads. A caller who sends the
|
|
@@ -63,7 +63,7 @@ export const tags = defineKayan({
|
|
|
63
63
|
input: ({ update }) => update.extend({ expectedUpdatedAt: z.date().optional() }),
|
|
64
64
|
steps: ({ steps }) => [expectedUpdatedAt, steps.update],
|
|
65
65
|
},
|
|
66
|
-
},
|
|
66
|
+
}),
|
|
67
67
|
},
|
|
68
68
|
},
|
|
69
69
|
fields: {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@asas/mithaq",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/mithaq.ts",
|
|
8
|
+
"./auth": "./src/mithaq/auth/index.ts",
|
|
9
|
+
"./auth/backoffice-roles": "./src/mithaq/auth/backoffice-roles.ts",
|
|
10
|
+
"./auth/organizations": "./src/mithaq/auth/organizations.ts",
|
|
11
|
+
"./*": "./src/*"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@better-auth/core": "1.7.4",
|
|
15
|
+
"@lexical/extension": "0.52.0",
|
|
16
|
+
"@lexical/link": "0.52.0",
|
|
17
|
+
"@lexical/list": "0.52.0",
|
|
18
|
+
"@lexical/rich-text": "0.52.0",
|
|
19
|
+
"aws4fetch": "1.0.20",
|
|
20
|
+
"better-auth": "1.7.4",
|
|
21
|
+
"file-type": "22.0.1",
|
|
22
|
+
"image-dimensions": "2.5.1",
|
|
23
|
+
"lexical": "0.52.0",
|
|
24
|
+
"mithaq": "*",
|
|
25
|
+
"mithaq-admin": "*"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/react": "^19.3.0",
|
|
29
|
+
"lucide-react": "^1.16.0",
|
|
30
|
+
"react": "^19.3.0",
|
|
31
|
+
"zod": "^4.6.5"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Which mailer this project sends with. `mailers.ts` beside this file is copied from the
|
|
2
|
+
// `mithaq-mail` registry item and owned by the project; a project on another service writes its
|
|
3
|
+
// adapter there and names it here. The auth setup sends the password reset, the invitation and the
|
|
4
|
+
// email verification through it, and refuses an open sign-up in production while none is configured.
|
|
5
|
+
import { defaultResendBaseURL, resendMailer, type Mailer } from './mailers.ts'
|
|
6
|
+
|
|
7
|
+
export { logMailer, type Mail, type Mailer } from './mailers.ts'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resend when both `RESEND_API_KEY` and `MAIL_FROM` are set, none when neither is. Half the settings
|
|
11
|
+
* is refused, so a typo never leaves a deploy sending nothing while it seems configured.
|
|
12
|
+
* `RESEND_BASE_URL`, Resend's own SDK variable, points it at another address that speaks its API.
|
|
13
|
+
*/
|
|
14
|
+
export const mailerFromEnv = (env: Record<string, string | undefined>): Mailer | undefined => {
|
|
15
|
+
const { RESEND_API_KEY: apiKey, MAIL_FROM: from } = env
|
|
16
|
+
if (!apiKey && !from) return undefined
|
|
17
|
+
if (!apiKey) throw new Error('MAIL_FROM is set and RESEND_API_KEY is not. Set both, or neither.')
|
|
18
|
+
if (!from) throw new Error('RESEND_API_KEY is set and MAIL_FROM is not: set MAIL_FROM to an address at a domain your Resend account verified.')
|
|
19
|
+
const baseURL = env.RESEND_BASE_URL || defaultResendBaseURL
|
|
20
|
+
// The key and every letter cross to it, so in the clear only to this machine, such as a test's catcher.
|
|
21
|
+
const at = URL.canParse(baseURL) ? new URL(baseURL) : undefined
|
|
22
|
+
if (!at || (at.protocol !== 'https:' && !(at.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(at.hostname)))) {
|
|
23
|
+
throw new Error(`RESEND_BASE_URL is ${JSON.stringify(baseURL)}: set an https address, or http on this machine.`)
|
|
24
|
+
}
|
|
25
|
+
return resendMailer({ apiKey, from, baseURL })
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The mailer this deploy configured, or none; the auth setup logs mail in development without one.
|
|
30
|
+
* The auth setup asks it as it loads, so every load reads the environment it is started with.
|
|
31
|
+
*/
|
|
32
|
+
export const configuredMailer = (): Mailer | undefined => mailerFromEnv(process.env)
|