import { ConflictException, Injectable, NotFoundException, OnModuleInit, Optional, UnauthorizedException, UnprocessableEntityException, } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { InjectDataSource } from '@nestjs/typeorm'; import * as bcrypt from 'bcrypt'; import { DataSource } from 'typeorm'; import { ADMIN_AUTH_CONFIG } from '@config/app.config'; import { PaginatedResponse } from '@app/common/paginated-response.dto'; import { AdminLoginDto, CreateAdminUserDto, CreatePermissionDto, CreateRoleDto, UpdateAdminProfileDto } from './admin.dto'; import { AdminUser } from './admin-user.entity'; import { Permission } from './permission.entity'; import { Role } from './role.entity'; import { AdminUserRecord, AdminUserView, PermissionRecord, RoleRecord, } from './admin.types'; interface PaginationInput { page: number; pageSize: number; } @Injectable() export class AdminService implements OnModuleInit { private permissionSequence = 1; private roleSequence = 1; private adminSequence = 1; private readonly permissions: PermissionRecord[] = []; private readonly roles: RoleRecord[] = []; private readonly admins: AdminUserRecord[] = []; constructor( private readonly jwtService: JwtService, @Optional() @InjectDataSource() readonly dataSource?: DataSource, ) { if (!this.hasDatabase()) { this.seedMemory(); } } async onModuleInit() { if (this.hasDatabase()) { await this.seedDatabase(); await this.loadDatabaseRecords(); } } async login(dto: AdminLoginDto) { const admin = this.admins.find((item) => item.username === dto.username); if ( !admin || admin.status !== 'active' || !bcrypt.compareSync(dto.password, admin.passwordHash) ) { throw new UnauthorizedException('Invalid username or password'); } return { accessToken: await this.jwtService.signAsync({ sub: admin.id, username: admin.username, type: 'admin', }), admin: this.toAdminView(admin), }; } async createPermission(dto: CreatePermissionDto): Promise { if (this.hasDatabase()) { const repository = this.dataSource.getRepository(Permission); if (await repository.findOne({ where: { code: dto.code } })) { throw new ConflictException('Permission code already exists'); } const saved = await repository.save(repository.create(dto)); await this.loadDatabaseRecords(); return this.toPermissionRecord(saved); } if (this.permissions.some((item) => item.code === dto.code)) { throw new ConflictException('Permission code already exists'); } const now = new Date().toISOString(); const permission: PermissionRecord = { id: this.permissionSequence++, code: dto.code, name: dto.name, description: dto.description, createdAt: now, updatedAt: now, }; this.permissions.push(permission); return permission; } async listPermissions( pagination: PaginationInput, ): Promise> { if (this.hasDatabase()) { await this.loadDatabaseRecords(); } return this.paginate([...this.permissions], pagination); } async createRole(dto: CreateRoleDto): Promise { if (this.hasDatabase()) { const repository = this.dataSource.getRepository(Role); if (await repository.findOne({ where: { code: dto.code } })) { throw new ConflictException('Role code already exists'); } const saved = await repository.save( repository.create({ code: dto.code, name: dto.name, description: dto.description, permissionIds: dto.permissionIds ?? [], }), ); await this.loadDatabaseRecords(); return this.roles.find((role) => role.id === Number(saved.id)) as RoleRecord; } if (this.roles.some((item) => item.code === dto.code)) { throw new ConflictException('Role code already exists'); } const now = new Date().toISOString(); const permissionIds = dto.permissionIds ?? []; const role: RoleRecord = { id: this.roleSequence++, code: dto.code, name: dto.name, description: dto.description, permissionIds, permissions: this.permissions.filter((permission) => permissionIds.includes(permission.id), ), createdAt: now, updatedAt: now, }; this.roles.push(role); return role; } async listRoles(pagination: PaginationInput): Promise> { if (this.hasDatabase()) { await this.loadDatabaseRecords(); } return this.paginate([...this.roles], pagination); } async updateRolePermissions(id: number, permissionIds: number[]): Promise { if (this.hasDatabase()) { const repository = this.dataSource.getRepository(Role); const role = await repository.findOne({ where: { id } }); if (!role) { throw new NotFoundException('Role not found'); } role.permissionIds = [...new Set(permissionIds)]; await repository.save(role); await this.loadDatabaseRecords(); return this.roles.find((item) => item.id === id) as RoleRecord; } const role = this.roles.find((item) => item.id === id); if (!role) { throw new NotFoundException('Role not found'); } const uniquePermissionIds = [...new Set(permissionIds)]; role.permissionIds = uniquePermissionIds; role.permissions = this.permissions.filter((permission) => uniquePermissionIds.includes(permission.id), ); role.updatedAt = new Date().toISOString(); return role; } async createAdminUser(dto: CreateAdminUserDto): Promise { if (this.hasDatabase()) { const repository = this.dataSource.getRepository(AdminUser); if (await repository.findOne({ where: { username: dto.username } })) { throw new ConflictException('Admin username already exists'); } const saved = await repository.save( repository.create({ username: dto.username, passwordHash: bcrypt.hashSync(dto.password, 10), nickname: dto.nickname, email: dto.email, status: 'active', isSuperAdmin: false, roleIds: dto.roleIds ?? [], }), ); await this.loadDatabaseRecords(); return this.toAdminView( this.admins.find((admin) => admin.id === Number(saved.id)) as AdminUserRecord, ); } if (this.admins.some((item) => item.username === dto.username)) { throw new ConflictException('Admin username already exists'); } const now = new Date().toISOString(); const admin: AdminUserRecord = { id: this.adminSequence++, username: dto.username, passwordHash: bcrypt.hashSync(dto.password, 10), nickname: dto.nickname, email: dto.email, status: 'active', isSuperAdmin: false, roleIds: dto.roleIds ?? [], createdAt: now, updatedAt: now, }; this.admins.push(admin); return this.toAdminView(admin); } async listAdminUsers( pagination: PaginationInput, ): Promise> { if (this.hasDatabase()) { await this.loadDatabaseRecords(); } return this.paginate( this.admins.map((admin) => this.toAdminView(admin)), pagination, ); } async updateAdminStatus( id: number, status: 'active' | 'disabled', ): Promise { if (this.hasDatabase()) { const repository = this.dataSource.getRepository(AdminUser); const admin = await repository.findOne({ where: { id } }); if (!admin) { throw new NotFoundException('Admin user not found'); } admin.status = status; await repository.save(admin); await this.loadDatabaseRecords(); return this.toAdminView(this.findAdminById(id) as AdminUserRecord); } const admin = this.findAdminById(id); if (!admin) { throw new NotFoundException('Admin user not found'); } admin.status = status; admin.updatedAt = new Date().toISOString(); return this.toAdminView(admin); } async updateAdminRoles(id: number, roleIds: number[]): Promise { if (this.hasDatabase()) { const repository = this.dataSource.getRepository(AdminUser); const admin = await repository.findOne({ where: { id } }); if (!admin) { throw new NotFoundException('Admin user not found'); } admin.roleIds = [...new Set(roleIds)]; await repository.save(admin); await this.loadDatabaseRecords(); return this.toAdminView(this.findAdminById(id) as AdminUserRecord); } const admin = this.findAdminById(id); if (!admin) { throw new NotFoundException('Admin user not found'); } admin.roleIds = [...new Set(roleIds)]; admin.updatedAt = new Date().toISOString(); return this.toAdminView(admin); } async updateProfile( adminId: number, dto: UpdateAdminProfileDto, ): Promise { if (this.hasDatabase()) { const repository = this.dataSource.getRepository(AdminUser); const admin = await repository.findOne({ where: { id: adminId } }); if (!admin) { throw new NotFoundException('Admin user not found'); } admin.nickname = dto.nickname; admin.email = dto.email; await repository.save(admin); await this.loadDatabaseRecords(); return this.toAdminView(this.findAdminById(adminId) as AdminUserRecord); } const admin = this.findAdminById(adminId); if (!admin) { throw new NotFoundException('Admin user not found'); } admin.nickname = dto.nickname; admin.email = dto.email; admin.updatedAt = new Date().toISOString(); return this.toAdminView(admin); } async changePassword( adminId: number, oldPassword: string, newPassword: string, ): Promise> { if (this.hasDatabase()) { const repository = this.dataSource.getRepository(AdminUser); const admin = await repository.findOne({ where: { id: adminId } }); if (!admin) { throw new NotFoundException('Admin user not found'); } if (!bcrypt.compareSync(oldPassword, admin.passwordHash)) { throw new UnprocessableEntityException('Old password is incorrect'); } admin.passwordHash = bcrypt.hashSync(newPassword, 10); await repository.save(admin); await this.loadDatabaseRecords(); return {}; } const admin = this.findAdminById(adminId); if (!admin) { throw new NotFoundException('Admin user not found'); } if (!bcrypt.compareSync(oldPassword, admin.passwordHash)) { throw new UnprocessableEntityException('Old password is incorrect'); } admin.passwordHash = bcrypt.hashSync(newPassword, 10); admin.updatedAt = new Date().toISOString(); return {}; } findAdminById(id: number) { return this.admins.find((admin) => admin.id === id); } getPermissionCodesForAdmin(adminId: number) { const admin = this.findAdminById(adminId); if (!admin) { return []; } if (admin.isSuperAdmin) { return ['*']; } return this.roles .filter((role) => admin.roleIds.includes(role.id)) .flatMap((role) => role.permissions.map((permission) => permission.code)); } toAdminView(admin: AdminUserRecord): AdminUserView { const roles = this.roles.filter((role) => admin.roleIds.includes(role.id)); const permissions: PermissionRecord[] | ['*'] = admin.isSuperAdmin ? ['*'] : roles.flatMap((role) => role.permissions); return { id: admin.id, username: admin.username, nickname: admin.nickname, email: admin.email, status: admin.status, isSuperAdmin: admin.isSuperAdmin, roles, permissions, createdAt: admin.createdAt, updatedAt: admin.updatedAt, }; } private seedMemory() { const codes = [ ['permission:read', '查看权限'], ['permission:create', '创建权限'], ['role:read', '查看角色'], ['role:create', '创建角色'], ['role:update', '更新角色'], ['admin-user:read', '查看人员'], ['admin-user:create', '创建人员'], ['admin-user:update', '更新人员'], ['user:read', '查看用户'], ['user:update', '更新用户'], ['channel:read', '查看频道'], ['channel:create', '创建频道'], ['channel:update', '更新频道'], ['channel:delete', '删除频道'], ['drama:read', '查看短剧'], ['drama:create', '创建短剧'], ['drama:update', '更新短剧'], ['drama:delete', '删除短剧'], ['episode:create', '创建剧集'], ['episode:update', '更新剧集'], ['media:read', '查看媒体'], ['media:create', '创建媒体'], ['data:read', '查看数据'], ['data:sync', '同步数据'], ['payment:read', '查看支付记录'], ['log:read', '查看日志'], ]; for (const [code, name] of codes) { this.createPermission({ code, name }); } const now = new Date().toISOString(); this.admins.push({ id: this.adminSequence++, username: ADMIN_AUTH_CONFIG.username, passwordHash: bcrypt.hashSync(ADMIN_AUTH_CONFIG.password, 10), nickname: 'Super Admin', email: ADMIN_AUTH_CONFIG.email, status: 'active', isSuperAdmin: true, roleIds: [], createdAt: now, updatedAt: now, }); } private hasDatabase(): this is this & { dataSource: DataSource } { return Boolean(this.dataSource?.isInitialized); } private async seedDatabase() { const dataSource = this.dataSource as DataSource; const permissionRepository = dataSource.getRepository(Permission); for (const [code, name] of this.permissionSeeds()) { const existing = await permissionRepository.findOne({ where: { code } }); if (!existing) { await permissionRepository.save( permissionRepository.create({ code, name }), ); } else if (existing.name !== name) { existing.name = name; await permissionRepository.save(existing); } } const adminRepository = dataSource.getRepository(AdminUser); const username = ADMIN_AUTH_CONFIG.username; const existingAdmin = await adminRepository.findOne({ where: { username } }); if (!existingAdmin) { await adminRepository.save( adminRepository.create({ username, passwordHash: bcrypt.hashSync(ADMIN_AUTH_CONFIG.password, 10), nickname: 'Super Admin', email: ADMIN_AUTH_CONFIG.email, status: 'active', isSuperAdmin: true, roleIds: [], }), ); } } private async loadDatabaseRecords() { const dataSource = this.dataSource as DataSource; const permissions = await dataSource.getRepository(Permission).find({ order: { id: 'ASC' }, }); this.permissions.splice( 0, this.permissions.length, ...permissions.map((permission) => this.toPermissionRecord(permission)), ); const roles = await dataSource.getRepository(Role).find({ order: { id: 'ASC' }, }); this.roles.splice( 0, this.roles.length, ...roles.map((role) => this.toRoleRecord(role)), ); const admins = await dataSource.getRepository(AdminUser).find({ order: { id: 'ASC' }, }); this.admins.splice( 0, this.admins.length, ...admins.map((admin) => this.toAdminRecord(admin)), ); } private permissionSeeds() { return [ ['permission:read', '查看权限'], ['permission:create', '创建权限'], ['role:read', '查看角色'], ['role:create', '创建角色'], ['role:update', '更新角色'], ['admin-user:read', '查看人员'], ['admin-user:create', '创建人员'], ['admin-user:update', '更新人员'], ['user:read', '查看用户'], ['user:update', '更新用户'], ['drama:read', '查看短剧'], ['drama:create', '创建短剧'], ['drama:update', '更新短剧'], ['episode:create', '创建剧集'], ['episode:update', '更新剧集'], ['media:read', '查看媒体'], ['media:create', '创建媒体'], ['data:read', '查看数据'], ['data:sync', '同步数据'], ['payment:read', '查看支付记录'], ['log:read', '查看日志'], ] as const; } private toPermissionRecord(permission: Permission): PermissionRecord { return { id: Number(permission.id), code: permission.code, name: permission.name, description: permission.description, createdAt: permission.createdAt?.toISOString?.() ?? new Date().toISOString(), updatedAt: permission.updatedAt?.toISOString?.() ?? new Date().toISOString(), }; } private toRoleRecord(role: Role): RoleRecord { const permissionIds = role.permissionIds ?? []; return { id: Number(role.id), code: role.code, name: role.name, description: role.description, permissionIds, permissions: this.permissions.filter((permission) => permissionIds.includes(permission.id), ), createdAt: role.createdAt?.toISOString?.() ?? new Date().toISOString(), updatedAt: role.updatedAt?.toISOString?.() ?? new Date().toISOString(), }; } private toAdminRecord(admin: AdminUser): AdminUserRecord { return { id: Number(admin.id), username: admin.username, passwordHash: admin.passwordHash, nickname: admin.nickname, email: admin.email, status: admin.status as 'active' | 'disabled', isSuperAdmin: admin.isSuperAdmin, roleIds: admin.roleIds ?? [], createdAt: admin.createdAt?.toISOString?.() ?? new Date().toISOString(), updatedAt: admin.updatedAt?.toISOString?.() ?? new Date().toISOString(), }; } private paginate( records: T[], input: PaginationInput, ): PaginatedResponse { const page = Math.max(input.page, 1); const pageSize = Math.min(Math.max(input.pageSize, 1), 100); const start = (page - 1) * pageSize; return { list: records.slice(start, start + pageSize), pagination: { page, pageSize, total: records.length, }, }; } }