Compare commits
No commits in common. "ce2aa35a24ab351433ae7bb98aba2cab38c7e676" and "c680fb9dc7b5b39c03e029f72ffc2776d2c731d5" have entirely different histories.
ce2aa35a24
...
c680fb9dc7
25
.env.example
25
.env.example
@ -1,25 +0,0 @@
|
||||
# 复制为 .env 并按需修改
|
||||
|
||||
# 数据库(MySQL 8)
|
||||
DATABASE_URL="mysql://root:Mysql.123@127.0.0.1:3306/sysmgr"
|
||||
|
||||
# 服务端口
|
||||
PORT=3000
|
||||
|
||||
# JWT(生产务必替换 secret)
|
||||
JWT_SECRET="dev-secret-change-me"
|
||||
JWT_ACCESS_TTL="7200"
|
||||
JWT_REFRESH_TTL="604800"
|
||||
|
||||
# 日志(详见 src/logging/logger.config.ts)
|
||||
LOG_LEVEL="info"
|
||||
LOG_PRETTY="true"
|
||||
LOG_FILE_ENABLED="false"
|
||||
LOG_FILE_PATH="logs/app.log"
|
||||
|
||||
# Redis(权限缓存;多 fork 项目共用一个实例时用 REDIS_KEY_PREFIX 隔离)
|
||||
REDIS_HOST="127.0.0.1"
|
||||
REDIS_PORT="6379"
|
||||
REDIS_DB="0"
|
||||
REDIS_PASSWORD=""
|
||||
REDIS_KEY_PREFIX="sysmgr:"
|
||||
@ -7,9 +7,6 @@
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"type": "module",
|
||||
"prisma": {
|
||||
"seed": "tsx prisma/seed.ts"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.23.1"
|
||||
},
|
||||
@ -26,8 +23,7 @@
|
||||
"test:watch": "vitest",
|
||||
"test:cov": "vitest run --coverage",
|
||||
"test:debug": "vitest --inspect-brk --no-file-parallelism",
|
||||
"test:e2e": "vitest run --config ./vitest.config.e2e.ts",
|
||||
"seed": "prisma db seed"
|
||||
"test:e2e": "vitest run --config ./vitest.config.e2e.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^12.0.1",
|
||||
@ -39,7 +35,6 @@
|
||||
"bcryptjs": "^3.0.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.15.1",
|
||||
"ioredis": "^6.0.0",
|
||||
"nestjs-pino": "^5.3.1",
|
||||
"pino": "^10.4.0",
|
||||
"pino-pretty": "^13.2.0",
|
||||
@ -64,7 +59,6 @@
|
||||
"prisma": "^6.19.3",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^7.0.0",
|
||||
"tsx": "^4.23.15",
|
||||
"typescript": "^6.0.2",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^4.1.2"
|
||||
|
||||
84
pnpm-lock.yaml
generated
84
pnpm-lock.yaml
generated
@ -193,9 +193,6 @@ importers:
|
||||
class-validator:
|
||||
specifier: ^0.15.1
|
||||
version: 0.15.1
|
||||
ioredis:
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0(supports-color@7.2.0)
|
||||
nestjs-pino:
|
||||
specifier: ^5.3.1
|
||||
version: 5.3.1(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)(pino-http@11.0.0)(pino@10.4.0)(rxjs@7.8.2)
|
||||
@ -263,18 +260,15 @@ importers:
|
||||
supertest:
|
||||
specifier: ^7.0.0
|
||||
version: 7.3.1(supports-color@7.2.0)
|
||||
tsx:
|
||||
specifier: ^4.23.15
|
||||
version: 4.23.15
|
||||
typescript:
|
||||
specifier: ^6.0.2
|
||||
version: 6.0.3
|
||||
vite-tsconfig-paths:
|
||||
specifier: ^5.1.4
|
||||
version: 5.1.4(supports-color@7.2.0)(typescript@6.0.3)(vite@8.3.4(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))
|
||||
version: 5.1.4(supports-color@7.2.0)(typescript@6.0.3)(vite@8.3.4(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.1))
|
||||
vitest:
|
||||
specifier: ^4.1.2
|
||||
version: 4.1.11(@types/node@24.19.1)(@vitest/coverage-v8@4.1.11)(vite@8.3.4(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))
|
||||
version: 4.1.11(@types/node@24.19.1)(@vitest/coverage-v8@4.1.11)(vite@8.3.4(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.1))
|
||||
|
||||
packages:
|
||||
|
||||
@ -614,9 +608,6 @@ packages:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@ioredis/commands@2.0.0':
|
||||
resolution: {integrity: sha512-vrx0AE/T0h7cRZwfo1M39Cr+ZhZrkf0V8mQN75wucKCxCLD9l/VX6no3gFvrLqD1IlG/1LtzWovqEw3t0Vr9zg==}
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2':
|
||||
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
@ -1372,10 +1363,6 @@ packages:
|
||||
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
|
||||
engines: {node: '>=0.8'}
|
||||
|
||||
cluster-key-slot@1.1.1:
|
||||
resolution: {integrity: sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
color-convert@2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
@ -1474,10 +1461,6 @@ packages:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
denque@2.1.0:
|
||||
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
|
||||
engines: {node: '>=0.10'}
|
||||
|
||||
depd@2.0.0:
|
||||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@ -1733,10 +1716,6 @@ packages:
|
||||
resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
ioredis@6.0.0:
|
||||
resolution: {integrity: sha512-f+Dtubxfpf6KYFq7WVXJoOLn0bk4TJrMrN9SzeE+jrWrCWj7XX3fA6vkryafhADX+GMymRxgDJDOI33COkJc0w==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
ipaddr.js@1.9.1:
|
||||
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
||||
engines: {node: '>= 0.10'}
|
||||
@ -2222,10 +2201,6 @@ packages:
|
||||
real-require@1.0.0:
|
||||
resolution: {integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==}
|
||||
|
||||
redis-errors@1.2.0:
|
||||
resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
reflect-metadata@0.2.2:
|
||||
resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
|
||||
|
||||
@ -2344,9 +2319,6 @@ packages:
|
||||
stackback@0.0.2:
|
||||
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
||||
|
||||
standard-as-callback@2.1.0:
|
||||
resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
|
||||
|
||||
statuses@2.0.2:
|
||||
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@ -2459,11 +2431,6 @@ packages:
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
tsx@4.23.15:
|
||||
resolution: {integrity: sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
|
||||
type-fest@0.21.3:
|
||||
resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
|
||||
engines: {node: '>=10'}
|
||||
@ -2880,8 +2847,6 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/node': 24.19.1
|
||||
|
||||
'@ioredis/commands@2.0.0': {}
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2': {}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.6.0': {}
|
||||
@ -3292,7 +3257,7 @@ snapshots:
|
||||
obug: 2.2.1
|
||||
std-env: 4.3.0
|
||||
tinyrainbow: 3.2.0
|
||||
vitest: 4.1.11(@types/node@24.19.1)(@vitest/coverage-v8@4.1.11)(vite@8.3.4(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))
|
||||
vitest: 4.1.11(@types/node@24.19.1)(@vitest/coverage-v8@4.1.11)(vite@8.3.4(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.1))
|
||||
|
||||
'@vitest/expect@4.1.11':
|
||||
dependencies:
|
||||
@ -3303,13 +3268,13 @@ snapshots:
|
||||
chai: 6.3.0
|
||||
tinyrainbow: 3.2.0
|
||||
|
||||
'@vitest/mocker@4.1.11(vite@8.3.4(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))':
|
||||
'@vitest/mocker@4.1.11(vite@8.3.4(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.1))':
|
||||
dependencies:
|
||||
'@vitest/spy': 4.1.11
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 8.3.4(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)
|
||||
vite: 8.3.4(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.1)
|
||||
|
||||
'@vitest/pretty-format@4.1.11':
|
||||
dependencies:
|
||||
@ -3520,8 +3485,6 @@ snapshots:
|
||||
|
||||
clone@1.0.4: {}
|
||||
|
||||
cluster-key-slot@1.1.1: {}
|
||||
|
||||
color-convert@2.0.1:
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
@ -3590,8 +3553,6 @@ snapshots:
|
||||
|
||||
delayed-stream@1.0.0: {}
|
||||
|
||||
denque@2.1.0: {}
|
||||
|
||||
depd@2.0.0: {}
|
||||
|
||||
destr@2.0.5: {}
|
||||
@ -3683,6 +3644,7 @@ snapshots:
|
||||
'@esbuild/win32-arm64': 0.28.2
|
||||
'@esbuild/win32-ia32': 0.28.2
|
||||
'@esbuild/win32-x64': 0.28.2
|
||||
optional: true
|
||||
|
||||
escape-html@1.0.3: {}
|
||||
|
||||
@ -3901,17 +3863,6 @@ snapshots:
|
||||
through: 2.3.8
|
||||
wrap-ansi: 6.2.0
|
||||
|
||||
ioredis@6.0.0(supports-color@7.2.0):
|
||||
dependencies:
|
||||
'@ioredis/commands': 2.0.0
|
||||
cluster-key-slot: 1.1.1
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
denque: 2.1.0
|
||||
redis-errors: 1.2.0
|
||||
standard-as-callback: 2.1.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
ipaddr.js@1.9.1: {}
|
||||
|
||||
is-fullwidth-code-point@3.0.0: {}
|
||||
@ -4375,8 +4326,6 @@ snapshots:
|
||||
|
||||
real-require@1.0.0: {}
|
||||
|
||||
redis-errors@1.2.0: {}
|
||||
|
||||
reflect-metadata@0.2.2: {}
|
||||
|
||||
require-from-string@2.0.2: {}
|
||||
@ -4526,8 +4475,6 @@ snapshots:
|
||||
|
||||
stackback@0.0.2: {}
|
||||
|
||||
standard-as-callback@2.1.0: {}
|
||||
|
||||
statuses@2.0.2: {}
|
||||
|
||||
std-env@4.3.0: {}
|
||||
@ -4638,12 +4585,6 @@ snapshots:
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
tsx@4.23.15:
|
||||
dependencies:
|
||||
esbuild: 0.28.2
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
type-fest@0.21.3: {}
|
||||
|
||||
type-is@1.6.18:
|
||||
@ -4679,18 +4620,18 @@ snapshots:
|
||||
|
||||
vary@1.1.2: {}
|
||||
|
||||
vite-tsconfig-paths@5.1.4(supports-color@7.2.0)(typescript@6.0.3)(vite@8.3.4(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)):
|
||||
vite-tsconfig-paths@5.1.4(supports-color@7.2.0)(typescript@6.0.3)(vite@8.3.4(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.1)):
|
||||
dependencies:
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
globrex: 0.1.2
|
||||
tsconfck: 3.1.6(typescript@6.0.3)
|
||||
optionalDependencies:
|
||||
vite: 8.3.4(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)
|
||||
vite: 8.3.4(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
vite@8.3.4(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1):
|
||||
vite@8.3.4(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.1):
|
||||
dependencies:
|
||||
lightningcss: 1.33.0
|
||||
picomatch: 4.0.7
|
||||
@ -4702,13 +4643,12 @@ snapshots:
|
||||
esbuild: 0.28.2
|
||||
fsevents: 2.3.3
|
||||
jiti: 2.7.0
|
||||
tsx: 4.23.15
|
||||
yaml: 2.9.1
|
||||
|
||||
vitest@4.1.11(@types/node@24.19.1)(@vitest/coverage-v8@4.1.11)(vite@8.3.4(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)):
|
||||
vitest@4.1.11(@types/node@24.19.1)(@vitest/coverage-v8@4.1.11)(vite@8.3.4(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.1)):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.1.11
|
||||
'@vitest/mocker': 4.1.11(vite@8.3.4(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))
|
||||
'@vitest/mocker': 4.1.11(vite@8.3.4(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.1))
|
||||
'@vitest/pretty-format': 4.1.11
|
||||
'@vitest/runner': 4.1.11
|
||||
'@vitest/snapshot': 4.1.11
|
||||
@ -4725,7 +4665,7 @@ snapshots:
|
||||
tinyexec: 1.3.1
|
||||
tinyglobby: 0.2.17
|
||||
tinyrainbow: 3.2.0
|
||||
vite: 8.3.4(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)
|
||||
vite: 8.3.4(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.1)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 24.19.1
|
||||
|
||||
181
prisma/seed.ts
181
prisma/seed.ts
@ -1,181 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { Permissions } from '../src/common/permissions.js';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
const TENANT = 'touyun';
|
||||
const SUPER_ROLE = 'super_admin';
|
||||
const ADMIN_USERNAME = 'touyun';
|
||||
const ADMIN_PASSWORD = 'Touyun@123';
|
||||
|
||||
/** 菜单资源(导航)。 */
|
||||
const MENUS = [
|
||||
{ code: 'dashboard', name: '工作台', parent: null, path: '/dashboard', component: 'dashboard/index', sort: 1 },
|
||||
{ code: 'system', name: '系统管理', parent: null, path: '/system', component: 'Layout', sort: 2 },
|
||||
{ code: 'tenant', name: '租户管理', parent: 'system', path: '/system/tenant', component: 'system/tenant/index', sort: 1 },
|
||||
{ code: 'user', name: '用户管理', parent: 'system', path: '/system/user', component: 'system/user/index', sort: 2 },
|
||||
{ code: 'role', name: '角色管理', parent: 'system', path: '/system/role', component: 'system/role/index', sort: 3 },
|
||||
{ code: 'resource', name: '资源管理', parent: 'system', path: '/system/resource', component: 'system/resource/index', sort: 4 },
|
||||
{ code: 'org', name: '组织管理', parent: 'system', path: '/system/org', component: 'system/org/index', sort: 5 },
|
||||
{ code: 'dict', name: '字典管理', parent: 'system', path: '/system/dict', component: 'system/dict/index', sort: 6 },
|
||||
{ code: 'config', name: '参数配置', parent: 'system', path: '/system/config', component: 'system/config/index', sort: 7 },
|
||||
{ code: 'monitor', name: '系统监控', parent: null, path: '/monitor', component: 'Layout', sort: 3 },
|
||||
{ code: 'audit', name: '操作日志', parent: 'monitor', path: '/monitor/audit', component: 'monitor/audit/index', sort: 1 },
|
||||
{ code: 'login-log', name: '登录日志', parent: 'monitor', path: '/monitor/login-log', component: 'monitor/login-log/index', sort: 2 },
|
||||
];
|
||||
|
||||
/** 接口资源(每个受控接口一个,code = 权限码)。 */
|
||||
const APIS: { code: string; name: string; method: string; path: string; parent: string }[] = [
|
||||
{ code: Permissions.tenant.list, name: '租户列表', method: 'GET', path: '/api/v1/tenants', parent: 'tenant' },
|
||||
{ code: Permissions.tenant.create, name: '创建租户', method: 'POST', path: '/api/v1/tenants', parent: 'tenant' },
|
||||
{ code: Permissions.tenant.update, name: '更新租户', method: 'PATCH', path: '/api/v1/tenants/:id', parent: 'tenant' },
|
||||
{ code: Permissions.tenant.delete, name: '删除租户', method: 'DELETE', path: '/api/v1/tenants/:id', parent: 'tenant' },
|
||||
{ code: Permissions.user.list, name: '用户列表', method: 'GET', path: '/api/v1/users', parent: 'user' },
|
||||
{ code: Permissions.user.create, name: '创建用户', method: 'POST', path: '/api/v1/users', parent: 'user' },
|
||||
{ code: Permissions.user.update, name: '更新用户', method: 'PATCH', path: '/api/v1/users/:id', parent: 'user' },
|
||||
{ code: Permissions.user.delete, name: '删除用户', method: 'DELETE', path: '/api/v1/users/:id', parent: 'user' },
|
||||
{ code: Permissions.user.resetPwd, name: '重置密码', method: 'PATCH', path: '/api/v1/users/:id/password', parent: 'user' },
|
||||
{ code: Permissions.role.list, name: '角色列表', method: 'GET', path: '/api/v1/roles', parent: 'role' },
|
||||
{ code: Permissions.role.create, name: '创建角色', method: 'POST', path: '/api/v1/roles', parent: 'role' },
|
||||
{ code: Permissions.role.update, name: '更新角色', method: 'PATCH', path: '/api/v1/roles/:id', parent: 'role' },
|
||||
{ code: Permissions.role.delete, name: '删除角色', method: 'DELETE', path: '/api/v1/roles/:id', parent: 'role' },
|
||||
{ code: Permissions.role.assignResource, name: '分配资源', method: 'PUT', path: '/api/v1/roles/:id/resources', parent: 'role' },
|
||||
{ code: Permissions.resource.list, name: '资源列表', method: 'GET', path: '/api/v1/resources', parent: 'resource' },
|
||||
{ code: Permissions.resource.create, name: '创建资源', method: 'POST', path: '/api/v1/resources', parent: 'resource' },
|
||||
{ code: Permissions.resource.update, name: '更新资源', method: 'PATCH', path: '/api/v1/resources/:id', parent: 'resource' },
|
||||
{ code: Permissions.resource.delete, name: '删除资源', method: 'DELETE', path: '/api/v1/resources/:id', parent: 'resource' },
|
||||
{ code: Permissions.org.list, name: '组织列表', method: 'GET', path: '/api/v1/orgs', parent: 'org' },
|
||||
{ code: Permissions.org.create, name: '创建组织', method: 'POST', path: '/api/v1/orgs', parent: 'org' },
|
||||
{ code: Permissions.org.update, name: '更新组织', method: 'PATCH', path: '/api/v1/orgs/:id', parent: 'org' },
|
||||
{ code: Permissions.org.delete, name: '删除组织', method: 'DELETE', path: '/api/v1/orgs/:id', parent: 'org' },
|
||||
{ code: Permissions.dict.list, name: '字典列表', method: 'GET', path: '/api/v1/dict-types', parent: 'dict' },
|
||||
{ code: Permissions.dict.create, name: '创建字典', method: 'POST', path: '/api/v1/dict-types', parent: 'dict' },
|
||||
{ code: Permissions.dict.update, name: '更新字典', method: 'PATCH', path: '/api/v1/dict-types/:id', parent: 'dict' },
|
||||
{ code: Permissions.dict.delete, name: '删除字典', method: 'DELETE', path: '/api/v1/dict-types/:id', parent: 'dict' },
|
||||
{ code: Permissions.config.list, name: '配置列表', method: 'GET', path: '/api/v1/configs', parent: 'config' },
|
||||
{ code: Permissions.config.create, name: '创建配置', method: 'POST', path: '/api/v1/configs', parent: 'config' },
|
||||
{ code: Permissions.config.update, name: '更新配置', method: 'PATCH', path: '/api/v1/configs/:id', parent: 'config' },
|
||||
{ code: Permissions.config.delete, name: '删除配置', method: 'DELETE', path: '/api/v1/configs/:id', parent: 'config' },
|
||||
{ code: Permissions.audit.list, name: '操作日志列表', method: 'GET', path: '/api/v1/audit-logs', parent: 'audit' },
|
||||
{ code: Permissions.loginLog.list, name: '登录日志列表', method: 'GET', path: '/api/v1/login-logs', parent: 'login-log' },
|
||||
];
|
||||
|
||||
/** 按钮资源(纯前端按钮,无独立受控接口)。 */
|
||||
const BUTTONS = [
|
||||
{ code: 'user:export', name: '用户导出', parent: 'user' },
|
||||
{ code: 'role:export', name: '角色导出', parent: 'role' },
|
||||
{ code: 'audit:export', name: '操作日志导出', parent: 'audit' },
|
||||
{ code: 'login-log:export', name: '登录日志导出', parent: 'login-log' },
|
||||
];
|
||||
|
||||
async function main(): Promise<void> {
|
||||
// 1. 租户
|
||||
await prisma.tenant.upsert({
|
||||
where: { code: TENANT },
|
||||
update: { name: '投云', status: 'enabled' },
|
||||
create: { code: TENANT, name: '投云', status: 'enabled' },
|
||||
});
|
||||
|
||||
// 2. 根组织
|
||||
await prisma.org.upsert({
|
||||
where: { tenantCode_code: { tenantCode: TENANT, code: TENANT } },
|
||||
update: { name: '投云总部' },
|
||||
create: { tenantCode: TENANT, code: TENANT, name: '投云总部', parentCode: null, sort: 0, status: 'enabled' },
|
||||
});
|
||||
|
||||
// 3. 超管角色
|
||||
await prisma.role.upsert({
|
||||
where: { tenantCode_code: { tenantCode: TENANT, code: SUPER_ROLE } },
|
||||
update: { name: '超级管理员', dataScope: 'all', status: 'enabled' },
|
||||
create: { tenantCode: TENANT, code: SUPER_ROLE, name: '超级管理员', dataScope: 'all', status: 'enabled' },
|
||||
});
|
||||
|
||||
// 4. 管理员用户
|
||||
const passwordHash = await bcrypt.hash(ADMIN_PASSWORD, 10);
|
||||
const user = await prisma.user.upsert({
|
||||
where: { tenantCode_username: { tenantCode: TENANT, username: ADMIN_USERNAME } },
|
||||
update: { passwordHash, nickname: '管理员', status: 'enabled' },
|
||||
create: {
|
||||
tenantCode: TENANT,
|
||||
username: ADMIN_USERNAME,
|
||||
passwordHash,
|
||||
nickname: '管理员',
|
||||
status: 'enabled',
|
||||
orgCode: TENANT,
|
||||
},
|
||||
});
|
||||
|
||||
// 5. 绑定超管角色
|
||||
await prisma.userRole.upsert({
|
||||
where: { tenantCode_userId_roleCode: { tenantCode: TENANT, userId: user.id, roleCode: SUPER_ROLE } },
|
||||
update: {},
|
||||
create: { tenantCode: TENANT, userId: user.id, roleCode: SUPER_ROLE },
|
||||
});
|
||||
|
||||
// 6. 资源树(菜单 + 接口 + 按钮)
|
||||
const allResourceCodes: string[] = [];
|
||||
for (const m of MENUS) {
|
||||
allResourceCodes.push(m.code);
|
||||
await prisma.resource.upsert({
|
||||
where: { tenantCode_code: { tenantCode: TENANT, code: m.code } },
|
||||
update: { name: m.name, parentCode: m.parent, path: m.path, component: m.component, sort: m.sort, type: 'menu' },
|
||||
create: {
|
||||
tenantCode: TENANT,
|
||||
code: m.code,
|
||||
name: m.name,
|
||||
parentCode: m.parent,
|
||||
type: 'menu',
|
||||
path: m.path,
|
||||
component: m.component,
|
||||
sort: m.sort,
|
||||
status: 'enabled',
|
||||
},
|
||||
});
|
||||
}
|
||||
for (const a of APIS) {
|
||||
allResourceCodes.push(a.code);
|
||||
await prisma.resource.upsert({
|
||||
where: { tenantCode_code: { tenantCode: TENANT, code: a.code } },
|
||||
update: { name: a.name, parentCode: a.parent, method: a.method, apiPath: a.path, type: 'api' },
|
||||
create: {
|
||||
tenantCode: TENANT,
|
||||
code: a.code,
|
||||
name: a.name,
|
||||
parentCode: a.parent,
|
||||
type: 'api',
|
||||
method: a.method,
|
||||
apiPath: a.path,
|
||||
status: 'enabled',
|
||||
},
|
||||
});
|
||||
}
|
||||
for (const b of BUTTONS) {
|
||||
allResourceCodes.push(b.code);
|
||||
await prisma.resource.upsert({
|
||||
where: { tenantCode_code: { tenantCode: TENANT, code: b.code } },
|
||||
update: { name: b.name, parentCode: b.parent, type: 'button' },
|
||||
create: { tenantCode: TENANT, code: b.code, name: b.name, parentCode: b.parent, type: 'button', status: 'enabled' },
|
||||
});
|
||||
}
|
||||
|
||||
// 7. 超管全量授权(菜单 + 接口 + 按钮)
|
||||
for (const resourceCode of allResourceCodes) {
|
||||
await prisma.roleResource.upsert({
|
||||
where: { tenantCode_roleCode_resourceCode: { tenantCode: TENANT, roleCode: SUPER_ROLE, resourceCode } },
|
||||
update: {},
|
||||
create: { tenantCode: TENANT, roleCode: SUPER_ROLE, resourceCode },
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`seed 完成:租户 ${TENANT}、用户 ${ADMIN_USERNAME}、角色 ${SUPER_ROLE}、资源 ${allResourceCodes.length} 个(已全量授权)。`);
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
@ -1,7 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
|
||||
import { PrismaModule } from './prisma/prisma.module.js';
|
||||
import { RedisModule } from './common/redis/redis.module.js';
|
||||
import { createLoggerModule } from './logging/logger.config.js';
|
||||
import { AuthModule } from './auth/auth.module.js';
|
||||
import { JwtAuthGuard } from './auth/jwt-auth.guard.js';
|
||||
@ -9,7 +8,6 @@ import { PermissionsGuard } from './auth/permissions.guard.js';
|
||||
import { RequestContextInterceptor } from './auth/request-context.interceptor.js';
|
||||
import { BodyLoggingInterceptor } from './logging/body-logging.interceptor.js';
|
||||
import { TransformInterceptor } from './common/interceptors/transform.interceptor.js';
|
||||
import { AuditLogInterceptor } from './common/interceptors/audit-log.interceptor.js';
|
||||
import { AllExceptionsFilter } from './common/filters/http-exception.filter.js';
|
||||
import { TenantsModule } from './tenants/tenants.module.js';
|
||||
import { UsersModule } from './users/users.module.js';
|
||||
@ -27,7 +25,6 @@ import { AppService } from './app.service.js';
|
||||
imports: [
|
||||
createLoggerModule(),
|
||||
PrismaModule,
|
||||
RedisModule,
|
||||
AuthModule,
|
||||
TenantsModule,
|
||||
UsersModule,
|
||||
@ -47,7 +44,6 @@ import { AppService } from './app.service.js';
|
||||
{ provide: APP_INTERCEPTOR, useClass: RequestContextInterceptor },
|
||||
{ provide: APP_INTERCEPTOR, useClass: BodyLoggingInterceptor },
|
||||
{ provide: APP_INTERCEPTOR, useClass: TransformInterceptor },
|
||||
{ provide: APP_INTERCEPTOR, useClass: AuditLogInterceptor },
|
||||
{ provide: APP_FILTER, useClass: AllExceptionsFilter },
|
||||
],
|
||||
})
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import { Controller, Get, Query } from '@nestjs/common';
|
||||
import { PageQuery } from '../common/page.dto.js';
|
||||
import { Permissions } from '../common/permissions.js';
|
||||
import { RequirePermissions } from '../auth/decorators.js';
|
||||
import { AuditLogsService } from './audit-logs.service.js';
|
||||
|
||||
@Controller('audit-logs')
|
||||
@ -9,7 +7,6 @@ export class AuditLogsController {
|
||||
constructor(private readonly service: AuditLogsService) {}
|
||||
|
||||
@Get()
|
||||
@RequirePermissions(Permissions.audit.list)
|
||||
list(@Query() q: PageQuery) {
|
||||
return this.service.paginate(q.page, q.pageSize);
|
||||
}
|
||||
|
||||
@ -5,6 +5,5 @@ import { AuditLogsService } from './audit-logs.service.js';
|
||||
@Module({
|
||||
controllers: [AuditLogsController],
|
||||
providers: [AuditLogsService],
|
||||
exports: [AuditLogsService],
|
||||
})
|
||||
export class AuditLogsModule {}
|
||||
|
||||
@ -1,40 +1,10 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectPinoLogger, PinoLogger } from 'nestjs-pino';
|
||||
import { BaseCrudService } from '../common/base-crud.service.js';
|
||||
import { PrismaService } from '../prisma/prisma.service.js';
|
||||
|
||||
export interface AuditLogEntry {
|
||||
tenantCode: string;
|
||||
userId?: string | null;
|
||||
username?: string | null;
|
||||
module?: string | null;
|
||||
action?: string | null;
|
||||
method?: string | null;
|
||||
path?: string | null;
|
||||
params?: string | null;
|
||||
result?: string | null;
|
||||
status?: string | null;
|
||||
errorMessage?: string | null;
|
||||
ip?: string | null;
|
||||
userAgent?: string | null;
|
||||
durationMs?: number | null;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AuditLogsService extends BaseCrudService {
|
||||
constructor(
|
||||
prisma: PrismaService,
|
||||
@InjectPinoLogger(AuditLogsService.name) private readonly logger: PinoLogger,
|
||||
) {
|
||||
constructor(prisma: PrismaService) {
|
||||
super(prisma, 'auditLog');
|
||||
}
|
||||
|
||||
/** 写入一条审计日志;失败仅告警,不抛出(异步审计不阻塞业务)。 */
|
||||
async record(data: AuditLogEntry): Promise<void> {
|
||||
try {
|
||||
await this.db().create({ data });
|
||||
} catch (err) {
|
||||
this.logger.warn({ err }, '审计日志写入失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { Body, Controller, Post, Req } from '@nestjs/common';
|
||||
import type { Request } from 'express';
|
||||
import { Body, Controller, Post } from '@nestjs/common';
|
||||
import { AuthService } from './auth.service.js';
|
||||
import { LoginDto, RefreshDto } from './dto.js';
|
||||
import { Public } from './decorators.js';
|
||||
@ -10,8 +9,8 @@ export class AuthController {
|
||||
|
||||
@Public()
|
||||
@Post('login')
|
||||
login(@Body() dto: LoginDto, @Req() req: Request) {
|
||||
return this.auth.login(dto, req.ip, req.headers['user-agent']);
|
||||
login(@Body() dto: LoginDto) {
|
||||
return this.auth.login(dto);
|
||||
}
|
||||
|
||||
@Public()
|
||||
@ -22,7 +21,7 @@ export class AuthController {
|
||||
|
||||
@Public()
|
||||
@Post('logout')
|
||||
logout(@Body() dto: RefreshDto, @Req() req: Request) {
|
||||
return this.auth.logout(dto, req.ip, req.headers['user-agent']);
|
||||
logout(@Body() dto: RefreshDto) {
|
||||
return this.auth.logout(dto);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,8 +2,6 @@ import { Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { AuthController } from './auth.controller.js';
|
||||
import { AuthService } from './auth.service.js';
|
||||
import { AuditLogsModule } from '../audit-logs/audit-logs.module.js';
|
||||
import { LoginLogsModule } from '../login-logs/login-logs.module.js';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -12,8 +10,6 @@ import { LoginLogsModule } from '../login-logs/login-logs.module.js';
|
||||
secret: process.env.JWT_SECRET ?? 'dev-secret-change-me',
|
||||
signOptions: { expiresIn: Number(process.env.JWT_ACCESS_TTL ?? 7200) },
|
||||
}),
|
||||
AuditLogsModule,
|
||||
LoginLogsModule,
|
||||
],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService],
|
||||
|
||||
@ -3,72 +3,34 @@ import { JwtService } from '@nestjs/jwt';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { createHash, randomUUID } from 'node:crypto';
|
||||
import { PrismaService } from '../prisma/prisma.service.js';
|
||||
import { RedisService } from '../common/redis/redis.service.js';
|
||||
import { LoginDto, RefreshDto } from './dto.js';
|
||||
import { resolveDataScope, resolveVisibleOrgs } from '../common/data-scope.js';
|
||||
import type { DataScope } from '../common/enums.js';
|
||||
import type { RequestContext } from '../common/context/request-context.js';
|
||||
import { AuditLogsService } from '../audit-logs/audit-logs.service.js';
|
||||
import { LoginLogsService } from '../login-logs/login-logs.service.js';
|
||||
|
||||
const REFRESH_TTL = Number(process.env.JWT_REFRESH_TTL ?? 604800);
|
||||
const ACCESS_TTL = Number(process.env.JWT_ACCESS_TTL ?? 7200);
|
||||
const SUPER_ROLE = 'super_admin';
|
||||
|
||||
export interface PermissionSet {
|
||||
super: boolean;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly jwt: JwtService,
|
||||
private readonly redis: RedisService,
|
||||
private readonly auditLogs: AuditLogsService,
|
||||
private readonly loginLogs: LoginLogsService,
|
||||
) {}
|
||||
|
||||
async login(
|
||||
dto: LoginDto,
|
||||
ip?: string,
|
||||
userAgent?: string,
|
||||
): Promise<{ accessToken: string; refreshToken: string; expiresIn: number }> {
|
||||
async login(dto: LoginDto): Promise<{ accessToken: string; refreshToken: string; expiresIn: number }> {
|
||||
const tenant = await this.prisma.client.tenant.findUnique({ where: { code: dto.tenantCode } });
|
||||
if (!tenant || tenant.status !== 'enabled') {
|
||||
await this.recordLoginLog(dto.tenantCode, dto.username, 'fail', '租户不存在或已停用', ip, userAgent);
|
||||
throw new UnauthorizedException('租户不存在或已停用');
|
||||
}
|
||||
const user = await this.prisma.client.user.findFirst({
|
||||
where: { tenantCode: dto.tenantCode, username: dto.username, deletedAt: null },
|
||||
});
|
||||
if (!user) {
|
||||
await this.recordLoginLog(dto.tenantCode, dto.username, 'fail', '账号或密码错误', ip, userAgent);
|
||||
throw new UnauthorizedException('账号或密码错误');
|
||||
}
|
||||
if (user.status !== 'enabled') {
|
||||
await this.recordLoginLog(dto.tenantCode, dto.username, 'fail', '账号已停用或锁定', ip, userAgent);
|
||||
throw new UnauthorizedException('账号已停用或锁定');
|
||||
}
|
||||
if (!user) throw new UnauthorizedException('账号或密码错误');
|
||||
if (user.status !== 'enabled') throw new UnauthorizedException('账号已停用或锁定');
|
||||
const ok = await bcrypt.compare(dto.password, user.passwordHash);
|
||||
if (!ok) {
|
||||
await this.recordLoginLog(dto.tenantCode, dto.username, 'fail', '账号或密码错误', ip, userAgent);
|
||||
throw new UnauthorizedException('账号或密码错误');
|
||||
}
|
||||
if (!ok) throw new UnauthorizedException('账号或密码错误');
|
||||
|
||||
await this.writePermissionCache(dto.tenantCode, user.id);
|
||||
await this.recordLoginLog(dto.tenantCode, dto.username, 'success', null, ip, userAgent);
|
||||
await this.recordAudit({
|
||||
tenantCode: dto.tenantCode,
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
module: 'auth',
|
||||
action: 'login',
|
||||
status: 'success',
|
||||
ip: ip ?? null,
|
||||
userAgent: userAgent ?? null,
|
||||
});
|
||||
return this.issue(dto.tenantCode, user.id, user.username);
|
||||
}
|
||||
|
||||
@ -82,44 +44,25 @@ export class AuthService {
|
||||
});
|
||||
if (!user || user.status !== 'enabled') throw new UnauthorizedException('账号已失效');
|
||||
|
||||
// 旋转:作废旧 refresh,并重聚合权限集(权限变更在刷新时生效)
|
||||
// 旋转:作废旧 refresh
|
||||
await this.prisma.client.refreshToken.update({
|
||||
where: { jti: dto.refreshToken },
|
||||
data: { revokedAt: new Date() },
|
||||
});
|
||||
await this.writePermissionCache(record.tenantCode, user.id);
|
||||
return this.issue(record.tenantCode, user.id, user.username);
|
||||
}
|
||||
|
||||
async logout(dto: RefreshDto, ip?: string, userAgent?: string): Promise<null> {
|
||||
async logout(dto: RefreshDto): Promise<null> {
|
||||
const record = await this.prisma.client.refreshToken.findUnique({ where: { jti: dto.refreshToken } });
|
||||
if (record) {
|
||||
await this.prisma.client.refreshToken.update({
|
||||
where: { jti: dto.refreshToken },
|
||||
data: { revokedAt: new Date() },
|
||||
});
|
||||
await this.redis.del(this.permKey(record.tenantCode, record.userId));
|
||||
await this.recordAudit({
|
||||
tenantCode: record.tenantCode,
|
||||
userId: record.userId,
|
||||
username: null,
|
||||
module: 'auth',
|
||||
action: 'logout',
|
||||
status: 'success',
|
||||
ip: ip ?? null,
|
||||
userAgent: userAgent ?? null,
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 读取用户权限集;缓存缺失时重新聚合并回填(Redis 重启自愈)。 */
|
||||
async getPermissionSet(tenantCode: string, userId: string): Promise<PermissionSet> {
|
||||
const cached = await this.redis.getJson<PermissionSet>(this.permKey(tenantCode, userId));
|
||||
if (cached) return cached;
|
||||
return this.writePermissionCache(tenantCode, userId);
|
||||
}
|
||||
|
||||
/** 由 RequestContextInterceptor 调用,构建当前请求的租户/用户/数据权限上下文。 */
|
||||
async buildContext(user: { userId: string; tenantCode: string; username: string }): Promise<RequestContext> {
|
||||
const u = await this.prisma.client.user.findFirst({
|
||||
@ -133,93 +76,24 @@ export class AuthService {
|
||||
select: { roleCode: true },
|
||||
});
|
||||
const roleCodes = roleRows.map((r) => r.roleCode);
|
||||
const isSuper = roleCodes.includes(SUPER_ROLE);
|
||||
const roles = roleCodes.length
|
||||
? await this.prisma.client.role.findMany({
|
||||
where: { tenantCode: user.tenantCode, code: { in: roleCodes }, deletedAt: null },
|
||||
select: { dataScope: true },
|
||||
})
|
||||
: [];
|
||||
const dataScope: DataScope = isSuper ? 'all' : resolveDataScope(roles.map((r) => r.dataScope as DataScope));
|
||||
const orgCodes = isSuper
|
||||
? []
|
||||
: await resolveVisibleOrgs({
|
||||
prisma: this.prisma.client,
|
||||
tenantCode: user.tenantCode,
|
||||
dataScope,
|
||||
orgCode,
|
||||
roleCodes,
|
||||
});
|
||||
const dataScope = resolveDataScope(roles.map((r) => r.dataScope as DataScope));
|
||||
const orgCodes = await resolveVisibleOrgs({
|
||||
prisma: this.prisma.client,
|
||||
tenantCode: user.tenantCode,
|
||||
dataScope,
|
||||
orgCode,
|
||||
roleCodes,
|
||||
});
|
||||
|
||||
return { tenantCode: user.tenantCode, userId: user.userId, username: user.username, dataScope, orgCode, orgCodes };
|
||||
}
|
||||
|
||||
private permKey(tenantCode: string, userId: string): string {
|
||||
return `perm:${tenantCode}:${userId}`;
|
||||
}
|
||||
|
||||
/** 聚合权限码与超管标志。 */
|
||||
private async resolveAuth(tenantCode: string, userId: string): Promise<PermissionSet> {
|
||||
const roleRows = await this.prisma.client.userRole.findMany({
|
||||
where: { tenantCode, userId },
|
||||
select: { roleCode: true },
|
||||
});
|
||||
const roleCodes = roleRows.map((r) => r.roleCode);
|
||||
const isSuper = roleCodes.includes(SUPER_ROLE);
|
||||
const resourceRows = await this.prisma.client.roleResource.findMany({
|
||||
where: { tenantCode, roleCode: { in: roleCodes } },
|
||||
select: { resourceCode: true },
|
||||
});
|
||||
const resourceCodes = [...new Set(resourceRows.map((r) => r.resourceCode))];
|
||||
const permissions: string[] = resourceCodes.length
|
||||
? (
|
||||
await this.prisma.client.resource.findMany({
|
||||
where: { tenantCode, code: { in: resourceCodes }, type: { in: ['button', 'api'] }, deletedAt: null },
|
||||
select: { code: true },
|
||||
})
|
||||
).map((r) => r.code)
|
||||
: [];
|
||||
return { super: isSuper, permissions };
|
||||
}
|
||||
|
||||
/** 聚合并写入权限缓存(登录/刷新时调用)。 */
|
||||
private async writePermissionCache(tenantCode: string, userId: string): Promise<PermissionSet> {
|
||||
const set = await this.resolveAuth(tenantCode, userId);
|
||||
await this.redis.setJson(this.permKey(tenantCode, userId), set, REFRESH_TTL);
|
||||
return set;
|
||||
}
|
||||
|
||||
private async recordLoginLog(
|
||||
tenantCode: string,
|
||||
username: string,
|
||||
status: string,
|
||||
message: string | null,
|
||||
ip?: string,
|
||||
userAgent?: string,
|
||||
): Promise<void> {
|
||||
await this.loginLogs.record({
|
||||
tenantCode,
|
||||
username,
|
||||
status,
|
||||
message,
|
||||
ip: ip ?? null,
|
||||
userAgent: userAgent ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
private async recordAudit(entry: {
|
||||
tenantCode: string;
|
||||
userId: string | null;
|
||||
username: string | null;
|
||||
module: string;
|
||||
action: string;
|
||||
status: string;
|
||||
ip?: string | null;
|
||||
userAgent?: string | null;
|
||||
}): Promise<void> {
|
||||
await this.auditLogs.record(entry);
|
||||
}
|
||||
|
||||
private async issue(
|
||||
tenantCode: string,
|
||||
userId: string,
|
||||
|
||||
@ -8,14 +8,6 @@ export const PERMISSIONS_KEY = 'permissions';
|
||||
/** 要求当前用户具备给定权限标识(resource code)。 */
|
||||
export const RequirePermissions = (...permissions: string[]) => SetMetadata(PERMISSIONS_KEY, permissions);
|
||||
|
||||
export const AUDIT_KEY = 'audit';
|
||||
export interface AuditMeta {
|
||||
module: string;
|
||||
action: string;
|
||||
}
|
||||
/** 标记写接口需自动落审计日志(module/action 记录到 sys_audit_log)。 */
|
||||
export const AuditLog = (module: string, action: string) => SetMetadata(AUDIT_KEY, { module, action } satisfies AuditMeta);
|
||||
|
||||
export const CurrentUser = createParamDecorator((_data: unknown, ctx: ExecutionContext) => {
|
||||
return ctx.switchToHttp().getRequest().user;
|
||||
});
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { AuthService } from './auth.service.js';
|
||||
import { PrismaService } from '../prisma/prisma.service.js';
|
||||
import { PERMISSIONS_KEY } from './decorators.js';
|
||||
|
||||
@Injectable()
|
||||
export class PermissionsGuard implements CanActivate {
|
||||
constructor(
|
||||
private readonly auth: AuthService,
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly reflector: Reflector,
|
||||
) {}
|
||||
|
||||
@ -21,9 +21,17 @@ export class PermissionsGuard implements CanActivate {
|
||||
const user = request.user as { userId: string; tenantCode: string } | undefined;
|
||||
if (!user) throw new ForbiddenException('无权限');
|
||||
|
||||
const { super: isSuper, permissions } = await this.auth.getPermissionSet(user.tenantCode, user.userId);
|
||||
if (isSuper) return true;
|
||||
if (required.every((code) => permissions.includes(code))) return true;
|
||||
throw new ForbiddenException('无权限');
|
||||
const roleRows = await this.prisma.client.userRole.findMany({
|
||||
where: { tenantCode: user.tenantCode, userId: user.userId },
|
||||
select: { roleCode: true },
|
||||
});
|
||||
const roleCodes = roleRows.map((r) => r.roleCode);
|
||||
if (roleCodes.length === 0) throw new ForbiddenException('无权限');
|
||||
|
||||
const count = await this.prisma.client.roleResource.count({
|
||||
where: { tenantCode: user.tenantCode, roleCode: { in: roleCodes }, resourceCode: { in: required } },
|
||||
});
|
||||
if (count === 0) throw new ForbiddenException('无权限');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,58 +0,0 @@
|
||||
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { catchError, Observable, tap, throwError } from 'rxjs';
|
||||
import { AUDIT_KEY, AuditMeta } from '../../auth/decorators.js';
|
||||
import { AuditLogsService } from '../../audit-logs/audit-logs.service.js';
|
||||
import { maskSensitive } from '../mask.js';
|
||||
|
||||
/**
|
||||
* 审计日志拦截器:对标注 @AuditLog(module, action) 的接口,在请求完成后异步写入
|
||||
* sys_audit_log(成功 status=success,异常 status=fail + error_message)。
|
||||
* 入参脱敏;写入失败由 AuditLogsService 内部告警,不阻塞响应。
|
||||
*/
|
||||
@Injectable()
|
||||
export class AuditLogInterceptor implements NestInterceptor {
|
||||
constructor(
|
||||
private readonly reflector: Reflector,
|
||||
private readonly auditLogs: AuditLogsService,
|
||||
) {}
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
|
||||
const meta = this.reflector.getAllAndOverride<AuditMeta>(AUDIT_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
if (!meta) return next.handle();
|
||||
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const user = request.user as { userId?: string; tenantCode?: string; username?: string } | undefined;
|
||||
const start = Date.now();
|
||||
const base = {
|
||||
tenantCode: user?.tenantCode ?? '',
|
||||
userId: user?.userId ?? null,
|
||||
username: user?.username ?? null,
|
||||
module: meta.module,
|
||||
action: meta.action,
|
||||
method: request.method,
|
||||
path: request.originalUrl ?? request.url,
|
||||
params: request.body ? JSON.stringify(maskSensitive(request.body)) : null,
|
||||
ip: request.ip ?? request.remoteAddress ?? null,
|
||||
userAgent: request.headers?.['user-agent'] ?? null,
|
||||
};
|
||||
|
||||
return next.handle().pipe(
|
||||
tap(() => {
|
||||
void this.auditLogs.record({ ...base, status: 'success', durationMs: Date.now() - start });
|
||||
}),
|
||||
catchError((err) => {
|
||||
void this.auditLogs.record({
|
||||
...base,
|
||||
status: 'fail',
|
||||
errorMessage: err instanceof Error ? err.message : String(err),
|
||||
durationMs: Date.now() - start,
|
||||
});
|
||||
return throwError(() => err);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
/** 敏感字段递归脱敏:命中的字段值替换为 ***(供请求体日志与审计参数复用)。 */
|
||||
const SENSITIVE_KEYS = new Set([
|
||||
'password',
|
||||
'passwordhash',
|
||||
'oldpassword',
|
||||
'newpassword',
|
||||
'confirmpassword',
|
||||
'refreshtoken',
|
||||
'token',
|
||||
'secret',
|
||||
'authorization',
|
||||
]);
|
||||
|
||||
export function maskSensitive(value: unknown): unknown {
|
||||
if (value === null || value === undefined) return value;
|
||||
if (Array.isArray(value)) return value.map(maskSensitive);
|
||||
if (typeof value === 'object') {
|
||||
const out: Record<string, unknown> = {};
|
||||
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
||||
out[k] = SENSITIVE_KEYS.has(k.toLowerCase()) ? '***' : maskSensitive(v);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@ -1,57 +0,0 @@
|
||||
/**
|
||||
* 权限码单一事实源(细粒度 domain:action)。
|
||||
* 控制器 @RequirePermissions 与 seed 资源树共用,避免漂移。
|
||||
*/
|
||||
export const Permissions = {
|
||||
tenant: {
|
||||
list: 'tenant:list',
|
||||
create: 'tenant:create',
|
||||
update: 'tenant:update',
|
||||
delete: 'tenant:delete',
|
||||
},
|
||||
user: {
|
||||
list: 'user:list',
|
||||
create: 'user:create',
|
||||
update: 'user:update',
|
||||
delete: 'user:delete',
|
||||
resetPwd: 'user:reset-pwd',
|
||||
},
|
||||
role: {
|
||||
list: 'role:list',
|
||||
create: 'role:create',
|
||||
update: 'role:update',
|
||||
delete: 'role:delete',
|
||||
assignResource: 'role:assign-resource',
|
||||
},
|
||||
resource: {
|
||||
list: 'resource:list',
|
||||
create: 'resource:create',
|
||||
update: 'resource:update',
|
||||
delete: 'resource:delete',
|
||||
},
|
||||
org: {
|
||||
list: 'org:list',
|
||||
create: 'org:create',
|
||||
update: 'org:update',
|
||||
delete: 'org:delete',
|
||||
},
|
||||
dict: {
|
||||
list: 'dict:list',
|
||||
create: 'dict:create',
|
||||
update: 'dict:update',
|
||||
delete: 'dict:delete',
|
||||
},
|
||||
config: {
|
||||
list: 'config:list',
|
||||
create: 'config:create',
|
||||
update: 'config:update',
|
||||
delete: 'config:delete',
|
||||
},
|
||||
audit: { list: 'audit:list' },
|
||||
loginLog: { list: 'login-log:list' },
|
||||
} as const;
|
||||
|
||||
/** 全部权限码扁平列表(seed 授权与资源生成用)。 */
|
||||
export const ALL_PERMISSIONS: string[] = Object.values(Permissions).flatMap((m) =>
|
||||
Object.values(m),
|
||||
);
|
||||
@ -1,9 +0,0 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { RedisService } from './redis.service.js';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [RedisService],
|
||||
exports: [RedisService],
|
||||
})
|
||||
export class RedisModule {}
|
||||
@ -1,56 +0,0 @@
|
||||
import { Injectable, OnModuleDestroy } from '@nestjs/common';
|
||||
import { Redis } from 'ioredis';
|
||||
|
||||
/**
|
||||
* Redis 封装:统一 key 前缀(REDIS_KEY_PREFIX),支持 JSON 读写与按前缀删除。
|
||||
* 多个 fork 项目共用一个 Redis 实例时,通过不同前缀隔离。
|
||||
*/
|
||||
@Injectable()
|
||||
export class RedisService implements OnModuleDestroy {
|
||||
readonly prefix: string;
|
||||
private readonly client: Redis;
|
||||
|
||||
constructor() {
|
||||
this.prefix = process.env.REDIS_KEY_PREFIX ?? 'sysmgr:';
|
||||
this.client = new Redis({
|
||||
host: process.env.REDIS_HOST ?? '127.0.0.1',
|
||||
port: Number(process.env.REDIS_PORT ?? 6379),
|
||||
db: Number(process.env.REDIS_DB ?? 0),
|
||||
password: process.env.REDIS_PASSWORD || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
private key(k: string): string {
|
||||
return this.prefix + k;
|
||||
}
|
||||
|
||||
async getJson<T>(key: string): Promise<T | null> {
|
||||
const raw = await this.client.get(this.key(key));
|
||||
return raw ? (JSON.parse(raw) as T) : null;
|
||||
}
|
||||
|
||||
async setJson(key: string, value: unknown, ttlSeconds?: number): Promise<void> {
|
||||
const raw = JSON.stringify(value);
|
||||
if (ttlSeconds && ttlSeconds > 0) await this.client.set(this.key(key), raw, 'EX', ttlSeconds);
|
||||
else await this.client.set(this.key(key), raw);
|
||||
}
|
||||
|
||||
async del(key: string): Promise<void> {
|
||||
await this.client.del(this.key(key));
|
||||
}
|
||||
|
||||
/** 按前缀删除(SCAN 而非 KEYS,避免阻塞)。 */
|
||||
async delByPrefix(prefix: string): Promise<void> {
|
||||
const fullPrefix = this.prefix + prefix;
|
||||
let cursor = '0';
|
||||
do {
|
||||
const [next, keys] = await this.client.scan(cursor, 'MATCH', `${fullPrefix}*`, 'COUNT', 200);
|
||||
cursor = next;
|
||||
if (keys.length > 0) await this.client.del(...keys);
|
||||
} while (cursor !== '0');
|
||||
}
|
||||
|
||||
async onModuleDestroy(): Promise<void> {
|
||||
await this.client.quit();
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,5 @@
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common';
|
||||
import { PageQuery } from '../common/page.dto.js';
|
||||
import { Permissions } from '../common/permissions.js';
|
||||
import { AuditLog, RequirePermissions } from '../auth/decorators.js';
|
||||
import { CreateConfigDto, UpdateConfigDto } from './dto.js';
|
||||
import { ConfigsService } from './configs.service.js';
|
||||
|
||||
@ -10,34 +8,26 @@ export class ConfigsController {
|
||||
constructor(private readonly service: ConfigsService) {}
|
||||
|
||||
@Get()
|
||||
@RequirePermissions(Permissions.config.list)
|
||||
list(@Query() q: PageQuery) {
|
||||
return this.service.paginate(q.page, q.pageSize);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@RequirePermissions(Permissions.config.list)
|
||||
get(@Param('id') id: string) {
|
||||
return this.service.findById(id);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@RequirePermissions(Permissions.config.create)
|
||||
@AuditLog('config', 'create')
|
||||
create(@Body() dto: CreateConfigDto) {
|
||||
return this.service.createConfig(dto);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@RequirePermissions(Permissions.config.update)
|
||||
@AuditLog('config', 'update')
|
||||
update(@Param('id') id: string, @Body() dto: UpdateConfigDto) {
|
||||
return this.service.updateConfig(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@RequirePermissions(Permissions.config.delete)
|
||||
@AuditLog('config', 'delete')
|
||||
remove(@Param('id') id: string) {
|
||||
return this.service.softRemove(id);
|
||||
}
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common';
|
||||
import { Permissions } from '../common/permissions.js';
|
||||
import { AuditLog, RequirePermissions } from '../auth/decorators.js';
|
||||
import { CreateDictItemDto, UpdateDictItemDto } from './dto.js';
|
||||
import { DictItemsService } from './dict-items.service.js';
|
||||
|
||||
@ -9,34 +7,26 @@ export class DictItemsController {
|
||||
constructor(private readonly service: DictItemsService) {}
|
||||
|
||||
@Get()
|
||||
@RequirePermissions(Permissions.dict.list)
|
||||
list(@Query('dictTypeCode') dictTypeCode?: string) {
|
||||
return this.service.listByType(dictTypeCode);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@RequirePermissions(Permissions.dict.list)
|
||||
get(@Param('id') id: string) {
|
||||
return this.service.findById(id);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@RequirePermissions(Permissions.dict.create)
|
||||
@AuditLog('dict', 'create')
|
||||
create(@Body() dto: CreateDictItemDto) {
|
||||
return this.service.createItem(dto);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@RequirePermissions(Permissions.dict.update)
|
||||
@AuditLog('dict', 'update')
|
||||
update(@Param('id') id: string, @Body() dto: UpdateDictItemDto) {
|
||||
return this.service.updateItem(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@RequirePermissions(Permissions.dict.delete)
|
||||
@AuditLog('dict', 'delete')
|
||||
remove(@Param('id') id: string) {
|
||||
return this.service.softRemove(id);
|
||||
}
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common';
|
||||
import { PageQuery } from '../common/page.dto.js';
|
||||
import { Permissions } from '../common/permissions.js';
|
||||
import { AuditLog, RequirePermissions } from '../auth/decorators.js';
|
||||
import { CreateDictTypeDto, UpdateDictTypeDto } from './dto.js';
|
||||
import { DictTypesService } from './dict-types.service.js';
|
||||
|
||||
@ -10,34 +8,26 @@ export class DictTypesController {
|
||||
constructor(private readonly service: DictTypesService) {}
|
||||
|
||||
@Get()
|
||||
@RequirePermissions(Permissions.dict.list)
|
||||
list(@Query() q: PageQuery) {
|
||||
return this.service.paginate(q.page, q.pageSize);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@RequirePermissions(Permissions.dict.list)
|
||||
get(@Param('id') id: string) {
|
||||
return this.service.findById(id);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@RequirePermissions(Permissions.dict.create)
|
||||
@AuditLog('dict', 'create')
|
||||
create(@Body() dto: CreateDictTypeDto) {
|
||||
return this.service.createType(dto);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@RequirePermissions(Permissions.dict.update)
|
||||
@AuditLog('dict', 'update')
|
||||
update(@Param('id') id: string, @Body() dto: UpdateDictTypeDto) {
|
||||
return this.service.updateType(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@RequirePermissions(Permissions.dict.delete)
|
||||
@AuditLog('dict', 'delete')
|
||||
remove(@Param('id') id: string) {
|
||||
return this.service.softRemove(id);
|
||||
}
|
||||
|
||||
@ -1,7 +1,32 @@
|
||||
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { InjectPinoLogger, PinoLogger } from 'nestjs-pino';
|
||||
import { maskSensitive } from '../common/mask.js';
|
||||
|
||||
const SENSITIVE_KEYS = new Set([
|
||||
'password',
|
||||
'passwordhash',
|
||||
'oldpassword',
|
||||
'newpassword',
|
||||
'confirmpassword',
|
||||
'refreshtoken',
|
||||
'token',
|
||||
'secret',
|
||||
'authorization',
|
||||
]);
|
||||
|
||||
/** 递归脱敏:命中的字段值替换为 ***。 */
|
||||
function mask(value: unknown): unknown {
|
||||
if (value === null || value === undefined) return value;
|
||||
if (Array.isArray(value)) return value.map(mask);
|
||||
if (typeof value === 'object') {
|
||||
const out: Record<string, unknown> = {};
|
||||
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
||||
out[k] = SENSITIVE_KEYS.has(k.toLowerCase()) ? '***' : mask(v);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** 记录请求 body(POST/PUT/PATCH/DELETE),敏感字段脱敏,级别 debug。 */
|
||||
@Injectable()
|
||||
@ -12,7 +37,7 @@ export class BodyLoggingInterceptor implements NestInterceptor {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const { method, url, body } = request;
|
||||
if (body && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method) && Object.keys(body).length > 0) {
|
||||
this.logger.debug({ method, url, body: maskSensitive(body) }, 'request body');
|
||||
this.logger.debug({ method, url, body: mask(body) }, 'request body');
|
||||
}
|
||||
return next.handle();
|
||||
}
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import { Controller, Get, Query } from '@nestjs/common';
|
||||
import { PageQuery } from '../common/page.dto.js';
|
||||
import { Permissions } from '../common/permissions.js';
|
||||
import { RequirePermissions } from '../auth/decorators.js';
|
||||
import { LoginLogsService } from './login-logs.service.js';
|
||||
|
||||
@Controller('login-logs')
|
||||
@ -9,7 +7,6 @@ export class LoginLogsController {
|
||||
constructor(private readonly service: LoginLogsService) {}
|
||||
|
||||
@Get()
|
||||
@RequirePermissions(Permissions.loginLog.list)
|
||||
list(@Query() q: PageQuery) {
|
||||
return this.service.paginate(q.page, q.pageSize);
|
||||
}
|
||||
|
||||
@ -5,6 +5,5 @@ import { LoginLogsService } from './login-logs.service.js';
|
||||
@Module({
|
||||
controllers: [LoginLogsController],
|
||||
providers: [LoginLogsService],
|
||||
exports: [LoginLogsService],
|
||||
})
|
||||
export class LoginLogsModule {}
|
||||
|
||||
@ -1,32 +1,10 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectPinoLogger, PinoLogger } from 'nestjs-pino';
|
||||
import { BaseCrudService } from '../common/base-crud.service.js';
|
||||
import { PrismaService } from '../prisma/prisma.service.js';
|
||||
|
||||
export interface LoginLogEntry {
|
||||
tenantCode: string;
|
||||
username?: string | null;
|
||||
ip?: string | null;
|
||||
userAgent?: string | null;
|
||||
status: string;
|
||||
message?: string | null;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class LoginLogsService extends BaseCrudService {
|
||||
constructor(
|
||||
prisma: PrismaService,
|
||||
@InjectPinoLogger(LoginLogsService.name) private readonly logger: PinoLogger,
|
||||
) {
|
||||
constructor(prisma: PrismaService) {
|
||||
super(prisma, 'loginLog');
|
||||
}
|
||||
|
||||
/** 写入一条登录日志;失败仅告警,不抛出。 */
|
||||
async record(data: LoginLogEntry): Promise<void> {
|
||||
try {
|
||||
await this.db().create({ data });
|
||||
} catch (err) {
|
||||
this.logger.warn({ err }, '登录日志写入失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common';
|
||||
import { Permissions } from '../common/permissions.js';
|
||||
import { AuditLog, RequirePermissions } from '../auth/decorators.js';
|
||||
import { CreateOrgDto, UpdateOrgDto } from './dto.js';
|
||||
import { OrgsService } from './orgs.service.js';
|
||||
|
||||
@ -9,34 +7,26 @@ export class OrgsController {
|
||||
constructor(private readonly service: OrgsService) {}
|
||||
|
||||
@Get()
|
||||
@RequirePermissions(Permissions.org.list)
|
||||
list() {
|
||||
return this.service.list({}, { sort: 'asc' });
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@RequirePermissions(Permissions.org.list)
|
||||
get(@Param('id') id: string) {
|
||||
return this.service.findById(id);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@RequirePermissions(Permissions.org.create)
|
||||
@AuditLog('org', 'create')
|
||||
create(@Body() dto: CreateOrgDto) {
|
||||
return this.service.createOrg(dto);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@RequirePermissions(Permissions.org.update)
|
||||
@AuditLog('org', 'update')
|
||||
update(@Param('id') id: string, @Body() dto: UpdateOrgDto) {
|
||||
return this.service.updateOrg(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@RequirePermissions(Permissions.org.delete)
|
||||
@AuditLog('org', 'delete')
|
||||
remove(@Param('id') id: string) {
|
||||
return this.service.softRemove(id);
|
||||
}
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common';
|
||||
import { Permissions } from '../common/permissions.js';
|
||||
import { AuditLog, RequirePermissions } from '../auth/decorators.js';
|
||||
import { CreateResourceDto, UpdateResourceDto } from './dto.js';
|
||||
import { ResourcesService } from './resources.service.js';
|
||||
|
||||
@ -9,34 +7,26 @@ export class ResourcesController {
|
||||
constructor(private readonly service: ResourcesService) {}
|
||||
|
||||
@Get()
|
||||
@RequirePermissions(Permissions.resource.list)
|
||||
list() {
|
||||
return this.service.list({}, { sort: 'asc' });
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@RequirePermissions(Permissions.resource.list)
|
||||
get(@Param('id') id: string) {
|
||||
return this.service.findById(id);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@RequirePermissions(Permissions.resource.create)
|
||||
@AuditLog('resource', 'create')
|
||||
create(@Body() dto: CreateResourceDto) {
|
||||
return this.service.createResource(dto);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@RequirePermissions(Permissions.resource.update)
|
||||
@AuditLog('resource', 'update')
|
||||
update(@Param('id') id: string, @Body() dto: UpdateResourceDto) {
|
||||
return this.service.updateResource(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@RequirePermissions(Permissions.resource.delete)
|
||||
@AuditLog('resource', 'delete')
|
||||
remove(@Param('id') id: string) {
|
||||
return this.service.softRemove(id);
|
||||
}
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common';
|
||||
import { PageQuery } from '../common/page.dto.js';
|
||||
import { Permissions } from '../common/permissions.js';
|
||||
import { AuditLog, RequirePermissions } from '../auth/decorators.js';
|
||||
import { AssignResourcesDto, CreateRoleDto, UpdateRoleDto } from './dto.js';
|
||||
import { RolesService } from './roles.service.js';
|
||||
|
||||
@ -10,47 +8,36 @@ export class RolesController {
|
||||
constructor(private readonly service: RolesService) {}
|
||||
|
||||
@Get()
|
||||
@RequirePermissions(Permissions.role.list)
|
||||
list(@Query() q: PageQuery) {
|
||||
return this.service.paginate(q.page, q.pageSize);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@RequirePermissions(Permissions.role.list)
|
||||
get(@Param('id') id: string) {
|
||||
return this.service.findById(id);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@RequirePermissions(Permissions.role.create)
|
||||
@AuditLog('role', 'create')
|
||||
create(@Body() dto: CreateRoleDto) {
|
||||
return this.service.createRole(dto);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@RequirePermissions(Permissions.role.update)
|
||||
@AuditLog('role', 'update')
|
||||
update(@Param('id') id: string, @Body() dto: UpdateRoleDto) {
|
||||
return this.service.updateRole(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@RequirePermissions(Permissions.role.delete)
|
||||
@AuditLog('role', 'delete')
|
||||
remove(@Param('id') id: string) {
|
||||
return this.service.softRemove(id);
|
||||
}
|
||||
|
||||
@Get(':id/resources')
|
||||
@RequirePermissions(Permissions.role.list)
|
||||
resources(@Param('id') id: string) {
|
||||
return this.service.assignedResources(id);
|
||||
}
|
||||
|
||||
@Put(':id/resources')
|
||||
@RequirePermissions(Permissions.role.assignResource)
|
||||
@AuditLog('role', 'assign-resource')
|
||||
assign(@Param('id') id: string, @Body() dto: AssignResourcesDto) {
|
||||
return this.service.assignResources(id, dto);
|
||||
}
|
||||
|
||||
@ -1,16 +1,12 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { BaseCrudService } from '../common/base-crud.service.js';
|
||||
import { PrismaService } from '../prisma/prisma.service.js';
|
||||
import { RedisService } from '../common/redis/redis.service.js';
|
||||
import { getRequestContext } from '../common/context/request-context.js';
|
||||
import { AssignResourcesDto, CreateRoleDto, UpdateRoleDto } from './dto.js';
|
||||
|
||||
@Injectable()
|
||||
export class RolesService extends BaseCrudService {
|
||||
constructor(
|
||||
prisma: PrismaService,
|
||||
private readonly redis: RedisService,
|
||||
) {
|
||||
constructor(prisma: PrismaService) {
|
||||
super(prisma, 'role');
|
||||
}
|
||||
|
||||
@ -39,8 +35,6 @@ export class RolesService extends BaseCrudService {
|
||||
data: dto.resourceCodes.map((resourceCode) => ({ tenantCode: ctx.tenantCode, roleCode, resourceCode })),
|
||||
});
|
||||
}
|
||||
// 授权变更:删除该租户全部用户权限缓存,下一次请求守卫重新聚合并回填
|
||||
await this.redis.delByPrefix(`perm:${ctx.tenantCode}:`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common';
|
||||
import { PageQuery } from '../common/page.dto.js';
|
||||
import { Permissions } from '../common/permissions.js';
|
||||
import { AuditLog, RequirePermissions } from '../auth/decorators.js';
|
||||
import { CreateTenantDto, UpdateTenantDto } from './dto.js';
|
||||
import { TenantsService } from './tenants.service.js';
|
||||
|
||||
@ -10,34 +8,26 @@ export class TenantsController {
|
||||
constructor(private readonly service: TenantsService) {}
|
||||
|
||||
@Get()
|
||||
@RequirePermissions(Permissions.tenant.list)
|
||||
list(@Query() q: PageQuery) {
|
||||
return this.service.paginate(q.page, q.pageSize);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@RequirePermissions(Permissions.tenant.list)
|
||||
get(@Param('id') id: string) {
|
||||
return this.service.findById(id);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@RequirePermissions(Permissions.tenant.create)
|
||||
@AuditLog('tenant', 'create')
|
||||
create(@Body() dto: CreateTenantDto) {
|
||||
return this.service.createTenant(dto);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@RequirePermissions(Permissions.tenant.update)
|
||||
@AuditLog('tenant', 'update')
|
||||
update(@Param('id') id: string, @Body() dto: UpdateTenantDto) {
|
||||
return this.service.updateTenant(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@RequirePermissions(Permissions.tenant.delete)
|
||||
@AuditLog('tenant', 'delete')
|
||||
remove(@Param('id') id: string) {
|
||||
return this.service.removeTenant(id);
|
||||
}
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common';
|
||||
import { PageQuery } from '../common/page.dto.js';
|
||||
import { Permissions } from '../common/permissions.js';
|
||||
import { AuditLog, RequirePermissions } from '../auth/decorators.js';
|
||||
import { CreateUserDto, ResetPasswordDto, UpdateUserDto } from './dto.js';
|
||||
import { UsersService } from './users.service.js';
|
||||
|
||||
@ -10,41 +8,31 @@ export class UsersController {
|
||||
constructor(private readonly service: UsersService) {}
|
||||
|
||||
@Get()
|
||||
@RequirePermissions(Permissions.user.list)
|
||||
list(@Query() q: PageQuery) {
|
||||
return this.service.listUsers(q);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@RequirePermissions(Permissions.user.list)
|
||||
get(@Param('id') id: string) {
|
||||
return this.service.findById(id);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@RequirePermissions(Permissions.user.create)
|
||||
@AuditLog('user', 'create')
|
||||
create(@Body() dto: CreateUserDto) {
|
||||
return this.service.createUser(dto);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@RequirePermissions(Permissions.user.update)
|
||||
@AuditLog('user', 'update')
|
||||
update(@Param('id') id: string, @Body() dto: UpdateUserDto) {
|
||||
return this.service.updateUser(id, dto);
|
||||
}
|
||||
|
||||
@Patch(':id/password')
|
||||
@RequirePermissions(Permissions.user.resetPwd)
|
||||
@AuditLog('user', 'reset-pwd')
|
||||
resetPassword(@Param('id') id: string, @Body() dto: ResetPasswordDto) {
|
||||
return this.service.resetPassword(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@RequirePermissions(Permissions.user.delete)
|
||||
@AuditLog('user', 'delete')
|
||||
remove(@Param('id') id: string) {
|
||||
return this.service.softRemove(id);
|
||||
}
|
||||
|
||||
@ -14,9 +14,8 @@ export class UsersService extends BaseCrudService {
|
||||
|
||||
async createUser(dto: CreateUserDto) {
|
||||
const passwordHash = await bcrypt.hash(dto.password, 10);
|
||||
const { password: _pw, ...rest } = dto;
|
||||
const created = await this.create({ ...rest, passwordHash });
|
||||
return this.omitPasswordHash(created);
|
||||
const { password: _p, ...rest } = dto;
|
||||
return this.create({ ...rest, passwordHash });
|
||||
}
|
||||
|
||||
async listUsers(q: PageQuery) {
|
||||
@ -26,35 +25,15 @@ export class UsersService extends BaseCrudService {
|
||||
if (ctx && ctx.orgCodes.length > 0) {
|
||||
where.orgCode = { in: ctx.orgCodes };
|
||||
}
|
||||
const result = await this.paginate(q.page, q.pageSize, where);
|
||||
result.list = result.list.map((u: { passwordHash?: unknown }) => this.omitPasswordHash(u));
|
||||
return result;
|
||||
}
|
||||
|
||||
async findById(id: string) {
|
||||
const user = await super.findById(id);
|
||||
return user ? this.omitPasswordHash(user) : user;
|
||||
return this.paginate(q.page, q.pageSize, where);
|
||||
}
|
||||
|
||||
async updateUser(id: string, dto: UpdateUserDto) {
|
||||
const updated = await this.update(id, { ...dto });
|
||||
return this.omitPasswordHash(updated);
|
||||
return this.update(id, { ...dto });
|
||||
}
|
||||
|
||||
async resetPassword(id: string, dto: ResetPasswordDto) {
|
||||
const passwordHash = await bcrypt.hash(dto.password, 10);
|
||||
const updated = await this.update(id, { passwordHash });
|
||||
return this.omitPasswordHash(updated);
|
||||
}
|
||||
|
||||
async softRemove(id: string) {
|
||||
const removed = await super.softRemove(id);
|
||||
return this.omitPasswordHash(removed);
|
||||
}
|
||||
|
||||
/** 响应边界剥掉 passwordHash,避免泄漏(登录内部校验直接查库,不受影响)。 */
|
||||
private omitPasswordHash<T extends { passwordHash?: unknown }>(row: T): Omit<T, 'passwordHash'> {
|
||||
const { passwordHash: _passwordHash, ...rest } = row;
|
||||
return rest;
|
||||
return this.update(id, { passwordHash });
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user