feat: middleware & user module init
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import argv from 'argv'
|
||||
import { isDev } from './utils'
|
||||
import { isDev } from './utils/index.util'
|
||||
export const CROSS_DOMAIN = {
|
||||
allowedOrigins: [
|
||||
'innei.ren',
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import { CacheInterceptor, Module } from '@nestjs/common'
|
||||
import {
|
||||
CacheInterceptor,
|
||||
MiddlewareConsumer,
|
||||
Module,
|
||||
NestModule,
|
||||
RequestMethod,
|
||||
} from '@nestjs/common'
|
||||
import { ConfigModule } from '@nestjs/config'
|
||||
import { APP_INTERCEPTOR } from '@nestjs/core'
|
||||
import { AppController } from './app.controller'
|
||||
import { HttpCacheInterceptor } from './common/interceptors/cache.interceptor'
|
||||
import { AnalyzeMiddleware } from './common/middlewares/analyze.middleware'
|
||||
import { SkipBrowserDefaultRequestMiddleware } from './common/middlewares/favicon.middleware'
|
||||
import { SecurityMiddleware } from './common/middlewares/security.middleware'
|
||||
import { AuthModule } from './modules/auth/auth.module'
|
||||
// must after post
|
||||
import { CategoryModule } from './modules/category/category.module'
|
||||
@@ -47,4 +56,12 @@ import { HelperModule } from './processors/helper/helper.module'
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
export class AppModule implements NestModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer
|
||||
.apply(AnalyzeMiddleware)
|
||||
.forRoutes({ path: '*', method: RequestMethod.GET })
|
||||
.apply(SkipBrowserDefaultRequestMiddleware, SecurityMiddleware)
|
||||
.forRoutes({ path: '*', method: RequestMethod.ALL })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { applyDecorators, UseGuards } from '@nestjs/common'
|
||||
import { AuthGuard } from '@nestjs/passport'
|
||||
import { ApiBearerAuth, ApiUnauthorizedResponse } from '@nestjs/swagger'
|
||||
import { isDev } from '~/utils'
|
||||
import { isDev } from '~/utils/index.util'
|
||||
|
||||
export function Auth() {
|
||||
const decorators = []
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common'
|
||||
import { FastifyRequest } from 'fastify'
|
||||
import { getIp } from '~/utils/ip'
|
||||
import { getIp } from '~/utils/ip.util'
|
||||
|
||||
export type IpRecord = {
|
||||
ip: string
|
||||
|
||||
66
src/common/filters/any-exception.filter.ts
Normal file
66
src/common/filters/any-exception.filter.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import {
|
||||
ArgumentsHost,
|
||||
Catch,
|
||||
ExceptionFilter,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Logger,
|
||||
} from '@nestjs/common'
|
||||
import { FastifyReply, FastifyRequest } from 'fastify'
|
||||
import { isDev } from '~/utils/index.util'
|
||||
import { getIp } from '../../utils/ip.util'
|
||||
import { writeFileSync } from 'fs'
|
||||
import { LOGGER_DIR } from '~/constants/path.constant'
|
||||
import { resolve } from 'path'
|
||||
type myError = {
|
||||
readonly status: number
|
||||
readonly statusCode?: number
|
||||
|
||||
readonly message?: string
|
||||
}
|
||||
|
||||
@Catch()
|
||||
export class AllExceptionsFilter implements ExceptionFilter {
|
||||
private readonly logger = new Logger('捕获异常')
|
||||
catch(exception: unknown, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp()
|
||||
const response = ctx.getResponse<FastifyReply>()
|
||||
const request = ctx.getRequest<FastifyRequest>()
|
||||
|
||||
const status =
|
||||
exception instanceof HttpException
|
||||
? exception.getStatus()
|
||||
: (exception as myError)?.status ||
|
||||
(exception as myError)?.statusCode ||
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
if (isDev) {
|
||||
console.error(exception)
|
||||
} else {
|
||||
const ip = getIp(request)
|
||||
this.logger.warn(
|
||||
`IP: ${ip} 错误信息: (${status}) ${
|
||||
(exception as any)?.response?.message ||
|
||||
(exception as myError)?.message ||
|
||||
''
|
||||
} Path: ${decodeURI(request.raw.url)}`,
|
||||
)
|
||||
|
||||
writeFileSync(
|
||||
resolve(LOGGER_DIR, 'error.log'),
|
||||
`[${new Date().toISOString()}] ${decodeURI(request.raw.url)}: ${
|
||||
(exception as any)?.response?.message ||
|
||||
(exception as myError)?.message
|
||||
} \n ${(exception as Error).stack || ''} \n`,
|
||||
{ encoding: 'utf-8', flag: 'a+' },
|
||||
)
|
||||
}
|
||||
|
||||
response.status(status).send({
|
||||
ok: 0,
|
||||
message:
|
||||
(exception as any)?.response?.message ||
|
||||
(exception as any)?.message ||
|
||||
'未知错误',
|
||||
})
|
||||
}
|
||||
}
|
||||
38
src/common/guard/spider.guard.ts
Normal file
38
src/common/guard/spider.guard.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* @Author: Innei
|
||||
* @Date: 2020-04-30 19:09:37
|
||||
* @LastEditTime: 2020-07-08 21:35:06
|
||||
* @LastEditors: Innei
|
||||
* @FilePath: /mx-server/src/core/guards/spider.guard.ts
|
||||
* @Coding with Love
|
||||
*/
|
||||
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
} from '@nestjs/common'
|
||||
import { FastifyRequest } from 'fastify'
|
||||
import { Observable } from 'rxjs'
|
||||
import { isDev } from '~/utils/index.util'
|
||||
|
||||
@Injectable()
|
||||
export class SpiderGuard implements CanActivate {
|
||||
canActivate(
|
||||
context: ExecutionContext,
|
||||
): boolean | Promise<boolean> | Observable<boolean> {
|
||||
if (isDev) {
|
||||
return true
|
||||
}
|
||||
const http = context.switchToHttp()
|
||||
const request = http.getRequest<FastifyRequest>()
|
||||
const headers = request.headers
|
||||
const ua: string = headers['user-agent'] || ''
|
||||
const isSpiderUA = !!ua.match(/(Scrapy|Curl|HttpClient|python|requests)/i)
|
||||
if (ua && !isSpiderUA) {
|
||||
return true
|
||||
}
|
||||
throw new ForbiddenException('爬虫, 禁止')
|
||||
}
|
||||
}
|
||||
47
src/common/interceptors/logging.interceptor.ts
Normal file
47
src/common/interceptors/logging.interceptor.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Logging interceptor.
|
||||
* @file 日志拦截器
|
||||
* @module interceptor/logging
|
||||
* @author Surmon <https://github.com/surmon-china>
|
||||
*/
|
||||
|
||||
import { Observable } from 'rxjs'
|
||||
import { tap } from 'rxjs/operators'
|
||||
import {
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
CallHandler,
|
||||
ExecutionContext,
|
||||
Logger,
|
||||
} from '@nestjs/common'
|
||||
import { isDev } from '~/utils/index.util'
|
||||
|
||||
@Injectable()
|
||||
export class LoggingInterceptor implements NestInterceptor {
|
||||
private logger: Logger
|
||||
|
||||
constructor() {
|
||||
this.logger = new Logger(LoggingInterceptor.name)
|
||||
}
|
||||
intercept(
|
||||
context: ExecutionContext,
|
||||
next: CallHandler<any>,
|
||||
): Observable<any> {
|
||||
const call$ = next.handle()
|
||||
if (!isDev) {
|
||||
return call$
|
||||
}
|
||||
const request = context.switchToHttp().getRequest()
|
||||
const content = request.method + ' -> ' + request.url
|
||||
Logger.debug('+++ 收到请求:' + content, LoggingInterceptor.name)
|
||||
const now = +new Date()
|
||||
|
||||
return call$.pipe(
|
||||
tap(() =>
|
||||
this.logger.debug(
|
||||
'--- 响应请求:' + content + ` +${+new Date() - now}ms`,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
101
src/common/interceptors/response.interceptors.ts
Normal file
101
src/common/interceptors/response.interceptors.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* @Author: Innei
|
||||
* @Date: 2020-11-24 16:20:37
|
||||
* @LastEditTime: 2021-03-21 20:12:56
|
||||
* @LastEditors: Innei
|
||||
* @FilePath: /server/shared/core/interceptors/response.interceptors.ts
|
||||
* Mark: Coding with Love
|
||||
*/
|
||||
import {
|
||||
CallHandler,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
UnprocessableEntityException,
|
||||
} from '@nestjs/common'
|
||||
import { isArrayLike, isObjectLike } from 'lodash'
|
||||
import { Observable } from 'rxjs'
|
||||
import { map } from 'rxjs/operators'
|
||||
|
||||
import snakecaseKeys from 'snakecase-keys'
|
||||
|
||||
export interface Response<T> {
|
||||
data: T
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ResponseInterceptor<T> implements NestInterceptor<T, Response<T>> {
|
||||
intercept(
|
||||
context: ExecutionContext,
|
||||
next: CallHandler,
|
||||
): Observable<Response<T>> {
|
||||
const reorganize = (data) => {
|
||||
if (!data) {
|
||||
throw new UnprocessableEntityException('数据丢失了(。 ́︿ ̀。)')
|
||||
}
|
||||
return typeof data !== 'object' || data.__proto__.constructor === Object
|
||||
? { ...data }
|
||||
: { data }
|
||||
}
|
||||
return next.handle().pipe(
|
||||
map((data) =>
|
||||
typeof data === 'undefined'
|
||||
? // HINT: hack way to solve `undefined` as cache value set into redis got an error.
|
||||
''
|
||||
: typeof data === 'object' && data !== null
|
||||
? { ...reorganize(data) }
|
||||
: data,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class JSONSerializeInterceptor implements NestInterceptor {
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||
return next.handle().pipe(
|
||||
map((data) => {
|
||||
return this.serialize(data)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
private serialize(obj: any) {
|
||||
if (!isObjectLike(obj)) {
|
||||
return obj
|
||||
}
|
||||
|
||||
if (isArrayLike(obj)) {
|
||||
obj = Array.from(obj).map((i) => {
|
||||
return this.serialize(i)
|
||||
})
|
||||
} else {
|
||||
// if is Object
|
||||
|
||||
if (obj.toJSON || obj.toObject) {
|
||||
obj = obj.toJSON?.() ?? obj.toObject?.()
|
||||
}
|
||||
|
||||
const keys = Object.keys(obj)
|
||||
for (const key of keys) {
|
||||
const val = obj[key]
|
||||
// first
|
||||
if (!isObjectLike(val)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (val.toJSON) {
|
||||
obj[key] = val.toJSON()
|
||||
// second
|
||||
if (!isObjectLike(obj[key])) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
obj[key] = this.serialize(obj[key])
|
||||
// obj[key] = snakecaseKeys(obj[key])
|
||||
}
|
||||
obj = snakecaseKeys(obj)
|
||||
delete obj.v
|
||||
}
|
||||
return obj
|
||||
}
|
||||
}
|
||||
7
src/common/middlewares/analyze.middleware.ts
Normal file
7
src/common/middlewares/analyze.middleware.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { NestMiddleware } from '@nestjs/common'
|
||||
// TODO:
|
||||
export class AnalyzeMiddleware implements NestMiddleware {
|
||||
use(req, res, next) {
|
||||
next()
|
||||
}
|
||||
}
|
||||
17
src/common/middlewares/favicon.middleware.ts
Normal file
17
src/common/middlewares/favicon.middleware.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common'
|
||||
import { IncomingMessage, ServerResponse } from 'http'
|
||||
import { parseRelativeUrl } from '~/utils/ip.util'
|
||||
|
||||
@Injectable()
|
||||
export class SkipBrowserDefaultRequestMiddleware implements NestMiddleware {
|
||||
async use(req: IncomingMessage, res: ServerResponse, next: () => void) {
|
||||
// @ts-ignore
|
||||
const url = parseRelativeUrl(req.originalUrl).pathname
|
||||
|
||||
if (url.match(/favicon.ico$/) || url.match(/manifest.json$/)) {
|
||||
res.writeHead(204)
|
||||
return res.end()
|
||||
}
|
||||
next()
|
||||
}
|
||||
}
|
||||
21
src/common/middlewares/security.middleware.ts
Normal file
21
src/common/middlewares/security.middleware.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common'
|
||||
import { IncomingMessage, ServerResponse } from 'http'
|
||||
import { parseRelativeUrl } from '~/utils/ip.util'
|
||||
// 用于屏蔽 PHP 的请求
|
||||
|
||||
@Injectable()
|
||||
export class SecurityMiddleware implements NestMiddleware {
|
||||
async use(req: IncomingMessage, res: ServerResponse, next: () => void) {
|
||||
// @ts-ignore
|
||||
const url = parseRelativeUrl(req.originalUrl).pathname
|
||||
|
||||
if (url.match(/\.php$/g)) {
|
||||
res.statusMessage =
|
||||
'Eh. PHP is not support on this machine. Yep, I also think PHP is bestest programming language. But for me it is beyond my reach.'
|
||||
return res.writeHead(666).end()
|
||||
} else if (url.match(/\/(adminer|admin|wp-login)$/g)) {
|
||||
res.statusMessage = 'Hey, What the fuck are you doing!'
|
||||
return res.writeHead(200).end()
|
||||
} else next()
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
import { homedir } from 'os'
|
||||
import { join } from 'path'
|
||||
import { isDev } from '~/utils'
|
||||
import { isDev } from '~/utils/index.util'
|
||||
|
||||
export const HOME = homedir()
|
||||
|
||||
|
||||
12
src/main.ts
12
src/main.ts
@@ -2,10 +2,16 @@ import { NestFactory } from '@nestjs/core'
|
||||
import { AppModule } from './app.module'
|
||||
import { NestFastifyApplication } from '@nestjs/platform-fastify'
|
||||
import { fastifyApp } from './common/adapt/fastify'
|
||||
import { isDev } from './utils'
|
||||
import { isDev } from './utils/index.util'
|
||||
import { CacheInterceptor, Logger } from '@nestjs/common'
|
||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'
|
||||
import { CROSS_DOMAIN } from './app.config'
|
||||
import {
|
||||
JSONSerializeInterceptor,
|
||||
ResponseInterceptor,
|
||||
} from './common/interceptors/response.interceptors'
|
||||
import { SpiderGuard } from './common/guard/spider.guard'
|
||||
import { LoggingInterceptor } from './common/interceptors/logging.interceptor'
|
||||
// const PORT = parseInt(process.env.PORT) || 2333
|
||||
const PORT = 2333
|
||||
const APIVersion = 1
|
||||
@@ -30,6 +36,10 @@ async function bootstrap() {
|
||||
})
|
||||
|
||||
app.setGlobalPrefix(isDev ? '' : `api/v${APIVersion}`)
|
||||
app.useGlobalInterceptors(new ResponseInterceptor())
|
||||
app.useGlobalInterceptors(new JSONSerializeInterceptor())
|
||||
app.useGlobalInterceptors(new LoggingInterceptor())
|
||||
app.useGlobalGuards(new SpiderGuard())
|
||||
if (isDev) {
|
||||
const options = new DocumentBuilder()
|
||||
.setTitle('API')
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { forwardRef, Inject, Injectable } from '@nestjs/common'
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import { ReturnModelType } from '@typegoose/typegoose'
|
||||
import { InjectModel } from 'nestjs-typegoose'
|
||||
import { PostService } from '../post/post.service'
|
||||
import { CategoryModel } from './category.model'
|
||||
|
||||
@Injectable()
|
||||
|
||||
18
src/modules/init/init.controller.ts
Normal file
18
src/modules/init/init.controller.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Controller, Get } from '@nestjs/common'
|
||||
import { ApiTags } from '@nestjs/swagger'
|
||||
import { UserService } from '../user/user.service'
|
||||
|
||||
@Controller({
|
||||
path: '/init',
|
||||
})
|
||||
@ApiTags('Init Routes')
|
||||
export class InitController {
|
||||
constructor(private readonly userService: UserService) {}
|
||||
|
||||
@Get('/')
|
||||
async isInit() {
|
||||
return {
|
||||
isInit: await this.userService.hasMaster(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
import { Module } from '@nestjs/common'
|
||||
import { UserModule } from '../user/user.module'
|
||||
import { InitController } from './init.controller'
|
||||
import { InitService } from './init.service'
|
||||
|
||||
@Module({
|
||||
providers: [InitService],
|
||||
exports: [InitService],
|
||||
controllers: [InitController],
|
||||
imports: [UserModule],
|
||||
})
|
||||
export class InitModule {}
|
||||
|
||||
78
src/modules/user/dto/user.dto.ts
Normal file
78
src/modules/user/dto/user.dto.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { ApiProperty } from '@nestjs/swagger'
|
||||
import {
|
||||
IsEmail,
|
||||
IsNotEmpty,
|
||||
IsObject,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUrl,
|
||||
} from 'class-validator'
|
||||
|
||||
class UserOptionDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({ example: '我是练习时长两年半的个人练习生' })
|
||||
readonly introduce?: string
|
||||
|
||||
@ApiProperty({ required: false, example: 'example@example.com' })
|
||||
@IsEmail()
|
||||
@IsOptional()
|
||||
readonly mail?: string
|
||||
|
||||
@ApiProperty({ required: false, example: 'http://example.com' })
|
||||
@IsUrl({ require_protocol: true }, { message: '请更正为正确的网址' })
|
||||
@IsOptional()
|
||||
readonly url?: string
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
name?: string
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsUrl({ require_protocol: true })
|
||||
@IsOptional()
|
||||
readonly avatar?: string
|
||||
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
@ApiProperty({ description: '各种社交 id 记录' })
|
||||
readonly socialIds?: Record<string, any>
|
||||
}
|
||||
|
||||
export class UserDto extends UserOptionDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
readonly username: string
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
@IsNotEmpty()
|
||||
readonly password: string
|
||||
}
|
||||
|
||||
export class LoginDto {
|
||||
@ApiProperty({ required: true })
|
||||
@IsString()
|
||||
username: string
|
||||
|
||||
@ApiProperty({ required: true })
|
||||
@IsString()
|
||||
password: string
|
||||
}
|
||||
|
||||
export class UserPatchDto extends UserOptionDto {
|
||||
@ApiProperty({ required: false })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@IsOptional()
|
||||
readonly username: string
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ required: false })
|
||||
@IsNotEmpty()
|
||||
@IsOptional()
|
||||
readonly password: string
|
||||
}
|
||||
@@ -1,6 +1,93 @@
|
||||
import { Controller } from '@nestjs/common'
|
||||
import { ApiTags } from '@nestjs/swagger'
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Patch,
|
||||
Post,
|
||||
SerializeOptions,
|
||||
UseGuards,
|
||||
} from '@nestjs/common'
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'
|
||||
import { CurrentUser } from '~/common/decorator/current-user.decorator'
|
||||
import { IpLocation, IpRecord } from '~/common/decorator/ip.decorator'
|
||||
import { AuthService } from '../auth/auth.service'
|
||||
import { RolesGuard } from '../auth/roles.guard'
|
||||
import { UserDocument, UserModel } from './user.model'
|
||||
import { UserService } from './user.service'
|
||||
import { IsMaster } from '~/common/decorator/role.decorator'
|
||||
import { AuthGuard } from '@nestjs/passport'
|
||||
import { getAvatar } from '~/utils/index.util'
|
||||
import { LoginDto, UserDto, UserPatchDto } from './dto/user.dto'
|
||||
|
||||
@ApiTags('User Routes')
|
||||
@Controller(['user', 'master'])
|
||||
export class UserController {}
|
||||
@Controller(['master', 'user'])
|
||||
export class UserController {
|
||||
constructor(
|
||||
private readonly userService: UserService,
|
||||
private readonly authService: AuthService,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: '获取主人信息' })
|
||||
@UseGuards(RolesGuard)
|
||||
async getMasterInfo(@IsMaster() isMaster: boolean) {
|
||||
return await this.userService.getMasterInfo(isMaster)
|
||||
}
|
||||
|
||||
@Post('register')
|
||||
@SerializeOptions({
|
||||
excludePrefixes: ['password'],
|
||||
})
|
||||
@ApiOperation({ summary: '注册' })
|
||||
async register(@Body() userDto: UserDto) {
|
||||
userDto.name = userDto.name ?? userDto.username
|
||||
return await this.userService.createMaster(userDto as UserModel)
|
||||
}
|
||||
|
||||
@Post('login')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: '登录' })
|
||||
@UseGuards(AuthGuard('local'))
|
||||
async login(
|
||||
@Body() dto: LoginDto,
|
||||
@CurrentUser() user: UserDocument,
|
||||
@IpLocation() ipLocation: IpRecord,
|
||||
) {
|
||||
const footstep = await this.userService.recordFootstep(ipLocation.ip)
|
||||
const { name, username, created, url, mail } = user
|
||||
const avatar = user.avatar ?? getAvatar(mail)
|
||||
|
||||
return {
|
||||
token: await this.authService.signToken(user._id),
|
||||
...footstep,
|
||||
name,
|
||||
username,
|
||||
created,
|
||||
url,
|
||||
mail,
|
||||
avatar,
|
||||
expiresIn: 7,
|
||||
}
|
||||
}
|
||||
|
||||
@Get('check_logged')
|
||||
@ApiOperation({ summary: '判断当前 Token 是否有效 ' })
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(RolesGuard)
|
||||
checkLogged(@IsMaster() isMaster: boolean) {
|
||||
return { ok: Number(isMaster), isGuest: !isMaster }
|
||||
}
|
||||
|
||||
@Patch()
|
||||
@ApiOperation({ summary: '修改主人的信息 ' })
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
async patchMasterData(
|
||||
@Body() body: UserPatchDto,
|
||||
@CurrentUser() user: UserDocument,
|
||||
) {
|
||||
return await this.userService.patchUserData(user, body)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { Module } from '@nestjs/common'
|
||||
import { AuthModule } from '../auth/auth.module'
|
||||
import { UserController } from './user.controller'
|
||||
import { UserService } from './user.service'
|
||||
|
||||
@Module({
|
||||
controllers: [UserController],
|
||||
providers: [UserService],
|
||||
imports: [AuthModule],
|
||||
exports: [UserService],
|
||||
})
|
||||
export class UserModule {}
|
||||
|
||||
@@ -1,12 +1,129 @@
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
Logger,
|
||||
UnprocessableEntityException,
|
||||
} from '@nestjs/common'
|
||||
import { ReturnModelType } from '@typegoose/typegoose'
|
||||
import { compareSync } from 'bcrypt'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { InjectModel } from 'nestjs-typegoose'
|
||||
import { UserModel } from './user.model'
|
||||
import { getAvatar } from '~/utils/index.util'
|
||||
import { AuthService } from '../auth/auth.service'
|
||||
import { UserDocument, UserModel } from './user.model'
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
private Logger = new Logger(UserService.name)
|
||||
constructor(
|
||||
@InjectModel(UserModel)
|
||||
private userModel: ReturnModelType<typeof UserModel>,
|
||||
private readonly userModel: ReturnModelType<typeof UserModel>,
|
||||
private readonly authService: AuthService,
|
||||
) {}
|
||||
|
||||
async getMasterInfo(getLoginIp = false) {
|
||||
const user = await this.userModel
|
||||
.findOne()
|
||||
.select('-authCode' + (getLoginIp ? ' +lastLoginIp' : ''))
|
||||
.lean({ virtuals: true })
|
||||
if (!user) {
|
||||
throw new BadRequestException('没有完成初始化!')
|
||||
}
|
||||
const avatar = user.avatar ?? getAvatar(user.mail)
|
||||
return { ...user, avatar }
|
||||
}
|
||||
async hasMaster() {
|
||||
return !!(await this.userModel.countDocuments())
|
||||
}
|
||||
async createMaster(
|
||||
model: Pick<UserModel, 'username' | 'name' | 'password'> &
|
||||
Partial<Pick<UserModel, 'introduce' | 'avatar' | 'url'>>,
|
||||
) {
|
||||
const hasMaster = await this.hasMaster()
|
||||
// 禁止注册两个以上账户
|
||||
if (hasMaster) {
|
||||
throw new BadRequestException('我已经有一个主人了哦')
|
||||
}
|
||||
const authCode = nanoid(10)
|
||||
|
||||
// @ts-ignore
|
||||
const res = await this.userModel.create({ ...model, authCode })
|
||||
const token = await this.authService.signToken(res._id)
|
||||
return { token, username: res.username, authCode: res.authCode }
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
*
|
||||
* @async
|
||||
* @param {DocumentType} user - 用户查询结果, 已经挂载在 req.user
|
||||
* @param {Partial} data - 部分修改数据
|
||||
*/
|
||||
async patchUserData(user: UserDocument, data: Partial<UserModel>) {
|
||||
const { password } = data
|
||||
const doc = { ...data }
|
||||
if (password !== undefined) {
|
||||
const { _id } = user
|
||||
const currentUser = await this.userModel
|
||||
.findById(_id)
|
||||
.select('+password +apiToken')
|
||||
// 1. 验证新旧密码是否一致
|
||||
const isSamePassword = compareSync(password, currentUser.password)
|
||||
if (isSamePassword) {
|
||||
throw new UnprocessableEntityException('密码可不能和原来的一样哦')
|
||||
}
|
||||
|
||||
// 2. 认证码重新生成
|
||||
const newCode = nanoid(10)
|
||||
doc.authCode = newCode
|
||||
}
|
||||
return await this.userModel
|
||||
.updateOne({ _id: user._id }, doc)
|
||||
.setOptions({ omitUndefined: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录登陆的足迹(ip, 时间)
|
||||
*
|
||||
* @async
|
||||
* @param {string} ip - string
|
||||
* @return {Promise<Record<string, Date|string>>} 返回上次足迹
|
||||
*/
|
||||
async recordFootstep(ip: string): Promise<Record<string, Date | string>> {
|
||||
const master = await this.userModel.findOne()
|
||||
const PrevFootstep = {
|
||||
lastLoginTime: master.lastLoginTime || new Date(1586090559569),
|
||||
lastLoginIp: master.lastLoginIp || null,
|
||||
}
|
||||
await master.updateOne({
|
||||
lastLoginTime: new Date(),
|
||||
lastLoginIp: ip,
|
||||
})
|
||||
// save to redis
|
||||
new Promise(async (resolve) => {
|
||||
// const redisClient = this.redisService.getClient(RedisNames.LoginRecord)
|
||||
// const dateFormat = dayjs().format('YYYY-MM-DD')
|
||||
// const value = JSON.parse(
|
||||
// (await redisClient.get(dateFormat)) || '[]',
|
||||
// ) as LoginRecord[]
|
||||
// const stringify = fastJson({
|
||||
// title: 'login-record schema',
|
||||
// type: 'array',
|
||||
// items: {
|
||||
// type: 'object',
|
||||
// properties: {
|
||||
// ip: { type: 'string' },
|
||||
// date: { type: 'string' },
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
// await redisClient.set(
|
||||
// dateFormat,
|
||||
// stringify(value.concat({ date: new Date().toISOString(), ip })),
|
||||
// )
|
||||
// resolve(null)
|
||||
})
|
||||
this.Logger.warn('主人已登录, IP: ' + ip)
|
||||
return PrevFootstep
|
||||
}
|
||||
}
|
||||
|
||||
17
src/processors/logger/logger.module.ts
Normal file
17
src/processors/logger/logger.module.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
import { Logger, LoggerService } from '@nestjs/common'
|
||||
|
||||
class LoggerModule implements LoggerService {
|
||||
logger: Logger
|
||||
constructor(context: string, options?: { timestamp?: boolean }) {
|
||||
this.logger = new Logger(context, options)
|
||||
}
|
||||
debug(...message: any[]) {
|
||||
// chalk.
|
||||
// this.logger.debug(message)
|
||||
}
|
||||
error(...message: any[]) {}
|
||||
log(...message: any[]) {}
|
||||
verbose(...message: any[]) {}
|
||||
warn(...message: any[]) {}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export const isDev = process.env.NODE_ENV == 'development'
|
||||
|
||||
export * from './ip'
|
||||
11
src/utils/index.util.ts
Normal file
11
src/utils/index.util.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export * from './ip.util'
|
||||
export const isDev = process.env.NODE_ENV == 'development'
|
||||
|
||||
export const md5 = (text: string) =>
|
||||
require('crypto').createHash('md5').update(text).digest('hex')
|
||||
export function getAvatar(mail: string) {
|
||||
if (!mail) {
|
||||
return ''
|
||||
}
|
||||
return `https://sdn.geekzu.org/avatar/${md5(mail)}`
|
||||
}
|
||||
@@ -24,3 +24,10 @@ export const getIp = (request: FastifyRequest | IncomingMessage) => {
|
||||
}
|
||||
return ip
|
||||
}
|
||||
|
||||
export const parseRelativeUrl = (path: string) => {
|
||||
if (!path || !path.startsWith('/')) {
|
||||
return new URL('http://a.com')
|
||||
}
|
||||
return new URL('http://a.com' + path)
|
||||
}
|
||||
Reference in New Issue
Block a user