Merge branch 'develop'
This commit is contained in:
commit
4f885f74d4
@ -1,6 +1,7 @@
|
||||
{
|
||||
"bundles": {
|
||||
"app": "js/app.js",
|
||||
"register": "js/register.js",
|
||||
"layout": "sass/layout.scss",
|
||||
"error": "sass/error.scss",
|
||||
"logo": "img/logo.svg",
|
||||
|
@ -1,5 +1,6 @@
|
||||
import './external_links';
|
||||
import './message_icons';
|
||||
import './forms';
|
||||
|
||||
import '../sass/app.scss';
|
||||
|
||||
|
11
assets/js/forms.js
Normal file
11
assets/js/forms.js
Normal file
@ -0,0 +1,11 @@
|
||||
// For labels to update their state (css selectors based on the value attribute)
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.querySelectorAll('input').forEach(el => {
|
||||
if (el.type !== 'checkbox') {
|
||||
el.setAttribute('value', el.value);
|
||||
el.addEventListener('change', () => {
|
||||
el.setAttribute('value', el.value);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
22
assets/js/register.js
Normal file
22
assets/js/register.js
Normal file
@ -0,0 +1,22 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const createEmailAddress = document.getElementById('field-create_email');
|
||||
const username = document.getElementById('field-username');
|
||||
const domain = document.getElementById('field-domain');
|
||||
const recovery_email = document.getElementById('field-recovery_email');
|
||||
const recovery_email_label = recovery_email.parentElement.querySelector('.hint');
|
||||
|
||||
function updateForm() {
|
||||
if (createEmailAddress.checked) {
|
||||
recovery_email.removeAttribute('required');
|
||||
recovery_email_label.style.display = 'block';
|
||||
username.disabled = domain.disabled = false;
|
||||
} else {
|
||||
recovery_email.setAttribute('required', 'required');
|
||||
recovery_email_label.style.display = 'none';
|
||||
username.disabled = domain.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
createEmailAddress.addEventListener('change', updateForm);
|
||||
updateForm();
|
||||
});
|
@ -179,6 +179,15 @@ form {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
[disabled] {
|
||||
opacity: 0.5;
|
||||
|
||||
& ~ label {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
input, select, .input-group {
|
||||
border: 0;
|
||||
border-bottom: 2px solid #0008;
|
||||
@ -272,7 +281,32 @@ form {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inline-fields {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin: 16px auto;
|
||||
|
||||
.form-field {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
> :not(.form-field) {
|
||||
padding: 32px 8px 8px 8px;
|
||||
}
|
||||
|
||||
+ {
|
||||
.error, .hint {
|
||||
margin-top: -16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-field, .inline-fields + {
|
||||
.error, .hint {
|
||||
padding: 2px;
|
||||
text-align: left;
|
||||
@ -308,6 +342,11 @@ button, .button {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.feather.last {
|
||||
margin-right: 0;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
&, &.primary {
|
||||
color: $primaryForeground;
|
||||
background-color: $secondary;
|
||||
|
@ -1,2 +1,15 @@
|
||||
export default Object.assign(require("wms-core/config/default").default, {
|
||||
mysql: {
|
||||
connectionLimit: 10,
|
||||
host: "localhost",
|
||||
user: "root",
|
||||
password: "",
|
||||
database: "aldap",
|
||||
create_database_automatically: false
|
||||
},
|
||||
redis: {
|
||||
host: "127.0.0.1",
|
||||
port: 6379,
|
||||
prefix: 'aldap'
|
||||
}
|
||||
});
|
@ -1,2 +1,6 @@
|
||||
export default Object.assign(require("wms-core/config/test").default, {
|
||||
mysql: {
|
||||
database: "aldap_test",
|
||||
create_database_automatically: true
|
||||
},
|
||||
});
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "aldap",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"description": "Authentication LDAP server",
|
||||
"repository": "git@gitlab.com:ArisuOngaku/aldap.git",
|
||||
"author": "Alice Gaudon <alice@gaudon.pro>",
|
||||
@ -16,6 +16,7 @@
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.9.0",
|
||||
"@babel/preset-env": "^7.9.5",
|
||||
"@types/argon2": "^0.15.0",
|
||||
"@types/config": "^0.0.36",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/jest": "^25.2.1",
|
||||
@ -41,9 +42,10 @@
|
||||
"uglifyjs-webpack-plugin": "^2.2.0",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-cli": "^3.3.11",
|
||||
"wms-core": "^0.2.0"
|
||||
"wms-core": "^0"
|
||||
},
|
||||
"dependencies": {
|
||||
"argon2": "^0.26.2",
|
||||
"config": "^3.3.1",
|
||||
"express": "^4.17.1"
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
import Application from "wms-core/Application";
|
||||
import {Type} from "wms-core/Utils";
|
||||
import Migration from "wms-core/db/Migration";
|
||||
import CreateMigrationsTable from "wms-core/migrations/CreateMigrationsTable";
|
||||
import CreateLogsTable from "wms-core/migrations/CreateLogsTable";
|
||||
import ExpressAppComponent from "wms-core/components/ExpressAppComponent";
|
||||
import NunjucksComponent from "wms-core/components/NunjucksComponent";
|
||||
import MysqlComponent from "wms-core/components/MysqlComponent";
|
||||
@ -17,8 +15,13 @@ import FormHelperComponent from "wms-core/components/FormHelperComponent";
|
||||
import CsrfProtectionComponent from "wms-core/components/CsrfProtectionComponent";
|
||||
import WebSocketServerComponent from "wms-core/components/WebSocketServerComponent";
|
||||
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 {MIGRATIONS} from "./migrations";
|
||||
|
||||
export default class ExampleApp extends Application {
|
||||
export default class Aldap extends Application {
|
||||
private readonly port: number;
|
||||
|
||||
constructor(port: number) {
|
||||
@ -27,10 +30,7 @@ export default class ExampleApp extends Application {
|
||||
}
|
||||
|
||||
protected getMigrations(): Type<Migration>[] {
|
||||
return [
|
||||
CreateMigrationsTable,
|
||||
CreateLogsTable,
|
||||
];
|
||||
return MIGRATIONS;
|
||||
}
|
||||
|
||||
protected async init(): Promise<void> {
|
||||
@ -74,6 +74,13 @@ export default class ExampleApp extends Application {
|
||||
|
||||
// WebSocket server
|
||||
this.use(new WebSocketServerComponent(this, expressAppComponent, redisComponent));
|
||||
|
||||
// Auth
|
||||
this.use(new AuthComponent(new class extends AuthGuard<PasswordAuthProof> {
|
||||
public async getProofForSession(session: Express.Session): Promise<any | null> {
|
||||
return PasswordAuthProof.getProofForSession(session);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private registerWebSocketListeners() {
|
||||
@ -81,5 +88,6 @@ export default class ExampleApp extends Application {
|
||||
|
||||
private registerControllers() {
|
||||
this.use(new HomeController());
|
||||
this.use(new AuthController());
|
||||
}
|
||||
}
|
94
src/controllers/AuthController.ts
Normal file
94
src/controllers/AuthController.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import Controller from "wms-core/Controller";
|
||||
import {REQUIRE_AUTH_MIDDLEWARE, REQUIRE_GUEST_MIDDLEWARE} from "wms-core/auth/AuthComponent";
|
||||
import {Request, Response} from "express";
|
||||
import Validator from "wms-core/db/Validator";
|
||||
import {EMAIL_REGEX} from "wms-core/db/Model";
|
||||
import {PasswordAuthProof} from "../models/UserPassword";
|
||||
import UserEmail from "wms-core/auth/models/UserEmail";
|
||||
|
||||
export default class AuthController extends Controller {
|
||||
routes(): void {
|
||||
this.get('/login', this.getLogin, 'login', REQUIRE_GUEST_MIDDLEWARE);
|
||||
this.post('/login', this.postLogin, 'login', REQUIRE_GUEST_MIDDLEWARE);
|
||||
this.get('/register', this.getRegister, 'register', REQUIRE_GUEST_MIDDLEWARE);
|
||||
this.post('/register', this.postRegister, 'register', REQUIRE_GUEST_MIDDLEWARE);
|
||||
this.get('/logout', this.getLogout, 'logout', REQUIRE_AUTH_MIDDLEWARE);
|
||||
}
|
||||
|
||||
private async getLogin(req: Request, res: Response): Promise<void> {
|
||||
res.render('login');
|
||||
}
|
||||
|
||||
private async postLogin(req: Request, res: Response): Promise<void> {
|
||||
await this.validate({
|
||||
email: new Validator().defined().regexp(EMAIL_REGEX),
|
||||
password: new Validator().acceptUndefined(),
|
||||
}, req.body);
|
||||
|
||||
const passwordAuthProof = new PasswordAuthProof(req.body.email);
|
||||
const user = await passwordAuthProof.getUser();
|
||||
if (!user) {
|
||||
req.flash('error', 'Unknown email address');
|
||||
res.redirect(Controller.route('login'));
|
||||
return;
|
||||
}
|
||||
|
||||
await passwordAuthProof.authorize(req.session!, req.body.password);
|
||||
await req.authGuard.authenticateOrRegister(req.session!, passwordAuthProof);
|
||||
|
||||
req.flash('success', `Welcome, ${user.name}.`);
|
||||
res.redirect(Controller.route('home'));
|
||||
}
|
||||
|
||||
private async getRegister(req: Request, res: Response): Promise<void> {
|
||||
res.render('register');
|
||||
}
|
||||
|
||||
private async postRegister(req: Request, res: Response): Promise<void> {
|
||||
const validationMap: any = {
|
||||
password: new Validator().defined().minLength(8),
|
||||
password_confirmation: new Validator().defined().sameAs('password', req.body.password),
|
||||
terms: new Validator().defined(),
|
||||
};
|
||||
|
||||
let email: string;
|
||||
if (req.body.create_email) {
|
||||
validationMap['username'] = new Validator().defined().minLength(3).regexp(/^[0-9a-zA-Z_-]+$/);
|
||||
validationMap['domain'] = new Validator().defined().regexp(/^(toot\.party)$/);
|
||||
validationMap['recovery_email'] = new Validator().acceptUndefined().regexp(EMAIL_REGEX).unique(UserEmail, 'email');
|
||||
email = req.body.email = req.body.username + '@' + req.body.domain;
|
||||
validationMap['email'] = new Validator().defined().regexp(EMAIL_REGEX).unique(UserEmail, 'email');
|
||||
} else {
|
||||
validationMap['recovery_email'] = new Validator().defined().regexp(EMAIL_REGEX).unique(UserEmail, 'email');
|
||||
email = req.body.recovery_email;
|
||||
}
|
||||
await this.validate(validationMap, req.body);
|
||||
|
||||
const passwordAuthProof = new PasswordAuthProof(email, false);
|
||||
const userPassword = await passwordAuthProof.register(req.body.password);
|
||||
await passwordAuthProof.authorize(req.session!, req.body.password_confirmation);
|
||||
await req.authGuard.authenticateOrRegister(req.session!, passwordAuthProof, async (connection, userID) => {
|
||||
const callbacks: (() => Promise<void>)[] = [];
|
||||
await userPassword.setUser(userID);
|
||||
await userPassword.save(connection, c => callbacks.push(c));
|
||||
if (req.body.create_email && req.body.recovery_email) {
|
||||
await new UserEmail({
|
||||
user_id: userID,
|
||||
email: req.body.recovery_email,
|
||||
main: false,
|
||||
}).save(connection, c => callbacks.push(c));
|
||||
}
|
||||
return callbacks;
|
||||
});
|
||||
|
||||
const user = (await passwordAuthProof.getUser())!;
|
||||
|
||||
req.flash('success', `Your account was successfully created! Welcome, ${user.name}.`);
|
||||
res.redirect(Controller.route('home'));
|
||||
}
|
||||
|
||||
private async getLogout(req: Request, res: Response): Promise<void> {
|
||||
await req.authGuard.logout(req.session!);
|
||||
res.redirect(Controller.route('home'));
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
import Logger from "wms-core/Logger";
|
||||
import ExampleApp from "./ExampleApp";
|
||||
import Aldap from "./Aldap";
|
||||
|
||||
(async () => {
|
||||
const app = new ExampleApp(4899);
|
||||
const app = new Aldap(4899);
|
||||
await app.start();
|
||||
})().catch(err => {
|
||||
Logger.error(err);
|
||||
|
11
src/migrations.ts
Normal file
11
src/migrations.ts
Normal file
@ -0,0 +1,11 @@
|
||||
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";
|
||||
|
||||
export const MIGRATIONS = [
|
||||
CreateMigrationsTable,
|
||||
CreateLogsTable,
|
||||
CreateUsersAndUserEmailsTable,
|
||||
CreateUserPasswordsTable,
|
||||
];
|
17
src/migrations/CreateUserPasswordsTable.ts
Normal file
17
src/migrations/CreateUserPasswordsTable.ts
Normal file
@ -0,0 +1,17 @@
|
||||
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> {
|
||||
}
|
||||
}
|
116
src/models/UserPassword.ts
Normal file
116
src/models/UserPassword.ts
Normal file
@ -0,0 +1,116 @@
|
||||
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";
|
||||
|
||||
export default class UserPassword extends Model {
|
||||
public static async getByEmail(email: string): Promise<UserPassword | null> {
|
||||
const user = await User.fromEmail(email);
|
||||
if (!user) return null;
|
||||
|
||||
const result = await this.models<UserPassword>(this.select().where('user_id', user.id).first());
|
||||
return result && result.length > 0 ? result[0] : null;
|
||||
}
|
||||
|
||||
private user_id?: number;
|
||||
private password?: string;
|
||||
|
||||
protected defineProperties(): void {
|
||||
this.defineProperty<number>('user_id', new Validator().defined().unique(this).exists(User, 'id'));
|
||||
this.defineProperty<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> {
|
||||
return await User.fromEmail(await this.getEmail());
|
||||
}
|
||||
|
||||
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(session: Express.Session, passwordGuess: string): Promise<boolean> {
|
||||
const password = await this.getUserPassword();
|
||||
if (!password || !await password.verifyPassword(passwordGuess)) return false;
|
||||
|
||||
this.authorized = true;
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
2
views/errors/429.njk
Normal file
2
views/errors/429.njk
Normal file
@ -0,0 +1,2 @@
|
||||
{% extends './error.njk' %}
|
||||
{% import 'macros.njk' as macros %}
|
@ -1,7 +1,7 @@
|
||||
{% extends 'layouts/base.njk' %}
|
||||
|
||||
{% set title = 'Example App - Hello world!' %}
|
||||
{% set title = 'ALDAP - Welcome to the toot.party auth center!' %}
|
||||
|
||||
{% block body %}
|
||||
<h1>Hello world!</h1>
|
||||
<h1>{{ title }}</h1>
|
||||
{% endblock %}
|
@ -12,10 +12,17 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
<a href="/" class="logo"><img src="/img/logo.svg" alt="Logo"> Example app</a>
|
||||
<a href="/" class="logo"><img src="/img/logo.svg" alt="Logo"> ALDAP</a>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="{{ route('about') }}"><i data-feather="info"></i> About</a></li>
|
||||
{% if user %}
|
||||
<li><a href="{{ route('home') }}"><i data-feather="user"></i> {{ user.name }}</a></li>
|
||||
<li><a href="{{ route('logout') }}"><i data-feather="log-out"></i> Logout</a></li>
|
||||
{% else %}
|
||||
<li><a href="{{ route('login') }}"><i data-feather="log-in"></i> Login</a></li>
|
||||
<li><a href="{{ route('register') }}"><i data-feather="user-plus"></i> Register</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
@ -37,4 +44,4 @@
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
{% block footer %}Example app v{{ app_version }} - all rights reserved.{% endblock %}
|
||||
{% block footer %}ALDAP v{{ app_version }} - all rights reserved.{% endblock %}
|
24
views/login.njk
Normal file
24
views/login.njk
Normal file
@ -0,0 +1,24 @@
|
||||
{% extends 'layouts/base.njk' %}
|
||||
|
||||
{% set title = 'Example App - Hello world!' %}
|
||||
|
||||
{% block body %}
|
||||
<h1>Login</h1>
|
||||
|
||||
<div class="container">
|
||||
<div class="panel center">
|
||||
<form action="{{ route('login') }}" method="POST">
|
||||
{{ macros.field(_locals, 'email', 'email', null, 'Your email address', null, 'required') }}
|
||||
{{ macros.field(_locals, 'password', 'password', null, 'Your password', null, 'required') }}
|
||||
|
||||
<button type="submit">Login</button>
|
||||
|
||||
{{ macros.csrf(getCSRFToken) }}
|
||||
</form>
|
||||
|
||||
<p>
|
||||
<a href="{{ route('register') }}">I don't have an account</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -63,7 +63,7 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% elseif type == 'select' %}
|
||||
<select name="{{ name }}" id="field-{{ name }}">
|
||||
<select name="{{ name }}" id="field-{{ name }}" {{ validation_attributes|safe }}>
|
||||
{% for option in extraData %}
|
||||
<option value="{{ option }}" {{ 'selected' if value == option }}>{{ option }}</option>
|
||||
{% endfor %}
|
||||
@ -77,9 +77,7 @@
|
||||
{% endif %}
|
||||
|
||||
<label for="field-{{ name }}{{ '-' + extraData[0] if type == 'duration' }}">{{ placeholder }}</label>
|
||||
{% if validation %}
|
||||
<div class="error"><i data-feather="x-circle"></i> {{ validation.message }}</div>
|
||||
{% endif %}
|
||||
{{ fieldError(_locals, name) }}
|
||||
{% if hint %}
|
||||
<div class="hint"><i data-feather="info"></i> {{ hint }}</div>
|
||||
{% endif %}
|
||||
@ -87,6 +85,14 @@
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro fieldError(_locals, name) %}
|
||||
{% set validation = _locals.validation() %}
|
||||
{% set validation = validation[name] if validation[name] or null %}
|
||||
{% if validation %}
|
||||
<div class="error"><i data-feather="x-circle"></i> {{ validation.message }}</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro websocket(websocketUrl, listener, reconnectOnClose = 1, checkFunction = 0) %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
46
views/register.njk
Normal file
46
views/register.njk
Normal file
@ -0,0 +1,46 @@
|
||||
{% extends 'layouts/base.njk' %}
|
||||
|
||||
{% set title = 'Example App - Hello world!' %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/js/register.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<h1>Register</h1>
|
||||
|
||||
<div class="container">
|
||||
<div class="panel center">
|
||||
<form action="{{ route('register') }}" method="POST">
|
||||
<section class="sub-panel">
|
||||
<h2>Email</h2>
|
||||
{{ macros.field(_locals, 'checkbox', 'create_email', null, 'Create an email address') }}
|
||||
|
||||
<div class="inline-fields">
|
||||
{{ macros.field(_locals, 'text', 'username', null, 'Choose your username', null, 'disabled') }}
|
||||
<span>@</span>
|
||||
{{ macros.field(_locals, 'select', 'domain', null, 'Choose your domain', null, 'disabled', ['toot.party']) }}
|
||||
</div>
|
||||
{{ macros.fieldError(_locals, 'email') }}
|
||||
</section>
|
||||
|
||||
<section class="sub-panel">
|
||||
<h2>Recovery email</h2>
|
||||
{{ macros.field(_locals, 'email', 'recovery_email', null, 'Your email address', 'Optional') }}
|
||||
</section>
|
||||
|
||||
<section class="sub-panel">
|
||||
<h2>Password</h2>
|
||||
{{ 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 terms of services', null, 'required') }}
|
||||
|
||||
<button type="submit">Register</button>
|
||||
|
||||
{{ macros.csrf(getCSRFToken) }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
Loading…
Reference in New Issue
Block a user