feat: add JWT authentication, user registration, and device management

- AuthService with register, login, refresh token flows
- JWT strategy + guard (Passport)
- User, Home, Device services
- REST endpoints: /auth/register, /auth/login, /auth/me, /auth/refresh
- Device registration with long-lived JWT tokens (365d)
- Device listing per home
- bcrypt password hashing (cost 12) and device token hashing
- Fix snake_case column names on all entity date columns

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ordinarthur 2026-03-27 09:27:13 +01:00
parent 674109ea22
commit 0115669464
24 changed files with 679 additions and 20 deletions

View File

@ -25,12 +25,17 @@
"@nestjs/common": "^11.1.17",
"@nestjs/config": "^4.0.3",
"@nestjs/core": "^11.1.17",
"@nestjs/jwt": "^11.0.2",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.1.17",
"@nestjs/typeorm": "^11.0.0",
"bcrypt": "^6.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.15.1",
"dotenv": "^17.3.1",
"ioredis": "^5.10.1",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"pg": "^8.20.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2",
@ -40,8 +45,10 @@
"@nestjs/cli": "^11.0.16",
"@nestjs/schematics": "^11.0.9",
"@nestjs/testing": "^11.1.17",
"@types/bcrypt": "^6.0.0",
"@types/jest": "^30.0.0",
"@types/node": "^25.5.0",
"@types/passport-jwt": "^4.0.1",
"@typescript-eslint/eslint-plugin": "^8.57.2",
"@typescript-eslint/parser": "^8.57.2",
"eslint": "^10.1.0",

View File

@ -0,0 +1,36 @@
import { Controller, Post, Body, UseGuards, Request, Get } from '@nestjs/common';
import { AuthService } from '../../../../core/services/auth.service';
import { RegisterDto } from './dto/register.dto';
import { LoginDto } from './dto/login.dto';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('register')
async register(@Body() dto: RegisterDto) {
return this.authService.register({
email: dto.email,
password: dto.password,
displayName: dto.displayName,
homeName: dto.homeName,
});
}
@Post('login')
async login(@Body() dto: LoginDto) {
return this.authService.login(dto.email, dto.password);
}
@Post('refresh')
async refresh(@Body('refreshToken') refreshToken: string) {
return this.authService.refreshAccessToken(refreshToken);
}
@UseGuards(JwtAuthGuard)
@Get('me')
async me(@Request() req: { user: { id: string; email: string; homeId: string } }) {
return req.user;
}
}

View File

@ -0,0 +1,9 @@
import { IsEmail, IsString } from 'class-validator';
export class LoginDto {
@IsEmail()
email!: string;
@IsString()
password!: string;
}

View File

@ -0,0 +1,21 @@
import { IsEmail, IsString, MinLength, MaxLength } from 'class-validator';
export class RegisterDto {
@IsEmail()
email!: string;
@IsString()
@MinLength(8)
@MaxLength(128)
password!: string;
@IsString()
@MinLength(1)
@MaxLength(100)
displayName!: string;
@IsString()
@MinLength(1)
@MaxLength(100)
homeName!: string;
}

View File

@ -0,0 +1,5 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

View File

@ -0,0 +1,31 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
export interface JwtPayload {
sub: string;
email: string;
homeId: string;
type: 'user' | 'device';
}
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('auth.jwtSecret', 'dev-secret-change-in-production'),
});
}
validate(payload: JwtPayload) {
return {
id: payload.sub,
email: payload.email,
homeId: payload.homeId,
type: payload.type,
};
}
}

View File

@ -0,0 +1,27 @@
import { Controller, Post, Get, Body, UseGuards, Request } from '@nestjs/common';
import { AuthService } from '../../../../core/services/auth.service';
import { DeviceService } from '../../../../core/services/device.service';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RegisterDeviceDto } from './dto/register-device.dto';
@Controller('devices')
@UseGuards(JwtAuthGuard)
export class DeviceController {
constructor(
private readonly authService: AuthService,
private readonly deviceService: DeviceService,
) {}
@Post()
async registerDevice(
@Body() dto: RegisterDeviceDto,
@Request() req: { user: { homeId: string } },
) {
return this.authService.registerDevice(req.user.homeId, dto.name);
}
@Get()
async listDevices(@Request() req: { user: { homeId: string } }) {
return this.deviceService.findByHomeId(req.user.homeId);
}
}

View File

@ -0,0 +1,8 @@
import { IsString, MinLength, MaxLength } from 'class-validator';
export class RegisterDeviceDto {
@IsString()
@MinLength(1)
@MaxLength(100)
name!: string;
}

View File

@ -1,23 +1,45 @@
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { typeormConfig } from './config/typeorm.config';
import { redisConfig } from './config/redis.config';
import { appConfig } from './config/app.config';
import { authConfig } from './config/auth.config';
import { Home } from './core/domain/entities/home.entity';
import { User } from './core/domain/entities/user.entity';
import { Device } from './core/domain/entities/device.entity';
import { AuthService } from './core/services/auth.service';
import { UserService } from './core/services/user.service';
import { HomeService } from './core/services/home.service';
import { DeviceService } from './core/services/device.service';
import { JwtStrategy } from './adapters/inbound/rest/auth/strategies/jwt.strategy';
import { AuthController } from './adapters/inbound/rest/auth/auth.controller';
import { DeviceController } from './adapters/inbound/rest/device/device.controller';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [appConfig, redisConfig],
load: [appConfig, redisConfig, authConfig],
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => typeormConfig(configService),
}),
TypeOrmModule.forFeature([Home, User, Device]),
PassportModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
secret: configService.get<string>('auth.jwtSecret', 'dev-secret-change-in-production'),
}),
}),
],
controllers: [],
providers: [],
controllers: [AuthController, DeviceController],
providers: [AuthService, UserService, HomeService, DeviceService, JwtStrategy],
})
export class AppModule {}

View File

@ -0,0 +1,8 @@
import { registerAs } from '@nestjs/config';
export const authConfig = registerAs('auth', () => ({
jwtSecret: process.env.JWT_SECRET || 'dev-secret-change-in-production',
jwtExpiresIn: process.env.JWT_EXPIRES_IN || '15m',
jwtRefreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d',
deviceTokenExpiresIn: process.env.DEVICE_TOKEN_EXPIRES_IN || '365d',
}));

View File

@ -64,6 +64,6 @@ export class ConversationSession {
@OneToMany(() => MemoryEntry, (memory) => memory.session)
memories!: MemoryEntry[];
@CreateDateColumn({ type: 'timestamptz' })
@CreateDateColumn({ type: 'timestamptz', name: 'created_at' })
createdAt!: Date;
}

View File

@ -45,9 +45,9 @@ export class Device {
@Column({ type: 'timestamptz', nullable: true, name: 'last_seen_at' })
lastSeenAt!: Date | null;
@CreateDateColumn({ type: 'timestamptz' })
@CreateDateColumn({ type: 'timestamptz', name: 'created_at' })
createdAt!: Date;
@UpdateDateColumn({ type: 'timestamptz' })
@UpdateDateColumn({ type: 'timestamptz', name: 'updated_at' })
updatedAt!: Date;
}

View File

@ -23,9 +23,9 @@ export class Home {
@OneToMany(() => Device, (device) => device.home)
devices!: Device[];
@CreateDateColumn({ type: 'timestamptz' })
@CreateDateColumn({ type: 'timestamptz', name: 'created_at' })
createdAt!: Date;
@UpdateDateColumn({ type: 'timestamptz' })
@UpdateDateColumn({ type: 'timestamptz', name: 'updated_at' })
updatedAt!: Date;
}

View File

@ -32,6 +32,6 @@ export class MemoryEmbedding {
@Column({ type: 'varchar', nullable: true })
embedding!: string | null;
@CreateDateColumn({ type: 'timestamptz' })
@CreateDateColumn({ type: 'timestamptz', name: 'created_at' })
createdAt!: Date;
}

View File

@ -54,9 +54,9 @@ export class MemoryEntry {
@Column({ type: 'boolean', default: true, name: 'is_active' })
isActive!: boolean;
@CreateDateColumn({ type: 'timestamptz' })
@CreateDateColumn({ type: 'timestamptz', name: 'created_at' })
createdAt!: Date;
@UpdateDateColumn({ type: 'timestamptz' })
@UpdateDateColumn({ type: 'timestamptz', name: 'updated_at' })
updatedAt!: Date;
}

View File

@ -45,6 +45,6 @@ export class Message {
@Column({ type: 'integer', default: 0, name: 'tokens_used' })
tokensUsed!: number;
@CreateDateColumn({ type: 'timestamptz' })
@CreateDateColumn({ type: 'timestamptz', name: 'created_at' })
createdAt!: Date;
}

View File

@ -54,6 +54,6 @@ export class Timer {
@Column({ type: 'enum', enum: TimerStatus, default: TimerStatus.ACTIVE })
status!: TimerStatus;
@CreateDateColumn({ type: 'timestamptz' })
@CreateDateColumn({ type: 'timestamptz', name: 'created_at' })
createdAt!: Date;
}

View File

@ -41,9 +41,9 @@ export class UserServiceCredential {
@Column({ type: 'timestamptz', nullable: true, name: 'expires_at' })
expiresAt!: Date | null;
@CreateDateColumn({ type: 'timestamptz' })
@CreateDateColumn({ type: 'timestamptz', name: 'created_at' })
createdAt!: Date;
@UpdateDateColumn({ type: 'timestamptz' })
@UpdateDateColumn({ type: 'timestamptz', name: 'updated_at' })
updatedAt!: Date;
}

View File

@ -21,13 +21,13 @@ export class User {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ type: 'uuid' })
homeId!: string;
@ManyToOne(() => Home, (home) => home.users, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'home_id' })
home!: Home;
@Column({ type: 'uuid', name: 'home_id' })
homeId!: string;
@Column({ type: 'varchar', length: 255, unique: true })
email!: string;
@ -46,9 +46,9 @@ export class User {
@OneToMany(() => UserServiceCredential, (cred) => cred.user)
serviceCredentials!: UserServiceCredential[];
@CreateDateColumn({ type: 'timestamptz' })
@CreateDateColumn({ type: 'timestamptz', name: 'created_at' })
createdAt!: Date;
@UpdateDateColumn({ type: 'timestamptz' })
@UpdateDateColumn({ type: 'timestamptz', name: 'updated_at' })
updatedAt!: Date;
}

View File

@ -0,0 +1,112 @@
import { Injectable, UnauthorizedException, ConflictException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import * as crypto from 'crypto';
import { UserService } from './user.service';
import { HomeService } from './home.service';
import { DeviceService } from './device.service';
import { UserRole } from '../domain/entities/user.entity';
import { JwtPayload } from '../../adapters/inbound/rest/auth/strategies/jwt.strategy';
export interface AuthTokens {
accessToken: string;
refreshToken: string;
}
export interface DeviceTokenResult {
deviceId: string;
token: string;
}
@Injectable()
export class AuthService {
constructor(
private readonly userService: UserService,
private readonly homeService: HomeService,
private readonly deviceService: DeviceService,
private readonly jwtService: JwtService,
) {}
async register(data: {
email: string;
password: string;
displayName: string;
homeName: string;
}): Promise<AuthTokens> {
const existing = await this.userService.findByEmail(data.email);
if (existing) {
throw new ConflictException('Email already registered');
}
const home = await this.homeService.create(data.homeName);
const passwordHash = await bcrypt.hash(data.password, 12);
const user = await this.userService.create({
email: data.email,
passwordHash,
displayName: data.displayName,
homeId: home.id,
role: UserRole.OWNER,
});
return this.generateTokens(user.id, user.email, user.homeId);
}
async login(email: string, password: string): Promise<AuthTokens> {
const user = await this.userService.findByEmail(email);
if (!user) {
throw new UnauthorizedException('Invalid credentials');
}
const valid = await bcrypt.compare(password, user.passwordHash);
if (!valid) {
throw new UnauthorizedException('Invalid credentials');
}
return this.generateTokens(user.id, user.email, user.homeId);
}
async registerDevice(homeId: string, deviceName: string): Promise<DeviceTokenResult> {
const rawToken = crypto.randomBytes(32).toString('hex');
const tokenHash = await bcrypt.hash(rawToken, 10);
const device = await this.deviceService.create({
name: deviceName,
homeId,
tokenHash,
});
const token = this.jwtService.sign(
{ sub: device.id, email: '', homeId, type: 'device' },
{ expiresIn: '365d' as const },
);
return { deviceId: device.id, token };
}
async refreshAccessToken(refreshToken: string): Promise<AuthTokens> {
try {
const payload = this.jwtService.verify<JwtPayload>(refreshToken);
if (payload.type !== 'user') {
throw new UnauthorizedException('Invalid refresh token');
}
return this.generateTokens(payload.sub, payload.email, payload.homeId);
} catch {
throw new UnauthorizedException('Invalid refresh token');
}
}
private generateTokens(userId: string, email: string, homeId: string): AuthTokens {
const payload = { sub: userId, email, homeId, type: 'user' } as Record<string, unknown>;
const accessToken = this.jwtService.sign(payload, {
expiresIn: '15m' as const,
});
const refreshToken = this.jwtService.sign(payload, {
expiresIn: '7d' as const,
});
return { accessToken, refreshToken };
}
}

View File

@ -0,0 +1,40 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import * as bcrypt from 'bcrypt';
import { Device } from '../domain/entities/device.entity';
@Injectable()
export class DeviceService {
constructor(
@InjectRepository(Device)
private readonly deviceRepository: Repository<Device>,
) {}
async findById(id: string): Promise<Device | null> {
return this.deviceRepository.findOne({ where: { id } });
}
async findByHomeId(homeId: string): Promise<Device[]> {
return this.deviceRepository.find({ where: { homeId } });
}
async create(data: { name: string; homeId: string; tokenHash: string }): Promise<Device> {
const device = this.deviceRepository.create({
name: data.name,
homeId: data.homeId,
deviceTokenHash: data.tokenHash,
});
return this.deviceRepository.save(device);
}
async validateToken(deviceId: string, token: string): Promise<boolean> {
const device = await this.findById(deviceId);
if (!device) return false;
return bcrypt.compare(token, device.deviceTokenHash);
}
async updateLastSeen(id: string): Promise<void> {
await this.deviceRepository.update(id, { lastSeenAt: new Date() });
}
}

View File

@ -0,0 +1,21 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Home } from '../domain/entities/home.entity';
@Injectable()
export class HomeService {
constructor(
@InjectRepository(Home)
private readonly homeRepository: Repository<Home>,
) {}
async create(name: string): Promise<Home> {
const home = this.homeRepository.create({ name });
return this.homeRepository.save(home);
}
async findById(id: string): Promise<Home | null> {
return this.homeRepository.findOne({ where: { id } });
}
}

View File

@ -0,0 +1,25 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from '../domain/entities/user.entity';
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {}
async findById(id: string): Promise<User | null> {
return this.userRepository.findOne({ where: { id } });
}
async findByEmail(email: string): Promise<User | null> {
return this.userRepository.findOne({ where: { email } });
}
async create(data: Partial<User>): Promise<User> {
const user = this.userRepository.create(data);
return this.userRepository.save(user);
}
}

287
pnpm-lock.yaml generated
View File

@ -19,12 +19,21 @@ importers:
'@nestjs/core':
specifier: ^11.1.17
version: 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/jwt':
specifier: ^11.0.2
version: 11.0.2(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))
'@nestjs/passport':
specifier: ^11.0.5
version: 11.0.5(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)
'@nestjs/platform-express':
specifier: ^11.1.17
version: 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)
'@nestjs/typeorm':
specifier: ^11.0.0
version: 11.0.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@types/node@25.5.0)(typescript@5.8.3)))
bcrypt:
specifier: ^6.0.0
version: 6.0.0
class-transformer:
specifier: ^0.5.1
version: 0.5.1
@ -37,6 +46,12 @@ importers:
ioredis:
specifier: ^5.10.1
version: 5.10.1
passport:
specifier: ^0.7.0
version: 0.7.0
passport-jwt:
specifier: ^4.0.1
version: 4.0.1
pg:
specifier: ^8.20.0
version: 8.20.0
@ -59,12 +74,18 @@ importers:
'@nestjs/testing':
specifier: ^11.1.17
version: 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@nestjs/platform-express@11.1.17)
'@types/bcrypt':
specifier: ^6.0.0
version: 6.0.0
'@types/jest':
specifier: ^30.0.0
version: 30.0.0
'@types/node':
specifier: ^25.5.0
version: 25.5.0
'@types/passport-jwt':
specifier: ^4.0.1
version: 4.0.1
'@typescript-eslint/eslint-plugin':
specifier: ^8.57.2
version: 8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0)(typescript@5.8.3))(eslint@10.1.0)(typescript@5.8.3)
@ -841,6 +862,17 @@ packages:
'@nestjs/websockets':
optional: true
'@nestjs/jwt@11.0.2':
resolution: {integrity: sha512-rK8aE/3/Ma45gAWfCksAXUNbOoSOUudU0Kn3rT39htPF7wsYXtKfjALKeKKJbFrIWbLjsbqfXX5bIJNvgBugGA==}
peerDependencies:
'@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0
'@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.17':
resolution: {integrity: sha512-mAf4eOsSBsTOn/VbrUO1gsjW6dVh91qqXPMXun4dN8SnNjf7PTQagM9o8d6ab8ZBpNe6UdZftdrZoDetU+n4Qg==}
peerDependencies:
@ -933,6 +965,15 @@ 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==}
'@types/connect@3.4.38':
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
'@types/eslint-scope@3.7.7':
resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==}
@ -945,6 +986,15 @@ packages:
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/express-serve-static-core@5.1.1':
resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==}
'@types/express@5.0.6':
resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==}
'@types/http-errors@2.0.5':
resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
'@types/istanbul-lib-coverage@2.0.6':
resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==}
@ -960,9 +1010,36 @@ 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/ms@2.1.0':
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
'@types/node@25.5.0':
resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==}
'@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/qs@6.15.0':
resolution: {integrity: sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==}
'@types/range-parser@1.2.7':
resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
'@types/send@1.2.1':
resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==}
'@types/serve-static@2.2.0':
resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==}
'@types/stack-utils@2.0.3':
resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==}
@ -1349,6 +1426,10 @@ packages:
engines: {node: '>=6.0.0'}
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==}
@ -1378,6 +1459,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==}
@ -1633,6 +1717,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==}
@ -2273,6 +2360,16 @@ packages:
jsonfile@6.2.0:
resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
jsonwebtoken@9.0.3:
resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==}
engines: {node: '>=12', npm: '>=6'}
jwa@2.0.1:
resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==}
jws@4.0.1:
resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==}
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@ -2309,12 +2406,33 @@ packages:
lodash.defaults@4.2.0:
resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
lodash.includes@4.3.0:
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
lodash.isarguments@3.1.0:
resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
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.once@4.1.1:
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
lodash@4.17.23:
resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==}
@ -2435,9 +2553,17 @@ packages:
node-abort-controller@3.1.1:
resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==}
node-addon-api@8.7.0:
resolution: {integrity: sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==}
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==}
@ -2514,6 +2640,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'}
@ -2541,6 +2678,9 @@ packages:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
engines: {node: '>=8'}
pause@0.0.1:
resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==}
pg-cloudflare@1.3.0:
resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==}
@ -3144,6 +3284,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
@ -4085,6 +4229,17 @@ snapshots:
optionalDependencies:
'@nestjs/platform-express': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)
'@nestjs/jwt@11.0.2(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))':
dependencies:
'@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@types/jsonwebtoken': 9.0.10
jsonwebtoken: 9.0.3
'@nestjs/passport@11.0.5(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)':
dependencies:
'@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
passport: 0.7.0
'@nestjs/platform-express@11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)':
dependencies:
'@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
@ -4199,6 +4354,19 @@ snapshots:
dependencies:
'@babel/types': 7.29.0
'@types/bcrypt@6.0.0':
dependencies:
'@types/node': 25.5.0
'@types/body-parser@1.19.6':
dependencies:
'@types/connect': 3.4.38
'@types/node': 25.5.0
'@types/connect@3.4.38':
dependencies:
'@types/node': 25.5.0
'@types/eslint-scope@3.7.7':
dependencies:
'@types/eslint': 9.6.1
@ -4213,6 +4381,21 @@ snapshots:
'@types/estree@1.0.8': {}
'@types/express-serve-static-core@5.1.1':
dependencies:
'@types/node': 25.5.0
'@types/qs': 6.15.0
'@types/range-parser': 1.2.7
'@types/send': 1.2.1
'@types/express@5.0.6':
dependencies:
'@types/body-parser': 1.19.6
'@types/express-serve-static-core': 5.1.1
'@types/serve-static': 2.2.0
'@types/http-errors@2.0.5': {}
'@types/istanbul-lib-coverage@2.0.6': {}
'@types/istanbul-lib-report@3.0.3':
@ -4230,10 +4413,44 @@ snapshots:
'@types/json-schema@7.0.15': {}
'@types/jsonwebtoken@9.0.10':
dependencies:
'@types/ms': 2.1.0
'@types/node': 25.5.0
'@types/ms@2.1.0': {}
'@types/node@25.5.0':
dependencies:
undici-types: 7.18.2
'@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.6
'@types/passport': 1.0.17
'@types/passport@1.0.17':
dependencies:
'@types/express': 5.0.6
'@types/qs@6.15.0': {}
'@types/range-parser@1.2.7': {}
'@types/send@1.2.1':
dependencies:
'@types/node': 25.5.0
'@types/serve-static@2.2.0':
dependencies:
'@types/http-errors': 2.0.5
'@types/node': 25.5.0
'@types/stack-utils@2.0.3': {}
'@types/validator@13.15.10': {}
@ -4636,6 +4853,11 @@ snapshots:
baseline-browser-mapping@2.10.11: {}
bcrypt@6.0.0:
dependencies:
node-addon-api: 8.7.0
node-gyp-build: 4.8.4
bl@4.1.0:
dependencies:
buffer: 5.7.1
@ -4685,6 +4907,8 @@ snapshots:
dependencies:
node-int64: 0.4.0
buffer-equal-constant-time@1.0.1: {}
buffer-from@1.1.2: {}
buffer@5.7.1:
@ -4892,6 +5116,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.328: {}
@ -5778,6 +6006,30 @@ snapshots:
optionalDependencies:
graceful-fs: 4.2.11
jsonwebtoken@9.0.3:
dependencies:
jws: 4.0.1
lodash.includes: 4.3.0
lodash.isboolean: 3.0.3
lodash.isinteger: 4.0.4
lodash.isnumber: 3.0.3
lodash.isplainobject: 4.0.6
lodash.isstring: 4.0.1
lodash.once: 4.1.1
ms: 2.1.3
semver: 7.7.4
jwa@2.0.1:
dependencies:
buffer-equal-constant-time: 1.0.1
ecdsa-sig-formatter: 1.0.11
safe-buffer: 5.2.1
jws@4.0.1:
dependencies:
jwa: 2.0.1
safe-buffer: 5.2.1
keyv@4.5.4:
dependencies:
json-buffer: 3.0.1
@ -5807,10 +6059,24 @@ snapshots:
lodash.defaults@4.2.0: {}
lodash.includes@4.3.0: {}
lodash.isarguments@3.1.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.once@4.1.1: {}
lodash@4.17.23: {}
log-symbols@4.1.0:
@ -5905,10 +6171,14 @@ snapshots:
node-abort-controller@3.1.1: {}
node-addon-api@8.7.0: {}
node-emoji@1.11.0:
dependencies:
lodash: 4.17.23
node-gyp-build@4.8.4: {}
node-int64@0.4.0: {}
node-releases@2.0.36: {}
@ -5989,6 +6259,19 @@ snapshots:
parseurl@1.3.3: {}
passport-jwt@4.0.1:
dependencies:
jsonwebtoken: 9.0.3
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: {}
@ -6009,6 +6292,8 @@ snapshots:
path-type@4.0.0: {}
pause@0.0.1: {}
pg-cloudflare@1.3.0:
optional: true
@ -6568,6 +6853,8 @@ snapshots:
util-deprecate@1.0.2: {}
utils-merge@1.0.1: {}
uuid@11.1.0: {}
v8-compile-cache-lib@3.0.1: {}