| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310 |
- 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<User>,
- @InjectRepository(ApiKey)
- private apiKeyRepository: Repository<ApiKey>,
- @InjectRepository(TenantMember)
- private tenantMemberRepository: Repository<TenantMember>,
- private i18nService: I18nService,
- private tenantService: TenantService,
- ) { }
- async findOneByUsername(username: string): Promise<User | null> {
- return this.usersRepository.findOne({ where: { username } });
- }
- async create(createUserDto: CreateUserDto): Promise<User> {
- const user = this.usersRepository.create(createUserDto as any);
- return this.usersRepository.save(user as any);
- }
- async onModuleInit() {
- await this.createAdminIfNotExists();
- }
- async findAll(): Promise<User[]> {
- return this.usersRepository.find({
- select: ['id', 'username', 'isAdmin', 'role', 'createdAt', 'tenantId'],
- relations: ['tenantMembers', 'tenantMembers.tenant'],
- order: { createdAt: 'DESC' },
- });
- }
- async isAdmin(userId: string): Promise<boolean> {
- 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<User | null> {
- return this.usersRepository.findOne({
- where: { id: userId },
- relations: ['tenantMembers', 'tenantMembers.tenant']
- });
- }
- async findByApiKey(apiKeyValue: string): Promise<User | null> {
- const apiKey = await this.apiKeyRepository.findOne({
- where: { key: apiKeyValue },
- relations: ['user']
- });
- return apiKey ? apiKey.user : null;
- }
- async findByTenantId(tenantId: string): Promise<User[]> {
- 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<string> {
- 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<string> {
- 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');
- }
- }
- }
|