This commit is contained in:
2026-06-30 10:33:56 +08:00
commit 6e047dc0a5
607 changed files with 65966 additions and 0 deletions
@@ -0,0 +1,14 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { AuthUser } from '../guards/jwt-auth.guard';
export const CurrentUser = createParamDecorator(
(_data: unknown, ctx: ExecutionContext): AuthUser => {
return ctx.switchToHttp().getRequest().user;
},
);
export function serializeBigInt<T>(value: T): T {
return JSON.parse(
JSON.stringify(value, (_k, v) => (typeof v === 'bigint' ? v.toString() : v)),
);
}
@@ -0,0 +1,37 @@
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
HttpStatus,
} from '@nestjs/common';
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
if (exception instanceof HttpException) {
const status = exception.getStatus();
const res = exception.getResponse();
const message =
typeof res === 'string'
? res
: (res as { message?: string | string[] }).message || exception.message;
response.status(status).json({
code: status,
message: Array.isArray(message) ? message.join(', ') : message,
data: null,
});
return;
}
console.error(exception);
response.status(HttpStatus.INTERNAL_SERVER_ERROR).json({
code: 500,
message: 'Internal server error',
data: null,
});
}
}
@@ -0,0 +1,49 @@
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { CLIENT_APP_ACTOR_MAP, ClientApp } from '@dukang/shared-types';
export interface AuthUser {
actorType: string;
actorId: bigint;
clientApp: ClientApp;
sub: string;
}
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(protected readonly jwtService: JwtService) {}
canActivate(context: ExecutionContext): boolean {
const req = context.switchToHttp().getRequest();
const auth = req.headers.authorization as string | undefined;
if (!auth?.startsWith('Bearer ')) {
throw new UnauthorizedException('Missing token');
}
try {
const payload = this.jwtService.verify(auth.slice(7));
const clientApp = req.headers['x-client-app'] as ClientApp;
if (!clientApp || payload.clientApp !== clientApp) {
throw new UnauthorizedException('Invalid client app');
}
const expectedActor = CLIENT_APP_ACTOR_MAP[clientApp];
if (payload.actorType !== expectedActor) {
throw new UnauthorizedException('Actor mismatch');
}
req.user = {
actorType: payload.actorType,
actorId: BigInt(payload.actorId),
clientApp,
sub: payload.sub,
} satisfies AuthUser;
return true;
} catch (err) {
if (err instanceof UnauthorizedException) throw err;
throw new UnauthorizedException('Invalid token');
}
}
}
@@ -0,0 +1,20 @@
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable, map } from 'rxjs';
@Injectable()
export class ResponseInterceptor implements NestInterceptor {
intercept(_context: ExecutionContext, next: CallHandler): Observable<unknown> {
return next.handle().pipe(
map((data) => ({
code: 0,
message: 'ok',
data: data ?? null,
})),
);
}
}
@@ -0,0 +1,16 @@
import { Global, Injectable, Module, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
}
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}
@@ -0,0 +1,16 @@
import { Global, Module } from '@nestjs/common';
import Redis from 'ioredis';
import { RedisService } from './redis.service';
@Global()
@Module({
providers: [
{
provide: 'REDIS_CLIENT',
useFactory: () => new Redis(process.env.REDIS_URL || 'redis://localhost:6379'),
},
RedisService,
],
exports: ['REDIS_CLIENT', RedisService],
})
export class RedisModule {}
@@ -0,0 +1,29 @@
import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
@Injectable()
export class RedisService {
constructor(@Inject('REDIS_CLIENT') private readonly redis: Redis) {}
get client() {
return this.redis;
}
async setJson(key: string, value: unknown, ttlSeconds?: number) {
const payload = JSON.stringify(value);
if (ttlSeconds) {
await this.redis.set(key, payload, 'EX', ttlSeconds);
} else {
await this.redis.set(key, payload);
}
}
async getJson<T>(key: string): Promise<T | null> {
const raw = await this.redis.get(key);
return raw ? (JSON.parse(raw) as T) : null;
}
async del(key: string) {
await this.redis.del(key);
}
}