Commit 18c4bf84 authored by mingyard's avatar mingyard

feat:auth:login register logout

parent f890cb0a
......@@ -6,12 +6,16 @@
"README.md"
],
"words": [
"activerecords",
"dotenv",
"healthz",
"hset",
"Millis",
"mingyard",
"nestjs",
"oidc",
"pids",
"regtime",
"typeorm"
]
}
......@@ -23,24 +23,25 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/cache-manager": "^2.3.0",
"@nestjs-modules/ioredis": "^2.0.2",
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/jwt": "^10.2.0",
"@nestjs/platform-express": "^10.4.15",
"@nestjs/swagger": "^8.1.0",
"@nestjs/typeorm": "^10.0.2",
"axios": "^1.7.9",
"cache-manager": "^6.3.2",
"cache-manager-redis-yet": "^5.1.5",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"dotenv": "^16.4.7",
"ioredis": "^5.4.2",
"lodash": "^4.17.21",
"mysql2": "^3.12.0",
"on-headers": "^1.0.2",
"redis": "^4.7.0",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
"sha1": "^1.1.1",
"typeorm": "^0.3.20"
},
"devDependencies": {
......
......@@ -8,6 +8,8 @@ import { config } from './config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { SystemController } from './controller/system.controller';
import { UserModule } from './controller/user/user.module';
import { AuthModule } from './controller/auth/auth.module';
import { RedisModule } from '@nestjs-modules/ioredis';
@Module({
imports: [
......@@ -20,7 +22,12 @@ import { UserModule } from './controller/user/user.module';
maxQueryExecutionTime: 200,
logging: [config.database.logging],
}),
RedisModule.forRoot({
type: 'single',
url: config.cache.redis.url,
}),
UserModule,
AuthModule,
],
controllers: [SystemController],
})
......
import { CacheModule } from '@nestjs/cache-manager';
import { RedisClientOptions } from 'redis';
import { redisStore } from 'cache-manager-redis-yet';
import { Module } from '@nestjs/common';
import { config } from '../../config';
import { CacheService } from './cache.service';
@Module({
imports: [
CacheModule.register<RedisClientOptions>({
store: redisStore,
url: config.cache.redis.url,
isGlobal: true,
}),
],
providers: [CacheService],
exports: [CacheService],
})
export class AppCacheModule {}
import { Inject, Injectable } from '@nestjs/common';
import { Response } from 'express';
import { v4 } from 'uuid';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { RedisCache } from 'cache-manager-redis-yet';
import { IRequest } from '@/interface';
import { config } from '../../config';
// const EXPIRE_TIME_IN_SEC = 86400;
const EXPIRE_TIME_IN_SEC = 3600;
export function readKeyFromCookie(
type: string,
req: IRequest,
throwError = true,
) {
const key = req.cookies[type];
if (!key && throwError) {
throw new Error('Without ' + type);
}
return key;
}
export function saveKeyIntoCookie(
type: string,
key: string,
res: Response,
options: {
timeout?: number;
} = {},
) {
const { timeout = EXPIRE_TIME_IN_SEC } = options;
res.cookie(type, key, {
maxAge: timeout * 1000,
secure: true,
sameSite: 'none',
httpOnly: true,
signed: true,
});
}
export async function clearKeyFromCookie(type: string, res: Response) {
res.clearCookie(type, {
// domain: getSubDomainHost('').split(':')[0],
});
}
/**
* 负责在顶级域名下存储一次性的临时 session,用来在流程中的各端点之间传递状态信息
* 将redis的key保存在cookie中,将js对象转为string生成redis键值对
*/
@Injectable()
export class CacheService {
constructor(@Inject(CACHE_MANAGER) private cacheManager: RedisCache) {}
async setNx<T = Record<string, any>>(
type: string,
key: string,
value: T,
options: {
timeout?: number;
} = {},
) {
const { timeout = EXPIRE_TIME_IN_SEC } = options;
await this.cacheManager.store.client.set(
config.env + ':' + type + ':' + key,
JSON.stringify(value),
// 超时秒数
{ EX: timeout, NX: true },
);
}
/**
* 创建一个 Interaction,存储到 redis 中。
*
* @param type 类型
* @param value 值
* @param options 额外参数
* - timeout:过期时间,默认为 86400 秒
* - key:唯一标识,不传会自动生成
* @returns Interaction 的唯一标识
*/
async create<T = Record<string, any>>(
type: string,
key: string,
value: T,
options: {
timeout?: number;
} = {},
) {
const { timeout = EXPIRE_TIME_IN_SEC } = options;
await this.cacheManager.store.client.set(
config.env + ':' + type + ':' + key,
JSON.stringify(value),
// 超时秒数
{ EX: timeout },
);
return key;
}
/**
* 创建一个 Interaction,存储到 cookie 和 redis 中。
*
* @param type 类型
* @param value 值
* @param res 响应对象
* @param options 额外参数
* - timeout:过期时间,默认为 86400 秒
* - key:唯一标识,不传会自动生成`
* @returns Interaction 的唯一标识
*/
async createIntoCookie<T = Record<string, any>>(
type: string,
value: T,
res: Response,
options: {
timeout?: number;
key?: string;
} = {},
) {
const key = options.key ?? v4();
await this.create(type, key, value, options);
saveKeyIntoCookie(type, key, res, options);
return key;
}
/**
* 更新一个 Interaction 的值。
*
* @param type 类型
* @param value 值
* @param key 唯一标识
*/
async update<T = Record<string, any>>(
type: string,
key: string,
value: T,
options: {
timeout?: number;
} = {},
) {
await this.cacheManager.set(
config.env + ':' + type + ':' + key,
JSON.stringify(value),
options.timeout ?? EXPIRE_TIME_IN_SEC,
);
}
/**
* 读取缓存值。
*
* @param type 类型
* @param key 唯一标识
* @returns Interaction 的值
*/
async read<T = Record<string, any>>(type: string, key: string) {
const value = await this.cacheManager.get<T>(
config.env + ':' + type + ':' + key,
);
return value;
}
/**
* 清除一个 Interaction。
*
* @param type 类型
* @param key 唯一标识
*/
async clear(type: string, key: string) {
await this.cacheManager.set(config.env + ':' + type + ':' + key, 10);
}
}
import { UseGuards } from '@nestjs/common';
import { AuthGuard } from '../../controller/auth/auth.guard';
/**
* 认证 装饰器
*
*/
export const Auth = () => {
return (...args: any[]) => {
// 装饰器由上至下求值,由下至上执行
const decorators: any[] = [];
decorators.push(UseGuards(AuthGuard));
decorators.reverse().forEach((f) => f(...args));
};
};
import { ErrorCodeEnum } from '@/common/enum/ErrorCodeEnum';
import { UnauthorizedError } from './UnauthorizedError';
export class UserStatusError extends UnauthorizedError {
protected getErrorCode() {
return ErrorCodeEnum.EMAIL_NOT_VERIFIED;
}
static default(message?: string) {
return new UserStatusError(message ?? 'User is not active');
}
}
import {
ArgumentsHost,
BadRequestException,
Catch,
ExceptionFilter,
ForbiddenException,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { Response } from 'express';
import { IRequest } from '@/interface';
import { isArray, isString } from 'lodash';
import { BaseError } from '@/common/exception/BaseError';
@Injectable()
@Catch()
......@@ -19,38 +16,14 @@ export class ApiExceptionsFilter implements ExceptionFilter {
const response = ctx.getResponse<Response>();
const req = ctx.getRequest<IRequest>();
if (exception instanceof ForbiddenException) {
return response.status(403).json({
code: 403,
message: 'Not Permission',
});
}
if (exception instanceof UnauthorizedException) {
const statusCode = (exception as any)?.response?.statusCode || 400;
return response.status(401).json({
code: statusCode,
message: (exception as any)?.response?.message || 'Bad Request',
});
}
if (exception instanceof BadRequestException) {
const validationErrors = (exception as any)?.response?.message;
const statusCode = (exception as any)?.response?.statusCode || 400;
if (isArray(validationErrors) && validationErrors?.length) {
return response.status(statusCode).json({
code: statusCode,
message: validationErrors[0],
});
} else {
return response.status(statusCode).json({
code: statusCode,
message: (exception as any)?.response?.message || 'Bad Request',
if (exception instanceof BaseError) {
const json = exception.toJson();
return response.status(200).json({
code: json.code,
message: json.message,
requestId: req.requestId,
});
}
}
// 未知错误,触发错误报警
console.error('请求 HOST:' + req?.hostname);
......@@ -58,20 +31,21 @@ export class ApiExceptionsFilter implements ExceptionFilter {
console.error('请求参数 QUERY:' + JSON.stringify(req?.query));
console.error('请求参数 BODY:' + JSON.stringify(req?.body));
console.error(
'\n***\n',
'未知错误',
JSON.stringify(exception?.message ?? exception),
);
const status = exception?.status || 500;
console.error('\n***\n', '未知错误', exception);
const status = exception?.status || exception?.statusCode || 500;
console.error('requestId:' + req.requestId);
const errorMessage = exception?.response?.message ?? exception.message;
return response.status(status || 500).json({
code: status,
message: isString(exception.message)
? exception.message
console.error(
'errorMessage:' + errorMessage
? errorMessage
: JSON.parse(exception.message),
);
return response.status(200).json({
code: status,
message: errorMessage ? errorMessage : JSON.parse(exception.message),
requestId: req.requestId,
});
}
......
......@@ -29,6 +29,11 @@ interface AppConfig {
secret: string;
};
jwt: {
secret: string;
expiresIn: string;
};
rsaSecret: {
/** 应用公钥 */
publicKey: string;
......@@ -54,7 +59,12 @@ export const app: AppConfig = {
},
session: {
secret: env.APP_SESSION_SECRET ?? 'api-service',
secret: env.APP_SESSION_SECRET ?? 'session',
},
jwt: {
secret: env.APP_JWT_SECRET ?? 'secret',
expiresIn: env.APP_JWT_EXPIRES_IN ?? '1d',
},
rsaSecret: {
......
import { Test, TestingModule } from '@nestjs/testing';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
describe('AuthController', () => {
let controller: AuthController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuthController],
providers: [AuthService],
}).compile();
controller = module.get<AuthController>(AuthController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});
import {
Controller,
Post,
Body,
Req,
UseInterceptors,
UseFilters,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/req/login.dto';
import { IRequest } from '@/interface';
import { LoginRecordDto } from './dto/loginRecord.dto';
import { ApiResponseInterceptor } from '@/common/interceptor/api.response.interceptor';
import { ApiExceptionsFilter } from '@/common/filter/api.exception.filter';
import { RegisterDto } from './dto/req/register.dto';
import { Auth } from '@/common/decorators/auth.decorator';
@Controller('auth')
@UseInterceptors(ApiResponseInterceptor)
@UseFilters(ApiExceptionsFilter)
export class AuthController {
constructor(private readonly authService: AuthService) {}
// 登录接口
@Post('login')
async login(@Body() loginDto: LoginDto, @Req() req: IRequest) {
const record: LoginRecordDto = {
userId: null,
ip: req.ip,
lastActiveTime: Math.floor(Date.now() / 1000),
level: 0,
platform: 9,
userAgent: req.userAgent,
};
// 调用服务层登录方法
return await this.authService.login(loginDto, record);
}
// 注册接口
@Post('register')
async register(@Body() registerDto: RegisterDto) {
return await this.authService.register(registerDto);
}
// 退出登录接口
@Auth()
@Post('logout')
async logout(@Req() req: IRequest) {
return await this.authService.logout(req.token, req.decodedToken.exp);
}
}
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { config } from '@/config';
import { Request } from 'express';
import { UnauthorizedError } from '@/common/exception/unauthorized/UnauthorizedError';
import { UserService } from '../user/user.service';
import { AuthService } from './auth.service';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private jwtService: JwtService,
private authService: AuthService,
private readonly userService: UserService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw UnauthorizedError.default('Token not found');
}
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: config.jwt.secret,
});
// 💡 We're assigning the payload to the request object here
// so that we can access it in our route handlers
request['decodedToken'] = payload;
request['token'] = token;
const isBlack = await this.authService.isTokenInBlackList(token);
if (isBlack) {
throw UnauthorizedError.default('Invalid token');
}
const user = await this.userService.getUserById(payload.sub);
if (!user) {
throw UnauthorizedError.default('User not found');
}
request['user'] = user;
} catch {
throw UnauthorizedError.default('Invalid token');
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtModule } from '@nestjs/jwt';
import { config } from '@/config';
import { ActiveRecordsEntity } from '@/entities/activeRecords.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserService } from '../user/user.service';
import { AuthGuard } from './auth.guard';
import { UserModule } from '../user/user.module';
@Module({
imports: [
JwtModule.register({
global: true,
secret: config.jwt.secret,
signOptions: { expiresIn: config.jwt.expiresIn },
}),
TypeOrmModule.forFeature([ActiveRecordsEntity]),
UserModule,
],
controllers: [AuthController],
providers: [AuthService, UserService, AuthGuard],
exports: [AuthService, AuthGuard],
})
export class AuthModule {}
import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from './auth.service';
describe('AuthService', () => {
let service: AuthService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AuthService],
}).compile();
service = module.get<AuthService>(AuthService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
import { Injectable } from '@nestjs/common';
import { LoginDto } from './dto/req/login.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { UserEntity } from '../../entities/user.entity';
import { MoreThanOrEqual, Repository } from 'typeorm';
import * as sha1 from 'sha1';
import { UnauthorizedError } from '@/common/exception/unauthorized/UnauthorizedError';
import { ActiveRecordsEntity } from '@/entities/activeRecords.entity';
import { LoginRecordDto } from './dto/loginRecord.dto';
import { JwtService } from '@nestjs/jwt';
import { plainToClass } from 'class-transformer';
import { RegisterDto } from './dto/req/register.dto';
import { LoginResDto } from './dto/res/loginRes.dto';
import { UserService } from '../user/user.service';
import { InjectRedis } from '@nestjs-modules/ioredis';
import Redis from 'ioredis';
import { MD5 } from '@/common/utils/tool';
import { DecodedToken } from '@/interface';
@Injectable()
export class AuthService {
constructor(
@InjectRepository(ActiveRecordsEntity)
private readonly activeRecordsRepository: Repository<ActiveRecordsEntity>,
private readonly jwtService: JwtService,
private readonly userService: UserService,
@InjectRedis() private readonly redis: Redis,
) {}
// 登录
async login(
loginDto: LoginDto,
loginRecord: LoginRecordDto,
): Promise<LoginResDto> {
const { phone, password } = loginDto;
// 验证用户
const user = await this.validateUser(phone, password);
// 更新用户登录时间
await this.logSingIn(user);
// 记录登录信息
await this.loginRecords(user, loginRecord);
// jwt
const payload = { sub: user.id, username: user.phone };
const token = await this.jwtService.signAsync(payload);
// 转换并返回用户信息
return plainToClass(
LoginResDto,
{ ...user, token },
{ excludeExtraneousValues: true },
);
}
// 注册
async register(registerDto: RegisterDto): Promise<LoginResDto> {
// 校验 验证码
await this.verifyCode(registerDto.code);
// 校验手机号是否已注册
const exist = await this.userService.existByPhone(registerDto.phone);
if (exist) {
throw UnauthorizedError.default('手机号已注册');
}
// 创建用户
const user = await this.userService.createUser(registerDto);
// jwt
const payload = { sub: user.id, username: user.phone };
const token = await this.jwtService.signAsync(payload);
// 转换并返回用户信息
return plainToClass(
LoginResDto,
{ ...user, token },
{ excludeExtraneousValues: true },
);
}
// 登出
async logout(token: string, exp: number): Promise<void> {
// 将 token 添加到 Redis 黑名单中,并设置过期时间
const ttl = exp - Math.floor(Date.now() / 1000);
const key = this.getBlackListKey(token);
await this.redis.set(key, token, 'EX', ttl);
}
// 获取黑名单 Key
getBlackListKey(token: string): string {
return `translation:blacklist:${MD5(token)}`;
}
// 验证 token 是否在黑名单中
async isTokenInBlackList(token: string): Promise<boolean> {
const key = this.getBlackListKey(token);
const exists = await this.redis.exists(key);
return !!exists;
}
// 重置密码
async resetPwd(resetDto: RegisterDto): Promise<LoginResDto> {
// 校验 验证码
await this.verifyCode(resetDto.code);
// 校验手机号是否已注册
const exist = await this.userService.existByPhone(resetDto.phone);
if (!exist) {
throw UnauthorizedError.default('手机号未注册');
}
// 更新用户密码
const user = await this.userService.updatePwd(resetDto);
// jwt
const payload = { sub: user.id, username: user.phone };
const token = await this.jwtService.signAsync(payload);
// 转换并返回用户信息
return plainToClass(
LoginResDto,
{ ...user, token },
{ excludeExtraneousValues: true },
);
}
// 验证用户
async validateUser(phone: string, password: string): Promise<UserEntity> {
const user = await this.userService.getUserByPhone(phone);
if (user && user.pwd === sha1(password)) {
return user;
}
throw UnauthorizedError.default('用户名或密码错误');
}
// 更新用户登录时间
async logSingIn(user: UserEntity): Promise<void> {
user.lastLoginTime = Math.floor(Date.now() / 1000);
await this.userService.update(user);
}
// 添加登录日志
async loginRecords(
user: UserEntity,
loginRecord: LoginRecordDto,
): Promise<void> {
// 查询用户登录日志
const record = await this.activeRecordsRepository.findOne({
where: {
userId: user.id,
lastActiveTime: MoreThanOrEqual(new Date().setHours(0, 0, 0, 0) / 1000),
},
order: {
lastActiveTime: 'DESC',
},
});
loginRecord.userId = user.id;
loginRecord.level = user.level;
loginRecord.lastActiveTime = Math.floor(Date.now() / 1000);
if (record) {
// 更新记录
await this.activeRecordsRepository.update(
{
id: record.id,
},
{
...loginRecord,
times: (record.times || 0) + 1,
},
);
}
// 新增记录
await this.activeRecordsRepository.save({
...loginRecord,
});
}
// 校验验证码
async verifyCode(code: number): Promise<boolean> {
// TODO
return true;
}
}
export class LoginRecordDto {
userId: number | null;
ip: string;
lastActiveTime: number;
level: number;
platform: number;
userAgent: string;
}
import { IsString, IsNotEmpty, IsPhoneNumber } from 'class-validator';
export class LoginDto {
@IsNotEmpty()
@IsString()
@IsPhoneNumber('CN')
phone: string;
@IsNotEmpty()
@IsString()
password: string;
}
import {
IsString,
IsNotEmpty,
IsPhoneNumber,
IsNumber,
Matches,
} from 'class-validator';
export class RegisterDto {
@IsNotEmpty()
@IsString()
@IsPhoneNumber('CN')
phone: string;
@IsNotEmpty()
@IsNumber()
code: number;
@IsNotEmpty()
@IsString()
@Matches(/^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/, {
message: '密码必须包含至少8个字符,包括字母和数字',
})
password: string;
}
import { Expose } from 'class-transformer';
import { UserBaseDto } from '../userBase.dto';
export class LoginResDto extends UserBaseDto {
@Expose()
token: string;
}
import { Expose } from 'class-transformer';
export class UserBaseDto {
@Expose()
id: number;
@Expose()
acc: string;
@Expose()
phone: string;
@Expose()
mailAddr: string;
@Expose()
lastLoginTime: number;
@Expose()
state: number;
@Expose()
nickname?: string;
}
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
import { Controller } from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}
@Get()
findAll() {
return this.userService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.userService.findOne(+id);
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return this.userService.update(+id, updateUserDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.userService.remove(+id);
}
}
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserEntity } from '@/entities/user.entity';
import { UserInfoEntity } from '@/entities/userInfo.entity';
@Module({
imports: [TypeOrmModule.forFeature([UserEntity, UserInfoEntity])],
controllers: [UserController],
providers: [UserService],
exports: [UserService, TypeOrmModule],
})
export class UserModule {}
import { UserEntity } from '@/entities/user.entity';
import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { RegisterDto } from '../auth/dto/req/register.dto';
import { v4 as uuid } from 'uuid';
import * as sha1 from 'sha1';
import { UserInfoEntity } from '@/entities/userInfo.entity';
import { UserBaseDto } from '../auth/dto/userBase.dto';
import { plainToClass } from 'class-transformer';
@Injectable()
export class UserService {
create(createUserDto: CreateUserDto) {
return 'This action adds a new user';
constructor(
@InjectRepository(UserEntity)
private readonly userRepository: Repository<UserEntity>,
@InjectRepository(UserInfoEntity)
private readonly userInfoRepository: Repository<UserInfoEntity>,
) {}
// 用户是否存在
async existByPhone(phone: string): Promise<boolean> {
const user = await this.userRepository.findOne({ where: { phone } });
return !!user;
}
// 查找最大用户id
async findMaxUserId(): Promise<number | undefined> {
const result = await this.userRepository
.createQueryBuilder('user')
.select('MAX(user.id)', 'max')
.getRawOne();
return result ? result.max : undefined;
}
// 创建用户
async createUser({ phone, password }: RegisterDto): Promise<UserBaseDto> {
// 注册
const userEntity = new UserEntity();
userEntity.phone = phone;
userEntity.pwd = sha1(password);
// 注册类型 0: 手机号注册
userEntity.regType = 0;
userEntity.state = 0;
// 注册来源 18: 翻译机
userEntity.regSource = 18;
userEntity.regtime = Math.floor(Date.now() / 1000);
userEntity.uid = uuid();
const maxId = await this.findMaxUserId();
// userEntity.acc = (maxId + 100000000 + Math.random() * 100).toString();
userEntity.acc = `${maxId + 100000000}${Math.floor(Math.random() * 100)}`;
// 保存用户
const user = await this.userRepository.save(userEntity);
// 创建用户信息
const userInfo = await this.userInfoRepository.save({
userId: userEntity.id,
nickname: `InnAIO-${user.id * 11}`,
});
// 转换并返回用户信息
return plainToClass(
UserBaseDto,
{ ...userInfo, ...user },
{ excludeExtraneousValues: true },
);
}
// 重置密码
async updatePwd({ phone, password }: RegisterDto): Promise<UserEntity> {
const user = await this.userRepository.findOne({ where: { phone } });
if (user) {
user.pwd = sha1(password);
await this.userRepository.save(user);
}
findAll() {
return `This action returns all user`;
return user;
}
findOne(id: number) {
return `This action returns a #${id} user`;
async getUserById(id: number): Promise<UserEntity> {
return await this.userRepository.findOneBy({ id });
}
update(id: number, updateUserDto: UpdateUserDto) {
return `This action updates a #${id} user`;
async getUserByPhone(phone: string): Promise<UserEntity> {
return await this.userRepository.findOneBy({ phone });
}
remove(id: number) {
return `This action removes a #${id} user`;
async update(user: UserEntity): Promise<void> {
await this.userRepository.update(user.id, user);
}
}
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity('activerecords')
export class ActiveRecordsEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({
nullable: true,
name: 'userId',
comment: '用户id',
})
userId: number;
@Column({
nullable: true,
name: 'lastActiveTime',
comment: '上次活跃时间',
})
lastActiveTime: number;
@Column({
nullable: true,
name: 'state',
comment: '是否删除',
})
state: number;
@Column({
nullable: true,
name: 'ip',
comment: '当前ip',
})
ip: string;
@Column({
nullable: true,
name: 'platform',
comment: '平台',
})
platform: number;
@Column({
nullable: true,
name: 'userAgent',
comment: '客户端信息',
})
userAgent: string;
@Column({
nullable: true,
name: 'level',
comment: '会员级别',
})
level: number;
@Column({
nullable: true,
name: 'logoutTime',
comment: '离线时间',
})
logoutTime: number;
@Column({
nullable: true,
name: 'times',
comment: '登录次数',
})
times: number;
}
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity('user')
export class UserEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({
nullable: false,
name: 'acc',
comment: '账号',
})
acc: string;
@Column({
nullable: true,
name: 'uid',
comment: '唯一id',
})
uid: string;
@Column({
nullable: false,
name: 'pwd',
comment: '密码',
})
pwd: string;
@Column({
nullable: true,
name: 'mailAddr',
comment: '邮箱',
})
mailAddr: string;
@Column({
nullable: true,
name: 'phone',
comment: '手机号',
})
phone: string;
@Column({
nullable: true,
name: 'state',
comment: '状态',
})
state: number;
@Column({
nullable: true,
name: 'regType',
comment: '注册类型',
})
regType: number;
@Column({
nullable: true,
name: 'mailValidate',
comment: '邮箱验证',
})
mailValidate: number;
@Column({
nullable: false,
name: 'type',
comment: '类型',
})
type: number;
@Column({
nullable: false,
name: 'regtime',
comment: '注册时间',
})
regtime: number;
@Column({
nullable: false,
name: 'regSource',
comment: '注册来源',
})
regSource: number;
@Column({
nullable: true,
name: 'lastLoginTime',
comment: '最后登录时间',
})
lastLoginTime: number;
@Column({
nullable: true,
name: 'level',
comment: '会员等级',
})
level: number;
}
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity('userinfo')
export class UserInfoEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({
nullable: true,
name: 'nickname',
comment: '昵称',
})
nickname: string;
@Column({
nullable: true,
name: 'sex',
comment: '性别',
})
birth: string;
@Column({
nullable: true,
name: 'headImgUrl',
comment: '头像',
})
headImgUrl: string;
@Column({
nullable: true,
name: 'age',
comment: '年龄',
})
age: number;
@Column({
nullable: true,
name: 'username',
comment: '用户名',
})
username: string;
}
import { UserEntity } from '@/entities/user.entity';
import * as express from 'express';
// JSON 类型递归定义
......@@ -45,11 +46,14 @@ export interface IRequest extends express.Request {
userAgent: string;
loggedIn: boolean;
user: UserEntity;
clientIp: string;
device: string;
os: string;
octetString: string;
token: string;
decodedToken: DecodedToken;
lang: Lang;
......@@ -81,6 +85,7 @@ export interface DecodedToken {
phone_number?: string;
phone_number_verified?: boolean;
address?: JSONValue;
exp: number;
}
export type KeyValuePair = Record<string, any>;
......@@ -6,12 +6,21 @@ import * as onHeaders from 'on-headers';
import { setupOpenApi } from './openapi';
import { NestExpressApplication } from '@nestjs/platform-express';
import { config } from './config';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
rawBody: true,
});
app.enableCors({
credentials: true,
origin: config.cors.origin,
maxAge: 604800,
});
app.useGlobalPipes(new ValidationPipe());
app.use((req: IRequest, res: Response, next) => {
onHeaders(res, () => {
res.setHeader('Cache-Control', 'no-cache');
......
import { Injectable } from '@nestjs/common';
import { UserEntity } from '../entities/user.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
@Injectable()
export class UserProvider {
constructor(
@InjectRepository(UserEntity)
private userRepository: Repository<UserEntity>,
) {}
async getUserById(id: number): Promise<UserEntity> {
return await this.userRepository.findOneBy({ id });
}
}
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment