126 lines
4.3 KiB
TypeScript
126 lines
4.3 KiB
TypeScript
import crypto from "crypto";
|
|
import config from "config";
|
|
import Model, {EMAIL_REGEX} from "../../db/Model";
|
|
import AuthProof from "../AuthProof";
|
|
import Validator from "../../db/Validator";
|
|
import User from "./User";
|
|
import argon2 from "argon2";
|
|
|
|
export default class MagicLink extends Model implements AuthProof {
|
|
public static async bySessionID(sessionID: string, actionType?: string | string[]): Promise<MagicLink | null> {
|
|
let query = this.select().where('session_id', sessionID);
|
|
if (actionType !== undefined) {
|
|
if (typeof actionType === 'string') {
|
|
query = query.where('action_type', actionType);
|
|
} else {
|
|
query = query.whereIn('action_type', actionType);
|
|
}
|
|
}
|
|
const links = await this.models<MagicLink>(query.first());
|
|
return links.length > 0 ? links[0] : null;
|
|
}
|
|
|
|
public static validityPeriod(): number {
|
|
return config.get<number>('magic_link.validity_period') * 1000;
|
|
}
|
|
|
|
private session_id?: string;
|
|
private email?: string;
|
|
private token?: string;
|
|
private action_type?: string;
|
|
private original_url?: string;
|
|
private generated_at?: Date;
|
|
private authorized?: boolean;
|
|
|
|
constructor(data: any) {
|
|
super(data);
|
|
if (this.action_type === undefined) throw new Error('Action type must always be defined.');
|
|
if (this.original_url === undefined) throw new Error('Origin url must always be defined.');
|
|
if (this.authorized === undefined) {
|
|
this.authorized = false;
|
|
}
|
|
}
|
|
|
|
protected defineProperties(): void {
|
|
this.defineProperty<string>('session_id', new Validator().defined().length(32).unique(this));
|
|
this.defineProperty<string>('email', new Validator().defined().regexp(EMAIL_REGEX));
|
|
this.defineProperty<string>('token', new Validator().defined().length(96));
|
|
this.defineProperty<string>('action_type', new Validator().defined().maxLength(64));
|
|
this.defineProperty<string>('original_url', new Validator().defined().maxLength(1745));
|
|
this.defineProperty<Date>('generated_at', new Validator());
|
|
this.defineProperty<boolean>('authorized', new Validator().defined());
|
|
}
|
|
|
|
public async isOwnedBy(userId: number): Promise<boolean> {
|
|
const user = await this.getUser();
|
|
return user !== null && user.id === userId;
|
|
}
|
|
|
|
public async getUser(): Promise<User | null> {
|
|
return await User.fromEmail(await this.getEmail());
|
|
}
|
|
|
|
public async revoke(): Promise<void> {
|
|
await this.delete();
|
|
}
|
|
|
|
public async isValid(): Promise<boolean> {
|
|
if (await this.isAuthorized()) return true;
|
|
|
|
return new Date().getTime() < this.getExpirationDate().getTime();
|
|
}
|
|
|
|
public async isAuthorized(): Promise<boolean> {
|
|
return this.authorized!;
|
|
}
|
|
|
|
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
|
|
});
|
|
return rawToken;
|
|
}
|
|
|
|
/**
|
|
* @returns {@code null} if the token is valid, an error {@code string} otherwise.
|
|
*/
|
|
public async verifyToken(tokenGuess: string): Promise<string | null> {
|
|
if (this.token === undefined || this.generated_at === undefined) return 'This token was not generated.'; // There is no token
|
|
if (new Date().getTime() - this.generated_at.getTime() > MagicLink.validityPeriod()) return 'This token has expired.'; // Token has expired
|
|
if (!await argon2.verify(this.token, tokenGuess)) return 'This token is invalid.';
|
|
|
|
return null;
|
|
}
|
|
|
|
public getSessionID(): string {
|
|
return this.session_id!;
|
|
}
|
|
|
|
public async getEmail(): Promise<string> {
|
|
return this.email!;
|
|
}
|
|
|
|
public getActionType(): string {
|
|
return this.action_type!;
|
|
}
|
|
|
|
public getOriginalURL(): string {
|
|
return this.original_url!;
|
|
}
|
|
|
|
public getExpirationDate(): Date {
|
|
if (!this.generated_at) return new Date();
|
|
|
|
return new Date(this.generated_at?.getTime() + MagicLink.validityPeriod());
|
|
}
|
|
|
|
public authorize() {
|
|
this.authorized = true;
|
|
}
|
|
}
|