rainbox.email/src/models/UserPassword.ts

118 lines
3.9 KiB
TypeScript

import Model from "wms-core/db/Model";
import Validator from "wms-core/db/Validator";
import User from "wms-core/auth/models/User";
import argon2 from "argon2";
import AuthProof from "wms-core/auth/AuthProof";
import {UserAlreadyExistsAuthError} from "wms-core/auth/AuthGuard";
export default class UserPassword extends Model {
public static async getByEmail(email: string): Promise<UserPassword | null> {
const user = await User.fromEmail(email);
if (!user) return null;
const result = await this.models<UserPassword>(this.select().where('user_id', user.id).first());
return result && result.length > 0 ? result[0] : null;
}
private user_id?: number;
private password?: string;
protected defineProperties(): void {
this.defineProperty<number>('user_id', new Validator().defined().unique(this).exists(User, 'id'));
this.defineProperty<string>('password', new Validator().defined());
}
public async setUser(userID: number): Promise<void> {
if (this.user_id) throw new Error(`Cannot change this password's user.`);
this.user_id = userID;
}
public async setPassword(rawPassword: string): Promise<void> {
this.password = await argon2.hash(rawPassword, {
timeCost: 10,
memoryCost: 4096,
parallelism: 4,
});
}
public async verifyPassword(passwordGuess: string): Promise<boolean> {
if (!this.password) return false;
return await argon2.verify(this.password, passwordGuess);
}
public isOwnedBy(userId: number): boolean {
return this.user_id === userId;
}
}
export class PasswordAuthProof implements AuthProof {
public static getProofForSession(session: Express.Session): PasswordAuthProof | null {
const authPasswordProof = session.auth_password_proof;
if (!authPasswordProof) return null;
return new PasswordAuthProof(authPasswordProof.email, authPasswordProof.authorized);
}
private readonly email?: string;
private authorized: boolean;
private userPassword?: UserPassword;
public constructor(email: string, authorized: boolean = false) {
this.email = email;
this.authorized = authorized;
}
public async getEmail(): Promise<string> {
return this.email!;
}
public async getUser(): Promise<User | null> {
return await User.fromEmail(await this.getEmail());
}
public async isAuthorized(): Promise<boolean> {
return this.authorized;
}
public async isOwnedBy(userId: number): Promise<boolean> {
const password = await this.getUserPassword();
return password !== null && password.isOwnedBy(userId);
}
public async isValid(): Promise<boolean> {
return await this.getUserPassword() !== null;
}
public async revoke(session: Express.Session): Promise<void> {
session.auth_password_proof = undefined;
}
private async getUserPassword(): Promise<UserPassword | null> {
return this.userPassword ? this.userPassword : await UserPassword.getByEmail(await this.getEmail());
}
public async authorize(passwordGuess: string, session?: Express.Session): Promise<boolean> {
const password = await this.getUserPassword();
if (!password || !await password.verifyPassword(passwordGuess)) return false;
this.authorized = true;
if (session) {
this.save(session);
}
return true;
}
public async register(password: string): Promise<UserPassword> {
if (await this.getUserPassword()) throw new UserAlreadyExistsAuthError(await this.getEmail());
this.userPassword = new UserPassword({});
await this.userPassword.setPassword(password);
return this.userPassword;
}
private save(session: Express.Session) {
session.auth_password_proof = {
email: this.email,
authorized: this.authorized,
};
}
}