108 lines
3.4 KiB
TypeScript
108 lines
3.4 KiB
TypeScript
import argon2 from "argon2";
|
|
import config from "config";
|
|
import crypto from "crypto";
|
|
|
|
import Model from "../../db/Model.js";
|
|
import {EMAIL_REGEX} from "../../db/Validator.js";
|
|
import AuthProof from "../AuthProof.js";
|
|
import User from "./User.js";
|
|
import UserEmail from "./UserEmail.js";
|
|
|
|
export default class MagicLink extends Model implements AuthProof<User> {
|
|
public static validityPeriod(): number {
|
|
return config.get<number>('magic_link.validity_period') * 1000;
|
|
}
|
|
|
|
public readonly id?: number = undefined;
|
|
public readonly session_id?: string = undefined;
|
|
private email?: string = undefined;
|
|
private token?: string = undefined;
|
|
public readonly action_type?: string = undefined;
|
|
public readonly original_url?: string = undefined;
|
|
private generated_at?: Date = undefined;
|
|
private authorized: boolean = false;
|
|
private used: boolean = false;
|
|
|
|
protected init(): void {
|
|
this.setValidation('session_id').defined().length(32);
|
|
this.setValidation('email').defined().regexp(EMAIL_REGEX);
|
|
this.setValidation('token').defined().length(96);
|
|
this.setValidation('action_type').defined().maxLength(64);
|
|
this.setValidation('original_url').defined().maxLength(1745);
|
|
this.setValidation('authorized').defined();
|
|
this.setValidation('used').defined();
|
|
}
|
|
|
|
public async getResource(): Promise<User | null> {
|
|
const email = await UserEmail.select()
|
|
.with('user')
|
|
.where('email', await this.getOrFail('email'))
|
|
.first();
|
|
return email ? await email.user.get() : null;
|
|
}
|
|
|
|
public async revoke(): Promise<void> {
|
|
await this.delete();
|
|
}
|
|
|
|
public async isValid(): Promise<boolean> {
|
|
return await this.isAuthorized() ||
|
|
new Date().getTime() < this.getExpirationDate().getTime();
|
|
}
|
|
|
|
public async isAuthorized(): Promise<boolean> {
|
|
return this.authorized;
|
|
}
|
|
|
|
public authorize(): void {
|
|
this.authorized = true;
|
|
}
|
|
|
|
public isUsed(): boolean {
|
|
return this.used;
|
|
}
|
|
|
|
public useLink(): void {
|
|
this.used = true;
|
|
}
|
|
|
|
public async generateToken(email: string): Promise<string> {
|
|
const rawToken = crypto.randomBytes(48).toString('base64'); // Raw token length = 64
|
|
this.email = email;
|
|
this.generated_at = new Date();
|
|
this.token = await argon2.hash(rawToken, {
|
|
timeCost: 10,
|
|
memoryCost: 4096,
|
|
parallelism: 4,
|
|
hashLength: 32,
|
|
});
|
|
return rawToken;
|
|
}
|
|
|
|
/**
|
|
* @returns {@code null} if the token is valid, an error {@code string} otherwise.
|
|
*/
|
|
public async verifyToken(tokenGuess: string): Promise<string | null> {
|
|
// There is no token
|
|
if (this.token === undefined || this.generated_at === undefined)
|
|
return 'This token was not generated.';
|
|
|
|
// Token has expired
|
|
if (new Date().getTime() - this.generated_at.getTime() > MagicLink.validityPeriod())
|
|
return 'This token has expired.';
|
|
|
|
// Token is invalid
|
|
if (!await argon2.verify(this.token, tokenGuess))
|
|
return 'This token is invalid.';
|
|
|
|
return null;
|
|
}
|
|
|
|
|
|
public getExpirationDate(): Date {
|
|
if (!this.generated_at) return new Date();
|
|
|
|
return new Date(this.generated_at.getTime() + MagicLink.validityPeriod());
|
|
}
|
|
}
|