user.service.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. import { BadRequestException, ConflictException, Injectable, NotFoundException, OnModuleInit, ForbiddenException, Logger } from '@nestjs/common';
  2. import { InjectRepository } from '@nestjs/typeorm';
  3. import { Repository } from 'typeorm';
  4. import { User } from './user.entity';
  5. import { UserRole } from './user-role.enum';
  6. import { TenantMember } from '../tenant/tenant-member.entity';
  7. import { ApiKey } from '../auth/entities/api-key.entity';
  8. import * as bcrypt from 'bcrypt';
  9. import { CreateUserDto } from './dto/create-user.dto';
  10. import * as crypto from 'crypto';
  11. import { I18nService } from '../i18n/i18n.service';
  12. import { TenantService } from '../tenant/tenant.service';
  13. @Injectable()
  14. export class UserService implements OnModuleInit {
  15. private readonly logger = new Logger(UserService.name);
  16. constructor(
  17. @InjectRepository(User)
  18. private usersRepository: Repository<User>,
  19. @InjectRepository(ApiKey)
  20. private apiKeyRepository: Repository<ApiKey>,
  21. @InjectRepository(TenantMember)
  22. private tenantMemberRepository: Repository<TenantMember>,
  23. private i18nService: I18nService,
  24. private tenantService: TenantService,
  25. ) { }
  26. async findOneByUsername(username: string): Promise<User | null> {
  27. return this.usersRepository.findOne({ where: { username } });
  28. }
  29. async create(createUserDto: CreateUserDto): Promise<User> {
  30. const user = this.usersRepository.create(createUserDto as any);
  31. return this.usersRepository.save(user as any);
  32. }
  33. async onModuleInit() {
  34. await this.createAdminIfNotExists();
  35. }
  36. async findAll(page?: number, limit?: number): Promise<{ data: User[]; total: number }> {
  37. const queryBuilder = this.usersRepository.createQueryBuilder('user')
  38. .leftJoinAndSelect('user.tenantMembers', 'tenantMember')
  39. .leftJoinAndSelect('tenantMember.tenant', 'tenant')
  40. .select(['user.id', 'user.username', 'user.displayName', 'user.isAdmin', 'user.createdAt', 'user.tenantId', 'tenantMember', 'tenant'])
  41. .orderBy('user.createdAt', 'DESC');
  42. if (page && limit) {
  43. const [data, total] = await queryBuilder
  44. .skip((page - 1) * limit)
  45. .take(limit)
  46. .getManyAndCount();
  47. return { data, total };
  48. }
  49. const [data, total] = await queryBuilder.getManyAndCount();
  50. return { data, total };
  51. }
  52. async findByTenantId(tenantId: string, page?: number, limit?: number): Promise<{ data: User[]; total: number }> {
  53. const queryBuilder = this.usersRepository.createQueryBuilder('user')
  54. .innerJoin('user.tenantMembers', 'member', 'member.tenantId = :tenantId', { tenantId })
  55. .select(['user.id', 'user.username', 'user.displayName', 'user.isAdmin', 'user.createdAt', 'user.tenantId'])
  56. .orderBy('user.createdAt', 'DESC');
  57. if (page && limit) {
  58. const [data, total] = await queryBuilder
  59. .skip((page - 1) * limit)
  60. .take(limit)
  61. .getManyAndCount();
  62. return { data, total };
  63. }
  64. const [data, total] = await queryBuilder.getManyAndCount();
  65. return { data, total };
  66. }
  67. async isAdmin(userId: string): Promise<boolean> {
  68. const user = await this.usersRepository.findOne({
  69. where: { id: userId },
  70. select: ['isAdmin'],
  71. });
  72. return user?.isAdmin || false;
  73. }
  74. async changePassword(
  75. userId: string,
  76. currentPassword: string,
  77. newPassword: string,
  78. ): Promise<{ message: string }> {
  79. const user = await this.usersRepository.findOne({ where: { id: userId } });
  80. if (!user) {
  81. throw new NotFoundException(this.i18nService.getMessage('userNotFound'));
  82. }
  83. const isCurrentPasswordValid = await bcrypt.compare(
  84. currentPassword,
  85. user.password,
  86. );
  87. if (!isCurrentPasswordValid) {
  88. throw new BadRequestException(this.i18nService.getMessage('incorrectCurrentPassword'));
  89. }
  90. const hashedNewPassword = await bcrypt.hash(newPassword, 10);
  91. await this.usersRepository.update(userId, { password: hashedNewPassword });
  92. return { message: this.i18nService.getMessage('passwordChanged') };
  93. }
  94. async createUser(
  95. username: string,
  96. password: string,
  97. isAdmin: boolean = false,
  98. tenantId?: string,
  99. displayName?: string,
  100. ): Promise<{ message: string; user: { id: string; username: string; displayName: string; isAdmin: boolean } }> {
  101. const existingUser = await this.findOneByUsername(username);
  102. if (existingUser) {
  103. throw new ConflictException(this.i18nService.getMessage('usernameExists'));
  104. }
  105. const hashedPassword = await bcrypt.hash(password, 10);
  106. console.log(`[UserService] Creating user: ${username}, isAdmin: ${isAdmin}`);
  107. const user = await this.usersRepository.save({
  108. username,
  109. password: hashedPassword,
  110. displayName,
  111. isAdmin,
  112. tenantId: tenantId ?? undefined,
  113. } as any);
  114. return {
  115. message: this.i18nService.getMessage('userCreated'),
  116. user: { id: user.id, username: user.username, displayName: user.displayName, isAdmin: user.isAdmin },
  117. };
  118. }
  119. async findOneById(userId: string): Promise<User | null> {
  120. return this.usersRepository.findOne({
  121. where: { id: userId },
  122. relations: ['tenantMembers', 'tenantMembers.tenant']
  123. });
  124. }
  125. async findByApiKey(apiKeyValue: string): Promise<User | null> {
  126. const apiKey = await this.apiKeyRepository.findOne({
  127. where: { key: apiKeyValue },
  128. relations: ['user']
  129. });
  130. return apiKey ? apiKey.user : null;
  131. }
  132. async getUserTenants(userId: string): Promise<(TenantMember & { features?: { isNotebookEnabled: boolean } })[]> {
  133. const user = await this.usersRepository.findOne({ where: { id: userId }, select: ['isAdmin'] });
  134. if (user?.isAdmin) {
  135. const tenantsData = await this.tenantService.findAll();
  136. const allTenants = Array.isArray(tenantsData) ? tenantsData : tenantsData.data;
  137. const results = await Promise.all(allTenants.map(async t => {
  138. const settings = await this.tenantService.getSettings(t.id);
  139. return {
  140. tenantId: t.id,
  141. tenant: t,
  142. role: UserRole.SUPER_ADMIN,
  143. userId: userId,
  144. features: {
  145. isNotebookEnabled: settings?.isNotebookEnabled ?? true,
  146. },
  147. } as TenantMember & { features: { isNotebookEnabled: boolean } };
  148. }));
  149. return results;
  150. }
  151. const members = await this.tenantMemberRepository.find({
  152. where: { userId },
  153. relations: ['tenant']
  154. });
  155. // Filter out the "Default" tenant for non-super admins
  156. const filtered = members.filter(m => m.tenant?.name !== TenantService.DEFAULT_TENANT_NAME);
  157. // Attach per-tenant feature flags
  158. return Promise.all(filtered.map(async m => {
  159. const settings = await this.tenantService.getSettings(m.tenantId);
  160. return {
  161. ...m,
  162. features: {
  163. isNotebookEnabled: settings?.isNotebookEnabled ?? true,
  164. },
  165. };
  166. }));
  167. }
  168. /**
  169. * Generates a new API key for the user, or returns the existing one (first one).
  170. */
  171. async getOrCreateApiKey(userId: string): Promise<string> {
  172. const user = await this.usersRepository.findOne({
  173. where: { id: userId },
  174. relations: ['apiKeys']
  175. });
  176. if (!user) throw new NotFoundException(this.i18nService.getMessage('userNotFound'));
  177. if (user.apiKeys && user.apiKeys.length > 0) {
  178. return user.apiKeys[0].key;
  179. }
  180. const keyString = 'kb_' + crypto.randomBytes(32).toString('hex');
  181. const newApiKey = this.apiKeyRepository.create({
  182. userId: user.id,
  183. key: keyString
  184. });
  185. await this.apiKeyRepository.save(newApiKey);
  186. return keyString;
  187. }
  188. /**
  189. * Regenerates (rotates) the API key for the user.
  190. * This clears existing keys and creates a new one.
  191. */
  192. async regenerateApiKey(userId: string): Promise<string> {
  193. const user = await this.usersRepository.findOne({ where: { id: userId } });
  194. if (!user) throw new NotFoundException(this.i18nService.getMessage('userNotFound'));
  195. // Delete existing keys
  196. await this.apiKeyRepository.delete({ userId: user.id });
  197. // Create new key
  198. const keyString = 'kb_' + crypto.randomBytes(32).toString('hex');
  199. const newApiKey = this.apiKeyRepository.create({
  200. userId: user.id,
  201. key: keyString
  202. });
  203. await this.apiKeyRepository.save(newApiKey);
  204. return keyString;
  205. }
  206. async updateUser(
  207. userId: string,
  208. updateData: { username?: string; isAdmin?: boolean; password?: string; tenantId?: string; displayName?: string },
  209. ): Promise<{ message: string; user: { id: string; username: string; displayName: string; isAdmin: boolean } }> {
  210. const user = await this.usersRepository.findOne({ where: { id: userId } });
  211. if (!user) {
  212. throw new NotFoundException(this.i18nService.getMessage('userNotFound'));
  213. }
  214. // Hash password first if update needed
  215. if (updateData.password) {
  216. const hashedPassword = await bcrypt.hash(updateData.password, 10);
  217. updateData.password = hashedPassword;
  218. }
  219. // Block any changes to user "admin"
  220. if (user.username === 'admin') {
  221. throw new ForbiddenException(this.i18nService.getMessage('cannotModifyBuiltinAdmin'));
  222. }
  223. await this.usersRepository.update(userId, updateData as any);
  224. const updatedUser = await this.usersRepository.findOne({
  225. where: { id: userId },
  226. select: ['id', 'username', 'displayName', 'isAdmin'],
  227. });
  228. return {
  229. message: this.i18nService.getMessage('userInfoUpdated'),
  230. user: {
  231. id: updatedUser!.id,
  232. username: updatedUser!.username,
  233. displayName: updatedUser!.displayName,
  234. isAdmin: updatedUser!.isAdmin
  235. },
  236. };
  237. }
  238. async deleteUser(userId: string): Promise<{ message: string }> {
  239. const user = await this.usersRepository.findOne({ where: { id: userId } });
  240. if (!user) {
  241. throw new NotFoundException(this.i18nService.getMessage('userNotFound'));
  242. }
  243. // Block deletion of user "admin"
  244. if (user.username === 'admin') {
  245. throw new ForbiddenException(this.i18nService.getMessage('cannotDeleteBuiltinAdmin'));
  246. }
  247. await this.usersRepository.delete(userId);
  248. return {
  249. message: this.i18nService.getMessage('userDeleted'),
  250. };
  251. }
  252. async getTenantSettings(tenantId: string) {
  253. return this.tenantService.getSettings(tenantId);
  254. }
  255. private async createAdminIfNotExists() {
  256. const adminUser = await this.findOneByUsername('admin');
  257. if (!adminUser) {
  258. const randomPassword = Math.random().toString(36).slice(-8);
  259. const hashedPassword = await bcrypt.hash(randomPassword, 10);
  260. await this.usersRepository.save({
  261. username: 'admin',
  262. password: hashedPassword,
  263. isAdmin: true,
  264. role: UserRole.SUPER_ADMIN,
  265. });
  266. console.log('\n=== Admin account created ===');
  267. console.log('Username: admin');
  268. console.log('Password:', randomPassword);
  269. console.log('========================================\n');
  270. }
  271. }
  272. }