Add required username to magic link authentication and fix many errors
This commit is contained in:
parent
acc5233185
commit
b75b227ca1
@ -46,5 +46,8 @@
|
|||||||
view: {
|
view: {
|
||||||
cache: false
|
cache: false
|
||||||
},
|
},
|
||||||
|
magic_link: {
|
||||||
|
validity_period: 20,
|
||||||
|
},
|
||||||
approval_mode: false,
|
approval_mode: false,
|
||||||
}
|
}
|
||||||
|
@ -14,5 +14,8 @@
|
|||||||
mail: {
|
mail: {
|
||||||
secure: true,
|
secure: true,
|
||||||
allow_invalid_tls: false
|
allow_invalid_tls: false
|
||||||
}
|
},
|
||||||
|
magic_link: {
|
||||||
|
validity_period: 900,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,6 @@
|
|||||||
user: "root",
|
user: "root",
|
||||||
password: "",
|
password: "",
|
||||||
database: "swaf_test",
|
database: "swaf_test",
|
||||||
create_database_automatically: true
|
create_database_automatically: true,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import config from "config";
|
import config from "config";
|
||||||
import {MailTemplate} from "./Mail";
|
import {MailTemplate} from "./mail/Mail";
|
||||||
|
|
||||||
export const MAGIC_LINK_MAIL = new MailTemplate(
|
export const MAGIC_LINK_MAIL = new MailTemplate(
|
||||||
'magic_link',
|
'magic_link',
|
||||||
|
@ -23,22 +23,23 @@ import MagicLinkWebSocketListener from "./auth/magic_link/MagicLinkWebSocketList
|
|||||||
import MagicLinkController from "./auth/magic_link/MagicLinkController";
|
import MagicLinkController from "./auth/magic_link/MagicLinkController";
|
||||||
import AddPasswordToUsersMigration from "./auth/password/AddPasswordToUsersMigration";
|
import AddPasswordToUsersMigration from "./auth/password/AddPasswordToUsersMigration";
|
||||||
import AddNameToUsersMigration from "./auth/migrations/AddNameToUsersMigration";
|
import AddNameToUsersMigration from "./auth/migrations/AddNameToUsersMigration";
|
||||||
import packageJson = require('../package.json');
|
|
||||||
import CsrfProtectionComponent from "./components/CsrfProtectionComponent";
|
import CsrfProtectionComponent from "./components/CsrfProtectionComponent";
|
||||||
|
import MailController from "./mail/MailController";
|
||||||
|
import WebSocketServerComponent from "./components/WebSocketServerComponent";
|
||||||
|
import Controller from "./Controller";
|
||||||
|
import packageJson = require('../package.json');
|
||||||
|
|
||||||
export const MIGRATIONS = [
|
export const MIGRATIONS = [
|
||||||
CreateMigrationsTable,
|
CreateMigrationsTable,
|
||||||
CreateUsersAndUserEmailsTableMigration,
|
CreateUsersAndUserEmailsTableMigration,
|
||||||
AddNameToUsersMigration,
|
|
||||||
AddPasswordToUsersMigration,
|
AddPasswordToUsersMigration,
|
||||||
CreateMagicLinksTableMigration,
|
CreateMagicLinksTableMigration,
|
||||||
|
AddNameToUsersMigration,
|
||||||
];
|
];
|
||||||
|
|
||||||
export default class TestApp extends Application {
|
export default class TestApp extends Application {
|
||||||
private readonly addr: string;
|
private readonly addr: string;
|
||||||
private readonly port: number;
|
private readonly port: number;
|
||||||
private expressAppComponent?: ExpressAppComponent;
|
|
||||||
private magicLinkWebSocketListener?: MagicLinkWebSocketListener<this>;
|
|
||||||
|
|
||||||
public constructor(addr: string, port: number) {
|
public constructor(addr: string, port: number) {
|
||||||
super(packageJson.version, true);
|
super(packageJson.version, true);
|
||||||
@ -57,12 +58,8 @@ export default class TestApp extends Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected registerComponents(): void {
|
protected registerComponents(): void {
|
||||||
this.expressAppComponent = new ExpressAppComponent(this.addr, this.port);
|
|
||||||
const redisComponent = new RedisComponent();
|
|
||||||
const mysqlComponent = new MysqlComponent();
|
|
||||||
|
|
||||||
// Base
|
// Base
|
||||||
this.use(this.expressAppComponent);
|
this.use(new ExpressAppComponent(this.addr, this.port));
|
||||||
this.use(new LogRequestsComponent());
|
this.use(new LogRequestsComponent());
|
||||||
|
|
||||||
// Static files
|
// Static files
|
||||||
@ -73,12 +70,12 @@ export default class TestApp extends Application {
|
|||||||
this.use(new RedirectBackComponent());
|
this.use(new RedirectBackComponent());
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
this.use(mysqlComponent);
|
this.use(new MysqlComponent());
|
||||||
this.use(new MailComponent());
|
this.use(new MailComponent());
|
||||||
|
|
||||||
// Session
|
// Session
|
||||||
this.use(redisComponent);
|
this.use(new RedisComponent());
|
||||||
this.use(new SessionComponent(redisComponent));
|
this.use(new SessionComponent(this.as(RedisComponent)));
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
this.use(new FormHelperComponent());
|
this.use(new FormHelperComponent());
|
||||||
@ -88,18 +85,29 @@ export default class TestApp extends Application {
|
|||||||
|
|
||||||
// Auth
|
// Auth
|
||||||
this.use(new AuthComponent(this, new MagicLinkAuthMethod(this, MAGIC_LINK_MAIL), new PasswordAuthMethod(this)));
|
this.use(new AuthComponent(this, new MagicLinkAuthMethod(this, MAGIC_LINK_MAIL), new PasswordAuthMethod(this)));
|
||||||
|
|
||||||
|
// WebSocket server
|
||||||
|
this.use(new WebSocketServerComponent(this, this.as(ExpressAppComponent), this.as(RedisComponent)));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected registerWebSocketListeners(): void {
|
protected registerWebSocketListeners(): void {
|
||||||
this.magicLinkWebSocketListener = new MagicLinkWebSocketListener();
|
this.use(new MagicLinkWebSocketListener());
|
||||||
this.use(this.magicLinkWebSocketListener);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected registerControllers(): void {
|
protected registerControllers(): void {
|
||||||
|
this.use(new MailController());
|
||||||
this.use(new AuthController());
|
this.use(new AuthController());
|
||||||
|
|
||||||
if (!this.magicLinkWebSocketListener) throw new Error('Magic link websocket listener not initialized.');
|
this.use(new MagicLinkController(this.as<MagicLinkWebSocketListener<this>>(MagicLinkWebSocketListener)));
|
||||||
this.use(new MagicLinkController(this.magicLinkWebSocketListener));
|
|
||||||
|
// Special home controller
|
||||||
|
this.use(new class extends Controller {
|
||||||
|
public routes(): void {
|
||||||
|
this.get('/', (req, res) => {
|
||||||
|
res.render('home');
|
||||||
|
}, 'home');
|
||||||
|
}
|
||||||
|
}());
|
||||||
}
|
}
|
||||||
|
|
||||||
public getExpressApp(): Express {
|
public getExpressApp(): Express {
|
||||||
|
@ -2,6 +2,11 @@ import Controller from "../Controller";
|
|||||||
import {NextFunction, Request, Response} from "express";
|
import {NextFunction, Request, Response} from "express";
|
||||||
import AuthComponent, {AuthMiddleware, RequireAuthMiddleware, RequireGuestMiddleware} from "./AuthComponent";
|
import AuthComponent, {AuthMiddleware, RequireAuthMiddleware, RequireGuestMiddleware} from "./AuthComponent";
|
||||||
import {BadRequestError} from "../HttpError";
|
import {BadRequestError} from "../HttpError";
|
||||||
|
import ModelFactory from "../db/ModelFactory";
|
||||||
|
import User from "./models/User";
|
||||||
|
import UserPasswordComponent from "./password/UserPasswordComponent";
|
||||||
|
import UserNameComponent from "./models/UserNameComponent";
|
||||||
|
import {log} from "../Logger";
|
||||||
|
|
||||||
export default class AuthController extends Controller {
|
export default class AuthController extends Controller {
|
||||||
public getRoutesPrefix(): string {
|
public getRoutesPrefix(): string {
|
||||||
@ -9,6 +14,8 @@ export default class AuthController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public routes(): void {
|
public routes(): void {
|
||||||
|
this.post('/logout', this.postLogout, 'logout', RequireAuthMiddleware);
|
||||||
|
|
||||||
this.use(async (req, res, next) => {
|
this.use(async (req, res, next) => {
|
||||||
const authGuard = this.getApp().as(AuthComponent).getAuthGuard();
|
const authGuard = this.getApp().as(AuthComponent).getAuthGuard();
|
||||||
if (await authGuard.interruptAuth(req, res)) return;
|
if (await authGuard.interruptAuth(req, res)) return;
|
||||||
@ -18,14 +25,17 @@ export default class AuthController extends Controller {
|
|||||||
this.get('/', this.getAuth, 'auth', RequireGuestMiddleware);
|
this.get('/', this.getAuth, 'auth', RequireGuestMiddleware);
|
||||||
this.post('/login', this.postLogin, 'login', RequireGuestMiddleware);
|
this.post('/login', this.postLogin, 'login', RequireGuestMiddleware);
|
||||||
this.post('/register', this.postRegister, 'register', RequireGuestMiddleware);
|
this.post('/register', this.postRegister, 'register', RequireGuestMiddleware);
|
||||||
this.post('/logout', this.postLogout, 'logout', RequireAuthMiddleware);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getAuth(req: Request, res: Response, _next: NextFunction): Promise<void> {
|
protected async getAuth(req: Request, res: Response, _next: NextFunction): Promise<void> {
|
||||||
const authGuard = this.getApp().as(AuthComponent).getAuthGuard();
|
const authGuard = this.getApp().as(AuthComponent).getAuthGuard();
|
||||||
|
|
||||||
|
const userModelFactory = ModelFactory.get(User);
|
||||||
|
const hasUsername = userModelFactory.hasComponent(UserNameComponent);
|
||||||
res.render('auth/auth', {
|
res.render('auth/auth', {
|
||||||
auth_methods: authGuard.getAuthMethodNames(),
|
auth_methods: authGuard.getAuthMethodNames(),
|
||||||
|
has_username: hasUsername,
|
||||||
|
register_with_password: hasUsername && userModelFactory.hasComponent(UserPasswordComponent),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,9 +63,10 @@ export default class AuthController extends Controller {
|
|||||||
|
|
||||||
const user = await method.findUserByIdentifier(identifier);
|
const user = await method.findUserByIdentifier(identifier);
|
||||||
if (!user) { // Register
|
if (!user) { // Register
|
||||||
return isRegistration ?
|
if (!isRegistration) return await this.redirectToRegistration(req, res, identifier);
|
||||||
await method.attemptRegister(req, res, identifier) :
|
|
||||||
await this.redirectToRegistration(req, res, identifier);
|
await method.attemptRegister(req, res, identifier);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Login
|
// Login
|
||||||
@ -66,9 +77,10 @@ export default class AuthController extends Controller {
|
|||||||
const methods = await authGuard.getAuthMethodsByIdentifier(identifier);
|
const methods = await authGuard.getAuthMethodsByIdentifier(identifier);
|
||||||
|
|
||||||
if (methods.length === 0) { // Register
|
if (methods.length === 0) { // Register
|
||||||
return isRegistration ?
|
if (!isRegistration) return await this.redirectToRegistration(req, res, identifier);
|
||||||
await authGuard.getRegistrationMethod().attemptRegister(req, res, identifier) :
|
|
||||||
await this.redirectToRegistration(req, res, identifier);
|
await authGuard.getRegistrationMethod().attemptRegister(req, res, identifier);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {user, method} = methods[0];
|
const {user, method} = methods[0];
|
||||||
|
@ -4,7 +4,7 @@ import User from "./models/User";
|
|||||||
import {Connection} from "mysql";
|
import {Connection} from "mysql";
|
||||||
import {Request, Response} from "express";
|
import {Request, Response} from "express";
|
||||||
import {PENDING_ACCOUNT_REVIEW_MAIL_TEMPLATE} from "../Mails";
|
import {PENDING_ACCOUNT_REVIEW_MAIL_TEMPLATE} from "../Mails";
|
||||||
import Mail from "../Mail";
|
import Mail from "../mail/Mail";
|
||||||
import Controller from "../Controller";
|
import Controller from "../Controller";
|
||||||
import config from "config";
|
import config from "config";
|
||||||
import Application from "../Application";
|
import Application from "../Application";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export default {
|
export default {
|
||||||
LOGIN: 'Login',
|
LOGIN: 'login',
|
||||||
REGISTER: 'Register',
|
REGISTER: 'register',
|
||||||
};
|
};
|
||||||
|
@ -9,7 +9,7 @@ import geoip from "geoip-lite";
|
|||||||
import MagicLinkController from "./MagicLinkController";
|
import MagicLinkController from "./MagicLinkController";
|
||||||
import RedirectBackComponent from "../../components/RedirectBackComponent";
|
import RedirectBackComponent from "../../components/RedirectBackComponent";
|
||||||
import Application from "../../Application";
|
import Application from "../../Application";
|
||||||
import {MailTemplate} from "../../Mail";
|
import {MailTemplate} from "../../mail/Mail";
|
||||||
import AuthMagicLinkActionType from "./AuthMagicLinkActionType";
|
import AuthMagicLinkActionType from "./AuthMagicLinkActionType";
|
||||||
|
|
||||||
export default class MagicLinkAuthMethod implements AuthMethod<MagicLink> {
|
export default class MagicLinkAuthMethod implements AuthMethod<MagicLink> {
|
||||||
@ -25,7 +25,7 @@ export default class MagicLinkAuthMethod implements AuthMethod<MagicLink> {
|
|||||||
|
|
||||||
public async findUserByIdentifier(identifier: string): Promise<User | null> {
|
public async findUserByIdentifier(identifier: string): Promise<User | null> {
|
||||||
return (await UserEmail.select()
|
return (await UserEmail.select()
|
||||||
.with('user')
|
.with('user.mainEmail')
|
||||||
.where('email', identifier)
|
.where('email', identifier)
|
||||||
.first())?.user.getOrFail() || null;
|
.first())?.user.getOrFail() || null;
|
||||||
}
|
}
|
||||||
@ -40,15 +40,17 @@ export default class MagicLinkAuthMethod implements AuthMethod<MagicLink> {
|
|||||||
public async interruptAuth(req: Request, res: Response): Promise<boolean> {
|
public async interruptAuth(req: Request, res: Response): Promise<boolean> {
|
||||||
const pendingLink = await MagicLink.select()
|
const pendingLink = await MagicLink.select()
|
||||||
.where('session_id', req.getSession().id)
|
.where('session_id', req.getSession().id)
|
||||||
.where('action_type', [AuthMagicLinkActionType.LOGIN, AuthMagicLinkActionType.REGISTER], WhereTest.IN)
|
|
||||||
.where('authorized', false)
|
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
if (pendingLink && await pendingLink.isValid()) {
|
if (pendingLink) {
|
||||||
|
if (await pendingLink.isValid()) {
|
||||||
res.redirect(Controller.route('magic_link_lobby', undefined, {
|
res.redirect(Controller.route('magic_link_lobby', undefined, {
|
||||||
redirect_uri: req.query.redirect_uri?.toString() || pendingLink.original_url || undefined,
|
redirect_uri: req.query.redirect_uri?.toString() || pendingLink.original_url || undefined,
|
||||||
}));
|
}));
|
||||||
return true;
|
return true;
|
||||||
|
} else {
|
||||||
|
await pendingLink.delete();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@ -70,7 +72,6 @@ export default class MagicLinkAuthMethod implements AuthMethod<MagicLink> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async auth(req: Request, res: Response, isRegistration: boolean, email: string): Promise<void> {
|
private async auth(req: Request, res: Response, isRegistration: boolean, email: string): Promise<void> {
|
||||||
if (!isRegistration || req.body.confirm_register === 'confirm') {
|
|
||||||
const geo = geoip.lookup(req.ip);
|
const geo = geoip.lookup(req.ip);
|
||||||
const actionType = isRegistration ? AuthMagicLinkActionType.REGISTER : AuthMagicLinkActionType.LOGIN;
|
const actionType = isRegistration ? AuthMagicLinkActionType.REGISTER : AuthMagicLinkActionType.LOGIN;
|
||||||
|
|
||||||
@ -88,16 +89,13 @@ export default class MagicLinkAuthMethod implements AuthMethod<MagicLink> {
|
|||||||
ip: req.ip,
|
ip: req.ip,
|
||||||
geo: geo ? `${geo.city}, ${geo.country}` : 'Unknown location',
|
geo: geo ? `${geo.city}, ${geo.country}` : 'Unknown location',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
username: req.body.name,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
res.redirect(Controller.route('magic_link_lobby', undefined, {
|
res.redirect(Controller.route('magic_link_lobby', undefined, {
|
||||||
redirect_uri: req.query.redirect_uri?.toString() || RedirectBackComponent.getPreviousURL(req),
|
redirect_uri: req.query.redirect_uri?.toString() || RedirectBackComponent.getPreviousURL(req),
|
||||||
}));
|
}));
|
||||||
} else {
|
|
||||||
req.flash('register_identifier', email);
|
|
||||||
res.redirect(Controller.route('auth', undefined, {
|
|
||||||
redirect_uri: req.query.redirect_uri?.toString() || undefined,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import {Request, Response} from "express";
|
|||||||
import MagicLinkWebSocketListener from "./MagicLinkWebSocketListener";
|
import MagicLinkWebSocketListener from "./MagicLinkWebSocketListener";
|
||||||
import {BadRequestError, NotFoundHttpError} from "../../HttpError";
|
import {BadRequestError, NotFoundHttpError} from "../../HttpError";
|
||||||
import Throttler from "../../Throttler";
|
import Throttler from "../../Throttler";
|
||||||
import Mail, {MailTemplate} from "../../Mail";
|
import Mail, {MailTemplate} from "../../mail/Mail";
|
||||||
import MagicLink from "../models/MagicLink";
|
import MagicLink from "../models/MagicLink";
|
||||||
import config from "config";
|
import config from "config";
|
||||||
import Application from "../../Application";
|
import Application from "../../Application";
|
||||||
@ -14,6 +14,9 @@ import AuthComponent, {AuthMiddleware} from "../AuthComponent";
|
|||||||
import {AuthError, PendingApprovalAuthError, RegisterCallback} from "../AuthGuard";
|
import {AuthError, PendingApprovalAuthError, RegisterCallback} from "../AuthGuard";
|
||||||
import UserEmail from "../models/UserEmail";
|
import UserEmail from "../models/UserEmail";
|
||||||
import AuthMagicLinkActionType from "./AuthMagicLinkActionType";
|
import AuthMagicLinkActionType from "./AuthMagicLinkActionType";
|
||||||
|
import {QueryVariable} from "../../db/MysqlConnectionManager";
|
||||||
|
import UserNameComponent from "../models/UserNameComponent";
|
||||||
|
import MagicLinkUserNameComponent from "../models/MagicLinkUserNameComponent";
|
||||||
|
|
||||||
export default class MagicLinkController<A extends Application> extends Controller {
|
export default class MagicLinkController<A extends Application> extends Controller {
|
||||||
public static async sendMagicLink(
|
public static async sendMagicLink(
|
||||||
@ -24,15 +27,16 @@ export default class MagicLinkController<A extends Application> extends Controll
|
|||||||
email: string,
|
email: string,
|
||||||
mailTemplate: MailTemplate,
|
mailTemplate: MailTemplate,
|
||||||
data: ParsedUrlQueryInput,
|
data: ParsedUrlQueryInput,
|
||||||
|
magicLinkData: Record<string, QueryVariable> = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
Throttler.throttle('magic_link', 2, MagicLink.validityPeriod(), sessionId, 0, 0);
|
Throttler.throttle('magic_link', 2, MagicLink.validityPeriod(), sessionId, 0, 0);
|
||||||
Throttler.throttle('magic_link', 1, MagicLink.validityPeriod(), email, 0, 0);
|
Throttler.throttle('magic_link', 1, MagicLink.validityPeriod(), email, 0, 0);
|
||||||
|
|
||||||
const link = MagicLink.create({
|
const link = MagicLink.create(Object.assign(magicLinkData, {
|
||||||
session_id: sessionId,
|
session_id: sessionId,
|
||||||
action_type: actionType,
|
action_type: actionType,
|
||||||
original_url: original_url,
|
original_url: original_url,
|
||||||
});
|
}));
|
||||||
|
|
||||||
const token = await link.generateToken(email);
|
const token = await link.generateToken(email);
|
||||||
await link.save();
|
await link.save();
|
||||||
@ -55,7 +59,11 @@ export default class MagicLinkController<A extends Application> extends Controll
|
|||||||
// Auth
|
// Auth
|
||||||
try {
|
try {
|
||||||
return await req.as(AuthMiddleware).getAuthGuard().authenticateOrRegister(
|
return await req.as(AuthMiddleware).getAuthGuard().authenticateOrRegister(
|
||||||
session, magicLink, undefined, undefined, async (connection, user) => {
|
session, magicLink, undefined, async (connection, user) => {
|
||||||
|
const userNameComponent = user.asOptional(UserNameComponent);
|
||||||
|
if (userNameComponent) userNameComponent.name = magicLink.as(MagicLinkUserNameComponent).username;
|
||||||
|
return [];
|
||||||
|
}, async (connection, user) => {
|
||||||
const callbacks: RegisterCallback[] = [];
|
const callbacks: RegisterCallback[] = [];
|
||||||
|
|
||||||
const userEmail = UserEmail.create({
|
const userEmail = UserEmail.create({
|
||||||
|
@ -1,19 +1,25 @@
|
|||||||
import Migration from "../../db/Migration";
|
import Migration from "../../db/Migration";
|
||||||
import ModelFactory from "../../db/ModelFactory";
|
import ModelFactory from "../../db/ModelFactory";
|
||||||
import User from "../models/User";
|
import User from "../models/User";
|
||||||
import UserNameComponent from "../UserNameComponent";
|
import UserNameComponent from "../models/UserNameComponent";
|
||||||
|
import MagicLink from "../models/MagicLink";
|
||||||
|
import MagicLinkUserNameComponent from "../models/MagicLinkUserNameComponent";
|
||||||
|
|
||||||
export default class AddNameToUsersMigration extends Migration {
|
export default class AddNameToUsersMigration extends Migration {
|
||||||
public async install(): Promise<void> {
|
public async install(): Promise<void> {
|
||||||
await this.query(`ALTER TABLE users
|
await this.query(`ALTER TABLE users
|
||||||
ADD COLUMN name VARCHAR(64) UNIQUE NOT NULL`);
|
ADD COLUMN name VARCHAR(64) UNIQUE NOT NULL`);
|
||||||
|
await this.query(`ALTER TABLE magic_links
|
||||||
|
ADD COLUMN username VARCHAR(64) DEFAULT NULL`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async rollback(): Promise<void> {
|
public async rollback(): Promise<void> {
|
||||||
await this.query('ALTER TABLE users DROP COLUMN name');
|
await this.query('ALTER TABLE users DROP COLUMN name');
|
||||||
|
await this.query('ALTER TABLE magic_links DROP COLUMN username');
|
||||||
}
|
}
|
||||||
|
|
||||||
public registerModels(): void {
|
public registerModels(): void {
|
||||||
ModelFactory.get(User).addComponent(UserNameComponent);
|
ModelFactory.get(User).addComponent(UserNameComponent);
|
||||||
|
ModelFactory.get(MagicLink).addComponent(MagicLinkUserNameComponent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
12
src/auth/models/MagicLinkUserNameComponent.ts
Normal file
12
src/auth/models/MagicLinkUserNameComponent.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import ModelComponent from "../../db/ModelComponent";
|
||||||
|
import MagicLink from "./MagicLink";
|
||||||
|
import {USERNAME_REGEXP} from "./UserNameComponent";
|
||||||
|
import User from "./User";
|
||||||
|
|
||||||
|
export default class MagicLinkUserNameComponent extends ModelComponent<MagicLink> {
|
||||||
|
public readonly username?: string = undefined;
|
||||||
|
|
||||||
|
protected init(): void {
|
||||||
|
this.setValidation('name').defined().between(3, 64).regexp(USERNAME_REGEXP).unique(User, 'name');
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import ModelComponent from "../db/ModelComponent";
|
import ModelComponent from "../../db/ModelComponent";
|
||||||
import User from "./models/User";
|
import User from "../models/User";
|
||||||
|
|
||||||
export const USERNAME_REGEXP = /^[0-9a-z_-]+$/;
|
export const USERNAME_REGEXP = /^[0-9a-z_-]+$/;
|
||||||
|
|
@ -6,7 +6,7 @@ import UserPasswordComponent from "./UserPasswordComponent";
|
|||||||
export default class AddPasswordToUsersMigration extends Migration {
|
export default class AddPasswordToUsersMigration extends Migration {
|
||||||
public async install(): Promise<void> {
|
public async install(): Promise<void> {
|
||||||
await this.query(`ALTER TABLE users
|
await this.query(`ALTER TABLE users
|
||||||
ADD COLUMN password VARCHAR(128) NOT NULL`);
|
ADD COLUMN password VARCHAR(128) DEFAULT NULL`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async rollback(): Promise<void> {
|
public async rollback(): Promise<void> {
|
||||||
|
@ -10,7 +10,7 @@ import {AuthError, PendingApprovalAuthError, RegisterCallback} from "../AuthGuar
|
|||||||
import Validator, {InvalidFormatValidationError, ValidationBag} from "../../db/Validator";
|
import Validator, {InvalidFormatValidationError, ValidationBag} from "../../db/Validator";
|
||||||
import Controller from "../../Controller";
|
import Controller from "../../Controller";
|
||||||
import UserPasswordComponent from "./UserPasswordComponent";
|
import UserPasswordComponent from "./UserPasswordComponent";
|
||||||
import UserNameComponent, {USERNAME_REGEXP} from "../UserNameComponent";
|
import UserNameComponent, {USERNAME_REGEXP} from "../models/UserNameComponent";
|
||||||
import ModelFactory from "../../db/ModelFactory";
|
import ModelFactory from "../../db/ModelFactory";
|
||||||
import {WhereOperator, WhereTest} from "../../db/ModelQuery";
|
import {WhereOperator, WhereTest} from "../../db/ModelQuery";
|
||||||
import {ServerError} from "../../HttpError";
|
import {ServerError} from "../../HttpError";
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import ApplicationComponent from "../ApplicationComponent";
|
import ApplicationComponent from "../ApplicationComponent";
|
||||||
import {Express} from "express";
|
import {Express} from "express";
|
||||||
import Mail from "../Mail";
|
import Mail from "../mail/Mail";
|
||||||
import config from "config";
|
import config from "config";
|
||||||
import SecurityError from "../SecurityError";
|
import SecurityError from "../SecurityError";
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import Controller from "../Controller";
|
|||||||
import User from "../auth/models/User";
|
import User from "../auth/models/User";
|
||||||
import {Request, Response} from "express";
|
import {Request, Response} from "express";
|
||||||
import {BadRequestError, NotFoundHttpError} from "../HttpError";
|
import {BadRequestError, NotFoundHttpError} from "../HttpError";
|
||||||
import Mail from "../Mail";
|
import Mail from "../mail/Mail";
|
||||||
import {ACCOUNT_REVIEW_NOTICE_MAIL_TEMPLATE} from "../Mails";
|
import {ACCOUNT_REVIEW_NOTICE_MAIL_TEMPLATE} from "../Mails";
|
||||||
import UserEmail from "../auth/models/UserEmail";
|
import UserEmail from "../auth/models/UserEmail";
|
||||||
import UserApprovedComponent from "../auth/models/UserApprovedComponent";
|
import UserApprovedComponent from "../auth/models/UserApprovedComponent";
|
||||||
|
@ -3,10 +3,10 @@ import config from "config";
|
|||||||
import {Options} from "nodemailer/lib/mailer";
|
import {Options} from "nodemailer/lib/mailer";
|
||||||
import {Environment} from 'nunjucks';
|
import {Environment} from 'nunjucks';
|
||||||
import * as util from "util";
|
import * as util from "util";
|
||||||
import {WrappingError} from "./Utils";
|
import {WrappingError} from "../Utils";
|
||||||
import mjml2html from "mjml";
|
import mjml2html from "mjml";
|
||||||
import {log} from "./Logger";
|
import {log} from "../Logger";
|
||||||
import Controller from "./Controller";
|
import Controller from "../Controller";
|
||||||
import {ParsedUrlQueryInput} from "querystring";
|
import {ParsedUrlQueryInput} from "querystring";
|
||||||
|
|
||||||
export default class Mail {
|
export default class Mail {
|
@ -1,6 +1,6 @@
|
|||||||
import {Request, Response} from "express";
|
import {Request, Response} from "express";
|
||||||
import Controller from "../Controller";
|
import Controller from "../Controller";
|
||||||
import Mail from "../Mail";
|
import Mail from "./Mail";
|
||||||
import NunjucksComponent from "../components/NunjucksComponent";
|
import NunjucksComponent from "../components/NunjucksComponent";
|
||||||
|
|
||||||
export default class MailController extends Controller {
|
export default class MailController extends Controller {
|
7
test/views/home.njk
Normal file
7
test/views/home.njk
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{% extends 'layouts/base.njk' %}
|
||||||
|
|
||||||
|
{% set title = app.name + ' - Home' %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<h1>{{ title }}</h1>
|
||||||
|
{% endblock %}
|
@ -1,4 +1,5 @@
|
|||||||
{% extends 'layouts/barebone.njk' %}
|
{% extends 'layouts/barebone.njk' %}
|
||||||
|
{% import 'macros.njk' as macros %}
|
||||||
|
|
||||||
{% block _stylesheets %}
|
{% block _stylesheets %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
@ -16,14 +17,18 @@
|
|||||||
<button id="menu-button"><i data-feather="menu"></i></button>
|
<button id="menu-button"><i data-feather="menu"></i></button>
|
||||||
<ul id="main-menu">
|
<ul id="main-menu">
|
||||||
{% if user %}
|
{% if user %}
|
||||||
<li class="dropdown"><a href="{{ route('widgets') }}"><i data-feather="zap"></i> <span class="tip">Widgets</span></a></li>
|
|
||||||
<div class="separator"></div>
|
|
||||||
<li><a href="{{ route('account') }}"><i data-feather="user"></i> <span class="tip">{{ user.name }} - Account</span></a></li>
|
|
||||||
|
|
||||||
{% if user.is_admin %}
|
{% if user.is_admin %}
|
||||||
<li><a href="{{ route('early-access-backend') }}"><i data-feather="sliders"></i> <span class="tip">Backend</span></a></li>
|
<li><a href="{{ route('backend') }}"><i data-feather="settings"></i> <span class="tip">Backend</span></a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><a href="{{ route('logout') }}"><i data-feather="log-out"></i> <span class="tip">Log out</span></a></li>
|
|
||||||
|
{# <li><a href="{{ route('account') }}"><i data-feather="user"></i>#}
|
||||||
|
{# <span class="tip">{{ user.name }}</span></a></li>#}
|
||||||
|
<li>
|
||||||
|
<form action="{{ route('logout') }}" method="POST">
|
||||||
|
<button><i data-feather="log-out"></i> <span class="tip">Logout</span></button>
|
||||||
|
{{ macros.csrf(getCsrfToken) }}
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li><a href="{{ route('auth') }}"><i data-feather="log-in"></i> Log in / Register</a></li>
|
<li><a href="{{ route('auth') }}"><i data-feather="log-in"></i> Log in / Register</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -7,42 +7,68 @@
|
|||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="panel">
|
|
||||||
{% set queryStr = '' %}
|
{% set queryStr = '' %}
|
||||||
{% if query.redirect_uri | length %}
|
{% if query.redirect_uri | length %}
|
||||||
{% set queryStr = '?' + querystring.stringify({redirect_uri: query.redirect_uri}) %}
|
{% set queryStr = '?' + querystring.stringify({redirect_uri: query.redirect_uri}) %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% set action = route('auth') + queryStr %}
|
|
||||||
|
|
||||||
{% if register_confirm_email %}
|
<section class="panel">
|
||||||
<form action="{{ action }}" method="POST" id="register-form">
|
<h2>Log in</h2>
|
||||||
<h2>Register</h2>
|
|
||||||
{{ macros.message('question', 'Do you wish to create a new account with ' + register_confirm_email + '?', false, false) }}
|
|
||||||
{{ macros.message('warning', 'If you already have an account, please log in with your existing email first and then add your new email in the Account page.', false, true) }}
|
|
||||||
<input type="hidden" name="email" value="{{ register_confirm_email }}">
|
|
||||||
<input type="hidden" name="confirm_register" value="confirm">
|
|
||||||
|
|
||||||
<div class="form-field">
|
<form action="{{ route('login') + queryStr }}" method="POST" id="login-form">
|
||||||
<div class="form-display">Email: {{ register_confirm_email }}</div>
|
{{ macros.field(_locals, 'text', 'identifier', query.identifier or '', 'Your email address or username', null, 'required') }}
|
||||||
</div>
|
|
||||||
|
|
||||||
<a href="/auth" class="button transparent">Go back</a>
|
{{ macros.field(_locals, 'password', 'password', null, 'Your password', 'Do not fill to log in via magic link.') }}
|
||||||
<button type="submit" class="primary">Register</button>
|
|
||||||
|
|
||||||
{{ macros.csrf(getCsrfToken) }}
|
|
||||||
</form>
|
|
||||||
{% else %}
|
|
||||||
<form action="{{ action }}" method="POST" id="login-form">
|
|
||||||
<h2>Log in or register</h2>
|
|
||||||
{# {{ macros.message('info', 'If we don\'t find your email address in our database, you will be able to register.', false, true) }} #}
|
|
||||||
<div class="input-field">
|
|
||||||
{{ macros.field(_locals, 'email', 'email', query.email or '', 'Your email address', "If we don't find your email address in our database, you will be able to register.", 'required') }}
|
|
||||||
</div>
|
|
||||||
<button type="submit">Authenticate</button>
|
<button type="submit">Authenticate</button>
|
||||||
|
|
||||||
{{ macros.csrf(getCsrfToken) }}
|
{{ macros.csrf(getCsrfToken) }}
|
||||||
</form>
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<h2>Register with email</h2>
|
||||||
|
|
||||||
|
<form action="{{ route('register') + queryStr }}" method="POST" id="register-form">
|
||||||
|
<input type="hidden" name="auth_method" value="magic_link">
|
||||||
|
{{ macros.csrf(getCsrfToken) }}
|
||||||
|
|
||||||
|
{% if has_username %}
|
||||||
|
{{ macros.field(_locals, 'text', 'name', null, 'Choose your username', 'This cannot be changed later.', 'pattern="[0-9a-z_-]+" required') }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{{ macros.field(_locals, 'email', 'identifier', null, 'Your email address', null, 'required') }}
|
||||||
|
|
||||||
|
{{ macros.field(_locals, 'checkbox', 'terms', null, 'I accept the <a href="/terms-of-services" target="_blank">Terms Of Services</a>.' | safe, null, 'required') }}
|
||||||
|
|
||||||
|
<button type="submit" class="primary">Register</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{% if register_with_password %}
|
||||||
|
<section class="panel">
|
||||||
|
<h2>Register with password</h2>
|
||||||
|
|
||||||
|
<form action="{{ route('register') + queryStr }}" method="POST" id="register-form">
|
||||||
|
<input type="hidden" name="auth_method" value="password">
|
||||||
|
{{ macros.csrf(getCsrfToken) }}
|
||||||
|
|
||||||
|
<section class="sub-panel">
|
||||||
|
<h3>Username</h3>
|
||||||
|
{{ macros.field(_locals, 'text', 'identifier', null, 'Choose your username', 'This cannot be changed later.', 'pattern="[0-9a-z_-]+" required') }}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="sub-panel">
|
||||||
|
<h3>Password</h3>
|
||||||
|
{{ macros.field(_locals, 'password', 'password', null, 'Choose a password', null, 'required') }}
|
||||||
|
{{ macros.field(_locals, 'password', 'password_confirmation', null, 'Confirm your password', null, 'required') }}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{{ macros.field(_locals, 'checkbox', 'terms', null, 'I accept the <a href="/terms-of-services" target="_blank">Terms Of Services</a>.' | safe, null, 'required') }}
|
||||||
|
|
||||||
|
<button type="submit" class="primary">Register</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
{% extends 'layouts/base.njk' %}
|
{% extends 'layouts/base.njk' %}
|
||||||
{% import 'macros.njk' as macros %}
|
|
||||||
|
|
||||||
{% set actionType = magicLink.action_type %}
|
{% set actionType = magicLink.action_type %}
|
||||||
{% set title = app.name + 'Magic Link' + (' - ' + actionType if actionType) %}
|
{% set title = app.name + 'Magic Link' + (' - ' + actionType if actionType) %}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
{% extends 'layouts/base.njk' %}
|
{% extends 'layouts/base.njk' %}
|
||||||
{% import 'macros.njk' as macros %}
|
|
||||||
|
|
||||||
{% set title = 'Authentication lobby' %}
|
{% set title = 'Authentication lobby' %}
|
||||||
{% set h1 = 'Authentication lobby' %}
|
{% set h1 = 'Authentication lobby' %}
|
||||||
|
Loading…
Reference in New Issue
Block a user