import { Injectable, UnauthorizedException, BadRequestException, Logger, InternalServerErrorException, } from '@nestjs/common'; import { StaffService } from '../models/staff/staff.service'; import { db, AuthSchema, JwtPayload, } from '@nicestack/common'; import * as argon2 from 'argon2'; import { JwtService } from '@nestjs/jwt'; import { redis } from '@server/utils/redis/redis.service'; import { extractTokenFromAuthorization, UserProfileService } from './utils'; import { SessionInfo, SessionService } from './session.service'; import { tokenConfig } from './config'; import { z } from 'zod'; import { FileAuthResult, FileRequest, FileValidationErrorType } from './types'; import { extractFilePathFromUri } from '@server/utils/file'; @Injectable() export class AuthService { private logger = new Logger(AuthService.name) constructor( private readonly staffService: StaffService, private readonly jwtService: JwtService, private readonly sessionService: SessionService, ) { } async validateFileRequest(params: FileRequest): Promise { try { // 基础参数验证 if (!params?.originalUri) { return { isValid: false, error: FileValidationErrorType.INVALID_URI }; } const fileId = extractFilePathFromUri(params.originalUri); const resource = await db.resource.findFirst({ where: { fileId } }); // 资源验证 if (!resource) { return { isValid: false, error: FileValidationErrorType.RESOURCE_NOT_FOUND }; } // 处理公开资源 if (resource.isPublic) { return { isValid: true, resourceType: resource.type || 'unknown' }; } // 处理私有资源 const token = extractTokenFromAuthorization(params.authorization); if (!token) { return { isValid: false, error: FileValidationErrorType.AUTHORIZATION_REQUIRED }; } const payload: JwtPayload = await this.jwtService.verify(token) if (!payload.sub) { return { isValid: false, error: FileValidationErrorType.INVALID_TOKEN }; } return { isValid: true, userId: payload.sub, resourceType: resource.type || 'unknown' }; } catch (error) { this.logger.error('File validation error:', error); return { isValid: false, error: FileValidationErrorType.UNKNOWN_ERROR }; } } private async generateTokens(payload: JwtPayload): Promise<{ accessToken: string; refreshToken: string; }> { const [accessToken, refreshToken] = await Promise.all([ this.jwtService.signAsync(payload, { expiresIn: `${tokenConfig.accessToken.expirationMs / 1000}s`, }), this.jwtService.signAsync( { sub: payload.sub }, { expiresIn: `${tokenConfig.refreshToken.expirationMs / 1000}s` }, ), ]); return { accessToken, refreshToken }; } async signIn(data: z.infer): Promise { const { username, password, phoneNumber } = data; let staff = await db.staff.findFirst({ where: { OR: [{ username }, { phoneNumber }], deletedAt: null }, }); if (!staff && phoneNumber) { staff = await this.signUp({ showname: '新用户', username: phoneNumber, phoneNumber, password: phoneNumber, }); } else if (!staff) { throw new UnauthorizedException('帐号不存在'); } if (!staff.enabled) { throw new UnauthorizedException('帐号已禁用'); } const isPasswordMatch = phoneNumber || await argon2.verify(staff.password, password); if (!isPasswordMatch) { throw new UnauthorizedException('帐号或密码错误'); } try { const payload = { sub: staff.id, username: staff.username }; const { accessToken, refreshToken } = await this.generateTokens(payload); return await this.sessionService.createSession( staff.id, accessToken, refreshToken, { accessTokenExpirationMs: tokenConfig.accessToken.expirationMs, refreshTokenExpirationMs: tokenConfig.refreshToken.expirationMs, sessionTTL: tokenConfig.accessToken.expirationTTL, }, ); } catch (error) { this.logger.error(error); throw new InternalServerErrorException('创建会话失败'); } } async signUp(data: z.infer) { const { username, phoneNumber, officerId } = data; const existingUser = await db.staff.findFirst({ where: { OR: [{ username }, { officerId }, { phoneNumber }], deletedAt: null }, }); if (existingUser) { throw new BadRequestException('帐号或证件号已存在'); } return this.staffService.create({ data: { ...data, domainId: data.deptId, } }); } async refreshToken(data: z.infer) { const { refreshToken, sessionId } = data; let payload: JwtPayload; try { payload = this.jwtService.verify(refreshToken); } catch { throw new UnauthorizedException('用户会话已过期'); } const session = await this.sessionService.getSession(payload.sub, sessionId); if (!session || session.refresh_token !== refreshToken) { throw new UnauthorizedException('用户会话已过期'); } const user = await db.staff.findUnique({ where: { id: payload.sub, deletedAt: null } }); if (!user) { throw new UnauthorizedException('用户不存在'); } const { accessToken } = await this.generateTokens({ sub: user.id, username: user.username, }); const updatedSession = { ...session, access_token: accessToken, access_token_expires_at: Date.now() + tokenConfig.accessToken.expirationMs, }; await this.sessionService.saveSession( payload.sub, updatedSession, tokenConfig.accessToken.expirationTTL, ); await redis.del(UserProfileService.instance.getProfileCacheKey(payload.sub)); return { access_token: accessToken, access_token_expires_at: updatedSession.access_token_expires_at, }; } async changePassword(data: z.infer) { const { newPassword, phoneNumber, username } = data; const user = await db.staff.findFirst({ where: { OR: [{ username }, { phoneNumber }], deletedAt: null }, }); if (!user) { throw new UnauthorizedException('用户不存在'); } await this.staffService.update({ where: { id: user?.id }, data: { password: newPassword, } }); return { message: '密码已修改' }; } async logout(data: z.infer) { const { refreshToken, sessionId } = data; try { const payload = this.jwtService.verify(refreshToken); await Promise.all([ this.sessionService.deleteSession(payload.sub, sessionId), redis.del(UserProfileService.instance.getProfileCacheKey(payload.sub)), ]); } catch { throw new UnauthorizedException('无效的会话'); } return { message: '注销成功' }; } }