代码评审 & 安全修复
后端:
- 创建 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,6 +2,7 @@ import { Controller, Get, Post, Body, Query, HttpException, HttpStatus, UseGuard
|
||||
import { InjectModel } from '@nestjs/mongoose'
|
||||
import { Model } from 'mongoose'
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'
|
||||
import { AdminGuard } from '../../common/guards/admin.guard'
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator'
|
||||
import { User, UserDocument } from '../user/user.schema'
|
||||
import { Interview, InterviewDocument } from '../interview/interview.schema'
|
||||
@@ -11,6 +12,7 @@ import { WechatPayService } from '../payment/wechat-pay.service'
|
||||
|
||||
const VIP_DURATION_DAYS = 30
|
||||
|
||||
@UseGuards(JwtAuthGuard, AdminGuard)
|
||||
@Controller('admin')
|
||||
export class AdminController {
|
||||
constructor(
|
||||
@@ -22,27 +24,18 @@ export class AdminController {
|
||||
) {}
|
||||
|
||||
@Get('check')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
async checkAdmin(@CurrentUser('userId') userId: string) {
|
||||
const user = await this.userModel.findById(userId).select('role').exec()
|
||||
return { isAdmin: user?.role === 'admin' }
|
||||
async checkAdmin(@CurrentUser('role') role: string) {
|
||||
return { isAdmin: role === 'admin' }
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post('verify')
|
||||
async verify(@CurrentUser('userId') userId: string) {
|
||||
const user = await this.userModel.findById(userId).exec()
|
||||
if (!user || user.role !== 'admin') {
|
||||
throw new HttpException('无权限访问', HttpStatus.FORBIDDEN)
|
||||
}
|
||||
return { ok: true, nickname: user.nickname || '管理员' }
|
||||
const user = await this.userModel.findById(userId).select('nickname').exec()
|
||||
return { ok: true, nickname: user?.nickname || '管理员' }
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get('overview')
|
||||
async overview(@CurrentUser('userId') adminUserId: string) {
|
||||
const admin = await this.userModel.findById(adminUserId).exec()
|
||||
if (admin?.role !== 'admin') throw new HttpException('无权限', HttpStatus.FORBIDDEN)
|
||||
async overview() {
|
||||
const [userCount, interviewCount, todayUsers, todayInterviews] = await Promise.all([
|
||||
this.userModel.countDocuments().exec(),
|
||||
this.interviewModel.countDocuments().exec(),
|
||||
@@ -52,13 +45,11 @@ export class AdminController {
|
||||
return { userCount, interviewCount, todayUsers, todayInterviews }
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get('users')
|
||||
async getUsers(@Query('keyword') keyword: string, @Query('page') page = '1', @Query('limit') limit = '20', @CurrentUser('userId') adminUserId: string) {
|
||||
const admin = await this.userModel.findById(adminUserId).exec()
|
||||
if (admin?.role !== 'admin') throw new HttpException('无权限', HttpStatus.FORBIDDEN)
|
||||
async getUsers(@Query('keyword') keyword: string, @Query('page') page = '1', @Query('limit') limit = '20') {
|
||||
const filter: any = {}
|
||||
if (keyword) {
|
||||
if (keyword.length > 50) throw new HttpException('关键词过长', HttpStatus.BAD_REQUEST)
|
||||
const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
filter.$or = [
|
||||
{ phone: { $regex: escaped, $options: 'i' } },
|
||||
@@ -67,17 +58,14 @@ export class AdminController {
|
||||
}
|
||||
const skip = (Math.max(1, +page) - 1) * +limit
|
||||
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(),
|
||||
])
|
||||
return { users, total, page: +page }
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get('interviews')
|
||||
async getInterviews(@Query('page') page = '1', @Query('limit') limit = '20', @CurrentUser('userId') adminUserId: string) {
|
||||
const admin = await this.userModel.findById(adminUserId).exec()
|
||||
if (admin?.role !== 'admin') throw new HttpException('无权限', HttpStatus.FORBIDDEN)
|
||||
async getInterviews(@Query('page') page = '1', @Query('limit') limit = '20') {
|
||||
const skip = (Math.max(1, +page) - 1) * +limit
|
||||
const [interviews, total] = await Promise.all([
|
||||
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 }
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post('set-vip')
|
||||
async setVip(@Body('userId') targetUserId: string, @CurrentUser('userId') adminUserId: string) {
|
||||
const admin = await this.userModel.findById(adminUserId).exec()
|
||||
if (admin?.role !== 'admin') throw new HttpException('无权限', HttpStatus.FORBIDDEN)
|
||||
async setVip(@Body('userId') targetUserId: string) {
|
||||
const user = await this.userModel.findById(targetUserId).exec()
|
||||
if (!user) throw new HttpException('用户不存在', HttpStatus.NOT_FOUND)
|
||||
const expireAt = new Date()
|
||||
@@ -102,20 +87,14 @@ export class AdminController {
|
||||
return { success: true, plan: 'growth', expireAt }
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get('admins')
|
||||
async getAdmins(@CurrentUser('userId') adminUserId: string) {
|
||||
const admin = await this.userModel.findById(adminUserId).exec()
|
||||
if (admin?.role !== 'admin') throw new HttpException('无权限', HttpStatus.FORBIDDEN)
|
||||
async getAdmins() {
|
||||
const admins = await this.userModel.find({ role: 'admin' }).select('phone nickname email createdAt isSystemAdmin').lean().exec()
|
||||
return { admins }
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post('set-admin')
|
||||
async setAdmin(@Body('userId') targetUserId: string, @CurrentUser('userId') adminUserId: string) {
|
||||
const admin = await this.userModel.findById(adminUserId).exec()
|
||||
if (admin?.role !== 'admin') throw new HttpException('无权限', HttpStatus.FORBIDDEN)
|
||||
async setAdmin(@Body('userId') targetUserId: string) {
|
||||
const user = await this.userModel.findById(targetUserId).exec()
|
||||
if (!user) throw new HttpException('用户不存在', HttpStatus.NOT_FOUND)
|
||||
if (user.role === 'admin') throw new HttpException('该用户已是管理员', HttpStatus.BAD_REQUEST)
|
||||
@@ -124,13 +103,8 @@ export class AdminController {
|
||||
return { success: true, message: '已设为管理员' }
|
||||
}
|
||||
|
||||
// ─── 订单管理 ──────────────
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get('orders')
|
||||
async getOrders(@Query('page') page = '1', @Query('limit') limit = '20', @Query('status') status: string, @CurrentUser('userId') adminUserId: string) {
|
||||
const admin = await this.userModel.findById(adminUserId).exec()
|
||||
if (admin?.role !== 'admin') throw new HttpException('无权限', HttpStatus.FORBIDDEN)
|
||||
async getOrders(@Query('page') page = '1', @Query('limit') limit = '20', @Query('status') status: string) {
|
||||
const filter: any = {}
|
||||
if (status) filter.status = status
|
||||
const skip = (Math.max(1, +page) - 1) * +limit
|
||||
@@ -141,11 +115,8 @@ export class AdminController {
|
||||
return { orders, total, page: +page }
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post('order/sync')
|
||||
async syncOrder(@Body('outTradeNo') outTradeNo: string, @CurrentUser('userId') adminUserId: string) {
|
||||
const admin = await this.userModel.findById(adminUserId).exec()
|
||||
if (admin?.role !== 'admin') throw new HttpException('无权限', HttpStatus.FORBIDDEN)
|
||||
async syncOrder(@Body('outTradeNo') outTradeNo: string) {
|
||||
const wxResult = await this.wechatPay.queryOrder(outTradeNo)
|
||||
const tradeState = wxResult?.trade_state
|
||||
const order = await this.orderModel.findOne({ outTradeNo }).exec()
|
||||
@@ -168,24 +139,15 @@ export class AdminController {
|
||||
return { order, wxResult }
|
||||
}
|
||||
|
||||
// ─── 系统配置(支持管理后台编辑) ──────
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get('config')
|
||||
async getConfig(@CurrentUser('userId') adminUserId: string) {
|
||||
const admin = await this.userModel.findById(adminUserId).exec()
|
||||
if (admin?.role !== 'admin') throw new HttpException('无权限', HttpStatus.FORBIDDEN)
|
||||
// 从数据库读,不存在则返回默认值
|
||||
async getConfig() {
|
||||
const cfg = await this.configModel.findOne({ key: 'site_config' }).exec()
|
||||
if (cfg) return cfg.value
|
||||
return DEFAULT_CONFIG
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post('config/save')
|
||||
async saveConfig(@Body() body: any, @CurrentUser('userId') adminUserId: string) {
|
||||
const admin = await this.userModel.findById(adminUserId).exec()
|
||||
if (admin?.role !== 'admin') throw new HttpException('无权限', HttpStatus.FORBIDDEN)
|
||||
async saveConfig(@Body() body: any) {
|
||||
await this.configModel.findOneAndUpdate(
|
||||
{ key: 'site_config' },
|
||||
{ key: 'site_config', value: body, description: '站点配置' },
|
||||
@@ -194,22 +156,14 @@ export class AdminController {
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
// ─── 每日一题管理 ──────────────
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get('questions')
|
||||
async getQuestions(@CurrentUser('userId') adminUserId: string) {
|
||||
const admin = await this.userModel.findById(adminUserId).exec()
|
||||
if (admin?.role !== 'admin') throw new HttpException('无权限', HttpStatus.FORBIDDEN)
|
||||
async getQuestions() {
|
||||
const cfg = await this.configModel.findOne({ key: 'daily_questions' }).exec()
|
||||
return cfg?.value || DEFAULT_QUESTIONS
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post('questions/save')
|
||||
async saveQuestions(@Body() body: any, @CurrentUser('userId') adminUserId: string) {
|
||||
const admin = await this.userModel.findById(adminUserId).exec()
|
||||
if (admin?.role !== 'admin') throw new HttpException('无权限', HttpStatus.FORBIDDEN)
|
||||
async saveQuestions(@Body() body: any) {
|
||||
await this.configModel.findOneAndUpdate(
|
||||
{ key: 'daily_questions' },
|
||||
{ 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 { PaymentOrder, PaymentOrderSchema } from '../payment/payment-order.schema'
|
||||
import { WechatPayService } from '../payment/wechat-pay.service'
|
||||
|
||||
import { AdminGuard } from '../../common/guards/admin.guard'
|
||||
import { SiteConfig, SiteConfigSchema } from '../schemas/site-config.schema'
|
||||
|
||||
@Module({
|
||||
@@ -18,6 +18,6 @@ import { SiteConfig, SiteConfigSchema } from '../schemas/site-config.schema'
|
||||
]),
|
||||
],
|
||||
controllers: [AdminController],
|
||||
providers: [WechatPayService],
|
||||
providers: [WechatPayService, AdminGuard],
|
||||
})
|
||||
export class AdminModule {}
|
||||
|
||||
Reference in New Issue
Block a user