代码评审 & 安全修复
后端:
- 创建 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:
@@ -44,7 +44,7 @@ export class User {
|
||||
@Prop({ sparse: true })
|
||||
email?: string
|
||||
|
||||
@Prop({ default: '' })
|
||||
@Prop({ default: '', select: false })
|
||||
password?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -26,12 +26,12 @@ describe('UserService', () => {
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
const chainable = (value: any) => ({ exec: jest.fn().mockResolvedValue(value), select: jest.fn().mockReturnThis() })
|
||||
mockUserModel = {
|
||||
findOne: jest.fn().mockReturnThis(),
|
||||
findById: jest.fn().mockReturnThis(),
|
||||
findByIdAndUpdate: jest.fn().mockReturnThis(),
|
||||
findOne: jest.fn().mockReturnValue(chainable(null)),
|
||||
findById: jest.fn().mockReturnValue(chainable(null)),
|
||||
findByIdAndUpdate: jest.fn().mockReturnValue(chainable(null)),
|
||||
create: jest.fn().mockResolvedValue(mockUser),
|
||||
exec: jest.fn().mockResolvedValue(null),
|
||||
}
|
||||
mockJwtService = {
|
||||
sign: jest.fn().mockReturnValue('mock-jwt-token'),
|
||||
@@ -70,7 +70,7 @@ describe('UserService', () => {
|
||||
})
|
||||
|
||||
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')
|
||||
const result = await service.loginByPhone('13800138000', '123456')
|
||||
expect(result).toHaveProperty('token', 'mock-jwt-token')
|
||||
@@ -79,7 +79,7 @@ describe('UserService', () => {
|
||||
})
|
||||
|
||||
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')
|
||||
const result = await service.loginByPhone('13800138000', '123456')
|
||||
expect(result).toHaveProperty('token')
|
||||
@@ -105,7 +105,7 @@ describe('UserService', () => {
|
||||
})
|
||||
|
||||
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')
|
||||
await service.sendEmailCode('test@example.com')
|
||||
@@ -119,7 +119,7 @@ describe('UserService', () => {
|
||||
|
||||
describe('loginByPassword', () => {
|
||||
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'))
|
||||
.rejects.toThrow(HttpException)
|
||||
})
|
||||
|
||||
@@ -108,7 +108,7 @@ export class UserService {
|
||||
emailCodeStore.delete(email)
|
||||
|
||||
// 按邮箱查找或创建用户
|
||||
let user = await this.userModel.findOne({ email }).exec()
|
||||
let user = await this.userModel.findOne({ email }).select('+password').exec()
|
||||
let isNew = false
|
||||
if (!user) {
|
||||
isNew = true
|
||||
@@ -120,7 +120,7 @@ export class UserService {
|
||||
|
||||
// 🔐 密码登录
|
||||
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.password) throw new HttpException('该账号未设置密码,请使用验证码登录', HttpStatus.UNAUTHORIZED)
|
||||
const match = await bcrypt.compare(password, user.password)
|
||||
@@ -136,7 +136,7 @@ export class UserService {
|
||||
if (!password || password.length < 6) {
|
||||
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.password) {
|
||||
throw new HttpException('该邮箱已注册,请直接登录', HttpStatus.CONFLICT)
|
||||
@@ -192,7 +192,7 @@ export class UserService {
|
||||
}
|
||||
|
||||
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 {
|
||||
token: this.jwtService.sign(payload),
|
||||
user: this.safeUser(user),
|
||||
|
||||
Reference in New Issue
Block a user