Greatly simplify authentication system
This commit is contained in:
parent
40181a973b
commit
a79e2292d7
@ -15,7 +15,7 @@ export default class AuthComponent extends ApplicationComponent<void> {
|
|||||||
public async init(router: Router): Promise<void> {
|
public async init(router: Router): Promise<void> {
|
||||||
router.use(async (req, res, next) => {
|
router.use(async (req, res, next) => {
|
||||||
req.authGuard = this.authGuard;
|
req.authGuard = this.authGuard;
|
||||||
res.locals.user = await req.authGuard.getUserForSession(req.session!);
|
res.locals.user = await (await req.authGuard.getProofForSession(req.session!))?.getResource();
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -30,13 +30,13 @@ export const REQUIRE_REQUEST_AUTH_MIDDLEWARE = async (req: Request, res: Respons
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
req.models.user = await req.authGuard.getUserForRequest(req);
|
req.models.user = await (await req.authGuard.getProofForRequest(req))?.getResource();
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const REQUIRE_AUTH_MIDDLEWARE = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
export const REQUIRE_AUTH_MIDDLEWARE = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
if (await req.authGuard.isAuthenticatedViaRequest(req)) {
|
if (await req.authGuard.isAuthenticatedViaRequest(req)) {
|
||||||
req.models.user = await req.authGuard.getUserForRequest(req);
|
req.models.user = await (await req.authGuard.getProofForRequest(req))?.getResource();
|
||||||
next();
|
next();
|
||||||
} else {
|
} else {
|
||||||
if (!await req.authGuard.isAuthenticated(req.session!)) {
|
if (!await req.authGuard.isAuthenticated(req.session!)) {
|
||||||
@ -47,7 +47,7 @@ export const REQUIRE_AUTH_MIDDLEWARE = async (req: Request, res: Response, next:
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
req.models.user = await req.authGuard.getUserForSession(req.session!);
|
req.models.user = await (await req.authGuard.getProofForSession(req.session!))?.getResource();
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -26,7 +26,8 @@ export default abstract class AuthController extends Controller {
|
|||||||
protected abstract async getCheckAuth(req: Request, res: Response, next: NextFunction): Promise<void>;
|
protected abstract async getCheckAuth(req: Request, res: Response, next: NextFunction): Promise<void>;
|
||||||
|
|
||||||
protected async postLogout(req: Request, res: Response, next: NextFunction): Promise<void> {
|
protected async postLogout(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||||
await req.authGuard.logout(req.session!);
|
const proof = await req.authGuard.getProofForSession(req.session!);
|
||||||
|
await proof?.revoke();
|
||||||
req.flash('success', 'Successfully logged out.');
|
req.flash('success', 'Successfully logged out.');
|
||||||
res.redirect(req.query.redirect_uri?.toString() || '/');
|
res.redirect(req.query.redirect_uri?.toString() || '/');
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import AuthProof from "./AuthProof";
|
import AuthProof from "./AuthProof";
|
||||||
import MysqlConnectionManager from "../db/MysqlConnectionManager";
|
import MysqlConnectionManager from "../db/MysqlConnectionManager";
|
||||||
import User from "./models/User";
|
import User from "./models/User";
|
||||||
import UserEmail from "./models/UserEmail";
|
|
||||||
import {Connection} from "mysql";
|
import {Connection} from "mysql";
|
||||||
import {Request} from "express";
|
import {Request} from "express";
|
||||||
import {PENDING_ACCOUNT_REVIEW_MAIL_TEMPLATE} from "../Mails";
|
import {PENDING_ACCOUNT_REVIEW_MAIL_TEMPLATE} from "../Mails";
|
||||||
@ -9,42 +8,34 @@ import Mail from "../Mail";
|
|||||||
import Controller from "../Controller";
|
import Controller from "../Controller";
|
||||||
import config from "config";
|
import config from "config";
|
||||||
|
|
||||||
export default abstract class AuthGuard<P extends AuthProof> {
|
export default abstract class AuthGuard<P extends AuthProof<User>> {
|
||||||
public abstract async getProofForSession(session: Express.Session): Promise<P | null>;
|
public abstract async getProofForSession(session: Express.Session): Promise<P | null>;
|
||||||
|
|
||||||
public async getProofForRequest(req: Request): Promise<P | null> {
|
public async getProofForRequest(req: Request): Promise<P | null> {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getUserForSession(session: Express.Session): Promise<User | null> {
|
public async authenticateOrRegister(
|
||||||
if (!await this.isAuthenticated(session)) return null;
|
session: Express.Session,
|
||||||
return await User.getById<User>(session.auth_id);
|
proof: P,
|
||||||
}
|
onLogin?: (user: User) => Promise<void>,
|
||||||
|
onRegister?: (connection: Connection, user: User) => Promise<RegisterCallback[]>
|
||||||
|
): Promise<void> {
|
||||||
|
if (!await proof.isValid()) throw new InvalidAuthProofError();
|
||||||
|
if (!await proof.isAuthorized()) throw new UnauthorizedAuthProofError();
|
||||||
|
|
||||||
public async authenticateOrRegister(session: Express.Session, proof: P, registerCallback?: (connection: Connection, userID: number) => Promise<(() => Promise<void>)[]>): Promise<void> {
|
let user = await proof.getResource();
|
||||||
if (!await proof.isAuthorized()) {
|
|
||||||
throw new AuthError('Invalid argument: cannot authenticate with an unauthorized proof.');
|
|
||||||
}
|
|
||||||
|
|
||||||
let user = await proof.getUser();
|
// Register if user doesn't exist
|
||||||
if (!user) { // Register
|
if (!user) {
|
||||||
const callbacks: (() => Promise<void>)[] = [];
|
const callbacks: RegisterCallback[] = [];
|
||||||
|
|
||||||
await MysqlConnectionManager.wrapTransaction(async connection => {
|
await MysqlConnectionManager.wrapTransaction(async connection => {
|
||||||
const email = await proof.getEmail();
|
user = new User({});
|
||||||
user = new User({
|
|
||||||
name: email.split('@')[0],
|
|
||||||
});
|
|
||||||
await user.save(connection, c => callbacks.push(c));
|
await user.save(connection, c => callbacks.push(c));
|
||||||
|
|
||||||
const userEmail = new UserEmail({
|
if (onRegister) {
|
||||||
user_id: user.id,
|
(await onRegister(connection, user)).forEach(c => callbacks.push(c));
|
||||||
email: email,
|
|
||||||
main: true,
|
|
||||||
});
|
|
||||||
await userEmail.save(connection, c => callbacks.push(c));
|
|
||||||
if (registerCallback) {
|
|
||||||
(await registerCallback(connection, user.id!)).forEach(c => callbacks.push(c));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -62,36 +53,25 @@ export default abstract class AuthGuard<P extends AuthProof> {
|
|||||||
} else {
|
} else {
|
||||||
throw new Error('Unable to register user.');
|
throw new Error('Unable to register user.');
|
||||||
}
|
}
|
||||||
} else if (registerCallback) {
|
|
||||||
throw new UserAlreadyExistsAuthError(await proof.getEmail());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't login if user isn't approved
|
||||||
if (!user.isApproved()) {
|
if (!user.isApproved()) {
|
||||||
throw new PendingApprovalAuthError();
|
throw new PendingApprovalAuthError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Login
|
||||||
session.auth_id = user.id;
|
session.auth_id = user.id;
|
||||||
|
if (onLogin) await onLogin(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async isAuthenticated(session: Express.Session): Promise<boolean> {
|
public async isAuthenticated(session: Express.Session): Promise<boolean> {
|
||||||
return await this.checkCurrentSessionProofValidity(session);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async logout(session: Express.Session): Promise<void> {
|
|
||||||
const proof = await this.getProofForSession(session);
|
|
||||||
if (proof) {
|
|
||||||
await proof.revoke(session);
|
|
||||||
}
|
|
||||||
session.auth_id = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async checkCurrentSessionProofValidity(session: Express.Session): Promise<boolean> {
|
|
||||||
if (typeof session.auth_id !== 'number') return false;
|
if (typeof session.auth_id !== 'number') return false;
|
||||||
|
|
||||||
const proof = await this.getProofForSession(session);
|
const proof = await this.getProofForSession(session);
|
||||||
|
|
||||||
if (!proof || !await proof.isValid() || !await proof.isAuthorized() || !await proof.isOwnedBy(session.auth_id)) {
|
if (!proof || !await proof.isValid() || !await proof.isAuthorized()) {
|
||||||
await this.logout(session);
|
await proof?.revoke();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,16 +80,7 @@ export default abstract class AuthGuard<P extends AuthProof> {
|
|||||||
|
|
||||||
public async isAuthenticatedViaRequest(req: Request): Promise<boolean> {
|
public async isAuthenticatedViaRequest(req: Request): Promise<boolean> {
|
||||||
const proof = await this.getProofForRequest(req);
|
const proof = await this.getProofForRequest(req);
|
||||||
if (proof && await proof.isValid() && await proof.isAuthorized()) {
|
return Boolean(proof && await proof.isValid() && await proof.isAuthorized());
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getUserForRequest(req: Request): Promise<User | null> {
|
|
||||||
const proof = await this.getProofForRequest(req);
|
|
||||||
return proof ? await proof.getUser() : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -120,12 +91,18 @@ export class AuthError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UserAlreadyExistsAuthError extends AuthError {
|
export class AuthProofError extends AuthError {
|
||||||
public readonly email: string;
|
}
|
||||||
|
|
||||||
constructor(userEmail: string) {
|
export class InvalidAuthProofError extends AuthProofError {
|
||||||
super(`User with email ${userEmail} already exists.`);
|
constructor() {
|
||||||
this.email = userEmail;
|
super('Invalid auth proof.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnauthorizedAuthProofError extends AuthProofError {
|
||||||
|
constructor() {
|
||||||
|
super('Unauthorized auth proof.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,3 +111,5 @@ export class PendingApprovalAuthError extends AuthError {
|
|||||||
super(`User is not approved.`);
|
super(`User is not approved.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RegisterCallback = () => Promise<void>;
|
||||||
|
@ -1,15 +1,40 @@
|
|||||||
import User from "./models/User";
|
/**
|
||||||
|
* This class is most commonly used for authentication. It can be more generically used to represent a verification
|
||||||
export default interface AuthProof {
|
* state of whether a given resource is owned by a session.
|
||||||
|
*
|
||||||
|
* Any auth system should consider this auth proof valid if and only if both {@code isValid()} and {@code isAuthorized()}
|
||||||
|
* both return {@code true}.
|
||||||
|
*
|
||||||
|
* @type <R> The resource type this AuthProof authorizes.
|
||||||
|
*/
|
||||||
|
export default interface AuthProof<R> {
|
||||||
|
/**
|
||||||
|
* Is this auth proof valid in time (and context)?
|
||||||
|
*
|
||||||
|
* For example, it can return true for an initial short validity time period then false, and increase that time
|
||||||
|
* period if {@code isAuthorized()} returns true.
|
||||||
|
*/
|
||||||
isValid(): Promise<boolean>;
|
isValid(): Promise<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Was this proof authorized?
|
||||||
|
*
|
||||||
|
* Return true once the session is proven to own the associated resource.
|
||||||
|
*/
|
||||||
isAuthorized(): Promise<boolean>;
|
isAuthorized(): Promise<boolean>;
|
||||||
|
|
||||||
isOwnedBy(userId: number): Promise<boolean>;
|
/**
|
||||||
|
* Retrieve the resource this auth proof is supposed to authorize.
|
||||||
|
* If this resource doesn't exist yet, return {@code null}.
|
||||||
|
*/
|
||||||
|
getResource(): Promise<R | null>;
|
||||||
|
|
||||||
getUser(): Promise<User | null>;
|
/**
|
||||||
|
* Manually revokes this authentication proof. Once this method is called, all of the following must be true:
|
||||||
getEmail(): Promise<string>;
|
* - {@code isAuthorized} returns {@code false}
|
||||||
|
* - There is no way to re-authorize this proof (i.e. {@code isAuthorized} can never return {@code true} again)
|
||||||
revoke(session: Express.Session): Promise<void>;
|
*
|
||||||
|
* Additionally, this method should delete any stored data that could lead to restoration of this AuthProof instance.
|
||||||
|
*/
|
||||||
|
revoke(): Promise<void>;
|
||||||
}
|
}
|
@ -5,7 +5,7 @@ import {BadRequestError} from "../../HttpError";
|
|||||||
import UserEmail from "../models/UserEmail";
|
import UserEmail from "../models/UserEmail";
|
||||||
import MagicLinkController from "./MagicLinkController";
|
import MagicLinkController from "./MagicLinkController";
|
||||||
import {MailTemplate} from "../../Mail";
|
import {MailTemplate} from "../../Mail";
|
||||||
import {AuthError, PendingApprovalAuthError} from "../AuthGuard";
|
import {AuthError, PendingApprovalAuthError, RegisterCallback} from "../AuthGuard";
|
||||||
import geoip from "geoip-lite";
|
import geoip from "geoip-lite";
|
||||||
import AuthController from "../AuthController";
|
import AuthController from "../AuthController";
|
||||||
import NunjucksComponent from "../../components/NunjucksComponent";
|
import NunjucksComponent from "../../components/NunjucksComponent";
|
||||||
@ -19,7 +19,19 @@ export default abstract class MagicLinkAuthController extends AuthController {
|
|||||||
|
|
||||||
// Auth
|
// Auth
|
||||||
try {
|
try {
|
||||||
await req.authGuard.authenticateOrRegister(req.session!, magicLink);
|
await req.authGuard.authenticateOrRegister(req.session!, magicLink, undefined, async (connection, user) => {
|
||||||
|
const callbacks: RegisterCallback[] = [];
|
||||||
|
|
||||||
|
const userEmail = new UserEmail({
|
||||||
|
user_id: user.id,
|
||||||
|
email: magicLink.getEmail(),
|
||||||
|
});
|
||||||
|
await userEmail.save(connection, c => callbacks.push(c));
|
||||||
|
user.main_email_id = userEmail.id;
|
||||||
|
await user.save(connection, c => callbacks.push(c));
|
||||||
|
|
||||||
|
return callbacks;
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof PendingApprovalAuthError) {
|
if (e instanceof PendingApprovalAuthError) {
|
||||||
res.format({
|
res.format({
|
||||||
|
@ -39,16 +39,16 @@ export default abstract class MagicLinkController extends Controller {
|
|||||||
this.magicLinkWebsocketPath = magicLinkWebsocketListener.path();
|
this.magicLinkWebsocketPath = magicLinkWebsocketListener.path();
|
||||||
}
|
}
|
||||||
|
|
||||||
getRoutesPrefix(): string {
|
public getRoutesPrefix(): string {
|
||||||
return '/magic';
|
return '/magic';
|
||||||
}
|
}
|
||||||
|
|
||||||
routes(): void {
|
public routes(): void {
|
||||||
this.get('/lobby', this.getLobby, 'magic_link_lobby');
|
this.get('/lobby', this.getLobby, 'magic_link_lobby');
|
||||||
this.get('/link', this.getMagicLink, 'magic_link');
|
this.get('/link', this.getMagicLink, 'magic_link');
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getLobby(req: Request, res: Response): Promise<void> {
|
protected async getLobby(req: Request, res: Response): Promise<void> {
|
||||||
const link = await MagicLink.bySessionID(req.sessionID!);
|
const link = await MagicLink.bySessionID(req.sessionID!);
|
||||||
if (!link) {
|
if (!link) {
|
||||||
throw new NotFoundHttpError('magic link', req.url);
|
throw new NotFoundHttpError('magic link', req.url);
|
||||||
@ -66,14 +66,14 @@ export default abstract class MagicLinkController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
res.render('magic_link_lobby', {
|
res.render('magic_link_lobby', {
|
||||||
email: await link.getEmail(),
|
email: link.getEmail(),
|
||||||
type: await link.getActionType(),
|
type: link.getActionType(),
|
||||||
validUntil: link.getExpirationDate().getTime(),
|
validUntil: link.getExpirationDate().getTime(),
|
||||||
websocketUrl: config.get<string>('public_websocket_url') + this.magicLinkWebsocketPath,
|
websocketUrl: config.get<string>('public_websocket_url') + this.magicLinkWebsocketPath,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getMagicLink(req: Request, res: Response): Promise<void> {
|
protected async getMagicLink(req: Request, res: Response): Promise<void> {
|
||||||
const id = parseInt(<string>req.query.id);
|
const id = parseInt(<string>req.query.id);
|
||||||
const token = <string>req.query.token;
|
const token = <string>req.query.token;
|
||||||
if (!id || !token) throw new BadRequestError('Need parameters id, token.', 'Please try again.', req.originalUrl);
|
if (!id || !token) throw new BadRequestError('Need parameters id, token.', 'Please try again.', req.originalUrl);
|
||||||
|
15
src/auth/migrations/DropNameFromUsers.ts
Normal file
15
src/auth/migrations/DropNameFromUsers.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import Migration from "../../db/Migration";
|
||||||
|
import {Connection} from "mysql";
|
||||||
|
|
||||||
|
export default class DropNameFromUsers extends Migration {
|
||||||
|
public async install(connection: Connection): Promise<void> {
|
||||||
|
await this.query('ALTER TABLE users DROP COLUMN name', connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async rollback(connection: Connection): Promise<void> {
|
||||||
|
await this.query('ALTER TABLE users ADD COLUMN name VARCHAR(64)', connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
public registerModels(): void {
|
||||||
|
}
|
||||||
|
}
|
@ -4,10 +4,10 @@ import {Connection} from "mysql";
|
|||||||
export default class FixUserMainEmailRelation extends Migration {
|
export default class FixUserMainEmailRelation extends Migration {
|
||||||
public async install(connection: Connection): Promise<void> {
|
public async install(connection: Connection): Promise<void> {
|
||||||
await this.query(`ALTER TABLE users
|
await this.query(`ALTER TABLE users
|
||||||
ADD COLUMN main_user_email_id INT,
|
ADD COLUMN main_email_id INT,
|
||||||
ADD FOREIGN KEY main_user_email_fk (main_user_email_id) REFERENCES user_emails (id)`, connection);
|
ADD FOREIGN KEY main_user_email_fk (main_email_id) REFERENCES user_emails (id)`, connection);
|
||||||
await this.query(`UPDATE users u LEFT JOIN user_emails ue ON u.id = ue.user_id
|
await this.query(`UPDATE users u LEFT JOIN user_emails ue ON u.id = ue.user_id
|
||||||
SET u.main_user_email_id=ue.id
|
SET u.main_email_id=ue.id
|
||||||
WHERE ue.main = true`, connection);
|
WHERE ue.main = true`, connection);
|
||||||
await this.query(`ALTER TABLE user_emails
|
await this.query(`ALTER TABLE user_emails
|
||||||
DROP COLUMN main`, connection);
|
DROP COLUMN main`, connection);
|
||||||
@ -16,11 +16,11 @@ export default class FixUserMainEmailRelation extends Migration {
|
|||||||
public async rollback(connection: Connection): Promise<void> {
|
public async rollback(connection: Connection): Promise<void> {
|
||||||
await this.query(`ALTER TABLE user_emails
|
await this.query(`ALTER TABLE user_emails
|
||||||
ADD COLUMN main BOOLEAN DEFAULT false`, connection);
|
ADD COLUMN main BOOLEAN DEFAULT false`, connection);
|
||||||
await this.query(`UPDATE user_emails ue LEFT JOIN users u ON ue.id = u.main_user_email_id
|
await this.query(`UPDATE user_emails ue LEFT JOIN users u ON ue.id = u.main_email_id
|
||||||
SET ue.main = true`, connection)
|
SET ue.main = true`, connection)
|
||||||
await this.query(`ALTER TABLE users
|
await this.query(`ALTER TABLE users
|
||||||
DROP FOREIGN KEY main_user_email_fk,
|
DROP FOREIGN KEY main_user_email_fk,
|
||||||
DROP COLUMN main_user_email_id`, connection);
|
DROP COLUMN main_email_id`, connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
public registerModels(): void {
|
public registerModels(): void {
|
||||||
|
@ -7,7 +7,7 @@ import argon2 from "argon2";
|
|||||||
import {WhereTest} from "../../db/ModelQuery";
|
import {WhereTest} from "../../db/ModelQuery";
|
||||||
import UserEmail from "./UserEmail";
|
import UserEmail from "./UserEmail";
|
||||||
|
|
||||||
export default class MagicLink extends Model implements AuthProof {
|
export default class MagicLink extends Model implements AuthProof<User> {
|
||||||
public static async bySessionID(sessionID: string, actionType?: string | string[]): Promise<MagicLink | null> {
|
public static async bySessionID(sessionID: string, actionType?: string | string[]): Promise<MagicLink | null> {
|
||||||
let query = this.select().where('session_id', sessionID);
|
let query = this.select().where('session_id', sessionID);
|
||||||
if (actionType !== undefined) {
|
if (actionType !== undefined) {
|
||||||
@ -31,15 +31,12 @@ export default class MagicLink extends Model implements AuthProof {
|
|||||||
private action_type?: string = undefined;
|
private action_type?: string = undefined;
|
||||||
private original_url?: string = undefined;
|
private original_url?: string = undefined;
|
||||||
private generated_at?: Date = undefined;
|
private generated_at?: Date = undefined;
|
||||||
private authorized?: boolean = undefined;
|
private authorized: boolean = false;
|
||||||
|
|
||||||
constructor(data: any) {
|
constructor(data: any) {
|
||||||
super(data);
|
super(data);
|
||||||
if (this.action_type === undefined) throw new Error('Action type must always be defined.');
|
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.original_url === undefined) throw new Error('Origin url must always be defined.');
|
||||||
if (this.authorized === undefined) {
|
|
||||||
this.authorized = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected init(): void {
|
protected init(): void {
|
||||||
@ -51,13 +48,9 @@ export default class MagicLink extends Model implements AuthProof {
|
|||||||
this.setValidation('authorized').defined();
|
this.setValidation('authorized').defined();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async isOwnedBy(userId: number): Promise<boolean> {
|
public async getResource(): Promise<User | null> {
|
||||||
const user = await this.getUser();
|
|
||||||
return user !== null && user.id === userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getUser(): Promise<User | null> {
|
|
||||||
const email = await UserEmail.select()
|
const email = await UserEmail.select()
|
||||||
|
.with('user')
|
||||||
.where('email', await this.getEmail())
|
.where('email', await this.getEmail())
|
||||||
.first();
|
.first();
|
||||||
return email ? email.user.get() : null;
|
return email ? email.user.get() : null;
|
||||||
@ -68,15 +61,18 @@ export default class MagicLink extends Model implements AuthProof {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async isValid(): Promise<boolean> {
|
public async isValid(): Promise<boolean> {
|
||||||
if (await this.isAuthorized()) return true;
|
return await this.isAuthorized() ||
|
||||||
|
new Date().getTime() < this.getExpirationDate().getTime();
|
||||||
return new Date().getTime() < this.getExpirationDate().getTime();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async isAuthorized(): Promise<boolean> {
|
public async isAuthorized(): Promise<boolean> {
|
||||||
return this.authorized!;
|
return this.authorized!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public authorize() {
|
||||||
|
this.authorized = true;
|
||||||
|
}
|
||||||
|
|
||||||
public async generateToken(email: string): Promise<string> {
|
public async generateToken(email: string): Promise<string> {
|
||||||
const rawToken = crypto.randomBytes(48).toString('base64'); // Raw token length = 64
|
const rawToken = crypto.randomBytes(48).toString('base64'); // Raw token length = 64
|
||||||
this.email = email;
|
this.email = email;
|
||||||
@ -84,7 +80,8 @@ export default class MagicLink extends Model implements AuthProof {
|
|||||||
this.token = await argon2.hash(rawToken, {
|
this.token = await argon2.hash(rawToken, {
|
||||||
timeCost: 10,
|
timeCost: 10,
|
||||||
memoryCost: 4096,
|
memoryCost: 4096,
|
||||||
parallelism: 4
|
parallelism: 4,
|
||||||
|
hashLength: 32,
|
||||||
});
|
});
|
||||||
return rawToken;
|
return rawToken;
|
||||||
}
|
}
|
||||||
@ -104,7 +101,7 @@ export default class MagicLink extends Model implements AuthProof {
|
|||||||
return this.session_id!;
|
return this.session_id!;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getEmail(): Promise<string> {
|
public getEmail(): string {
|
||||||
return this.email!;
|
return this.email!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,8 +118,4 @@ export default class MagicLink extends Model implements AuthProof {
|
|||||||
|
|
||||||
return new Date(this.generated_at.getTime() + MagicLink.validityPeriod());
|
return new Date(this.generated_at.getTime() + MagicLink.validityPeriod());
|
||||||
}
|
}
|
||||||
|
|
||||||
public authorize() {
|
|
||||||
this.authorized = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,7 @@ export default class User extends Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public readonly id?: number = undefined;
|
public readonly id?: number = undefined;
|
||||||
public name?: string = undefined;
|
public main_email_id?: number = undefined;
|
||||||
public main_user_email_id?: number = undefined;
|
|
||||||
public is_admin: boolean = false;
|
public is_admin: boolean = false;
|
||||||
public created_at?: Date = undefined;
|
public created_at?: Date = undefined;
|
||||||
public updated_at?: Date = undefined;
|
public updated_at?: Date = undefined;
|
||||||
@ -24,7 +23,7 @@ export default class User extends Model {
|
|||||||
foreignKey: 'user_id'
|
foreignKey: 'user_id'
|
||||||
});
|
});
|
||||||
|
|
||||||
public readonly mainEmail = this.emails.cloneReduceToOne().constraint(q => q.where('id', this.main_user_email_id));
|
public readonly mainEmail = this.emails.cloneReduceToOne().constraint(q => q.where('id', this.main_email_id));
|
||||||
|
|
||||||
public constructor(data: any) {
|
public constructor(data: any) {
|
||||||
super(data);
|
super(data);
|
||||||
@ -32,7 +31,7 @@ export default class User extends Model {
|
|||||||
|
|
||||||
protected init(): void {
|
protected init(): void {
|
||||||
this.setValidation('name').acceptUndefined().between(3, 64);
|
this.setValidation('name').acceptUndefined().between(3, 64);
|
||||||
this.setValidation('main_user_email_id').acceptUndefined().exists(UserEmail, 'id');
|
this.setValidation('main_email_id').acceptUndefined().exists(UserEmail, 'id');
|
||||||
if (User.isApprovalMode()) {
|
if (User.isApprovalMode()) {
|
||||||
this.setValidation('approved').defined();
|
this.setValidation('approved').defined();
|
||||||
}
|
}
|
||||||
|
@ -19,8 +19,8 @@ export default abstract class ModelComponent<T extends Model> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected setValidation<T>(propertyName: keyof this): Validator<T> {
|
protected setValidation<V>(propertyName: keyof this): Validator<V> {
|
||||||
const validator = new Validator<T>();
|
const validator = new Validator<V>();
|
||||||
this._validators[propertyName as string] = validator;
|
this._validators[propertyName as string] = validator;
|
||||||
return validator;
|
return validator;
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,14 @@ import CreateMigrationsTable from "../src/migrations/CreateMigrationsTable";
|
|||||||
import CreateLogsTable from "../src/migrations/CreateLogsTable";
|
import CreateLogsTable from "../src/migrations/CreateLogsTable";
|
||||||
import CreateUsersAndUserEmailsTable from "../src/auth/migrations/CreateUsersAndUserEmailsTable";
|
import CreateUsersAndUserEmailsTable from "../src/auth/migrations/CreateUsersAndUserEmailsTable";
|
||||||
import FixUserMainEmailRelation from "../src/auth/migrations/FixUserMainEmailRelation";
|
import FixUserMainEmailRelation from "../src/auth/migrations/FixUserMainEmailRelation";
|
||||||
|
import DropNameFromUsers from "../src/auth/migrations/DropNameFromUsers";
|
||||||
|
import CreateMagicLinksTable from "../src/auth/migrations/CreateMagicLinksTable";
|
||||||
|
|
||||||
export const MIGRATIONS = [
|
export const MIGRATIONS = [
|
||||||
CreateMigrationsTable,
|
CreateMigrationsTable,
|
||||||
CreateLogsTable,
|
CreateLogsTable,
|
||||||
CreateUsersAndUserEmailsTable,
|
CreateUsersAndUserEmailsTable,
|
||||||
FixUserMainEmailRelation,
|
FixUserMainEmailRelation,
|
||||||
|
DropNameFromUsers,
|
||||||
|
CreateMagicLinksTable,
|
||||||
];
|
];
|
Loading…
Reference in New Issue
Block a user