import { Injectable } from '@nestjs/common'; import { InjectKysely } from 'nestjs-kysely'; import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types'; import { DB, Users } from '@docmost/db/types/db'; import { hashPassword } from '../../../common/helpers'; import { dbOrTx } from '@docmost/db/utils'; import { InsertableUser, UpdatableUser, User, } from '@docmost/db/types/entity.types'; import { PaginationOptions } from '../../pagination/pagination-options'; import { executeWithCursorPagination } from '@docmost/db/pagination/cursor-pagination'; import { ExpressionBuilder, sql } from 'kysely'; import { jsonObjectFrom } from 'kysely/helpers/postgres'; @Injectable() export class UserRepo { constructor(@InjectKysely() private readonly db: KyselyDB) {} public baseFields: Array = [ 'id', 'email', 'name', 'emailVerifiedAt', 'avatarUrl', 'role', 'workspaceId', 'locale', 'timezone', 'settings', 'lastLoginAt', 'deactivatedAt', 'createdAt', 'updatedAt', 'deletedAt', 'hasGeneratedPassword', ]; async findById( userId: string, workspaceId: string, opts?: { includePassword?: boolean; includeUserMfa?: boolean; trx?: KyselyTransaction; }, ): Promise { const db = dbOrTx(this.db, opts?.trx); return db .selectFrom('users') .select(this.baseFields) .$if(opts?.includePassword, (qb) => qb.select('password')) .$if(opts?.includeUserMfa, (qb) => qb.select(this.withUserMfa)) .where('id', '=', userId) .where('workspaceId', '=', workspaceId) .executeTakeFirst(); } async findByEmail( email: string, workspaceId: string, opts?: { includePassword?: boolean; includeUserMfa?: boolean; trx?: KyselyTransaction; }, ): Promise { const db = dbOrTx(this.db, opts?.trx); return db .selectFrom('users') .select(this.baseFields) .$if(opts?.includePassword, (qb) => qb.select('password')) .$if(opts?.includeUserMfa, (qb) => qb.select(this.withUserMfa)) .where(sql`LOWER(email)`, '=', sql`LOWER(${email})`) .where('workspaceId', '=', workspaceId) .executeTakeFirst(); } async updateUser( updatableUser: UpdatableUser, userId: string, workspaceId: string, trx?: KyselyTransaction, ) { const db = dbOrTx(this.db, trx); return await db .updateTable('users') .set({ ...updatableUser, updatedAt: new Date() }) .where('id', '=', userId) .where('workspaceId', '=', workspaceId) .execute(); } async updateLastLogin(userId: string, workspaceId: string) { return await this.db .updateTable('users') .set({ lastLoginAt: new Date(), }) .where('id', '=', userId) .where('workspaceId', '=', workspaceId) .execute(); } async insertUser( insertableUser: InsertableUser, trx?: KyselyTransaction, ): Promise { const user: InsertableUser = { name: insertableUser.name || insertableUser.email.split('@')[0].toLowerCase(), email: insertableUser.email.toLowerCase(), password: await hashPassword(insertableUser.password), locale: 'en-US', role: insertableUser?.role, lastLoginAt: new Date(), }; const db = dbOrTx(this.db, trx); return db .insertInto('users') .values({ ...insertableUser, ...user }) .returning(this.baseFields) .executeTakeFirst(); } async roleCountByWorkspaceId( role: string, workspaceId: string, ): Promise { const { count } = await this.db .selectFrom('users') .select((eb) => eb.fn.count('role').as('count')) .where('role', '=', role) .where('workspaceId', '=', workspaceId) .executeTakeFirst(); return count as number; } async getUsersPaginated(workspaceId: string, pagination: PaginationOptions) { let query = this.db .selectFrom('users') .select(this.baseFields) .where('workspaceId', '=', workspaceId) .where('deletedAt', 'is', null); if (pagination.query) { query = query.where((eb) => eb( sql`f_unaccent(users.name)`, 'ilike', sql`f_unaccent(${'%' + pagination.query + '%'})`, ).or( sql`users.email`, 'ilike', sql`f_unaccent(${'%' + pagination.query + '%'})`, ), ); } return executeWithCursorPagination(query, { perPage: pagination.limit, cursor: pagination.cursor, beforeCursor: pagination.beforeCursor, fields: [{ expression: 'id', direction: 'asc' }], parseCursor: (cursor) => ({ id: cursor.id }), }); } async updatePreference( userId: string, prefKey: string, prefValue: string | boolean, ) { return await this.db .updateTable('users') .set({ settings: sql`COALESCE(settings, '{}'::jsonb) || jsonb_build_object('preferences', COALESCE(settings->'preferences', '{}'::jsonb) || jsonb_build_object('${sql.raw(prefKey)}', ${sql.lit(prefValue)}))`, updatedAt: new Date(), }) .where('id', '=', userId) .returning(this.baseFields) .executeTakeFirst(); } withUserMfa(eb: ExpressionBuilder) { return jsonObjectFrom( eb .selectFrom('userMfa') .select([ 'userMfa.id', 'userMfa.method', 'userMfa.isEnabled', 'userMfa.createdAt', ]) .whereRef('userMfa.userId', '=', 'users.id'), ).as('mfa'); } }