| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324 |
- 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(page?: number, limit?: number): Promise<{ data: User[]; total: number }> {
- const queryBuilder = this.usersRepository.createQueryBuilder('user')
- .leftJoinAndSelect('user.tenantMembers', 'tenantMember')
- .leftJoinAndSelect('tenantMember.tenant', 'tenant')
- .select(['user.id', 'user.username', 'user.displayName', 'user.isAdmin', 'user.createdAt', 'user.tenantId', 'tenantMember', 'tenant'])
- .orderBy('user.createdAt', 'DESC');
- if (page && limit) {
- const [data, total] = await queryBuilder
- .skip((page - 1) * limit)
- .take(limit)
- .getManyAndCount();
- return { data, total };
- }
- const [data, total] = await queryBuilder.getManyAndCount();
- return { data, total };
- }
- async findByTenantId(tenantId: string, page?: number, limit?: number): Promise<{ data: User[]; total: number }> {
- const queryBuilder = this.usersRepository.createQueryBuilder('user')
- .innerJoin('user.tenantMembers', 'member', 'member.tenantId = :tenantId', { tenantId })
- .select(['user.id', 'user.username', 'user.displayName', 'user.isAdmin', 'user.createdAt', 'user.tenantId'])
- .orderBy('user.createdAt', 'DESC');
- if (page && limit) {
- const [data, total] = await queryBuilder
- .skip((page - 1) * limit)
- .take(limit)
- .getManyAndCount();
- return { data, total };
- }
- const [data, total] = await queryBuilder.getManyAndCount();
- return { data, total };
- }
- 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,
- displayName?: string,
- ): Promise<{ message: string; user: { id: string; username: string; displayName: 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);
- console.log(`[UserService] Creating user: ${username}, isAdmin: ${isAdmin}`);
- const user = await this.usersRepository.save({
- username,
- password: hashedPassword,
- displayName,
- isAdmin,
- tenantId: tenantId ?? undefined,
- } as any);
- return {
- message: this.i18nService.getMessage('userCreated'),
- user: { id: user.id, username: user.username, displayName: user.displayName, 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 getUserTenants(userId: string): Promise<(TenantMember & { features?: { isNotebookEnabled: boolean } })[]> {
- const user = await this.usersRepository.findOne({ where: { id: userId }, select: ['isAdmin'] });
- if (user?.isAdmin) {
- const tenantsData = await this.tenantService.findAll();
- const allTenants = Array.isArray(tenantsData) ? tenantsData : tenantsData.data;
- 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: { username?: string; isAdmin?: boolean; password?: string; tenantId?: string; displayName?: string },
- ): Promise<{ message: string; user: { id: string; username: string; displayName: string; isAdmin: boolean } }> {
- const user = await this.usersRepository.findOne({ where: { id: userId } });
- if (!user) {
- throw new NotFoundException(this.i18nService.getMessage('userNotFound'));
- }
- // Hash password first if update needed
- if (updateData.password) {
- const hashedPassword = await bcrypt.hash(updateData.password, 10);
- updateData.password = hashedPassword;
- }
- // Block any changes to user "admin"
- if (user.username === 'admin') {
- throw new ForbiddenException(this.i18nService.getMessage('cannotModifyBuiltinAdmin'));
- }
- await this.usersRepository.update(userId, updateData as any);
- const updatedUser = await this.usersRepository.findOne({
- where: { id: userId },
- select: ['id', 'username', 'displayName', 'isAdmin'],
- });
- return {
- message: this.i18nService.getMessage('userInfoUpdated'),
- user: {
- id: updatedUser!.id,
- username: updatedUser!.username,
- displayName: updatedUser!.displayName,
- 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'));
- }
- // Block deletion of user "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=== Admin account created ===');
- console.log('Username: admin');
- console.log('Password:', randomPassword);
- console.log('========================================\n');
- }
- }
- }
|