diff --git a/.gitignore b/.gitignore index 3ee761b..fad5c77 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ node_modules/ # Build outputs dist/ +dist-ssr build/ *.tsbuildinfo .next/ @@ -74,3 +75,13 @@ dist/ # pnpm .pnpm-store/ +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/apps/admin/.gitignore b/apps/admin/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/apps/admin/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/apps/admin/README.md b/apps/admin/README.md new file mode 100644 index 0000000..4dcad1f --- /dev/null +++ b/apps/admin/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is currently not compatible with SWC. See [this issue](https://github.com/vitejs/vite-plugin-react/issues/428) for tracking the progress. + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/apps/admin/eslint.config.js b/apps/admin/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/apps/admin/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/apps/admin/index.html b/apps/admin/index.html new file mode 100644 index 0000000..14f807b --- /dev/null +++ b/apps/admin/index.html @@ -0,0 +1,16 @@ + + + + + + + + admin + + + +
+ + + + \ No newline at end of file diff --git a/apps/admin/package.json b/apps/admin/package.json new file mode 100644 index 0000000..d258783 --- /dev/null +++ b/apps/admin/package.json @@ -0,0 +1,30 @@ +{ + "name": "admin", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.0", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "@vitejs/plugin-react-swc": "^4.2.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.3", + "vite": "^7.2.2" + } +} diff --git a/apps/admin/public/vite.svg b/apps/admin/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/apps/admin/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/admin/src/App.css b/apps/admin/src/App.css new file mode 100644 index 0000000..b9d355d --- /dev/null +++ b/apps/admin/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/apps/admin/src/App.tsx b/apps/admin/src/App.tsx new file mode 100644 index 0000000..3d7ded3 --- /dev/null +++ b/apps/admin/src/App.tsx @@ -0,0 +1,35 @@ +import { useState } from 'react' +import reactLogo from './assets/react.svg' +import viteLogo from '/vite.svg' +import './App.css' + +function App() { + const [count, setCount] = useState(0) + + return ( + <> +
+ + Vite logo + + + React logo + +
+

Vite + React

+
+ +

+ Edit src/App.tsx and save to test HMR +

+
+

+ Click on the Vite and React logos to learn more +

+ + ) +} + +export default App diff --git a/apps/admin/src/assets/react.svg b/apps/admin/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/apps/admin/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/admin/src/index.css b/apps/admin/src/index.css new file mode 100644 index 0000000..08a3ac9 --- /dev/null +++ b/apps/admin/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/apps/admin/src/main.tsx b/apps/admin/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/apps/admin/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/apps/admin/tsconfig.app.json b/apps/admin/tsconfig.app.json new file mode 100644 index 0000000..a9b5a59 --- /dev/null +++ b/apps/admin/tsconfig.app.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/apps/admin/tsconfig.json b/apps/admin/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/apps/admin/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/apps/admin/tsconfig.node.json b/apps/admin/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/apps/admin/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/apps/admin/vite.config.ts b/apps/admin/vite.config.ts new file mode 100644 index 0000000..2328e17 --- /dev/null +++ b/apps/admin/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react-swc' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) diff --git a/apps/api/.env.development b/apps/api/.env.development index 6697180..0fb3a10 100644 --- a/apps/api/.env.development +++ b/apps/api/.env.development @@ -7,3 +7,6 @@ DB_USER= DB_PASSWORD= DB_DATABASE=vest_mind_test # DB_DATABASE_TEST=vest_mind_test + +JWT_SECRET=vest_thinking_key +JWT_EXPIRES_IN=7d \ No newline at end of file diff --git a/apps/api/package.json b/apps/api/package.json index 89c85ad..a9ba651 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -24,16 +24,22 @@ "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", + "@nestjs/jwt": "^11.0.1", "@nestjs/mapped-types": "^2.1.0", + "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.0.1", "@nestjs/swagger": "^11.2.2", "@nestjs/typeorm": "^11.0.0", + "@types/jsonwebtoken": "^9.0.10", + "bcrypt": "^6.0.0", "body-parser": "^2.2.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", "compression": "^1.8.1", "express-rate-limit": "^8.2.1", "helmet": "^8.1.0", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", "pg": "^8.16.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", @@ -45,11 +51,13 @@ "@nestjs/cli": "^11.0.0", "@nestjs/schematics": "^11.0.0", "@nestjs/testing": "^11.0.1", + "@types/bcrypt": "^6.0.0", "@types/body-parser": "^1.19.6", "@types/compression": "^1.8.1", "@types/express": "^5.0.0", "@types/jest": "^30.0.0", "@types/node": "^22.10.7", + "@types/passport-jwt": "^4.0.1", "@types/pg": "^8.15.6", "@types/supertest": "^6.0.2", "eslint": "^9.18.0", diff --git a/apps/api/src/app.controller.spec.ts b/apps/api/src/app.controller.spec.ts deleted file mode 100644 index 2552ec5..0000000 --- a/apps/api/src/app.controller.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; - -describe('AppController', () => { - let appController: AppController; - - beforeEach(async () => { - const app: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - providers: [AppService], - }).compile(); - - appController = app.get(AppController); - }); - - describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); - }); - }); -}); diff --git a/apps/api/src/app.controller.ts b/apps/api/src/app.controller.ts deleted file mode 100644 index a325e8b..0000000 --- a/apps/api/src/app.controller.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; -import { AppService } from './app.service'; - -@Controller() -export class AppController { - constructor(private readonly appService: AppService) {} - - @Get() - getHello(): string { - return this.appService.getHello(); - } -} diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index 2f7b470..b29f273 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -3,8 +3,8 @@ import { ConfigModule } from '@nestjs/config'; import { DatabaseModule } from './database/database.module'; import { CoreModule } from './core/core.module'; import { BrokerModule } from './modules/broker/broker.module'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; +import { UserModule } from './modules/user/user.module'; +import { AuthModule } from './modules/auth/auth.module'; @Module({ imports: [ @@ -18,8 +18,10 @@ import { AppService } from './app.service'; CoreModule, DatabaseModule, BrokerModule, + UserModule, + AuthModule, ], - controllers: [AppController], - providers: [AppService], + controllers: [], + providers: [], }) export class AppModule {} diff --git a/apps/api/src/app.service.ts b/apps/api/src/app.service.ts deleted file mode 100644 index 61b7a5b..0000000 --- a/apps/api/src/app.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class AppService { - getHello(): string { - return 'Hello World!'; - } -} diff --git a/apps/api/src/modules/auth/auth.controller.ts b/apps/api/src/modules/auth/auth.controller.ts new file mode 100644 index 0000000..968b759 --- /dev/null +++ b/apps/api/src/modules/auth/auth.controller.ts @@ -0,0 +1,66 @@ +import { Controller, Post, Body, HttpCode, HttpStatus } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { AuthService } from './auth.service'; +import { LoginDto } from './dto/login.dto'; +import { LoginResponse } from './interfaces/login-response.interface'; + +@ApiTags('auth') +@Controller('auth') +export class AuthController { + constructor(private readonly authService: AuthService) {} + + /** + * 用户登录 + */ + @Post('login') + @HttpCode(HttpStatus.OK) + @ApiOperation({ + summary: '用户登录', + description: '使用用户名/邮箱和密码登录,返回 access_token 和用户信息', + }) + @ApiResponse({ + status: 200, + description: '登录成功', + schema: { + type: 'object', + properties: { + code: { type: 'number', example: 0 }, + message: { type: 'string', example: 'success' }, + data: { + type: 'object', + properties: { + accessToken: { + type: 'string', + example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', + }, + user: { + type: 'object', + properties: { + userId: { type: 'number', example: 1 }, + username: { + type: 'string', + example: 'john_doe', + }, + email: { + type: 'string', + example: 'user@example.com', + }, + nickname: { + type: 'string', + example: 'John Doe', + }, + role: { type: 'string', example: 'user' }, + }, + }, + }, + }, + timestamp: { type: 'string' }, + }, + }, + }) + @ApiResponse({ status: 401, description: '用户名或密码错误' }) + @ApiResponse({ status: 400, description: '请求参数错误' }) + async login(@Body() loginDto: LoginDto): Promise { + return this.authService.login(loginDto); + } +} diff --git a/apps/api/src/modules/auth/auth.module.ts b/apps/api/src/modules/auth/auth.module.ts new file mode 100644 index 0000000..22791b9 --- /dev/null +++ b/apps/api/src/modules/auth/auth.module.ts @@ -0,0 +1,41 @@ +import { Module } from '@nestjs/common'; +import { JwtModule } from '@nestjs/jwt'; +import { PassportModule } from '@nestjs/passport'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AuthService } from './auth.service'; +import { AuthController } from './auth.controller'; +import { JwtStrategy } from './strategies/jwt.strategy'; +import { User } from '../user/user.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([User]), + PassportModule.register({ defaultStrategy: 'jwt' }), + JwtModule.registerAsync({ + imports: [ConfigModule], + // @ts-expect-error - JWT expiresIn accepts string but type definition is strict + useFactory: (configService: ConfigService) => { + const expiresIn = configService.get( + 'JWT_EXPIRES_IN', + '7d', + ); + const secret = configService.get( + 'JWT_SECRET', + 'your-secret-key-change-in-production', + ); + return { + secret: secret, + signOptions: { + expiresIn: expiresIn || '7d', // 默认7天过期 + }, + }; + }, + inject: [ConfigService], + }), + ], + controllers: [AuthController], + providers: [AuthService, JwtStrategy], + exports: [AuthService, JwtModule], +}) +export class AuthModule {} diff --git a/apps/api/src/modules/auth/auth.service.ts b/apps/api/src/modules/auth/auth.service.ts new file mode 100644 index 0000000..d6cf2c4 --- /dev/null +++ b/apps/api/src/modules/auth/auth.service.ts @@ -0,0 +1,73 @@ +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import * as bcrypt from 'bcrypt'; +import { User } from '../user/user.entity'; +import { LoginDto } from './dto/login.dto'; +import { JwtPayload } from './interfaces/jwt-payload.interface'; +import { LoginResponse } from './interfaces/login-response.interface'; + +@Injectable() +export class AuthService { + constructor( + @InjectRepository(User) + private readonly userRepository: Repository, + private readonly jwtService: JwtService, + ) {} + + /** + * 用户登录 + */ + async login(loginDto: LoginDto): Promise { + // 根据用户名或邮箱查找用户 + const user = await this.userRepository.findOne({ + where: [ + { username: loginDto.usernameOrEmail }, + { email: loginDto.usernameOrEmail }, + ], + }); + + if (!user) { + throw new UnauthorizedException('用户名或密码错误'); + } + + // 检查用户状态 + if (user.status !== 'active') { + throw new UnauthorizedException('用户已被禁用'); + } + + // 验证密码 + const isPasswordValid = await bcrypt.compare( + loginDto.password, + user.passwordHash, + ); + + if (!isPasswordValid) { + throw new UnauthorizedException('用户名或密码错误'); + } + + // 更新最后登录时间 + user.lastLoginAt = new Date(); + await this.userRepository.save(user); + + // 生成 JWT token + const payload: JwtPayload = { + sub: user.userId, + username: user.username, + email: user.email, + role: user.role, + }; + + const accessToken = this.jwtService.sign(payload); + + // 返回 token 和用户信息(排除密码) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { passwordHash, ...userWithoutPassword } = user; + + return { + accessToken, + user: userWithoutPassword, + }; + } +} diff --git a/apps/api/src/modules/auth/decorators/roles.decorator.ts b/apps/api/src/modules/auth/decorators/roles.decorator.ts new file mode 100644 index 0000000..802d2d0 --- /dev/null +++ b/apps/api/src/modules/auth/decorators/roles.decorator.ts @@ -0,0 +1,37 @@ +import { SetMetadata } from '@nestjs/common'; + +/** + * 角色权限装饰器 + * + * 这个装饰器用于在控制器或方法上标记需要的角色权限。 + * 配合 RolesGuard 使用,实现基于角色的访问控制(RBAC)。 + * + * 使用示例: + * @Roles('admin', 'super_admin') + * @Get() + * findAll() { ... } + * + * 工作原理: + * 1. SetMetadata 会在目标方法或类上设置元数据 + * 2. ROLES_KEY 作为元数据的键,存储角色数组 + * 3. RolesGuard 通过 Reflector 读取这些元数据来验证用户权限 + */ + +/** + * 元数据键名 + * 用于在 Reflector 中存储和读取角色信息 + */ +export const ROLES_KEY = 'roles'; + +/** + * 角色权限装饰器工厂函数 + * + * @param roles - 允许访问的角色列表(可变参数) + * @returns 返回一个装饰器函数,用于设置元数据 + * + * 示例: + * - @Roles('admin') - 只允许 admin 角色访问 + * - @Roles('admin', 'super_admin') - 允许 admin 或 super_admin 角色访问 + * - @Roles() - 不传参数时,RolesGuard 会允许所有已认证用户访问 + */ +export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles); diff --git a/apps/api/src/modules/auth/dto/login.dto.ts b/apps/api/src/modules/auth/dto/login.dto.ts new file mode 100644 index 0000000..142dd58 --- /dev/null +++ b/apps/api/src/modules/auth/dto/login.dto.ts @@ -0,0 +1,22 @@ +import { IsString, IsNotEmpty, MinLength } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class LoginDto { + @ApiProperty({ + description: '用户名或邮箱', + example: 'john_doe', + }) + @IsString() + @IsNotEmpty() + @MinLength(3) + usernameOrEmail: string; + + @ApiProperty({ + description: '密码', + example: 'password123', + }) + @IsString() + @IsNotEmpty() + @MinLength(6) + password: string; +} diff --git a/apps/api/src/modules/auth/guards/jwt-auth.guard.ts b/apps/api/src/modules/auth/guards/jwt-auth.guard.ts new file mode 100644 index 0000000..c26e82a --- /dev/null +++ b/apps/api/src/modules/auth/guards/jwt-auth.guard.ts @@ -0,0 +1,24 @@ +import { + Injectable, + ExecutionContext, + UnauthorizedException, +} from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class JwtAuthGuard extends AuthGuard('jwt') { + handleRequest( + err: Error | null, + user: TUser | false, + info: Error | string | undefined, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _context: ExecutionContext, + ): TUser { + // 如果认证失败(user 为 false 或 undefined,或者有错误) + if (err || !user || info) { + throw new UnauthorizedException('身份验证失败'); + } + + return user; + } +} diff --git a/apps/api/src/modules/auth/guards/roles.guard.ts b/apps/api/src/modules/auth/guards/roles.guard.ts new file mode 100644 index 0000000..12b51c6 --- /dev/null +++ b/apps/api/src/modules/auth/guards/roles.guard.ts @@ -0,0 +1,44 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + ForbiddenException, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { ROLES_KEY } from '../decorators/roles.decorator'; + +@Injectable() +export class RolesGuard implements CanActivate { + constructor(private reflector: Reflector) {} + + canActivate(context: ExecutionContext): boolean { + const requiredRoles = this.reflector.getAllAndOverride( + ROLES_KEY, + [context.getHandler(), context.getClass()], + ); + + if (!requiredRoles) { + // 如果没有设置角色要求,允许访问 + return true; + } + + const request = context.switchToHttp().getRequest<{ + user?: { role: string }; + }>(); + const user = request.user; + + if (!user) { + throw new ForbiddenException('未授权访问'); + } + + const hasRole = requiredRoles.some((role) => user.role === role); + + if (!hasRole) { + throw new ForbiddenException( + `需要以下角色之一:${requiredRoles.join('、')}`, + ); + } + + return true; + } +} diff --git a/apps/api/src/modules/auth/interfaces/jwt-payload.interface.ts b/apps/api/src/modules/auth/interfaces/jwt-payload.interface.ts new file mode 100644 index 0000000..bbab832 --- /dev/null +++ b/apps/api/src/modules/auth/interfaces/jwt-payload.interface.ts @@ -0,0 +1,6 @@ +export interface JwtPayload { + sub: number; // 用户ID + username: string; + email: string; + role: string; +} diff --git a/apps/api/src/modules/auth/interfaces/login-response.interface.ts b/apps/api/src/modules/auth/interfaces/login-response.interface.ts new file mode 100644 index 0000000..4bcb2b8 --- /dev/null +++ b/apps/api/src/modules/auth/interfaces/login-response.interface.ts @@ -0,0 +1,6 @@ +import { User } from '../../user/user.entity'; + +export interface LoginResponse { + accessToken: string; + user: Omit; // 排除密码哈希 +} diff --git a/apps/api/src/modules/auth/strategies/jwt.strategy.ts b/apps/api/src/modules/auth/strategies/jwt.strategy.ts new file mode 100644 index 0000000..b5a99ea --- /dev/null +++ b/apps/api/src/modules/auth/strategies/jwt.strategy.ts @@ -0,0 +1,38 @@ +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { ConfigService } from '@nestjs/config'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { User } from '../../user/user.entity'; +import { JwtPayload } from '../interfaces/jwt-payload.interface'; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor( + private readonly configService: ConfigService, + @InjectRepository(User) + private readonly userRepository: Repository, + ) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: configService.get( + 'JWT_SECRET', + 'your-secret-key', + ), + }); + } + + async validate(payload: JwtPayload): Promise { + const user = await this.userRepository.findOne({ + where: { userId: payload.sub }, + }); + + if (!user || user.status !== 'active') { + throw new UnauthorizedException('用户不存在或已被禁用'); + } + + return user; + } +} diff --git a/apps/api/src/modules/broker/broker.service.ts b/apps/api/src/modules/broker/broker.service.ts index a9e753c..7c3ef10 100644 --- a/apps/api/src/modules/broker/broker.service.ts +++ b/apps/api/src/modules/broker/broker.service.ts @@ -32,7 +32,7 @@ export class BrokerService { if (existingByCode) { throw new ConflictException( - `Broker with code "${createBrokerDto.brokerCode}" already exists in region "${createBrokerDto.region}"`, + `地区 "${createBrokerDto.region}" 中已存在代码为 "${createBrokerDto.brokerCode}" 的券商`, ); } @@ -46,7 +46,7 @@ export class BrokerService { if (existingByName) { throw new ConflictException( - `Broker with name "${createBrokerDto.brokerName}" already exists in region "${createBrokerDto.region}"`, + `地区 "${createBrokerDto.region}" 中已存在名称为 "${createBrokerDto.brokerName}" 的券商`, ); } @@ -88,10 +88,10 @@ export class BrokerService { if (existingBrokers.length > 0) { const conflicts = existingBrokers.map( - (b) => `${b.brokerCode} in ${b.region}`, + (b) => `${b.brokerCode} (${b.region})`, ); throw new ConflictException( - `The following brokers already exist: ${conflicts.join(', ')}`, + `以下券商已存在:${conflicts.join('、')}`, ); } @@ -101,7 +101,7 @@ export class BrokerService { ); if (uniquePairs.size !== codeRegionPairs.length) { throw new ConflictException( - 'Duplicate broker_code and region combinations in batch data', + '批量数据中存在重复的券商代码和地区组合', ); } @@ -152,7 +152,7 @@ export class BrokerService { }); if (!broker) { - throw new NotFoundException(`Broker with ID ${id} not found`); + throw new NotFoundException(`未找到ID为 ${id} 的券商`); } return broker; @@ -187,9 +187,7 @@ export class BrokerService { const broker = await this.brokerRepository.findOne({ where }); if (!broker) { - throw new NotFoundException( - 'Broker not found with the given conditions', - ); + throw new NotFoundException('未找到符合给定条件的券商'); } return broker; @@ -218,7 +216,7 @@ export class BrokerService { if (existing && existing.brokerId !== id) { throw new ConflictException( - `Broker with code "${newCode}" already exists in region "${newRegion}"`, + `地区 "${newRegion}" 中已存在代码为 "${newCode}" 的券商`, ); } } @@ -237,7 +235,7 @@ export class BrokerService { if (existing && existing.brokerId !== id) { throw new ConflictException( - `Broker with name "${newName}" already exists in region "${newRegion}"`, + `地区 "${newRegion}" 中已存在名称为 "${newName}" 的券商`, ); } } diff --git a/apps/api/src/modules/user/dto/change-password.dto.ts b/apps/api/src/modules/user/dto/change-password.dto.ts new file mode 100644 index 0000000..50fa22c --- /dev/null +++ b/apps/api/src/modules/user/dto/change-password.dto.ts @@ -0,0 +1,23 @@ +import { IsString, IsNotEmpty, MinLength, MaxLength } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class ChangePasswordDto { + @ApiProperty({ + description: '旧密码', + example: 'OldPassword123!', + }) + @IsString() + @IsNotEmpty() + oldPassword: string; + + @ApiProperty({ + description: '新密码', + example: 'NewPassword123!', + minLength: 6, + }) + @IsString() + @IsNotEmpty() + @MinLength(6) + @MaxLength(100) + newPassword: string; +} diff --git a/apps/api/src/modules/user/dto/create-user.dto.ts b/apps/api/src/modules/user/dto/create-user.dto.ts new file mode 100644 index 0000000..25c6ff2 --- /dev/null +++ b/apps/api/src/modules/user/dto/create-user.dto.ts @@ -0,0 +1,109 @@ +import { + IsString, + IsNotEmpty, + IsOptional, + IsEmail, + MinLength, + MaxLength, + Matches, + IsIn, +} from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class CreateUserDto { + @ApiProperty({ + description: '用户名', + example: 'john_doe', + maxLength: 100, + }) + @IsString() + @IsNotEmpty() + @MinLength(3) + @MaxLength(100) + username: string; + + @ApiProperty({ + description: '密码(明文,服务端会使用 bcrypt 加密后存储)', + example: 'SecurePassword123!', + minLength: 6, + }) + @IsString() + @IsNotEmpty() + @MinLength(6) + @MaxLength(100) + password: string; + + @ApiProperty({ + description: '邮箱', + example: 'user@example.com', + maxLength: 100, + }) + @IsEmail() + @IsNotEmpty() + @MaxLength(100) + email: string; + + @ApiPropertyOptional({ + description: '用户昵称', + example: 'John Doe', + maxLength: 100, + }) + @IsOptional() + @IsString() + @MaxLength(100) + nickname?: string; + + @ApiPropertyOptional({ + description: '头像URL', + example: 'https://example.com/avatar.jpg', + maxLength: 255, + }) + @IsOptional() + @IsString() + @MaxLength(255) + avatarUrl?: string; + + @ApiPropertyOptional({ + description: '电话号码', + example: '13800138000', + maxLength: 20, + }) + @IsOptional() + @IsString() + @Matches(/^[0-9+\-() ]+$/, { + message: '电话号码格式不正确', + }) + @MaxLength(20) + phone?: string; + + @ApiPropertyOptional({ + description: '微信 openId', + example: 'openid123456', + maxLength: 100, + }) + @IsOptional() + @IsString() + @MaxLength(100) + openId?: string; + + @ApiPropertyOptional({ + description: '微信 unionId', + example: 'unionid123456', + maxLength: 100, + }) + @IsOptional() + @IsString() + @MaxLength(100) + unionId?: string; + + @ApiPropertyOptional({ + description: '用户角色', + example: 'user', + enum: ['user', 'admin', 'super_admin'], + default: 'user', + }) + @IsOptional() + @IsString() + @IsIn(['user', 'admin', 'super_admin']) + role?: string; +} diff --git a/apps/api/src/modules/user/dto/query-user.dto.ts b/apps/api/src/modules/user/dto/query-user.dto.ts new file mode 100644 index 0000000..90d6f57 --- /dev/null +++ b/apps/api/src/modules/user/dto/query-user.dto.ts @@ -0,0 +1,20 @@ +import { IsOptional, IsString, IsEmail } from 'class-validator'; +import { ApiPropertyOptional } from '@nestjs/swagger'; + +export class QueryUserDto { + @ApiPropertyOptional({ + description: '用户名', + example: 'john_doe', + }) + @IsOptional() + @IsString() + username?: string; + + @ApiPropertyOptional({ + description: '邮箱', + example: 'user@example.com', + }) + @IsOptional() + @IsEmail() + email?: string; +} diff --git a/apps/api/src/modules/user/dto/update-user.dto.ts b/apps/api/src/modules/user/dto/update-user.dto.ts new file mode 100644 index 0000000..4f45ebc --- /dev/null +++ b/apps/api/src/modules/user/dto/update-user.dto.ts @@ -0,0 +1,73 @@ +import { + IsString, + IsOptional, + IsEmail, + MaxLength, + Matches, + IsIn, +} from 'class-validator'; +import { ApiPropertyOptional } from '@nestjs/swagger'; + +export class UpdateUserDto { + @ApiPropertyOptional({ + description: '邮箱', + example: 'newemail@example.com', + maxLength: 100, + }) + @IsOptional() + @IsEmail() + @MaxLength(100) + email?: string; + + @ApiPropertyOptional({ + description: '电话号码', + example: '13800138000', + maxLength: 20, + }) + @IsOptional() + @IsString() + @Matches(/^[0-9+\-() ]+$/, { + message: '电话号码格式不正确', + }) + @MaxLength(20) + phone?: string; + + @ApiPropertyOptional({ + description: '用户昵称', + example: 'John Doe', + maxLength: 100, + }) + @IsOptional() + @IsString() + @MaxLength(100) + nickname?: string; + + @ApiPropertyOptional({ + description: '头像URL', + example: 'https://example.com/avatar.jpg', + maxLength: 255, + }) + @IsOptional() + @IsString() + @MaxLength(255) + avatarUrl?: string; + + @ApiPropertyOptional({ + description: '用户状态', + example: 'active', + enum: ['active', 'inactive', 'deleted'], + }) + @IsOptional() + @IsString() + status?: string; + + @ApiPropertyOptional({ + description: '用户角色', + example: 'user', + enum: ['user', 'admin', 'super_admin'], + }) + @IsOptional() + @IsString() + @IsIn(['user', 'admin', 'super_admin']) + role?: string; +} diff --git a/apps/api/src/modules/user/user.controller.ts b/apps/api/src/modules/user/user.controller.ts new file mode 100644 index 0000000..8cb51da --- /dev/null +++ b/apps/api/src/modules/user/user.controller.ts @@ -0,0 +1,180 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, + Query, + HttpCode, + HttpStatus, + UseGuards, +} from '@nestjs/common'; +import { + ApiTags, + ApiOperation, + ApiResponse, + ApiParam, + ApiBearerAuth, +} from '@nestjs/swagger'; +import { UserService } from './user.service'; +import { User } from './user.entity'; +import { CreateUserDto } from './dto/create-user.dto'; +import { UpdateUserDto } from './dto/update-user.dto'; +import { QueryUserDto } from './dto/query-user.dto'; +import { ChangePasswordDto } from './dto/change-password.dto'; +import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +import { RolesGuard } from '../auth/guards/roles.guard'; +import { Roles } from '../auth/decorators/roles.decorator'; + +@ApiTags('user') +@Controller('user') +export class UserController { + constructor(private readonly userService: UserService) {} + + /** + * 注册用户 + */ + @Post('register') + @HttpCode(HttpStatus.CREATED) + @ApiOperation({ + summary: '注册用户', + description: '创建新用户,username、password、email 为必填项', + }) + @ApiResponse({ + status: 201, + description: '注册成功', + type: User, + }) + @ApiResponse({ status: 400, description: '请求参数错误' }) + @ApiResponse({ + status: 409, + description: '用户名、邮箱或其他唯一字段已存在', + }) + create(@Body() createUserDto: CreateUserDto): Promise { + return this.userService.create(createUserDto); + } + + /** + * 查询所有用户 + */ + @Get() + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles('admin', 'super_admin') + @ApiBearerAuth() + @ApiOperation({ + summary: '查询所有用户', + description: '获取所有用户列表(需要管理员权限)', + }) + @ApiResponse({ + status: 200, + description: '查询成功', + type: [User], + }) + @ApiResponse({ status: 401, description: '未授权' }) + @ApiResponse({ status: 403, description: '权限不足' }) + findAll(): Promise { + return this.userService.findAll(); + } + + /** + * 根据 username 或 email 查询单个用户 + */ + @Get('find') + @ApiOperation({ + summary: '查询单个用户', + description: '根据 username 或 email 查询用户', + }) + @ApiResponse({ + status: 200, + description: '查询成功', + type: User, + }) + @ApiResponse({ + status: 400, + description: '请求参数错误,必须提供 username 或 email', + }) + @ApiResponse({ status: 404, description: '用户不存在' }) + findOne(@Query() queryDto: QueryUserDto): Promise { + return this.userService.findOne(queryDto); + } + + /** + * 根据 ID 查询单个用户 + */ + @Get(':id') + @ApiOperation({ + summary: '根据ID查询用户', + description: '根据用户ID获取详细信息', + }) + @ApiParam({ name: 'id', description: '用户ID', type: Number }) + @ApiResponse({ + status: 200, + description: '查询成功', + type: User, + }) + @ApiResponse({ status: 404, description: '用户不存在' }) + findOneById(@Param('id') id: string): Promise { + return this.userService.findOneById(+id); + } + + /** + * 更新用户信息 + */ + @Patch(':id') + @ApiOperation({ + summary: '更新用户信息', + description: '更新用户信息,不允许修改 username、openId、unionId', + }) + @ApiParam({ name: 'id', description: '用户ID', type: Number }) + @ApiResponse({ + status: 200, + description: '更新成功', + type: User, + }) + @ApiResponse({ status: 404, description: '用户不存在' }) + @ApiResponse({ status: 409, description: '邮箱或手机号已存在' }) + update( + @Param('id') id: string, + @Body() updateUserDto: UpdateUserDto, + ): Promise { + return this.userService.update(+id, updateUserDto); + } + + /** + * 修改密码 + */ + @Patch(':id/password') + @HttpCode(HttpStatus.NO_CONTENT) + @ApiOperation({ + summary: '修改密码', + description: '修改用户密码,需要先验证旧密码', + }) + @ApiParam({ name: 'id', description: '用户ID', type: Number }) + @ApiResponse({ status: 204, description: '密码修改成功' }) + @ApiResponse({ status: 400, description: '旧密码错误' }) + @ApiResponse({ status: 404, description: '用户不存在' }) + changePassword( + @Param('id') id: string, + @Body() changePasswordDto: ChangePasswordDto, + ): Promise { + return this.userService.changePassword(+id, changePasswordDto); + } + + /** + * 删除用户(软删除) + */ + @Delete(':id') + @HttpCode(HttpStatus.NO_CONTENT) + @ApiOperation({ + summary: '删除用户', + description: '软删除用户,将状态更新为 deleted', + }) + @ApiParam({ name: 'id', description: '用户ID', type: Number }) + @ApiResponse({ status: 204, description: '删除成功' }) + @ApiResponse({ status: 404, description: '用户不存在' }) + remove(@Param('id') id: string): Promise { + return this.userService.remove(+id); + } +} diff --git a/apps/api/src/modules/user/user.entity.ts b/apps/api/src/modules/user/user.entity.ts new file mode 100644 index 0000000..6271736 --- /dev/null +++ b/apps/api/src/modules/user/user.entity.ts @@ -0,0 +1,142 @@ +import { + Entity, + Column, + PrimaryGeneratedColumn, + CreateDateColumn, + UpdateDateColumn, + Index, +} from 'typeorm'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +@Entity('user') +export class User { + @ApiProperty({ description: '用户ID', example: 1 }) + @PrimaryGeneratedColumn({ name: 'user_id' }) + userId: number; + + @ApiPropertyOptional({ + description: '微信 openId', + example: 'openid123456', + maxLength: 100, + }) + @Column({ name: 'open_id', type: 'varchar', length: 100, nullable: true }) + @Index() + openId?: string; + + @ApiPropertyOptional({ + description: '微信 unionId', + example: 'unionid123456', + maxLength: 100, + }) + @Column({ name: 'union_id', type: 'varchar', length: 100, nullable: true }) + @Index() + unionId?: string; + + @ApiProperty({ + description: '用户名', + example: 'john_doe', + maxLength: 100, + }) + @Column({ name: 'username', type: 'varchar', length: 100 }) + @Index() + username: string; + + @ApiProperty({ + description: '密码哈希值(bcrypt加密)', + example: '$2b$10$...', + maxLength: 255, + }) + @Column({ name: 'password_hash', type: 'varchar', length: 255 }) + passwordHash: string; + + @ApiProperty({ + description: '邮箱', + example: 'user@example.com', + maxLength: 100, + }) + @Column({ name: 'email', type: 'varchar', length: 100 }) + @Index() + email: string; + + @ApiPropertyOptional({ + description: '电话号码', + example: '13800138000', + maxLength: 20, + }) + @Column({ name: 'phone', type: 'varchar', length: 20, nullable: true }) + @Index() + phone?: string; + + @ApiPropertyOptional({ + description: '用户昵称', + example: 'John Doe', + maxLength: 100, + }) + @Column({ name: 'nickname', type: 'varchar', length: 100, nullable: true }) + nickname?: string; + + @ApiPropertyOptional({ + description: '头像URL', + example: 'https://example.com/avatar.jpg', + maxLength: 255, + }) + @Column({ + name: 'avatar_url', + type: 'varchar', + length: 255, + nullable: true, + }) + avatarUrl?: string; + + @ApiProperty({ + description: '创建时间', + example: '2024-01-01T00:00:00.000Z', + }) + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @ApiProperty({ + description: '更新时间', + example: '2024-01-01T00:00:00.000Z', + }) + @UpdateDateColumn({ name: 'updated_at' }) + updatedAt: Date; + + @ApiPropertyOptional({ + description: '最后登录时间', + example: '2024-01-01T00:00:00.000Z', + }) + @Column({ name: 'last_login_at', type: 'timestamp', nullable: true }) + lastLoginAt?: Date; + + @ApiProperty({ + description: '用户状态', + example: 'active', + enum: ['active', 'inactive', 'deleted'], + default: 'active', + }) + @Column({ + name: 'status', + type: 'varchar', + length: 20, + default: 'active', + nullable: false, + }) + status: string; + + @ApiProperty({ + description: '用户角色', + example: 'user', + enum: ['user', 'admin', 'super_admin'], + default: 'user', + }) + @Column({ + name: 'role', + type: 'varchar', + length: 20, + default: 'user', + nullable: false, + }) + @Index() + role: string; +} diff --git a/apps/api/src/modules/user/user.module.ts b/apps/api/src/modules/user/user.module.ts index 309e84a..28a1728 100644 --- a/apps/api/src/modules/user/user.module.ts +++ b/apps/api/src/modules/user/user.module.ts @@ -1,4 +1,13 @@ import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { UserService } from './user.service'; +import { UserController } from './user.controller'; +import { User } from './user.entity'; -@Module({}) +@Module({ + imports: [TypeOrmModule.forFeature([User])], + controllers: [UserController], + providers: [UserService], + exports: [UserService], +}) export class UserModule {} diff --git a/apps/api/src/modules/user/user.service.ts b/apps/api/src/modules/user/user.service.ts new file mode 100644 index 0000000..c8ce85e --- /dev/null +++ b/apps/api/src/modules/user/user.service.ts @@ -0,0 +1,240 @@ +import { + Injectable, + NotFoundException, + ConflictException, + BadRequestException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import * as bcrypt from 'bcrypt'; +import { User } from './user.entity'; +import { CreateUserDto } from './dto/create-user.dto'; +import { UpdateUserDto } from './dto/update-user.dto'; +import { QueryUserDto } from './dto/query-user.dto'; +import { ChangePasswordDto } from './dto/change-password.dto'; + +@Injectable() +export class UserService { + constructor( + @InjectRepository(User) + private readonly userRepository: Repository, + ) {} + + /** + * 注册用户 + */ + async create(createUserDto: CreateUserDto): Promise { + // 检查用户名是否已存在 + const existingByUsername = await this.userRepository.findOne({ + where: { username: createUserDto.username }, + }); + + if (existingByUsername) { + throw new ConflictException( + `用户名 "${createUserDto.username}" 已存在`, + ); + } + + // 检查邮箱是否已存在 + const existingByEmail = await this.userRepository.findOne({ + where: { email: createUserDto.email }, + }); + + if (existingByEmail) { + throw new ConflictException(`邮箱 "${createUserDto.email}" 已存在`); + } + + // 检查 phone 是否已存在(如果提供了) + /* if (createUserDto.phone) { + const existingByPhone = await this.userRepository.findOne({ + where: { phone: createUserDto.phone }, + }); + + if (existingByPhone) { + throw new ConflictException( + `Phone "${createUserDto.phone}" already exists`, + ); + } + } + + // 检查 openId 是否已存在(如果提供了) + if (createUserDto.openId) { + const existingByOpenId = await this.userRepository.findOne({ + where: { openId: createUserDto.openId }, + }); + + if (existingByOpenId) { + throw new ConflictException( + `OpenId "${createUserDto.openId}" already exists`, + ); + } + } + + // 检查 unionId 是否已存在(如果提供了) + if (createUserDto.unionId) { + const existingByUnionId = await this.userRepository.findOne({ + where: { unionId: createUserDto.unionId }, + }); + + if (existingByUnionId) { + throw new ConflictException( + `UnionId "${createUserDto.unionId}" already exists`, + ); + } + } */ + + // 使用 bcrypt 加密密码 + const saltRounds = 10; + const passwordHash = await bcrypt.hash( + createUserDto.password, + saltRounds, + ); + + // 创建用户 + const user = this.userRepository.create({ + username: createUserDto.username, + passwordHash, + email: createUserDto.email, + nickname: createUserDto.nickname, + avatarUrl: createUserDto.avatarUrl, + phone: createUserDto.phone, + openId: createUserDto.openId, + unionId: createUserDto.unionId, + status: 'active', + role: createUserDto.role || 'user', // 默认为普通用户 + }); + + return this.userRepository.save(user); + } + + /** + * 查询所有用户 + */ + async findAll(): Promise { + return this.userRepository.find({ + order: { + createdAt: 'DESC', + }, + }); + } + + /** + * 根据 username 或 email 查询单个用户 + */ + async findOne(queryDto: QueryUserDto): Promise { + if (!queryDto.username && !queryDto.email) { + throw new BadRequestException('必须提供用户名或邮箱'); + } + + const where: { username?: string; email?: string } = {}; + if (queryDto.username) { + where.username = queryDto.username; + } + if (queryDto.email) { + where.email = queryDto.email; + } + + const user = await this.userRepository.findOne({ where }); + + if (!user) { + const identifier = queryDto.username || queryDto.email; + throw new NotFoundException( + `未找到${queryDto.username ? '用户名' : '邮箱'}为 "${identifier}" 的用户`, + ); + } + + return user; + } + + /** + * 根据 ID 查询单个用户 + */ + async findOneById(id: number): Promise { + const user = await this.userRepository.findOne({ + where: { userId: id }, + }); + + if (!user) { + throw new NotFoundException(`未找到ID为 ${id} 的用户`); + } + + return user; + } + + /** + * 更新用户信息(不允许修改 username、openId、unionId) + */ + async update(id: number, updateUserDto: UpdateUserDto): Promise { + const user = await this.findOneById(id); + + // 如果更新邮箱,检查是否与其他用户冲突 + if (updateUserDto.email && updateUserDto.email !== user.email) { + const existingByEmail = await this.userRepository.findOne({ + where: { email: updateUserDto.email }, + }); + + if (existingByEmail) { + throw new ConflictException( + `邮箱 "${updateUserDto.email}" 已存在`, + ); + } + } + + // 如果更新 phone,检查是否与其他用户冲突 + if (updateUserDto.phone && updateUserDto.phone !== user.phone) { + const existingByPhone = await this.userRepository.findOne({ + where: { phone: updateUserDto.phone }, + }); + + if (existingByPhone) { + throw new ConflictException( + `手机号 "${updateUserDto.phone}" 已存在`, + ); + } + } + + // 更新用户信息 + Object.assign(user, updateUserDto); + return this.userRepository.save(user); + } + + /** + * 修改密码(需要先验证旧密码) + */ + async changePassword( + id: number, + changePasswordDto: ChangePasswordDto, + ): Promise { + const user = await this.findOneById(id); + + // 验证旧密码 + const isOldPasswordValid = await bcrypt.compare( + changePasswordDto.oldPassword, + user.passwordHash, + ); + + if (!isOldPasswordValid) { + throw new BadRequestException('旧密码错误'); + } + + // 加密新密码 + const saltRounds = 10; + const newPasswordHash = await bcrypt.hash( + changePasswordDto.newPassword, + saltRounds, + ); + + // 更新密码 + user.passwordHash = newPasswordHash; + await this.userRepository.save(user); + } + + /** + * 删除用户(软删除,更新状态为 deleted) + */ + async remove(id: number): Promise { + const user = await this.findOneById(id); + user.status = 'deleted'; + await this.userRepository.save(user); + } +} diff --git a/apps/api/src/modules/user/user.spec.ts b/apps/api/src/modules/user/user.spec.ts deleted file mode 100644 index f30b81e..0000000 --- a/apps/api/src/modules/user/user.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { User } from './user'; - -describe('User', () => { - it('should be defined', () => { - expect(new User()).toBeDefined(); - }); -}); diff --git a/apps/api/src/modules/user/user.ts b/apps/api/src/modules/user/user.ts deleted file mode 100644 index 4f82c14..0000000 --- a/apps/api/src/modules/user/user.ts +++ /dev/null @@ -1 +0,0 @@ -export class User {} diff --git a/apps/api/test/modules/user/user.service.integration.spec.ts b/apps/api/test/modules/user/user.service.integration.spec.ts new file mode 100644 index 0000000..8efb363 --- /dev/null +++ b/apps/api/test/modules/user/user.service.integration.spec.ts @@ -0,0 +1,293 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { UserService } from '../../../src/modules/user/user.service'; +import { User } from '../../../src/modules/user/user.entity'; +import { UserModule } from '../../../src/modules/user/user.module'; +import { CreateUserDto } from '../../../src/modules/user/dto/create-user.dto'; +import { getDatabaseConfig } from '../../../src/database/database.config'; +import { ConflictException } from '@nestjs/common'; + +describe('UserService (集成测试)', () => { + let service: UserService; + let repository: Repository; + let module: TestingModule; + + beforeAll(async () => { + jest.setTimeout(30000); // 设置超时为 30 秒 + // 创建测试模块,使用真实数据库连接 + module = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + envFilePath: ['.env.development', '.env'], + }), + TypeOrmModule.forRootAsync({ + imports: [ConfigModule], + useFactory: (configService: ConfigService) => { + const config = getDatabaseConfig(configService); + // 使用测试数据库(如果配置了),否则使用开发数据库 + const testDatabase = + configService.get('DB_DATABASE') || + (config.database as string); + return { + ...config, + database: testDatabase, + synchronize: true, // 测试环境允许同步 + dropSchema: false, // 不删除现有数据 + } as typeof config; + }, + inject: [ConfigService], + }), + UserModule, + ], + }).compile(); + + service = module.get(UserService); + repository = module.get>(getRepositoryToken(User)); + + // 清理测试数据(可选) + await repository.clear(); + }); + + afterAll(async () => { + // 测试结束后清理 + // await repository.clear(); + await module.close(); + }); + + describe('create - 集成测试', () => { + it('应该成功在数据库中创建用户', async () => { + const createUserDto: CreateUserDto = { + username: 'test_user_1', + password: 'password123', + email: 'test1@example.com', + nickname: '测试用户1', + }; + + const result = await service.create(createUserDto); + console.log('regitser result', result); + expect(result).toBeDefined(); + expect(result.userId).toBeDefined(); + expect(result.username).toBe(createUserDto.username); + expect(result.email).toBe(createUserDto.email); + expect(result.nickname).toBe(createUserDto.nickname); + expect(result.status).toBe('active'); + expect(result.role).toBe('user'); // 默认角色 + expect(result.passwordHash).toBeDefined(); + expect(result.passwordHash).not.toBe(createUserDto.password); // 密码应该被加密 + + // 验证数据确实保存到数据库 + const savedUser = await repository.findOne({ + where: { userId: result.userId }, + }); + expect(savedUser).toBeDefined(); + expect(savedUser?.username).toBe(createUserDto.username); + expect(savedUser?.email).toBe(createUserDto.email); + + // 清理 + await repository.remove(result); + }); + + it('应该使用默认角色和状态', async () => { + const createUserDto: CreateUserDto = { + username: 'test_user_2', + password: 'password123', + email: 'test2@example.com', + // 不提供 role + }; + + const result = await service.create(createUserDto); + + expect(result.role).toBe('user'); // 默认角色 + expect(result.status).toBe('active'); // 默认状态 + + // 清理 + // await repository.remove(result); + }); + + it('应该支持可选字段', async () => { + const createUserDto: CreateUserDto = { + username: 'test_user_3', + password: 'password123', + email: 'test3@example.com', + nickname: '测试用户3', + avatarUrl: 'https://example.com/avatar.jpg', + phone: '13800138000', + }; + + const result = await service.create(createUserDto); + + expect(result.nickname).toBe(createUserDto.nickname); + expect(result.avatarUrl).toBe(createUserDto.avatarUrl); + expect(result.phone).toBe(createUserDto.phone); + + // 清理 + // await repository.remove(result); + }); + + it('应该抛出 ConflictException 当用户名已存在时', async () => { + const createUserDto: CreateUserDto = { + username: 'duplicate_username', + password: 'password123', + email: 'unique@example.com', + }; + + // 先创建一个用户 + await service.create(createUserDto); + + // 尝试创建相同用户名的用户 + const duplicateDto: CreateUserDto = { + username: 'duplicate_username', + password: 'password456', + email: 'another@example.com', + }; + + await expect(service.create(duplicateDto)).rejects.toThrow( + ConflictException, + ); + await expect(service.create(duplicateDto)).rejects.toThrow( + '用户名', + ); + + // 清理 + // const user = await repository.findOne({ where: { username: 'duplicate_username' } }); + // if (user) await repository.remove(user); + }); + + it('应该抛出 ConflictException 当邮箱已存在时', async () => { + const createUserDto: CreateUserDto = { + username: 'unique_username', + password: 'password123', + email: 'duplicate@example.com', + }; + + // 先创建一个用户 + await service.create(createUserDto); + + // 尝试创建相同邮箱的用户 + const duplicateDto: CreateUserDto = { + username: 'another_username', + password: 'password456', + email: 'duplicate@example.com', + }; + + await expect(service.create(duplicateDto)).rejects.toThrow( + ConflictException, + ); + await expect(service.create(duplicateDto)).rejects.toThrow('邮箱'); + + // 清理 + // const user = await repository.findOne({ where: { email: 'duplicate@example.com' } }); + // if (user) await repository.remove(user); + }); + }); + + describe('findAll - 集成测试', () => { + it('应该返回所有用户列表', async () => { + // 先创建几个测试用户 + const users: CreateUserDto[] = [ + { + username: 'findall_user_1', + password: 'password123', + email: 'findall1@example.com', + nickname: '查询测试用户1', + }, + { + username: 'findall_user_2', + password: 'password123', + email: 'findall2@example.com', + nickname: '查询测试用户2', + }, + { + username: 'findall_user_3', + password: 'password123', + email: 'findall3@example.com', + nickname: '查询测试用户3', + role: 'admin', + }, + ]; + + const createdUsers: User[] = []; + for (const userDto of users) { + const user = await service.create(userDto); + createdUsers.push(user); + } + + // 查询所有用户 + const result = await service.findAll(); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBeGreaterThanOrEqual(users.length); + + // 验证创建的用户都在列表中 + const usernames = result.map((u) => u.username); + users.forEach((userDto) => { + expect(usernames).toContain(userDto.username); + }); + + // 验证排序(按创建时间倒序) + if (result.length > 1) { + for (let i = 0; i < result.length - 1; i++) { + expect( + result[i].createdAt.getTime(), + ).toBeGreaterThanOrEqual(result[i + 1].createdAt.getTime()); + } + } + + // 清理 + // await repository.remove(createdUsers); + }); + + it('应该返回空数组当没有用户时', async () => { + // 清理所有用户 + await repository.clear(); + + const result = await service.findAll(); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(0); + }); + + it('应该包含用户的所有字段', async () => { + // 创建一个完整信息的用户 + const createUserDto: CreateUserDto = { + username: 'full_info_user', + password: 'password123', + email: 'fullinfo@example.com', + nickname: '完整信息用户', + avatarUrl: 'https://example.com/avatar.jpg', + phone: '13800138000', + role: 'admin', + }; + + await service.create(createUserDto); + + // 查询所有用户 + const result = await service.findAll(); + + const foundUser = result.find( + (u) => u.username === createUserDto.username, + ); + + expect(foundUser).toBeDefined(); + expect(foundUser?.userId).toBeDefined(); + expect(foundUser?.username).toBe(createUserDto.username); + expect(foundUser?.email).toBe(createUserDto.email); + expect(foundUser?.nickname).toBe(createUserDto.nickname); + expect(foundUser?.avatarUrl).toBe(createUserDto.avatarUrl); + expect(foundUser?.phone).toBe(createUserDto.phone); + expect(foundUser?.role).toBe(createUserDto.role); + expect(foundUser?.status).toBe('active'); + expect(foundUser?.createdAt).toBeDefined(); + expect(foundUser?.updatedAt).toBeDefined(); + + // 清理 + // if (foundUser) await repository.remove(foundUser); + }); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 12a191e..765c37e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,52 @@ importers: .: {} + apps/admin: + dependencies: + react: + specifier: ^19.2.0 + version: 19.2.0 + react-dom: + specifier: ^19.2.0 + version: 19.2.0(react@19.2.0) + devDependencies: + '@eslint/js': + specifier: ^9.39.1 + version: 9.39.1 + '@types/node': + specifier: ^24.10.0 + version: 24.10.1 + '@types/react': + specifier: ^19.2.2 + version: 19.2.6 + '@types/react-dom': + specifier: ^19.2.2 + version: 19.2.3(@types/react@19.2.6) + '@vitejs/plugin-react-swc': + specifier: ^4.2.1 + version: 4.2.2(vite@7.2.2(@types/node@24.10.1)(terser@5.44.1)) + eslint: + specifier: ^9.39.1 + version: 9.39.1 + eslint-plugin-react-hooks: + specifier: ^7.0.1 + version: 7.0.1(eslint@9.39.1) + eslint-plugin-react-refresh: + specifier: ^0.4.24 + version: 0.4.24(eslint@9.39.1) + globals: + specifier: ^16.5.0 + version: 16.5.0 + typescript: + specifier: ~5.9.3 + version: 5.9.3 + typescript-eslint: + specifier: ^8.46.3 + version: 8.46.4(eslint@9.39.1)(typescript@5.9.3) + vite: + specifier: ^7.2.2 + version: 7.2.2(@types/node@24.10.1)(terser@5.44.1) + apps/api: dependencies: '@nestjs/common': @@ -19,9 +65,15 @@ importers: '@nestjs/core': specifier: ^11.0.1 version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/jwt': + specifier: ^11.0.1 + version: 11.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)) '@nestjs/mapped-types': specifier: ^2.1.0 version: 2.1.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2) + '@nestjs/passport': + specifier: ^11.0.5 + version: 11.0.5(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) '@nestjs/platform-express': specifier: ^11.0.1 version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) @@ -30,7 +82,13 @@ importers: version: 11.2.2(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2) '@nestjs/typeorm': specifier: ^11.0.0 - version: 11.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))) + version: 11.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@swc/core@1.15.2)(@types/node@22.19.1)(typescript@5.9.3))) + '@types/jsonwebtoken': + specifier: ^9.0.10 + version: 9.0.10 + bcrypt: + specifier: ^6.0.0 + version: 6.0.0 body-parser: specifier: ^2.2.0 version: 2.2.0 @@ -49,6 +107,12 @@ importers: helmet: specifier: ^8.1.0 version: 8.1.0 + passport: + specifier: ^0.7.0 + version: 0.7.0 + passport-jwt: + specifier: ^4.0.1 + version: 4.0.1 pg: specifier: ^8.16.3 version: 8.16.3 @@ -60,7 +124,7 @@ importers: version: 7.8.2 typeorm: specifier: ^0.3.27 - version: 0.3.27(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + version: 0.3.27(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@swc/core@1.15.2)(@types/node@22.19.1)(typescript@5.9.3)) devDependencies: '@eslint/eslintrc': specifier: ^3.2.0 @@ -70,13 +134,16 @@ importers: version: 9.39.1 '@nestjs/cli': specifier: ^11.0.0 - version: 11.0.10(@types/node@22.19.1) + version: 11.0.10(@swc/core@1.15.2)(@types/node@22.19.1) '@nestjs/schematics': specifier: ^11.0.0 version: 11.0.9(chokidar@4.0.3)(typescript@5.9.3) '@nestjs/testing': specifier: ^11.0.1 version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-express@11.1.9) + '@types/bcrypt': + specifier: ^6.0.0 + version: 6.0.0 '@types/body-parser': specifier: ^1.19.6 version: 1.19.6 @@ -92,6 +159,9 @@ importers: '@types/node': specifier: ^22.10.7 version: 22.19.1 + '@types/passport-jwt': + specifier: ^4.0.1 + version: 4.0.1 '@types/pg': specifier: ^8.15.6 version: 8.15.6 @@ -112,7 +182,7 @@ importers: version: 16.5.0 jest: specifier: ^30.0.0 - version: 30.2.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + version: 30.2.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.2)(@types/node@22.19.1)(typescript@5.9.3)) prettier: specifier: ^3.4.2 version: 3.6.2 @@ -124,13 +194,13 @@ importers: version: 7.1.4 ts-jest: specifier: ^29.2.5 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.2)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-loader: specifier: ^9.5.2 - version: 9.5.4(typescript@5.9.3)(webpack@5.100.2) + version: 9.5.4(typescript@5.9.3)(webpack@5.100.2(@swc/core@1.15.2)) ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) + version: 10.9.2(@swc/core@1.15.2)(@types/node@22.19.1)(typescript@5.9.3) tsconfig-paths: specifier: ^4.2.0 version: 4.2.0 @@ -359,6 +429,162 @@ packages: '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.0': resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -740,6 +966,11 @@ packages: '@nestjs/websockets': optional: true + '@nestjs/jwt@11.0.1': + resolution: {integrity: sha512-HXSsc7SAnCnjA98TsZqrE7trGtHDnYXWp4Ffy6LwSmck1QvbGYdMzBquXofX5l6tIRpeY4Qidl2Ti2CVG77Pdw==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + '@nestjs/mapped-types@2.1.0': resolution: {integrity: sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==} peerDependencies: @@ -753,6 +984,12 @@ packages: class-validator: optional: true + '@nestjs/passport@11.0.5': + resolution: {integrity: sha512-ulQX6mbjlws92PIM15Naes4F4p2JoxGnIJuUsdXQPT+Oo2sqQmENEZXM7eYuimocfHnKlcfZOuyzbA33LwUlOQ==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + passport: ^0.5.0 || ^0.6.0 || ^0.7.0 + '@nestjs/platform-express@11.1.9': resolution: {integrity: sha512-GVd3+0lO0mJq2m1kl9hDDnVrX3Nd4oH3oDfklz0pZEVEVS0KVSp63ufHq2Lu9cyPdSBuelJr9iPm2QQ1yX+Kmw==} peerDependencies: @@ -835,6 +1072,119 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@rolldown/pluginutils@1.0.0-beta.47': + resolution: {integrity: sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==} + + '@rollup/rollup-android-arm-eabi@4.53.3': + resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.53.3': + resolution: {integrity: sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.53.3': + resolution: {integrity: sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.53.3': + resolution: {integrity: sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.53.3': + resolution: {integrity: sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.53.3': + resolution: {integrity: sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.53.3': + resolution: {integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.53.3': + resolution: {integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.53.3': + resolution: {integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.53.3': + resolution: {integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.53.3': + resolution: {integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.53.3': + resolution: {integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.53.3': + resolution: {integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.53.3': + resolution: {integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.53.3': + resolution: {integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.53.3': + resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.53.3': + resolution: {integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.53.3': + resolution: {integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.53.3': + resolution: {integrity: sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.53.3': + resolution: {integrity: sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.53.3': + resolution: {integrity: sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.53.3': + resolution: {integrity: sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==} + cpu: [x64] + os: [win32] + '@scarf/scarf@1.4.0': resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} @@ -850,6 +1200,81 @@ packages: '@sqltools/formatter@1.2.5': resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==} + '@swc/core-darwin-arm64@1.15.2': + resolution: {integrity: sha512-Ghyz4RJv4zyXzrUC1B2MLQBbppIB5c4jMZJybX2ebdEQAvryEKp3gq1kBksCNsatKGmEgXul88SETU19sMWcrw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.15.2': + resolution: {integrity: sha512-7n/PGJOcL2QoptzL42L5xFFfXY5rFxLHnuz1foU+4ruUTG8x2IebGhtwVTpaDN8ShEv2UZObBlT1rrXTba15Zw==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.15.2': + resolution: {integrity: sha512-ZUQVCfRJ9wimuxkStRSlLwqX4TEDmv6/J+E6FicGkQ6ssLMWoKDy0cAo93HiWt/TWEee5vFhFaSQYzCuBEGO6A==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.15.2': + resolution: {integrity: sha512-GZh3pYBmfnpQ+JIg+TqLuz+pM+Mjsk5VOzi8nwKn/m+GvQBsxD5ectRtxuWUxMGNG8h0lMy4SnHRqdK3/iJl7A==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-arm64-musl@1.15.2': + resolution: {integrity: sha512-5av6VYZZeneiYIodwzGMlnyVakpuYZryGzFIbgu1XP8wVylZxduEzup4eP8atiMDFmIm+s4wn8GySJmYqeJC0A==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-x64-gnu@1.15.2': + resolution: {integrity: sha512-1nO/UfdCLuT/uE/7oB3EZgTeZDCIa6nL72cFEpdegnqpJVNDI6Qb8U4g/4lfVPkmHq2lvxQ0L+n+JdgaZLhrRA==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-linux-x64-musl@1.15.2': + resolution: {integrity: sha512-Ksfrb0Tx310kr+TLiUOvB/I80lyZ3lSOp6cM18zmNRT/92NB4mW8oX2Jo7K4eVEI2JWyaQUAFubDSha2Q+439A==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-win32-arm64-msvc@1.15.2': + resolution: {integrity: sha512-IzUb5RlMUY0r1A9IuJrQ7Tbts1wWb73/zXVXT8VhewbHGoNlBKE0qUhKMED6Tv4wDF+pmbtUJmKXDthytAvLmg==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.15.2': + resolution: {integrity: sha512-kCATEzuY2LP9AlbU2uScjcVhgnCAkRdu62vbce17Ro5kxEHxYWcugkveyBRS3AqZGtwAKYbMAuNloer9LS/hpw==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.15.2': + resolution: {integrity: sha512-iJaHeYCF4jTn7OEKSa3KRiuVFIVYts8jYjNmCdyz1u5g8HRyTDISD76r8+ljEOgm36oviRQvcXaw6LFp1m0yyA==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.15.2': + resolution: {integrity: sha512-OQm+yJdXxvSjqGeaWhP6Ia264ogifwAO7Q12uTDVYj/Ks4jBTI4JknlcjDRAXtRhqbWsfbZyK/5RtuIPyptk3w==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '>=0.5.17' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/types@0.1.25': + resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==} + '@tokenizer/inflate@0.3.1': resolution: {integrity: sha512-4oeoZEBQdLdt5WmP/hx1KZ6D3/Oid/0cUb2nk4F0pTDAWy+KCH3/EnAkZF/bvckWo8I33EqBm01lIPgmgc8rCA==} engines: {node: '>=18'} @@ -884,6 +1309,9 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/bcrypt@6.0.0': + resolution: {integrity: sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==} + '@types/body-parser@1.19.6': resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} @@ -929,15 +1357,33 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@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/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@22.19.1': resolution: {integrity: sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==} + '@types/node@24.10.1': + resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} + + '@types/passport-jwt@4.0.1': + resolution: {integrity: sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==} + + '@types/passport-strategy@0.2.38': + resolution: {integrity: sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==} + + '@types/passport@1.0.17': + resolution: {integrity: sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==} + '@types/pg@8.15.6': resolution: {integrity: sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==} @@ -947,6 +1393,14 @@ packages: '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.6': + resolution: {integrity: sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==} + '@types/send@0.17.6': resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} @@ -1131,6 +1585,12 @@ packages: cpu: [x64] os: [win32] + '@vitejs/plugin-react-swc@4.2.2': + resolution: {integrity: sha512-x+rE6tsxq/gxrEJN3Nv3dIV60lFflPj94c90b+NNo6n1QV1QQUTLoL0MpaOVasUZ0zqVBn7ead1B5ecx1JAGfA==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^4 || ^5 || ^6 || ^7 + '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -1342,6 +1802,10 @@ packages: resolution: {integrity: sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ==} hasBin: true + bcrypt@6.0.0: + resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==} + engines: {node: '>= 18'} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -1371,6 +1835,9 @@ packages: bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + 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==} @@ -1567,6 +2034,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + dayjs@1.11.19: resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} @@ -1643,6 +2113,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -1689,6 +2162,11 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -1724,6 +2202,17 @@ packages: eslint-config-prettier: optional: true + eslint-plugin-react-hooks@7.0.1: + resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react-refresh@0.4.24: + resolution: {integrity: sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==} + peerDependencies: + eslint: '>=8.40' + eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} @@ -1837,6 +2326,15 @@ packages: fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} @@ -2014,6 +2512,12 @@ packages: resolution: {integrity: sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==} engines: {node: '>=18.0.0'} + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -2329,6 +2833,16 @@ packages: jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + + jwa@1.4.2: + resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==} + + jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -2362,12 +2876,33 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + 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.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -2494,6 +3029,11 @@ packages: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + napi-postinstall@0.3.4: resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -2516,9 +3056,17 @@ packages: node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + node-addon-api@8.5.0: + resolution: {integrity: sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==} + engines: {node: ^18 || ^20 || >= 21} + node-emoji@1.11.0: resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} @@ -2599,6 +3147,17 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + passport-jwt@4.0.1: + resolution: {integrity: sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==} + + passport-strategy@1.0.0: + resolution: {integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==} + engines: {node: '>= 0.4.0'} + + passport@0.7.0: + resolution: {integrity: sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==} + engines: {node: '>= 0.4.0'} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -2626,6 +3185,9 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pause@0.0.1: + resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==} + pg-cloudflare@1.2.7: resolution: {integrity: sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==} @@ -2691,6 +3253,10 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} engines: {node: '>=4'} @@ -2753,9 +3319,18 @@ packages: resolution: {integrity: sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==} engines: {node: '>= 0.10'} + react-dom@19.2.0: + resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==} + peerDependencies: + react: ^19.2.0 + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react@19.2.0: + resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} + engines: {node: '>=0.10.0'} + readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} @@ -2795,6 +3370,11 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rollup@4.53.3: + resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -2814,6 +3394,9 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + schema-utils@3.3.0: resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} engines: {node: '>= 10.13.0'} @@ -2889,6 +3472,10 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + source-map-support@0.5.13: resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} @@ -3033,6 +3620,10 @@ packages: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -3241,6 +3832,9 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -3264,6 +3858,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + uuid@11.1.0: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true @@ -3283,6 +3881,46 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vite@7.2.2: + resolution: {integrity: sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} @@ -3377,6 +4015,15 @@ packages: resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} engines: {node: '>=18'} + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@4.1.12: + resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} + snapshots: '@angular-devkit/core@19.2.15(chokidar@4.0.3)': @@ -3647,6 +4294,84 @@ snapshots: tslib: 2.8.1 optional: true + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.1)': dependencies: eslint: 9.39.1 @@ -3878,7 +4603,7 @@ snapshots: jest-util: 30.2.0 slash: 3.0.0 - '@jest/core@30.2.0(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))': + '@jest/core@30.2.0(ts-node@10.9.2(@swc/core@1.15.2)(@types/node@22.19.1)(typescript@5.9.3))': dependencies: '@jest/console': 30.2.0 '@jest/pattern': 30.0.1 @@ -3893,7 +4618,7 @@ snapshots: exit-x: 0.2.2 graceful-fs: 4.2.11 jest-changed-files: 30.2.0 - jest-config: 30.2.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + jest-config: 30.2.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.2)(@types/node@22.19.1)(typescript@5.9.3)) jest-haste-map: 30.2.0 jest-message-util: 30.2.0 jest-regex-util: 30.0.1 @@ -4088,7 +4813,7 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@nestjs/cli@11.0.10(@types/node@22.19.1)': + '@nestjs/cli@11.0.10(@swc/core@1.15.2)(@types/node@22.19.1)': dependencies: '@angular-devkit/core': 19.2.15(chokidar@4.0.3) '@angular-devkit/schematics': 19.2.15(chokidar@4.0.3) @@ -4099,7 +4824,7 @@ snapshots: chokidar: 4.0.3 cli-table3: 0.6.5 commander: 4.1.1 - fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.8.3)(webpack@5.100.2) + fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.8.3)(webpack@5.100.2(@swc/core@1.15.2)) glob: 11.0.3 node-emoji: 1.11.0 ora: 5.4.1 @@ -4107,8 +4832,10 @@ snapshots: tsconfig-paths: 4.2.0 tsconfig-paths-webpack-plugin: 4.2.0 typescript: 5.8.3 - webpack: 5.100.2 + webpack: 5.100.2(@swc/core@1.15.2) webpack-node-externals: 3.0.0 + optionalDependencies: + '@swc/core': 1.15.2 transitivePeerDependencies: - '@types/node' - esbuild @@ -4152,6 +4879,12 @@ snapshots: optionalDependencies: '@nestjs/platform-express': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) + '@nestjs/jwt@11.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))': + dependencies: + '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@types/jsonwebtoken': 9.0.10 + jsonwebtoken: 9.0.2 + '@nestjs/mapped-types@2.1.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)': dependencies: '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -4160,6 +4893,11 @@ snapshots: class-transformer: 0.5.1 class-validator: 0.14.2 + '@nestjs/passport@11.0.5(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)': + dependencies: + '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + passport: 0.7.0 + '@nestjs/platform-express@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)': dependencies: '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -4217,13 +4955,13 @@ snapshots: optionalDependencies: '@nestjs/platform-express': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) - '@nestjs/typeorm@11.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))': + '@nestjs/typeorm@11.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@swc/core@1.15.2)(@types/node@22.19.1)(typescript@5.9.3)))': dependencies: '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 rxjs: 7.8.2 - typeorm: 0.3.27(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + typeorm: 0.3.27(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@swc/core@1.15.2)(@types/node@22.19.1)(typescript@5.9.3)) '@noble/hashes@1.8.0': {} @@ -4252,6 +4990,74 @@ snapshots: '@pkgr/core@0.2.9': {} + '@rolldown/pluginutils@1.0.0-beta.47': {} + + '@rollup/rollup-android-arm-eabi@4.53.3': + optional: true + + '@rollup/rollup-android-arm64@4.53.3': + optional: true + + '@rollup/rollup-darwin-arm64@4.53.3': + optional: true + + '@rollup/rollup-darwin-x64@4.53.3': + optional: true + + '@rollup/rollup-freebsd-arm64@4.53.3': + optional: true + + '@rollup/rollup-freebsd-x64@4.53.3': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.53.3': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.53.3': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.53.3': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.53.3': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-x64-musl@4.53.3': + optional: true + + '@rollup/rollup-openharmony-arm64@4.53.3': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.53.3': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.53.3': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.53.3': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.53.3': + optional: true + '@scarf/scarf@1.4.0': {} '@sinclair/typebox@0.34.41': {} @@ -4266,6 +5072,58 @@ snapshots: '@sqltools/formatter@1.2.5': {} + '@swc/core-darwin-arm64@1.15.2': + optional: true + + '@swc/core-darwin-x64@1.15.2': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.15.2': + optional: true + + '@swc/core-linux-arm64-gnu@1.15.2': + optional: true + + '@swc/core-linux-arm64-musl@1.15.2': + optional: true + + '@swc/core-linux-x64-gnu@1.15.2': + optional: true + + '@swc/core-linux-x64-musl@1.15.2': + optional: true + + '@swc/core-win32-arm64-msvc@1.15.2': + optional: true + + '@swc/core-win32-ia32-msvc@1.15.2': + optional: true + + '@swc/core-win32-x64-msvc@1.15.2': + optional: true + + '@swc/core@1.15.2': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.25 + optionalDependencies: + '@swc/core-darwin-arm64': 1.15.2 + '@swc/core-darwin-x64': 1.15.2 + '@swc/core-linux-arm-gnueabihf': 1.15.2 + '@swc/core-linux-arm64-gnu': 1.15.2 + '@swc/core-linux-arm64-musl': 1.15.2 + '@swc/core-linux-x64-gnu': 1.15.2 + '@swc/core-linux-x64-musl': 1.15.2 + '@swc/core-win32-arm64-msvc': 1.15.2 + '@swc/core-win32-ia32-msvc': 1.15.2 + '@swc/core-win32-x64-msvc': 1.15.2 + + '@swc/counter@0.1.3': {} + + '@swc/types@0.1.25': + dependencies: + '@swc/counter': 0.1.3 + '@tokenizer/inflate@0.3.1': dependencies: debug: 4.4.3 @@ -4310,6 +5168,10 @@ snapshots: dependencies: '@babel/types': 7.28.5 + '@types/bcrypt@6.0.0': + dependencies: + '@types/node': 22.19.1 + '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 @@ -4370,14 +5232,39 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/jsonwebtoken@9.0.10': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 22.19.1 + '@types/methods@1.1.4': {} '@types/mime@1.3.5': {} + '@types/ms@2.1.0': {} + '@types/node@22.19.1': dependencies: undici-types: 6.21.0 + '@types/node@24.10.1': + dependencies: + undici-types: 7.16.0 + + '@types/passport-jwt@4.0.1': + dependencies: + '@types/jsonwebtoken': 9.0.10 + '@types/passport-strategy': 0.2.38 + + '@types/passport-strategy@0.2.38': + dependencies: + '@types/express': 5.0.5 + '@types/passport': 1.0.17 + + '@types/passport@1.0.17': + dependencies: + '@types/express': 5.0.5 + '@types/pg@8.15.6': dependencies: '@types/node': 22.19.1 @@ -4388,6 +5275,14 @@ snapshots: '@types/range-parser@1.2.7': {} + '@types/react-dom@19.2.3(@types/react@19.2.6)': + dependencies: + '@types/react': 19.2.6 + + '@types/react@19.2.6': + dependencies: + csstype: 3.2.3 + '@types/send@0.17.6': dependencies: '@types/mime': 1.3.5 @@ -4579,6 +5474,14 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true + '@vitejs/plugin-react-swc@4.2.2(vite@7.2.2(@types/node@24.10.1)(terser@5.44.1))': + dependencies: + '@rolldown/pluginutils': 1.0.0-beta.47 + '@swc/core': 1.15.2 + vite: 7.2.2(@types/node@24.10.1)(terser@5.44.1) + transitivePeerDependencies: + - '@swc/helpers' + '@webassemblyjs/ast@1.14.1': dependencies: '@webassemblyjs/helper-numbers': 1.13.2 @@ -4816,6 +5719,11 @@ snapshots: baseline-browser-mapping@2.8.28: {} + bcrypt@6.0.0: + dependencies: + node-addon-api: 8.5.0 + node-gyp-build: 4.8.4 + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -4865,6 +5773,8 @@ snapshots: dependencies: node-int64: 0.4.0 + buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} buffer@5.7.1: @@ -5048,6 +5958,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + csstype@3.2.3: {} + dayjs@1.11.19: {} debug@2.6.9: @@ -5101,6 +6013,10 @@ snapshots: eastasianwidth@0.2.0: {} + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + ee-first@1.1.1: {} electron-to-chromium@1.5.254: {} @@ -5139,6 +6055,35 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -5161,6 +6106,21 @@ snapshots: '@types/eslint': 9.6.1 eslint-config-prettier: 10.1.8(eslint@9.39.1) + eslint-plugin-react-hooks@7.0.1(eslint@9.39.1): + dependencies: + '@babel/core': 7.28.5 + '@babel/parser': 7.28.5 + eslint: 9.39.1 + hermes-parser: 0.25.1 + zod: 4.1.12 + zod-validation-error: 4.0.2(zod@4.1.12) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-refresh@0.4.24(eslint@9.39.1): + dependencies: + eslint: 9.39.1 + eslint-scope@5.1.1: dependencies: esrecurse: 4.3.0 @@ -5328,6 +6288,10 @@ snapshots: dependencies: bser: 2.1.1 + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + fflate@0.8.2: {} file-entry-cache@8.0.0: @@ -5384,7 +6348,7 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - fork-ts-checker-webpack-plugin@9.1.0(typescript@5.8.3)(webpack@5.100.2): + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.8.3)(webpack@5.100.2(@swc/core@1.15.2)): dependencies: '@babel/code-frame': 7.27.1 chalk: 4.1.2 @@ -5399,7 +6363,7 @@ snapshots: semver: 7.7.3 tapable: 2.3.0 typescript: 5.8.3 - webpack: 5.100.2 + webpack: 5.100.2(@swc/core@1.15.2) form-data@4.0.5: dependencies: @@ -5534,6 +6498,12 @@ snapshots: helmet@8.1.0: {} + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + html-escaper@2.0.2: {} http-errors@2.0.0: @@ -5690,15 +6660,15 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@30.2.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): + jest-cli@30.2.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.2)(@types/node@22.19.1)(typescript@5.9.3)): dependencies: - '@jest/core': 30.2.0(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + '@jest/core': 30.2.0(ts-node@10.9.2(@swc/core@1.15.2)(@types/node@22.19.1)(typescript@5.9.3)) '@jest/test-result': 30.2.0 '@jest/types': 30.2.0 chalk: 4.1.2 exit-x: 0.2.2 import-local: 3.2.0 - jest-config: 30.2.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + jest-config: 30.2.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.2)(@types/node@22.19.1)(typescript@5.9.3)) jest-util: 30.2.0 jest-validate: 30.2.0 yargs: 17.7.2 @@ -5709,7 +6679,7 @@ snapshots: - supports-color - ts-node - jest-config@30.2.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): + jest-config@30.2.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.2)(@types/node@22.19.1)(typescript@5.9.3)): dependencies: '@babel/core': 7.28.5 '@jest/get-type': 30.1.0 @@ -5737,7 +6707,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 22.19.1 - ts-node: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) + ts-node: 10.9.2(@swc/core@1.15.2)(@types/node@22.19.1)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -5963,12 +6933,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@30.2.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): + jest@30.2.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.2)(@types/node@22.19.1)(typescript@5.9.3)): dependencies: - '@jest/core': 30.2.0(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + '@jest/core': 30.2.0(ts-node@10.9.2(@swc/core@1.15.2)(@types/node@22.19.1)(typescript@5.9.3)) '@jest/types': 30.2.0 import-local: 3.2.0 - jest-cli: 30.2.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + jest-cli: 30.2.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.2)(@types/node@22.19.1)(typescript@5.9.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -6009,6 +6979,30 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonwebtoken@9.0.2: + dependencies: + jws: 3.2.2 + 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.7.3 + + jwa@1.4.2: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@3.2.2: + dependencies: + jwa: 1.4.2 + safe-buffer: 5.2.1 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -6036,10 +7030,24 @@ snapshots: dependencies: p-locate: 5.0.0 + 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.memoize@4.1.2: {} lodash.merge@4.6.2: {} + lodash.once@4.1.1: {} + lodash@4.17.21: {} log-symbols@4.1.0: @@ -6144,6 +7152,8 @@ snapshots: mute-stream@2.0.0: {} + nanoid@3.3.11: {} + napi-postinstall@0.3.4: {} natural-compare@1.4.0: {} @@ -6156,10 +7166,14 @@ snapshots: node-abort-controller@3.1.1: {} + node-addon-api@8.5.0: {} + node-emoji@1.11.0: dependencies: lodash: 4.17.21 + node-gyp-build@4.8.4: {} + node-int64@0.4.0: {} node-releases@2.0.27: {} @@ -6242,6 +7256,19 @@ snapshots: parseurl@1.3.3: {} + passport-jwt@4.0.1: + dependencies: + jsonwebtoken: 9.0.2 + passport-strategy: 1.0.0 + + passport-strategy@1.0.0: {} + + passport@0.7.0: + dependencies: + passport-strategy: 1.0.0 + pause: 0.0.1 + utils-merge: 1.0.1 + path-exists@4.0.0: {} path-is-absolute@1.0.1: {} @@ -6262,6 +7289,8 @@ snapshots: path-type@4.0.0: {} + pause@0.0.1: {} + pg-cloudflare@1.2.7: optional: true @@ -6315,6 +7344,12 @@ snapshots: possible-typed-array-names@1.1.0: {} + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + postgres-array@2.0.0: {} postgres-bytea@1.0.0: {} @@ -6367,8 +7402,15 @@ snapshots: iconv-lite: 0.7.0 unpipe: 1.0.0 + react-dom@19.2.0(react@19.2.0): + dependencies: + react: 19.2.0 + scheduler: 0.27.0 + react-is@18.3.1: {} + react@19.2.0: {} + readable-stream@3.6.2: dependencies: inherits: 2.0.4 @@ -6398,6 +7440,34 @@ snapshots: reusify@1.1.0: {} + rollup@4.53.3: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.53.3 + '@rollup/rollup-android-arm64': 4.53.3 + '@rollup/rollup-darwin-arm64': 4.53.3 + '@rollup/rollup-darwin-x64': 4.53.3 + '@rollup/rollup-freebsd-arm64': 4.53.3 + '@rollup/rollup-freebsd-x64': 4.53.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.53.3 + '@rollup/rollup-linux-arm-musleabihf': 4.53.3 + '@rollup/rollup-linux-arm64-gnu': 4.53.3 + '@rollup/rollup-linux-arm64-musl': 4.53.3 + '@rollup/rollup-linux-loong64-gnu': 4.53.3 + '@rollup/rollup-linux-ppc64-gnu': 4.53.3 + '@rollup/rollup-linux-riscv64-gnu': 4.53.3 + '@rollup/rollup-linux-riscv64-musl': 4.53.3 + '@rollup/rollup-linux-s390x-gnu': 4.53.3 + '@rollup/rollup-linux-x64-gnu': 4.53.3 + '@rollup/rollup-linux-x64-musl': 4.53.3 + '@rollup/rollup-openharmony-arm64': 4.53.3 + '@rollup/rollup-win32-arm64-msvc': 4.53.3 + '@rollup/rollup-win32-ia32-msvc': 4.53.3 + '@rollup/rollup-win32-x64-gnu': 4.53.3 + '@rollup/rollup-win32-x64-msvc': 4.53.3 + fsevents: 2.3.3 + router@2.2.0: dependencies: debug: 4.4.3 @@ -6424,6 +7494,8 @@ snapshots: safer-buffer@2.1.2: {} + scheduler@0.27.0: {} + schema-utils@3.3.0: dependencies: '@types/json-schema': 7.0.15 @@ -6527,6 +7599,8 @@ snapshots: slash@3.0.0: {} + source-map-js@1.2.1: {} + source-map-support@0.5.13: dependencies: buffer-from: 1.1.2 @@ -6641,14 +7715,16 @@ snapshots: tapable@2.3.0: {} - terser-webpack-plugin@5.3.14(webpack@5.100.2): + terser-webpack-plugin@5.3.14(@swc/core@1.15.2)(webpack@5.100.2(@swc/core@1.15.2)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 serialize-javascript: 6.0.2 terser: 5.44.1 - webpack: 5.100.2 + webpack: 5.100.2(@swc/core@1.15.2) + optionalDependencies: + '@swc/core': 1.15.2 terser@5.44.1: dependencies: @@ -6663,6 +7739,11 @@ snapshots: glob: 7.2.3 minimatch: 3.1.2 + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + tmpl@1.0.5: {} to-buffer@1.2.2: @@ -6689,12 +7770,12 @@ snapshots: dependencies: typescript: 5.9.3 - ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3): + ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.2)(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 handlebars: 4.7.8 - jest: 30.2.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + jest: 30.2.0(@types/node@22.19.1)(ts-node@10.9.2(@swc/core@1.15.2)(@types/node@22.19.1)(typescript@5.9.3)) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 @@ -6709,7 +7790,7 @@ snapshots: babel-jest: 30.2.0(@babel/core@7.28.5) jest-util: 30.2.0 - ts-loader@9.5.4(typescript@5.9.3)(webpack@5.100.2): + ts-loader@9.5.4(typescript@5.9.3)(webpack@5.100.2(@swc/core@1.15.2)): dependencies: chalk: 4.1.2 enhanced-resolve: 5.18.3 @@ -6717,9 +7798,9 @@ snapshots: semver: 7.7.3 source-map: 0.7.6 typescript: 5.9.3 - webpack: 5.100.2 + webpack: 5.100.2(@swc/core@1.15.2) - ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3): + ts-node@10.9.2(@swc/core@1.15.2)(@types/node@22.19.1)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.12 @@ -6736,6 +7817,8 @@ snapshots: typescript: 5.9.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.15.2 tsconfig-paths-webpack-plugin@4.2.0: dependencies: @@ -6781,7 +7864,7 @@ snapshots: typedarray@0.0.6: {} - typeorm@0.3.27(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): + typeorm@0.3.27(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@swc/core@1.15.2)(@types/node@22.19.1)(typescript@5.9.3)): dependencies: '@sqltools/formatter': 1.2.5 ansis: 3.17.0 @@ -6800,7 +7883,7 @@ snapshots: yargs: 17.7.2 optionalDependencies: pg: 8.16.3 - ts-node: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) + ts-node: 10.9.2(@swc/core@1.15.2)(@types/node@22.19.1)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -6831,6 +7914,8 @@ snapshots: undici-types@6.21.0: {} + undici-types@7.16.0: {} + universalify@2.0.1: {} unpipe@1.0.0: {} @@ -6871,6 +7956,8 @@ snapshots: util-deprecate@1.0.2: {} + utils-merge@1.0.1: {} + uuid@11.1.0: {} v8-compile-cache-lib@3.0.1: {} @@ -6885,6 +7972,19 @@ snapshots: vary@1.1.2: {} + vite@7.2.2(@types/node@24.10.1)(terser@5.44.1): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.53.3 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.10.1 + fsevents: 2.3.3 + terser: 5.44.1 + walker@1.0.8: dependencies: makeerror: 1.0.12 @@ -6902,7 +8002,7 @@ snapshots: webpack-sources@3.3.3: {} - webpack@5.100.2: + webpack@5.100.2(@swc/core@1.15.2): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -6926,7 +8026,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.0 - terser-webpack-plugin: 5.3.14(webpack@5.100.2) + terser-webpack-plugin: 5.3.14(@swc/core@1.15.2)(webpack@5.100.2(@swc/core@1.15.2)) watchpack: 2.4.4 webpack-sources: 3.3.3 transitivePeerDependencies: @@ -7000,3 +8100,9 @@ snapshots: yocto-queue@0.1.0: {} yoctocolors-cjs@2.1.3: {} + + zod-validation-error@4.0.2(zod@4.1.12): + dependencies: + zod: 4.1.12 + + zod@4.1.12: {}