408 lines
11 KiB
TypeScript
408 lines
11 KiB
TypeScript
|
|
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||
|
|
import { JwtService } from '@nestjs/jwt';
|
||
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
||
|
|
import { Repository } from 'typeorm';
|
||
|
|
import { ConfigService } from '@nestjs/config';
|
||
|
|
import * as bcrypt from 'bcrypt';
|
||
|
|
import { AuthToken } from '../entities/AuthToken';
|
||
|
|
import { LoginDto, RefreshTokenDto, LogoutDto } from '../dto/AuthDto';
|
||
|
|
|
||
|
|
// 导入Admin和Member服务
|
||
|
|
import { CoreAdminService } from '../../admin/services/core/CoreAdminService';
|
||
|
|
import { CoreMemberService } from '../../member/services/core/CoreMemberService';
|
||
|
|
|
||
|
|
@Injectable()
|
||
|
|
export class AuthService {
|
||
|
|
constructor(
|
||
|
|
@InjectRepository(AuthToken)
|
||
|
|
private readonly authTokenRepository: Repository<AuthToken>,
|
||
|
|
private readonly jwtService: JwtService,
|
||
|
|
private readonly configService: ConfigService,
|
||
|
|
private readonly adminService: CoreAdminService,
|
||
|
|
private readonly memberService: CoreMemberService,
|
||
|
|
) {}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 管理员登录
|
||
|
|
*/
|
||
|
|
async adminLogin(loginDto: LoginDto, ipAddress: string, userAgent: string) {
|
||
|
|
const { username, password, siteId = 0 } = loginDto;
|
||
|
|
|
||
|
|
// 调用AdminService验证用户名密码
|
||
|
|
const adminUser = await this.validateAdminUser(username, password, siteId);
|
||
|
|
|
||
|
|
if (!adminUser) {
|
||
|
|
throw new UnauthorizedException('用户名或密码错误');
|
||
|
|
}
|
||
|
|
|
||
|
|
// 生成JWT Token
|
||
|
|
const tokenPayload = {
|
||
|
|
userId: adminUser.uid,
|
||
|
|
username: adminUser.username,
|
||
|
|
userType: 'admin',
|
||
|
|
siteId,
|
||
|
|
};
|
||
|
|
|
||
|
|
const accessToken = this.jwtService.sign(tokenPayload, {
|
||
|
|
expiresIn: this.configService.get('JWT_EXPIRES_IN', '7d'),
|
||
|
|
});
|
||
|
|
|
||
|
|
const refreshToken = this.jwtService.sign(tokenPayload, {
|
||
|
|
expiresIn: this.configService.get('JWT_REFRESH_EXPIRES_IN', '30d'),
|
||
|
|
});
|
||
|
|
|
||
|
|
// 计算过期时间
|
||
|
|
const expiresIn = this.configService.get('JWT_EXPIRES_IN', '7d');
|
||
|
|
const refreshExpiresIn = this.configService.get('JWT_REFRESH_EXPIRES_IN', '30d');
|
||
|
|
|
||
|
|
const expiresAt = this.calculateExpiryDate(expiresIn);
|
||
|
|
const refreshExpiresAt = this.calculateExpiryDate(refreshExpiresIn);
|
||
|
|
|
||
|
|
// 保存Token到数据库
|
||
|
|
const authToken = this.authTokenRepository.create({
|
||
|
|
token: accessToken,
|
||
|
|
userId: adminUser.uid,
|
||
|
|
userType: 'admin',
|
||
|
|
siteId,
|
||
|
|
expiresAt,
|
||
|
|
refreshToken,
|
||
|
|
refreshExpiresAt,
|
||
|
|
ipAddress: ipAddress,
|
||
|
|
userAgent: userAgent,
|
||
|
|
deviceType: this.detectDeviceType(userAgent),
|
||
|
|
isRevoked: 0,
|
||
|
|
});
|
||
|
|
|
||
|
|
await this.authTokenRepository.save(authToken);
|
||
|
|
|
||
|
|
// 更新管理员登录信息
|
||
|
|
await this.adminService.updateLoginInfo(adminUser.uid, ipAddress);
|
||
|
|
|
||
|
|
return {
|
||
|
|
accessToken,
|
||
|
|
refreshToken,
|
||
|
|
expiresIn,
|
||
|
|
user: {
|
||
|
|
userId: adminUser.uid,
|
||
|
|
username: adminUser.username,
|
||
|
|
realname: adminUser.real_name,
|
||
|
|
userType: 'admin',
|
||
|
|
siteId,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 会员登录
|
||
|
|
*/
|
||
|
|
async memberLogin(loginDto: LoginDto, ipAddress: string, userAgent: string) {
|
||
|
|
const { username, password, siteId = 0 } = loginDto;
|
||
|
|
|
||
|
|
// 调用MemberService验证用户名密码
|
||
|
|
const memberUser = await this.validateMemberUser(username, password, siteId);
|
||
|
|
|
||
|
|
if (!memberUser) {
|
||
|
|
throw new UnauthorizedException('用户名或密码错误');
|
||
|
|
}
|
||
|
|
|
||
|
|
// 生成JWT Token
|
||
|
|
const tokenPayload = {
|
||
|
|
userId: memberUser.member_id,
|
||
|
|
username: memberUser.username,
|
||
|
|
userType: 'member',
|
||
|
|
siteId,
|
||
|
|
};
|
||
|
|
|
||
|
|
const accessToken = this.jwtService.sign(tokenPayload, {
|
||
|
|
expiresIn: this.configService.get('JWT_EXPIRES_IN', '7d'),
|
||
|
|
});
|
||
|
|
|
||
|
|
const refreshToken = this.jwtService.sign(tokenPayload, {
|
||
|
|
expiresIn: this.configService.get('JWT_REFRESH_EXPIRES_IN', '30d'),
|
||
|
|
});
|
||
|
|
|
||
|
|
// 计算过期时间
|
||
|
|
const expiresIn = this.configService.get('JWT_EXPIRES_IN', '7d');
|
||
|
|
const refreshExpiresIn = this.configService.get('JWT_REFRESH_EXPIRES_IN', '30d');
|
||
|
|
|
||
|
|
const expiresAt = this.calculateExpiryDate(expiresIn);
|
||
|
|
const refreshExpiresAt = this.calculateExpiryDate(refreshExpiresIn);
|
||
|
|
|
||
|
|
// 保存Token到数据库
|
||
|
|
const authToken = this.authTokenRepository.create({
|
||
|
|
token: accessToken,
|
||
|
|
userId: memberUser.member_id,
|
||
|
|
userType: 'member',
|
||
|
|
siteId,
|
||
|
|
expiresAt,
|
||
|
|
refreshToken,
|
||
|
|
refreshExpiresAt,
|
||
|
|
ipAddress: ipAddress,
|
||
|
|
userAgent: userAgent,
|
||
|
|
deviceType: this.detectDeviceType(userAgent),
|
||
|
|
isRevoked: 0,
|
||
|
|
});
|
||
|
|
|
||
|
|
await this.authTokenRepository.save(authToken);
|
||
|
|
|
||
|
|
// 更新会员登录信息
|
||
|
|
await this.memberService.updateLastLogin(memberUser.member_id, {
|
||
|
|
ip: ipAddress,
|
||
|
|
address: ipAddress, // 这里可以调用IP地址解析服务
|
||
|
|
device: this.detectDeviceType(userAgent),
|
||
|
|
});
|
||
|
|
|
||
|
|
return {
|
||
|
|
accessToken,
|
||
|
|
refreshToken,
|
||
|
|
expiresIn,
|
||
|
|
user: {
|
||
|
|
userId: memberUser.member_id,
|
||
|
|
username: memberUser.username,
|
||
|
|
nickname: memberUser.nickname,
|
||
|
|
userType: 'member',
|
||
|
|
siteId,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 刷新Token
|
||
|
|
*/
|
||
|
|
async refreshToken(refreshTokenDto: RefreshTokenDto) {
|
||
|
|
const { refreshToken } = refreshTokenDto;
|
||
|
|
|
||
|
|
try {
|
||
|
|
// 验证刷新Token
|
||
|
|
const payload = this.jwtService.verify(refreshToken);
|
||
|
|
|
||
|
|
// 检查数据库中的Token记录
|
||
|
|
const tokenRecord = await this.authTokenRepository.findOne({
|
||
|
|
where: { refreshToken, isRevoked: 0 },
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!tokenRecord || tokenRecord.isRefreshExpired()) {
|
||
|
|
throw new UnauthorizedException('刷新Token无效或已过期');
|
||
|
|
}
|
||
|
|
|
||
|
|
// 生成新的访问Token
|
||
|
|
const newTokenPayload = {
|
||
|
|
userId: payload.userId,
|
||
|
|
username: payload.username,
|
||
|
|
userType: payload.userType,
|
||
|
|
siteId: payload.siteId,
|
||
|
|
};
|
||
|
|
|
||
|
|
const newAccessToken = this.jwtService.sign(newTokenPayload, {
|
||
|
|
expiresIn: this.configService.get('JWT_EXPIRES_IN', '7d'),
|
||
|
|
});
|
||
|
|
|
||
|
|
// 更新数据库中的Token
|
||
|
|
tokenRecord.token = newAccessToken;
|
||
|
|
tokenRecord.expiresAt = this.calculateExpiryDate(this.configService.get('JWT_EXPIRES_IN', '7d'));
|
||
|
|
await this.authTokenRepository.save(tokenRecord);
|
||
|
|
|
||
|
|
return {
|
||
|
|
accessToken: newAccessToken,
|
||
|
|
expiresIn: this.configService.get('JWT_EXPIRES_IN', '7d'),
|
||
|
|
};
|
||
|
|
} catch (error) {
|
||
|
|
throw new UnauthorizedException('刷新Token无效');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 登出
|
||
|
|
*/
|
||
|
|
async logout(logoutDto: LogoutDto) {
|
||
|
|
const { token } = logoutDto;
|
||
|
|
|
||
|
|
// 撤销Token
|
||
|
|
const tokenRecord = await this.authTokenRepository.findOne({
|
||
|
|
where: { token, isRevoked: 0 },
|
||
|
|
});
|
||
|
|
|
||
|
|
if (tokenRecord) {
|
||
|
|
tokenRecord.isRevoked = 1;
|
||
|
|
tokenRecord.revokedAt = new Date();
|
||
|
|
tokenRecord.revokedReason = '用户主动登出';
|
||
|
|
await this.authTokenRepository.save(tokenRecord);
|
||
|
|
}
|
||
|
|
|
||
|
|
return { message: '登出成功' };
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 验证Token
|
||
|
|
*/
|
||
|
|
async validateToken(token: string): Promise<any> {
|
||
|
|
try {
|
||
|
|
// 验证JWT Token
|
||
|
|
const payload = this.jwtService.verify(token);
|
||
|
|
|
||
|
|
// 检查数据库中的Token记录
|
||
|
|
const tokenRecord = await this.authTokenRepository.findOne({
|
||
|
|
where: { token, isRevoked: 0 },
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!tokenRecord || tokenRecord.isExpired()) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return payload;
|
||
|
|
} catch (error) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 获取用户Token信息
|
||
|
|
*/
|
||
|
|
async getUserTokens(userId: number, userType: string, siteId: number = 0) {
|
||
|
|
return await this.authTokenRepository.find({
|
||
|
|
where: { userId, userType, siteId, isRevoked: 0 },
|
||
|
|
order: { createdAt: 'DESC' },
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 撤销用户所有Token
|
||
|
|
*/
|
||
|
|
async revokeUserTokens(userId: number, userType: string, siteId: number = 0, reason: string = '管理员撤销') {
|
||
|
|
const tokens = await this.authTokenRepository.find({
|
||
|
|
where: { userId, userType, siteId, isRevoked: 0 },
|
||
|
|
});
|
||
|
|
|
||
|
|
for (const token of tokens) {
|
||
|
|
token.isRevoked = 1;
|
||
|
|
token.revokedAt = new Date();
|
||
|
|
token.revokedReason = reason;
|
||
|
|
}
|
||
|
|
|
||
|
|
await this.authTokenRepository.save(tokens);
|
||
|
|
return { message: 'Token撤销成功', count: tokens.length };
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 清理过期Token
|
||
|
|
*/
|
||
|
|
async cleanupExpiredTokens() {
|
||
|
|
const expiredTokens = await this.authTokenRepository
|
||
|
|
.createQueryBuilder('token')
|
||
|
|
.where('token.expires_at < :now', { now: new Date() })
|
||
|
|
.andWhere('token.is_revoked = :revoked', { revoked: 0 })
|
||
|
|
.getMany();
|
||
|
|
|
||
|
|
for (const token of expiredTokens) {
|
||
|
|
token.isRevoked = 1;
|
||
|
|
token.revokedAt = new Date();
|
||
|
|
token.revokedReason = 'Token过期自动清理';
|
||
|
|
}
|
||
|
|
|
||
|
|
if (expiredTokens.length > 0) {
|
||
|
|
await this.authTokenRepository.save(expiredTokens);
|
||
|
|
}
|
||
|
|
|
||
|
|
return { message: '过期Token清理完成', count: expiredTokens.length };
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 计算过期时间
|
||
|
|
*/
|
||
|
|
private calculateExpiryDate(expiresIn: string): Date {
|
||
|
|
const now = new Date();
|
||
|
|
const unit = expiresIn.slice(-1);
|
||
|
|
const value = parseInt(expiresIn.slice(0, -1));
|
||
|
|
|
||
|
|
switch (unit) {
|
||
|
|
case 's':
|
||
|
|
return new Date(now.getTime() + value * 1000);
|
||
|
|
case 'm':
|
||
|
|
return new Date(now.getTime() + value * 60 * 1000);
|
||
|
|
case 'h':
|
||
|
|
return new Date(now.getTime() + value * 60 * 60 * 1000);
|
||
|
|
case 'd':
|
||
|
|
return new Date(now.getTime() + value * 24 * 60 * 60 * 1000);
|
||
|
|
default:
|
||
|
|
return new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000); // 默认7天
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 检测设备类型
|
||
|
|
*/
|
||
|
|
private detectDeviceType(userAgent: string): string {
|
||
|
|
if (/mobile|android|iphone|ipad|phone/i.test(userAgent)) {
|
||
|
|
return 'mobile';
|
||
|
|
} else if (/app|application/i.test(userAgent)) {
|
||
|
|
return 'app';
|
||
|
|
} else {
|
||
|
|
return 'web';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 验证管理员用户
|
||
|
|
*/
|
||
|
|
private async validateAdminUser(username: string, password: string, siteId: number): Promise<any> {
|
||
|
|
try {
|
||
|
|
// 根据用户名查找管理员
|
||
|
|
const admin = await this.adminService.getAdminByUsername(username);
|
||
|
|
if (!admin) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 验证密码
|
||
|
|
const isValidPassword = await this.adminService.validatePassword(admin.uid, password);
|
||
|
|
if (!isValidPassword) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 检查状态
|
||
|
|
if (admin.status !== 1 || admin.is_del !== 0) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return admin;
|
||
|
|
} catch (error) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 验证会员用户
|
||
|
|
*/
|
||
|
|
private async validateMemberUser(username: string, password: string, siteId: number): Promise<any> {
|
||
|
|
try {
|
||
|
|
// 根据用户名查找会员
|
||
|
|
let member = await this.memberService.findByUsername(username);
|
||
|
|
|
||
|
|
// 如果用户名没找到,尝试用手机号或邮箱查找
|
||
|
|
if (!member) {
|
||
|
|
member = await this.memberService.findByMobile(username);
|
||
|
|
}
|
||
|
|
if (!member) {
|
||
|
|
member = await this.memberService.findByEmail(username);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!member) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 验证密码
|
||
|
|
const isValidPassword = await bcrypt.compare(password, member.password);
|
||
|
|
if (!isValidPassword) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 检查状态
|
||
|
|
if (member.status !== 1 || member.is_del !== 0) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return member;
|
||
|
|
} catch (error) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|