代码评审 & 安全修复
后端:
- 创建 AdminGuard 替代 12 处手动 role 查库检查,统一用 JWT payload 中的 role
- 密码字段 select: false,所有需密码的查询显式 select('+password')
- 文件上传接口移除 @Public(),需 JWT 认证
- 管理员搜索关键词限长 50 字符防 ReDoS
- CORS 收窄,不再对非生产环境放行所有源
- postbuild 复制 certs 路径同步到 dist/src/certs
- package.json main/start:prod 路径更新为 dist/src/main
前端:
- resume.vue 文件上传补充 Authorization header
- login.vue 移除含用户邮箱的 console.log 日志
This commit is contained in:
@@ -2,13 +2,13 @@
|
|||||||
"name": "zhiyin-backend",
|
"name": "zhiyin-backend",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "职引 - AI简历优化后端服务",
|
"description": "职引 - AI简历优化后端服务",
|
||||||
"main": "dist/main.js",
|
"main": "dist/src/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "nest start",
|
"start": "nest start",
|
||||||
"start:dev": "nest start --watch",
|
"start:dev": "nest start --watch",
|
||||||
"start:prod": "node dist/main",
|
"start:prod": "node dist/src/main",
|
||||||
"build": "nest build",
|
"build": "nest build",
|
||||||
"postbuild": "node -e \"const fs=require('fs');if(fs.existsSync('certs')){fs.cpSync('certs','dist/certs',{recursive:true})}\"",
|
"postbuild": "node -e \"const fs=require('fs');if(fs.existsSync('certs')){fs.cpSync('certs','dist/src/certs',{recursive:true})}\"",
|
||||||
"test": "jest --forceExit --detectOpenHandles",
|
"test": "jest --forceExit --detectOpenHandles",
|
||||||
"test:watch": "jest --watch --forceExit",
|
"test:watch": "jest --watch --forceExit",
|
||||||
"test:cov": "jest --coverage --forceExit",
|
"test:cov": "jest --coverage --forceExit",
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { Injectable, CanActivate, ExecutionContext, HttpException, HttpStatus } from '@nestjs/common'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AdminGuard implements CanActivate {
|
||||||
|
canActivate(context: ExecutionContext): boolean {
|
||||||
|
const request = context.switchToHttp().getRequest()
|
||||||
|
const user = request.user
|
||||||
|
if (!user) {
|
||||||
|
throw new HttpException('未登录', HttpStatus.UNAUTHORIZED)
|
||||||
|
}
|
||||||
|
if (user.role !== 'admin') {
|
||||||
|
throw new HttpException('无权限访问', HttpStatus.FORBIDDEN)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async validate(payload: { userId: string; phone: string }) {
|
async validate(payload: { userId: string; phone: string; role?: string }) {
|
||||||
return { userId: payload.userId, phone: payload.phone }
|
return { userId: payload.userId, phone: payload.phone, role: payload.role || 'user' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -42,10 +42,10 @@ async function bootstrap() {
|
|||||||
const app = await NestFactory.create(AppModule)
|
const app = await NestFactory.create(AppModule)
|
||||||
|
|
||||||
app.setGlobalPrefix('api')
|
app.setGlobalPrefix('api')
|
||||||
const allowedOrigins = process.env.CORS_ORIGINS?.split(',') || ['http://localhost:8085', 'http://localhost:3006']
|
const allowedOrigins = process.env.CORS_ORIGINS?.split(',') || ['http://localhost:8085', 'http://localhost:3006', 'https://zhiyin.yzrcloud.cn']
|
||||||
app.enableCors({
|
app.enableCors({
|
||||||
origin: (origin, callback) => {
|
origin: (origin, callback) => {
|
||||||
if (!origin || allowedOrigins.includes(origin) || process.env.NODE_ENV !== 'production') {
|
if (!origin || allowedOrigins.includes(origin)) {
|
||||||
callback(null, true)
|
callback(null, true)
|
||||||
} else {
|
} else {
|
||||||
callback(new Error('Not allowed by CORS'))
|
callback(new Error('Not allowed by CORS'))
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Controller, Get, Post, Body, Query, HttpException, HttpStatus, UseGuard
|
|||||||
import { InjectModel } from '@nestjs/mongoose'
|
import { InjectModel } from '@nestjs/mongoose'
|
||||||
import { Model } from 'mongoose'
|
import { Model } from 'mongoose'
|
||||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'
|
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'
|
||||||
|
import { AdminGuard } from '../../common/guards/admin.guard'
|
||||||
import { CurrentUser } from '../../common/decorators/current-user.decorator'
|
import { CurrentUser } from '../../common/decorators/current-user.decorator'
|
||||||
import { User, UserDocument } from '../user/user.schema'
|
import { User, UserDocument } from '../user/user.schema'
|
||||||
import { Interview, InterviewDocument } from '../interview/interview.schema'
|
import { Interview, InterviewDocument } from '../interview/interview.schema'
|
||||||
@@ -11,6 +12,7 @@ import { WechatPayService } from '../payment/wechat-pay.service'
|
|||||||
|
|
||||||
const VIP_DURATION_DAYS = 30
|
const VIP_DURATION_DAYS = 30
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard, AdminGuard)
|
||||||
@Controller('admin')
|
@Controller('admin')
|
||||||
export class AdminController {
|
export class AdminController {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -22,27 +24,18 @@ export class AdminController {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get('check')
|
@Get('check')
|
||||||
@UseGuards(JwtAuthGuard)
|
async checkAdmin(@CurrentUser('role') role: string) {
|
||||||
async checkAdmin(@CurrentUser('userId') userId: string) {
|
return { isAdmin: role === 'admin' }
|
||||||
const user = await this.userModel.findById(userId).select('role').exec()
|
|
||||||
return { isAdmin: user?.role === 'admin' }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@Post('verify')
|
@Post('verify')
|
||||||
async verify(@CurrentUser('userId') userId: string) {
|
async verify(@CurrentUser('userId') userId: string) {
|
||||||
const user = await this.userModel.findById(userId).exec()
|
const user = await this.userModel.findById(userId).select('nickname').exec()
|
||||||
if (!user || user.role !== 'admin') {
|
return { ok: true, nickname: user?.nickname || '管理员' }
|
||||||
throw new HttpException('无权限访问', HttpStatus.FORBIDDEN)
|
|
||||||
}
|
|
||||||
return { ok: true, nickname: user.nickname || '管理员' }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@Get('overview')
|
@Get('overview')
|
||||||
async overview(@CurrentUser('userId') adminUserId: string) {
|
async overview() {
|
||||||
const admin = await this.userModel.findById(adminUserId).exec()
|
|
||||||
if (admin?.role !== 'admin') throw new HttpException('无权限', HttpStatus.FORBIDDEN)
|
|
||||||
const [userCount, interviewCount, todayUsers, todayInterviews] = await Promise.all([
|
const [userCount, interviewCount, todayUsers, todayInterviews] = await Promise.all([
|
||||||
this.userModel.countDocuments().exec(),
|
this.userModel.countDocuments().exec(),
|
||||||
this.interviewModel.countDocuments().exec(),
|
this.interviewModel.countDocuments().exec(),
|
||||||
@@ -52,13 +45,11 @@ export class AdminController {
|
|||||||
return { userCount, interviewCount, todayUsers, todayInterviews }
|
return { userCount, interviewCount, todayUsers, todayInterviews }
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@Get('users')
|
@Get('users')
|
||||||
async getUsers(@Query('keyword') keyword: string, @Query('page') page = '1', @Query('limit') limit = '20', @CurrentUser('userId') adminUserId: string) {
|
async getUsers(@Query('keyword') keyword: string, @Query('page') page = '1', @Query('limit') limit = '20') {
|
||||||
const admin = await this.userModel.findById(adminUserId).exec()
|
|
||||||
if (admin?.role !== 'admin') throw new HttpException('无权限', HttpStatus.FORBIDDEN)
|
|
||||||
const filter: any = {}
|
const filter: any = {}
|
||||||
if (keyword) {
|
if (keyword) {
|
||||||
|
if (keyword.length > 50) throw new HttpException('关键词过长', HttpStatus.BAD_REQUEST)
|
||||||
const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||||
filter.$or = [
|
filter.$or = [
|
||||||
{ phone: { $regex: escaped, $options: 'i' } },
|
{ phone: { $regex: escaped, $options: 'i' } },
|
||||||
@@ -67,17 +58,14 @@ export class AdminController {
|
|||||||
}
|
}
|
||||||
const skip = (Math.max(1, +page) - 1) * +limit
|
const skip = (Math.max(1, +page) - 1) * +limit
|
||||||
const [users, total] = await Promise.all([
|
const [users, total] = await Promise.all([
|
||||||
this.userModel.find(filter).sort({ createdAt: -1 }).skip(skip).limit(+limit).select('-password').lean().exec(),
|
this.userModel.find(filter).sort({ createdAt: -1 }).skip(skip).limit(+limit).lean().exec(),
|
||||||
this.userModel.countDocuments(filter).exec(),
|
this.userModel.countDocuments(filter).exec(),
|
||||||
])
|
])
|
||||||
return { users, total, page: +page }
|
return { users, total, page: +page }
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@Get('interviews')
|
@Get('interviews')
|
||||||
async getInterviews(@Query('page') page = '1', @Query('limit') limit = '20', @CurrentUser('userId') adminUserId: string) {
|
async getInterviews(@Query('page') page = '1', @Query('limit') limit = '20') {
|
||||||
const admin = await this.userModel.findById(adminUserId).exec()
|
|
||||||
if (admin?.role !== 'admin') throw new HttpException('无权限', HttpStatus.FORBIDDEN)
|
|
||||||
const skip = (Math.max(1, +page) - 1) * +limit
|
const skip = (Math.max(1, +page) - 1) * +limit
|
||||||
const [interviews, total] = await Promise.all([
|
const [interviews, total] = await Promise.all([
|
||||||
this.interviewModel.find().sort({ createdAt: -1 }).skip(skip).limit(+limit).populate('userId', 'phone nickname').lean().exec(),
|
this.interviewModel.find().sort({ createdAt: -1 }).skip(skip).limit(+limit).populate('userId', 'phone nickname').lean().exec(),
|
||||||
@@ -86,11 +74,8 @@ export class AdminController {
|
|||||||
return { interviews, total, page: +page }
|
return { interviews, total, page: +page }
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@Post('set-vip')
|
@Post('set-vip')
|
||||||
async setVip(@Body('userId') targetUserId: string, @CurrentUser('userId') adminUserId: string) {
|
async setVip(@Body('userId') targetUserId: string) {
|
||||||
const admin = await this.userModel.findById(adminUserId).exec()
|
|
||||||
if (admin?.role !== 'admin') throw new HttpException('无权限', HttpStatus.FORBIDDEN)
|
|
||||||
const user = await this.userModel.findById(targetUserId).exec()
|
const user = await this.userModel.findById(targetUserId).exec()
|
||||||
if (!user) throw new HttpException('用户不存在', HttpStatus.NOT_FOUND)
|
if (!user) throw new HttpException('用户不存在', HttpStatus.NOT_FOUND)
|
||||||
const expireAt = new Date()
|
const expireAt = new Date()
|
||||||
@@ -102,20 +87,14 @@ export class AdminController {
|
|||||||
return { success: true, plan: 'growth', expireAt }
|
return { success: true, plan: 'growth', expireAt }
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@Get('admins')
|
@Get('admins')
|
||||||
async getAdmins(@CurrentUser('userId') adminUserId: string) {
|
async getAdmins() {
|
||||||
const admin = await this.userModel.findById(adminUserId).exec()
|
|
||||||
if (admin?.role !== 'admin') throw new HttpException('无权限', HttpStatus.FORBIDDEN)
|
|
||||||
const admins = await this.userModel.find({ role: 'admin' }).select('phone nickname email createdAt isSystemAdmin').lean().exec()
|
const admins = await this.userModel.find({ role: 'admin' }).select('phone nickname email createdAt isSystemAdmin').lean().exec()
|
||||||
return { admins }
|
return { admins }
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@Post('set-admin')
|
@Post('set-admin')
|
||||||
async setAdmin(@Body('userId') targetUserId: string, @CurrentUser('userId') adminUserId: string) {
|
async setAdmin(@Body('userId') targetUserId: string) {
|
||||||
const admin = await this.userModel.findById(adminUserId).exec()
|
|
||||||
if (admin?.role !== 'admin') throw new HttpException('无权限', HttpStatus.FORBIDDEN)
|
|
||||||
const user = await this.userModel.findById(targetUserId).exec()
|
const user = await this.userModel.findById(targetUserId).exec()
|
||||||
if (!user) throw new HttpException('用户不存在', HttpStatus.NOT_FOUND)
|
if (!user) throw new HttpException('用户不存在', HttpStatus.NOT_FOUND)
|
||||||
if (user.role === 'admin') throw new HttpException('该用户已是管理员', HttpStatus.BAD_REQUEST)
|
if (user.role === 'admin') throw new HttpException('该用户已是管理员', HttpStatus.BAD_REQUEST)
|
||||||
@@ -124,13 +103,8 @@ export class AdminController {
|
|||||||
return { success: true, message: '已设为管理员' }
|
return { success: true, message: '已设为管理员' }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 订单管理 ──────────────
|
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@Get('orders')
|
@Get('orders')
|
||||||
async getOrders(@Query('page') page = '1', @Query('limit') limit = '20', @Query('status') status: string, @CurrentUser('userId') adminUserId: string) {
|
async getOrders(@Query('page') page = '1', @Query('limit') limit = '20', @Query('status') status: string) {
|
||||||
const admin = await this.userModel.findById(adminUserId).exec()
|
|
||||||
if (admin?.role !== 'admin') throw new HttpException('无权限', HttpStatus.FORBIDDEN)
|
|
||||||
const filter: any = {}
|
const filter: any = {}
|
||||||
if (status) filter.status = status
|
if (status) filter.status = status
|
||||||
const skip = (Math.max(1, +page) - 1) * +limit
|
const skip = (Math.max(1, +page) - 1) * +limit
|
||||||
@@ -141,11 +115,8 @@ export class AdminController {
|
|||||||
return { orders, total, page: +page }
|
return { orders, total, page: +page }
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@Post('order/sync')
|
@Post('order/sync')
|
||||||
async syncOrder(@Body('outTradeNo') outTradeNo: string, @CurrentUser('userId') adminUserId: string) {
|
async syncOrder(@Body('outTradeNo') outTradeNo: string) {
|
||||||
const admin = await this.userModel.findById(adminUserId).exec()
|
|
||||||
if (admin?.role !== 'admin') throw new HttpException('无权限', HttpStatus.FORBIDDEN)
|
|
||||||
const wxResult = await this.wechatPay.queryOrder(outTradeNo)
|
const wxResult = await this.wechatPay.queryOrder(outTradeNo)
|
||||||
const tradeState = wxResult?.trade_state
|
const tradeState = wxResult?.trade_state
|
||||||
const order = await this.orderModel.findOne({ outTradeNo }).exec()
|
const order = await this.orderModel.findOne({ outTradeNo }).exec()
|
||||||
@@ -168,24 +139,15 @@ export class AdminController {
|
|||||||
return { order, wxResult }
|
return { order, wxResult }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 系统配置(支持管理后台编辑) ──────
|
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@Get('config')
|
@Get('config')
|
||||||
async getConfig(@CurrentUser('userId') adminUserId: string) {
|
async getConfig() {
|
||||||
const admin = await this.userModel.findById(adminUserId).exec()
|
|
||||||
if (admin?.role !== 'admin') throw new HttpException('无权限', HttpStatus.FORBIDDEN)
|
|
||||||
// 从数据库读,不存在则返回默认值
|
|
||||||
const cfg = await this.configModel.findOne({ key: 'site_config' }).exec()
|
const cfg = await this.configModel.findOne({ key: 'site_config' }).exec()
|
||||||
if (cfg) return cfg.value
|
if (cfg) return cfg.value
|
||||||
return DEFAULT_CONFIG
|
return DEFAULT_CONFIG
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@Post('config/save')
|
@Post('config/save')
|
||||||
async saveConfig(@Body() body: any, @CurrentUser('userId') adminUserId: string) {
|
async saveConfig(@Body() body: any) {
|
||||||
const admin = await this.userModel.findById(adminUserId).exec()
|
|
||||||
if (admin?.role !== 'admin') throw new HttpException('无权限', HttpStatus.FORBIDDEN)
|
|
||||||
await this.configModel.findOneAndUpdate(
|
await this.configModel.findOneAndUpdate(
|
||||||
{ key: 'site_config' },
|
{ key: 'site_config' },
|
||||||
{ key: 'site_config', value: body, description: '站点配置' },
|
{ key: 'site_config', value: body, description: '站点配置' },
|
||||||
@@ -194,22 +156,14 @@ export class AdminController {
|
|||||||
return { success: true }
|
return { success: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 每日一题管理 ──────────────
|
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@Get('questions')
|
@Get('questions')
|
||||||
async getQuestions(@CurrentUser('userId') adminUserId: string) {
|
async getQuestions() {
|
||||||
const admin = await this.userModel.findById(adminUserId).exec()
|
|
||||||
if (admin?.role !== 'admin') throw new HttpException('无权限', HttpStatus.FORBIDDEN)
|
|
||||||
const cfg = await this.configModel.findOne({ key: 'daily_questions' }).exec()
|
const cfg = await this.configModel.findOne({ key: 'daily_questions' }).exec()
|
||||||
return cfg?.value || DEFAULT_QUESTIONS
|
return cfg?.value || DEFAULT_QUESTIONS
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@Post('questions/save')
|
@Post('questions/save')
|
||||||
async saveQuestions(@Body() body: any, @CurrentUser('userId') adminUserId: string) {
|
async saveQuestions(@Body() body: any) {
|
||||||
const admin = await this.userModel.findById(adminUserId).exec()
|
|
||||||
if (admin?.role !== 'admin') throw new HttpException('无权限', HttpStatus.FORBIDDEN)
|
|
||||||
await this.configModel.findOneAndUpdate(
|
await this.configModel.findOneAndUpdate(
|
||||||
{ key: 'daily_questions' },
|
{ key: 'daily_questions' },
|
||||||
{ key: 'daily_questions', value: body, description: '每日一题题库' },
|
{ key: 'daily_questions', value: body, description: '每日一题题库' },
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { User, UserSchema } from '../user/user.schema'
|
|||||||
import { Interview, InterviewSchema } from '../interview/interview.schema'
|
import { Interview, InterviewSchema } from '../interview/interview.schema'
|
||||||
import { PaymentOrder, PaymentOrderSchema } from '../payment/payment-order.schema'
|
import { PaymentOrder, PaymentOrderSchema } from '../payment/payment-order.schema'
|
||||||
import { WechatPayService } from '../payment/wechat-pay.service'
|
import { WechatPayService } from '../payment/wechat-pay.service'
|
||||||
|
import { AdminGuard } from '../../common/guards/admin.guard'
|
||||||
import { SiteConfig, SiteConfigSchema } from '../schemas/site-config.schema'
|
import { SiteConfig, SiteConfigSchema } from '../schemas/site-config.schema'
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@@ -18,6 +18,6 @@ import { SiteConfig, SiteConfigSchema } from '../schemas/site-config.schema'
|
|||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
controllers: [AdminController],
|
controllers: [AdminController],
|
||||||
providers: [WechatPayService],
|
providers: [WechatPayService, AdminGuard],
|
||||||
})
|
})
|
||||||
export class AdminModule {}
|
export class AdminModule {}
|
||||||
|
|||||||
@@ -2,14 +2,12 @@ import { Controller, Post, UseInterceptors, UploadedFile, HttpException, HttpSta
|
|||||||
import { FileInterceptor } from '@nestjs/platform-express'
|
import { FileInterceptor } from '@nestjs/platform-express'
|
||||||
import * as mammoth from 'mammoth'
|
import * as mammoth from 'mammoth'
|
||||||
import { memoryStorage } from 'multer'
|
import { memoryStorage } from 'multer'
|
||||||
import { Public } from '../../common/decorators/public.decorator'
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const pdfParse = require('pdf-parse')
|
const pdfParse = require('pdf-parse')
|
||||||
|
|
||||||
@Controller('upload')
|
@Controller('upload')
|
||||||
export class UploadController {
|
export class UploadController {
|
||||||
@Public()
|
|
||||||
@Post()
|
@Post()
|
||||||
@UseInterceptors(FileInterceptor('file', {
|
@UseInterceptors(FileInterceptor('file', {
|
||||||
storage: memoryStorage(),
|
storage: memoryStorage(),
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export class User {
|
|||||||
@Prop({ sparse: true })
|
@Prop({ sparse: true })
|
||||||
email?: string
|
email?: string
|
||||||
|
|
||||||
@Prop({ default: '' })
|
@Prop({ default: '', select: false })
|
||||||
password?: string
|
password?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,12 +26,12 @@ describe('UserService', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
const chainable = (value: any) => ({ exec: jest.fn().mockResolvedValue(value), select: jest.fn().mockReturnThis() })
|
||||||
mockUserModel = {
|
mockUserModel = {
|
||||||
findOne: jest.fn().mockReturnThis(),
|
findOne: jest.fn().mockReturnValue(chainable(null)),
|
||||||
findById: jest.fn().mockReturnThis(),
|
findById: jest.fn().mockReturnValue(chainable(null)),
|
||||||
findByIdAndUpdate: jest.fn().mockReturnThis(),
|
findByIdAndUpdate: jest.fn().mockReturnValue(chainable(null)),
|
||||||
create: jest.fn().mockResolvedValue(mockUser),
|
create: jest.fn().mockResolvedValue(mockUser),
|
||||||
exec: jest.fn().mockResolvedValue(null),
|
|
||||||
}
|
}
|
||||||
mockJwtService = {
|
mockJwtService = {
|
||||||
sign: jest.fn().mockReturnValue('mock-jwt-token'),
|
sign: jest.fn().mockReturnValue('mock-jwt-token'),
|
||||||
@@ -70,7 +70,7 @@ describe('UserService', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should create user on first login and return token', async () => {
|
it('should create user on first login and return token', async () => {
|
||||||
mockUserModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(null) })
|
mockUserModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(null), select: jest.fn().mockReturnThis() })
|
||||||
await service.sendCode('13800138000')
|
await service.sendCode('13800138000')
|
||||||
const result = await service.loginByPhone('13800138000', '123456')
|
const result = await service.loginByPhone('13800138000', '123456')
|
||||||
expect(result).toHaveProperty('token', 'mock-jwt-token')
|
expect(result).toHaveProperty('token', 'mock-jwt-token')
|
||||||
@@ -79,7 +79,7 @@ describe('UserService', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should login existing user', async () => {
|
it('should login existing user', async () => {
|
||||||
mockUserModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(mockUser) })
|
mockUserModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(mockUser), select: jest.fn().mockReturnThis() })
|
||||||
await service.sendCode('13800138000')
|
await service.sendCode('13800138000')
|
||||||
const result = await service.loginByPhone('13800138000', '123456')
|
const result = await service.loginByPhone('13800138000', '123456')
|
||||||
expect(result).toHaveProperty('token')
|
expect(result).toHaveProperty('token')
|
||||||
@@ -105,7 +105,7 @@ describe('UserService', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should login with valid email code', async () => {
|
it('should login with valid email code', async () => {
|
||||||
mockUserModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(mockUser) })
|
mockUserModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(mockUser), select: jest.fn().mockReturnThis() })
|
||||||
|
|
||||||
const spy = jest.spyOn(mockEmailService, 'sendVerificationCode')
|
const spy = jest.spyOn(mockEmailService, 'sendVerificationCode')
|
||||||
await service.sendEmailCode('test@example.com')
|
await service.sendEmailCode('test@example.com')
|
||||||
@@ -119,7 +119,7 @@ describe('UserService', () => {
|
|||||||
|
|
||||||
describe('loginByPassword', () => {
|
describe('loginByPassword', () => {
|
||||||
it('should throw for nonexistent user', async () => {
|
it('should throw for nonexistent user', async () => {
|
||||||
mockUserModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(null) })
|
mockUserModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(null), select: jest.fn().mockReturnThis() })
|
||||||
await expect(service.loginByPassword('test@example.com', 'pass'))
|
await expect(service.loginByPassword('test@example.com', 'pass'))
|
||||||
.rejects.toThrow(HttpException)
|
.rejects.toThrow(HttpException)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ export class UserService {
|
|||||||
emailCodeStore.delete(email)
|
emailCodeStore.delete(email)
|
||||||
|
|
||||||
// 按邮箱查找或创建用户
|
// 按邮箱查找或创建用户
|
||||||
let user = await this.userModel.findOne({ email }).exec()
|
let user = await this.userModel.findOne({ email }).select('+password').exec()
|
||||||
let isNew = false
|
let isNew = false
|
||||||
if (!user) {
|
if (!user) {
|
||||||
isNew = true
|
isNew = true
|
||||||
@@ -120,7 +120,7 @@ export class UserService {
|
|||||||
|
|
||||||
// 🔐 密码登录
|
// 🔐 密码登录
|
||||||
async loginByPassword(email: string, password: string) {
|
async loginByPassword(email: string, password: string) {
|
||||||
const user = await this.userModel.findOne({ email }).exec()
|
const user = await this.userModel.findOne({ email }).select('+password').exec()
|
||||||
if (!user) throw new HttpException('账号不存在', HttpStatus.NOT_FOUND)
|
if (!user) throw new HttpException('账号不存在', HttpStatus.NOT_FOUND)
|
||||||
if (!user.password) throw new HttpException('该账号未设置密码,请使用验证码登录', HttpStatus.UNAUTHORIZED)
|
if (!user.password) throw new HttpException('该账号未设置密码,请使用验证码登录', HttpStatus.UNAUTHORIZED)
|
||||||
const match = await bcrypt.compare(password, user.password)
|
const match = await bcrypt.compare(password, user.password)
|
||||||
@@ -136,7 +136,7 @@ export class UserService {
|
|||||||
if (!password || password.length < 6) {
|
if (!password || password.length < 6) {
|
||||||
throw new HttpException('密码至少6位', HttpStatus.BAD_REQUEST)
|
throw new HttpException('密码至少6位', HttpStatus.BAD_REQUEST)
|
||||||
}
|
}
|
||||||
const existing = await this.userModel.findOne({ email }).exec()
|
const existing = await this.userModel.findOne({ email }).select('+password').exec()
|
||||||
if (existing) {
|
if (existing) {
|
||||||
if (existing.password) {
|
if (existing.password) {
|
||||||
throw new HttpException('该邮箱已注册,请直接登录', HttpStatus.CONFLICT)
|
throw new HttpException('该邮箱已注册,请直接登录', HttpStatus.CONFLICT)
|
||||||
@@ -192,7 +192,7 @@ export class UserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private generateAuthResponse(user: UserDocument) {
|
private generateAuthResponse(user: UserDocument) {
|
||||||
const payload = { userId: user._id.toString(), phone: user.phone || '' }
|
const payload = { userId: user._id.toString(), phone: user.phone || '', role: user.role || 'user' }
|
||||||
return {
|
return {
|
||||||
token: this.jwtService.sign(payload),
|
token: this.jwtService.sign(payload),
|
||||||
user: this.safeUser(user),
|
user: this.safeUser(user),
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"rootDir": "src"
|
|
||||||
},
|
|
||||||
"include": ["src/**/*"],
|
|
||||||
"exclude": ["node_modules", "dist", "test", "**/*spec.ts", "**/*.spec.ts"]
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"incremental": true,
|
"incremental": false,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
|
|||||||
@@ -182,17 +182,14 @@ const sendEmailCode = () => {
|
|||||||
if (cooldown.value > 0) { showToast('请稍后再试'); return }
|
if (cooldown.value > 0) { showToast('请稍后再试'); return }
|
||||||
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||||
if (!re.test(email.value)) { showToast('请输入正确的邮箱'); return }
|
if (!re.test(email.value)) { showToast('请输入正确的邮箱'); return }
|
||||||
console.log('[sendEmailCode] 发送中,email:', email.value)
|
|
||||||
uni.request({
|
uni.request({
|
||||||
url: api('/user/send-email-code'),
|
url: api('/user/send-email-code'),
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
header: { 'Content-Type': 'application/json' },
|
header: { 'Content-Type': 'application/json' },
|
||||||
data: { email: email.value },
|
data: { email: email.value },
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
console.log('[sendEmailCode] success res:', JSON.stringify(res))
|
|
||||||
if (res.statusCode === 200) {
|
if (res.statusCode === 200) {
|
||||||
emailSent.value = true
|
emailSent.value = true
|
||||||
console.log('[sendEmailCode] emailSent 设为 true')
|
|
||||||
showToast('验证码已发送', 'success')
|
showToast('验证码已发送', 'success')
|
||||||
startCooldown()
|
startCooldown()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ const uploadMpFile = async (filePath, name) => {
|
|||||||
fileName.value = name
|
fileName.value = name
|
||||||
uploading.value = true
|
uploading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await uni.uploadFile({ url: api('/upload'), filePath, name: 'file' })
|
const res = await uni.uploadFile({ url: api('/upload'), filePath, name: 'file', header: { 'Authorization': `Bearer ${token()}` } })
|
||||||
const data = JSON.parse(res.data)
|
const data = JSON.parse(res.data)
|
||||||
if (res.statusCode === 200) {
|
if (res.statusCode === 200) {
|
||||||
resumeText.value = data.text
|
resumeText.value = data.text
|
||||||
@@ -271,7 +271,7 @@ const onFileSelected = async (e) => {
|
|||||||
try {
|
try {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', file)
|
formData.append('file', file)
|
||||||
const res = await fetch(api('/upload'), { method: 'POST', body: formData })
|
const res = await fetch(api('/upload'), { method: 'POST', headers: { 'Authorization': `Bearer ${token()}` }, body: formData })
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
resumeText.value = data.text
|
resumeText.value = data.text
|
||||||
|
|||||||
Reference in New Issue
Block a user