Compare commits

...

2 Commits

Author SHA1 Message Date
85b2062a2a add user module and jwt 2025-06-22 19:16:23 +08:00
1e043718b4 add user module and jwt 2025-06-22 17:57:19 +08:00
11 changed files with 223 additions and 6 deletions

View File

@ -1,9 +1,9 @@
import { BadGatewayException, Global, Module, ModuleMetadata, Type } from '@nestjs/common'; import { BadGatewayException, Global, Module, ModuleMetadata, Type } from '@nestjs/common';
import { APP_FILTER, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'; import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
import { useContainer } from 'class-validator'; import { useContainer } from 'class-validator';
import { omit } from 'lodash'; import { isNil, omit } from 'lodash';
import { ConfigModule } from '@/modules/config/config.module'; import { ConfigModule } from '@/modules/config/config.module';
import { Configure } from '@/modules/config/configure'; import { Configure } from '@/modules/config/configure';
@ -85,6 +85,13 @@ export async function createBootModule(
}); });
} }
if (!isNil(globals.guard)) {
providers.push({
provide: APP_GUARD,
useClass: globals.guard,
});
}
return CreateModule('BootModule', () => ({ return CreateModule('BootModule', () => ({
imports, imports,
providers, providers,

View File

@ -1,4 +1,5 @@
import { ModuleMetadata, PipeTransform, Type } from '@nestjs/common'; import { ModuleMetadata, PipeTransform, Type } from '@nestjs/common';
import { IAuthGuard } from '@nestjs/passport';
import { NestFastifyApplication } from '@nestjs/platform-fastify'; import { NestFastifyApplication } from '@nestjs/platform-fastify';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
@ -28,6 +29,11 @@ export interface CreateOptions {
interceptor?: Type<any> | null; interceptor?: Type<any> | null;
filter?: Type<any> | null; filter?: Type<any> | null;
/**
*
*/
guard?: Type<IAuthGuard>;
}; };
providers?: ModuleMetadata['providers']; providers?: ModuleMetadata['providers'];

View File

@ -31,3 +31,12 @@ export enum UserOrderType {
CREATED = 'createdAt', CREATED = 'createdAt',
UPDATED = 'updatedAt', UPDATED = 'updatedAt',
} }
export const TokenConst = {
USER_TOKEN_SECRET: 'USER_TOKEN_SECRET',
DEFAULT_USER_TOKEN_SECRET: 'my-access-secret',
USER_REFRESH_TOKEN_EXPIRED: 'USER_REFRESH_TOKEN_EXPIRED',
DEFAULT_USER_REFRESH_TOKEN_EXPIRED: 'my-refresh-secret',
};
export const ALLOW_GUEST = 'allowGuest';

View File

@ -0,0 +1,5 @@
import { SetMetadata } from '@nestjs/common';
import { ALLOW_GUEST } from '@/modules/user/constants';
export const Guest = () => SetMetadata(ALLOW_GUEST, true);

View File

@ -0,0 +1,8 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { UserEntity } from '../entities/UserEntity';
export const RequestUser = createParamDecorator(async (_data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user as ClassToPlain<UserEntity>;
});

View File

@ -0,0 +1,82 @@
import { ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { isNil } from 'lodash';
import { ExtractJwt } from 'passport-jwt';
import { TokenService } from '@/modules/user/services/token.service';
import { ALLOW_GUEST } from '../constants';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(
protected ref: Reflector,
protected tokenService: TokenService,
) {
super();
}
async canActivate(context: ExecutionContext) {
const allowGuest = this.ref.getAllAndOverride<boolean>(ALLOW_GUEST, [
context.getHandler(),
context.getClass(),
]);
if (allowGuest) {
return true;
}
const request = this.getRequest(context);
const requestToken = ExtractJwt.fromAuthHeaderAsBearerToken()(request);
if (isNil(requestToken)) {
throw new UnauthorizedException();
}
const accessToken = await this.tokenService.checkAccessToken(requestToken);
if (isNil(accessToken)) {
throw new UnauthorizedException();
}
try {
return (await super.canActivate(context)) as boolean;
} catch {
const response = this.getResponse(context);
const token = await this.tokenService.refreshToken(accessToken, response);
if (isNil(token)) {
throw new UnauthorizedException();
}
if (token.accessToken) {
request.headers.authorization = `Bearer ${token.accessToken.value}`;
}
return (await super.canActivate(context)) as boolean;
}
}
/**
*
*
* 401
* @param err
* @param user
* @param _info
*/
handleRequest(err: any, user: any, _info: any) {
if (err || isNil(user)) {
if (isNil(user)) {
throw new UnauthorizedException();
}
throw err;
}
return user;
}
getRequest(context: ExecutionContext) {
return context.switchToHttp().getRequest();
}
getResponse(context: ExecutionContext) {
return context.switchToHttp().getResponse();
}
}

View File

@ -0,0 +1,28 @@
import { BadGatewayException, ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { plainToClass } from 'class-transformer';
import { validateOrReject } from 'class-validator';
import { CredentialDto } from '../dtos/auth.dto';
/**
*
*/
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
try {
await validateOrReject(plainToClass(CredentialDto, request.body), {
validationError: { target: false },
});
} catch (error) {
const messages = (error as any[])
.map((e) => e.constraints ?? {})
.reduce((o, n) => ({ ...o, ...n }), {});
throw new BadGatewayException(Object.values(messages));
}
return super.canActivate(context) as boolean;
}
}

View File

@ -16,6 +16,8 @@ import { AccessTokenEntity } from '@/modules/user/entities/access.token.entity';
import { RefreshTokenEntity } from '@/modules/user/entities/refresh.token.entity'; import { RefreshTokenEntity } from '@/modules/user/entities/refresh.token.entity';
import { JwtConfig, JwtPayload, UserConfig } from '@/modules/user/types'; import { JwtConfig, JwtPayload, UserConfig } from '@/modules/user/types';
import { TokenConst } from '../constants';
/** /**
* *
*/ */
@ -85,7 +87,10 @@ export class TokenService {
const refreshToken = new RefreshTokenEntity(); const refreshToken = new RefreshTokenEntity();
refreshToken.value = jwt.sign( refreshToken.value = jwt.sign(
refreshTokenPayload, refreshTokenPayload,
this.configure.env.get('USER_REFRESH_TOKEN_EXPIRED', 'my-refresh-secret'), this.configure.env.get(
TokenConst.USER_REFRESH_TOKEN_EXPIRED,
TokenConst.DEFAULT_USER_REFRESH_TOKEN_EXPIRED,
),
); );
refreshToken.expiredAt = now.add(config.refreshTokenExpired, 'second').toDate(); refreshToken.expiredAt = now.add(config.refreshTokenExpired, 'second').toDate();
refreshToken.accessToken = accessToken; refreshToken.accessToken = accessToken;
@ -136,7 +141,10 @@ export class TokenService {
async verifyAccessToken(token: AccessTokenEntity) { async verifyAccessToken(token: AccessTokenEntity) {
const result = jwt.verify( const result = jwt.verify(
token.value, token.value,
this.configure.env.get('USER_TOKEN_SECRET', 'my-access-secret'), this.configure.env.get(
TokenConst.USER_TOKEN_SECRET,
TokenConst.DEFAULT_USER_TOKEN_SECRET,
),
); );
if (result) { if (result) {
return token.user; return token.user;
@ -152,7 +160,10 @@ export class TokenService {
defaultUserConfig(configure), defaultUserConfig(configure),
); );
const options: JwtModuleOptions = { const options: JwtModuleOptions = {
secret: configure.env.get('USER_TOKEN_SECRET', 'my-access-secret'), secret: configure.env.get(
TokenConst.USER_TOKEN_SECRET,
TokenConst.DEFAULT_USER_TOKEN_SECRET,
),
verifyOptions: { verifyOptions: {
ignoreExpiration: !configure.env.isProd(), ignoreExpiration: !configure.env.isProd(),
}, },

View File

@ -0,0 +1,37 @@
import { PassportStrategy } from '@nestjs/passport';
import { instanceToPlain } from 'class-transformer';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { Configure } from '@/modules/config/configure';
import { TokenConst } from '../constants';
import { UserRepository } from '../repositories/UserRepository';
import { JwtPayload } from '../types';
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
protected configure: Configure,
protected userRepository: UserRepository,
) {
const secret = configure.env.get(
TokenConst.USER_TOKEN_SECRET,
TokenConst.DEFAULT_USER_TOKEN_SECRET,
);
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: secret,
});
}
/**
* ID
* ID查询出用户是否存在,id放入request方便后续操作
* @param payload
*/
async validate(payload: JwtPayload) {
const user = await this.userRepository.findOneOrFail({ where: { id: payload.sub } });
return instanceToPlain(user);
}
}

View File

@ -0,0 +1,23 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { AuthService } from '../services/auth.service';
/**
*
*/
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(protected authService: AuthService) {
super({ usernameField: 'credential', passwordField: 'password' });
}
async validate(credential: string, password: string): Promise<any> {
const user = await this.authService.validateUser(credential, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}

View File

@ -16,6 +16,7 @@ import { MeiliModule } from './modules/meilisearch/meili.module';
import { Restful } from './modules/restful/restful'; import { Restful } from './modules/restful/restful';
import { RestfulModule } from './modules/restful/restful.module'; import { RestfulModule } from './modules/restful/restful.module';
import { ApiConfig } from './modules/restful/types'; import { ApiConfig } from './modules/restful/types';
import { JwtAuthGuard } from './modules/user/guards/jwt.auth.guard';
export const createOptions: CreateOptions = { export const createOptions: CreateOptions = {
commands: () => [...Object.values(dbCommands)], commands: () => [...Object.values(dbCommands)],
@ -26,7 +27,7 @@ export const createOptions: CreateOptions = {
await RestfulModule.forRoot(configure), await RestfulModule.forRoot(configure),
await ContentModule.forRoot(configure), await ContentModule.forRoot(configure),
], ],
globals: {}, globals: { guard: JwtAuthGuard },
builder: async ({ configure, BootModule }) => { builder: async ({ configure, BootModule }) => {
const container = await NestFactory.create<NestFastifyApplication>( const container = await NestFactory.create<NestFastifyApplication>(
BootModule, BootModule,