345 lines
7.9 KiB
TypeScript
345 lines
7.9 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
import { Repository } from 'typeorm';
|
|
import * as bcrypt from 'bcrypt';
|
|
import * as jwt from 'jsonwebtoken';
|
|
import { ConfigService } from '@nestjs/config';
|
|
import { AdminORM } from '../infrastructure/database/entities/admin.orm';
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
|
|
/**
|
|
* 管理员角色
|
|
*/
|
|
export enum AdminRole {
|
|
SUPER_ADMIN = 'SUPER_ADMIN',
|
|
ADMIN = 'ADMIN',
|
|
OPERATOR = 'OPERATOR',
|
|
VIEWER = 'VIEWER',
|
|
}
|
|
|
|
/**
|
|
* 角色权限映射
|
|
*/
|
|
const ROLE_PERMISSIONS: Record<AdminRole, string[]> = {
|
|
[AdminRole.SUPER_ADMIN]: ['*'],
|
|
[AdminRole.ADMIN]: [
|
|
'knowledge:*',
|
|
'experience:*',
|
|
'user:read',
|
|
'conversation:read',
|
|
'statistics:*',
|
|
'admin:read',
|
|
],
|
|
[AdminRole.OPERATOR]: [
|
|
'knowledge:read',
|
|
'knowledge:create',
|
|
'knowledge:update',
|
|
'experience:read',
|
|
'experience:approve',
|
|
'user:read',
|
|
'conversation:read',
|
|
'statistics:read',
|
|
],
|
|
[AdminRole.VIEWER]: [
|
|
'knowledge:read',
|
|
'experience:read',
|
|
'user:read',
|
|
'conversation:read',
|
|
'statistics:read',
|
|
],
|
|
};
|
|
|
|
/**
|
|
* 登录结果
|
|
*/
|
|
export interface LoginResult {
|
|
admin: {
|
|
id: string;
|
|
username: string;
|
|
name: string;
|
|
role: string;
|
|
permissions: string[];
|
|
};
|
|
token: string;
|
|
expiresIn: number;
|
|
}
|
|
|
|
/**
|
|
* 管理员服务
|
|
*/
|
|
@Injectable()
|
|
export class AdminService {
|
|
private readonly jwtSecret: string;
|
|
private readonly jwtExpiresIn: number = 24 * 60 * 60; // 24小时
|
|
|
|
constructor(
|
|
@InjectRepository(AdminORM)
|
|
private adminRepo: Repository<AdminORM>,
|
|
private configService: ConfigService,
|
|
) {
|
|
this.jwtSecret = this.configService.get('JWT_SECRET') || 'iconsulting-secret-key';
|
|
}
|
|
|
|
/**
|
|
* 管理员登录
|
|
*/
|
|
async login(username: string, password: string, ip?: string): Promise<LoginResult> {
|
|
const admin = await this.adminRepo.findOne({ where: { username } });
|
|
|
|
if (!admin || !admin.isActive) {
|
|
throw new Error('用户名或密码错误');
|
|
}
|
|
|
|
const isPasswordValid = await bcrypt.compare(password, admin.passwordHash);
|
|
if (!isPasswordValid) {
|
|
throw new Error('用户名或密码错误');
|
|
}
|
|
|
|
// 更新登录信息
|
|
admin.lastLoginAt = new Date();
|
|
admin.lastLoginIp = ip;
|
|
await this.adminRepo.save(admin);
|
|
|
|
// 生成Token
|
|
const token = jwt.sign(
|
|
{
|
|
sub: admin.id,
|
|
username: admin.username,
|
|
role: admin.role,
|
|
},
|
|
this.jwtSecret,
|
|
{ expiresIn: this.jwtExpiresIn },
|
|
);
|
|
|
|
// 获取权限
|
|
const permissions = this.getPermissions(admin.role as AdminRole, admin.permissions);
|
|
|
|
return {
|
|
admin: {
|
|
id: admin.id,
|
|
username: admin.username,
|
|
name: admin.name,
|
|
role: admin.role,
|
|
permissions,
|
|
},
|
|
token,
|
|
expiresIn: this.jwtExpiresIn,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 验证Token
|
|
*/
|
|
async verifyToken(token: string): Promise<{
|
|
valid: boolean;
|
|
admin?: {
|
|
id: string;
|
|
username: string;
|
|
role: string;
|
|
permissions: string[];
|
|
};
|
|
}> {
|
|
try {
|
|
const decoded = jwt.verify(token, this.jwtSecret) as {
|
|
sub: string;
|
|
username: string;
|
|
role: string;
|
|
};
|
|
|
|
const admin = await this.adminRepo.findOne({ where: { id: decoded.sub } });
|
|
if (!admin || !admin.isActive) {
|
|
return { valid: false };
|
|
}
|
|
|
|
const permissions = this.getPermissions(admin.role as AdminRole, admin.permissions);
|
|
|
|
return {
|
|
valid: true,
|
|
admin: {
|
|
id: admin.id,
|
|
username: admin.username,
|
|
role: admin.role,
|
|
permissions,
|
|
},
|
|
};
|
|
} catch {
|
|
return { valid: false };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 创建管理员
|
|
*/
|
|
async createAdmin(params: {
|
|
username: string;
|
|
password: string;
|
|
name: string;
|
|
email?: string;
|
|
phone?: string;
|
|
role: AdminRole;
|
|
}): Promise<AdminORM> {
|
|
// 检查用户名是否存在
|
|
const existing = await this.adminRepo.findOne({
|
|
where: { username: params.username },
|
|
});
|
|
if (existing) {
|
|
throw new Error('用户名已存在');
|
|
}
|
|
|
|
// 加密密码
|
|
const passwordHash = await bcrypt.hash(params.password, 10);
|
|
|
|
const admin = this.adminRepo.create({
|
|
id: uuidv4(),
|
|
username: params.username,
|
|
passwordHash,
|
|
name: params.name,
|
|
email: params.email,
|
|
phone: params.phone,
|
|
role: params.role,
|
|
permissions: ROLE_PERMISSIONS[params.role],
|
|
isActive: true,
|
|
});
|
|
|
|
await this.adminRepo.save(admin);
|
|
|
|
return admin;
|
|
}
|
|
|
|
/**
|
|
* 获取管理员列表
|
|
*/
|
|
async listAdmins(options?: {
|
|
role?: AdminRole;
|
|
isActive?: boolean;
|
|
page?: number;
|
|
pageSize?: number;
|
|
}): Promise<{
|
|
items: AdminORM[];
|
|
total: number;
|
|
}> {
|
|
const page = options?.page || 1;
|
|
const pageSize = options?.pageSize || 20;
|
|
|
|
const query = this.adminRepo.createQueryBuilder('admin');
|
|
|
|
if (options?.role) {
|
|
query.andWhere('admin.role = :role', { role: options.role });
|
|
}
|
|
|
|
if (options?.isActive !== undefined) {
|
|
query.andWhere('admin.isActive = :active', { active: options.isActive });
|
|
}
|
|
|
|
query.orderBy('admin.createdAt', 'DESC');
|
|
|
|
const [items, total] = await query
|
|
.skip((page - 1) * pageSize)
|
|
.take(pageSize)
|
|
.getManyAndCount();
|
|
|
|
return { items, total };
|
|
}
|
|
|
|
/**
|
|
* 更新管理员
|
|
*/
|
|
async updateAdmin(
|
|
adminId: string,
|
|
params: {
|
|
name?: string;
|
|
email?: string;
|
|
phone?: string;
|
|
role?: AdminRole;
|
|
isActive?: boolean;
|
|
},
|
|
): Promise<AdminORM> {
|
|
const admin = await this.adminRepo.findOne({ where: { id: adminId } });
|
|
if (!admin) {
|
|
throw new Error('管理员不存在');
|
|
}
|
|
|
|
if (params.name) admin.name = params.name;
|
|
if (params.email !== undefined) admin.email = params.email;
|
|
if (params.phone !== undefined) admin.phone = params.phone;
|
|
if (params.role) {
|
|
admin.role = params.role;
|
|
admin.permissions = ROLE_PERMISSIONS[params.role];
|
|
}
|
|
if (params.isActive !== undefined) admin.isActive = params.isActive;
|
|
|
|
await this.adminRepo.save(admin);
|
|
|
|
return admin;
|
|
}
|
|
|
|
/**
|
|
* 修改密码
|
|
*/
|
|
async changePassword(
|
|
adminId: string,
|
|
oldPassword: string,
|
|
newPassword: string,
|
|
): Promise<void> {
|
|
const admin = await this.adminRepo.findOne({ where: { id: adminId } });
|
|
if (!admin) {
|
|
throw new Error('管理员不存在');
|
|
}
|
|
|
|
const isOldPasswordValid = await bcrypt.compare(oldPassword, admin.passwordHash);
|
|
if (!isOldPasswordValid) {
|
|
throw new Error('原密码错误');
|
|
}
|
|
|
|
admin.passwordHash = await bcrypt.hash(newPassword, 10);
|
|
await this.adminRepo.save(admin);
|
|
}
|
|
|
|
/**
|
|
* 重置密码(超管功能)
|
|
*/
|
|
async resetPassword(adminId: string, newPassword: string): Promise<void> {
|
|
const admin = await this.adminRepo.findOne({ where: { id: adminId } });
|
|
if (!admin) {
|
|
throw new Error('管理员不存在');
|
|
}
|
|
|
|
admin.passwordHash = await bcrypt.hash(newPassword, 10);
|
|
await this.adminRepo.save(admin);
|
|
}
|
|
|
|
/**
|
|
* 检查权限
|
|
*/
|
|
hasPermission(adminPermissions: string[], requiredPermission: string): boolean {
|
|
// 超管拥有所有权限
|
|
if (adminPermissions.includes('*')) {
|
|
return true;
|
|
}
|
|
|
|
// 完全匹配
|
|
if (adminPermissions.includes(requiredPermission)) {
|
|
return true;
|
|
}
|
|
|
|
// 通配符匹配 (如 knowledge:* 匹配 knowledge:read)
|
|
const [resource, action] = requiredPermission.split(':');
|
|
if (adminPermissions.includes(`${resource}:*`)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 获取管理员权限列表
|
|
*/
|
|
private getPermissions(role: AdminRole, customPermissions?: string[]): string[] {
|
|
const rolePermissions = ROLE_PERMISSIONS[role] || [];
|
|
if (customPermissions && customPermissions.length > 0) {
|
|
return [...new Set([...rolePermissions, ...customPermissions])];
|
|
}
|
|
return rolePermissions;
|
|
}
|
|
}
|