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
|
@@ -4,17 +4,29 @@
|
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"
|
|
7
|
+
"dev:setup": "node --env-file-if-exists=.env src/dev-setup.ts",
|
|
8
|
+
"seed": "node --env-file-if-exists=.env src/seed.ts"
|
|
8
9
|
},
|
|
9
10
|
"exports": {
|
|
10
11
|
".": "./src/asas.ts",
|
|
11
12
|
"./ui-dashboard/*": "./src/ui-dashboard/*",
|
|
12
|
-
"./audit-log/*": "./src/audit-log/*",
|
|
13
13
|
"./messages/*": "./src/messages/*",
|
|
14
|
+
"./organizations": "./src/organizations.ts",
|
|
14
15
|
"./package.json": "./package.json"
|
|
15
16
|
},
|
|
16
17
|
"dependencies": {
|
|
18
|
+
"@asas/audit-log": "*",
|
|
17
19
|
"@asas/auth": "*",
|
|
20
|
+
"@asas/courses": "*",
|
|
21
|
+
"@asas/media": "*",
|
|
22
|
+
"@asas/mithaq": "*",
|
|
23
|
+
"@asas/roles": "*",
|
|
24
|
+
"@asas/settings": "*",
|
|
25
|
+
"@asas/tags": "*",
|
|
26
|
+
"@lexical/extension": "0.52.0",
|
|
27
|
+
"@lexical/link": "0.52.0",
|
|
28
|
+
"@lexical/list": "0.52.0",
|
|
29
|
+
"@lexical/rich-text": "0.52.0",
|
|
18
30
|
"@orpc/openapi": "1.15.1",
|
|
19
31
|
"@orpc/server": "1.15.1",
|
|
20
32
|
"aws4fetch": "1.0.20",
|
|
@@ -23,7 +35,10 @@
|
|
|
23
35
|
"file-type": "22.0.1",
|
|
24
36
|
"image-dimensions": "2.5.1",
|
|
25
37
|
"jose": "6.2.12",
|
|
38
|
+
"lexical": "0.52.0",
|
|
26
39
|
"mithaq": "*",
|
|
40
|
+
"mithaq-admin": "*",
|
|
41
|
+
"pg-connection-string": "2.14.0",
|
|
27
42
|
"zod": "^4.6.5"
|
|
28
43
|
},
|
|
29
44
|
"devDependencies": {
|
|
@@ -31,8 +46,8 @@
|
|
|
31
46
|
"@types/pg": "^8.15.6",
|
|
32
47
|
"@types/react": "^19.3.0",
|
|
33
48
|
"lucide-react": "^1.16.0",
|
|
34
|
-
"mithaq-admin": "*",
|
|
35
49
|
"react": "^19.3.0",
|
|
36
|
-
"recharts": "3.8.0"
|
|
50
|
+
"recharts": "3.8.0",
|
|
51
|
+
"typescript": "^7.0.2"
|
|
37
52
|
}
|
|
38
53
|
}
|
|
@@ -1,43 +1,70 @@
|
|
|
1
|
-
import { providerStatements } from '@asas/auth'
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
import
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
import
|
|
1
|
+
import { providerStatements } from '@asas/mithaq/auth'
|
|
2
|
+
// Each feature is read through its index alone, as a namespace, so its list
|
|
3
|
+
// (`courses.kayan`) never collides with a kayan of the same name.
|
|
4
|
+
// add-on: audit-log
|
|
5
|
+
import * as auditLog from '@asas/audit-log'
|
|
6
|
+
// end add-on: audit-log
|
|
7
|
+
import * as auth from '@asas/auth'
|
|
8
|
+
// add-on: courses
|
|
9
|
+
import * as courses from '@asas/courses'
|
|
10
|
+
// end add-on: courses
|
|
11
|
+
// add-on: media
|
|
12
|
+
import * as media from '@asas/media'
|
|
13
|
+
// end add-on: media
|
|
14
|
+
// add-on: roles
|
|
15
|
+
import * as roles from '@asas/roles'
|
|
16
|
+
// end add-on: roles
|
|
17
|
+
// add-on: settings
|
|
18
|
+
import * as settings from '@asas/settings'
|
|
19
|
+
// end add-on: settings
|
|
20
|
+
import * as tags from '@asas/tags'
|
|
21
|
+
import { defaultMaxBodyBytes, fetchHttp, Notice } from 'mithaq'
|
|
19
22
|
import { orpcHttp } from 'mithaq/orpc'
|
|
20
|
-
import { buildAsas, requestContext } from '
|
|
21
|
-
import {
|
|
22
|
-
import { projectStatements } from '
|
|
23
|
-
import { superAdmin } from '
|
|
23
|
+
import { buildAsas, requestContext } from '@asas/mithaq'
|
|
24
|
+
import { backofficeHttp } from './backoffice-http.ts'
|
|
25
|
+
import { projectStatements } from '@asas/mithaq/roles/statements.ts'
|
|
26
|
+
import { superAdmin } from '@asas/mithaq/roles/tenant-access.ts'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Where a fault the build answers 500 to, or a strategy that threw, is reported: this project's
|
|
30
|
+
* log, the console. A project with an error tracker names it here. A `Notice` is expected, such
|
|
31
|
+
* as the build of a local auth kayan with no mailer, and prints as its one line; anything else
|
|
32
|
+
* prints with its stack.
|
|
33
|
+
*/
|
|
34
|
+
const report = (error: unknown) => console.error(error instanceof Notice ? error.message : error)
|
|
24
35
|
|
|
25
36
|
export const asas = buildAsas({
|
|
26
37
|
kayan: [
|
|
27
|
-
...
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
roles
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
media
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
...auth.kayan,
|
|
39
|
+
// add-on: roles
|
|
40
|
+
...roles.kayan,
|
|
41
|
+
// end add-on: roles
|
|
42
|
+
...tags.kayan,
|
|
43
|
+
// add-on: media
|
|
44
|
+
// An image is read by a holder of `media.create`, and otherwise through
|
|
45
|
+
// what the features pointing at images say of it.
|
|
46
|
+
...media.kayan({
|
|
47
|
+
// add-on: courses
|
|
48
|
+
reads: [courses.coverReads],
|
|
49
|
+
// end add-on: courses
|
|
50
|
+
// without add-on: courses
|
|
51
|
+
// reads: [],
|
|
52
|
+
// end without add-on: courses
|
|
53
|
+
}),
|
|
54
|
+
// end add-on: media
|
|
55
|
+
// add-on: courses
|
|
56
|
+
...courses.kayan,
|
|
57
|
+
// end add-on: courses
|
|
58
|
+
// add-on: audit-log
|
|
59
|
+
...auditLog.kayan,
|
|
60
|
+
// end add-on: audit-log
|
|
61
|
+
],
|
|
62
|
+
global: [
|
|
63
|
+
// add-on: settings
|
|
64
|
+
...settings.global,
|
|
65
|
+
// end add-on: settings
|
|
38
66
|
],
|
|
39
|
-
|
|
40
|
-
// The HTTP layer, a list the handler asks in order. `orpcHttp()` is first
|
|
67
|
+
// The HTTP layer, a list the handler asks in order. `orpcHttp` is first
|
|
41
68
|
// because it owns `/api/rpc`, where a typed client would reach the build, and
|
|
42
69
|
// because `asas.router` is the first plug's. It also claims `/api`, so the
|
|
43
70
|
// plain plug behind it answers only what oRPC's own OpenAPI arm matched
|
|
@@ -46,48 +73,46 @@ export const asas = buildAsas({
|
|
|
46
73
|
// leaves `http` out is refused.
|
|
47
74
|
// Each plug is handed the same context function, so a step reads the same
|
|
48
75
|
// bag whichever plug served it.
|
|
49
|
-
// The
|
|
50
|
-
// plain plug would read `/api/
|
|
51
|
-
// A fault either plug answers 500 is
|
|
52
|
-
//
|
|
76
|
+
// The backoffice's rename of an organization sits between them, before the
|
|
77
|
+
// plain plug would read `/api/backoffice/*` as a kayan's route.
|
|
78
|
+
// A fault either plug answers 500 is handed to `report`; core reports none of its own.
|
|
79
|
+
// A body past `maxBodyBytes` is answered 413 before it is read whole.
|
|
53
80
|
http: [
|
|
54
|
-
orpcHttp({ context: (request, actor) => ({ actor, ...requestContext(request) }), onError:
|
|
55
|
-
|
|
56
|
-
fetchHttp({ context: requestContext }),
|
|
81
|
+
orpcHttp({ context: (request, actor) => ({ actor, ...requestContext(request) }), onError: report, maxBodyBytes: defaultMaxBodyBytes }),
|
|
82
|
+
backofficeHttp(),
|
|
83
|
+
fetchHttp({ context: requestContext, onError: report, maxBodyBytes: defaultMaxBodyBytes }),
|
|
57
84
|
],
|
|
58
85
|
// Better Auth's own actions, which no declaration of this project derives.
|
|
59
86
|
// They join the catalog under the resources they name, so a role holds
|
|
60
87
|
// `invitation.cancel` or `user.impersonate` exactly as it holds
|
|
61
|
-
// `
|
|
88
|
+
// `tags.find`, and the hooks in @asas/mithaq/auth ask for them under whichever
|
|
62
89
|
// store this project turned on. `organization.rename` is this project's own,
|
|
63
|
-
// the key the
|
|
90
|
+
// the key the backoffice's rename route asks, which is neither the
|
|
64
91
|
// provider's `organization.update` nor the managed kayan's, both of which
|
|
65
92
|
// share that name. The project's own keys beside a kayan's operations are
|
|
66
93
|
// listed with the role form that offers them.
|
|
67
94
|
statements: { ...providerStatements, organization: [...providerStatements.organization, 'rename'], ...projectStatements },
|
|
68
95
|
// `onError` hears a strategy that threw and an access target that could not
|
|
69
|
-
// answer, which the build reports nowhere of its own.
|
|
70
|
-
// project's log; a project with an error tracker names it here. A `Notice`
|
|
71
|
-
// is expected, such as the build of a local auth kayan with no mailer, and
|
|
72
|
-
// prints as its one line; anything else prints with its stack.
|
|
96
|
+
// answer, which the build reports nowhere of its own.
|
|
73
97
|
//
|
|
74
98
|
// `admin.access` is the answer `/api/access` reports as `canAccessAdmin`:
|
|
75
|
-
// `true` for a super admin in the
|
|
99
|
+
// `true` for a super admin in the backoffice app, `false` for every other
|
|
76
100
|
// actor. The local `unlock` endpoint asks the same answer of the caller local
|
|
77
101
|
// auth alone resolves, which is never a Better Auth super admin, so it stays
|
|
78
102
|
// closed.
|
|
79
103
|
//
|
|
80
|
-
// The administrator is a super admin in the
|
|
104
|
+
// The administrator is a super admin in the backoffice app and nobody else: the
|
|
81
105
|
// one who verifies a local sign-up. A kayan with no callback of its own falls
|
|
82
106
|
// to this answer, so every table Better Auth alone writes closes itself.
|
|
83
|
-
auth: { admin: { kayan: user, access: superAdmin }, onError:
|
|
107
|
+
auth: { admin: { kayan: auth.user, access: superAdmin }, onError: report, maxBodyBytes: defaultMaxBodyBytes },
|
|
84
108
|
})
|
|
85
109
|
|
|
86
|
-
// The
|
|
87
|
-
// answers the
|
|
110
|
+
// The backoffice slug, the screen the backoffice app serves, the reader that
|
|
111
|
+
// answers the backoffice roles, and the tenant app's link to the backoffice, for a
|
|
88
112
|
// host.
|
|
89
|
-
export {
|
|
90
|
-
// What the
|
|
113
|
+
export { BACKOFFICE, BACKOFFICE_SCREEN, assignedRoles, backofficeEntry } from '@asas/mithaq/roles/tenant-access.ts'
|
|
114
|
+
// What the backoffice app wraps every request it serves in, whether a request is
|
|
91
115
|
// one of those, and where each app is served: re-exported here so a host reaches
|
|
92
116
|
// the one instance of the store behind it through the same module the build reads.
|
|
93
|
-
export {
|
|
117
|
+
export { asBackoffice, onBackoffice, backofficeURL, tenantURL } from '@asas/mithaq/auth'
|
|
118
|
+
export type { Language } from '@asas/mithaq'
|
|
@@ -1,23 +1,24 @@
|
|
|
1
|
-
// The
|
|
1
|
+
// The backoffice screen's one action no managed operation carries: renaming an
|
|
2
2
|
// organization and its slug. An organization has no admin route for staff:
|
|
3
3
|
// every route of Better Auth's organization plugin acts as a member of that
|
|
4
4
|
// organization, which staff are not, and the managed `organization` kayan
|
|
5
5
|
// writes nothing, because Better Auth owns the table and the tenant's `'*'`
|
|
6
|
-
// row, which every
|
|
6
|
+
// row, which every backoffice role takes too, names no update. So the rename is
|
|
7
7
|
// this one route of the project's own, refused without `organization.rename`,
|
|
8
8
|
// a key of the project's own that only `super_admin` holds, and audited in the
|
|
9
|
-
// line every admin route of the
|
|
9
|
+
// line every admin route of the backoffice writes.
|
|
10
10
|
//
|
|
11
11
|
// Deleting an organization, removing a member and changing a member's
|
|
12
12
|
// governance role stay out: each is irreversible or passes the plugin's own
|
|
13
13
|
// owner rules by, and staff do each by impersonating a member, which is
|
|
14
14
|
// audited under the real person's name.
|
|
15
|
-
import { AsasError, actorPermissions, denied, invalid, missing, type HttpPlug } from 'mithaq'
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
15
|
+
import { AsasError, actorPermissions, conflict, denied, invalid, missing, retryConflict, type HttpPlug } from 'mithaq'
|
|
16
|
+
import { namesOf, oneLine, backofficeRoles } from '@asas/mithaq/auth'
|
|
17
|
+
import { organizations as mode } from '@asas/mithaq/auth/organizations'
|
|
18
|
+
import { targets } from '@asas/mithaq'
|
|
19
|
+
import { assignedRoles, type OrganizationActor } from '@asas/mithaq/roles/tenant-access.ts'
|
|
19
20
|
|
|
20
|
-
const path = '/api/
|
|
21
|
+
const path = '/api/backoffice/organizations/update'
|
|
21
22
|
/**
|
|
22
23
|
* The project's own key, not `organization.update`: that name is both the
|
|
23
24
|
* provider's action and the managed kayan's update, which the rows refuse to
|
|
@@ -29,10 +30,19 @@ const slugShape = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
|
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* The longest name and slug a rename writes. The columns are unbounded `text`,
|
|
32
|
-
* so the caps are this route's own: a name as long as
|
|
33
|
-
* slug that fits in a URL segment.
|
|
33
|
+
* so the caps are this route's own: a name as long as the auth setup lets a
|
|
34
|
+
* founder write one, and a slug that fits in a URL segment.
|
|
34
35
|
*/
|
|
35
|
-
const most = { name:
|
|
36
|
+
const most = { name: 100, slug: 64 }
|
|
37
|
+
|
|
38
|
+
/** The names `written` holds as the auth setup keeps them, or `null` where it refuses them. */
|
|
39
|
+
const namedOrNull = (written: unknown) => {
|
|
40
|
+
try {
|
|
41
|
+
return namesOf(written)
|
|
42
|
+
} catch {
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
}
|
|
36
46
|
|
|
37
47
|
/** The body the shipped plain plug answers every refusal with. */
|
|
38
48
|
const codes: Record<number, string> = { 400: 'BAD_REQUEST', 403: 'FORBIDDEN', 404: 'NOT_FOUND', 409: 'CONFLICT' }
|
|
@@ -40,48 +50,58 @@ const refusal = (thrown: AsasError) =>
|
|
|
40
50
|
Response.json({ code: codes[thrown.status], data: { key: thrown.key, params: thrown.params }, message: thrown.message }, { status: thrown.status })
|
|
41
51
|
|
|
42
52
|
/**
|
|
43
|
-
* Answers the route only for an actor the template's reader gave a
|
|
44
|
-
* role holding `organization.rename`, which it does in the
|
|
53
|
+
* Answers the route only for an actor the template's reader gave a backoffice
|
|
54
|
+
* role holding `organization.rename`, which it does in the backoffice app alone:
|
|
45
55
|
* `'*'` inside a tenant is a tenant's and renames nothing here. The body is
|
|
46
|
-
* `{ id,
|
|
47
|
-
*
|
|
56
|
+
* `{ id, names, slug }`, a name per locale with the default locale's required,
|
|
57
|
+
* or `{ id, name, slug }`, the default locale's alone; a slug another
|
|
58
|
+
* organization holds is the store's own 409. With one organization its name
|
|
59
|
+
* lives in code, so the route renames nothing: `organizations` is the project's
|
|
60
|
+
* mode, which a test sets.
|
|
48
61
|
*/
|
|
49
|
-
export const
|
|
50
|
-
name: '
|
|
62
|
+
export const backofficeHttp = ({ organizations }: { organizations: 'one' | 'many' } = { organizations: mode }) => (): HttpPlug => ({
|
|
63
|
+
name: 'backoffice',
|
|
51
64
|
router: undefined,
|
|
52
65
|
handle: async (request, who) => {
|
|
53
66
|
if (request.method !== 'POST' || new URL(request.url).pathname !== path) return null
|
|
54
67
|
try {
|
|
55
68
|
const actor = (await who()) as (OrganizationActor & { id?: string }) | null
|
|
56
69
|
const held = actorPermissions(actor)
|
|
57
|
-
const staff = !!actor && assignedRoles(actor).some((slug) => Object.hasOwn(
|
|
70
|
+
const staff = !!actor && assignedRoles(actor).some((slug) => Object.hasOwn(backofficeRoles, slug))
|
|
58
71
|
if (!staff || !(held === true || held?.has(key))) {
|
|
59
|
-
throw denied({ message: `Forbidden: renaming an organization is the
|
|
72
|
+
throw denied({ message: `Forbidden: renaming an organization is the backoffice's, and it asks "${key}".`, key: 'error.forbidden.permission', params: { permission: key } })
|
|
60
73
|
}
|
|
61
74
|
// JSON alone, so a form another site posts, which a browser sends with
|
|
62
75
|
// no preflight, is never read as one.
|
|
63
76
|
const body = request.headers.get('content-type')?.startsWith('application/json') ? ((await request.json().catch(() => null)) as Record<string, unknown> | null) : null
|
|
77
|
+
if (organizations === 'one') {
|
|
78
|
+
throw conflict({ message: 'With one organization its name lives in code: organizationName in src/mithaq/auth/organizations.ts. Change it there and deploy.', key: 'error.conflict.organization_name_in_code' })
|
|
79
|
+
}
|
|
64
80
|
const id = body?.id
|
|
65
|
-
const name = typeof body?.name === 'string' ? body.name.trim() : ''
|
|
66
81
|
const slug = typeof body?.slug === 'string' ? body.slug.trim() : ''
|
|
67
|
-
|
|
82
|
+
// Each locale's name is the auth setup's own rule, so a rename keeps what a founder may write and no more.
|
|
83
|
+
const names = body?.names === undefined ? undefined : namedOrNull(body.names)
|
|
84
|
+
const name = names ? names.name : typeof body?.name === 'string' ? body.name.trim() : ''
|
|
85
|
+
// A name is one line: mail bodies carry it, where a break would be a line of the writer's own.
|
|
86
|
+
if (typeof id !== 'string' || !id || names === null || !name || name.length > most.name || new RegExp(oneLine.source, 'u').test(name) || !slugShape.test(slug) || slug.length > most.slug) {
|
|
68
87
|
throw invalid({
|
|
69
|
-
message: `An organization is renamed with its id, a name of at most ${most.name} characters, and a slug of at most ${most.slug} lowercase letters, digits and hyphens.`,
|
|
88
|
+
message: `An organization is renamed with its id, a name of one line and at most ${most.name} characters, and a slug of at most ${most.slug} lowercase letters, digits and hyphens.`,
|
|
70
89
|
key: 'error.invalid.value',
|
|
71
90
|
})
|
|
72
91
|
}
|
|
73
92
|
const store = targets.drizzle
|
|
74
93
|
// The row is found and written under one lock, so a delete between the
|
|
75
|
-
// two answers 404 rather than failing the write.
|
|
94
|
+
// two answers 404 rather than failing the write. `FOR UPDATE`, not `FOR NO
|
|
95
|
+
// KEY UPDATE`: the slug is unique, and the lock is taken before we know whether it changes.
|
|
76
96
|
await store.transaction(async (tx) => {
|
|
77
97
|
const found = await store.find({ kayan: 'organization', args: { limit: 1, lock: 'update', page: 1, sort: { field: 'id', direction: 'asc' }, where: { id: { equals: id } } }, tx })
|
|
78
98
|
if (!found.docs.length) throw missing({ message: `No organization "${id}".`, key: 'error.not_found.row', params: { kayan: 'organization' } })
|
|
79
|
-
await store.update({ tx, kayan: 'organization', id, data: { name, slug, updatedAt: new Date() } })
|
|
99
|
+
await store.update({ tx, kayan: 'organization', id, data: { name, ...(names && { names: names.names }), slug, updatedAt: new Date() } })
|
|
80
100
|
})
|
|
81
|
-
// The line `
|
|
101
|
+
// The line `backofficeAudit` writes for an admin route, under this route's own name.
|
|
82
102
|
console.log(JSON.stringify({
|
|
83
|
-
audit: '
|
|
84
|
-
action: '/
|
|
103
|
+
audit: 'backoffice',
|
|
104
|
+
action: '/backoffice/organizations/update',
|
|
85
105
|
actor: actor.id ?? null,
|
|
86
106
|
impersonatedBy: actor.impersonatedBy ?? null,
|
|
87
107
|
target: id,
|
|
@@ -89,6 +109,7 @@ export const platformHttp = () => (): HttpPlug => ({
|
|
|
89
109
|
}))
|
|
90
110
|
return Response.json({ id, name, slug })
|
|
91
111
|
} catch (thrown) {
|
|
112
|
+
thrown = retryConflict(thrown)
|
|
92
113
|
if (thrown instanceof AsasError) return refusal(thrown)
|
|
93
114
|
throw thrown
|
|
94
115
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Makes a fresh development database usable in one command: pushes the schema,
|
|
2
|
+
// makes the dev admin that DEV_ADMIN_EMAIL and DEV_ADMIN_PASSWORD name, and
|
|
3
|
+
// with --seed fills its organization.
|
|
4
|
+
//
|
|
5
|
+
// bun run dev:setup [--seed]
|
|
6
|
+
//
|
|
7
|
+
// The dev admin owns an organization and, where the project serves the backoffice
|
|
8
|
+
// console, is its super_admin, with a proved address, so it signs in to both
|
|
9
|
+
// apps at once. A development tool only: it refuses production, a database on
|
|
10
|
+
// another machine, and a database that already holds a user.
|
|
11
|
+
import { spawnSync } from 'node:child_process'
|
|
12
|
+
import { fileURLToPath } from 'node:url'
|
|
13
|
+
import { parseArgs } from 'node:util'
|
|
14
|
+
import { getTableConfig, type PgTable } from 'drizzle-orm/pg-core'
|
|
15
|
+
import { organizations as mode } from '@asas/mithaq/auth/organizations'
|
|
16
|
+
import { databaseOf } from './local-database.ts'
|
|
17
|
+
|
|
18
|
+
function refuse(reason: string): never {
|
|
19
|
+
console.error(`Refusing dev:setup: ${reason}`)
|
|
20
|
+
process.exit(1)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { values } = parseArgs({ options: { seed: { type: 'boolean' } } })
|
|
24
|
+
// Checked before the build is imported, since the import is what reads them.
|
|
25
|
+
if (!process.env.DATABASE_URL) refuse('set DATABASE_URL to the development database to set up.')
|
|
26
|
+
if (process.env.NODE_ENV && process.env.NODE_ENV !== 'development') refuse(`NODE_ENV is ${process.env.NODE_ENV}, and it sets up a development database only.`)
|
|
27
|
+
// The account it makes signs in with a password every project's .env.example
|
|
28
|
+
// holds, so it is made on this machine's database alone.
|
|
29
|
+
const at = databaseOf(process.env.DATABASE_URL)
|
|
30
|
+
if (!at) refuse('DATABASE_URL is not a connection string pg reads.')
|
|
31
|
+
const { database, local } = at
|
|
32
|
+
if (!local) refuse(`DATABASE_URL names ${database}, which is not this machine.`)
|
|
33
|
+
const { DEV_ADMIN_EMAIL: email, DEV_ADMIN_PASSWORD: password } = process.env
|
|
34
|
+
if (!email || !password) refuse('set DEV_ADMIN_EMAIL and DEV_ADMIN_PASSWORD in .env, as .env.example does.')
|
|
35
|
+
// Unset is development here, and the auth setup reads it: on this machine
|
|
36
|
+
// first-run then asks no setup code, as it does under `vite dev`.
|
|
37
|
+
Object.assign(process.env, { NODE_ENV: 'development' })
|
|
38
|
+
|
|
39
|
+
const { asas } = await import('./asas.ts')
|
|
40
|
+
const { isLocalHost, backofficeApp, backofficeURL, tenantURL } = await import('@asas/mithaq/auth')
|
|
41
|
+
// A deploy run with plain `node` sets no NODE_ENV and may hold its database on this machine; its address is what
|
|
42
|
+
// tells it apart, as the auth setup reads it, so an app served elsewhere gets no dev admin.
|
|
43
|
+
for (const [variable, url] of [['BETTER_AUTH_URL', tenantURL], ...(backofficeApp ? [['BACKOFFICE_URL', backofficeURL]] : [])] as const) {
|
|
44
|
+
if (!isLocalHost(url)) refuse(`${variable} names ${url}, which is not this machine.`)
|
|
45
|
+
}
|
|
46
|
+
const signIn = `Sign in at ${tenantURL}${backofficeApp ? ` or ${backofficeURL}` : ''}`
|
|
47
|
+
type Row = Record<string, unknown> & { id: string }
|
|
48
|
+
const { db, tables } = asas.targets.drizzle as unknown as { db: { $client: { query(text: string, values?: unknown[]): Promise<{ rows: Record<string, unknown>[] }> } }; tables: Record<'user' | 'organization', PgTable> }
|
|
49
|
+
const client = db.$client
|
|
50
|
+
/** A table of the build's own, as SQL names it. */
|
|
51
|
+
const named = (table: PgTable) => {
|
|
52
|
+
const { schema = 'public', name } = getTableConfig(table)
|
|
53
|
+
return [schema, name].map((part) => `"${part.replaceAll('"', '""')}"`).join('.')
|
|
54
|
+
}
|
|
55
|
+
// The provider's own routes and adapter: the generated auth kayan answer no
|
|
56
|
+
// operation to anyone.
|
|
57
|
+
const server = (asas.auth.strategies.find((strategy) => strategy.name === 'user') as unknown as {
|
|
58
|
+
server: {
|
|
59
|
+
$context: Promise<{
|
|
60
|
+
password: { hash(password: string): Promise<string>; config: { minPasswordLength: number; maxPasswordLength: number } }
|
|
61
|
+
internalAdapter: {
|
|
62
|
+
createUser(user: Record<string, unknown>): Promise<Row>
|
|
63
|
+
linkAccount(account: { userId: string; providerId: string; accountId: string; password: string }): Promise<unknown>
|
|
64
|
+
}
|
|
65
|
+
}>
|
|
66
|
+
api: {
|
|
67
|
+
firstRun(input: { body: Record<string, string> }): Promise<{ organization: { id: string } }>
|
|
68
|
+
createOrganization(input: { body: { name: string; slug: string; userId: string } }): Promise<{ id: string }>
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}).server
|
|
72
|
+
const name = 'Dev admin'
|
|
73
|
+
const workspace = `${name}'s workspace`
|
|
74
|
+
try {
|
|
75
|
+
// Asked before the push, so a database someone already uses is never changed by it.
|
|
76
|
+
const users = named(tables.user)
|
|
77
|
+
const [table] = (await client.query('select to_regclass($1) is not null as held', [users])).rows
|
|
78
|
+
if (table!.held && (await client.query(`select exists (select 1 from ${users}) as held`)).rows[0]!.held) {
|
|
79
|
+
refuse(`the database already holds a user. ${signIn} as DEV_ADMIN_EMAIL with DEV_ADMIN_PASSWORD from .env, or as the account you made.`)
|
|
80
|
+
}
|
|
81
|
+
const auth = await server.$context
|
|
82
|
+
// First-run asks the same of its owner; the path with many makes the account itself, so it asks here.
|
|
83
|
+
const { minPasswordLength: min, maxPasswordLength: max } = auth.password.config
|
|
84
|
+
if (password.length < min || password.length > max) refuse(`DEV_ADMIN_PASSWORD holds ${min} to ${max} characters.`)
|
|
85
|
+
await asas.connect()
|
|
86
|
+
let organization: string
|
|
87
|
+
if (mode === 'one') {
|
|
88
|
+
// First-run's own path: the owner, the one organization and, with the backoffice console, its super_admin.
|
|
89
|
+
organization = (await server.api.firstRun({ body: { name, email, password } })).organization.id
|
|
90
|
+
} else {
|
|
91
|
+
// With many, an account proves its address by a mailed link and then founds its organization; the dev admin is
|
|
92
|
+
// made proved, as first-run makes its owner, and its workspace made for it, as the page of someone in none would.
|
|
93
|
+
const user = await auth.internalAdapter.createUser({ name, email, emailVerified: true, ...(backofficeApp && { role: 'super_admin' }) })
|
|
94
|
+
await auth.internalAdapter.linkAccount({ userId: user.id, providerId: 'credential', accountId: user.id, password: await auth.password.hash(password) })
|
|
95
|
+
organization = (await server.api.createOrganization({ body: { name: workspace, slug: 'dev-admin-workspace', userId: user.id } })).id
|
|
96
|
+
}
|
|
97
|
+
// Read back, since first-run names the organization from code (`organizationName`).
|
|
98
|
+
const [made] = (await client.query(`select name from ${named(tables.organization)} where id = $1`, [organization])).rows
|
|
99
|
+
console.log(`Made the dev admin, owner of "${String(made!.name)}"${backofficeApp ? ' and backoffice super_admin' : ''}. ${signIn} with DEV_ADMIN_EMAIL and DEV_ADMIN_PASSWORD from .env.`)
|
|
100
|
+
} finally {
|
|
101
|
+
await asas.destroy()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (values.seed) {
|
|
105
|
+
// The seed names whose organization it fills by address, which stays in .env, in any case: the provider stores it
|
|
106
|
+
// lowercased.
|
|
107
|
+
const seeded = spawnSync(process.execPath, [fileURLToPath(new URL('./seed.ts', import.meta.url))], { env: { ...process.env, SEED_EMAIL: email }, stdio: ['inherit', 'pipe', 'pipe'], encoding: 'utf8' })
|
|
108
|
+
const address = new RegExp(email.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi')
|
|
109
|
+
process.stdout.write(seeded.stdout.replace(address, 'DEV_ADMIN_EMAIL'))
|
|
110
|
+
process.stderr.write(seeded.stderr.replace(address, 'DEV_ADMIN_EMAIL'))
|
|
111
|
+
process.exitCode = seeded.status ?? 1
|
|
112
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { parse } from 'pg-connection-string'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The database `url` names, as `host:port/name`, and whether it is on this machine, for the scripts that write to a
|
|
5
|
+
* development database; nothing when pg cannot read it. Read by pg's own parser, so the guard and the connection never
|
|
6
|
+
* disagree on the host, and a host left out is pg's default. Nothing of the URL is repeated on a failure, since it
|
|
7
|
+
* holds the password.
|
|
8
|
+
*/
|
|
9
|
+
export const databaseOf = (url: string) => {
|
|
10
|
+
let config: ReturnType<typeof parse>
|
|
11
|
+
try {
|
|
12
|
+
config = parse(url)
|
|
13
|
+
} catch {
|
|
14
|
+
return undefined
|
|
15
|
+
}
|
|
16
|
+
const host = config.host || process.env.PGHOST || 'localhost'
|
|
17
|
+
return {
|
|
18
|
+
database: `${host}:${config.port || process.env.PGPORT || '5432'}/${config.database ?? ''}`,
|
|
19
|
+
// A path is a socket on this machine.
|
|
20
|
+
local: host.startsWith('/') || ['localhost', '127.0.0.1', '::1', '[::1]'].includes(host),
|
|
21
|
+
}
|
|
22
|
+
}
|