Merge branch 'develop'
This commit is contained in:
commit
0a11792557
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "swaf",
|
||||
"version": "0.23.2",
|
||||
"version": "0.23.3",
|
||||
"description": "Structure Web Application Framework.",
|
||||
"repository": "https://eternae.ink/arisu/swaf",
|
||||
"author": "Alice Gaudon <alice@gaudon.pro>",
|
||||
@ -17,8 +17,8 @@
|
||||
"clean": "(test ! -d dist || rm -r dist)",
|
||||
"prepareSources": "cp package.json src/",
|
||||
"compile": "yarn clean && tsc",
|
||||
"dev": "yarn prepareSources && concurrently -k -n \"Typescript,Node,Webpack,Maildev\" -p \"[{name}]\" -c \"blue,green,red,yellow\" \"tsc --watch\" \"nodemon\" \"maildev\"",
|
||||
"build": "yarn prepareSources && yarn compile && cp -r yarn.lock README.md config/ views/ dist/ && mkdir dist/types && cp src/types/* dist/types/",
|
||||
"dev": "yarn prepareSources && concurrently -k -n \"Typescript,Node,Webpack,Maildev\" -p \"[{name}]\" -c \"blue,green,red,yellow\" \"tsc --watch\" \"nodemon\" \"maildev\"",
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
||||
"release": "yarn build && yarn lint && yarn test && cd dist && yarn publish"
|
||||
},
|
||||
|
@ -11,6 +11,7 @@ import Application from "../Application";
|
||||
import NunjucksComponent from "../components/NunjucksComponent";
|
||||
import AuthMethod from "./AuthMethod";
|
||||
import {Session, SessionData} from "express-session";
|
||||
import UserNameComponent from "./models/UserNameComponent";
|
||||
|
||||
export default class AuthGuard {
|
||||
private readonly authMethods: AuthMethod<AuthProof<User>>[];
|
||||
@ -140,7 +141,9 @@ export default class AuthGuard {
|
||||
|
||||
if (!user.isApproved()) {
|
||||
await new Mail(this.app.as(NunjucksComponent).getEnvironment(), PENDING_ACCOUNT_REVIEW_MAIL_TEMPLATE, {
|
||||
username: (await user.mainEmail.get())?.getOrFail('email'),
|
||||
username: user.asOptional(UserNameComponent)?.name ||
|
||||
(await user.mainEmail.get())?.getOrFail('email') ||
|
||||
'Could not find an identifier',
|
||||
link: config.get<string>('public_url') + Controller.route('accounts-approval'),
|
||||
}).send(config.get<string>('app.contact_email'));
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ export default class AddUsedToMagicLinksMigration extends Migration {
|
||||
public async install(): Promise<void> {
|
||||
await this.query(`ALTER TABLE magic_links
|
||||
ADD COLUMN used BOOLEAN NOT NULL`);
|
||||
await this.query(`DELETE FROM magic_links`);
|
||||
}
|
||||
|
||||
public async rollback(): Promise<void> {
|
||||
|
@ -49,6 +49,7 @@ export default class MagicLinkAuthMethod implements AuthMethod<MagicLink> {
|
||||
public async interruptAuth(req: Request, res: Response): Promise<boolean> {
|
||||
const pendingLink = await MagicLink.select()
|
||||
.where('session_id', req.getSession().id)
|
||||
.where('used', 0)
|
||||
.first();
|
||||
|
||||
if (pendingLink) {
|
||||
|
@ -4,13 +4,24 @@ import User from "../models/User";
|
||||
import UserNameComponent from "../models/UserNameComponent";
|
||||
import MagicLink from "../models/MagicLink";
|
||||
import MagicLinkUserNameComponent from "../models/MagicLinkUserNameComponent";
|
||||
import {nanoid} from "nanoid";
|
||||
|
||||
export default class AddNameToUsersMigration extends Migration {
|
||||
public async install(): Promise<void> {
|
||||
await this.query(`ALTER TABLE users
|
||||
ADD COLUMN name VARCHAR(64) UNIQUE NOT NULL`);
|
||||
ADD COLUMN name VARCHAR(64) NOT NULL`);
|
||||
await this.query(`ALTER TABLE magic_links
|
||||
ADD COLUMN username VARCHAR(64) DEFAULT NULL`);
|
||||
|
||||
// Give every user a random name
|
||||
const users = await User.select().get(this.getCurrentConnection());
|
||||
await Promise.all(users.map(user => {
|
||||
user.name = nanoid();
|
||||
return user.save(this.getCurrentConnection());
|
||||
}));
|
||||
|
||||
await this.query(`ALTER TABLE users
|
||||
ADD CONSTRAINT UNIQUE (name)`);
|
||||
}
|
||||
|
||||
public async rollback(): Promise<void> {
|
||||
|
@ -23,12 +23,21 @@ export default class FormHelperComponent extends ApplicationComponent {
|
||||
|
||||
return _previousFormData;
|
||||
};
|
||||
|
||||
let _formPrefix: string | null;
|
||||
res.locals.getFormPrefix = () => {
|
||||
return _formPrefix;
|
||||
};
|
||||
res.locals.setFormPrefix = (formPrefix: string) => {
|
||||
_formPrefix = formPrefix;
|
||||
return '';
|
||||
};
|
||||
next();
|
||||
});
|
||||
|
||||
router.use((req, res, next) => {
|
||||
if (['GET', 'POST'].find(m => m === req.method)) {
|
||||
if (typeof req.body === 'object') {
|
||||
if (typeof req.body === 'object' && Object.keys(req.body).length > 0) {
|
||||
req.flash('previousFormData', req.body);
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import Application from "../Application";
|
||||
import RedisComponent from "./RedisComponent";
|
||||
import WebSocketListener from "../WebSocketListener";
|
||||
import NunjucksComponent from "./NunjucksComponent";
|
||||
import {Session} from "express-session";
|
||||
|
||||
export default class WebSocketServerComponent extends ApplicationComponent {
|
||||
private wss?: WebSocket.Server;
|
||||
@ -65,7 +66,7 @@ export default class WebSocketServerComponent extends ApplicationComponent {
|
||||
session.id = sid;
|
||||
|
||||
store.createSession(<Request>request, session);
|
||||
listener.handle(socket, request, (<Request>request).session).catch(err => {
|
||||
listener.handle(socket, request, session as Session).catch(err => {
|
||||
logger.error(err, 'Error in websocket listener.');
|
||||
});
|
||||
});
|
||||
|
@ -206,9 +206,7 @@ export default class MysqlConnectionManager {
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const migration of this.migrations) {
|
||||
migration.registerModels?.();
|
||||
}
|
||||
}
|
||||
|
@ -110,7 +110,7 @@ export default class Mail {
|
||||
this.data.app = config.get('app');
|
||||
|
||||
// Log
|
||||
logger.debug('Send mail', this.options);
|
||||
logger.debug('Send mail', this.options, this.data);
|
||||
|
||||
// Render email
|
||||
this.options.html = Mail.parse(this.environment, 'mails/' + this.template.template + '.mjml.njk',
|
||||
|
@ -16,8 +16,9 @@
|
||||
{% endif %}
|
||||
|
||||
<section class="panel">
|
||||
<h2>Log in</h2>
|
||||
<h2><i data-feather="log-in"></i> Log in</h2>
|
||||
|
||||
{{ setFormPrefix('login-') }}
|
||||
<form action="{{ route('login') + queryStr }}" method="POST" id="login-form">
|
||||
{{ macros.field(_locals, 'text', 'identifier', query.identifier or '', 'Your email address or username', null, 'required') }}
|
||||
|
||||
@ -32,8 +33,9 @@
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Register with email</h2>
|
||||
<h2><i data-feather="user-plus"></i> Register</h2>
|
||||
|
||||
{{ setFormPrefix('register-') }}
|
||||
<form action="{{ route('register') + queryStr }}" method="POST" id="register-form">
|
||||
<input type="hidden" name="auth_method" value="magic_link">
|
||||
{{ macros.csrf(getCsrfToken) }}
|
||||
@ -42,38 +44,69 @@
|
||||
{{ macros.field(_locals, 'text', 'name', null, 'Choose your username', 'This cannot be changed later.', 'pattern="[0-9a-z_-]+" required') }}
|
||||
{% endif %}
|
||||
|
||||
<div id="register-magic_link_method_fields">
|
||||
{{ macros.field(_locals, 'email', 'identifier', null, 'Your email address', null, 'required') }}
|
||||
<a href="javascript: void(0);">Use password instead</a>
|
||||
</div>
|
||||
|
||||
<div id="register-password_method_fields" class="hidden">
|
||||
{{ macros.field(_locals, 'password', 'password', null, 'Choose a password', null, 'required disabled') }}
|
||||
{{ macros.field(_locals, 'password', 'password_confirmation', null, 'Confirm your password', null, 'required disabled') }}
|
||||
<a href="javascript: void(0);">Use email address instead</a>
|
||||
</div>
|
||||
|
||||
{{ 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 %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Register form dynamics
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const form = document.getElementById('register-form');
|
||||
const authMethodField = form.querySelector('input[name=auth_method]');
|
||||
const usernameField = form.querySelector('input[name=name]');
|
||||
const magicLinkFields = document.getElementById('register-magic_link_method_fields');
|
||||
const passwordFields = document.getElementById('register-password_method_fields');
|
||||
|
||||
let switchToPassword;
|
||||
magicLinkFields.querySelector('a').addEventListener('click', switchToPassword = () => {
|
||||
authMethodField.value = 'password';
|
||||
usernameField.name = 'identifier';
|
||||
|
||||
magicLinkFields.classList.add('hidden');
|
||||
magicLinkFields.querySelectorAll('input').forEach(el => {
|
||||
el.disabled = true;
|
||||
});
|
||||
|
||||
passwordFields.classList.remove('hidden');
|
||||
passwordFields.querySelectorAll('input').forEach(el => {
|
||||
el.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
passwordFields.querySelector('a').addEventListener('click', () => {
|
||||
authMethodField.value = 'magic_link';
|
||||
usernameField.name = 'name';
|
||||
|
||||
magicLinkFields.classList.remove('hidden');
|
||||
magicLinkFields.querySelectorAll('input').forEach(el => {
|
||||
el.disabled = false;
|
||||
});
|
||||
|
||||
passwordFields.classList.add('hidden');
|
||||
passwordFields.querySelectorAll('input').forEach(el => {
|
||||
el.disabled = true;
|
||||
});
|
||||
});
|
||||
|
||||
if (`{{ _locals.previousFormData()['auth_method'] | default('') }}` === 'password') {
|
||||
switchToPassword();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@ -37,6 +37,7 @@
|
||||
{% set validation = validation[name] if validation[name] or null %}
|
||||
{% set previousFormData = _locals.previousFormData() %}
|
||||
{% set value = previousFormData[name] or value or validation.value or '' %}
|
||||
{% set prefix = _locals.getFormPrefix() | default('no-') %}
|
||||
|
||||
{% if type == 'hidden' %}
|
||||
{% if validation %}
|
||||
@ -63,16 +64,16 @@
|
||||
{% else %}
|
||||
{% set v = (value % 60) if f == 's' else (((value - value % 60) / 60 % 60) if f == 'm' else ((value - value % 3600) / 3600 if f == 'h')) %}
|
||||
{% endif %}
|
||||
<input type="number" name="{{ name }}[{{ f }}]" id="field-{{ name }}-{{ f }}"
|
||||
<input type="number" name="{{ name }}[{{ f }}]" id="field-{{ prefix }}{{ name }}-{{ f }}"
|
||||
value="{{ v }}"
|
||||
min="0" {{ 'max=60' if (f == 's' or f == 'm') }}
|
||||
{{ validation_attributes }}>
|
||||
<label for="field-{{ name }}-{{ f }}">{{ f }}</label>
|
||||
<label for="field-{{ prefix }}{{ name }}-{{ f }}">{{ f }}</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% elseif type == 'select' %}
|
||||
<select name="{{ name }}" id="field-{{ name }}" {{ validation_attributes|safe }}>
|
||||
<select name="{{ name }}" id="field-{{ prefix }}{{ name }}" {{ validation_attributes|safe }}>
|
||||
{% for option in extraData %}
|
||||
<option value="{% if option.display === undefined or option.value !== undefined %}{{ option.value | default(option) }}{% endif %}"
|
||||
{{ 'selected' if value == (option.value | default(option)) }}>{{ option.display | default(option) }}</option>
|
||||
@ -80,16 +81,16 @@
|
||||
</select>
|
||||
<i data-feather="chevron-down"></i>
|
||||
{% elseif type == 'textarea' %}
|
||||
<textarea name="{{ name }}" id="field-{{ name }}"
|
||||
<textarea name="{{ name }}" id="field-{{ prefix }}{{ name }}"
|
||||
{{ validation_attributes|safe }} value="{{ value }}">{{ value }}</textarea>
|
||||
{% else %}
|
||||
<input type="{{ type }}" name="{{ name }}" id="field-{{ name }}"
|
||||
<input type="{{ type }}" name="{{ name }}" id="field-{{ prefix }}{{ name }}"
|
||||
{% if type != 'checkbox' %} value="{{ value }}" {% endif %}
|
||||
{{ 'checked' if (type == 'checkbox' and value == 'on') }}
|
||||
{{ validation_attributes|safe }}>
|
||||
{% endif %}
|
||||
|
||||
<label for="field-{{ name }}{{ '-' + extraData[0] if type == 'duration' }}">{{ placeholder }}</label>
|
||||
<label for="field-{{ prefix }}{{ name }}{{ '-' + extraData[0] if type == 'duration' }}">{{ placeholder }}</label>
|
||||
</div>
|
||||
|
||||
{{ fieldError(_locals, name) }}
|
||||
|
Loading…
Reference in New Issue
Block a user