add user module and jwt

This commit is contained in:
liuyi 2025-06-22 17:12:45 +08:00
parent 6fa6e1f076
commit b8cc65a768
16 changed files with 266 additions and 27 deletions

View File

@ -1,4 +1,4 @@
import { IsUUID, IsDefined } from 'class-validator';
import { IsDefined, IsUUID } from 'class-validator';
import { DtoValidation } from '@/modules/core/decorator/dto.validation.decorator';

View File

@ -1,4 +1,5 @@
import { Exclude, Expose, Type } from 'class-transformer';
import type { Relation } from 'typeorm';
import {
BaseEntity,
Column,
@ -10,8 +11,6 @@ import {
TreeParent,
} from 'typeorm';
import type { Relation } from 'typeorm';
import { PostEntity } from '@/modules/content/entities/post.entity';
@Exclude()

View File

@ -1,6 +1,6 @@
import { Exclude, Expose } from 'class-transformer';
import { Column, Entity, ManyToMany, PrimaryColumn } from 'typeorm';
import type { Relation } from 'typeorm';
import { Column, Entity, ManyToMany, PrimaryColumn } from 'typeorm';
import { PostEntity } from '@/modules/content/entities/post.entity';

View File

@ -8,6 +8,7 @@ import { QueryHook } from '@/modules/database/types';
type FindCommentTreeOptions = FindTreeOptions & {
addQuery?: QueryHook<CommentEntity>;
};
@CustomRepository(CommentEntity)
export class CommentRepository extends BaseTreeRepository<CommentEntity> {
protected _qbName = 'comment';

View File

@ -4,6 +4,7 @@ import { deepMerge } from '@/modules/core/helpers';
export class SanitizeService {
protected config: sanitizeHtml.IOptions = {};
constructor() {
this.config = {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img', 'code']),

View File

@ -13,6 +13,7 @@ export class MatchConstraint implements ValidatorConstraintInterface {
const relatedValue = (validationArguments.object as any)[relatedProperty];
return (value === relatedValue) !== reverse;
}
defaultMessage?(validationArguments?: ValidationArguments): string {
const [relatedProperty, reverse] = validationArguments.constraints;
return `${relatedProperty} and ${validationArguments.property} ${

View File

@ -1,8 +1,8 @@
import {
ValidationArguments,
ValidatorConstraintInterface,
ValidationOptions,
registerDecorator,
ValidationArguments,
ValidationOptions,
ValidatorConstraintInterface,
} from 'class-validator';
type ModelType = 1 | 2 | 3 | 4 | 5;
@ -30,6 +30,7 @@ export class PasswordConstraint implements ValidatorConstraintInterface {
return /\d/.test(value) && /[A-Za-z]/.test(value);
}
}
defaultMessage?(validationArguments?: ValidationArguments): string {
return "($value) 's format error";
}

View File

@ -1,13 +1,13 @@
import { Injectable } from '@nestjs/common';
import {
registerDecorator,
ValidationArguments,
ValidationOptions,
ValidatorConstraint,
ValidatorConstraintInterface,
ValidationOptions,
registerDecorator,
} from 'class-validator';
import { isArray, isNil } from 'lodash';
import { ObjectType, Repository, DataSource } from 'typeorm';
import { DataSource, ObjectType, Repository } from 'typeorm';
type Condition = {
entity: ObjectType<any>;
@ -52,6 +52,7 @@ export class DataExistConstraint implements ValidatorConstraintInterface {
const item = await repo.findOne({ where: { [map]: value } });
return !!item;
}
defaultMessage?(validationArguments?: ValidationArguments): string {
if (!validationArguments.constraints[0]) {
return 'Model not been specified!';

View File

@ -1,12 +1,12 @@
import { Injectable } from '@nestjs/common';
import {
registerDecorator,
ValidationArguments,
ValidationOptions,
ValidatorConstraint,
ValidatorConstraintInterface,
ValidationArguments,
registerDecorator,
ValidationOptions,
} from 'class-validator';
import { merge, isNil } from 'lodash';
import { isNil, merge } from 'lodash';
import { DataSource, ObjectType } from 'typeorm';
type Condition = {

View File

@ -2,9 +2,9 @@ import { Injectable } from '@nestjs/common';
import {
registerDecorator,
ValidationArguments,
ValidationOptions,
ValidatorConstraint,
ValidatorConstraintInterface,
ValidationOptions,
} from 'class-validator';
import { isNil, merge } from 'lodash';
import { DataSource, ObjectType } from 'typeorm';
@ -39,6 +39,7 @@ export class UniqueConstraint implements ValidatorConstraintInterface {
return false;
}
}
defaultMessage?(validationArguments?: ValidationArguments): string {
const { entity, property } = validationArguments.constraints[0];
const queryProperty = property ?? validationArguments.property;

View File

@ -18,6 +18,7 @@ type Condition = {
property?: string;
};
@Injectable()
@ValidatorConstraint({ name: 'dataUniqueExist', async: true })
export class UniqueExistConstraint implements ValidatorConstraintInterface {
@ -48,6 +49,7 @@ export class UniqueExistConstraint implements ValidatorConstraintInterface {
}),
);
}
defaultMessage?(args?: ValidationArguments): string {
const { entity, property } = args.constraints[0];
const queryProperty = property ?? args.property;

View File

@ -24,7 +24,7 @@ import { DBOptions } from './types';
export class DatabaseModule {
static async forRoot(configure: Configure): Promise<DynamicModule> {
if (!configure.has('database')) {
panic({ message: 'Database config not exists' });
await panic({ message: 'Database config not exists' });
}
const { connections } = await configure.get<DBOptions>('database');
const imports: ModuleMetadata['imports'] = [];
@ -46,6 +46,7 @@ export class DatabaseModule {
providers,
};
}
static forRepository<T extends Type<any>>(
repositories: T[],
datasourceName?: string,

View File

@ -0,0 +1,115 @@
/* eslint-disable import/no-extraneous-dependencies */
import { ForbiddenException, Injectable } from '@nestjs/common';
import { FastifyRequest as Request } from 'fastify';
import { ExtractJwt } from 'passport-jwt';
import { Configure } from '@/modules/config/configure';
import { getTime } from '@/modules/core/helpers/time';
import { RegisterDto } from '@/modules/user/dtos/auth.dto';
import { UserEntity } from '@/modules/user/entities/UserEntity';
import { TokenService } from '@/modules/user/services/token.service';
import { decrypt } from '@/modules/user/utils';
import { UpdatePasswordDto } from '../dtos/account.dto';
import { UserRepository } from '../repositories/UserRepository';
import { UserService } from './user.service';
@Injectable()
export class AuthService {
constructor(
protected configure: Configure,
protected userService: UserService,
protected tokenService: TokenService,
protected userRepository: UserRepository,
) {}
/**
*
* @param credential
* @param password
*/
async validateUser(credential: string, password: string) {
const user = await this.userService.findOneByCredential(credential, async (query) =>
query.addSelect('user.password'),
);
if (user && decrypt(password, user.password)) {
return user;
}
return null;
}
/**
* ,accessToken和refreshToken
* @param user
*/
async login(user: UserEntity) {
const now = await getTime(this.configure);
const { accessToken } = await this.tokenService.generateAccessToken(user, now);
return accessToken.value;
}
/**
*
* @param req
*/
async logout(req: Request) {
const accessToken = ExtractJwt.fromAuthHeaderAsBearerToken()(req as any);
if (accessToken) {
await this.tokenService.removeAccessToken(accessToken);
}
return { msg: 'logout_success' };
}
/**
* token和refreshToken
* @param id
*/
async createToken(id: string) {
const now = await getTime(this.configure);
let user: UserEntity;
try {
user = await this.userService.detail(id);
} catch (err) {
throw new ForbiddenException(err);
}
const { accessToken } = await this.tokenService.generateAccessToken(user, now);
return accessToken.value;
}
/**
* 使
* @param data
*/
async register(data: RegisterDto) {
const { username, nickname, password } = data;
const user = await this.userService.create({
username,
nickname,
password,
} as any);
return this.userService.findOneByCondition({ id: user.id });
}
/**
*
* @param user
* @param password
* @param oldPassword
*/
async changePassword(user: UserEntity, { password, oldPassword }: UpdatePasswordDto) {
const item = await this.userRepository.findOneOrFail({
select: ['password'],
where: { id: user.id },
});
if (decrypt(oldPassword, item.password)) {
await this.userRepository.save({ id: user.id, password }, { reload: true });
return this.userService.detail(user.id);
}
throw new ForbiddenException('old password do not match');
}
}

View File

@ -1,7 +1,7 @@
/* eslint-disable import/no-extraneous-dependencies */
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { JwtModule, JwtModuleOptions, JwtService } from '@nestjs/jwt';
import dayjs from 'dayjs';
import { FastifyReply as Response } from 'fastify';
@ -10,11 +10,12 @@ import { v4 as uuid } from 'uuid';
import { Configure } from '@/modules/config/configure';
import { getTime } from '@/modules/core/helpers/time';
import { getUserConfig } from '@/modules/user/config';
import { defaultUserConfig, getUserConfig } from '@/modules/user/config';
import { UserEntity } from '@/modules/user/entities/UserEntity';
import { AccessTokenEntity } from '@/modules/user/entities/access.token.entity';
import { RefreshTokenEntity } from '@/modules/user/entities/refresh.token.entity';
import { JwtConfig, JwtPayload } from '@/modules/user/types';
import { JwtConfig, JwtPayload, UserConfig } from '@/modules/user/types';
/**
*
*/
@ -142,4 +143,25 @@ export class TokenService {
}
return null;
}
static JwtModuleFactory(configure: Configure) {
return JwtModule.registerAsync({
useFactory: async (): Promise<JwtModuleOptions> => {
const config = await configure.get<UserConfig>(
'user',
defaultUserConfig(configure),
);
const options: JwtModuleOptions = {
secret: configure.env.get('USER_TOKEN_SECRET', 'my-access-secret'),
verifyOptions: {
ignoreExpiration: !configure.env.isProd(),
},
};
if (configure.env.isProd()) {
options.signOptions = { expiresIn: `${config.jwt.tokenExpired}s` };
}
return options;
},
});
}
}

View File

@ -0,0 +1,94 @@
import { Injectable } from '@nestjs/common';
import { isNil } from 'lodash';
import { DataSource, EntityNotFoundError, SelectQueryBuilder } from 'typeorm';
import { Configure } from '@/modules/config/configure';
import { BaseService } from '@/modules/database/base/service';
import { QueryHook } from '@/modules/database/types';
import { CreateUserDto, QueryUserDto, UpdateUserDto } from '../dtos/user.dto';
import { UserEntity } from '../entities/UserEntity';
import { UserRepository } from '../repositories/UserRepository';
@Injectable()
export class UserService extends BaseService<UserEntity, UserRepository> {
protected enableTrash = true;
constructor(
protected configure: Configure,
protected dataSource: DataSource,
protected userRepository: UserRepository,
) {
super(userRepository);
}
/**
*
* @param data
*/
async create(data: CreateUserDto): Promise<UserEntity> {
const user = await this.userRepository.save(data, { reload: true });
return this.detail(user.id);
}
/**
*
* @param data
*/
async update(data: UpdateUserDto): Promise<UserEntity> {
const updated = await this.userRepository.save(data, { reload: true });
return this.detail(updated.id);
}
/**
*
* @param credential
* @param callback
*/
async findOneByCredential(credential: string, callback?: QueryHook<UserEntity>) {
let query = this.userRepository.buildBaseQuery();
if (callback) {
query = await callback(query);
}
return query
.where('user.username = :credential', { credential })
.orWhere('user.email = :credential', { credential })
.orWhere('user.phone = :credential', { credential })
.getOne();
}
/**
* ,
* @param condition
* @param callback
*/
async findOneByCondition(condition: { [key: string]: any }, callback?: QueryHook<UserEntity>) {
let query = this.userRepository.buildBaseQuery();
if (callback) {
query = await callback(query);
}
const wheres = Object.fromEntries(
Object.entries(condition).map(([key, value]) => [key, value]),
);
const user = query.where(wheres).getOne();
if (!user) {
throw new EntityNotFoundError(UserEntity, Object.keys(condition).join(','));
}
return user;
}
protected async buildListQB(
queryBuilder: SelectQueryBuilder<UserEntity>,
options: QueryUserDto,
callback?: QueryHook<UserEntity>,
) {
const { orderBy } = options;
const qb = await super.buildListQB(queryBuilder, options, callback);
if (!isNil(orderBy)) {
qb.orderBy(`${this.repository.qbName}.${orderBy}`, 'ASC');
}
return qb;
}
}

16
typings/global.d.ts vendored
View File

@ -15,20 +15,20 @@ declare type RePartial<T> = {
[P in keyof T]?: T[P] extends (infer U)[] | undefined
? RePartial<U>[]
: T[P] extends object | undefined
? T[P] extends ((...args: any[]) => any) | ClassType<T[P]> | undefined
? T[P]
: RePartial<T[P]>
: T[P];
? T[P] extends ((...args: any[]) => any) | ClassType<T[P]> | undefined
? T[P]
: RePartial<T[P]>
: T[P];
};
declare type ReRequired<T> = {
[P in keyof T]-?: T[P] extends (infer U)[] | undefined
? ReRequired<U>[]
: T[P] extends object | undefined
? T[P] extends ((...args: any[]) => any) | ClassType<T[P]> | undefined
? T[P]
: ReRequired<T[P]>
: T[P];
? T[P] extends ((...args: any[]) => any) | ClassType<T[P]> | undefined
? T[P]
: ReRequired<T[P]>
: T[P];
};
declare type WrapperType<T> = T;