import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToMany, RelationCount, AfterLoad, OneToOne, } from 'typeorm'; import { Permission, hasPermission, PermissionCheckOptions, } from '../lib/permissions'; import { MediaRequest } from './MediaRequest'; import bcrypt from 'bcrypt'; import path from 'path'; import PreparedEmail from '../lib/email'; import logger from '../logger'; import { getSettings } from '../lib/settings'; import { default as generatePassword } from 'secure-random-password'; import { UserType } from '../constants/user'; import { v4 as uuid } from 'uuid'; import { UserSettings } from './UserSettings'; @Entity() export class User { public static filterMany( users: User[], showFiltered?: boolean ): Partial[] { return users.map((u) => u.filter(showFiltered)); } static readonly filteredFields: string[] = ['email']; public displayName: string; @PrimaryGeneratedColumn() public id: number; @Column({ unique: true }) public email: string; @Column({ nullable: true }) public plexUsername: string; @Column({ nullable: true }) public username?: string; @Column({ nullable: true, select: false }) public password?: string; @Column({ nullable: true, select: false }) public resetPasswordGuid?: string; @Column({ type: 'date', nullable: true }) public recoveryLinkExpirationDate?: Date | null; @Column({ type: 'integer', default: UserType.PLEX }) public userType: UserType; @Column({ nullable: true, select: false }) public plexId?: number; @Column({ nullable: true, select: false }) public plexToken?: string; @Column({ type: 'integer', default: 0 }) public permissions = 0; @Column() public avatar: string; @RelationCount((user: User) => user.requests) public requestCount: number; @OneToMany(() => MediaRequest, (request) => request.requestedBy) public requests: MediaRequest[]; @OneToOne(() => UserSettings, (settings) => settings.user, { cascade: true, eager: true, onDelete: 'CASCADE', }) public settings?: UserSettings; @CreateDateColumn() public createdAt: Date; @UpdateDateColumn() public updatedAt: Date; constructor(init?: Partial) { Object.assign(this, init); } public filter(showFiltered?: boolean): Partial { const filtered: Partial = Object.assign( {}, ...(Object.keys(this) as (keyof User)[]) .filter((k) => showFiltered || !User.filteredFields.includes(k)) .map((k) => ({ [k]: this[k] })) ); return filtered; } public hasPermission( permissions: Permission | Permission[], options?: PermissionCheckOptions ): boolean { return !!hasPermission(permissions, this.permissions, options); } public passwordMatch(password: string): Promise { return new Promise((resolve) => { if (this.password) { resolve(bcrypt.compare(password, this.password)); } else { return resolve(false); } }); } public async setPassword(password: string): Promise { const hashedPassword = await bcrypt.hash(password, 12); this.password = hashedPassword; } public async generatePassword(): Promise { const password = generatePassword.randomPassword({ length: 16 }); this.setPassword(password); const { applicationTitle, applicationUrl } = getSettings().main; try { logger.info(`Sending generated password email for ${this.email}`, { label: 'User Management', }); const email = new PreparedEmail(); await email.send({ template: path.join(__dirname, '../templates/email/generatedpassword'), message: { to: this.email, }, locals: { password: password, applicationUrl, applicationTitle, }, }); } catch (e) { logger.error('Failed to send out generated password email', { label: 'User Management', message: e.message, }); } } public async resetPassword(): Promise { const guid = uuid(); this.resetPasswordGuid = guid; // 24 hours into the future const targetDate = new Date(); targetDate.setDate(targetDate.getDate() + 1); this.recoveryLinkExpirationDate = targetDate; const { applicationTitle, applicationUrl } = getSettings().main; const resetPasswordLink = `${applicationUrl}/resetpassword/${guid}`; try { logger.info(`Sending reset password email for ${this.email}`, { label: 'User Management', }); const email = new PreparedEmail(); await email.send({ template: path.join(__dirname, '../templates/email/resetpassword'), message: { to: this.email, }, locals: { resetPasswordLink, applicationUrl: resetPasswordLink, applicationTitle, }, }); } catch (e) { logger.error('Failed to send out reset password email', { label: 'User Management', message: e.message, }); } } @AfterLoad() public setDisplayName(): void { this.displayName = this.username || this.plexUsername; } }