Add required username to magic link authentication and fix many errors

This commit is contained in:
Alice Gaudon 2020-11-14 17:24:42 +01:00
parent acc5233185
commit b75b227ca1
24 changed files with 208 additions and 122 deletions

View File

@ -46,5 +46,8 @@
view: { view: {
cache: false cache: false
}, },
magic_link: {
validity_period: 20,
},
approval_mode: false, approval_mode: false,
} }

View File

@ -14,5 +14,8 @@
mail: { mail: {
secure: true, secure: true,
allow_invalid_tls: false allow_invalid_tls: false
} },
magic_link: {
validity_period: 900,
},
} }

View File

@ -4,6 +4,6 @@
user: "root", user: "root",
password: "", password: "",
database: "swaf_test", database: "swaf_test",
create_database_automatically: true create_database_automatically: true,
} },
} }

View File

@ -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',

View File

@ -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 {

View File

@ -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];

View File

@ -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";

View File

@ -1,4 +1,4 @@
export default { export default {
LOGIN: 'Login', LOGIN: 'login',
REGISTER: 'Register', REGISTER: 'register',
}; };

View File

@ -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) {
res.redirect(Controller.route('magic_link_lobby', undefined, { if (await pendingLink.isValid()) {
redirect_uri: req.query.redirect_uri?.toString() || pendingLink.original_url || undefined, res.redirect(Controller.route('magic_link_lobby', 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,34 +72,30 @@ 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;
await MagicLinkController.sendMagicLink( await MagicLinkController.sendMagicLink(
this.app, this.app,
req.getSession().id, req.getSession().id,
actionType, actionType,
Controller.route('auth', undefined, { Controller.route('auth', undefined, {
redirect_uri: req.query.redirect_uri?.toString() || undefined,
}),
email,
this.magicLinkMailTemplate,
{
type: actionType,
ip: req.ip,
geo: geo ? `${geo.city}, ${geo.country}` : 'Unknown location',
},
);
res.redirect(Controller.route('magic_link_lobby', undefined, {
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, redirect_uri: req.query.redirect_uri?.toString() || undefined,
})); }),
} email,
this.magicLinkMailTemplate,
{
type: actionType,
ip: req.ip,
geo: geo ? `${geo.city}, ${geo.country}` : 'Unknown location',
},
{
username: req.body.name,
},
);
res.redirect(Controller.route('magic_link_lobby', undefined, {
redirect_uri: req.query.redirect_uri?.toString() || RedirectBackComponent.getPreviousURL(req),
}));
} }
} }

View File

@ -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({

View File

@ -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);
} }
} }

View 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');
}
}

View File

@ -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_-]+$/;

View File

@ -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> {

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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 {

View File

@ -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
View File

@ -0,0 +1,7 @@
{% extends 'layouts/base.njk' %}
{% set title = app.name + ' - Home' %}
{% block body %}
<h1>{{ title }}</h1>
{% endblock %}

View File

@ -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 %}

View File

@ -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>
{{ macros.field(_locals, 'password', 'password', null, 'Your password', 'Do not fill to log in via magic link.') }}
<button type="submit">Authenticate</button>
{{ macros.csrf(getCsrfToken) }}
</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') }}
<a href="/auth" class="button transparent">Go back</a>
<button type="submit" class="primary">Register</button> <button type="submit" class="primary">Register</button>
{{ macros.csrf(getCsrfToken) }}
</form> </form>
{% else %} </section>
<form action="{{ action }}" method="POST" id="login-form"> {% endif %}
<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>
{{ macros.csrf(getCsrfToken) }}
</form>
{% endif %}
</div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -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) %}

View File

@ -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' %}
@ -55,4 +54,4 @@
</script> </script>
{{ macros.websocket(websocketUrl, 'websocketListen', 1, 'isValid') }} {{ macros.websocket(websocketUrl, 'websocketListen', 1, 'isValid') }}
{% endblock %} {% endblock %}