@oj-module/cli 0.2.54 → 0.2.56
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/devkit/SKILL.md +3 -3
- package/devkit/manual.md +59 -52
- package/package.json +2 -2
- package/shell-dist/assets/{index-e_Mp2mb5.js → index-CImwskAz.js} +24 -11
- package/shell-dist/assets/runtime.js +2 -2
- package/shell-dist/index.html +3 -3
- package/shell-dist/versions.json +3 -3
- package/src/__tests__/layout.test.ts +30 -1
- package/src/__tests__/templates-schema-columns.test.ts +99 -0
- package/src/alias-paths.ts +11 -12
- package/src/builtin.ts +10 -3
- package/src/contract/__tests__/emit-client.test.ts +4 -4
- package/src/contract/__tests__/market-client.integration.test.ts +2 -2
- package/src/contract/__tests__/run-api.test.ts +2 -2
- package/src/contract/check.ts +6 -2
- package/src/contract/emit-client.ts +10 -3
- package/src/contract/run.ts +10 -2
- package/src/init.ts +19 -11
- package/src/layout.ts +72 -16
- package/src/lifecycle/__tests__/manifest-contract.test.ts +2 -2
- package/src/market/__tests__/bootstrap.test.ts +1 -1
- package/src/site.ts +1 -1
- package/templates/README.md +30 -20
- package/templates/init.include.yaml +19 -11
- package/templates/openapi.json +95 -86
- package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/users.ts +27 -0
- package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/oplog.ts +4 -2
- package/templates/src/app/api/_platform/migrations/0017__users_employee_no.mysql.sql +7 -0
- package/templates/src/app/api/_platform/migrations/0017__users_employee_no.pg.sql +4 -0
- package/templates/src/app/api/_platform/migrations/0017__users_employee_no.sql +8 -0
- package/templates/src/{_platform/api → app/api/_platform}/schema.yaml +5 -0
- package/templates/src/{auth/api → app/api/auth}/_shared/wecom/replay.ts +9 -3
- package/templates/src/{auth/api → app/api/auth}/_shared/wecom/tenant-admin.ts +18 -27
- package/templates/src/{system/api → app/api/system}/oplog/api.ts +5 -3
- package/templates/src/{_platform/web/__client/api.ts → app/web/_platform/__client/index.ts} +5 -2
- package/templates/src/{auth/web/__client/api.ts → app/web/auth/__client/index.ts} +5 -2
- package/templates/src/app/web/notification/pages/center/index.tsx +1 -1
- package/templates/src/app/web/notification/routes.ts +1 -1
- package/templates/src/app/web/personal-center/pages/my-profile/index.tsx +1 -1
- package/templates/src/app/web/personal-center/routes.ts +1 -1
- package/templates/src/app/web/system/__client/api.schemas.ts +1 -0
- package/templates/src/app/web/system/locales/en-US.json +2 -3
- package/templates/src/app/web/system/locales/zh-CN.json +2 -3
- package/templates/src/app/web/system/pages/departments/index.tsx +2 -2
- package/templates/src/app/web/system/pages/login-methods/index.tsx +1 -1
- package/templates/src/app/web/system/pages/oplog/index.tsx +44 -19
- package/templates/src/app/web/system/pages/roles/index.tsx +5 -3
- package/templates/src/app/web/system/pages/roles/visible-roles.ts +14 -0
- package/templates/src/app/web/system/pages/settings/index.tsx +2 -2
- package/templates/src/app/web/system/pages/users/can-manage.ts +21 -0
- package/templates/src/app/web/system/pages/users/index.tsx +25 -17
- package/templates/src/app/web/system/pages/users/roles.tsx +1 -1
- package/templates/src/app/web/system/routes.ts +1 -1
- package/templates/src/{web/web/__client/api.ts → app/web/web/__client/index.ts} +5 -2
- package/templates/src/oj-market/web/api/defaultClient.ts +1 -1
- package/templates/tests/auth-ldap.spec.ts +2 -2
- package/templates/tests/auth-session.spec.ts +3 -3
- package/templates/tests/auth-throttle.spec.ts +1 -1
- package/templates/tests/fixture.ts +6 -4
- package/templates/tests/guard.spec.ts +1 -1
- package/templates/tests/lifecycle/atomic-state.spec.ts +3 -3
- package/templates/tests/lifecycle/bootstrap.spec.ts +3 -3
- package/templates/tests/lifecycle/deploy-smoke.spec.ts +8 -8
- package/templates/tests/lifecycle/installer.spec.ts +10 -10
- package/templates/tests/lifecycle/market-api.spec.ts +14 -14
- package/templates/tests/lifecycle/migrate.spec.ts +6 -6
- package/templates/tests/lifecycle/preflight.spec.ts +3 -3
- package/templates/tests/lifecycle/register.spec.ts +1 -1
- package/templates/tests/lifecycle/semver.spec.ts +1 -1
- package/templates/tests/lifecycle/sha384.spec.ts +1 -1
- package/templates/tests/lifecycle/solutions-api.spec.ts +1 -1
- package/templates/tests/lifecycle/source-seq.spec.ts +1 -1
- package/templates/tests/lifecycle/steps-fs.spec.ts +9 -9
- package/templates/tests/lifecycle/text-edit.spec.ts +1 -1
- package/templates/tests/lifecycle/tgz-guard.spec.ts +2 -2
- package/templates/tests/lifecycle/trust.spec.ts +5 -5
- package/templates/tests/lifecycle/uninstall.spec.ts +7 -7
- package/templates/tests/lifecycle/verify-checks.spec.ts +5 -5
- package/templates/tests/lifecycle/verify-report.spec.ts +4 -4
- package/templates/tests/org-departments.spec.ts +10 -6
- package/templates/tests/org-read.spec.ts +7 -7
- package/templates/tests/query-utils.spec.ts +1 -1
- package/templates/tests/rbac-audit.spec.ts +1 -1
- package/templates/tests/rbac-contacts.spec.ts +2 -2
- package/templates/tests/rbac-guard.spec.ts +1 -1
- package/templates/tests/rbac-roles.spec.ts +10 -2
- package/templates/tests/rbac-seed.spec.ts +1 -1
- package/templates/tests/rbac-settings.spec.ts +77 -7
- package/templates/tests/rbac-solution-perms.spec.ts +1 -1
- package/templates/tests/rbac-tables.spec.ts +29 -0
- package/templates/tests/rbac-users.spec.ts +55 -3
- package/templates/tests/roles-page.spec.ts +40 -0
- package/templates/tests/seed-role-accounts.spec.ts +1 -1
- package/templates/tests/tenant-access-cache.spec.ts +1 -1
- package/templates/tests/tenant-forgery.spec.ts +5 -5
- package/templates/tests/tenant-isolation.spec.ts +5 -4
- package/templates/tests/username-namespace.spec.ts +3 -3
- package/templates/tests/users-page.spec.ts +39 -0
- package/templates/tests/wecom-admin.spec.ts +6 -6
- package/templates/tests/wecom-client-timeout.spec.ts +1 -1
- package/templates/tests/wecom-connector.spec.ts +9 -9
- package/templates/tests/wecom-corp.spec.ts +9 -9
- package/templates/tests/wecom-crypt.spec.ts +1 -1
- package/templates/tests/wecom-endpoints.spec.ts +2 -2
- package/templates/tests/wecom-landing.spec.ts +1 -1
- package/templates/tests/wecom-org-sync.spec.ts +7 -7
- package/templates/tests/wecom-sync.spec.ts +3 -3
- package/templates/tsconfig.paths.json +11 -11
- package/templates/vitest.config.ts +2 -2
- package/templates/web.config.ts +5 -2
- package/vendor/host-versions.json +3 -3
- package/templates/src/{_platform/api → app/api/_platform}/README.md +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/atomic.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/history.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/installer.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/limits.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/lock.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/semver.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/sha384.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/source-seq.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/state.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/deploy-web.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/download.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/extract.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/migrate.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/preflight.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/register.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/reload.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/smoke.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/verify.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/tasks.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/text-edit.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/tgz-guard.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/trust.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/types.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/uninstall.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/verify-checks.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/verify-report.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_shared/respond.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/contacts.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/org-read.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/org.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/roles.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/settings.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/solution-perms.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/access.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/audit.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/guard.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/query.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/rate.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/_shared/site-facade.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/contacts/api.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/contacts/detail/api.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/fixtures/l1-test-user.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/manifest.yaml +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/market/api.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/market/bootstrap.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/market/index-source.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0001__create_users.mysql.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0001__create_users.pg.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0001__create_users.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0002__add_users_avatar_base64.mysql.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0002__add_users_avatar_base64.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0003__users_source_display_name.mysql.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0003__users_source_display_name.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0004__sys_rbac.mysql.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0004__sys_rbac.pg.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0004__sys_rbac.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0005__users_must_change.mysql.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0005__users_must_change.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0006__users_is_forbidden.mysql.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0006__users_is_forbidden.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0007__users_email.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0008__sys_refresh_token.mysql.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0008__sys_refresh_token.pg.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0008__sys_refresh_token.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0009__sys_throttle.mysql.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0009__sys_throttle.pg.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0009__sys_throttle.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0010__sys_solution_route.mysql.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0010__sys_solution_route.pg.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0010__sys_solution_route.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0011__sys_setting_group_label.mysql.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0011__sys_setting_group_label.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0012__market_history.mysql.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0012__market_history.pg.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0012__market_history.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0013__users_profile.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0014__reg_source_seq.mysql.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0014__reg_source_seq.pg.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0014__reg_source_seq.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0015__tenant.mysql.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0015__tenant.pg.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0015__tenant.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0016__org.mysql.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0016__org.pg.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/migrations/0016__org.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/org/departments/api.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/org/is-member/api.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/org/members/api.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/org/picker/api.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/org/resolve/api.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/org/tree/api.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/org/user/api.ts +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/seed.mysql.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/seed.pg.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/seed.sql +0 -0
- package/templates/src/{_platform/api → app/api/_platform}/solutions/api.ts +0 -0
- package/templates/src/{auth/api → app/api/auth}/_shared/ldap-login.ts +0 -0
- package/templates/src/{auth/api → app/api/auth}/_shared/ldap.ts +0 -0
- package/templates/src/{auth/api → app/api/auth}/_shared/login-throttle.ts +0 -0
- package/templates/src/{auth/api → app/api/auth}/_shared/session.ts +0 -0
- package/templates/src/{auth/api → app/api/auth}/_shared/wecom/admin.ts +1 -1
- package/templates/src/{auth/api → app/api/auth}/_shared/wecom/client.ts +0 -0
- package/templates/src/{auth/api → app/api/auth}/_shared/wecom/config.ts +0 -0
- package/templates/src/{auth/api → app/api/auth}/_shared/wecom/contacts.ts +0 -0
- package/templates/src/{auth/api → app/api/auth}/_shared/wecom/crypt.ts +0 -0
- package/templates/src/{auth/api → app/api/auth}/_shared/wecom/events.ts +1 -1
- package/templates/src/{auth/api → app/api/auth}/_shared/wecom/login.ts +0 -0
- package/templates/src/{auth/api → app/api/auth}/_shared/wecom/payload.ts +0 -0
- package/templates/src/{auth/api → app/api/auth}/_shared/wecom/store.ts +0 -0
- package/templates/src/{auth/api → app/api/auth}/_shared/wecom/sync.ts +0 -0
- package/templates/src/{auth/api → app/api/auth}/api.ts +0 -0
- package/templates/src/{auth/api → app/api/auth}/ldap-login/api.ts +0 -0
- package/templates/src/{auth/api → app/api/auth}/manifest.yaml +0 -0
- package/templates/src/{auth/api → app/api/auth}/migrations/0001__wecom.mysql.sql +0 -0
- package/templates/src/{auth/api → app/api/auth}/migrations/0001__wecom.pg.sql +0 -0
- package/templates/src/{auth/api → app/api/auth}/migrations/0001__wecom.sql +0 -0
- package/templates/src/{auth/api → app/api/auth}/migrations/0002__org_sync.mysql.sql +0 -0
- package/templates/src/{auth/api → app/api/auth}/migrations/0002__org_sync.pg.sql +0 -0
- package/templates/src/{auth/api → app/api/auth}/migrations/0002__org_sync.sql +0 -0
- package/templates/src/{auth/api → app/api/auth}/migrations/0003__credential_kind.mysql.sql +0 -0
- package/templates/src/{auth/api → app/api/auth}/migrations/0003__credential_kind.pg.sql +0 -0
- package/templates/src/{auth/api → app/api/auth}/migrations/0003__credential_kind.sql +0 -0
- package/templates/src/{auth/api → app/api/auth}/schema.yaml +0 -0
- package/templates/src/{auth/api → app/api/auth}/wecom/admin/api.ts +0 -0
- package/templates/src/{auth/api → app/api/auth}/wecom/admin/tenant-admin/api.ts +1 -1
- package/templates/src/{auth/api → app/api/auth}/wecom/admin/tenant-admin/password/api.ts +1 -1
- /package/templates/src/{auth/api → app/api/auth}/wecom/api.ts +0 -0
- /package/templates/src/{auth/api → app/api/auth}/wecom/sync/api.ts +0 -0
- /package/templates/src/{home/api → app/api/home}/line/api.ts +0 -0
- /package/templates/src/{home/api → app/api/home}/manifest.yaml +0 -0
- /package/templates/src/{home/api → app/api/home}/migrations/0001__create_home_tables.sql +0 -0
- /package/templates/src/{home/api → app/api/home}/migrations/0002__tenant.mysql.sql +0 -0
- /package/templates/src/{home/api → app/api/home}/migrations/0002__tenant.sql +0 -0
- /package/templates/src/{home/api → app/api/home}/pie/api.ts +0 -0
- /package/templates/src/{home/api → app/api/home}/schema.yaml +0 -0
- /package/templates/src/{home/api → app/api/home}/seed.sql +0 -0
- /package/templates/src/{notification/api → app/api/notification}/_shared/notify.ts +0 -0
- /package/templates/src/{notification/api → app/api/notification}/api.ts +0 -0
- /package/templates/src/{notification/api → app/api/notification}/del/api.ts +0 -0
- /package/templates/src/{notification/api → app/api/notification}/manifest.yaml +0 -0
- /package/templates/src/{notification/api → app/api/notification}/migrations/0001__create_notifications.sql +0 -0
- /package/templates/src/{notification/api → app/api/notification}/migrations/0002__notifications_user.sql +0 -0
- /package/templates/src/{notification/api → app/api/notification}/migrations/0003__notifications_broadcast.sql +0 -0
- /package/templates/src/{notification/api → app/api/notification}/migrations/0004__notifications_broadcast_hidden.sql +0 -0
- /package/templates/src/{notification/api → app/api/notification}/migrations/0005__tenant.mysql.sql +0 -0
- /package/templates/src/{notification/api → app/api/notification}/migrations/0005__tenant.sql +0 -0
- /package/templates/src/{notification/api → app/api/notification}/schema.yaml +0 -0
- /package/templates/src/{notification/api → app/api/notification}/seed.sql +0 -0
- /package/templates/src/{personal-center/api → app/api/personal-center}/manifest.yaml +0 -0
- /package/templates/src/{personal-center/api → app/api/personal-center}/profile/api.ts +0 -0
- /package/templates/src/{personal-center/api → app/api/personal-center}/upload/api.ts +0 -0
- /package/templates/src/{system/api → app/api/system}/departments/api.ts +0 -0
- /package/templates/src/{system/api → app/api/system}/departments/detach/api.ts +0 -0
- /package/templates/src/{system/api → app/api/system}/departments/members/api.ts +0 -0
- /package/templates/src/{system/api → app/api/system}/departments/move/api.ts +0 -0
- /package/templates/src/{system/api → app/api/system}/login-methods/api.ts +0 -0
- /package/templates/src/{system/api → app/api/system}/manifest.yaml +0 -0
- /package/templates/src/{system/api → app/api/system}/permissions/api.ts +0 -0
- /package/templates/src/{system/api → app/api/system}/roles/api.ts +0 -0
- /package/templates/src/{system/api → app/api/system}/roles/bindings/api.ts +0 -0
- /package/templates/src/{system/api → app/api/system}/settings/api.ts +0 -0
- /package/templates/src/{system/api → app/api/system}/users/api.ts +0 -0
- /package/templates/src/{system/api → app/api/system}/users/forbidden/api.ts +0 -0
- /package/templates/src/{system/api → app/api/system}/users/password/api.ts +0 -0
- /package/templates/src/{system/api → app/api/system}/users/profile/api.ts +0 -0
- /package/templates/src/{system/api → app/api/system}/users/roles/api.ts +0 -0
- /package/templates/src/{web/api → app/api/web}/get-async-routes/api.ts +0 -0
- /package/templates/src/{web/api → app/api/web}/hello/api.ts +0 -0
- /package/templates/src/{web/api → app/api/web}/manifest.yaml +0 -0
- /package/templates/src/{web/api → app/api/web}/user-info/api.ts +0 -0
- /package/templates/src/{_platform/web → app/web/_platform}/__client/api.schemas.ts +0 -0
- /package/templates/src/{auth/web → app/web/auth}/__client/api.schemas.ts +0 -0
- /package/templates/src/app/web/home/__client/{api.ts → index.ts} +0 -0
- /package/templates/src/app/web/notification/__client/{api.ts → index.ts} +0 -0
- /package/templates/src/app/web/personal-center/__client/{api.ts → index.ts} +0 -0
- /package/templates/src/app/web/system/__client/{api.ts → index.ts} +0 -0
- /package/templates/src/{web → app/web}/web/__client/api.schemas.ts +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// BDD:P2-8 深检报告结构 + 留存 N=10 + 可信性项(trust_state)+ i18n 降级项 + 设值不进报告。
|
|
2
2
|
// 纯模块单测(verify-report.ts)+ 端点编排(persist-on-POST / GET verify/last / 降级项 / secret 不泄漏)。
|
|
3
3
|
/* eslint-disable test/prefer-lowercase-title -- 端点动词/路径是验收锚点,保留大写(verify-checks.spec 同约定) */
|
|
4
|
-
import type { FsFace } from "../../src/
|
|
5
|
-
import type { VerifyReport } from "../../src/
|
|
4
|
+
import type { FsFace } from "../../src/app/api/_platform/_lifecycle/atomic";
|
|
5
|
+
import type { VerifyReport } from "../../src/app/api/_platform/_lifecycle/verify-report";
|
|
6
6
|
import * as fsp from "node:fs/promises";
|
|
7
7
|
import { tmpdir } from "node:os";
|
|
8
8
|
import { join } from "node:path";
|
|
@@ -12,8 +12,8 @@ import {
|
|
|
12
12
|
persistReport,
|
|
13
13
|
VERIFY_REPORTS_KEEP,
|
|
14
14
|
verifyReportsDirOf,
|
|
15
|
-
} from "../../src/
|
|
16
|
-
import { createSolutionsApi, solutionsEnv } from "../../src/
|
|
15
|
+
} from "../../src/app/api/_platform/_lifecycle/verify-report";
|
|
16
|
+
import { createSolutionsApi, solutionsEnv } from "../../src/app/api/_platform/solutions/api";
|
|
17
17
|
import { installDb } from "../fixture";
|
|
18
18
|
|
|
19
19
|
const g = globalThis as any;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OrgDeps } from "../src/
|
|
1
|
+
import type { OrgDeps } from "../src/app/api/_platform/_shared/service/domain/org";
|
|
2
2
|
|
|
3
3
|
// BDD:组织架构领域(0016,方案 A1+A2)——CRUD 基线 + 归属权(D13)+ 保底出口(D14)。
|
|
4
4
|
//
|
|
@@ -23,8 +23,8 @@ import {
|
|
|
23
23
|
updateDepartment,
|
|
24
24
|
updateMember,
|
|
25
25
|
upsertDepartment,
|
|
26
|
-
} from "../src/
|
|
27
|
-
import { deleteUser, listUsers, updateUserProfile } from "../src/
|
|
26
|
+
} from "../src/app/api/_platform/_shared/service/domain/org";
|
|
27
|
+
import { deleteUser, listUsers, updateUserProfile } from "../src/app/api/_platform/_shared/service/domain/users";
|
|
28
28
|
import { ADMIN, installDb } from "./fixture";
|
|
29
29
|
|
|
30
30
|
let dbo: ReturnType<typeof installDb>;
|
|
@@ -263,12 +263,16 @@ describe("用户侧(改资料 / 删号)", () => {
|
|
|
263
263
|
expect(dbo.prepare("SELECT COUNT(*) c FROM sys_refresh_token WHERE user_id = ?").get(uid)).toMatchObject({ c: 0 });
|
|
264
264
|
});
|
|
265
265
|
|
|
266
|
-
it("
|
|
266
|
+
it("删号:禁止删自己;非 superadmin 删 superadmin → 403(最后超管的实际防线)", async () => {
|
|
267
267
|
await expect(deleteUser(ADMIN, ADMIN)).rejects.toMatchObject({ status: 400 });
|
|
268
268
|
const solo = mkUser("u_sup");
|
|
269
269
|
dbo.prepare("INSERT INTO sys_user_role (user_id, role_name) VALUES (?, 'superadmin')").run(solo);
|
|
270
|
-
|
|
271
|
-
//
|
|
270
|
+
// 目标门禁先于「最后一个 superadmin」计数:普通 admin 连碰都碰不到它。
|
|
271
|
+
// 计数兜底(400)因此在真实路径上不可达——操作者本身是 superadmin 时,
|
|
272
|
+
// 库里必然已有 ≥2 个 superadmin——保留它只为挡住未来的新写路径。
|
|
273
|
+
await expect(deleteUser(ADMIN, solo)).rejects.toMatchObject({ status: 403 });
|
|
274
|
+
expect(dbo.prepare("SELECT COUNT(*) c FROM users WHERE id = ?").get(solo)).toMatchObject({ c: 1 });
|
|
275
|
+
// 操作者升为 superadmin 后允许删(第二个超管在场)
|
|
272
276
|
dbo.prepare("INSERT INTO sys_user_role (user_id, role_name) VALUES (?, 'superadmin')").run(ADMIN);
|
|
273
277
|
await deleteUser(ADMIN, solo);
|
|
274
278
|
expect(dbo.prepare("SELECT COUNT(*) c FROM users WHERE id = ?").get(solo)).toMatchObject({ c: 0 });
|
|
@@ -8,15 +8,15 @@
|
|
|
8
8
|
// 反过来也钉住:返回字段与 scope 无关,消费模块不该按档位分支解析。
|
|
9
9
|
//
|
|
10
10
|
// 另外钉住 §9.1 阻断项:通讯录只出 activated=1,同步灌进来的「从未登录」账号不得被检索到。
|
|
11
|
-
import type { OrgDeps } from "../src/
|
|
11
|
+
import type { OrgDeps } from "../src/app/api/_platform/_shared/service/domain/org";
|
|
12
12
|
|
|
13
13
|
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
14
|
-
import { contactDetail, searchContacts } from "../src/
|
|
14
|
+
import { contactDetail, searchContacts } from "../src/app/api/_platform/_shared/service/domain/contacts";
|
|
15
15
|
import {
|
|
16
16
|
addMember,
|
|
17
17
|
createDepartment,
|
|
18
18
|
deleteDepartment,
|
|
19
|
-
} from "../src/
|
|
19
|
+
} from "../src/app/api/_platform/_shared/service/domain/org";
|
|
20
20
|
import {
|
|
21
21
|
isMemberOf,
|
|
22
22
|
pageDepartments,
|
|
@@ -28,10 +28,10 @@ import {
|
|
|
28
28
|
resolveReader,
|
|
29
29
|
resolveReadFace,
|
|
30
30
|
resolveUserIds,
|
|
31
|
-
} from "../src/
|
|
32
|
-
import { createUser } from "../src/
|
|
33
|
-
import { invalidateUser } from "../src/
|
|
34
|
-
import { ensureWecomUser } from "../src/
|
|
31
|
+
} from "../src/app/api/_platform/_shared/service/domain/org-read";
|
|
32
|
+
import { createUser } from "../src/app/api/_platform/_shared/service/domain/users";
|
|
33
|
+
import { invalidateUser } from "../src/app/api/_platform/_shared/service/infra/access";
|
|
34
|
+
import { ensureWecomUser } from "../src/app/api/auth/_shared/wecom/login";
|
|
35
35
|
import { ADMIN, installDb } from "./fixture";
|
|
36
36
|
|
|
37
37
|
let dbo: ReturnType<typeof installDb>;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/* eslint-disable test/prefer-lowercase-title -- AC 编号是验收锚点,保留大写 */
|
|
4
4
|
import { beforeAll, describe, expect, it } from "vitest";
|
|
5
5
|
|
|
6
|
-
import { likePattern, paged, pagedFactory } from "../src/
|
|
6
|
+
import { likePattern, paged, pagedFactory } from "../src/app/api/_platform/_shared/service/infra/query";
|
|
7
7
|
import { installDb } from "./fixture";
|
|
8
8
|
|
|
9
9
|
let dbo: ReturnType<typeof installDb>;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/* eslint-disable test/prefer-lowercase-title -- AC 编号是验收锚点,保留大写 */
|
|
4
4
|
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
|
5
5
|
|
|
6
|
-
import { auditDeny, auditOp, recordOp, resetDenyAudit } from "../src/
|
|
6
|
+
import { auditDeny, auditOp, recordOp, resetDenyAudit } from "../src/app/api/_platform/_shared/service/infra/audit";
|
|
7
7
|
import { installDb } from "./fixture";
|
|
8
8
|
|
|
9
9
|
let dbo: ReturnType<typeof installDb>;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
/* eslint-disable test/prefer-lowercase-title -- AC 编号是验收锚点,保留大写 */
|
|
6
6
|
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
|
7
7
|
|
|
8
|
-
import { contactDetail, searchContacts } from "../src/
|
|
8
|
+
import { contactDetail, searchContacts } from "../src/app/api/_platform/_shared/service/domain/contacts";
|
|
9
9
|
import { installDb } from "./fixture";
|
|
10
10
|
|
|
11
11
|
let dbo: ReturnType<typeof installDb>;
|
|
@@ -84,7 +84,7 @@ describe("详情与限速", () => {
|
|
|
84
84
|
it("接口级限速:同用户突发超限 → 429", async () => {
|
|
85
85
|
const uid = mkUser("c_rate");
|
|
86
86
|
g.http = { user: { id: uid } };
|
|
87
|
-
const { rateLimit } = await import("../src/
|
|
87
|
+
const { rateLimit } = await import("../src/app/api/_platform/_shared/service/infra/rate");
|
|
88
88
|
// 窗口 60s / 限额 3(测试参数;生产端点各自定额)
|
|
89
89
|
for (let i = 0; i < 3; i++)
|
|
90
90
|
await rateLimit(`contacts:${uid}`, 3, 60_000);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
/* eslint-disable test/prefer-lowercase-title -- AC 编号是验收锚点,保留大写 */
|
|
5
5
|
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
|
6
6
|
|
|
7
|
-
import { assertPermission, assertSuperAdmin, assertSystemAdmin, currentAccess, invalidateUser } from "../src/
|
|
7
|
+
import { assertPermission, assertSuperAdmin, assertSystemAdmin, currentAccess, invalidateUser } from "../src/app/api/_platform/_shared/service/infra/access";
|
|
8
8
|
import { installDb } from "./fixture";
|
|
9
9
|
|
|
10
10
|
let dbo: ReturnType<typeof installDb>;
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
/* eslint-disable test/prefer-lowercase-title -- AC 编号是验收锚点,保留大写 */
|
|
5
5
|
import { afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
6
6
|
|
|
7
|
-
import { createRole, deleteRole, listPermissions, listRoles, updateBindings } from "../src/
|
|
8
|
-
import { assertPermission } from "../src/
|
|
7
|
+
import { createRole, deleteRole, listPermissions, listRoles, updateBindings } from "../src/app/api/_platform/_shared/service/domain/roles";
|
|
8
|
+
import { assertPermission } from "../src/app/api/_platform/_shared/service/infra/access";
|
|
9
9
|
import { installDb } from "./fixture";
|
|
10
10
|
|
|
11
11
|
let dbo: ReturnType<typeof installDb>;
|
|
@@ -156,4 +156,12 @@ describe("列表支撑(C2 页面数据源)", () => {
|
|
|
156
156
|
expect(perms.find(p => p.code === "wiki.old")?.deprecated).toBe(true);
|
|
157
157
|
expect(perms.find(p => p.code === "wiki.new")?.deprecated).toBe(false);
|
|
158
158
|
});
|
|
159
|
+
|
|
160
|
+
it("listRoles 原样返回 superadmin(只在 /system/roles 展示层隐去)", async () => {
|
|
161
|
+
// 成员资格页(users/roles.tsx)把 superadmin 当可授予项(仅 superadmin 可勾选),
|
|
162
|
+
// 页面数据源里少了它会让「授予超级管理员」直接不可用。过滤只做在展示层。
|
|
163
|
+
dbo.prepare("INSERT INTO sys_role (name, module, builtin) VALUES ('superadmin', 'framework', 1)").run();
|
|
164
|
+
const roles = await listRoles();
|
|
165
|
+
expect(roles.map(r => r.name)).toContain("superadmin");
|
|
166
|
+
});
|
|
159
167
|
});
|
|
@@ -8,7 +8,7 @@ import { beforeAll, describe, expect, it } from "vitest";
|
|
|
8
8
|
|
|
9
9
|
import { installDb } from "./fixture";
|
|
10
10
|
|
|
11
|
-
const SEED = join(dirname(fileURLToPath(import.meta.url)), "..", "src", "
|
|
11
|
+
const SEED = join(dirname(fileURLToPath(import.meta.url)), "..", "src", "app", "api", "_platform", "seed.sql");
|
|
12
12
|
let dbo: ReturnType<typeof installDb>;
|
|
13
13
|
|
|
14
14
|
beforeAll(() => {
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
/* eslint-disable test/prefer-lowercase-title -- AC 编号是验收锚点,保留大写 */
|
|
3
3
|
import { afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
4
4
|
|
|
5
|
-
import { listLoginMethods, listSettings, updateSetting } from "../src/
|
|
6
|
-
import {
|
|
5
|
+
import { listLoginMethods, listSettings, updateSetting } from "../src/app/api/_platform/_shared/service/domain/settings";
|
|
6
|
+
import { recordOp } from "../src/app/api/_platform/_shared/service/infra/audit";
|
|
7
|
+
import { searchOplog } from "../src/app/api/_platform/_shared/service/infra/oplog";
|
|
7
8
|
import { installDb } from "./fixture";
|
|
8
9
|
|
|
9
10
|
let dbo: ReturnType<typeof installDb>;
|
|
@@ -27,6 +28,8 @@ describe("AC1-29/D13:操作日志查询(admin 可查)", () => {
|
|
|
27
28
|
// 按 api-manual §1153 契约建模(引擎语义的忠实复刻,非新逻辑)。
|
|
28
29
|
function fakeTable() {
|
|
29
30
|
const conds: Array<{ test: unknown, cond: any }> = [];
|
|
31
|
+
// 分页同样只记录不执行:count 查询不该带 limit/offset,list 查询必须带上
|
|
32
|
+
const paging: { limit: number | null, offset: number | null } = { limit: null, offset: null };
|
|
30
33
|
const b: any = {
|
|
31
34
|
whereIf: (test: unknown, cond: any) => {
|
|
32
35
|
if (test !== undefined && test !== null && test !== false && !(Array.isArray(test) && test.length === 0))
|
|
@@ -35,17 +38,23 @@ describe("AC1-29/D13:操作日志查询(admin 可查)", () => {
|
|
|
35
38
|
},
|
|
36
39
|
select: () => b,
|
|
37
40
|
orderBy: () => b,
|
|
38
|
-
limit: () =>
|
|
39
|
-
|
|
41
|
+
limit: (n: number) => {
|
|
42
|
+
paging.limit = n;
|
|
43
|
+
return b;
|
|
44
|
+
},
|
|
45
|
+
offset: (n: number) => {
|
|
46
|
+
paging.offset = n;
|
|
47
|
+
return b;
|
|
48
|
+
},
|
|
40
49
|
// 固定非零 total:保证 count/list 两条路径都构造(total=0 跳过 list 的行为
|
|
41
50
|
// 由 query-utils.spec 的 pagedFactory 用例覆盖,这里只关心条件发射)
|
|
42
51
|
all: async () => [{ c: 42 }],
|
|
43
52
|
};
|
|
44
|
-
return { b, conds };
|
|
53
|
+
return { b, conds, paging };
|
|
45
54
|
}
|
|
46
55
|
|
|
47
|
-
/** 以假构造器跑 searchOplog,回传 count
|
|
48
|
-
async function
|
|
56
|
+
/** 以假构造器跑 searchOplog,回传 count/list 两次构造([0]=count,[1]=list) */
|
|
57
|
+
async function captureBuilt(filter: Parameters<typeof searchOplog>[0]) {
|
|
49
58
|
const built: Array<ReturnType<typeof fakeTable>> = [];
|
|
50
59
|
const prevDb = g.db;
|
|
51
60
|
g.db = {
|
|
@@ -63,6 +72,12 @@ describe("AC1-29/D13:操作日志查询(admin 可查)", () => {
|
|
|
63
72
|
}
|
|
64
73
|
expect(built).toHaveLength(2); // count + list 各构造一次
|
|
65
74
|
expect(built[1].conds).toEqual(built[0].conds); // mk 工厂:两组条件一致
|
|
75
|
+
return built;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 回传 count 构造发射的条件 */
|
|
79
|
+
async function captureConds(filter: Parameters<typeof searchOplog>[0]) {
|
|
80
|
+
const built = await captureBuilt(filter);
|
|
66
81
|
return built[0].conds;
|
|
67
82
|
}
|
|
68
83
|
|
|
@@ -87,6 +102,61 @@ describe("AC1-29/D13:操作日志查询(admin 可查)", () => {
|
|
|
87
102
|
]);
|
|
88
103
|
expect(conds.some(c => (c.cond as { field?: string }).field === "level")).toBe(false);
|
|
89
104
|
});
|
|
105
|
+
|
|
106
|
+
it("page/limit 只作用在 list 查询:limit=size、offset=(page-1)*size,count 不带分页", async () => {
|
|
107
|
+
const [count, list] = await captureBuilt({ action: "user.create", page: 3, limit: 10 });
|
|
108
|
+
expect(count!.paging).toEqual({ limit: null, offset: null });
|
|
109
|
+
expect(list!.paging).toEqual({ limit: 10, offset: 20 });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("缺省/非法分页参数:第 1 页 20 条,size 静默钳 200", async () => {
|
|
113
|
+
expect((await captureBuilt({}))[1]!.paging).toEqual({ limit: 20, offset: 0 });
|
|
114
|
+
expect((await captureBuilt({ page: 0, limit: "abc" }))[1]!.paging).toEqual({ limit: 20, offset: 0 });
|
|
115
|
+
expect((await captureBuilt({ page: 2, limit: 10_000 }))[1]!.paging).toEqual({ limit: 200, offset: 200 });
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// 真库分页语义:上面用假构造器只钉「参数怎么发」,这里钉「切片/总数/还剩多少」算得对——
|
|
120
|
+
// 页导航能否正确显示页数全靠 total,靠 truncated 提示「结果被截断」的旧交互已删除。
|
|
121
|
+
describe("AC1-29:操作日志分页(真库切片 + total)", () => {
|
|
122
|
+
async function seedOplog(n: number) {
|
|
123
|
+
for (let i = 1; i <= n; i++)
|
|
124
|
+
await recordOp({ action: "test.page", module: "framework", target: `t${i}` });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
it("按 id 倒序切页:total 是全量而不是本页条数", async () => {
|
|
128
|
+
await seedOplog(5);
|
|
129
|
+
const p1 = await searchOplog({ page: 1, limit: 2 });
|
|
130
|
+
expect(p1.total).toBe(5);
|
|
131
|
+
expect(p1.list.map(r => r.target)).toEqual(["t5", "t4"]);
|
|
132
|
+
expect(p1.truncated).toBe(true);
|
|
133
|
+
|
|
134
|
+
const p2 = await searchOplog({ page: 2, limit: 2 });
|
|
135
|
+
expect(p2.list.map(r => r.target)).toEqual(["t3", "t2"]);
|
|
136
|
+
expect(p2.truncated).toBe(true);
|
|
137
|
+
|
|
138
|
+
// 末页:5 条 / 每页 2 → 第 3 页只剩 1 条,truncated=false(页导航据此停在第 3 页)
|
|
139
|
+
const p3 = await searchOplog({ page: 3, limit: 2 });
|
|
140
|
+
expect(p3.list.map(r => r.target)).toEqual(["t1"]);
|
|
141
|
+
expect(p3.truncated).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("越界页:list 为空但 total 仍为全量(前端据此纠正页码,而不是显示 0 条)", async () => {
|
|
145
|
+
await seedOplog(3);
|
|
146
|
+
const far = await searchOplog({ page: 99, limit: 20 });
|
|
147
|
+
expect(far.list).toEqual([]);
|
|
148
|
+
expect(far.total).toBe(3);
|
|
149
|
+
expect(far.truncated).toBe(false);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("过滤与分页叠加:total 按过滤后的集合算", async () => {
|
|
153
|
+
await seedOplog(5);
|
|
154
|
+
await recordOp({ action: "other.act", module: "framework", target: "x" });
|
|
155
|
+
const r = await searchOplog({ action: "test.page", page: 1, limit: 2 });
|
|
156
|
+
expect(r.total).toBe(5);
|
|
157
|
+
expect(r.list).toHaveLength(2);
|
|
158
|
+
expect(r.list.every(x => x.action === "test.page")).toBe(true);
|
|
159
|
+
});
|
|
90
160
|
});
|
|
91
161
|
|
|
92
162
|
describe("PRD §4.3:系统设置", () => {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
/* eslint-disable test/prefer-lowercase-title -- AC 编号是验收锚点,保留大写 */
|
|
5
5
|
import { afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
6
6
|
|
|
7
|
-
import { uninstallSolution, upsertSolutionPerms } from "../src/
|
|
7
|
+
import { uninstallSolution, upsertSolutionPerms } from "../src/app/api/_platform/_shared/service/domain/solution-perms";
|
|
8
8
|
import { installDb } from "./fixture";
|
|
9
9
|
|
|
10
10
|
let dbo: ReturnType<typeof installDb>;
|
|
@@ -61,3 +61,32 @@ describe("users 表扩展(PRD §4.2 数据模型)", () => {
|
|
|
61
61
|
expect(String(source?.dflt_value)).toContain("local");
|
|
62
62
|
});
|
|
63
63
|
});
|
|
64
|
+
|
|
65
|
+
// 回归:0017 给 users 加工号列。事故形态是「列进了库、schema.yaml 白名单漏了」,
|
|
66
|
+
// 构造器在发 SQL 前就拒成 `unknown column 'employee_no' in on 'users'`,日志里没有 SQL 可查。
|
|
67
|
+
// 白名单那半边在 packages/cli/src/__tests__/templates-schema-columns.test.ts 钉;
|
|
68
|
+
// 这里钉库侧语义:列真的落库、租户内唯一、NULL 不占位、跨租户可重复。
|
|
69
|
+
describe("users.employee_no(0017 工号列)", () => {
|
|
70
|
+
function addUser(username: string, tenant: string, employeeNo: string | null) {
|
|
71
|
+
dbo.prepare("INSERT INTO users (username, password_hash, tenant_id, employee_no) VALUES (?, 'x', ?, ?)")
|
|
72
|
+
.run(username, tenant, employeeNo);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
it("迁移落出 employee_no 列", () => {
|
|
76
|
+
expect(colsOf("users")).toContain("employee_no");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("唯一索引按 (tenant_id, employee_no) 且是部分索引(NULL 不占位)", () => {
|
|
80
|
+
const idx = dbo.prepare("SELECT sql FROM sqlite_master WHERE type = 'index' AND name = 'uk_users_employee_no'").get() as { sql: string | null } | undefined;
|
|
81
|
+
expect(idx?.sql).toContain("tenant_id");
|
|
82
|
+
expect(idx?.sql).toContain("employee_no IS NOT NULL");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("同租户工号重复被拒,跨租户可重复,NULL 可多行", () => {
|
|
86
|
+
addUser("eno-a", "acme", "E-100");
|
|
87
|
+
expect(() => addUser("eno-b", "acme", "E-100")).toThrow(/UNIQUE/);
|
|
88
|
+
expect(() => addUser("eno-c", "globex", "E-100")).not.toThrow();
|
|
89
|
+
expect(() => addUser("eno-d", "acme", null)).not.toThrow();
|
|
90
|
+
expect(() => addUser("eno-e", "acme", null)).not.toThrow();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
// 禁止操作自己(AC1-10 注);roles 写 sys_user_role 唯一权威源 + 写时失效(AC1-7/D10);
|
|
4
4
|
// 禁用/重置口令级联删 refresh(§3.1 吊销联动)。
|
|
5
5
|
/* eslint-disable test/prefer-lowercase-title -- AC 编号是验收锚点,保留大写 */
|
|
6
|
-
import type { SessionDeps } from "../src/
|
|
6
|
+
import type { SessionDeps } from "../src/app/api/auth/_shared/session";
|
|
7
7
|
|
|
8
8
|
import { afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
9
|
-
import { changePasswordByAdmin, createUser, listUsers, setForbidden, updateUserRoles } from "../src/
|
|
10
|
-
import { createSession } from "../src/
|
|
9
|
+
import { changePasswordByAdmin, createUser, deleteUser, listUsers, setForbidden, updateUserProfile, updateUserRoles } from "../src/app/api/_platform/_shared/service/domain/users";
|
|
10
|
+
import { createSession } from "../src/app/api/auth/_shared/session";
|
|
11
11
|
import { installDb } from "./fixture";
|
|
12
12
|
|
|
13
13
|
let dbo: ReturnType<typeof installDb>;
|
|
@@ -150,6 +150,58 @@ describe("B7:口令重置与禁用(级联吊销)", () => {
|
|
|
150
150
|
});
|
|
151
151
|
});
|
|
152
152
|
|
|
153
|
+
describe("红线:只有 superadmin 能改 superadmin 账号", () => {
|
|
154
|
+
function mkUserWithRole(username: string, role: string): number {
|
|
155
|
+
seedRole(role);
|
|
156
|
+
const uid = mkUser(username);
|
|
157
|
+
dbo.prepare("INSERT INTO sys_user_role (user_id, role_name) VALUES (?, ?)").run(uid, role);
|
|
158
|
+
return uid;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
it("admin 重置 superadmin 口令 / 启停 / 改资料 / 删除 → 403,且库里一个字段都没动", async () => {
|
|
162
|
+
seedRole("admin");
|
|
163
|
+
const actor = mkUserWithRole("u_admin1", "admin");
|
|
164
|
+
const target = mkUserWithRole("u_super1", "superadmin");
|
|
165
|
+
await expect(changePasswordByAdmin(actor, target, "newpw123", bcryptDeps)).rejects.toMatchObject({ status: 403 });
|
|
166
|
+
await expect(setForbidden(actor, target, true)).rejects.toMatchObject({ status: 403 });
|
|
167
|
+
await expect(updateUserProfile(actor, target, { displayName: "renamed", email: "x@y.z" })).rejects.toMatchObject({ status: 403 });
|
|
168
|
+
await expect(deleteUser(actor, target)).rejects.toMatchObject({ status: 403 });
|
|
169
|
+
const row = dbo.prepare("SELECT password_hash, is_forbidden, display_name, email FROM users WHERE id = ?").get(target) as { password_hash: string, is_forbidden: number, display_name: string | null, email: string | null };
|
|
170
|
+
expect(row.password_hash).toBe("x");
|
|
171
|
+
expect(Number(row.is_forbidden)).toBe(0);
|
|
172
|
+
expect(row.display_name).toBeNull();
|
|
173
|
+
expect(row.email).toBeNull();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("admin 清空 superadmin 的角色 → 403(成员资格只拦授予方向,反向降级靠这条)", async () => {
|
|
177
|
+
seedRole("admin");
|
|
178
|
+
const actor = mkUserWithRole("u_admin2", "admin");
|
|
179
|
+
const target = mkUserWithRole("u_super2", "superadmin");
|
|
180
|
+
await expect(updateUserRoles(actor, target, [])).rejects.toMatchObject({ status: 403 });
|
|
181
|
+
await expect(updateUserRoles(actor, target, ["viewer"])).rejects.toMatchObject({ status: 403 });
|
|
182
|
+
expect(rolesOf(target)).toEqual(["superadmin"]);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("superadmin 操作 superadmin → 放行(红线不是禁止改超管,而是限定谁能改)", async () => {
|
|
186
|
+
const actor = mkUserWithRole("u_super3", "superadmin");
|
|
187
|
+
const target = mkUserWithRole("u_super4", "superadmin");
|
|
188
|
+
await changePasswordByAdmin(actor, target, "newpw123", bcryptDeps);
|
|
189
|
+
await setForbidden(actor, target, true);
|
|
190
|
+
const row = dbo.prepare("SELECT password_hash, is_forbidden FROM users WHERE id = ?").get(target) as { password_hash: string, is_forbidden: number };
|
|
191
|
+
expect(row.password_hash).toBe("hashed:newpw123");
|
|
192
|
+
expect(Number(row.is_forbidden)).toBe(1);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("admin 操作普通用户照旧放行(门禁只盯 superadmin 目标,不能把整页封死)", async () => {
|
|
196
|
+
seedRole("admin");
|
|
197
|
+
const actor = mkUserWithRole("u_admin3", "admin");
|
|
198
|
+
const target = mkUser("u_plain");
|
|
199
|
+
await changePasswordByAdmin(actor, target, "newpw123", bcryptDeps);
|
|
200
|
+
await setForbidden(actor, target, true);
|
|
201
|
+
expect(Number((dbo.prepare("SELECT is_forbidden FROM users WHERE id = ?").get(target) as { is_forbidden: number }).is_forbidden)).toBe(1);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
153
205
|
describe("B7:列表(用户管理页数据源)", () => {
|
|
154
206
|
it("listUsers 带角色数组,排除口令哈希", async () => {
|
|
155
207
|
seedRole("editor");
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// BDD:/system/roles 展示页隐去 superadmin(事故:角色页把平台级角色当成站点可分配角色列出)。
|
|
2
|
+
//
|
|
3
|
+
// 过滤只发生在**展示层**,所以这里测纯函数而不是整页:页面本身只做 query + 渲染,
|
|
4
|
+
// 真正会回归的是「哪些行算可见」。后端那半边(listRoles 必须原样返回 superadmin)在
|
|
5
|
+
// tests/rbac-roles.spec.ts 里钉,两处合起来才防住「顺手把后端也过滤掉」的改法。
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
|
|
8
|
+
import { HIDDEN_ROLE, visibleRoles } from "../src/app/web/system/pages/roles/visible-roles";
|
|
9
|
+
|
|
10
|
+
const ROWS = [
|
|
11
|
+
{ name: "superadmin", module: "_platform" },
|
|
12
|
+
{ name: "admin", module: "_platform" },
|
|
13
|
+
{ name: "custom.wiki", module: "wiki" },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
describe("角色展示页:隐去 superadmin、其余原样", () => {
|
|
17
|
+
it("不列 superadmin,其余顺序不变", () => {
|
|
18
|
+
expect(visibleRoles(ROWS).map(r => r.name)).toEqual(["admin", "custom.wiki"]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("只按名字精确匹配:superadmin.* / 前缀相近的角色照常显示", () => {
|
|
22
|
+
const rows = [...ROWS, { name: "superadmin.readonly", module: "_platform" }, { name: "Admin", module: "_platform" }];
|
|
23
|
+
expect(visibleRoles(rows).map(r => r.name)).toEqual(["admin", "custom.wiki", "superadmin.readonly", "Admin"]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("不改入参(useQuery 缓存对象不能被就地改写)", () => {
|
|
27
|
+
const input = ROWS.map(r => ({ ...r }));
|
|
28
|
+
const before = input.map(r => r.name);
|
|
29
|
+
visibleRoles(input);
|
|
30
|
+
expect(input.map(r => r.name)).toEqual(before);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("空列表不炸(查询未返回时的形态)", () => {
|
|
34
|
+
expect(visibleRoles([])).toEqual([]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("隐藏项与后端保留角色名同值(改名必须两处一起改)", () => {
|
|
38
|
+
expect(HIDDEN_ROLE).toBe("superadmin");
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -16,7 +16,7 @@ import { beforeAll, describe, expect, it } from "vitest";
|
|
|
16
16
|
const req = createRequire(import.meta.url);
|
|
17
17
|
const { DatabaseSync: Db } = req("node:sqlite") as { DatabaseSync: typeof DatabaseSync };
|
|
18
18
|
|
|
19
|
-
const PLATFORM_API = join(dirname(fileURLToPath(import.meta.url)), "..", "src", "
|
|
19
|
+
const PLATFORM_API = join(dirname(fileURLToPath(import.meta.url)), "..", "src", "app", "api", "_platform");
|
|
20
20
|
|
|
21
21
|
let dbo: InstanceType<typeof DatabaseSync>;
|
|
22
22
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// 这里把「同一 uid 换租户必须重查」钉死,顺手钉住写时失效不得漏掉别的租户。
|
|
12
12
|
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
13
13
|
|
|
14
|
-
import { currentAccess, invalidateAllUsers, invalidateUser, isSelfTenant } from "../src/
|
|
14
|
+
import { currentAccess, invalidateAllUsers, invalidateUser, isSelfTenant } from "../src/app/api/_platform/_shared/service/infra/access";
|
|
15
15
|
import { installDb } from "./fixture";
|
|
16
16
|
|
|
17
17
|
const g = globalThis as any;
|
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
/* eslint-disable test/prefer-lowercase-title -- 用例标题带端点路径,保留原样更易检索 */
|
|
15
15
|
import { afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
16
16
|
|
|
17
|
-
import { invalidateUser } from "../src/
|
|
18
|
-
import notification from "../src/
|
|
19
|
-
import notificationDel from "../src/
|
|
20
|
-
import profile from "../src/personal-center/
|
|
21
|
-
import upload from "../src/personal-center/
|
|
17
|
+
import { invalidateUser } from "../src/app/api/_platform/_shared/service/infra/access";
|
|
18
|
+
import notification from "../src/app/api/notification/api";
|
|
19
|
+
import notificationDel from "../src/app/api/notification/del/api";
|
|
20
|
+
import profile from "../src/app/api/personal-center/profile/api";
|
|
21
|
+
import upload from "../src/app/api/personal-center/upload/api";
|
|
22
22
|
import { installDb } from "./fixture";
|
|
23
23
|
|
|
24
24
|
const g = globalThis as any;
|
|
@@ -80,14 +80,15 @@ describe("§5.7-6 读写自动按当前租户过滤", () => {
|
|
|
80
80
|
});
|
|
81
81
|
|
|
82
82
|
it("truncate 只清本租户(A 组表)", async () => {
|
|
83
|
+
// 用 home_pie 而非旧 demo 的 todos:后者已随示例模块删除,迁移里没有这张表。
|
|
83
84
|
asTenant("acme");
|
|
84
|
-
await db.table("
|
|
85
|
+
await db.table("home_pie").insert({ category: "acme-pie", value: 1 }).run();
|
|
85
86
|
asTenant("globex");
|
|
86
|
-
await db.table("
|
|
87
|
+
await db.table("home_pie").insert({ category: "globex-pie", value: 2 }).run();
|
|
87
88
|
|
|
88
89
|
asTenant("acme");
|
|
89
|
-
await db.table("
|
|
90
|
-
expect(rows("SELECT
|
|
90
|
+
await db.table("home_pie").truncate().run();
|
|
91
|
+
expect(rows("SELECT category FROM home_pie")).toEqual([{ category: "globex-pie" }]);
|
|
91
92
|
});
|
|
92
93
|
|
|
93
94
|
it("refresh token 换票:别家的 token_hash 在本租户查不到", async () => {
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
// · 端到端:改名被拒之后,真正的外部成员 JIT 登录拿到的是**新建**的那一行。
|
|
17
17
|
|
|
18
18
|
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
19
|
-
import { invalidateAllUsers } from "../src/
|
|
20
|
-
import { ensureWecomUser, externalUsername } from "../src/
|
|
21
|
-
import profile from "../src/personal-center/
|
|
19
|
+
import { invalidateAllUsers } from "../src/app/api/_platform/_shared/service/infra/access";
|
|
20
|
+
import { ensureWecomUser, externalUsername } from "../src/app/api/auth/_shared/wecom/login";
|
|
21
|
+
import profile from "../src/app/api/personal-center/profile/api";
|
|
22
22
|
import { installDb } from "./fixture";
|
|
23
23
|
|
|
24
24
|
const g = globalThis as any;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// BDD:/system/users 行级门禁——只有 superadmin 能改 superadmin 账号。
|
|
2
|
+
//
|
|
3
|
+
// 与 tests/rbac-users.spec.ts 的 assertCanManageTarget 用例互为两面:那边钉服务端 403,
|
|
4
|
+
// 这边钉「前端不给会 403 的按钮」(置灰)。两处都测同一个判定口径,改名必须一起改。
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { canManageUser, PLATFORM_ROLE } from "../src/app/web/system/pages/users/can-manage";
|
|
8
|
+
|
|
9
|
+
describe("用户管理页:superadmin 行对非超管置灰", () => {
|
|
10
|
+
it("admin 看普通用户 → 可操作", () => {
|
|
11
|
+
expect(canManageUser(["admin"], ["editor"])).toBe(true);
|
|
12
|
+
expect(canManageUser(["admin"], [])).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("admin 看 superadmin → 灰(口令/状态/成员资格一律禁用)", () => {
|
|
16
|
+
expect(canManageUser(["admin"], ["superadmin"])).toBe(false);
|
|
17
|
+
// 兼有 admin + editor 也不够:这条红线只认 superadmin
|
|
18
|
+
expect(canManageUser(["admin", "editor", "viewer"], ["superadmin", "editor"])).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("superadmin 看 superadmin → 可操作(红线是限定谁能改,不是禁止改)", () => {
|
|
22
|
+
expect(canManageUser(["superadmin"], ["superadmin"])).toBe(true);
|
|
23
|
+
expect(canManageUser(["superadmin"], ["admin"])).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("不看自己:本人行的禁用由页面另外用 uid 判,别混进来", () => {
|
|
27
|
+
// 空角色 + 空目标 = 可操作,说明本函数不承担 assertNotSelf 的职责
|
|
28
|
+
expect(canManageUser([], [])).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("角色名精确匹配:superadmin.readonly 不是平台超管", () => {
|
|
32
|
+
expect(canManageUser(["admin"], ["superadmin.readonly"])).toBe(true);
|
|
33
|
+
expect(canManageUser(["SuperAdmin"], ["superadmin"])).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("门禁常量与后端保留角色名同值(改名必须两处一起改)", () => {
|
|
37
|
+
expect(PLATFORM_ROLE).toBe("superadmin");
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
import { Buffer } from "node:buffer";
|
|
13
13
|
import { webcrypto } from "node:crypto";
|
|
14
14
|
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
15
|
-
import { assertSuperAdmin, invalidateAllUsers } from "../src/
|
|
16
|
-
import { listApps, setAppStatus } from "../src/
|
|
17
|
-
import { createWecomClient, runtimeCache, runtimeHttp } from "../src/
|
|
18
|
-
import { readSuiteConfig } from "../src/
|
|
15
|
+
import { assertSuperAdmin, invalidateAllUsers } from "../src/app/api/_platform/_shared/service/infra/access";
|
|
16
|
+
import { listApps, setAppStatus } from "../src/app/api/auth/_shared/wecom/admin";
|
|
17
|
+
import { createWecomClient, runtimeCache, runtimeHttp } from "../src/app/api/auth/_shared/wecom/client";
|
|
18
|
+
import { readSuiteConfig } from "../src/app/api/auth/_shared/wecom/config";
|
|
19
19
|
import { installDb } from "./fixture";
|
|
20
20
|
|
|
21
21
|
const g = globalThis as any;
|
|
@@ -124,11 +124,11 @@ const deps = { store: { encrypt: (p: string) => `gcm(${p})`, decrypt: (b: string
|
|
|
124
124
|
|
|
125
125
|
/** createInstall 的薄包装:省掉动态 import 的样板 */
|
|
126
126
|
async function createInstallForTest(cfg: ReturnType<typeof readSuiteConfig>, client: ReturnType<typeof createWecomClient>, state: string) {
|
|
127
|
-
const { createInstall } = await import("../src/
|
|
127
|
+
const { createInstall } = await import("../src/app/api/auth/_shared/wecom/admin");
|
|
128
128
|
return createInstall(cfg, client, state);
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
const api = async () => (await import("../src/
|
|
131
|
+
const api = async () => (await import("../src/app/api/auth/wecom/admin/api")).default;
|
|
132
132
|
|
|
133
133
|
beforeAll(() => {
|
|
134
134
|
dbo = installDb();
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// 4. tasks 缺失 → 安全降级为「不超时」,快速请求正常返回。
|
|
12
12
|
|
|
13
13
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
14
|
-
import { runtimeHttp } from "../src/
|
|
14
|
+
import { runtimeHttp } from "../src/app/api/auth/_shared/wecom/client";
|
|
15
15
|
|
|
16
16
|
const g = globalThis as any;
|
|
17
17
|
|