add user module and jwt
This commit is contained in:
parent
6fa6e1f076
commit
b8cc65a768
@ -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';
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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']),
|
||||
|
@ -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} ${
|
||||
|
@ -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";
|
||||
}
|
||||
|
@ -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!';
|
||||
|
@ -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 = {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
115
src/modules/user/services/auth.service.ts
Normal file
115
src/modules/user/services/auth.service.ts
Normal 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');
|
||||
}
|
||||
}
|
@ -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;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
94
src/modules/user/services/user.service.ts
Normal file
94
src/modules/user/services/user.service.ts
Normal 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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user