import { AbstractRepository, EntityManager, EntityRepository, getManager, Transaction, TransactionManager, } from 'typeorm'; import { PasswordAuthenticationStrategy, AnonymousAuthenticationStrategy, User, UserProfile, } from '@/entities'; import { PersistedPassword } from '@/entities/types'; import { UsernameBlacklistedError, UsernameUpdateFailedError } from './types'; import { isUsernameBlacklisted } from './utils'; @EntityRepository(User) export default class UserRepository extends AbstractRepository { async createWithAnonymous( { profile, }: { profile?: UserProfile; }, @TransactionManager() manager: EntityManager = getManager() ): Promise { const entity = manager.create(User, { profile, }); const user = await manager.save(entity, { reload: true }); const anonymousAuth = await manager.save( manager.create(AnonymousAuthenticationStrategy, { user, lastSignedIn: new Date(), }), { reload: true } ); user.anonymousAuth = anonymousAuth; return user; } /** * Creates a user if data is valid, and sends a one-time password * @throws {UsernameBlacklistedError} */ @Transaction() async createFromPassword( { passwordData, username, profile, email, allowBlacklistedUsername, }: { passwordData: PersistedPassword; email: string; username: string; /** * @default false */ allowBlacklistedUsername?: boolean; profile?: UserProfile; }, @TransactionManager() manager: EntityManager = getManager() ): Promise { if (!allowBlacklistedUsername && isUsernameBlacklisted(username)) { throw new UsernameBlacklistedError(username); } const entity = manager.create(User, { username, profile, }); const user = await manager.save(entity, { reload: true }); const passwordAuth = await manager.save( manager.create(PasswordAuthenticationStrategy, { user, email, passwordData, }), { reload: true } ); user.passwordAuth = passwordAuth; return user; } @Transaction() async checkIfUsernameIsAvailable( username: string, @TransactionManager() manager: EntityManager = getManager() ): Promise { const blacklisted = isUsernameBlacklisted(username); const available = !blacklisted && (await manager.count(User, { where: { username }, take: 1 })) === 0; return available; } /** * @throws {UsernameUpdateFailedError} * @throws {UsernameBlacklistedError} */ @Transaction() async updateProfileForUser( { username, userId, profile, }: { username?: string; userId: string; profile?: Partial }, @TransactionManager() manager: EntityManager = getManager() ): Promise { if (username) { const blacklisted = isUsernameBlacklisted(username); if (blacklisted) { throw new UsernameBlacklistedError(username); } try { await manager.update(User, userId, { username, }); } catch (error) { throw new UsernameUpdateFailedError(username); } } if (Object.keys(profile ?? {}).length > 0) { await manager.update(User, userId, { profile, }); } return manager.findOneOrFail(User, userId); } @Transaction() async setVerifiedOrFail( { user }: { user: User }, @TransactionManager() manager: EntityManager = getManager() ): Promise { const passwordAuth = await user.getPasswordAuth(manager); if (!passwordAuth) { throw new Error('You do not have a password setup'); } passwordAuth.isVerified = true; await manager.save(passwordAuth, { reload: true, }); return user; } @Transaction() async findByUsernameOrEmail( usernameOrEmail: string, @TransactionManager() manager: EntityManager = getManager() ): Promise { return manager .getRepository(User) .createQueryBuilder('user') .leftJoinAndSelect('user.passwordAuth', 'passwordAuth') .where('user.username = :username', { username: usernameOrEmail }) .orWhere('passwordAuth.email = :email', { email: usernameOrEmail }) .getOne(); } }