Turn UserPassword and Username into a User model component
This commit is contained in:
parent
3b8dee7fdf
commit
933d8f1cbb
28
src/App.ts
28
src/App.ts
|
@ -17,19 +17,25 @@ import HomeController from "./controllers/HomeController";
|
|||
import AuthController from "./controllers/AuthController";
|
||||
import AuthComponent from "wms-core/auth/AuthComponent";
|
||||
import AuthGuard from "wms-core/auth/AuthGuard";
|
||||
import {PasswordAuthProof} from "./models/UserPassword";
|
||||
import {PasswordAuthProof} from "./models/UserPasswordComponent";
|
||||
import LDAPServerComponent from "./LDAPServerComponent";
|
||||
import AutoUpdateComponent from "wms-core/components/AutoUpdateComponent";
|
||||
import AccountController from "./controllers/AccountController";
|
||||
import CreateMigrationsTable from "wms-core/migrations/CreateMigrationsTable";
|
||||
import CreateLogsTable from "wms-core/migrations/CreateLogsTable";
|
||||
import CreateUsersAndUserEmailsTable from "wms-core/auth/migrations/CreateUsersAndUserEmailsTable";
|
||||
import CreateUserPasswordsTable from "./migrations/CreateUserPasswordsTable";
|
||||
import CreateUsernamesTable from "./migrations/CreateUsernamesTable";
|
||||
import AddPasswordToUsers from "./migrations/AddPasswordToUsers";
|
||||
import CreateUsernamesTable from "./migrations/AddNameToUsers";
|
||||
import CreateMagicLinksTable from "wms-core/auth/migrations/CreateMagicLinksTable";
|
||||
import MailController from "wms-core/auth/MailController";
|
||||
import MagicLinkController from "./controllers/MagicLinkController";
|
||||
import MagicLinkWebSocketListener from "wms-core/auth/magic_link/MagicLinkWebSocketListener";
|
||||
import BackendController from "wms-core/helpers/BackendController";
|
||||
import AddApprovedFieldToUsersTable from "wms-core/auth/migrations/AddApprovedFieldToUsersTable";
|
||||
import FixUserMainEmailRelation from "wms-core/auth/migrations/FixUserMainEmailRelation";
|
||||
import DropNameFromUsers from "wms-core/auth/migrations/DropNameFromUsers";
|
||||
import MagicLink from "wms-core/auth/models/MagicLink";
|
||||
import AddNameToUsers from "./migrations/AddNameToUsers";
|
||||
|
||||
export default class App extends Application {
|
||||
private readonly port: number;
|
||||
|
@ -45,9 +51,13 @@ export default class App extends Application {
|
|||
CreateMigrationsTable,
|
||||
CreateLogsTable,
|
||||
CreateUsersAndUserEmailsTable,
|
||||
CreateUserPasswordsTable,
|
||||
AddPasswordToUsers,
|
||||
CreateUsernamesTable,
|
||||
CreateMagicLinksTable,
|
||||
AddApprovedFieldToUsersTable,
|
||||
FixUserMainEmailRelation,
|
||||
DropNameFromUsers,
|
||||
AddNameToUsers,
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -91,9 +101,10 @@ export default class App extends Application {
|
|||
this.use(new CsrfProtectionComponent());
|
||||
|
||||
// Auth
|
||||
this.use(new AuthComponent(new class extends AuthGuard<PasswordAuthProof> {
|
||||
public async getProofForSession(session: Express.Session): Promise<any | null> {
|
||||
return PasswordAuthProof.getProofForSession(session);
|
||||
this.use(new AuthComponent(new class extends AuthGuard<PasswordAuthProof | MagicLink> {
|
||||
public async getProofForSession(session: Express.Session): Promise<PasswordAuthProof | MagicLink | null> {
|
||||
return PasswordAuthProof.getProofForSession(session) ||
|
||||
MagicLink.bySessionID(session.id);
|
||||
}
|
||||
}));
|
||||
|
||||
|
@ -111,9 +122,10 @@ export default class App extends Application {
|
|||
|
||||
private registerControllers() {
|
||||
// Priority routes / interrupting middlewares
|
||||
this.use(new AuthController());
|
||||
this.use(new AccountController());
|
||||
this.use(new MagicLinkController(this.magicLinkWebSocketListener!))
|
||||
this.use(new BackendController());
|
||||
this.use(new AuthController());
|
||||
|
||||
// Core functionality
|
||||
this.use(new MailController());
|
||||
|
|
|
@ -3,7 +3,7 @@ import {Express} from "express";
|
|||
import ldap, {InvalidCredentialsError, Server} from "ldapjs";
|
||||
import Logger from "wms-core/Logger";
|
||||
import Username from "./models/Username";
|
||||
import {PasswordAuthProof} from "./models/UserPassword";
|
||||
import {PasswordAuthProof} from "./models/UserPasswordComponent";
|
||||
import Throttler from "wms-core/Throttler";
|
||||
|
||||
export default class LDAPServerComponent extends ApplicationComponent<void> {
|
||||
|
|
20
src/migrations/AddNameToUsers.ts
Normal file
20
src/migrations/AddNameToUsers.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import Migration from "wms-core/db/Migration";
|
||||
import ModelFactory from "wms-core/db/ModelFactory";
|
||||
import User from "wms-core/auth/models/User";
|
||||
import UserNameComponent from "../models/UserNameComponent";
|
||||
import {Connection} from "mysql";
|
||||
|
||||
export default class CreateUserPasswordsTable extends Migration {
|
||||
public async install(connection: Connection): Promise<void> {
|
||||
await this.query(`ALTER TABLE users
|
||||
ADD COLUMN name VARCHAR(64) UNIQUE NOT NULL`, connection);
|
||||
}
|
||||
|
||||
public async rollback(connection: Connection): Promise<void> {
|
||||
await this.query('ALTER TABLE users DROP COLUMN name', connection);
|
||||
}
|
||||
|
||||
public registerModels(): void {
|
||||
ModelFactory.get(User).addComponent(UserNameComponent);
|
||||
}
|
||||
}
|
21
src/migrations/AddPasswordToUsers.ts
Normal file
21
src/migrations/AddPasswordToUsers.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import Migration from "wms-core/db/Migration";
|
||||
import {Connection} from "mysql";
|
||||
import ModelFactory from "wms-core/db/ModelFactory";
|
||||
import User from "wms-core/auth/models/User";
|
||||
import UserPasswordComponent from "../models/UserPasswordComponent";
|
||||
|
||||
export default class AddPasswordToUsers extends Migration {
|
||||
public async install(connection: Connection): Promise<void> {
|
||||
await this.query(`ALTER TABLE users
|
||||
ADD COLUMN password VARCHAR(128) NOT NULL`, connection);
|
||||
}
|
||||
|
||||
public async rollback(connection: Connection): Promise<void> {
|
||||
await this.query(`ALTER TABLE users
|
||||
DROP COLUMN password`, connection);
|
||||
}
|
||||
|
||||
public registerModels(): void {
|
||||
ModelFactory.get(User).addComponent(UserPasswordComponent);
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import Migration from "wms-core/db/Migration";
|
||||
import {query} from "wms-core/db/MysqlConnectionManager";
|
||||
|
||||
export default class CreateUserPasswordsTable extends Migration {
|
||||
public async install(): Promise<void> {
|
||||
await query('CREATE TABLE user_passwords(' +
|
||||
'id INT NOT NULL AUTO_INCREMENT,' +
|
||||
'user_id INT NOT NULL,' +
|
||||
'password VARCHAR(256) NOT NULL,' +
|
||||
'PRIMARY KEY(id),' +
|
||||
'FOREIGN KEY user_pwd_fk (user_id) REFERENCES users (id) ON DELETE CASCADE' +
|
||||
')');
|
||||
}
|
||||
|
||||
public async rollback(): Promise<void> {
|
||||
await query('DROP TABLE user_passwords');
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import Migration from "wms-core/db/Migration";
|
||||
import {query} from "wms-core/db/MysqlConnectionManager";
|
||||
|
||||
export default class CreateUserPasswordsTable extends Migration {
|
||||
public async install(): Promise<void> {
|
||||
await query('CREATE TABLE usernames(' +
|
||||
'id INT NOT NULL AUTO_INCREMENT,' +
|
||||
'user_id INT UNIQUE NOT NULL,' +
|
||||
'username VARCHAR(64) UNIQUE NOT NULL,' +
|
||||
'PRIMARY KEY(id),' +
|
||||
'FOREIGN KEY user_name_fk (user_id) REFERENCES users (id) ON DELETE CASCADE' +
|
||||
')');
|
||||
}
|
||||
|
||||
public async rollback(): Promise<void> {
|
||||
await query('DROP TABLE usernames');
|
||||
}
|
||||
}
|
13
src/models/UserNameComponent.ts
Normal file
13
src/models/UserNameComponent.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import User from "wms-core/auth/models/User";
|
||||
import ModelComponent from "wms-core/db/ModelComponent";
|
||||
|
||||
export const USERNAME_REGEXP = /^[0-9a-z_-]+$/;
|
||||
|
||||
export default class UserNameComponent extends ModelComponent<User> {
|
||||
public name!: string;
|
||||
|
||||
public init(): void {
|
||||
this.setValidation('username').defined().between(3, 64).regexp(USERNAME_REGEXP).unique(this._model);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,126 +0,0 @@
|
|||
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";
|
||||
import UserEmail from "wms-core/auth/models/UserEmail";
|
||||
|
||||
export default class UserPassword extends Model {
|
||||
public static async getByEmail(email: string): Promise<UserPassword | null> {
|
||||
const userEmail = await UserEmail.select('user_id')
|
||||
.where('email', email)
|
||||
.first();
|
||||
return userEmail ? await UserPassword.select().where('user_id', userEmail.user_id).first() : null;
|
||||
}
|
||||
|
||||
private user_id!: number;
|
||||
private password!: string;
|
||||
|
||||
public constructor(props: any) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
protected init(): void {
|
||||
this.addProperty<number>('user_id', new Validator().defined().unique(this).exists(User, 'id'));
|
||||
this.addProperty<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> {
|
||||
const userEmail = await UserEmail.select()
|
||||
.where('email', await this.getEmail())
|
||||
.with('user')
|
||||
.first();
|
||||
return userEmail ? await userEmail.user.get() : null;
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
103
src/models/UserPasswordComponent.ts
Normal file
103
src/models/UserPasswordComponent.ts
Normal file
|
@ -0,0 +1,103 @@
|
|||
import Validator from "wms-core/db/Validator";
|
||||
import User from "wms-core/auth/models/User";
|
||||
import argon2, {argon2id} from "argon2";
|
||||
import AuthProof from "wms-core/auth/AuthProof";
|
||||
import ModelComponent from "wms-core/db/ModelComponent";
|
||||
|
||||
export default class UserPasswordComponent extends ModelComponent<User> {
|
||||
private password?: string = undefined;
|
||||
|
||||
public constructor(props: any) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
public init(): void {
|
||||
this.setValidation('password').acceptUndefined().maxLength(128);
|
||||
}
|
||||
|
||||
public async setPassword(rawPassword: string): Promise<void> {
|
||||
await new Validator<string>().defined().minLength(8).maxLength(512)
|
||||
.execute('password', rawPassword, true);
|
||||
this.password = await argon2.hash(rawPassword, {
|
||||
timeCost: 10,
|
||||
memoryCost: 65536,
|
||||
parallelism: 4,
|
||||
type: argon2id,
|
||||
hashLength: 32,
|
||||
});
|
||||
}
|
||||
|
||||
public async verifyPassword(passwordGuess: string): Promise<boolean> {
|
||||
if (!this.password) return false;
|
||||
|
||||
return await argon2.verify(this.password, passwordGuess);
|
||||
}
|
||||
}
|
||||
|
||||
export class PasswordAuthProof implements AuthProof<User> {
|
||||
public static getProofForSession(session: Express.Session): PasswordAuthProof | null {
|
||||
return session.auth_password_proof ? new PasswordAuthProof(session) : null;
|
||||
}
|
||||
|
||||
public static createAuthorizedProofForRegistration(session: Express.Session): PasswordAuthProof {
|
||||
const proofForSession = new PasswordAuthProof(session);
|
||||
proofForSession.authorized = true;
|
||||
proofForSession.save();
|
||||
return proofForSession;
|
||||
}
|
||||
|
||||
private readonly session: Express.Session;
|
||||
private userID: number | null;
|
||||
private authorized: boolean;
|
||||
private userPassword: UserPasswordComponent | null = null;
|
||||
|
||||
public constructor(session: Express.Session) {
|
||||
this.session = session;
|
||||
this.authorized = session.auth_password_proof.authorized || false;
|
||||
this.userID = session.auth_password_proof.userID || null;
|
||||
}
|
||||
|
||||
public async getResource(): Promise<User | null> {
|
||||
return await User.getById(this.userID);
|
||||
}
|
||||
|
||||
public setResource(user: User) {
|
||||
this.userID = user.id!;
|
||||
this.save();
|
||||
}
|
||||
|
||||
public async isAuthorized(): Promise<boolean> {
|
||||
return this.authorized;
|
||||
}
|
||||
|
||||
public async isValid(): Promise<boolean> {
|
||||
return await this.getUserPassword() !== null;
|
||||
}
|
||||
|
||||
public async revoke(): Promise<void> {
|
||||
this.session.auth_password_proof = undefined;
|
||||
}
|
||||
|
||||
private async getUserPassword(): Promise<UserPasswordComponent | null> {
|
||||
if (!this.userPassword) {
|
||||
this.userPassword = (await User.getById(this.userID))?.as(UserPasswordComponent) || null;
|
||||
}
|
||||
return this.userPassword;
|
||||
}
|
||||
|
||||
public async authorize(passwordGuess: string): Promise<boolean> {
|
||||
const password = await this.getUserPassword();
|
||||
if (!password || !await password.verifyPassword(passwordGuess)) return false;
|
||||
|
||||
this.authorized = true;
|
||||
this.save();
|
||||
return true;
|
||||
}
|
||||
|
||||
private save() {
|
||||
this.session.auth_password_proof = {
|
||||
authorized: this.authorized,
|
||||
userID: this.userID,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
import Model from "wms-core/db/Model";
|
||||
import Validator from "wms-core/db/Validator";
|
||||
import User from "wms-core/auth/models/User";
|
||||
import {OneModelRelation} from "wms-core/db/ModelRelation";
|
||||
|
||||
export const USERNAME_REGEXP = /^[0-9a-z_-]+$/;
|
||||
|
||||
export default class Username extends Model {
|
||||
public static async fromUser(userID: number): Promise<Username | null> {
|
||||
return await this.select().where('user_id', userID).first();
|
||||
}
|
||||
|
||||
public static async getUserFromUsername(username: string): Promise<User | null> {
|
||||
let model = await this.select()
|
||||
.with('user')
|
||||
.where('username', username.toLowerCase())
|
||||
.first();
|
||||
return model ? await model.user.get() : null;
|
||||
}
|
||||
|
||||
private readonly user_id!: number;
|
||||
public readonly user: OneModelRelation<this, User> = new OneModelRelation<this, User>(this, User, {
|
||||
localKey: 'user_id',
|
||||
foreignKey: 'id'
|
||||
});
|
||||
public username!: string;
|
||||
|
||||
|
||||
constructor(data: any) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
protected init(): void {
|
||||
this.addProperty<number>('user_id', new Validator().defined().exists(User, 'id').unique(this));
|
||||
this.addProperty<number>('username', new Validator().defined().between(3, 64).regexp(USERNAME_REGEXP).unique(this));
|
||||
}
|
||||
|
||||
}
|
12
yarn.lock
12
yarn.lock
|
@ -8067,9 +8067,9 @@ regenerator-runtime@^0.11.0:
|
|||
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
|
||||
|
||||
regenerator-runtime@^0.13.4:
|
||||
version "0.13.5"
|
||||
resolved "https://registry.toot.party/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697"
|
||||
integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==
|
||||
version "0.13.7"
|
||||
resolved "https://registry.toot.party/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
|
||||
integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==
|
||||
|
||||
regenerator-transform@^0.14.2:
|
||||
version "0.14.5"
|
||||
|
@ -9891,9 +9891,9 @@ widest-line@^3.1.0:
|
|||
string-width "^4.0.0"
|
||||
|
||||
wms-core@^0:
|
||||
version "0.17.2"
|
||||
resolved "https://registry.toot.party/wms-core/-/wms-core-0.17.2.tgz#3a18695e23b1c8314d023d74865a38cfd6aaf3cb"
|
||||
integrity sha512-oXPAviK7QpbllLuUb0qz/iIXBAXkuwQRkX5grNsw5pwpdi2SFgrtAHqagcW9TY1DuvmyobJ6qV9vbW4kSbveiw==
|
||||
version "0.19.5"
|
||||
resolved "https://registry.toot.party/wms-core/-/wms-core-0.19.5.tgz#e366f449a6c9c5bdbd2fe8aa8e653cdffe821af7"
|
||||
integrity sha512-ftXCHxbZlBQpLjXO1xpabX5+lcYYgw3stX/vPQuR1r0XLmFG5kn3U3QhlYAWx51dyOqqWDidP8bJ5EHBT9SByQ==
|
||||
dependencies:
|
||||
argon2 "^0.26.2"
|
||||
compression "^1.7.4"
|
||||
|
|
Loading…
Reference in New Issue
Block a user