2024-09-03 20:19:33 +08:00
|
|
|
import {
|
2024-09-09 18:48:07 +08:00
|
|
|
Injectable,
|
|
|
|
UnauthorizedException,
|
|
|
|
BadRequestException,
|
2024-12-30 08:26:40 +08:00
|
|
|
Logger,
|
|
|
|
InternalServerErrorException,
|
2025-01-03 09:24:46 +08:00
|
|
|
|
2024-09-03 20:19:33 +08:00
|
|
|
} from '@nestjs/common';
|
2024-12-30 08:26:40 +08:00
|
|
|
import { StaffService } from '../models/staff/staff.service';
|
|
|
|
import {
|
|
|
|
db,
|
|
|
|
AuthSchema,
|
|
|
|
JwtPayload,
|
|
|
|
} from '@nicestack/common';
|
|
|
|
import * as argon2 from 'argon2';
|
2024-09-03 20:19:33 +08:00
|
|
|
import { JwtService } from '@nestjs/jwt';
|
2024-12-30 08:26:40 +08:00
|
|
|
import { redis } from '@server/utils/redis/redis.service';
|
2025-01-03 09:24:46 +08:00
|
|
|
import { extractTokenFromAuthorization, UserProfileService } from './utils';
|
2024-12-30 08:26:40 +08:00
|
|
|
import { SessionInfo, SessionService } from './session.service';
|
|
|
|
import { tokenConfig } from './config';
|
|
|
|
import { z } from 'zod';
|
2025-01-03 09:24:46 +08:00
|
|
|
import { FileAuthResult, FileRequest, FileValidationErrorType } from './types';
|
|
|
|
import { extractFilePathFromUri } from '@server/utils/file';
|
2024-09-03 20:19:33 +08:00
|
|
|
@Injectable()
|
|
|
|
export class AuthService {
|
2024-12-30 08:26:40 +08:00
|
|
|
private logger = new Logger(AuthService.name)
|
2024-09-09 18:48:07 +08:00
|
|
|
constructor(
|
|
|
|
private readonly staffService: StaffService,
|
2024-12-30 08:26:40 +08:00
|
|
|
private readonly jwtService: JwtService,
|
|
|
|
private readonly sessionService: SessionService,
|
2024-09-09 18:48:07 +08:00
|
|
|
) { }
|
|
|
|
|
2025-01-03 09:24:46 +08:00
|
|
|
async validateFileRequest(params: FileRequest): Promise<FileAuthResult> {
|
|
|
|
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 };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-30 08:26:40 +08:00
|
|
|
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<typeof AuthSchema.signInRequset>): Promise<SessionInfo> {
|
2024-09-10 10:31:24 +08:00
|
|
|
const { username, password, phoneNumber } = data;
|
2024-12-30 08:26:40 +08:00
|
|
|
|
|
|
|
let staff = await db.staff.findFirst({
|
|
|
|
where: { OR: [{ username }, { phoneNumber }], deletedAt: null },
|
2024-09-10 10:31:24 +08:00
|
|
|
});
|
|
|
|
|
2024-12-30 08:26:40 +08:00
|
|
|
if (!staff && phoneNumber) {
|
|
|
|
staff = await this.signUp({
|
|
|
|
showname: '新用户',
|
|
|
|
username: phoneNumber,
|
|
|
|
phoneNumber,
|
|
|
|
password: phoneNumber,
|
|
|
|
});
|
|
|
|
} else if (!staff) {
|
|
|
|
throw new UnauthorizedException('帐号不存在');
|
2024-09-03 20:19:33 +08:00
|
|
|
}
|
2024-12-30 08:26:40 +08:00
|
|
|
if (!staff.enabled) {
|
|
|
|
throw new UnauthorizedException('帐号已禁用');
|
|
|
|
}
|
|
|
|
const isPasswordMatch = phoneNumber || await argon2.verify(staff.password, password);
|
2024-09-09 18:48:07 +08:00
|
|
|
if (!isPasswordMatch) {
|
2024-12-30 08:26:40 +08:00
|
|
|
throw new UnauthorizedException('帐号或密码错误');
|
2024-09-03 20:19:33 +08:00
|
|
|
}
|
|
|
|
|
2024-12-30 08:26:40 +08:00
|
|
|
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<typeof AuthSchema.signUpRequest>) {
|
|
|
|
const { username, phoneNumber, officerId } = data;
|
2024-09-09 18:48:07 +08:00
|
|
|
|
2024-12-30 08:26:40 +08:00
|
|
|
const existingUser = await db.staff.findFirst({
|
|
|
|
where: {
|
|
|
|
OR: [{ username }, { officerId }, { phoneNumber }],
|
|
|
|
deletedAt: null
|
2024-09-09 18:48:07 +08:00
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2024-12-30 08:26:40 +08:00
|
|
|
if (existingUser) {
|
|
|
|
throw new BadRequestException('帐号或证件号已存在');
|
|
|
|
}
|
2024-09-09 18:48:07 +08:00
|
|
|
|
2024-12-30 08:26:40 +08:00
|
|
|
return this.staffService.create({
|
|
|
|
data: {
|
|
|
|
...data,
|
|
|
|
domainId: data.deptId,
|
|
|
|
}
|
|
|
|
});
|
2024-09-09 18:48:07 +08:00
|
|
|
}
|
|
|
|
async refreshToken(data: z.infer<typeof AuthSchema.refreshTokenRequest>) {
|
2024-12-30 08:26:40 +08:00
|
|
|
const { refreshToken, sessionId } = data;
|
2024-09-09 18:48:07 +08:00
|
|
|
|
|
|
|
let payload: JwtPayload;
|
|
|
|
try {
|
|
|
|
payload = this.jwtService.verify(refreshToken);
|
2024-12-30 08:26:40 +08:00
|
|
|
} catch {
|
|
|
|
throw new UnauthorizedException('用户会话已过期');
|
2024-09-03 20:19:33 +08:00
|
|
|
}
|
|
|
|
|
2024-12-30 08:26:40 +08:00
|
|
|
const session = await this.sessionService.getSession(payload.sub, sessionId);
|
|
|
|
if (!session || session.refresh_token !== refreshToken) {
|
|
|
|
throw new UnauthorizedException('用户会话已过期');
|
2024-09-09 18:48:07 +08:00
|
|
|
}
|
|
|
|
|
2024-12-30 08:26:40 +08:00
|
|
|
const user = await db.staff.findUnique({ where: { id: payload.sub, deletedAt: null } });
|
2024-09-09 18:48:07 +08:00
|
|
|
if (!user) {
|
2024-12-30 08:26:40 +08:00
|
|
|
throw new UnauthorizedException('用户不存在');
|
2024-09-09 18:48:07 +08:00
|
|
|
}
|
|
|
|
|
2024-12-30 08:26:40 +08:00
|
|
|
const { accessToken } = await this.generateTokens({
|
|
|
|
sub: user.id,
|
|
|
|
username: user.username,
|
|
|
|
});
|
2024-09-03 20:19:33 +08:00
|
|
|
|
2024-12-30 08:26:40 +08:00
|
|
|
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));
|
2024-09-09 18:48:07 +08:00
|
|
|
return {
|
2024-12-30 08:26:40 +08:00
|
|
|
access_token: accessToken,
|
|
|
|
access_token_expires_at: updatedSession.access_token_expires_at,
|
2024-09-09 18:48:07 +08:00
|
|
|
};
|
|
|
|
}
|
2024-12-30 08:26:40 +08:00
|
|
|
async changePassword(data: z.infer<typeof AuthSchema.changePassword>) {
|
|
|
|
const { newPassword, phoneNumber, username } = data;
|
|
|
|
const user = await db.staff.findFirst({
|
|
|
|
where: { OR: [{ username }, { phoneNumber }], deletedAt: null },
|
|
|
|
});
|
2024-09-03 20:19:33 +08:00
|
|
|
|
2024-12-30 08:26:40 +08:00
|
|
|
if (!user) {
|
|
|
|
throw new UnauthorizedException('用户不存在');
|
2024-09-03 20:19:33 +08:00
|
|
|
}
|
2024-12-30 08:26:40 +08:00
|
|
|
await this.staffService.update({
|
|
|
|
where: { id: user?.id },
|
|
|
|
data: {
|
|
|
|
password: newPassword,
|
2024-09-10 10:31:24 +08:00
|
|
|
}
|
2024-09-09 18:48:07 +08:00
|
|
|
});
|
|
|
|
|
2024-12-30 08:26:40 +08:00
|
|
|
return { message: '密码已修改' };
|
2024-09-09 18:48:07 +08:00
|
|
|
}
|
|
|
|
async logout(data: z.infer<typeof AuthSchema.logoutRequest>) {
|
2024-12-30 08:26:40 +08:00
|
|
|
const { refreshToken, sessionId } = data;
|
2024-09-03 20:19:33 +08:00
|
|
|
|
2024-12-30 08:26:40 +08:00
|
|
|
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('无效的会话');
|
2024-09-03 20:19:33 +08:00
|
|
|
}
|
2024-09-09 18:48:07 +08:00
|
|
|
|
2024-12-30 08:26:40 +08:00
|
|
|
return { message: '注销成功' };
|
2024-09-09 18:48:07 +08:00
|
|
|
}
|
2024-12-30 08:26:40 +08:00
|
|
|
|
|
|
|
}
|