@oj-module/cli 0.2.54 → 0.2.57
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/README.md +7 -4
- package/devkit/SKILL.md +7 -4
- package/devkit/manual.md +63 -54
- package/devkit/module-dev-and-cdn-playground.md +7 -0
- package/devkit/publish-walkthrough.md +199 -0
- package/package.json +2 -2
- package/shell-dist/assets/{index-e_Mp2mb5.js → index-CImwskAz.js} +24 -11
- package/shell-dist/assets/runtime.js +5 -5
- 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/__tests__/vendor-devkit.test.ts +87 -0
- package/src/alias-paths.ts +11 -12
- package/src/build/backend.ts +131 -0
- package/src/build/module-build.ts +364 -0
- package/src/build/module-def.ts +381 -0
- package/src/build/site-merge.ts +33 -0
- package/src/build.ts +16 -885
- package/src/builtin.ts +10 -3
- package/src/bundle.ts +61 -0
- package/src/cli.ts +4 -1
- 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 +31 -46
- package/src/layout.ts +72 -16
- package/src/lifecycle/__tests__/bundle-summary.test.ts +106 -0
- package/src/lifecycle/__tests__/bundle.test.ts +6 -0
- 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/src/vendor.ts +34 -0
- package/templates/README.md +30 -20
- package/templates/init.include.yaml +19 -11
- package/templates/openapi.json +102 -86
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/installer.ts +8 -3
- package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/register.ts +53 -0
- package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/roles.ts +4 -1
- package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/solution-perms.ts +7 -3
- 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/app/api/_platform/migrations/0018__sys_role_title_key.mysql.sql +3 -0
- package/templates/src/app/api/_platform/migrations/0018__sys_role_title_key.pg.sql +3 -0
- package/templates/src/app/api/_platform/migrations/0018__sys_role_title_key.sql +8 -0
- package/templates/src/{_platform/api → app/api/_platform}/schema.yaml +7 -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/{system/api → app/api/system}/roles/api.ts +2 -1
- 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 +2 -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 +12 -4
- 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/role-title-translate.spec.ts +171 -0
- 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 +19 -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/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/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/settings.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/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
|
@@ -31,7 +31,7 @@ import { download } from "./steps/download";
|
|
|
31
31
|
import { extract } from "./steps/extract";
|
|
32
32
|
import { maxAppliedSeq, migrate, rollbackGuard } from "./steps/migrate";
|
|
33
33
|
import { preflight } from "./steps/preflight";
|
|
34
|
-
import { registerSolution } from "./steps/register";
|
|
34
|
+
import { registerSolution, resolveDeclTitles, siteLanguage } from "./steps/register";
|
|
35
35
|
import { reloadAndAssert } from "./steps/reload";
|
|
36
36
|
import { smoke } from "./steps/smoke";
|
|
37
37
|
import { verify } from "./steps/verify";
|
|
@@ -261,7 +261,8 @@ async function lockCommitStage(req: InstallRequest): Promise<StageResult> {
|
|
|
261
261
|
}
|
|
262
262
|
|
|
263
263
|
/**
|
|
264
|
-
* manifest.yaml(C2 形状)→ SolutionDecl(D10 出口入参):roles[].title_key
|
|
264
|
+
* manifest.yaml(C2 形状)→ SolutionDecl(D10 出口入参):roles[].title/title_key 透传
|
|
265
|
+
* (title 此刻仍是原始 key,registering 段落库前经 resolveDeclTitles 按站点语言解析,R1/R4)、
|
|
265
266
|
* settings.groups[].items[] 摊平、enum options 序列化为 JSON 字符串(sys_setting.options 形态)。
|
|
266
267
|
*/
|
|
267
268
|
export function declOf(req: InstallRequest): SolutionDecl {
|
|
@@ -297,6 +298,7 @@ export function declOf(req: InstallRequest): SolutionDecl {
|
|
|
297
298
|
roles: (Array.isArray(m.roles) ? m.roles : []).filter(isObj).map(r => ({
|
|
298
299
|
name: typeof r.name === "string" ? r.name : "",
|
|
299
300
|
title: typeof r.title_key === "string" ? r.title_key : "",
|
|
301
|
+
title_key: typeof r.title_key === "string" ? r.title_key : undefined,
|
|
300
302
|
permissions: Array.isArray(r.permissions) ? r.permissions.map(String) : [],
|
|
301
303
|
})),
|
|
302
304
|
// 顶层 permissions[] 的 code:无角色权限点(系统预置点)唯一能落库的来源,别在此丢弃
|
|
@@ -316,7 +318,10 @@ export function declOf(req: InstallRequest): SolutionDecl {
|
|
|
316
318
|
}
|
|
317
319
|
|
|
318
320
|
async function registerStage(req: InstallRequest): Promise<StageResult> {
|
|
319
|
-
|
|
321
|
+
// R1/R4:落库前按站点默认语言解析角色 title(数据源 = 包内 locales,staging 此刻仍在;
|
|
322
|
+
// 解析失败回退原文绝不失败安装)。fsFace 注入读文件面(测试 stub 同 DIP 纪律)。
|
|
323
|
+
const decl = await resolveDeclTitles(declOf(req), req.staging, await siteLanguage(), p => fsFace().readFile(p));
|
|
324
|
+
return registerSolution(decl);
|
|
320
325
|
}
|
|
321
326
|
|
|
322
327
|
async function deployWebStage(req: InstallRequest): Promise<StageResult> {
|
|
@@ -65,6 +65,59 @@ export interface SolutionSnapshot {
|
|
|
65
65
|
permissions: string[]
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/** 角色 title 的裸 i18n key 形状(`pm:role.sales`;0018 迁移回填同口径) */
|
|
69
|
+
const TITLE_KEY_SHAPE = /^[a-z][\w-]*:/;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 站点默认语言(R4):sys_setting `site.language`,无行/空值回退 zh-CN
|
|
73
|
+
* (与前端 preference 缺省同值;未来设置面落库即自动生效)。
|
|
74
|
+
*/
|
|
75
|
+
export async function siteLanguage(): Promise<string> {
|
|
76
|
+
const rows = await db.table("sys_setting").select(["value"]).where({ field: "key", op: "eq", value: "site.language" }).all() as Array<{ value: string | null }>;
|
|
77
|
+
const v = rows[0]?.value;
|
|
78
|
+
return typeof v === "string" && v.trim() ? v.trim() : "zh-CN";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 角色 title 落库前解析(R1,registering 段消费;升级重装同路径 = R3 重解析)。
|
|
83
|
+
* 仅 title 命中 key 形状才尝试;ns(冒号前段)=== 模块名才读本包 locale(跨 ns 不越界);
|
|
84
|
+
* dotted path 取值须为非空 string。数据源 = 方案包 `web/modules/<ns>/locales/<lang>.json`
|
|
85
|
+
* (bundle 侧随包,见 cli bundle.ts)。任何失败(无文件/坏 JSON/缺 key/非 string)保留原文
|
|
86
|
+
* 并继续——翻译永不失败安装(R1 红线)。
|
|
87
|
+
*/
|
|
88
|
+
export async function resolveDeclTitles(
|
|
89
|
+
decl: SolutionDecl,
|
|
90
|
+
stagingDir: string,
|
|
91
|
+
lang: string,
|
|
92
|
+
readFile: (path: string) => Promise<Uint8Array>,
|
|
93
|
+
): Promise<SolutionDecl> {
|
|
94
|
+
const roles = [...decl.roles];
|
|
95
|
+
for (let i = 0; i < roles.length; i++) {
|
|
96
|
+
const title = roles[i]!.title;
|
|
97
|
+
if (!TITLE_KEY_SHAPE.test(title)) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const ns = title.slice(0, title.indexOf(":"));
|
|
101
|
+
if (ns !== decl.module) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const bytes = await readFile(`${stagingDir}/web/modules/${ns}/locales/${lang}.json`);
|
|
106
|
+
let node: unknown = JSON.parse(new TextDecoder().decode(bytes));
|
|
107
|
+
for (const part of title.slice(title.indexOf(":") + 1).split(".")) {
|
|
108
|
+
node = node == null ? undefined : (node as Record<string, unknown>)[part];
|
|
109
|
+
}
|
|
110
|
+
if (typeof node === "string" && node.trim()) {
|
|
111
|
+
roles[i] = { ...roles[i]!, title: node };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// 解析失败 = 回退原文(R1)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { ...decl, roles };
|
|
119
|
+
}
|
|
120
|
+
|
|
68
121
|
/** 首段提取(/wiki/docs → wiki)——占用判定粒度:前缀段冲突即整段首段比较(P1-5 备案③) */
|
|
69
122
|
function firstSeg(path: string): string {
|
|
70
123
|
return path.split("/")[1] ?? "";
|
|
@@ -9,6 +9,8 @@ export interface RoleRow {
|
|
|
9
9
|
name: string
|
|
10
10
|
module: string
|
|
11
11
|
title: string | null
|
|
12
|
+
/** 原始 i18n key(R2:solution 角色安装期解析锚;custom/builtin 为 null,UI 据此实时翻译) */
|
|
13
|
+
title_key: string | null
|
|
12
14
|
description: string | null
|
|
13
15
|
builtin: boolean
|
|
14
16
|
deprecated: boolean
|
|
@@ -60,7 +62,7 @@ async function invalidateHolders(roleName: string): Promise<void> {
|
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
export async function listRoles(): Promise<RoleRow[]> {
|
|
63
|
-
const roles = await db.table("sys_role").select(["name", "module", "title", "description", "builtin", "deprecated"]).orderBy([{ field: "name" }]).all() as Array<Record<string, unknown>>;
|
|
65
|
+
const roles = await db.table("sys_role").select(["name", "module", "title", "title_key", "description", "builtin", "deprecated"]).orderBy([{ field: "name" }]).all() as Array<Record<string, unknown>>;
|
|
64
66
|
const holders = await db.table("sys_user_role").select(["role_name", { fn: "count", as: "c" }]).groupBy(["role_name"]).all() as Array<{ role_name: string, c: number }>;
|
|
65
67
|
const binds = await db.table("sys_role_permission").select(["role_name", "permission_code"]).orderBy([{ field: "permission_code" }]).all() as Array<{ role_name: string, permission_code: string }>;
|
|
66
68
|
const holderMap = new Map(holders.map(h => [h.role_name, Number(h.c)]));
|
|
@@ -71,6 +73,7 @@ export async function listRoles(): Promise<RoleRow[]> {
|
|
|
71
73
|
name: String(r.name),
|
|
72
74
|
module: String(r.module),
|
|
73
75
|
title: r.title == null ? null : String(r.title),
|
|
76
|
+
title_key: r.title_key == null ? null : String(r.title_key),
|
|
74
77
|
description: r.description == null ? null : String(r.description),
|
|
75
78
|
builtin: Number(r.builtin) === 1,
|
|
76
79
|
deprecated: Number(r.deprecated) === 1,
|
package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/solution-perms.ts
RENAMED
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
export interface SolutionRoleDecl {
|
|
8
8
|
/** 完整角色名(含 `<module>.` 前缀,AC1-11 由安装器侧前缀校验) */
|
|
9
9
|
name: string
|
|
10
|
+
/** 解析后的可读文本(站点默认语言;解析失败 = 原始 key,R1 回退) */
|
|
10
11
|
title: string
|
|
12
|
+
/** 原始 i18n key(R2:UI 实时翻译锚,对齐 sys_permission.title_key 先例;解析失败与 title 同值) */
|
|
13
|
+
title_key?: string
|
|
11
14
|
/** 声明绑定:insert-if-absent 写入,管理员调整永不被覆写(AC1-15) */
|
|
12
15
|
permissions?: string[]
|
|
13
16
|
}
|
|
@@ -37,12 +40,13 @@ export async function upsertSolutionPerms(m: SolutionManifest): Promise<UpsertRe
|
|
|
37
40
|
|
|
38
41
|
for (const role of m.roles) {
|
|
39
42
|
const existing = await db.table("sys_role").select(["name"]).where({ field: "name", op: "eq", value: role.name }).all() as unknown[];
|
|
40
|
-
// upsert:行存在只覆盖元数据(title
|
|
43
|
+
// upsert:行存在只覆盖元数据(title/title_key——R3 升级即重解析:新包 locale 改进随升级传播),
|
|
44
|
+
// name/builtin 不动(AC2-2 元数据覆盖)
|
|
41
45
|
if (existing.length) {
|
|
42
|
-
await db.table("sys_role").update({ title: role.title }).where({ field: "name", op: "eq", value: role.name }).run();
|
|
46
|
+
await db.table("sys_role").update({ title: role.title, title_key: role.title_key ?? null }).where({ field: "name", op: "eq", value: role.name }).run();
|
|
43
47
|
}
|
|
44
48
|
else {
|
|
45
|
-
await db.table("sys_role").insert({ name: role.name, module: m.module, title: role.title, builtin: 0 }).run();
|
|
49
|
+
await db.table("sys_role").insert({ name: role.name, module: m.module, title: role.title, title_key: role.title_key ?? null, builtin: 0 }).run();
|
|
46
50
|
// 声明绑定只在角色创建时写一次(角色级 insert-if-absent,AC1-15 红线):
|
|
47
51
|
// 升级/重装对已存在角色永不补写声明绑定——管理员撤过的绑定不复活;
|
|
48
52
|
// 新版本给既有角色新增权限点时,由管理员显式勾选(insert-if-absent 的取舍边界,§9.5)。
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// 账号管理应用服务(管理域 /api/system/users,AC1-7~10 + B6 建号加固)。
|
|
2
2
|
// 红线:admin 不碰 superadmin/admin 成员资格(AC1-8,service 按 actor 角色自查);
|
|
3
|
+
// 只有 superadmin 能改 superadmin 账号(口令/启停/资料/删除/角色,见 assertCanManageTarget);
|
|
3
4
|
// 禁止操作自己(AC1-10 注);最后 superadmin 兜底(服务端二次防线);
|
|
4
5
|
// 角色唯一权威源 sys_user_role + 写时失效(AC1-7/D10);禁用/重置口令级联删 refresh(§3.1)。
|
|
5
6
|
// bcrypt 经注入(Node 侧全局不可 stub,同 session.ts 约定)。
|
|
@@ -59,6 +60,27 @@ function assertNotSelf(actorId: number, targetUid: number): void {
|
|
|
59
60
|
fail(400, "cannot operate on yourself");
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
/**
|
|
64
|
+
* 红线(RBAC 保护红线):只有 superadmin 能改 superadmin。
|
|
65
|
+
*
|
|
66
|
+
* 与 `assertMembershipAllowed` 的分工:那条按**被授予的角色**管提权方向(谁能任命 admin /
|
|
67
|
+
* superadmin),这条按**目标账号当前的角色**管反向——谁能动一个已经是 superadmin 的号。
|
|
68
|
+
* 缺了它,同一租户的 admin 可以:重置 superadmin 口令(知道口令即接管账号,而 superadmin
|
|
69
|
+
* 是平台级、能跨租户)/把人禁用/把角色整集清空——后者连 `assertMembershipAllowed`
|
|
70
|
+
* 都拦不住(`next` 里没有 admin/superadmin 时它直接放行)。故口令/启停/资料/删除/角色
|
|
71
|
+
* 五条写路径统一走这里。
|
|
72
|
+
*
|
|
73
|
+
* 读角色走 `rolesOf()`,与写入同受请求租户注入约束:只能影响本租户的人。
|
|
74
|
+
*/
|
|
75
|
+
async function assertCanManageTarget(actorId: number, targetUid: number): Promise<void> {
|
|
76
|
+
const targetRoles = await rolesOf(Number(targetUid));
|
|
77
|
+
if (!targetRoles.includes("superadmin"))
|
|
78
|
+
return;
|
|
79
|
+
if ((await rolesOf(Number(actorId))).includes("superadmin"))
|
|
80
|
+
return;
|
|
81
|
+
fail(403, "modifying a superadmin requires superadmin");
|
|
82
|
+
}
|
|
83
|
+
|
|
62
84
|
/**
|
|
63
85
|
* AC1-8(已修订):成员资格提权守卫——按**被授予的角色**分层,不再一刀切。
|
|
64
86
|
*
|
|
@@ -200,6 +222,7 @@ export async function updateUserRoles(actorId: number, targetUid: number, roles:
|
|
|
200
222
|
const target = await db.table("users").select(["id"]).where({ field: "id", op: "eq", value: targetUid }).all();
|
|
201
223
|
if (!target.length)
|
|
202
224
|
fail(404, "user not found");
|
|
225
|
+
await assertCanManageTarget(actorId, targetUid);
|
|
203
226
|
const next = [...new Set(roles.map(String))];
|
|
204
227
|
if (next.length) {
|
|
205
228
|
const known = await db.table("sys_role").select(["name"]).where({ field: "name", op: "in", value: next }).all();
|
|
@@ -258,6 +281,7 @@ export async function changePasswordByAdmin(actorId: number, targetUid: number,
|
|
|
258
281
|
const rows = await db.table("users").select(["id"]).where({ field: "id", op: "eq", value: targetUid }).all();
|
|
259
282
|
if (!rows.length)
|
|
260
283
|
fail(404, "user not found");
|
|
284
|
+
await assertCanManageTarget(actorId, targetUid);
|
|
261
285
|
const hash = await deps.bcryptHash(String(newPassword));
|
|
262
286
|
await db.tx(async (t) => {
|
|
263
287
|
await t.table("users").update({ password_hash: hash, must_change: 1 }).where({ field: "id", op: "eq", value: targetUid }).run();
|
|
@@ -273,6 +297,7 @@ export async function setForbidden(actorId: number, targetUid: number, forbidden
|
|
|
273
297
|
const rows = await db.table("users").select(["id"]).where({ field: "id", op: "eq", value: targetUid }).all();
|
|
274
298
|
if (!rows.length)
|
|
275
299
|
fail(404, "user not found");
|
|
300
|
+
await assertCanManageTarget(actorId, targetUid);
|
|
276
301
|
await db.tx(async (t) => {
|
|
277
302
|
await t.table("users").update({ is_forbidden: forbidden ? 1 : 0 }).where({ field: "id", op: "eq", value: targetUid }).run();
|
|
278
303
|
if (forbidden)
|
|
@@ -294,6 +319,7 @@ export async function updateUserProfile(actorId: number, targetUid: number, patc
|
|
|
294
319
|
const rows = await db.table("users").select(["id"]).where({ field: "id", op: "eq", value: targetUid }).all();
|
|
295
320
|
if (!rows.length)
|
|
296
321
|
fail(404, "user not found");
|
|
322
|
+
await assertCanManageTarget(actorId, targetUid);
|
|
297
323
|
const cols: Record<string, unknown> = {};
|
|
298
324
|
if (patch.displayName !== undefined)
|
|
299
325
|
cols.display_name = patch.displayName == null ? null : String(patch.displayName).trim() || null;
|
|
@@ -320,6 +346,7 @@ export async function deleteUser(actorId: number, targetUid: number): Promise<vo
|
|
|
320
346
|
const rows = await db.table("users").select(["id", "username"]).where({ field: "id", op: "eq", value: targetUid }).all() as Array<{ id: number, username: string }>;
|
|
321
347
|
if (!rows.length)
|
|
322
348
|
fail(404, "user not found");
|
|
349
|
+
await assertCanManageTarget(actorId, targetUid);
|
|
323
350
|
const roles = await rolesOf(Number(targetUid));
|
|
324
351
|
if (roles.includes("superadmin")) {
|
|
325
352
|
const supers = await db.table("sys_user_role").select([{ fn: "count", as: "c" }]).where({ field: "role_name", op: "eq", value: "superadmin" }).all() as Array<{ c: number }>;
|
|
@@ -21,7 +21,7 @@ export interface OplogPage {
|
|
|
21
21
|
truncated: boolean
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export async function searchOplog(filter: { action?: unknown, username?: unknown, module?: unknown, limit?: unknown }): Promise<OplogPage> {
|
|
24
|
+
export async function searchOplog(filter: { action?: unknown, username?: unknown, module?: unknown, page?: unknown, limit?: unknown }): Promise<OplogPage> {
|
|
25
25
|
// 构造器(v0.1.82):可选过滤走 whereIf,条件写一份(mk 工厂),count + list 复用;
|
|
26
26
|
// username 走语义 op contains(引擎转义 %/_/!,等价旧 LIKE ? ESCAPE '!' 的 %kw%)。
|
|
27
27
|
// 照抄口径:devkit scenarios 场景 42 ④。
|
|
@@ -35,6 +35,8 @@ export async function searchOplog(filter: { action?: unknown, username?: unknown
|
|
|
35
35
|
.whereIf(action, { field: "action", op: "eq", value: action })
|
|
36
36
|
.whereIf(module, { field: "module", op: "eq", value: module })
|
|
37
37
|
.whereIf(username, { field: "username", op: "contains", value: username });
|
|
38
|
-
|
|
38
|
+
// page/limit 成对:limit 是每页条数(pageArgs 钳 200),缺省第 1 页 / 20 条。
|
|
39
|
+
// total 由 count 查询单独给出,页面按 total 算页数——不再靠 truncated 提示「结果被截断」。
|
|
40
|
+
const r = await pagedFactory<OplogRow>(mk, { page: filter.page, size: filter.limit });
|
|
39
41
|
return { list: r.list, total: r.total, truncated: r.truncated };
|
|
40
42
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
-- 0017__users_employee_no(mysql 方言):工号列 + 租户内唯一。列语义同 0017__users_employee_no.sql 头注释。
|
|
2
|
+
-- 差异(沿用 0015__tenant.mysql.sql 惯例):
|
|
3
|
+
-- · 键列不能用 TEXT 落唯一键 → 按业务长度收窄为 VARCHAR(64);
|
|
4
|
+
-- · 无部分索引 → 普通唯一键,NULL 在 MySQL 里可重复出现,效果等同「有值才唯一」;
|
|
5
|
+
-- · (255 + 64) × 4B = 1276B < InnoDB 3072B 上限。
|
|
6
|
+
ALTER TABLE users ADD COLUMN employee_no VARCHAR(64) NULL;
|
|
7
|
+
ALTER TABLE users ADD UNIQUE KEY uk_users_employee_no (tenant_id, employee_no);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
-- 0017__users_employee_no(postgres 方言):工号列 + 租户内唯一。列语义同 0017__users_employee_no.sql 头注释。
|
|
2
|
+
-- 差异(沿用 0016__org.pg.sql 惯例):TEXT 在 pg 合法且可用于部分唯一索引,无需收窄类型。
|
|
3
|
+
ALTER TABLE users ADD COLUMN employee_no TEXT;
|
|
4
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uk_users_employee_no ON users (tenant_id, employee_no) WHERE employee_no IS NOT NULL;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
-- 0017__users_employee_no(sqlite 方言):工号列 + 租户内唯一
|
|
2
|
+
--
|
|
3
|
+
-- 为什么单开一列而不是复用 display_name / 外部 origin_id:用户 CSV 导入导出要与源系统
|
|
4
|
+
-- 9 列表头完全互通,「工号」必须是独立可查字段。
|
|
5
|
+
-- 可空是必需的:存量本地账号没有工号,外部同步号也可能不带;唯一性只在有值时有意义,
|
|
6
|
+
-- 故用部分索引(sqlite/pg 都支持 WHERE),mysql 侧靠「NULL 可重复」的唯一键等效。
|
|
7
|
+
ALTER TABLE users ADD COLUMN employee_no TEXT;
|
|
8
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uk_users_employee_no ON users (tenant_id, employee_no) WHERE employee_no IS NOT NULL;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
-- 0018__sys_role_title_key(sqlite 方言):角色显示名双列(R2,计划 docs/plans/20261103-0948-role-title-translate)。
|
|
2
|
+
-- title = 安装期解析文本(站点默认语言);title_key = 原始 i18n key(UI 实时翻译锚,
|
|
3
|
+
-- 对齐 sys_permission.title_key 先例)。可空:内置角色与 custom 角色无 key。
|
|
4
|
+
-- 回填口径:builtin=0 且 title 命中裸 key 形状。sqlite 无 REGEXP → GLOB '[a-z]*:*'
|
|
5
|
+
-- (首字符小写字母 + 含冒号的近似);真实数据里含冒号的 title 只可能是裸 key
|
|
6
|
+
-- (内置中文/管理员文本不含冒号),pg/mysql 两侧用完整正则同义钉死。
|
|
7
|
+
ALTER TABLE sys_role ADD COLUMN title_key TEXT;
|
|
8
|
+
UPDATE sys_role SET title_key = title WHERE builtin = 0 AND title GLOB '[a-z]*:*';
|
|
@@ -37,6 +37,11 @@ tables:
|
|
|
37
37
|
activated: {type: integer, null: false}
|
|
38
38
|
# 职务(与来源无关的通用字段)
|
|
39
39
|
position: {type: text}
|
|
40
|
+
# 工号(0017,可空):与姓名/用户名分开的独立工号列,租户内有值时唯一。
|
|
41
|
+
# 必须在这里声明——列只落库而不进白名单时,任何模块 select 它都会被构造器拒为
|
|
42
|
+
# `unknown column 'employee_no'`(/system/departments 依赖 /system/users 的选人列表,
|
|
43
|
+
# 曾因此在打开部门页时整页报错)。
|
|
44
|
+
employee_no: {type: text}
|
|
40
45
|
# 外部源侧的状态快照(企微 1/2/4/5)。**绝不**自动改写 is_forbidden——那会和
|
|
41
46
|
# 本站管理员的手动启停互相覆盖(方案 §4.3)
|
|
42
47
|
external_status: {type: integer}
|
|
@@ -95,6 +100,8 @@ tables:
|
|
|
95
100
|
name: {type: text, null: false}
|
|
96
101
|
module: {type: text, null: false}
|
|
97
102
|
title: {type: text}
|
|
103
|
+
# 原始 i18n key(R2,0018):title = 安装期解析文本,本列存原文供 UI 实时翻译(sys_permission 先例)
|
|
104
|
+
title_key: {type: text}
|
|
98
105
|
description: {type: text}
|
|
99
106
|
builtin: {type: integer, null: false}
|
|
100
107
|
deprecated: {type: integer, null: false}
|
|
@@ -43,7 +43,13 @@ export function runtimeReplayGate(): ReplayGate {
|
|
|
43
43
|
incr: key => kv.incr(key),
|
|
44
44
|
expire: (key, ttlSecs) => kv.expire(key, ttlSecs),
|
|
45
45
|
get: key => kv.get(key),
|
|
46
|
-
set: async (key, ttlSecs) => {
|
|
46
|
+
set: async (key, ttlSecs) => {
|
|
47
|
+
await kv.set(key, "1");
|
|
48
|
+
if (!(await kv.expire(key, ttlSecs))) {
|
|
49
|
+
log.warn("wecom applied marker ttl not set", "key", key);
|
|
50
|
+
await kv.del(key);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
47
53
|
del: key => kv.del(key),
|
|
48
54
|
now: () => Math.floor(Date.now() / 1000),
|
|
49
55
|
};
|
|
@@ -61,7 +67,7 @@ export class ReplayAlreadyApplied extends Error {
|
|
|
61
67
|
|
|
62
68
|
/** 处理成功后调用:标记该 nonce 已落库成功,供重推幂等回 success(避免首次偏慢被企微判超时、重推又被重放守卫挡成 400)。 */
|
|
63
69
|
export async function markApplied(nonce: string, gate: ReplayGate): Promise<void> {
|
|
64
|
-
await gate.set(
|
|
70
|
+
await gate.set(`wecom:applied:${nonce}`, NONCE_TTL_SECS);
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
/**
|
|
@@ -85,7 +91,7 @@ export async function guardReplay(timestampRaw: string, nonce: string, gate: Rep
|
|
|
85
91
|
if (seen !== 1) {
|
|
86
92
|
// 重推场景:若首次处理已成功落库(标了 applied 标记),直接幂等回 success,
|
|
87
93
|
// 不让企微卡在「应用安装失败」。未标记者说明首推仍在进行或已失败(失败会 release 让重推重放),维持 400。
|
|
88
|
-
const applied = await gate.get(
|
|
94
|
+
const applied = await gate.get(`wecom:applied:${nonce}`);
|
|
89
95
|
if (applied)
|
|
90
96
|
throw new ReplayAlreadyApplied();
|
|
91
97
|
fail(400, "回调 nonce 重复:同一条指令回调已被处理过(重放请求已拒绝)");
|
|
@@ -61,15 +61,12 @@ function randomPassword(): string {
|
|
|
61
61
|
|
|
62
62
|
/** 跨租户读:指定租户下的管理员行(asSystem 例外,见文件头) */
|
|
63
63
|
async function adminRow(tenantId: string): Promise<{ id: number, must_change: number } | null> {
|
|
64
|
-
const rows = await db.asSystem().table("users")
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
],
|
|
71
|
-
})
|
|
72
|
-
.all() as Array<{ id: number, must_change: number | null }>;
|
|
64
|
+
const rows = await db.asSystem().table("users").select(["id", "must_change"]).where({
|
|
65
|
+
and: [
|
|
66
|
+
{ field: "tenant_id", op: "eq", value: tenantId },
|
|
67
|
+
{ field: "username", op: "eq", value: TENANT_ADMIN_USERNAME },
|
|
68
|
+
],
|
|
69
|
+
}).all() as Array<{ id: number, must_change: number | null }>;
|
|
73
70
|
if (!rows.length)
|
|
74
71
|
return null;
|
|
75
72
|
return { id: Number(rows[0]!.id), must_change: Number(rows[0]!.must_change ?? 0) };
|
|
@@ -77,15 +74,12 @@ async function adminRow(tenantId: string): Promise<{ id: number, must_change: nu
|
|
|
77
74
|
|
|
78
75
|
/** 跨租户读:该用户在本租户是否握着 admin 角色(账号存在 ≠ 有角色,可能被收回过) */
|
|
79
76
|
async function hasAdminRole(tenantId: string, uid: number): Promise<boolean> {
|
|
80
|
-
const roles = await db.asSystem().table("sys_user_role")
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
],
|
|
87
|
-
})
|
|
88
|
-
.all() as Array<{ role_name: string }>;
|
|
77
|
+
const roles = await db.asSystem().table("sys_user_role").select(["role_name"]).where({
|
|
78
|
+
and: [
|
|
79
|
+
{ field: "tenant_id", op: "eq", value: tenantId },
|
|
80
|
+
{ field: "user_id", op: "eq", value: uid },
|
|
81
|
+
],
|
|
82
|
+
}).all() as Array<{ role_name: string }>;
|
|
89
83
|
return roles.some(r => String(r.role_name) === TENANT_ADMIN_ROLE);
|
|
90
84
|
}
|
|
91
85
|
|
|
@@ -176,15 +170,12 @@ export async function setTenantAdminPassword(tenantId: string, password: string)
|
|
|
176
170
|
if (!row)
|
|
177
171
|
throw Object.assign(new Error("租户管理员建档失败"), { status: 500 });
|
|
178
172
|
|
|
179
|
-
await db.asSystem().table("users")
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
],
|
|
186
|
-
})
|
|
187
|
-
.run();
|
|
173
|
+
await db.asSystem().table("users").update({ password_hash: await bcrypt.hash(password), must_change: 1 }).where({
|
|
174
|
+
and: [
|
|
175
|
+
{ field: "tenant_id", op: "eq", value: tenantId },
|
|
176
|
+
{ field: "username", op: "eq", value: TENANT_ADMIN_USERNAME },
|
|
177
|
+
],
|
|
178
|
+
}).run();
|
|
188
179
|
|
|
189
180
|
// 改密后旧会话必须失效。跨租户删 refresh 走带 tenant_id 条件的裸 SQL
|
|
190
181
|
// (先例见 store.ts::revokeTenantSessions:守卫是 check-on-miss,带了租户条件即放行)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { respond } from "#/_platform/_shared/respond";
|
|
2
|
-
// GET /api/system/oplog?action=&username=&module=&limit= —— 操作日志查询(AC1-29;D13 admin 可查)。
|
|
2
|
+
// GET /api/system/oplog?action=&username=&module=&page=&limit= —— 操作日志查询(AC1-29;D13 admin 可查)。
|
|
3
3
|
// POST /api/system/oplog —— 操作日志写入(C3;登录态即可,解决方案模块记录自身操作)。
|
|
4
4
|
import { assertSystemAdmin, currentAccess } from "#/_platform/_shared/service/infra/access";
|
|
5
5
|
import { recordOp } from "#/_platform/_shared/service/infra/audit";
|
|
@@ -16,10 +16,12 @@ get.schema = {
|
|
|
16
16
|
type: "object",
|
|
17
17
|
properties: {
|
|
18
18
|
action: { type: "string" },
|
|
19
|
-
// username
|
|
19
|
+
// username 包含匹配(子串,service 走 contains),非精确
|
|
20
20
|
username: { type: "string" },
|
|
21
21
|
module: { type: "string" },
|
|
22
|
-
//
|
|
22
|
+
// 页码(1 起);service 的 pageArgs 非法回落 1
|
|
23
|
+
page: { type: "number", minimum: 1 },
|
|
24
|
+
// 每页条数 1~200(schema 兜住;service 的 pageArgs 再钳一次)
|
|
23
25
|
limit: { type: "number", minimum: 1, maximum: 200 },
|
|
24
26
|
},
|
|
25
27
|
},
|
|
@@ -19,11 +19,12 @@ get.response = {
|
|
|
19
19
|
type: "array",
|
|
20
20
|
items: {
|
|
21
21
|
type: "object",
|
|
22
|
-
required: ["name", "module", "title", "description", "builtin", "deprecated", "holders", "permissions"],
|
|
22
|
+
required: ["name", "module", "title", "title_key", "description", "builtin", "deprecated", "holders", "permissions"],
|
|
23
23
|
properties: {
|
|
24
24
|
name: { type: "string" },
|
|
25
25
|
module: { type: "string" },
|
|
26
26
|
title: { type: ["string", "null"] },
|
|
27
|
+
title_key: { type: ["string", "null"] },
|
|
27
28
|
description: { type: ["string", "null"] },
|
|
28
29
|
builtin: { type: "boolean" },
|
|
29
30
|
deprecated: { type: "boolean" },
|
|
@@ -17,6 +17,9 @@ export const API_PREFIX = "/_platform";
|
|
|
17
17
|
/** 认领身份前缀(契约 apiPrefix,字面等于模块目录名):跨模块认领护栏对比基准 */
|
|
18
18
|
export const MODULE_PREFIX = "/_platform";
|
|
19
19
|
|
|
20
|
+
/** 认领方模块(web.config clientOwners 指定):跨模块认领护栏对比基准 */
|
|
21
|
+
export const MODULE_OWNER = "/app";
|
|
22
|
+
|
|
20
23
|
/**
|
|
21
24
|
* 模块 entry 的 onInit 里一行创建(构造即 install):完成 apiPrefix 登记 +
|
|
22
25
|
* scoped request 绑定,返回绑好前缀的端点函数集合。一模块一 client;
|
|
@@ -28,8 +31,8 @@ export function create_platformClient(ctx: ModuleContext) {
|
|
|
28
31
|
// 单槽、卸载互相耦合;跨模块需求应由本模块创建后经 provider 暴露。
|
|
29
32
|
// 注意对比基准是 MODULE_PREFIX(目录名)而非 API_PREFIX——D-M8 映射时线上前缀
|
|
30
33
|
// 属于别的命名空间,认领身份仍按目录名。
|
|
31
|
-
if (import.meta.env.DEV && ctx.module.name !==
|
|
32
|
-
console.warn(`[ojm-api] 模块 "${ctx.module.name}" 正在认领 ${MODULE_PREFIX} 的 client——跨模块认领会共享 request 单槽、卸载互相耦合;应由 "${
|
|
34
|
+
if (import.meta.env.DEV && ctx.module.name !== MODULE_OWNER.slice(1)) {
|
|
35
|
+
console.warn(`[ojm-api] 模块 "${ctx.module.name}" 正在认领 ${MODULE_PREFIX} 的 client——跨模块认领会共享 request 单槽、卸载互相耦合;应由 "${MODULE_OWNER.slice(1)}" 模块在自身 onInit 创建,跨模块需求经 provider 暴露。`);
|
|
33
36
|
}
|
|
34
37
|
ctx.register.apiPrefix(API_PREFIX);
|
|
35
38
|
bindRequest(ctx.utils.request);
|
|
@@ -17,6 +17,9 @@ export const API_PREFIX = "/auth";
|
|
|
17
17
|
/** 认领身份前缀(契约 apiPrefix,字面等于模块目录名):跨模块认领护栏对比基准 */
|
|
18
18
|
export const MODULE_PREFIX = "/auth";
|
|
19
19
|
|
|
20
|
+
/** 认领方模块(web.config clientOwners 指定):跨模块认领护栏对比基准 */
|
|
21
|
+
export const MODULE_OWNER = "/app";
|
|
22
|
+
|
|
20
23
|
/**
|
|
21
24
|
* 模块 entry 的 onInit 里一行创建(构造即 install):完成 apiPrefix 登记 +
|
|
22
25
|
* scoped request 绑定,返回绑好前缀的端点函数集合。一模块一 client;
|
|
@@ -28,8 +31,8 @@ export function createAuthClient(ctx: ModuleContext) {
|
|
|
28
31
|
// 单槽、卸载互相耦合;跨模块需求应由本模块创建后经 provider 暴露。
|
|
29
32
|
// 注意对比基准是 MODULE_PREFIX(目录名)而非 API_PREFIX——D-M8 映射时线上前缀
|
|
30
33
|
// 属于别的命名空间,认领身份仍按目录名。
|
|
31
|
-
if (import.meta.env.DEV && ctx.module.name !==
|
|
32
|
-
console.warn(`[ojm-api] 模块 "${ctx.module.name}" 正在认领 ${MODULE_PREFIX} 的 client——跨模块认领会共享 request 单槽、卸载互相耦合;应由 "${
|
|
34
|
+
if (import.meta.env.DEV && ctx.module.name !== MODULE_OWNER.slice(1)) {
|
|
35
|
+
console.warn(`[ojm-api] 模块 "${ctx.module.name}" 正在认领 ${MODULE_PREFIX} 的 client——跨模块认领会共享 request 单槽、卸载互相耦合;应由 "${MODULE_OWNER.slice(1)}" 模块在自身 onInit 创建,跨模块需求经 provider 暴露。`);
|
|
33
36
|
}
|
|
34
37
|
ctx.register.apiPrefix(API_PREFIX);
|
|
35
38
|
bindRequest(ctx.utils.request);
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
notificationNotificationsGet,
|
|
12
12
|
notificationNotificationsReadAllPost,
|
|
13
13
|
notificationNotificationsReadPost,
|
|
14
|
-
} from "../../__client
|
|
14
|
+
} from "../../__client";
|
|
15
15
|
|
|
16
16
|
type Item = Awaited<ReturnType<typeof notificationNotificationsGet>>[number];
|
|
17
17
|
type CatFilter = "all" | NoticeCategory;
|
|
@@ -4,7 +4,7 @@ import { BellOutlined } from "@ant-design/icons";
|
|
|
4
4
|
|
|
5
5
|
import { createElement, lazy } from "react";
|
|
6
6
|
|
|
7
|
-
import { createNotificationClient } from "./__client
|
|
7
|
+
import { createNotificationClient } from "./__client";
|
|
8
8
|
|
|
9
9
|
const NotificationCenter = lazy(() => import("./pages/center"));
|
|
10
10
|
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
import { BasicContent, FormAvatarItem, useUserStore } from "@oj-module/runtime";
|
|
8
8
|
import { App, Form, Input } from "antd";
|
|
9
9
|
|
|
10
|
-
import { personalCenterProfilePost } from "../../__client
|
|
10
|
+
import { personalCenterProfilePost } from "../../__client";
|
|
11
11
|
|
|
12
12
|
export default function Profile() {
|
|
13
13
|
const currentUser = useUserStore();
|
|
@@ -3,7 +3,7 @@ import { ProfileCardIcon, RiAccountCircleLine, useAuthStore } from "@oj-module/r
|
|
|
3
3
|
|
|
4
4
|
import { createElement, lazy } from "react";
|
|
5
5
|
|
|
6
|
-
import { createPersonalCenterClient } from "./__client
|
|
6
|
+
import { createPersonalCenterClient } from "./__client";
|
|
7
7
|
|
|
8
8
|
const MyProfile = lazy(() => import("./pages/my-profile"));
|
|
9
9
|
|
|
@@ -130,6 +130,7 @@ export const schemas = {
|
|
|
130
130
|
"action": z.string().optional(),
|
|
131
131
|
"username": z.string().optional(),
|
|
132
132
|
"module": z.string().optional(),
|
|
133
|
+
"page": z.number().min(1).optional(),
|
|
133
134
|
"limit": z.number().min(1).max(200).optional()
|
|
134
135
|
}),
|
|
135
136
|
data: z.object({
|
|
@@ -174,6 +175,7 @@ export const schemas = {
|
|
|
174
175
|
"name": z.string(),
|
|
175
176
|
"module": z.string(),
|
|
176
177
|
"title": z.string().nullable(),
|
|
178
|
+
"title_key": z.string().nullable(),
|
|
177
179
|
"description": z.string().nullable(),
|
|
178
180
|
"builtin": z.boolean(),
|
|
179
181
|
"deprecated": z.boolean(),
|
|
@@ -99,10 +99,9 @@
|
|
|
99
99
|
},
|
|
100
100
|
"oplog": {
|
|
101
101
|
"filterAction": "Action",
|
|
102
|
-
"filterUsername": "Username (
|
|
102
|
+
"filterUsername": "Username (contains)",
|
|
103
103
|
"filterModule": "Module",
|
|
104
|
-
"
|
|
105
|
-
"truncated": "Results truncated by limit; narrow your filters",
|
|
104
|
+
"total": "{{total}} total",
|
|
106
105
|
"columns": {
|
|
107
106
|
"time": "Time",
|
|
108
107
|
"username": "Username",
|
|
@@ -99,10 +99,9 @@
|
|
|
99
99
|
},
|
|
100
100
|
"oplog": {
|
|
101
101
|
"filterAction": "操作 (action)",
|
|
102
|
-
"filterUsername": "
|
|
102
|
+
"filterUsername": "用户名(包含匹配)",
|
|
103
103
|
"filterModule": "模块",
|
|
104
|
-
"
|
|
105
|
-
"truncated": "结果已按上限截断,请缩小过滤范围",
|
|
104
|
+
"total": "共 {{total}} 条",
|
|
106
105
|
"columns": {
|
|
107
106
|
"time": "时间",
|
|
108
107
|
"username": "用户名",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SystemDepartmentsGetData, SystemDepartmentsMembersGetData } from "../../__client
|
|
1
|
+
import type { SystemDepartmentsGetData, SystemDepartmentsMembersGetData } from "../../__client";
|
|
2
2
|
|
|
3
3
|
import { PlusOutlined } from "@ant-design/icons";
|
|
4
4
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
systemDepartmentsPost,
|
|
33
33
|
systemDepartmentsPut,
|
|
34
34
|
systemUsersGet,
|
|
35
|
-
} from "../../__client
|
|
35
|
+
} from "../../__client";
|
|
36
36
|
import { explainError, isForbidden } from "../errors";
|
|
37
37
|
import { ForbiddenPanel } from "../forbidden";
|
|
38
38
|
import { WecomSyncBar } from "./sync-bar";
|
|
@@ -2,7 +2,7 @@ import { useQuery } from "@tanstack/react-query";
|
|
|
2
2
|
import { Card, Descriptions, Tag } from "antd";
|
|
3
3
|
import { useTranslation } from "react-i18next";
|
|
4
4
|
|
|
5
|
-
import { systemLoginMethodsGet } from "../../__client
|
|
5
|
+
import { systemLoginMethodsGet } from "../../__client";
|
|
6
6
|
|
|
7
7
|
const METHOD_KEY: Record<string, string> = {
|
|
8
8
|
local: "system:loginMethodsPage.methodLocal",
|