From d8d3055c737f605bef8e418869ff591592805d96 Mon Sep 17 00:00:00 2001 From: liuyun Date: Sat, 10 Oct 2026 21:13:35 +0800 Subject: [PATCH] feat: backend foundation (Prisma 6 + MySQL, auth, tenant/soft-delete/data-scope filters, core CRUD) Co-Authored-By: Claude Opus 4.8 --- package.json | 8 + pnpm-lock.yaml | 938 +++++++++++++++++- pnpm-workspace.yaml | 5 + .../20261010073709_init/migration.sql | 247 +++++ prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 257 +++++ src/app.module.ts | 38 +- src/auth/auth.controller.ts | 27 + src/auth/auth.module.ts | 18 + src/auth/auth.service.ts | 115 +++ src/auth/decorators.ts | 13 + src/auth/dto.ts | 21 + src/auth/jwt-auth.guard.ts | 44 + src/auth/permissions.guard.ts | 37 + src/auth/request-context.interceptor.ts | 27 + src/common/base-crud.service.ts | 52 + src/common/context/request-context.ts | 23 + src/common/data-scope.ts | 63 ++ src/common/enums.ts | 39 + src/common/filters/http-exception.filter.ts | 28 + .../interceptors/transform.interceptor.ts | 13 + src/common/page.dto.ts | 21 + src/configs/configs.controller.ts | 34 + src/configs/configs.module.ts | 9 + src/configs/configs.service.ts | 19 + src/configs/dto.ts | 12 + src/dicts/dict-items.controller.ts | 33 + src/dicts/dict-items.service.ts | 23 + src/dicts/dict-types.controller.ts | 34 + src/dicts/dict-types.service.ts | 19 + src/dicts/dicts.module.ts | 11 + src/dicts/dto.ts | 27 + src/main.ts | 7 +- src/orgs/dto.ts | 17 + src/orgs/orgs.controller.ts | 33 + src/orgs/orgs.module.ts | 9 + src/orgs/orgs.service.ts | 19 + src/prisma/prisma.module.ts | 9 + src/prisma/prisma.service.ts | 47 + src/resources/dto.ts | 26 + src/resources/resources.controller.ts | 33 + src/resources/resources.module.ts | 9 + src/resources/resources.service.ts | 19 + src/roles/dto.ts | 19 + src/roles/roles.controller.ts | 44 + src/roles/roles.module.ts | 9 + src/roles/roles.service.ts | 40 + src/tenants/dto.ts | 14 + src/tenants/tenants.controller.ts | 34 + src/tenants/tenants.module.ts | 9 + src/tenants/tenants.service.ts | 27 + src/users/dto.ts | 24 + src/users/users.controller.ts | 39 + src/users/users.module.ts | 9 + src/users/users.service.ts | 39 + 55 files changed, 2764 insertions(+), 29 deletions(-) create mode 100644 pnpm-workspace.yaml create mode 100644 prisma/migrations/20261010073709_init/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 prisma/schema.prisma create mode 100644 src/auth/auth.controller.ts create mode 100644 src/auth/auth.module.ts create mode 100644 src/auth/auth.service.ts create mode 100644 src/auth/decorators.ts create mode 100644 src/auth/dto.ts create mode 100644 src/auth/jwt-auth.guard.ts create mode 100644 src/auth/permissions.guard.ts create mode 100644 src/auth/request-context.interceptor.ts create mode 100644 src/common/base-crud.service.ts create mode 100644 src/common/context/request-context.ts create mode 100644 src/common/data-scope.ts create mode 100644 src/common/enums.ts create mode 100644 src/common/filters/http-exception.filter.ts create mode 100644 src/common/interceptors/transform.interceptor.ts create mode 100644 src/common/page.dto.ts create mode 100644 src/configs/configs.controller.ts create mode 100644 src/configs/configs.module.ts create mode 100644 src/configs/configs.service.ts create mode 100644 src/configs/dto.ts create mode 100644 src/dicts/dict-items.controller.ts create mode 100644 src/dicts/dict-items.service.ts create mode 100644 src/dicts/dict-types.controller.ts create mode 100644 src/dicts/dict-types.service.ts create mode 100644 src/dicts/dicts.module.ts create mode 100644 src/dicts/dto.ts create mode 100644 src/orgs/dto.ts create mode 100644 src/orgs/orgs.controller.ts create mode 100644 src/orgs/orgs.module.ts create mode 100644 src/orgs/orgs.service.ts create mode 100644 src/prisma/prisma.module.ts create mode 100644 src/prisma/prisma.service.ts create mode 100644 src/resources/dto.ts create mode 100644 src/resources/resources.controller.ts create mode 100644 src/resources/resources.module.ts create mode 100644 src/resources/resources.service.ts create mode 100644 src/roles/dto.ts create mode 100644 src/roles/roles.controller.ts create mode 100644 src/roles/roles.module.ts create mode 100644 src/roles/roles.service.ts create mode 100644 src/tenants/dto.ts create mode 100644 src/tenants/tenants.controller.ts create mode 100644 src/tenants/tenants.module.ts create mode 100644 src/tenants/tenants.service.ts create mode 100644 src/users/dto.ts create mode 100644 src/users/users.controller.ts create mode 100644 src/users/users.module.ts create mode 100644 src/users/users.service.ts diff --git a/package.json b/package.json index 4f78155..3991588 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,12 @@ "dependencies": { "@nestjs/common": "^12.0.1", "@nestjs/core": "^12.0.1", + "@nestjs/jwt": "^12.0.2", "@nestjs/platform-express": "^12.0.1", + "@prisma/client": "^6.19.3", + "bcryptjs": "^3.0.3", + "class-transformer": "^0.5.1", + "class-validator": "^0.15.1", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1" }, @@ -37,13 +42,16 @@ "@nestjs/mau": "^0.2.6", "@nestjs/schematics": "^12.0.0", "@nestjs/testing": "^12.0.1", + "@types/bcryptjs": "^3.0.0", "@types/express": "^5.0.0", "@types/node": "^24.0.0", "@types/supertest": "^7.0.0", "@vitest/coverage-v8": "^4.1.2", + "dotenv": "^18.0.6", "oxlint": "^1.58.0", "oxlint-tsgolint": "^7.0.2001", "prettier": "^3.4.2", + "prisma": "^6.19.3", "source-map-support": "^0.5.21", "supertest": "^7.0.0", "typescript": "^6.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c6c9ef2..89e47bb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,3 +1,161 @@ +--- +lockfileVersion: '9.0' + +importers: + + .: + configDependencies: {} + packageManagerDependencies: + pnpm: + specifier: 12.10.1 + version: 12.10.1 + +packages: + + '@pnpm/exe.android-arm64@12.10.1': + resolution: {integrity: sha512-CVTlHPwNvadQCJD9bGSMzWIhFX8QpjtqC2c27dhdnn/9iKzEjZ3qL62o3gHpKbIuo6qn4WxTKRoaokgNdReBig==} + cpu: [arm64] + os: [android] + + '@pnpm/exe.android-x64@12.10.1': + resolution: {integrity: sha512-NFiYLIaodY7da0XIebwcU3JoP7IK/lzF7zgKSSP+9H+QcFnQ1GDfBkuBKCYJzZ+Xu0WH54neIPLBBCaOjaaauA==} + cpu: [x64] + os: [android] + + '@pnpm/exe.darwin-arm64@12.10.1': + resolution: {integrity: sha512-0O8tdQ8ijlPJudXspLMTcitMkdBEz6uUflazL5uQE0WMs85oQIp0U+5hfEktMFB64QqEhjqlvvujTwbzdr9oMg==} + cpu: [arm64] + os: [darwin] + + '@pnpm/exe.darwin-x64@12.10.1': + resolution: {integrity: sha512-ud+1/uZOKEo8P/AsPynOJc/LNwfI0j6WwCElibj71rU9l4nIBnsSn+wWTIPZPkZ4BBSH5+82azSnAARMyJ1lNA==} + cpu: [x64] + os: [darwin] + + '@pnpm/exe.freebsd-x64@12.10.1': + resolution: {integrity: sha512-mj9PE5rf8uAfQQ2K66HSATMSut+lNiB4eSqFd5+azFVCMnxNSy/32xpZQJT21PhnN3Y4qbK+iKHe5nWAxSuRhw==} + cpu: [x64] + os: [freebsd] + + '@pnpm/exe.linux-arm64-musl@12.10.1': + resolution: {integrity: sha512-xba+00wZJ8Um6rBJJjWl88uJj0tydB8dtomvjlCLvi0uL32vpa9CRP8Vv26FIXvbiZ1bo2RCzYt/eChxJVV52w==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-arm64@12.10.1': + resolution: {integrity: sha512-oBdMp69ScC9UnniG4qZAQVr2fBDSXjm4yk/72vkEVyrlwS23yMxcRQl7Qdo3sgXPAygY6TWnPRKm4rXzjoyW7w==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-ppc64@12.10.1': + resolution: {integrity: sha512-Yyb3wu09h3c7mLme270uHFiVaAIUHC411Ru9LIU2BKUIstrGlXa9OqqVWoXaRgYRdFdOFknkwG7GNn6ygwLbnw==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-riscv64@12.10.1': + resolution: {integrity: sha512-PdkeUFFkybknnGkWP0H22eMpIiS4OZXL5BcQzF4lh+vmrW3IaC2HOkbANKgot/PKO0Kl77pzA/6PKFxgbgfXIQ==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-s390x@12.10.1': + resolution: {integrity: sha512-1okKuHKJwJPAbofDxfhZWAUxEWtee4ZSf7Pah8kcvocaJkf2WxFJ5KVTiNIvDml5GLidDQsgf7Pcf/WHbCak7Q==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-x64-musl@12.10.1': + resolution: {integrity: sha512-e6IqzVNkPa7srFKJKnY23uWJ+qz0LaRWiTUo/sHfFXRRMRzB5uNOr6qWWip/jqhdGwU91n9J7LrEYZ6e0tRczw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-x64@12.10.1': + resolution: {integrity: sha512-ZByV0DRgWzGaDSjT5ddRBASdwz+r/qPMYh7o6RGmPQjtRGNFbSl94xY2DcAM8mqCOZJUUDtAXQ+wpBXcdpKlkg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.win32-arm64@12.10.1': + resolution: {integrity: sha512-I7QZHcjioG7orFeCRUFUHByJ1G0zFPnAlaGGyrUvb6MGkv5oeNwNPl7t+Gp+7zHfnMcgLGJ1a+Wfim0IDw3lOw==} + cpu: [arm64] + os: [win32] + + '@pnpm/exe.win32-x64@12.10.1': + resolution: {integrity: sha512-4WJFdp58EKlCj0zLobrXU0vpszSZIr4X/3y/uNbAQqGW0zE+VdruZB0gFc3L2kzsUzVwTYbaCn+XEhnCYSAlTg==} + cpu: [x64] + os: [win32] + + pnpm@12.10.1: + resolution: {integrity: sha512-ukCjfrGjcNYP6oxM8dNkwTvMzvUY6YrSvO6VT1OdONo3RY5YJho/pV4GUjRQ6g//+fSXsTW+QfLBXBZCusrObA==} + engines: {node: '>=18.*'} + hasBin: true + +snapshots: + + '@pnpm/exe.android-arm64@12.10.1': + optional: true + + '@pnpm/exe.android-x64@12.10.1': + optional: true + + '@pnpm/exe.darwin-arm64@12.10.1': + optional: true + + '@pnpm/exe.darwin-x64@12.10.1': + optional: true + + '@pnpm/exe.freebsd-x64@12.10.1': + optional: true + + '@pnpm/exe.linux-arm64-musl@12.10.1': + optional: true + + '@pnpm/exe.linux-arm64@12.10.1': + optional: true + + '@pnpm/exe.linux-ppc64@12.10.1': + optional: true + + '@pnpm/exe.linux-riscv64@12.10.1': + optional: true + + '@pnpm/exe.linux-s390x@12.10.1': + optional: true + + '@pnpm/exe.linux-x64-musl@12.10.1': + optional: true + + '@pnpm/exe.linux-x64@12.10.1': + optional: true + + '@pnpm/exe.win32-arm64@12.10.1': + optional: true + + '@pnpm/exe.win32-x64@12.10.1': + optional: true + + pnpm@12.10.1: + optionalDependencies: + '@pnpm/exe.android-arm64': 12.10.1 + '@pnpm/exe.android-x64': 12.10.1 + '@pnpm/exe.darwin-arm64': 12.10.1 + '@pnpm/exe.darwin-x64': 12.10.1 + '@pnpm/exe.freebsd-x64': 12.10.1 + '@pnpm/exe.linux-arm64': 12.10.1 + '@pnpm/exe.linux-arm64-musl': 12.10.1 + '@pnpm/exe.linux-ppc64': 12.10.1 + '@pnpm/exe.linux-riscv64': 12.10.1 + '@pnpm/exe.linux-s390x': 12.10.1 + '@pnpm/exe.linux-x64': 12.10.1 + '@pnpm/exe.linux-x64-musl': 12.10.1 + '@pnpm/exe.win32-arm64': 12.10.1 + '@pnpm/exe.win32-x64': 12.10.1 + +--- lockfileVersion: '9.0' settings: @@ -10,13 +168,28 @@ importers: dependencies: '@nestjs/common': specifier: ^12.0.1 - version: 12.1.2(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0) + version: 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': specifier: ^12.0.1 - version: 12.1.2(@nestjs/common@12.1.2(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/platform-express@12.1.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 12.1.2(@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/platform-express@12.1.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/jwt': + specifier: ^12.0.2 + version: 12.0.2(@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/platform-express': specifier: ^12.0.1 - version: 12.1.2(@nestjs/common@12.1.2(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)(supports-color@7.2.0) + version: 12.1.2(@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)(supports-color@7.2.0) + '@prisma/client': + specifier: ^6.19.3 + version: 6.19.3(prisma@6.19.3(typescript@6.0.3))(typescript@6.0.3) + bcryptjs: + specifier: ^3.0.3 + version: 3.0.3 + class-transformer: + specifier: ^0.5.1 + version: 0.5.1 + class-validator: + specifier: ^0.15.1 + version: 0.15.1 reflect-metadata: specifier: ^0.2.2 version: 0.2.2 @@ -35,7 +208,10 @@ importers: version: 12.0.6(chokidar@5.0.0)(prettier@3.9.9)(typescript@6.0.3) '@nestjs/testing': specifier: ^12.0.1 - version: 12.1.2(@nestjs/common@12.1.2(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)(@nestjs/platform-express@12.1.2) + version: 12.1.2(@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)(@nestjs/platform-express@12.1.2) + '@types/bcryptjs': + specifier: ^3.0.0 + version: 3.0.0 '@types/express': specifier: ^5.0.0 version: 5.0.6 @@ -48,6 +224,9 @@ importers: '@vitest/coverage-v8': specifier: ^4.1.2 version: 4.1.11(vitest@4.1.11) + dotenv: + specifier: ^18.0.6 + version: 18.0.6 oxlint: specifier: ^1.58.0 version: 1.87.0(oxlint-tsgolint@7.0.2003) @@ -57,6 +236,9 @@ importers: prettier: specifier: ^3.4.2 version: 3.9.9 + prisma: + specifier: ^6.19.3 + version: 6.19.3(typescript@6.0.3) source-map-support: specifier: ^0.5.21 version: 0.5.21 @@ -68,10 +250,10 @@ importers: 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)) + 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)) + 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: @@ -121,6 +303,162 @@ packages: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@inquirer/ansi@2.0.9': resolution: {integrity: sha512-0Ywt3S8tqM6TWUcsRlBFQ1x+X3/o8kktX0ZiKAnByYb9GFDknnOhse1PVtfiD2BXPI8ar+BWx9PV9R0lQIcygw==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} @@ -331,6 +669,11 @@ packages: '@nestjs/websockets': optional: true + '@nestjs/jwt@12.0.2': + resolution: {integrity: sha512-1crs22hB7pNf7FnfRRRXvFTo595Rgx+JLyIuWInsyd5Ov/z0N8CCJPqMlTy41D5XTWe8CVr9miwB+2OXxVX9RA==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 + '@nestjs/mau@0.2.8': resolution: {integrity: sha512-nIb0i3c/KhKEbxZfYMEKv9Mcvk5miBdZcexnvkgnAUhoMO2BUjOGT+Khf2cZzu7B51Y7v01NG2JVl/HdQEUQ6w==} engines: {node: '>= 20.0.0'} @@ -527,6 +870,36 @@ packages: '@paralleldrive/cuid2@2.3.1': resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} + '@prisma/client@6.19.3': + resolution: {integrity: sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==} + engines: {node: '>=18.18'} + peerDependencies: + prisma: '*' + typescript: '>=5.1.0' + peerDependenciesMeta: + prisma: + optional: true + typescript: + optional: true + + '@prisma/config@6.19.3': + resolution: {integrity: sha512-CBPT44BjlQxEt8kiMEauji2WHTDoVBOKl7UlewXmUgBPnr/oPRZC3psci5chJnYmH0ivEIog2OU9PGWoki3DLQ==} + + '@prisma/debug@6.19.3': + resolution: {integrity: sha512-ljkJ+SgpXNktLG0Q/n4JGYCkKf0f8oYLyjImS2I8e2q2WCfdRRtWER062ZV/ixaNP2M2VKlWXVJiGzZaUgbKZw==} + + '@prisma/engines-version@7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7': + resolution: {integrity: sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==} + + '@prisma/engines@6.19.3': + resolution: {integrity: sha512-RSYxtlYFl5pJ8ZePgMv0lZ9IzVCOdTPOegrs2qcbAEFrBI1G33h6wyC9kjQvo0DnYEhEVY0X4LsuFHXLKQk88g==} + + '@prisma/fetch-engine@6.19.3': + resolution: {integrity: sha512-tKtl/qco9Nt7LU5iKhpultD8O4vMCZcU2CHjNTnRrL1QvSUr5W/GcyFPjNL87GtRrwBc7ubXXD9xy4EvLvt8JA==} + + '@prisma/get-platform@6.19.3': + resolution: {integrity: sha512-xFj1VcJ1N3MKooOQAGO0W5tsd0W2QzIvW7DD7c/8H14Zmp4jseeWAITm+w2LLoLrlhoHdPPh0NMZ8mfL6puoHA==} + '@rolldown/binding-android-arm-eabi@1.2.13': resolution: {integrity: sha512-Vu/NICrgdRvUOeaJZIyB2FP63s6fO/4ud5l8dXmxDx9yDyn+mN0y+gsvsVviSli+T361Aqo0h46EuMoobrwawA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -640,6 +1013,10 @@ packages: '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + '@types/bcryptjs@3.0.0': + resolution: {integrity: sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg==} + deprecated: This is a stub types definition. bcryptjs provides its own type definitions, so you do not need this installed. + '@types/body-parser@1.19.6': resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} @@ -667,9 +1044,15 @@ packages: '@types/http-errors@2.0.5': resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + '@types/methods@1.1.4': resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@24.19.1': resolution: {integrity: sha512-aS3/DG0oM05K0RIXXP+hKjinGG5IgSSVGzswZxW3O0sS3pH4/fycXundUC9XsszgKCk4gHXylTEK6hyFxVxnoQ==} @@ -691,6 +1074,9 @@ packages: '@types/supertest@7.2.1': resolution: {integrity: sha512-4CbBvoYVLHL7+yhbYrZET0vsvuyXTC05aRe7dNQkwMzm56auceoy6Yu3K50uZmwfHna1os3CMSgM/3QVkUtPTw==} + '@types/validator@13.15.10': + resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} + '@vitest/coverage-v8@4.1.11': resolution: {integrity: sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==} peerDependencies: @@ -785,6 +1171,10 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + bcryptjs@3.0.3: + resolution: {integrity: sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==} + hasBin: true + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -796,6 +1186,9 @@ packages: resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} engines: {node: 20 || >=22} + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -810,6 +1203,14 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + c12@3.1.0: + resolution: {integrity: sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==} + peerDependencies: + magicast: ^0.3.5 + peerDependenciesMeta: + magicast: + optional: true + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -840,10 +1241,26 @@ packages: chardet@2.2.0: resolution: {integrity: sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==} + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + chokidar@5.0.0: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} + citty@0.1.6: + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + + citty@0.2.2: + resolution: {integrity: sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==} + + class-transformer@0.5.1: + resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} + + class-validator@0.15.1: + resolution: {integrity: sha512-LqoS80HBBSCVhz/3KloUly0ovokxpdOLR++Al3J3+dHXWt9sTKlKd4eYtoxhxyUjoe5+UcIM+5k9MIxyBWnRTw==} + cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} @@ -906,6 +1323,16 @@ packages: component-emitter@1.3.1: resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + + confbox@0.3.1: + resolution: {integrity: sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + content-disposition@1.1.0: resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} engines: {node: '>=18'} @@ -945,9 +1372,16 @@ packages: supports-color: optional: true + deepmerge-ts@7.1.5: + resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} + engines: {node: '>=16.0.0'} + defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -956,6 +1390,9 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -963,19 +1400,38 @@ packages: dezalgo@1.0.4: resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dotenv@18.0.6: + resolution: {integrity: sha512-ZMVmohV6CvAJtj++pZNyggluUt6hbRdQAxc2+Z635RL6/AG/ruXv5lHnybUFoxMPL/yrTL/tsF4paZu2l7RA6Q==} + engines: {node: '>=12'} + hasBin: true + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + effect@3.21.0: + resolution: {integrity: sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} emojilib@2.4.0: resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==} + empathic@2.0.0: + resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} + engines: {node: '>=14'} + encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} @@ -999,6 +1455,11 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} @@ -1026,10 +1487,17 @@ packages: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} + exsolve@1.1.3: + resolution: {integrity: sha512-kuzL9zherLQoqDeQaS55AiiIg1kb4KYXaSusTtgZTPlvKP2gGCI82LNmBNh5RjDVUXIcmgx+/Mx7vPwRzBbhJA==} + external-editor@3.1.0: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} + fast-check@3.23.2: + resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} + engines: {node: '>=8.0.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1105,6 +1573,10 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + giget@2.0.0: + resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} + hasBin: true + globrex@0.1.2: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} @@ -1196,6 +1668,10 @@ packages: resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} engines: {node: '>=6'} + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + js-tokens@10.0.0: resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} @@ -1210,6 +1686,19 @@ packages: jsonc-parser@3.3.1: resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + + libphonenumber-js@1.13.15: + resolution: {integrity: sha512-AHdhEQeDFEJ4GKDAQ9AwxChy+v2kUILUB3TGQRpkXMkWePjXTU/WQEzWVQoIx/Gz8fE73Wr5SEuZCy2vk8jrMQ==} + lightningcss-android-arm64@1.33.0: resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} engines: {node: '>= 12.0.0'} @@ -1288,6 +1777,27 @@ packages: resolution: {integrity: sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==} engines: {node: '>=13.2.0'} + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash@4.18.1: resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} @@ -1399,6 +1909,14 @@ packages: resolution: {integrity: sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==} engines: {node: '>=18'} + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + + nypm@0.6.10: + resolution: {integrity: sha512-W72Hrj1petq+b3Hk2aAC+9zswetlIFTnDW4s5djseZh2nYqBbyQLOtj472HwcbcWykPBUW1WpWpjOd4nK6gMRw==} + engines: {node: '>=18'} + hasBin: true + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -1411,6 +1929,9 @@ packages: resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==} engines: {node: '>=12.20.0'} + ohash@2.0.12: + resolution: {integrity: sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -1465,6 +1986,9 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1472,6 +1996,9 @@ packages: resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} engines: {node: '>=12'} + pkg-types@2.3.3: + resolution: {integrity: sha512-j/lCFdcppV0JxWpCEITdbDltBxPP6cHT+yNJ6Go2OgoSA9518X847X9z0p6LtA4Nc16+eQzCZjRrWanTGvHJ5w==} + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -1485,10 +2012,23 @@ packages: engines: {node: '>=14'} hasBin: true + prisma@6.19.3: + resolution: {integrity: sha512-++ZJ0ijLrDJF6hNB4t4uxg2br3fC4H9Yc9tcbjr2fcNFP3rh/SBNrAgjhsqBU4Ght8JPrVofG/ZkXfnSfnYsFg==} + engines: {node: '>=18.18'} + hasBin: true + peerDependencies: + typescript: '>=5.1.0' + peerDependenciesMeta: + typescript: + optional: true + proxy-addr@2.0.8: resolution: {integrity: sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==} engines: {node: '>= 0.10'} + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + qs@6.16.0: resolution: {integrity: sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==} engines: {node: '>=0.6'} @@ -1501,10 +2041,17 @@ packages: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} + rc9@2.1.2: + resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} + readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + readdirp@5.1.1: resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} engines: {node: '>= 20.19.0'} @@ -1754,6 +2301,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + validator@13.15.35: + resolution: {integrity: sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==} + engines: {node: '>= 0.10'} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -1865,6 +2416,11 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + yaml@2.9.1: + resolution: {integrity: sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==} + engines: {node: '>= 14.6'} + hasBin: true + yoctocolors@2.2.0: resolution: {integrity: sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==} engines: {node: '>=18'} @@ -1921,6 +2477,84 @@ snapshots: '@colors/colors@1.5.0': optional: true + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + '@inquirer/ansi@2.0.9': {} '@inquirer/checkbox@5.2.6(@types/node@24.19.1)': @@ -2072,7 +2706,7 @@ snapshots: - '@types/node' - prettier - '@nestjs/common@12.1.2(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0)': + '@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)': dependencies: '@standard-schema/spec': 1.1.0 file-type: 22.1.1(supports-color@7.2.0) @@ -2082,12 +2716,15 @@ snapshots: rxjs: 7.8.2 tslib: 2.8.1 uid: 2.0.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.15.1 transitivePeerDependencies: - supports-color - '@nestjs/core@12.1.2(@nestjs/common@12.1.2(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/platform-express@12.1.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/core@12.1.2(@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/platform-express@12.1.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 12.1.2(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0) + '@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) fast-safe-stringify: 2.1.1 iterare: 1.2.1 path-to-regexp: 8.4.2 @@ -2096,7 +2733,13 @@ snapshots: tslib: 2.8.1 uid: 2.0.2 optionalDependencies: - '@nestjs/platform-express': 12.1.2(@nestjs/common@12.1.2(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)(supports-color@7.2.0) + '@nestjs/platform-express': 12.1.2(@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)(supports-color@7.2.0) + + '@nestjs/jwt@12.0.2(@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))': + dependencies: + '@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) + '@types/jsonwebtoken': 9.0.10 + jsonwebtoken: 9.0.3 '@nestjs/mau@0.2.8': dependencies: @@ -2108,10 +2751,10 @@ snapshots: ora: 5.4.1 undici: 6.20.1 - '@nestjs/platform-express@12.1.2(@nestjs/common@12.1.2(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)(supports-color@7.2.0)': + '@nestjs/platform-express@12.1.2(@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)(supports-color@7.2.0)': dependencies: - '@nestjs/common': 12.1.2(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0) - '@nestjs/core': 12.1.2(@nestjs/common@12.1.2(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/platform-express@12.1.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@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(@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/platform-express@12.1.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) cors: 2.8.6 express: 5.2.1(supports-color@7.2.0) multer: 2.4.0 @@ -2133,13 +2776,13 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/testing@12.1.2(@nestjs/common@12.1.2(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)(@nestjs/platform-express@12.1.2)': + '@nestjs/testing@12.1.2(@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)(@nestjs/platform-express@12.1.2)': dependencies: - '@nestjs/common': 12.1.2(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0) - '@nestjs/core': 12.1.2(@nestjs/common@12.1.2(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/platform-express@12.1.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@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(@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/platform-express@12.1.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 optionalDependencies: - '@nestjs/platform-express': 12.1.2(@nestjs/common@12.1.2(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)(supports-color@7.2.0) + '@nestjs/platform-express': 12.1.2(@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)(supports-color@7.2.0) '@noble/hashes@1.8.0': {} @@ -2224,6 +2867,41 @@ snapshots: dependencies: '@noble/hashes': 1.8.0 + '@prisma/client@6.19.3(prisma@6.19.3(typescript@6.0.3))(typescript@6.0.3)': + optionalDependencies: + prisma: 6.19.3(typescript@6.0.3) + typescript: 6.0.3 + + '@prisma/config@6.19.3': + dependencies: + c12: 3.1.0 + deepmerge-ts: 7.1.5 + effect: 3.21.0 + empathic: 2.0.0 + transitivePeerDependencies: + - magicast + + '@prisma/debug@6.19.3': {} + + '@prisma/engines-version@7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7': {} + + '@prisma/engines@6.19.3': + dependencies: + '@prisma/debug': 6.19.3 + '@prisma/engines-version': 7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7 + '@prisma/fetch-engine': 6.19.3 + '@prisma/get-platform': 6.19.3 + + '@prisma/fetch-engine@6.19.3': + dependencies: + '@prisma/debug': 6.19.3 + '@prisma/engines-version': 7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7 + '@prisma/get-platform': 6.19.3 + + '@prisma/get-platform@6.19.3': + dependencies: + '@prisma/debug': 6.19.3 + '@rolldown/binding-android-arm-eabi@1.2.13': optional: true @@ -2284,6 +2962,10 @@ snapshots: '@tokenizer/token@0.3.0': {} + '@types/bcryptjs@3.0.0': + dependencies: + bcryptjs: 3.0.3 + '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 @@ -2319,8 +3001,15 @@ snapshots: '@types/http-errors@2.0.5': {} + '@types/jsonwebtoken@9.0.10': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 24.19.1 + '@types/methods@1.1.4': {} + '@types/ms@2.1.0': {} + '@types/node@24.19.1': dependencies: undici-types: 7.24.6 @@ -2350,6 +3039,8 @@ snapshots: '@types/methods': 1.1.4 '@types/superagent': 8.1.11 + '@types/validator@13.15.10': {} + '@vitest/coverage-v8@4.1.11(vitest@4.1.11)': dependencies: '@bcoe/v8-coverage': 1.0.2 @@ -2362,7 +3053,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)) + 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: @@ -2373,13 +3064,13 @@ snapshots: chai: 6.3.0 tinyrainbow: 3.2.0 - '@vitest/mocker@4.1.11(vite@8.3.4(@types/node@24.19.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) + 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: @@ -2455,6 +3146,8 @@ snapshots: base64-js@1.5.1: {} + bcryptjs@3.0.3: {} + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -2479,6 +3172,8 @@ snapshots: dependencies: balanced-match: 4.0.4 + buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} buffer@5.7.1: @@ -2492,6 +3187,21 @@ snapshots: bytes@3.1.2: {} + c12@3.1.0: + dependencies: + chokidar: 4.0.3 + confbox: 0.2.4 + defu: 6.1.7 + dotenv: 16.6.1 + exsolve: 1.1.3 + giget: 2.0.0 + jiti: 2.7.0 + ohash: 2.0.12 + pathe: 2.0.3 + perfect-debounce: 1.0.0 + pkg-types: 2.3.3 + rc9: 2.1.2 + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -2517,10 +3227,28 @@ snapshots: chardet@2.2.0: {} + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + chokidar@5.0.0: dependencies: readdirp: 5.1.1 + citty@0.1.6: + dependencies: + consola: 3.4.2 + + citty@0.2.2: {} + + class-transformer@0.5.1: {} + + class-validator@0.15.1: + dependencies: + '@types/validator': 13.15.10 + libphonenumber-js: 1.13.15 + validator: 13.15.35 + cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0 @@ -2570,6 +3298,12 @@ snapshots: component-emitter@1.3.1: {} + confbox@0.2.4: {} + + confbox@0.3.1: {} + + consola@3.4.2: {} + content-disposition@1.1.0: {} content-type@1.0.5: {} @@ -2595,14 +3329,20 @@ snapshots: optionalDependencies: supports-color: 7.2.0 + deepmerge-ts@7.1.5: {} + defaults@1.0.4: dependencies: clone: 1.0.4 + defu@6.1.7: {} + delayed-stream@1.0.0: {} depd@2.0.0: {} + destr@2.0.5: {} + detect-libc@2.1.2: {} dezalgo@1.0.4: @@ -2610,18 +3350,33 @@ snapshots: asap: 2.0.6 wrappy: 1.0.2 + dotenv@16.6.1: {} + + dotenv@18.0.6: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 es-errors: 1.3.0 gopd: 1.2.0 + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + ee-first@1.1.1: {} + effect@3.21.0: + dependencies: + '@standard-schema/spec': 1.1.0 + fast-check: 3.23.2 + emoji-regex@8.0.0: {} emojilib@2.4.0: {} + empathic@2.0.0: {} + encodeurl@2.0.0: {} es-define-property@1.0.1: {} @@ -2641,6 +3396,36 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.4 + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@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: {} escape-string-regexp@1.0.5: {} @@ -2688,12 +3473,18 @@ snapshots: transitivePeerDependencies: - supports-color + exsolve@1.1.3: {} + external-editor@3.1.0: dependencies: chardet: 0.7.0 iconv-lite: 0.4.24 tmp: 0.0.33 + fast-check@3.23.2: + dependencies: + pure-rand: 6.1.0 + fast-deep-equal@3.1.3: {} fast-safe-stringify@2.1.1: {} @@ -2781,6 +3572,15 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.2 + giget@2.0.0: + dependencies: + citty: 0.1.6 + consola: 3.4.2 + defu: 6.1.7 + node-fetch-native: 1.6.7 + nypm: 0.6.10 + pathe: 2.0.3 + globrex@0.1.2: {} gopd@1.2.0: {} @@ -2866,6 +3666,8 @@ snapshots: iterare@1.2.1: {} + jiti@2.7.0: {} + js-tokens@10.0.0: {} json-schema-traverse@1.0.0: {} @@ -2874,6 +3676,32 @@ snapshots: jsonc-parser@3.3.1: {} + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.8.5 + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + + libphonenumber-js@1.13.15: {} + lightningcss-android-arm64@1.33.0: optional: true @@ -2925,6 +3753,20 @@ snapshots: load-esm@1.0.3: {} + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.once@4.1.1: {} + lodash@4.18.1: {} log-symbols@4.1.0: @@ -3021,12 +3863,22 @@ snapshots: emojilib: 2.4.0 skin-tone: 2.0.0 + node-fetch-native@1.6.7: {} + + nypm@0.6.10: + dependencies: + citty: 0.2.2 + pathe: 2.0.3 + tinyexec: 1.3.1 + object-assign@4.1.1: {} object-inspect@1.13.4: {} obug@2.2.1: {} + ohash@2.0.12: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -3106,10 +3958,18 @@ snapshots: pathe@2.0.3: {} + perfect-debounce@1.0.0: {} + picocolors@1.1.1: {} picomatch@4.0.7: {} + pkg-types@2.3.3: + dependencies: + confbox: 0.3.1 + exsolve: 1.1.3 + pathe: 2.0.3 + pluralize@8.0.0: {} postcss@8.5.29: @@ -3120,11 +3980,22 @@ snapshots: prettier@3.9.9: {} + prisma@6.19.3(typescript@6.0.3): + dependencies: + '@prisma/config': 6.19.3 + '@prisma/engines': 6.19.3 + optionalDependencies: + typescript: 6.0.3 + transitivePeerDependencies: + - magicast + proxy-addr@2.0.8: dependencies: forwarded: 0.2.0 ipaddr.js: 1.9.1 + pure-rand@6.1.0: {} + qs@6.16.0: dependencies: es-define-property: 1.0.1 @@ -3139,12 +4010,19 @@ snapshots: iconv-lite: 0.7.3 unpipe: 1.0.0 + rc9@2.1.2: + dependencies: + defu: 6.1.7 + destr: 2.0.5 + readable-stream@3.6.2: dependencies: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 + readdirp@4.1.2: {} + readdirp@5.1.1: {} reflect-metadata@0.2.2: {} @@ -3413,20 +4291,22 @@ snapshots: util-deprecate@1.0.2: {} + validator@13.15.35: {} + 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)): + 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) + 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): + 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 @@ -3435,12 +4315,15 @@ snapshots: tinyglobby: 0.2.17 optionalDependencies: '@types/node': 24.19.1 + esbuild: 0.28.2 fsevents: 2.3.3 + jiti: 2.7.0 + 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)): + 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)) + '@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 @@ -3457,7 +4340,7 @@ snapshots: tinyexec: 1.3.1 tinyglobby: 0.2.17 tinyrainbow: 3.2.0 - vite: 8.3.4(@types/node@24.19.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 @@ -3482,4 +4365,7 @@ snapshots: wrappy@1.0.2: {} + yaml@2.9.1: + optional: true + yoctocolors@2.2.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..1448601 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,5 @@ +allowBuilds: + '@prisma/client': true + '@prisma/engines': true + esbuild: true + prisma: true diff --git a/prisma/migrations/20261010073709_init/migration.sql b/prisma/migrations/20261010073709_init/migration.sql new file mode 100644 index 0000000..f8fa77d --- /dev/null +++ b/prisma/migrations/20261010073709_init/migration.sql @@ -0,0 +1,247 @@ +-- CreateTable +CREATE TABLE `sys_tenant` ( + `id` VARCHAR(36) NOT NULL, + `code` VARCHAR(64) NOT NULL, + `name` VARCHAR(128) NOT NULL, + `status` VARCHAR(16) NOT NULL DEFAULT 'enabled', + `remark` VARCHAR(255) NULL, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `created_by` VARCHAR(36) NULL, + `updated_at` DATETIME(3) NOT NULL, + + UNIQUE INDEX `sys_tenant_code_key`(`code`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `sys_user` ( + `id` VARCHAR(36) NOT NULL, + `tenant_code` VARCHAR(64) NOT NULL, + `username` VARCHAR(64) NOT NULL, + `password_hash` VARCHAR(255) NOT NULL, + `nickname` VARCHAR(64) NULL, + `email` VARCHAR(128) NULL, + `phone` VARCHAR(32) NULL, + `avatar` VARCHAR(255) NULL, + `status` VARCHAR(16) NOT NULL DEFAULT 'enabled', + `org_code` VARCHAR(64) NULL, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `created_by` VARCHAR(36) NULL, + `updated_at` DATETIME(3) NOT NULL, + `deleted_at` DATETIME(3) NULL, + + INDEX `sys_user_tenant_code_idx`(`tenant_code`), + INDEX `sys_user_org_code_idx`(`org_code`), + UNIQUE INDEX `sys_user_tenant_code_username_key`(`tenant_code`, `username`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `sys_role` ( + `id` VARCHAR(36) NOT NULL, + `tenant_code` VARCHAR(64) NOT NULL, + `code` VARCHAR(64) NOT NULL, + `name` VARCHAR(64) NOT NULL, + `data_scope` VARCHAR(16) NOT NULL DEFAULT 'all', + `status` VARCHAR(16) NOT NULL DEFAULT 'enabled', + `remark` VARCHAR(255) NULL, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `created_by` VARCHAR(36) NULL, + `updated_at` DATETIME(3) NOT NULL, + `deleted_at` DATETIME(3) NULL, + + INDEX `sys_role_tenant_code_idx`(`tenant_code`), + UNIQUE INDEX `sys_role_tenant_code_code_key`(`tenant_code`, `code`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `sys_resource` ( + `id` VARCHAR(36) NOT NULL, + `tenant_code` VARCHAR(64) NOT NULL, + `parent_code` VARCHAR(64) NULL, + `type` VARCHAR(16) NOT NULL, + `code` VARCHAR(128) NOT NULL, + `name` VARCHAR(64) NOT NULL, + `path` VARCHAR(255) NULL, + `component` VARCHAR(255) NULL, + `method` VARCHAR(8) NULL, + `api_path` VARCHAR(255) NULL, + `icon` VARCHAR(64) NULL, + `sort` INTEGER NOT NULL DEFAULT 0, + `status` VARCHAR(16) NOT NULL DEFAULT 'enabled', + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `created_by` VARCHAR(36) NULL, + `updated_at` DATETIME(3) NOT NULL, + `deleted_at` DATETIME(3) NULL, + + INDEX `sys_resource_tenant_code_idx`(`tenant_code`), + INDEX `sys_resource_parent_code_idx`(`parent_code`), + UNIQUE INDEX `sys_resource_tenant_code_code_key`(`tenant_code`, `code`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `sys_org` ( + `id` VARCHAR(36) NOT NULL, + `tenant_code` VARCHAR(64) NOT NULL, + `parent_code` VARCHAR(64) NULL, + `code` VARCHAR(64) NOT NULL, + `name` VARCHAR(64) NOT NULL, + `leader` VARCHAR(64) NULL, + `sort` INTEGER NOT NULL DEFAULT 0, + `status` VARCHAR(16) NOT NULL DEFAULT 'enabled', + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `created_by` VARCHAR(36) NULL, + `updated_at` DATETIME(3) NOT NULL, + `deleted_at` DATETIME(3) NULL, + + INDEX `sys_org_tenant_code_idx`(`tenant_code`), + INDEX `sys_org_parent_code_idx`(`parent_code`), + UNIQUE INDEX `sys_org_tenant_code_code_key`(`tenant_code`, `code`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `sys_dict_type` ( + `id` VARCHAR(36) NOT NULL, + `tenant_code` VARCHAR(64) NOT NULL, + `code` VARCHAR(64) NOT NULL, + `name` VARCHAR(64) NOT NULL, + `status` VARCHAR(16) NOT NULL DEFAULT 'enabled', + `remark` VARCHAR(255) NULL, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `created_by` VARCHAR(36) NULL, + `updated_at` DATETIME(3) NOT NULL, + `deleted_at` DATETIME(3) NULL, + + INDEX `sys_dict_type_tenant_code_idx`(`tenant_code`), + UNIQUE INDEX `sys_dict_type_tenant_code_code_key`(`tenant_code`, `code`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `sys_dict_item` ( + `id` VARCHAR(36) NOT NULL, + `tenant_code` VARCHAR(64) NOT NULL, + `dict_type_code` VARCHAR(64) NOT NULL, + `label` VARCHAR(64) NOT NULL, + `value` VARCHAR(64) NOT NULL, + `sort` INTEGER NOT NULL DEFAULT 0, + `status` VARCHAR(16) NOT NULL DEFAULT 'enabled', + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `created_by` VARCHAR(36) NULL, + `updated_at` DATETIME(3) NOT NULL, + `deleted_at` DATETIME(3) NULL, + + INDEX `sys_dict_item_tenant_code_dict_type_code_idx`(`tenant_code`, `dict_type_code`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `sys_config` ( + `id` VARCHAR(36) NOT NULL, + `tenant_code` VARCHAR(64) NOT NULL, + `key` VARCHAR(128) NOT NULL, + `value` TEXT NULL, + `remark` VARCHAR(255) NULL, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `created_by` VARCHAR(36) NULL, + `updated_at` DATETIME(3) NOT NULL, + `deleted_at` DATETIME(3) NULL, + + UNIQUE INDEX `sys_config_tenant_code_key_key`(`tenant_code`, `key`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `sys_audit_log` ( + `id` VARCHAR(36) NOT NULL, + `tenant_code` VARCHAR(64) NOT NULL, + `user_id` VARCHAR(36) NULL, + `username` VARCHAR(64) NULL, + `module` VARCHAR(64) NULL, + `action` VARCHAR(64) NULL, + `method` VARCHAR(8) NULL, + `path` VARCHAR(255) NULL, + `params` TEXT NULL, + `result` TEXT NULL, + `status` VARCHAR(16) NULL, + `error_message` TEXT NULL, + `ip` VARCHAR(64) NULL, + `user_agent` VARCHAR(255) NULL, + `duration_ms` INTEGER NULL, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + + INDEX `sys_audit_log_tenant_code_created_at_idx`(`tenant_code`, `created_at`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `sys_login_log` ( + `id` VARCHAR(36) NOT NULL, + `tenant_code` VARCHAR(64) NOT NULL, + `username` VARCHAR(64) NULL, + `ip` VARCHAR(64) NULL, + `user_agent` VARCHAR(255) NULL, + `status` VARCHAR(16) NOT NULL DEFAULT 'success', + `message` VARCHAR(255) NULL, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + + INDEX `sys_login_log_tenant_code_created_at_idx`(`tenant_code`, `created_at`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `sys_refresh_token` ( + `id` VARCHAR(36) NOT NULL, + `tenant_code` VARCHAR(64) NOT NULL, + `user_id` VARCHAR(36) NOT NULL, + `jti` VARCHAR(64) NOT NULL, + `token_hash` VARCHAR(255) NOT NULL, + `ip` VARCHAR(64) NULL, + `user_agent` VARCHAR(255) NULL, + `login_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `expire_at` DATETIME(3) NOT NULL, + `revoked_at` DATETIME(3) NULL, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + + UNIQUE INDEX `sys_refresh_token_jti_key`(`jti`), + INDEX `sys_refresh_token_tenant_code_user_id_idx`(`tenant_code`, `user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `sys_user_role` ( + `id` VARCHAR(36) NOT NULL, + `tenant_code` VARCHAR(64) NOT NULL, + `user_id` VARCHAR(36) NOT NULL, + `role_code` VARCHAR(64) NOT NULL, + + INDEX `sys_user_role_tenant_code_user_id_idx`(`tenant_code`, `user_id`), + UNIQUE INDEX `sys_user_role_tenant_code_user_id_role_code_key`(`tenant_code`, `user_id`, `role_code`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `sys_role_resource` ( + `id` VARCHAR(36) NOT NULL, + `tenant_code` VARCHAR(64) NOT NULL, + `role_code` VARCHAR(64) NOT NULL, + `resource_code` VARCHAR(128) NOT NULL, + + INDEX `sys_role_resource_tenant_code_role_code_idx`(`tenant_code`, `role_code`), + UNIQUE INDEX `sys_role_resource_tenant_code_role_code_resource_code_key`(`tenant_code`, `role_code`, `resource_code`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `sys_role_org` ( + `id` VARCHAR(36) NOT NULL, + `tenant_code` VARCHAR(64) NOT NULL, + `role_code` VARCHAR(64) NOT NULL, + `org_code` VARCHAR(64) NOT NULL, + + UNIQUE INDEX `sys_role_org_tenant_code_role_code_org_code_key`(`tenant_code`, `role_code`, `org_code`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..592fc0b --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "mysql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..daae274 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,257 @@ +// 通用系统管理脚手架 (sysmgr) 数据模型 +// 约定:表名 sys_ 前缀;列 snake_case(模型 camelCase + @map);业务键关联(角色/资源/组织/字典/租户用 code,用户用 id)。 + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "mysql" + url = env("DATABASE_URL") +} + +/// 租户 +model Tenant { + id String @id @default(uuid(7)) @db.VarChar(36) + code String @unique @db.VarChar(64) + name String @db.VarChar(128) + status String @default("enabled") @db.VarChar(16) + remark String? @db.VarChar(255) + createdAt DateTime @default(now()) @map("created_at") + createdBy String? @map("created_by") @db.VarChar(36) + updatedAt DateTime @updatedAt @map("updated_at") + + @@map("sys_tenant") +} + +/// 用户 +model User { + id String @id @default(uuid(7)) @db.VarChar(36) + tenantCode String @map("tenant_code") @db.VarChar(64) + username String @db.VarChar(64) + passwordHash String @map("password_hash") @db.VarChar(255) + nickname String? @db.VarChar(64) + email String? @db.VarChar(128) + phone String? @db.VarChar(32) + avatar String? @db.VarChar(255) + status String @default("enabled") @db.VarChar(16) + orgCode String? @map("org_code") @db.VarChar(64) + createdAt DateTime @default(now()) @map("created_at") + createdBy String? @map("created_by") @db.VarChar(36) + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + @@unique([tenantCode, username]) + @@index([tenantCode]) + @@index([orgCode]) + @@map("sys_user") +} + +/// 角色 +model Role { + id String @id @default(uuid(7)) @db.VarChar(36) + tenantCode String @map("tenant_code") @db.VarChar(64) + code String @db.VarChar(64) + name String @db.VarChar(64) + dataScope String @default("all") @map("data_scope") @db.VarChar(16) + status String @default("enabled") @db.VarChar(16) + remark String? @db.VarChar(255) + createdAt DateTime @default(now()) @map("created_at") + createdBy String? @map("created_by") @db.VarChar(36) + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + @@unique([tenantCode, code]) + @@index([tenantCode]) + @@map("sys_role") +} + +/// 资源(菜单/按钮/接口) +model Resource { + id String @id @default(uuid(7)) @db.VarChar(36) + tenantCode String @map("tenant_code") @db.VarChar(64) + parentCode String? @map("parent_code") @db.VarChar(64) + type String @db.VarChar(16) + code String @db.VarChar(128) + name String @db.VarChar(64) + path String? @db.VarChar(255) + component String? @db.VarChar(255) + method String? @db.VarChar(8) + apiPath String? @map("api_path") @db.VarChar(255) + icon String? @db.VarChar(64) + sort Int @default(0) + status String @default("enabled") @db.VarChar(16) + createdAt DateTime @default(now()) @map("created_at") + createdBy String? @map("created_by") @db.VarChar(36) + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + @@unique([tenantCode, code]) + @@index([tenantCode]) + @@index([parentCode]) + @@map("sys_resource") +} + +/// 组织 +model Org { + id String @id @default(uuid(7)) @db.VarChar(36) + tenantCode String @map("tenant_code") @db.VarChar(64) + parentCode String? @map("parent_code") @db.VarChar(64) + code String @db.VarChar(64) + name String @db.VarChar(64) + leader String? @db.VarChar(64) + sort Int @default(0) + status String @default("enabled") @db.VarChar(16) + createdAt DateTime @default(now()) @map("created_at") + createdBy String? @map("created_by") @db.VarChar(36) + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + @@unique([tenantCode, code]) + @@index([tenantCode]) + @@index([parentCode]) + @@map("sys_org") +} + +/// 字典类型 +model DictType { + id String @id @default(uuid(7)) @db.VarChar(36) + tenantCode String @map("tenant_code") @db.VarChar(64) + code String @db.VarChar(64) + name String @db.VarChar(64) + status String @default("enabled") @db.VarChar(16) + remark String? @db.VarChar(255) + createdAt DateTime @default(now()) @map("created_at") + createdBy String? @map("created_by") @db.VarChar(36) + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + @@unique([tenantCode, code]) + @@index([tenantCode]) + @@map("sys_dict_type") +} + +/// 字典项 +model DictItem { + id String @id @default(uuid(7)) @db.VarChar(36) + tenantCode String @map("tenant_code") @db.VarChar(64) + dictTypeCode String @map("dict_type_code") @db.VarChar(64) + label String @db.VarChar(64) + value String @db.VarChar(64) + sort Int @default(0) + status String @default("enabled") @db.VarChar(16) + createdAt DateTime @default(now()) @map("created_at") + createdBy String? @map("created_by") @db.VarChar(36) + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + @@index([tenantCode, dictTypeCode]) + @@map("sys_dict_item") +} + +/// 参数配置 +model Config { + id String @id @default(uuid(7)) @db.VarChar(36) + tenantCode String @map("tenant_code") @db.VarChar(64) + key String @db.VarChar(128) + value String? @db.Text + remark String? @db.VarChar(255) + createdAt DateTime @default(now()) @map("created_at") + createdBy String? @map("created_by") @db.VarChar(36) + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + @@unique([tenantCode, key]) + @@map("sys_config") +} + +/// 操作日志(只增,硬删) +model AuditLog { + id String @id @default(uuid(7)) @db.VarChar(36) + tenantCode String @map("tenant_code") @db.VarChar(64) + userId String? @map("user_id") @db.VarChar(36) + username String? @db.VarChar(64) + module String? @db.VarChar(64) + action String? @db.VarChar(64) + method String? @db.VarChar(8) + path String? @db.VarChar(255) + params String? @db.Text + result String? @db.Text + status String? @db.VarChar(16) + errorMessage String? @map("error_message") @db.Text + ip String? @db.VarChar(64) + userAgent String? @map("user_agent") @db.VarChar(255) + durationMs Int? @map("duration_ms") + createdAt DateTime @default(now()) @map("created_at") + + @@index([tenantCode, createdAt]) + @@map("sys_audit_log") +} + +/// 登录日志(只增,硬删) +model LoginLog { + id String @id @default(uuid(7)) @db.VarChar(36) + tenantCode String @map("tenant_code") @db.VarChar(64) + username String? @db.VarChar(64) + ip String? @db.VarChar(64) + userAgent String? @map("user_agent") @db.VarChar(255) + status String @default("success") @db.VarChar(16) + message String? @db.VarChar(255) + createdAt DateTime @default(now()) @map("created_at") + + @@index([tenantCode, createdAt]) + @@map("sys_login_log") +} + +/// 刷新令牌(会话载体,硬删) +model RefreshToken { + id String @id @default(uuid(7)) @db.VarChar(36) + tenantCode String @map("tenant_code") @db.VarChar(64) + userId String @map("user_id") @db.VarChar(36) + jti String @unique @db.VarChar(64) + tokenHash String @map("token_hash") @db.VarChar(255) + ip String? @db.VarChar(64) + userAgent String? @map("user_agent") @db.VarChar(255) + loginAt DateTime @default(now()) @map("login_at") + expireAt DateTime @map("expire_at") + revokedAt DateTime? @map("revoked_at") + createdAt DateTime @default(now()) @map("created_at") + + @@index([tenantCode, userId]) + @@map("sys_refresh_token") +} + +/// 用户-角色关联 +model UserRole { + id String @id @default(uuid(7)) @db.VarChar(36) + tenantCode String @map("tenant_code") @db.VarChar(64) + userId String @map("user_id") @db.VarChar(36) + roleCode String @map("role_code") @db.VarChar(64) + + @@unique([tenantCode, userId, roleCode]) + @@index([tenantCode, userId]) + @@map("sys_user_role") +} + +/// 角色-资源关联 +model RoleResource { + id String @id @default(uuid(7)) @db.VarChar(36) + tenantCode String @map("tenant_code") @db.VarChar(64) + roleCode String @map("role_code") @db.VarChar(64) + resourceCode String @map("resource_code") @db.VarChar(128) + + @@unique([tenantCode, roleCode, resourceCode]) + @@index([tenantCode, roleCode]) + @@map("sys_role_resource") +} + +/// 角色-组织关联(数据权限「自定义」) +model RoleOrg { + id String @id @default(uuid(7)) @db.VarChar(36) + tenantCode String @map("tenant_code") @db.VarChar(64) + roleCode String @map("role_code") @db.VarChar(64) + orgCode String @map("org_code") @db.VarChar(64) + + @@unique([tenantCode, roleCode, orgCode]) + @@map("sys_role_org") +} diff --git a/src/app.module.ts b/src/app.module.ts index b4ca839..fcfe3a9 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,10 +1,44 @@ import { Module } from '@nestjs/common'; +import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'; +import { PrismaModule } from './prisma/prisma.module.js'; +import { AuthModule } from './auth/auth.module.js'; +import { JwtAuthGuard } from './auth/jwt-auth.guard.js'; +import { PermissionsGuard } from './auth/permissions.guard.js'; +import { RequestContextInterceptor } from './auth/request-context.interceptor.js'; +import { TransformInterceptor } from './common/interceptors/transform.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'; +import { RolesModule } from './roles/roles.module.js'; +import { ResourcesModule } from './resources/resources.module.js'; +import { OrgsModule } from './orgs/orgs.module.js'; +import { DictsModule } from './dicts/dicts.module.js'; +import { ConfigsModule } from './configs/configs.module.js'; +import { LogsModule } from './logs/logs.module.js'; import { AppController } from './app.controller.js'; import { AppService } from './app.service.js'; @Module({ - imports: [], + imports: [ + PrismaModule, + AuthModule, + TenantsModule, + UsersModule, + RolesModule, + ResourcesModule, + OrgsModule, + DictsModule, + ConfigsModule, + LogsModule, + ], controllers: [AppController], - providers: [AppService], + providers: [ + AppService, + { provide: APP_GUARD, useClass: JwtAuthGuard }, + { provide: APP_GUARD, useClass: PermissionsGuard }, + { provide: APP_INTERCEPTOR, useClass: RequestContextInterceptor }, + { provide: APP_INTERCEPTOR, useClass: TransformInterceptor }, + { provide: APP_FILTER, useClass: AllExceptionsFilter }, + ], }) export class AppModule {} diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts new file mode 100644 index 0000000..efe5745 --- /dev/null +++ b/src/auth/auth.controller.ts @@ -0,0 +1,27 @@ +import { Body, Controller, Post } from '@nestjs/common'; +import { AuthService } from './auth.service.js'; +import { LoginDto, RefreshDto } from './dto.js'; +import { Public } from './decorators.js'; + +@Controller('auth') +export class AuthController { + constructor(private readonly auth: AuthService) {} + + @Public() + @Post('login') + login(@Body() dto: LoginDto) { + return this.auth.login(dto); + } + + @Public() + @Post('refresh') + refresh(@Body() dto: RefreshDto) { + return this.auth.refresh(dto); + } + + @Public() + @Post('logout') + logout(@Body() dto: RefreshDto) { + return this.auth.logout(dto); + } +} diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts new file mode 100644 index 0000000..b5fb134 --- /dev/null +++ b/src/auth/auth.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common'; +import { JwtModule } from '@nestjs/jwt'; +import { AuthController } from './auth.controller.js'; +import { AuthService } from './auth.service.js'; + +@Module({ + imports: [ + JwtModule.register({ + global: true, + secret: process.env.JWT_SECRET ?? 'dev-secret-change-me', + signOptions: { expiresIn: Number(process.env.JWT_ACCESS_TTL ?? 7200) }, + }), + ], + controllers: [AuthController], + providers: [AuthService], + exports: [AuthService], +}) +export class AuthModule {} diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts new file mode 100644 index 0000000..786b202 --- /dev/null +++ b/src/auth/auth.service.ts @@ -0,0 +1,115 @@ +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import bcrypt from 'bcryptjs'; +import { createHash, randomUUID } from 'node:crypto'; +import { PrismaService } from '../prisma/prisma.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'; + +const REFRESH_TTL = Number(process.env.JWT_REFRESH_TTL ?? 604800); +const ACCESS_TTL = Number(process.env.JWT_ACCESS_TTL ?? 7200); + +@Injectable() +export class AuthService { + constructor( + private readonly prisma: PrismaService, + private readonly jwt: JwtService, + ) {} + + 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') { + throw new UnauthorizedException('租户不存在或已停用'); + } + const user = await this.prisma.client.user.findFirst({ + where: { tenantCode: dto.tenantCode, username: dto.username, deletedAt: null }, + }); + if (!user) throw new UnauthorizedException('账号或密码错误'); + if (user.status !== 'enabled') throw new UnauthorizedException('账号已停用或锁定'); + const ok = await bcrypt.compare(dto.password, user.passwordHash); + if (!ok) throw new UnauthorizedException('账号或密码错误'); + + return this.issue(dto.tenantCode, user.id, user.username); + } + + async refresh(dto: RefreshDto): Promise<{ accessToken: string; refreshToken: string; expiresIn: number }> { + const record = await this.prisma.client.refreshToken.findUnique({ where: { jti: dto.refreshToken } }); + if (!record || record.revokedAt || record.expireAt.getTime() < Date.now()) { + throw new UnauthorizedException('refresh token 无效或已过期'); + } + const user = await this.prisma.client.user.findFirst({ + where: { tenantCode: record.tenantCode, id: record.userId, deletedAt: null }, + }); + if (!user || user.status !== 'enabled') throw new UnauthorizedException('账号已失效'); + + // 旋转:作废旧 refresh + await this.prisma.client.refreshToken.update({ + where: { jti: dto.refreshToken }, + data: { revokedAt: new Date() }, + }); + return this.issue(record.tenantCode, user.id, user.username); + } + + async logout(dto: RefreshDto): Promise { + 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() }, + }); + } + return null; + } + + /** 由 RequestContextInterceptor 调用,构建当前请求的租户/用户/数据权限上下文。 */ + async buildContext(user: { userId: string; tenantCode: string; username: string }): Promise { + const u = await this.prisma.client.user.findFirst({ + where: { tenantCode: user.tenantCode, id: user.userId, deletedAt: null }, + select: { orgCode: true }, + }); + const orgCode = u?.orgCode ?? null; + + 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); + const roles = roleCodes.length + ? await this.prisma.client.role.findMany({ + where: { tenantCode: user.tenantCode, code: { in: roleCodes }, deletedAt: null }, + select: { dataScope: true }, + }) + : []; + 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 async issue( + tenantCode: string, + userId: string, + username: string, + ): Promise<{ accessToken: string; refreshToken: string; expiresIn: number }> { + const accessToken = this.jwt.sign({ sub: userId, tenantCode, username }); + const jti = randomUUID(); + await this.prisma.client.refreshToken.create({ + data: { + tenantCode, + userId, + jti, + tokenHash: createHash('sha256').update(jti).digest('hex'), + expireAt: new Date(Date.now() + REFRESH_TTL * 1000), + }, + }); + return { accessToken, refreshToken: jti, expiresIn: ACCESS_TTL }; + } +} diff --git a/src/auth/decorators.ts b/src/auth/decorators.ts new file mode 100644 index 0000000..967b638 --- /dev/null +++ b/src/auth/decorators.ts @@ -0,0 +1,13 @@ +import { createParamDecorator, ExecutionContext, SetMetadata } from '@nestjs/common'; + +export const IS_PUBLIC_KEY = 'isPublic'; +/** 标记接口为公开接口(跳过登录校验)。 */ +export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); + +export const PERMISSIONS_KEY = 'permissions'; +/** 要求当前用户具备给定权限标识(resource code)。 */ +export const RequirePermissions = (...permissions: string[]) => SetMetadata(PERMISSIONS_KEY, permissions); + +export const CurrentUser = createParamDecorator((_data: unknown, ctx: ExecutionContext) => { + return ctx.switchToHttp().getRequest().user; +}); diff --git a/src/auth/dto.ts b/src/auth/dto.ts new file mode 100644 index 0000000..207ccc0 --- /dev/null +++ b/src/auth/dto.ts @@ -0,0 +1,21 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class LoginDto { + @IsString() + @IsNotEmpty() + tenantCode!: string; + + @IsString() + @IsNotEmpty() + username!: string; + + @IsString() + @IsNotEmpty() + password!: string; +} + +export class RefreshDto { + @IsString() + @IsNotEmpty() + refreshToken!: string; +} diff --git a/src/auth/jwt-auth.guard.ts b/src/auth/jwt-auth.guard.ts new file mode 100644 index 0000000..82d8628 --- /dev/null +++ b/src/auth/jwt-auth.guard.ts @@ -0,0 +1,44 @@ +import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { JwtService } from '@nestjs/jwt'; +import { IS_PUBLIC_KEY } from './decorators.js'; + +export interface AuthUser { + userId: string; + tenantCode: string; + username: string; +} + +@Injectable() +export class JwtAuthGuard implements CanActivate { + constructor( + private readonly jwt: JwtService, + private readonly reflector: Reflector, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ + context.getHandler(), + context.getClass(), + ]); + if (isPublic) return true; + + const request = context.switchToHttp().getRequest(); + const token = this.extractToken(request); + if (!token) throw new UnauthorizedException('未登录'); + try { + const payload = await this.jwt.verifyAsync<{ sub: string; tenantCode: string; username: string }>(token); + request.user = { userId: payload.sub, tenantCode: payload.tenantCode, username: payload.username } satisfies AuthUser; + return true; + } catch { + throw new UnauthorizedException('token 无效或已过期'); + } + } + + private extractToken(request: { headers?: Record }): string | undefined { + const auth = request.headers?.authorization; + if (!auth) return undefined; + const [type, token] = auth.split(' '); + return type === 'Bearer' && token ? token : undefined; + } +} diff --git a/src/auth/permissions.guard.ts b/src/auth/permissions.guard.ts new file mode 100644 index 0000000..1b6f5e1 --- /dev/null +++ b/src/auth/permissions.guard.ts @@ -0,0 +1,37 @@ +import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { PrismaService } from '../prisma/prisma.service.js'; +import { PERMISSIONS_KEY } from './decorators.js'; + +@Injectable() +export class PermissionsGuard implements CanActivate { + constructor( + private readonly prisma: PrismaService, + private readonly reflector: Reflector, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const required = this.reflector.getAllAndOverride(PERMISSIONS_KEY, [ + context.getHandler(), + context.getClass(), + ]); + if (!required || required.length === 0) return true; + + const request = context.switchToHttp().getRequest(); + const user = request.user as { userId: string; tenantCode: string } | undefined; + if (!user) 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; + } +} diff --git a/src/auth/request-context.interceptor.ts b/src/auth/request-context.interceptor.ts new file mode 100644 index 0000000..b51005d --- /dev/null +++ b/src/auth/request-context.interceptor.ts @@ -0,0 +1,27 @@ +import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; +import { from, mergeMap, Observable } from 'rxjs'; +import { AuthService } from './auth.service.js'; +import { runWithRequestContext } from '../common/context/request-context.js'; + +/** + * 在请求处理前构建租户/用户/数据权限上下文,并写入 AsyncLocalStorage, + * 使 PrismaService 的过滤与业务查询能拿到当前请求上下文。 + */ +@Injectable() +export class RequestContextInterceptor implements NestInterceptor { + constructor(private readonly auth: AuthService) {} + + intercept(context: ExecutionContext, next: CallHandler): Observable { + const request = context.switchToHttp().getRequest(); + const user = request.user as { userId: string; tenantCode: string; username: string } | undefined; + if (!user) return next.handle(); + + return from(this.auth.buildContext(user)).pipe( + mergeMap((ctx) => + new Observable((subscriber) => { + runWithRequestContext(ctx, () => next.handle().subscribe(subscriber)); + }), + ), + ); + } +} diff --git a/src/common/base-crud.service.ts b/src/common/base-crud.service.ts new file mode 100644 index 0000000..502389c --- /dev/null +++ b/src/common/base-crud.service.ts @@ -0,0 +1,52 @@ +import { PrismaService } from '../prisma/prisma.service.js'; +import { getRequestContext } from './context/request-context.js'; + +/** + * 通用 CRUD 基类:封装分页/创建/更新/软删。 + * 创建时自动注入 tenantCode(Tenant 模型除外);软删由子类按需调用 softRemove。 + */ +export class BaseCrudService { + constructor( + protected readonly prisma: PrismaService, + private readonly modelName: string, + ) {} + + protected db(): any { + return (this.prisma.client as any)[this.modelName]; + } + + async paginate( + page = 1, + pageSize = 20, + where: Record = {}, + orderBy: Record = { createdAt: 'desc' }, + ) { + const [list, total] = await Promise.all([ + this.db().findMany({ where, skip: (page - 1) * pageSize, take: pageSize, orderBy }), + this.db().count({ where }), + ]); + return { list, total, page, pageSize }; + } + + async list(where: Record = {}, orderBy?: Record) { + return this.db().findMany({ where, orderBy }); + } + + async findById(id: string) { + return this.db().findUnique({ where: { id } }); + } + + async create(data: Record) { + const ctx = getRequestContext(); + if (ctx && this.modelName !== 'Tenant') data.tenantCode = ctx.tenantCode; + return this.db().create({ data }); + } + + async update(id: string, data: Record) { + return this.db().update({ where: { id }, data }); + } + + async softRemove(id: string) { + return this.db().update({ where: { id }, data: { deletedAt: new Date() } }); + } +} diff --git a/src/common/context/request-context.ts b/src/common/context/request-context.ts new file mode 100644 index 0000000..ae7c5b9 --- /dev/null +++ b/src/common/context/request-context.ts @@ -0,0 +1,23 @@ +import { AsyncLocalStorage } from 'node:async_hooks'; +import type { DataScope } from '../enums.js'; + +/** 每个请求的租户/用户/数据权限上下文,由 RequestContextInterceptor 写入。 */ +export interface RequestContext { + tenantCode: string; + userId: string; + username: string; + dataScope: DataScope; + orgCode: string | null; + /** 数据权限可见的部门 code 集合;空数组表示「全部可见」 */ + orgCodes: string[]; +} + +const storage = new AsyncLocalStorage(); + +export function getRequestContext(): RequestContext | undefined { + return storage.getStore(); +} + +export function runWithRequestContext(ctx: RequestContext, fn: () => T): T { + return storage.run(ctx, fn); +} diff --git a/src/common/data-scope.ts b/src/common/data-scope.ts new file mode 100644 index 0000000..58cfd56 --- /dev/null +++ b/src/common/data-scope.ts @@ -0,0 +1,63 @@ +import type { PrismaClient } from '@prisma/client'; +import { DataScope } from './enums.js'; + +/** 数据权限档位(从大到小)的优先级,用于多角色取最高权限。 */ +const RANK: Record = { + all: 5, + dept_and_child: 4, + dept: 3, + custom: 2, + self: 1, +}; + +/** 从多个角色的 data_scope 中取最高权限档。 */ +export function resolveDataScope(scopes: DataScope[]): DataScope { + if (scopes.length === 0) return 'self'; + return scopes.reduce((a, b) => (RANK[a] >= RANK[b] ? a : b)); +} + +/** + * 计算数据权限可见的部门 code 集合。 + * 返回空数组表示「全部可见」(不受数据权限限制)。 + */ +export async function resolveVisibleOrgs(opts: { + prisma: PrismaClient; + tenantCode: string; + dataScope: DataScope; + orgCode: string | null; + roleCodes: string[]; +}): Promise { + const { prisma, tenantCode, dataScope, orgCode, roleCodes } = opts; + switch (dataScope) { + case 'all': + return []; + case 'self': + case 'dept': + return orgCode ? [orgCode] : ['__none__']; + case 'dept_and_child': + return orgCode ? collectDescendants(prisma, tenantCode, orgCode) : ['__none__']; + case 'custom': { + const rows = await prisma.roleOrg.findMany({ + where: { tenantCode, roleCode: { in: roleCodes } }, + select: { orgCode: true }, + }); + const codes: string[] = [...new Set(rows.map((r) => r.orgCode))]; + return codes.length ? codes : ['__none__']; + } + } +} + +/** 收集某部门及其全部后代部门的 code(含自身)。 */ +async function collectDescendants(prisma: PrismaClient, tenantCode: string, rootCode: string): Promise { + const result: string[] = []; + let frontier = [rootCode]; + while (frontier.length > 0) { + result.push(...frontier); + const children = await prisma.org.findMany({ + where: { tenantCode, parentCode: { in: frontier }, deletedAt: null }, + select: { code: true }, + }); + frontier = children.map((c) => c.code); + } + return result; +} diff --git a/src/common/enums.ts b/src/common/enums.ts new file mode 100644 index 0000000..801ea35 --- /dev/null +++ b/src/common/enums.ts @@ -0,0 +1,39 @@ +/** + * 应用层枚举(VARCHAR 存储 + TS 常量校验)。 + * 取值与数据库列存储值一一对应,作为全项目唯一枚举源。 + */ + +export const CommonStatus = { + ENABLED: 'enabled', + DISABLED: 'disabled', +} as const; +export type CommonStatus = (typeof CommonStatus)[keyof typeof CommonStatus]; + +export const UserStatus = { + ENABLED: 'enabled', + DISABLED: 'disabled', + LOCKED: 'locked', +} as const; +export type UserStatus = (typeof UserStatus)[keyof typeof UserStatus]; + +export const ResourceType = { + MENU: 'menu', + BUTTON: 'button', + API: 'api', +} as const; +export type ResourceType = (typeof ResourceType)[keyof typeof ResourceType]; + +export const DataScope = { + ALL: 'all', + DEPT: 'dept', + DEPT_AND_CHILD: 'dept_and_child', + SELF: 'self', + CUSTOM: 'custom', +} as const; +export type DataScope = (typeof DataScope)[keyof typeof DataScope]; + +export const LoginStatus = { + SUCCESS: 'success', + FAIL: 'fail', +} as const; +export type LoginStatus = (typeof LoginStatus)[keyof typeof LoginStatus]; diff --git a/src/common/filters/http-exception.filter.ts b/src/common/filters/http-exception.filter.ts new file mode 100644 index 0000000..12263a1 --- /dev/null +++ b/src/common/filters/http-exception.filter.ts @@ -0,0 +1,28 @@ +import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common'; +import type { Response } from 'express'; + +/** + * 全局异常过滤器:错误码分段与 HTTP 状态对齐(400xx/401xx/403xx/404xx/409xx/500xx)。 + */ +@Catch() +export class AllExceptionsFilter implements ExceptionFilter { + catch(exception: unknown, host: ArgumentsHost): void { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + + const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; + const body = exception instanceof HttpException ? exception.getResponse() : null; + const message = + typeof body === 'string' + ? body + : Array.isArray((body as { message?: unknown })?.message) + ? ((body as { message: string[] }).message.join('; ')) + : ((body as { message?: string })?.message ?? (exception as Error).message ?? 'Internal server error'); + + response.status(status).json({ + code: status * 100, + message, + data: null, + }); + } +} diff --git a/src/common/interceptors/transform.interceptor.ts b/src/common/interceptors/transform.interceptor.ts new file mode 100644 index 0000000..b3c7837 --- /dev/null +++ b/src/common/interceptors/transform.interceptor.ts @@ -0,0 +1,13 @@ +import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +/** + * 统一响应封装:{ code, message, data },成功 code = 0。 + */ +@Injectable() +export class TransformInterceptor implements NestInterceptor { + intercept(_context: ExecutionContext, next: CallHandler): Observable { + return next.handle().pipe(map((data) => ({ code: 0, message: 'ok', data: data ?? null }))); + } +} diff --git a/src/common/page.dto.ts b/src/common/page.dto.ts new file mode 100644 index 0000000..420b108 --- /dev/null +++ b/src/common/page.dto.ts @@ -0,0 +1,21 @@ +import { Type } from 'class-transformer'; +import { IsInt, IsOptional, Min } from 'class-validator'; + +export class PageQuery { + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + page = 1; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + pageSize = 20; +} + +export class IdParam { + @IsOptional() + id!: string; +} diff --git a/src/configs/configs.controller.ts b/src/configs/configs.controller.ts new file mode 100644 index 0000000..17faac4 --- /dev/null +++ b/src/configs/configs.controller.ts @@ -0,0 +1,34 @@ +import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common'; +import { PageQuery } from '../common/page.dto.js'; +import { CreateConfigDto, UpdateConfigDto } from './dto.js'; +import { ConfigsService } from './configs.service.js'; + +@Controller('configs') +export class ConfigsController { + constructor(private readonly service: ConfigsService) {} + + @Get() + list(@Query() q: PageQuery) { + return this.service.paginate(q.page, q.pageSize); + } + + @Get(':id') + get(@Param('id') id: string) { + return this.service.findById(id); + } + + @Post() + create(@Body() dto: CreateConfigDto) { + return this.service.createConfig(dto); + } + + @Patch(':id') + update(@Param('id') id: string, @Body() dto: UpdateConfigDto) { + return this.service.updateConfig(id, dto); + } + + @Delete(':id') + remove(@Param('id') id: string) { + return this.service.softRemove(id); + } +} diff --git a/src/configs/configs.module.ts b/src/configs/configs.module.ts new file mode 100644 index 0000000..f8982d3 --- /dev/null +++ b/src/configs/configs.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { ConfigsController } from './configs.controller.js'; +import { ConfigsService } from './configs.service.js'; + +@Module({ + controllers: [ConfigsController], + providers: [ConfigsService], +}) +export class ConfigsModule {} diff --git a/src/configs/configs.service.ts b/src/configs/configs.service.ts new file mode 100644 index 0000000..b9dc0c6 --- /dev/null +++ b/src/configs/configs.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@nestjs/common'; +import { BaseCrudService } from '../common/base-crud.service.js'; +import { PrismaService } from '../prisma/prisma.service.js'; +import { CreateConfigDto, UpdateConfigDto } from './dto.js'; + +@Injectable() +export class ConfigsService extends BaseCrudService { + constructor(prisma: PrismaService) { + super(prisma, 'config'); + } + + createConfig(dto: CreateConfigDto) { + return this.create({ ...dto }); + } + + updateConfig(id: string, dto: UpdateConfigDto) { + return this.update(id, { ...dto }); + } +} diff --git a/src/configs/dto.ts b/src/configs/dto.ts new file mode 100644 index 0000000..61070f1 --- /dev/null +++ b/src/configs/dto.ts @@ -0,0 +1,12 @@ +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +export class CreateConfigDto { + @IsString() @IsNotEmpty() key!: string; + @IsOptional() @IsString() value?: string; + @IsOptional() @IsString() remark?: string; +} + +export class UpdateConfigDto { + @IsOptional() @IsString() value?: string; + @IsOptional() @IsString() remark?: string; +} diff --git a/src/dicts/dict-items.controller.ts b/src/dicts/dict-items.controller.ts new file mode 100644 index 0000000..517f2c3 --- /dev/null +++ b/src/dicts/dict-items.controller.ts @@ -0,0 +1,33 @@ +import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common'; +import { CreateDictItemDto, UpdateDictItemDto } from './dto.js'; +import { DictItemsService } from './dict-items.service.js'; + +@Controller('dict-items') +export class DictItemsController { + constructor(private readonly service: DictItemsService) {} + + @Get() + list(@Query('dictTypeCode') dictTypeCode?: string) { + return this.service.listByType(dictTypeCode); + } + + @Get(':id') + get(@Param('id') id: string) { + return this.service.findById(id); + } + + @Post() + create(@Body() dto: CreateDictItemDto) { + return this.service.createItem(dto); + } + + @Patch(':id') + update(@Param('id') id: string, @Body() dto: UpdateDictItemDto) { + return this.service.updateItem(id, dto); + } + + @Delete(':id') + remove(@Param('id') id: string) { + return this.service.softRemove(id); + } +} diff --git a/src/dicts/dict-items.service.ts b/src/dicts/dict-items.service.ts new file mode 100644 index 0000000..ab6c3b3 --- /dev/null +++ b/src/dicts/dict-items.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@nestjs/common'; +import { BaseCrudService } from '../common/base-crud.service.js'; +import { PrismaService } from '../prisma/prisma.service.js'; +import { CreateDictItemDto, UpdateDictItemDto } from './dto.js'; + +@Injectable() +export class DictItemsService extends BaseCrudService { + constructor(prisma: PrismaService) { + super(prisma, 'dictItem'); + } + + createItem(dto: CreateDictItemDto) { + return this.create({ ...dto }); + } + + updateItem(id: string, dto: UpdateDictItemDto) { + return this.update(id, { ...dto }); + } + + listByType(dictTypeCode?: string) { + return this.list(dictTypeCode ? { dictTypeCode } : {}, { sort: 'asc' }); + } +} diff --git a/src/dicts/dict-types.controller.ts b/src/dicts/dict-types.controller.ts new file mode 100644 index 0000000..d81045c --- /dev/null +++ b/src/dicts/dict-types.controller.ts @@ -0,0 +1,34 @@ +import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common'; +import { PageQuery } from '../common/page.dto.js'; +import { CreateDictTypeDto, UpdateDictTypeDto } from './dto.js'; +import { DictTypesService } from './dict-types.service.js'; + +@Controller('dict-types') +export class DictTypesController { + constructor(private readonly service: DictTypesService) {} + + @Get() + list(@Query() q: PageQuery) { + return this.service.paginate(q.page, q.pageSize); + } + + @Get(':id') + get(@Param('id') id: string) { + return this.service.findById(id); + } + + @Post() + create(@Body() dto: CreateDictTypeDto) { + return this.service.createType(dto); + } + + @Patch(':id') + update(@Param('id') id: string, @Body() dto: UpdateDictTypeDto) { + return this.service.updateType(id, dto); + } + + @Delete(':id') + remove(@Param('id') id: string) { + return this.service.softRemove(id); + } +} diff --git a/src/dicts/dict-types.service.ts b/src/dicts/dict-types.service.ts new file mode 100644 index 0000000..0e76540 --- /dev/null +++ b/src/dicts/dict-types.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@nestjs/common'; +import { BaseCrudService } from '../common/base-crud.service.js'; +import { PrismaService } from '../prisma/prisma.service.js'; +import { CreateDictTypeDto, UpdateDictTypeDto } from './dto.js'; + +@Injectable() +export class DictTypesService extends BaseCrudService { + constructor(prisma: PrismaService) { + super(prisma, 'dictType'); + } + + createType(dto: CreateDictTypeDto) { + return this.create({ ...dto }); + } + + updateType(id: string, dto: UpdateDictTypeDto) { + return this.update(id, { ...dto }); + } +} diff --git a/src/dicts/dicts.module.ts b/src/dicts/dicts.module.ts new file mode 100644 index 0000000..db52765 --- /dev/null +++ b/src/dicts/dicts.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { DictTypesController } from './dict-types.controller.js'; +import { DictTypesService } from './dict-types.service.js'; +import { DictItemsController } from './dict-items.controller.js'; +import { DictItemsService } from './dict-items.service.js'; + +@Module({ + controllers: [DictTypesController, DictItemsController], + providers: [DictTypesService, DictItemsService], +}) +export class DictsModule {} diff --git a/src/dicts/dto.ts b/src/dicts/dto.ts new file mode 100644 index 0000000..c91e7a3 --- /dev/null +++ b/src/dicts/dto.ts @@ -0,0 +1,27 @@ +import { IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +export class CreateDictTypeDto { + @IsString() @IsNotEmpty() code!: string; + @IsString() @IsNotEmpty() name!: string; + @IsOptional() @IsString() remark?: string; +} + +export class UpdateDictTypeDto { + @IsOptional() @IsString() name?: string; + @IsOptional() @IsString() status?: string; + @IsOptional() @IsString() remark?: string; +} + +export class CreateDictItemDto { + @IsString() @IsNotEmpty() dictTypeCode!: string; + @IsString() @IsNotEmpty() label!: string; + @IsString() @IsNotEmpty() value!: string; + @IsOptional() @IsInt() sort?: number; +} + +export class UpdateDictItemDto { + @IsOptional() @IsString() label?: string; + @IsOptional() @IsString() value?: string; + @IsOptional() @IsInt() sort?: number; + @IsOptional() @IsString() status?: string; +} diff --git a/src/main.ts b/src/main.ts index 9edf749..eda39ca 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,13 @@ +import 'dotenv/config'; import { NestFactory } from '@nestjs/core'; +import { ValidationPipe } from '@nestjs/common'; import { AppModule } from './app.module.js'; -async function bootstrap() { +async function bootstrap(): Promise { const app = await NestFactory.create(AppModule); + app.setGlobalPrefix('api/v1'); + app.enableCors(); + app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true })); await app.listen(process.env.PORT ?? 3000); } await bootstrap(); diff --git a/src/orgs/dto.ts b/src/orgs/dto.ts new file mode 100644 index 0000000..15482c1 --- /dev/null +++ b/src/orgs/dto.ts @@ -0,0 +1,17 @@ +import { IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +export class CreateOrgDto { + @IsString() @IsNotEmpty() code!: string; + @IsString() @IsNotEmpty() name!: string; + @IsOptional() @IsString() parentCode?: string; + @IsOptional() @IsString() leader?: string; + @IsOptional() @IsInt() sort?: number; +} + +export class UpdateOrgDto { + @IsOptional() @IsString() name?: string; + @IsOptional() @IsString() parentCode?: string; + @IsOptional() @IsString() leader?: string; + @IsOptional() @IsInt() sort?: number; + @IsOptional() @IsString() status?: string; +} diff --git a/src/orgs/orgs.controller.ts b/src/orgs/orgs.controller.ts new file mode 100644 index 0000000..4abdbba --- /dev/null +++ b/src/orgs/orgs.controller.ts @@ -0,0 +1,33 @@ +import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common'; +import { CreateOrgDto, UpdateOrgDto } from './dto.js'; +import { OrgsService } from './orgs.service.js'; + +@Controller('orgs') +export class OrgsController { + constructor(private readonly service: OrgsService) {} + + @Get() + list() { + return this.service.list({}, { sort: 'asc' }); + } + + @Get(':id') + get(@Param('id') id: string) { + return this.service.findById(id); + } + + @Post() + create(@Body() dto: CreateOrgDto) { + return this.service.createOrg(dto); + } + + @Patch(':id') + update(@Param('id') id: string, @Body() dto: UpdateOrgDto) { + return this.service.updateOrg(id, dto); + } + + @Delete(':id') + remove(@Param('id') id: string) { + return this.service.softRemove(id); + } +} diff --git a/src/orgs/orgs.module.ts b/src/orgs/orgs.module.ts new file mode 100644 index 0000000..da3d0ea --- /dev/null +++ b/src/orgs/orgs.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { OrgsController } from './orgs.controller.js'; +import { OrgsService } from './orgs.service.js'; + +@Module({ + controllers: [OrgsController], + providers: [OrgsService], +}) +export class OrgsModule {} diff --git a/src/orgs/orgs.service.ts b/src/orgs/orgs.service.ts new file mode 100644 index 0000000..70e2e93 --- /dev/null +++ b/src/orgs/orgs.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@nestjs/common'; +import { BaseCrudService } from '../common/base-crud.service.js'; +import { PrismaService } from '../prisma/prisma.service.js'; +import { CreateOrgDto, UpdateOrgDto } from './dto.js'; + +@Injectable() +export class OrgsService extends BaseCrudService { + constructor(prisma: PrismaService) { + super(prisma, 'org'); + } + + createOrg(dto: CreateOrgDto) { + return this.create({ ...dto }); + } + + updateOrg(id: string, dto: UpdateOrgDto) { + return this.update(id, { ...dto }); + } +} diff --git a/src/prisma/prisma.module.ts b/src/prisma/prisma.module.ts new file mode 100644 index 0000000..efb7a8d --- /dev/null +++ b/src/prisma/prisma.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from '@nestjs/common'; +import { PrismaService } from './prisma.service.js'; + +@Global() +@Module({ + providers: [PrismaService], + exports: [PrismaService], +}) +export class PrismaModule {} diff --git a/src/prisma/prisma.service.ts b/src/prisma/prisma.service.ts new file mode 100644 index 0000000..7bc96c7 --- /dev/null +++ b/src/prisma/prisma.service.ts @@ -0,0 +1,47 @@ +import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; +import { getRequestContext } from '../common/context/request-context.js'; + +/** 不带 tenantCode 的模型(租户表自身)。 */ +const TENANT_FREE = new Set(['Tenant']); +/** 软删模型:读操作统一注入 deletedAt IS NULL。 */ +const SOFT_DELETE = new Set(['User', 'Role', 'Resource', 'Org', 'DictType', 'DictItem', 'Config']); +const READ_OPS = new Set(['findMany', 'findFirst', 'findUnique', 'count', 'aggregate', 'groupBy']); +/** 没有 where 子句的操作(create 的租户由 service 在 data 里显式写入)。 */ +const NO_WHERE_OPS = new Set(['create', 'createMany']); + +/** + * Prisma 客户端封装:统一注入多租户过滤(tenantCode)与软删过滤(deletedAt)。 + * 通过 `this.prisma.client` 访问扩展后的客户端。 + */ +@Injectable() +export class PrismaService implements OnModuleInit, OnModuleDestroy { + readonly client: PrismaClient; + + constructor() { + this.client = new PrismaClient().$extends({ + query: { + $allModels: { + async $allOperations({ model, operation, args, query }: any) { + const ctx = getRequestContext(); + if (ctx && !TENANT_FREE.has(model) && !NO_WHERE_OPS.has(operation)) { + args.where = { ...(args.where ?? {}), tenantCode: ctx.tenantCode }; + } + if (SOFT_DELETE.has(model) && READ_OPS.has(operation)) { + args.where = { ...(args.where ?? {}), deletedAt: null }; + } + return query(args); + }, + }, + }, + }) as unknown as PrismaClient; + } + + async onModuleInit(): Promise { + await this.client.$connect(); + } + + async onModuleDestroy(): Promise { + await this.client.$disconnect(); + } +} diff --git a/src/resources/dto.ts b/src/resources/dto.ts new file mode 100644 index 0000000..71741c4 --- /dev/null +++ b/src/resources/dto.ts @@ -0,0 +1,26 @@ +import { IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +export class CreateResourceDto { + @IsString() @IsNotEmpty() type!: string; + @IsString() @IsNotEmpty() code!: string; + @IsString() @IsNotEmpty() name!: string; + @IsOptional() @IsString() parentCode?: string; + @IsOptional() @IsString() path?: string; + @IsOptional() @IsString() component?: string; + @IsOptional() @IsString() method?: string; + @IsOptional() @IsString() apiPath?: string; + @IsOptional() @IsString() icon?: string; + @IsOptional() @IsInt() sort?: number; +} + +export class UpdateResourceDto { + @IsOptional() @IsString() name?: string; + @IsOptional() @IsString() parentCode?: string; + @IsOptional() @IsString() path?: string; + @IsOptional() @IsString() component?: string; + @IsOptional() @IsString() method?: string; + @IsOptional() @IsString() apiPath?: string; + @IsOptional() @IsString() icon?: string; + @IsOptional() @IsInt() sort?: number; + @IsOptional() @IsString() status?: string; +} diff --git a/src/resources/resources.controller.ts b/src/resources/resources.controller.ts new file mode 100644 index 0000000..2244557 --- /dev/null +++ b/src/resources/resources.controller.ts @@ -0,0 +1,33 @@ +import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common'; +import { CreateResourceDto, UpdateResourceDto } from './dto.js'; +import { ResourcesService } from './resources.service.js'; + +@Controller('resources') +export class ResourcesController { + constructor(private readonly service: ResourcesService) {} + + @Get() + list() { + return this.service.list({}, { sort: 'asc' }); + } + + @Get(':id') + get(@Param('id') id: string) { + return this.service.findById(id); + } + + @Post() + create(@Body() dto: CreateResourceDto) { + return this.service.createResource(dto); + } + + @Patch(':id') + update(@Param('id') id: string, @Body() dto: UpdateResourceDto) { + return this.service.updateResource(id, dto); + } + + @Delete(':id') + remove(@Param('id') id: string) { + return this.service.softRemove(id); + } +} diff --git a/src/resources/resources.module.ts b/src/resources/resources.module.ts new file mode 100644 index 0000000..07721af --- /dev/null +++ b/src/resources/resources.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { ResourcesController } from './resources.controller.js'; +import { ResourcesService } from './resources.service.js'; + +@Module({ + controllers: [ResourcesController], + providers: [ResourcesService], +}) +export class ResourcesModule {} diff --git a/src/resources/resources.service.ts b/src/resources/resources.service.ts new file mode 100644 index 0000000..fc68d3e --- /dev/null +++ b/src/resources/resources.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@nestjs/common'; +import { BaseCrudService } from '../common/base-crud.service.js'; +import { PrismaService } from '../prisma/prisma.service.js'; +import { CreateResourceDto, UpdateResourceDto } from './dto.js'; + +@Injectable() +export class ResourcesService extends BaseCrudService { + constructor(prisma: PrismaService) { + super(prisma, 'resource'); + } + + createResource(dto: CreateResourceDto) { + return this.create({ ...dto }); + } + + updateResource(id: string, dto: UpdateResourceDto) { + return this.update(id, { ...dto }); + } +} diff --git a/src/roles/dto.ts b/src/roles/dto.ts new file mode 100644 index 0000000..14a7239 --- /dev/null +++ b/src/roles/dto.ts @@ -0,0 +1,19 @@ +import { IsArray, IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +export class CreateRoleDto { + @IsString() @IsNotEmpty() code!: string; + @IsString() @IsNotEmpty() name!: string; + @IsOptional() @IsString() dataScope?: string; + @IsOptional() @IsString() remark?: string; +} + +export class UpdateRoleDto { + @IsOptional() @IsString() name?: string; + @IsOptional() @IsString() dataScope?: string; + @IsOptional() @IsString() status?: string; + @IsOptional() @IsString() remark?: string; +} + +export class AssignResourcesDto { + @IsArray() @IsString({ each: true }) resourceCodes!: string[]; +} diff --git a/src/roles/roles.controller.ts b/src/roles/roles.controller.ts new file mode 100644 index 0000000..d28cd63 --- /dev/null +++ b/src/roles/roles.controller.ts @@ -0,0 +1,44 @@ +import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common'; +import { PageQuery } from '../common/page.dto.js'; +import { AssignResourcesDto, CreateRoleDto, UpdateRoleDto } from './dto.js'; +import { RolesService } from './roles.service.js'; + +@Controller('roles') +export class RolesController { + constructor(private readonly service: RolesService) {} + + @Get() + list(@Query() q: PageQuery) { + return this.service.paginate(q.page, q.pageSize); + } + + @Get(':id') + get(@Param('id') id: string) { + return this.service.findById(id); + } + + @Post() + create(@Body() dto: CreateRoleDto) { + return this.service.createRole(dto); + } + + @Patch(':id') + update(@Param('id') id: string, @Body() dto: UpdateRoleDto) { + return this.service.updateRole(id, dto); + } + + @Delete(':id') + remove(@Param('id') id: string) { + return this.service.softRemove(id); + } + + @Get(':id/resources') + resources(@Param('id') id: string) { + return this.service.assignedResources(id); + } + + @Put(':id/resources') + assign(@Param('id') id: string, @Body() dto: AssignResourcesDto) { + return this.service.assignResources(id, dto); + } +} diff --git a/src/roles/roles.module.ts b/src/roles/roles.module.ts new file mode 100644 index 0000000..78a4259 --- /dev/null +++ b/src/roles/roles.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { RolesController } from './roles.controller.js'; +import { RolesService } from './roles.service.js'; + +@Module({ + controllers: [RolesController], + providers: [RolesService], +}) +export class RolesModule {} diff --git a/src/roles/roles.service.ts b/src/roles/roles.service.ts new file mode 100644 index 0000000..07a6db1 --- /dev/null +++ b/src/roles/roles.service.ts @@ -0,0 +1,40 @@ +import { Injectable } from '@nestjs/common'; +import { BaseCrudService } from '../common/base-crud.service.js'; +import { PrismaService } from '../prisma/prisma.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) { + super(prisma, 'role'); + } + + createRole(dto: CreateRoleDto) { + return this.create({ ...dto }); + } + + updateRole(id: string, dto: UpdateRoleDto) { + return this.update(id, { ...dto }); + } + + async assignedResources(roleCode: string) { + const ctx = getRequestContext()!; + const rows = await this.prisma.client.roleResource.findMany({ + where: { tenantCode: ctx.tenantCode, roleCode }, + select: { resourceCode: true }, + }); + return rows.map((r) => r.resourceCode); + } + + async assignResources(roleCode: string, dto: AssignResourcesDto) { + const ctx = getRequestContext()!; + await this.prisma.client.roleResource.deleteMany({ where: { tenantCode: ctx.tenantCode, roleCode } }); + if (dto.resourceCodes.length > 0) { + await this.prisma.client.roleResource.createMany({ + data: dto.resourceCodes.map((resourceCode) => ({ tenantCode: ctx.tenantCode, roleCode, resourceCode })), + }); + } + return null; + } +} diff --git a/src/tenants/dto.ts b/src/tenants/dto.ts new file mode 100644 index 0000000..bacdcf7 --- /dev/null +++ b/src/tenants/dto.ts @@ -0,0 +1,14 @@ +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +export class CreateTenantDto { + @IsString() @IsNotEmpty() code!: string; + @IsString() @IsNotEmpty() name!: string; + @IsOptional() @IsString() status?: string; + @IsOptional() @IsString() remark?: string; +} + +export class UpdateTenantDto { + @IsOptional() @IsString() name?: string; + @IsOptional() @IsString() status?: string; + @IsOptional() @IsString() remark?: string; +} diff --git a/src/tenants/tenants.controller.ts b/src/tenants/tenants.controller.ts new file mode 100644 index 0000000..eb78461 --- /dev/null +++ b/src/tenants/tenants.controller.ts @@ -0,0 +1,34 @@ +import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common'; +import { PageQuery } from '../common/page.dto.js'; +import { CreateTenantDto, UpdateTenantDto } from './dto.js'; +import { TenantsService } from './tenants.service.js'; + +@Controller('tenants') +export class TenantsController { + constructor(private readonly service: TenantsService) {} + + @Get() + list(@Query() q: PageQuery) { + return this.service.paginate(q.page, q.pageSize); + } + + @Get(':id') + get(@Param('id') id: string) { + return this.service.findById(id); + } + + @Post() + create(@Body() dto: CreateTenantDto) { + return this.service.createTenant(dto); + } + + @Patch(':id') + update(@Param('id') id: string, @Body() dto: UpdateTenantDto) { + return this.service.updateTenant(id, dto); + } + + @Delete(':id') + remove(@Param('id') id: string) { + return this.service.removeTenant(id); + } +} diff --git a/src/tenants/tenants.module.ts b/src/tenants/tenants.module.ts new file mode 100644 index 0000000..d8c1a97 --- /dev/null +++ b/src/tenants/tenants.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { TenantsController } from './tenants.controller.js'; +import { TenantsService } from './tenants.service.js'; + +@Module({ + controllers: [TenantsController], + providers: [TenantsService], +}) +export class TenantsModule {} diff --git a/src/tenants/tenants.service.ts b/src/tenants/tenants.service.ts new file mode 100644 index 0000000..d785012 --- /dev/null +++ b/src/tenants/tenants.service.ts @@ -0,0 +1,27 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { BaseCrudService } from '../common/base-crud.service.js'; +import { PrismaService } from '../prisma/prisma.service.js'; +import { CreateTenantDto, UpdateTenantDto } from './dto.js'; + +@Injectable() +export class TenantsService extends BaseCrudService { + constructor(prisma: PrismaService) { + super(prisma, 'tenant'); + } + + async createTenant(dto: CreateTenantDto) { + return this.create({ ...dto }); + } + + async updateTenant(id: string, dto: UpdateTenantDto) { + const existing = await this.findById(id); + if (!existing) throw new NotFoundException('租户不存在'); + return this.update(id, { ...dto }); + } + + async removeTenant(id: string) { + const existing = await this.findById(id); + if (!existing) throw new NotFoundException('租户不存在'); + return this.db().delete({ where: { id } }); + } +} diff --git a/src/users/dto.ts b/src/users/dto.ts new file mode 100644 index 0000000..79d86c6 --- /dev/null +++ b/src/users/dto.ts @@ -0,0 +1,24 @@ +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +export class CreateUserDto { + @IsString() @IsNotEmpty() username!: string; + @IsString() @IsNotEmpty() password!: string; + @IsOptional() @IsString() nickname?: string; + @IsOptional() @IsString() email?: string; + @IsOptional() @IsString() phone?: string; + @IsOptional() @IsString() avatar?: string; + @IsOptional() @IsString() orgCode?: string; +} + +export class UpdateUserDto { + @IsOptional() @IsString() nickname?: string; + @IsOptional() @IsString() email?: string; + @IsOptional() @IsString() phone?: string; + @IsOptional() @IsString() avatar?: string; + @IsOptional() @IsString() orgCode?: string; + @IsOptional() @IsString() status?: string; +} + +export class ResetPasswordDto { + @IsString() @IsNotEmpty() password!: string; +} diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts new file mode 100644 index 0000000..6753cda --- /dev/null +++ b/src/users/users.controller.ts @@ -0,0 +1,39 @@ +import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common'; +import { PageQuery } from '../common/page.dto.js'; +import { CreateUserDto, ResetPasswordDto, UpdateUserDto } from './dto.js'; +import { UsersService } from './users.service.js'; + +@Controller('users') +export class UsersController { + constructor(private readonly service: UsersService) {} + + @Get() + list(@Query() q: PageQuery) { + return this.service.listUsers(q); + } + + @Get(':id') + get(@Param('id') id: string) { + return this.service.findById(id); + } + + @Post() + create(@Body() dto: CreateUserDto) { + return this.service.createUser(dto); + } + + @Patch(':id') + update(@Param('id') id: string, @Body() dto: UpdateUserDto) { + return this.service.updateUser(id, dto); + } + + @Patch(':id/password') + resetPassword(@Param('id') id: string, @Body() dto: ResetPasswordDto) { + return this.service.resetPassword(id, dto); + } + + @Delete(':id') + remove(@Param('id') id: string) { + return this.service.softRemove(id); + } +} diff --git a/src/users/users.module.ts b/src/users/users.module.ts new file mode 100644 index 0000000..1a781ad --- /dev/null +++ b/src/users/users.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { UsersController } from './users.controller.js'; +import { UsersService } from './users.service.js'; + +@Module({ + controllers: [UsersController], + providers: [UsersService], +}) +export class UsersModule {} diff --git a/src/users/users.service.ts b/src/users/users.service.ts new file mode 100644 index 0000000..03e976d --- /dev/null +++ b/src/users/users.service.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@nestjs/common'; +import bcrypt from 'bcryptjs'; +import { BaseCrudService } from '../common/base-crud.service.js'; +import { PrismaService } from '../prisma/prisma.service.js'; +import { getRequestContext } from '../common/context/request-context.js'; +import { CreateUserDto, ResetPasswordDto, UpdateUserDto } from './dto.js'; +import { PageQuery } from '../common/page.dto.js'; + +@Injectable() +export class UsersService extends BaseCrudService { + constructor(prisma: PrismaService) { + super(prisma, 'user'); + } + + async createUser(dto: CreateUserDto) { + const passwordHash = await bcrypt.hash(dto.password, 10); + const { password: _p, ...rest } = dto; + return this.create({ ...rest, passwordHash }); + } + + async listUsers(q: PageQuery) { + const ctx = getRequestContext(); + const where: Record = {}; + // 数据权限:orgCodes 为空表示「全部可见」,否则限制在可见部门集合 + if (ctx && ctx.orgCodes.length > 0) { + where.orgCode = { in: ctx.orgCodes }; + } + return this.paginate(q.page, q.pageSize, where); + } + + async updateUser(id: string, dto: UpdateUserDto) { + return this.update(id, { ...dto }); + } + + async resetPassword(id: string, dto: ResetPasswordDto) { + const passwordHash = await bcrypt.hash(dto.password, 10); + return this.update(id, { passwordHash }); + } +}