import { BadRequestException, ConflictException, Injectable, NotFoundException, OnModuleInit, ForbiddenException, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { User } from './user.entity'; import { UserRole } from './user-role.enum'; import { TenantMember } from '../tenant/tenant-member.entity'; import { ApiKey } from '../auth/entities/api-key.entity'; import * as bcrypt from 'bcrypt'; import { CreateUserDto } from './dto/create-user.dto'; import * as crypto from 'crypto'; import { I18nService } from '../i18n/i18n.service'; import { TenantService } from '../tenant/tenant.service'; @Injectable() export class UserService implements OnModuleInit { private readonly logger = new Logger(UserService.name); constructor( @InjectRepository(User) private usersRepository: Repository, @InjectRepository(ApiKey) private apiKeyRepository: Repository, @InjectRepository(TenantMember) private tenantMemberRepository: Repository, private i18nService: I18nService, private tenantService: TenantService, ) { } async findOneByUsername(username: string): Promise { return this.usersRepository.findOne({ where: { username } }); } async create(createUserDto: CreateUserDto): Promise { const user = this.usersRepository.create(createUserDto as any); return this.usersRepository.save(user as any); } async onModuleInit() { await this.createAdminIfNotExists(); } async findAll(): Promise { return this.usersRepository.find({ select: ['id', 'username', 'isAdmin', 'role', 'createdAt', 'tenantId'], relations: ['tenantMembers', 'tenantMembers.tenant'], order: { createdAt: 'DESC' }, }); } async isAdmin(userId: string): Promise { const user = await this.usersRepository.findOne({ where: { id: userId }, select: ['isAdmin'], }); return user?.isAdmin || false; } async changePassword( userId: string, currentPassword: string, newPassword: string, ): Promise<{ message: string }> { const user = await this.usersRepository.findOne({ where: { id: userId } }); if (!user) { throw new NotFoundException(this.i18nService.getMessage('userNotFound')); } const isCurrentPasswordValid = await bcrypt.compare( currentPassword, user.password, ); if (!isCurrentPasswordValid) { throw new BadRequestException(this.i18nService.getMessage('incorrectCurrentPassword')); } const hashedNewPassword = await bcrypt.hash(newPassword, 10); await this.usersRepository.update(userId, { password: hashedNewPassword }); return { message: this.i18nService.getMessage('passwordChanged') }; } async createUser( username: string, password: string, isAdmin: boolean = false, tenantId?: string, role?: UserRole, ): Promise<{ message: string; user: { id: string; username: string; isAdmin: boolean } }> { const existingUser = await this.findOneByUsername(username); if (existingUser) { throw new ConflictException(this.i18nService.getMessage('usernameExists')); } const hashedPassword = await bcrypt.hash(password, 10); const targetRoleValue = role ?? (isAdmin ? UserRole.TENANT_ADMIN : UserRole.USER); console.log(`[UserService] Creating user: ${username}, isAdmin: ${isAdmin}, role: ${targetRoleValue}`); const user = await this.usersRepository.save({ username, password: hashedPassword, isAdmin, tenantId: tenantId ?? undefined, role: targetRoleValue, } as any); return { message: this.i18nService.getMessage('userCreated'), user: { id: user.id, username: user.username, isAdmin: user.isAdmin }, }; } async findOneById(userId: string): Promise { return this.usersRepository.findOne({ where: { id: userId }, relations: ['tenantMembers', 'tenantMembers.tenant'] }); } async findByApiKey(apiKeyValue: string): Promise { const apiKey = await this.apiKeyRepository.findOne({ where: { key: apiKeyValue }, relations: ['user'] }); return apiKey ? apiKey.user : null; } async findByTenantId(tenantId: string): Promise { const members = await this.tenantMemberRepository.find({ where: { tenantId }, relations: ['user'] }); return members.map(m => m.user); } async getUserTenants(userId: string): Promise<(TenantMember & { features?: { isNotebookEnabled: boolean } })[]> { const user = await this.usersRepository.findOne({ where: { id: userId }, select: ['role'] }); if (user?.role === UserRole.SUPER_ADMIN) { const allTenants = await this.tenantService.findAll(); const results = await Promise.all(allTenants.map(async t => { const settings = await this.tenantService.getSettings(t.id); return { tenantId: t.id, tenant: t, role: UserRole.SUPER_ADMIN, userId: userId, features: { isNotebookEnabled: settings?.isNotebookEnabled ?? true, }, } as TenantMember & { features: { isNotebookEnabled: boolean } }; })); return results; } const members = await this.tenantMemberRepository.find({ where: { userId }, relations: ['tenant'] }); // Filter out the "Default" tenant for non-super admins const filtered = members.filter(m => m.tenant?.name !== TenantService.DEFAULT_TENANT_NAME); // Attach per-tenant feature flags return Promise.all(filtered.map(async m => { const settings = await this.tenantService.getSettings(m.tenantId); return { ...m, features: { isNotebookEnabled: settings?.isNotebookEnabled ?? true, }, }; })); } /** * Generates a new API key for the user, or returns the existing one (first one). */ async getOrCreateApiKey(userId: string): Promise { const user = await this.usersRepository.findOne({ where: { id: userId }, relations: ['apiKeys'] }); if (!user) throw new NotFoundException(this.i18nService.getMessage('userNotFound')); if (user.apiKeys && user.apiKeys.length > 0) { return user.apiKeys[0].key; } const keyString = 'kb_' + crypto.randomBytes(32).toString('hex'); const newApiKey = this.apiKeyRepository.create({ userId: user.id, key: keyString }); await this.apiKeyRepository.save(newApiKey); return keyString; } /** * Regenerates (rotates) the API key for the user. * This clears existing keys and creates a new one. */ async regenerateApiKey(userId: string): Promise { const user = await this.usersRepository.findOne({ where: { id: userId } }); if (!user) throw new NotFoundException(this.i18nService.getMessage('userNotFound')); // Delete existing keys await this.apiKeyRepository.delete({ userId: user.id }); // Create new key const keyString = 'kb_' + crypto.randomBytes(32).toString('hex'); const newApiKey = this.apiKeyRepository.create({ userId: user.id, key: keyString }); await this.apiKeyRepository.save(newApiKey); return keyString; } async updateUser( userId: string, updateData: { isAdmin?: boolean; role?: string; password?: string; tenantId?: string }, ): Promise<{ message: string; user: { id: string; username: string; isAdmin: boolean } }> { const user = await this.usersRepository.findOne({ where: { id: userId } }); if (!user) { throw new NotFoundException(this.i18nService.getMessage('userNotFound')); } // パスワードの更新が必要な場合は、まずハッシュ化する if (updateData.password) { const hashedPassword = await bcrypt.hash(updateData.password, 10); updateData.password = hashedPassword; } // ユーザー名 "admin" のユーザーに対するいかなる変更も阻止 if (user.username === 'admin') { throw new ForbiddenException(this.i18nService.getMessage('cannotModifyBuiltinAdmin')); } // Apply role update logic if (updateData.role) { if (updateData.role === UserRole.TENANT_ADMIN || updateData.role === UserRole.SUPER_ADMIN) { updateData.isAdmin = true; } else { updateData.isAdmin = false; } } await this.usersRepository.update(userId, updateData as any); const updatedUser = await this.usersRepository.findOne({ where: { id: userId }, select: ['id', 'username', 'isAdmin'], }); return { message: this.i18nService.getMessage('userInfoUpdated'), user: { id: updatedUser!.id, username: updatedUser!.username, isAdmin: updatedUser!.isAdmin }, }; } async deleteUser(userId: string): Promise<{ message: string }> { const user = await this.usersRepository.findOne({ where: { id: userId } }); if (!user) { throw new NotFoundException(this.i18nService.getMessage('userNotFound')); } // ユーザー名 "admin" のユーザーの削除を阻止 if (user.username === 'admin') { throw new ForbiddenException(this.i18nService.getMessage('cannotDeleteBuiltinAdmin')); } await this.usersRepository.delete(userId); return { message: this.i18nService.getMessage('userDeleted'), }; } async getTenantSettings(tenantId: string) { return this.tenantService.getSettings(tenantId); } private async createAdminIfNotExists() { const adminUser = await this.findOneByUsername('admin'); if (!adminUser) { const randomPassword = Math.random().toString(36).slice(-8); const hashedPassword = await bcrypt.hash(randomPassword, 10); await this.usersRepository.save({ username: 'admin', password: hashedPassword, isAdmin: true, role: UserRole.SUPER_ADMIN, }); console.log('\n=== Administrator Account Created ==='); console.log('Username: admin'); console.log('パスワード:', randomPassword); console.log('========================================\n'); } } }