Merge branch 'develop'
This commit is contained in:
commit
cd3dd454aa
@ -4,8 +4,8 @@
|
||||
app: {
|
||||
listen_addr: '127.0.0.1',
|
||||
port: 4899,
|
||||
public_url: "http://localhost:4899",
|
||||
public_websocket_url: "ws://localhost:4899",
|
||||
public_url: "http://127.0.0.1:4899",
|
||||
public_websocket_url: "ws://127.0.0.1:4899",
|
||||
name: 'Example App',
|
||||
contact_email: 'contact@example.net',
|
||||
display_email_warning: true,
|
||||
@ -36,7 +36,7 @@
|
||||
},
|
||||
mysql: {
|
||||
connectionLimit: 10,
|
||||
host: "localhost",
|
||||
host: "127.0.0.1",
|
||||
user: "root",
|
||||
password: "",
|
||||
database: "swaf",
|
||||
|
@ -3,7 +3,7 @@
|
||||
approval_mode: true,
|
||||
},
|
||||
mysql: {
|
||||
host: "localhost",
|
||||
host: "127.0.0.1",
|
||||
user: "root",
|
||||
password: "",
|
||||
database: "swaf_test",
|
||||
|
@ -16,6 +16,5 @@ module.exports = {
|
||||
'**/test/**/*.test.ts'
|
||||
],
|
||||
testEnvironment: 'node',
|
||||
testTimeout: 60000,
|
||||
resolver: "jest-ts-webcompat-resolver",
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "swaf",
|
||||
"version": "0.24.8",
|
||||
"version": "0.24.9",
|
||||
"description": "Structure Web Application Framework.",
|
||||
"repository": "https://eternae.ink/ashpie/swaf",
|
||||
"author": "Alice Gaudon <alice@gaudon.pro>",
|
||||
@ -19,7 +19,7 @@
|
||||
"compile": "yarn clean && yarn prepare-sources && tsc --build",
|
||||
"build": "yarn compile && node . pre-compile-views && node scripts/dist.js",
|
||||
"build-production": "NODE_ENV=production yarn build",
|
||||
"dev": "yarn compile && concurrently -k -n \"Maildev,Typescript,ViewPreCompile,Node\" -p \"[{name}]\" -c \"yellow,blue,red,green\" \"maildev\" \"tsc --build --watch --preserveWatchOutput\" \"nodemon -i public -i intermediates -- pre-compile-views --watch\" \"nodemon -i public -i intermediates\"",
|
||||
"dev": "yarn compile && concurrently -k -n \"Maildev,Typescript,ViewPreCompile,Node\" -p \"[{name}]\" -c \"yellow,blue,red,green\" \"maildev --ip 127.0.0.1\" \"tsc --build --watch --preserveWatchOutput\" \"nodemon -i public -i intermediates -- pre-compile-views --watch\" \"nodemon -i public -i intermediates\"",
|
||||
"lint": "eslint .",
|
||||
"release": "yarn build && yarn lint && yarn test && cd dist && yarn publish"
|
||||
},
|
||||
@ -38,6 +38,7 @@
|
||||
"@types/jest": "^27.0.2",
|
||||
"@types/mjml": "^4.0.4",
|
||||
"@types/mysql": "^2.15.10",
|
||||
"@types/node": "^16.11.10",
|
||||
"@types/nodemailer": "^6.4.0",
|
||||
"@types/nunjucks": "^3.1.3",
|
||||
"@types/on-finished": "^2.3.1",
|
||||
|
@ -21,6 +21,8 @@ import SecurityError from "./SecurityError.js";
|
||||
import {doesFileExist, Type} from "./Utils.js";
|
||||
import WebSocketListener from "./WebSocketListener.js";
|
||||
import TemplateError = nunjucks.lib.TemplateError;
|
||||
import AppLocalsCoreComponents from "./components/core/AppLocalsCoreComponents.js";
|
||||
import LazyLocalsCoreComponent from "./components/core/LazyLocalsCoreComponent.js";
|
||||
|
||||
export default abstract class Application implements Extendable<ApplicationComponent | WebSocketListener<Application>> {
|
||||
private readonly version: string;
|
||||
@ -98,6 +100,8 @@ export default abstract class Application implements Extendable<ApplicationCompo
|
||||
MysqlConnectionManager.registerMigrations(this.getMigrations());
|
||||
|
||||
// Register and initialize all components and alike
|
||||
this.use(new AppLocalsCoreComponents());
|
||||
this.use(new LazyLocalsCoreComponent());
|
||||
await this.init();
|
||||
for (const component of this.components) {
|
||||
await component.init?.();
|
||||
@ -194,9 +198,9 @@ export default abstract class Application implements Extendable<ApplicationCompo
|
||||
error_instructions: httpError.instructions,
|
||||
error_id: errorId,
|
||||
};
|
||||
res.render('errors/' + httpError.errorCode, locals, (err: Error | undefined, html) => {
|
||||
res.formatViewData('errors/' + httpError.errorCode, locals, (err: Error | undefined, html) => {
|
||||
if (err) {
|
||||
res.render('errors/Error', locals);
|
||||
res.formatViewData('templates/ErrorTemplate', locals);
|
||||
} else {
|
||||
res.send(html);
|
||||
}
|
||||
@ -247,6 +251,7 @@ export default abstract class Application implements Extendable<ApplicationCompo
|
||||
|
||||
protected async processCommandLine(): Promise<boolean> {
|
||||
const args = process.argv;
|
||||
|
||||
// Flags
|
||||
const flags = {
|
||||
verbose: false,
|
||||
@ -272,9 +277,13 @@ export default abstract class Application implements Extendable<ApplicationCompo
|
||||
else throw new Error(`Only one main command can be used at once (${mainCommand},${args[i]})`);
|
||||
break;
|
||||
default:
|
||||
if (mainCommand) mainCommandArgs.push(args[i]);
|
||||
else logger.fatal('Unrecognized argument', args[i]);
|
||||
return true;
|
||||
if (mainCommand) {
|
||||
mainCommandArgs.push(args[i]);
|
||||
} else {
|
||||
logger.fatal('Unrecognized argument', args[i]);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,16 +1,16 @@
|
||||
import {NextFunction, Request, Response} from "express";
|
||||
import IncomingForm from "formidable/Formidable.js";
|
||||
import formidable, {Options} from "formidable";
|
||||
|
||||
import {FileError, ValidationBag} from "./db/Validator.js";
|
||||
import Middleware from "./Middleware.js";
|
||||
|
||||
export default abstract class FileUploadMiddleware extends Middleware {
|
||||
protected abstract makeForm(): IncomingForm;
|
||||
protected abstract getFormidableOptions(): Options;
|
||||
|
||||
protected abstract getDefaultField(): string;
|
||||
|
||||
public async handle(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
const form = this.makeForm();
|
||||
const form = formidable(this.getFormidableOptions());
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
form.parse(req, (err, fields, files) => {
|
||||
|
@ -148,10 +148,10 @@ export default class TestApp extends Application {
|
||||
this.use(new class extends Controller {
|
||||
public routes(): void {
|
||||
this.get('/', (req, res) => {
|
||||
res.render('home');
|
||||
res.formatViewData('home');
|
||||
}, 'home');
|
||||
this.get('/tests', (req, res) => {
|
||||
res.render('tests');
|
||||
res.formatViewData('tests');
|
||||
}, 'tests');
|
||||
this.get('/design', (req, res) => {
|
||||
req.flash('success', 'Success.');
|
||||
@ -159,7 +159,7 @@ export default class TestApp extends Application {
|
||||
req.flash('warning', 'Warning.');
|
||||
req.flash('error', 'Error.');
|
||||
req.flash('error-alert', 'Error alert.');
|
||||
res.render('design');
|
||||
res.formatViewData('design');
|
||||
}, 'design');
|
||||
}
|
||||
}());
|
||||
|
25
src/assets/ts/datetime-local.ts
Normal file
25
src/assets/ts/datetime-local.ts
Normal file
@ -0,0 +1,25 @@
|
||||
export function dateToDatetimeLocal(date: Date): string {
|
||||
function ten(i: number) {
|
||||
return (i < 10 ? '0' : '') + i;
|
||||
}
|
||||
const YYYY = date.getFullYear();
|
||||
const MM = ten(date.getMonth() + 1);
|
||||
const DD = ten(date.getDate());
|
||||
const HH = ten(date.getHours());
|
||||
const II = ten(date.getMinutes());
|
||||
const SS = ten(date.getSeconds());
|
||||
return YYYY + '-' + MM + '-' + DD + 'T' +
|
||||
HH + ':' + II + ':' + SS;
|
||||
}
|
||||
|
||||
export const dateToIsoString = (function (BST) {
|
||||
// BST should not be present as UTC time
|
||||
if (new Date(BST).toISOString().slice(0, 16) === BST) {
|
||||
return (date: Date): string => {
|
||||
return new Date(date.getTime() + date.getTimezoneOffset() * 60000)
|
||||
.toISOString();
|
||||
};
|
||||
} else {
|
||||
return (date: Date) => date.toISOString();
|
||||
}
|
||||
}('2006-06-06T06:06'));
|
@ -6,11 +6,11 @@
|
||||
import Icon from "../utils/Icon.svelte";
|
||||
import {hasRoute, route} from "../../../common/Routing";
|
||||
|
||||
let registerUsingMagicLink = $locals.previousFormData()?.['auth_method'] !== 'password';
|
||||
let registerUsingMagicLink = $locals.previousFormData?.['auth_method'] !== 'password';
|
||||
let loginUsingMagicLink = true;
|
||||
|
||||
let queryStr = '';
|
||||
let previousUrl = $locals.getPreviousUrl();
|
||||
let previousUrl: string = $locals.previousUrl as string | undefined;
|
||||
if ($locals.query?.redirect_uri) {
|
||||
queryStr = '?' + new URLSearchParams({redirect_uri: $locals.query?.redirect_uri}).toString();
|
||||
} else if (previousUrl) {
|
||||
|
@ -2,7 +2,7 @@
|
||||
import {locals} from "../../ts/stores.js";
|
||||
import Message from "./Message.svelte";
|
||||
|
||||
export let flashed = $locals.flash();
|
||||
export let flashed = $locals.flash;
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
69
src/assets/views/components/Loader.svelte
Normal file
69
src/assets/views/components/Loader.svelte
Normal file
@ -0,0 +1,69 @@
|
||||
<script>
|
||||
import {fade} from 'svelte/transition';
|
||||
|
||||
export let show = true;
|
||||
export let size = '72px';
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.loader {
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
background: var(--background-color);
|
||||
|
||||
.parts {
|
||||
--size: 72px;
|
||||
position: relative;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
|
||||
.bg-circle {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.fg-arc {
|
||||
border-top: 8px solid transparent;
|
||||
border-left: 8px solid transparent;
|
||||
border-bottom: 8px solid transparent;
|
||||
|
||||
animation: infinite linear 2s spin;
|
||||
}
|
||||
|
||||
> * {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
border: 8px solid #fff;
|
||||
border-radius: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
{#if show}
|
||||
<div class="loader" style="--size: {size};" transition:fade={{duration: 50}}>
|
||||
<div class="parts">
|
||||
<div class="bg-circle"></div>
|
||||
<div class="fg-arc"></div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
@ -19,11 +19,15 @@
|
||||
line-height: 1;
|
||||
|
||||
@include medium-le {
|
||||
margin-top: 8px;
|
||||
&:not(:first-child) {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@include large-ge {
|
||||
margin-left: 8px;
|
||||
&:not(:first-child) {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
|
@ -14,7 +14,7 @@
|
||||
|
||||
let countdown;
|
||||
let validUntilDate = new Date(validUntil);
|
||||
$: countdown = $locals.isPreRender ? '...' : Time.humanizeTimeTo(validUntilDate);
|
||||
$: countdown = $locals.isSsr ? '...' : Time.humanizeTimeTo(validUntilDate);
|
||||
|
||||
onMount(() => {
|
||||
const interval = setInterval(() => {
|
||||
|
@ -8,6 +8,12 @@
|
||||
export let h1: string = title;
|
||||
export let description: string;
|
||||
export let refresh_after: number | undefined = undefined;
|
||||
export let noHeader: boolean = false;
|
||||
export let noH1: boolean = false;
|
||||
export let noFooter: boolean = false;
|
||||
|
||||
export let noLogoLabel = false;
|
||||
export let noLoginLink = false;
|
||||
</script>
|
||||
|
||||
<CommonScripts/>
|
||||
@ -46,14 +52,16 @@
|
||||
<link rel="stylesheet" href="/css/layout.css">
|
||||
</svelte:head>
|
||||
|
||||
<BaseHeader/>
|
||||
{#if !noHeader}
|
||||
<BaseHeader {noLogoLabel} {noLoginLink}/>
|
||||
{/if}
|
||||
|
||||
<div class="flash-messages">
|
||||
<FlashMessages/>
|
||||
</div>
|
||||
|
||||
<main>
|
||||
{#if h1}
|
||||
{#if h1 && !noH1}
|
||||
<h1>{h1}</h1>
|
||||
{/if}
|
||||
{#if $$slots.subtitle}
|
||||
@ -64,4 +72,6 @@
|
||||
<slot/>
|
||||
</main>
|
||||
|
||||
<BaseFooter/>
|
||||
{#if !noFooter}
|
||||
<BaseFooter/>
|
||||
{/if}
|
||||
|
@ -4,7 +4,7 @@
|
||||
import Icon from "../utils/Icon.svelte";
|
||||
import CommonScripts from "./CommonScripts.svelte";
|
||||
|
||||
const previousUrl = $locals.getPreviousUrl();
|
||||
const previousUrl = $locals.previousUrl;
|
||||
|
||||
export let code;
|
||||
code = $locals.error_code || code;
|
||||
|
@ -9,4 +9,4 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<footer>{$locals.app.name} v{$locals.app_version} - all rights reserved.</footer>
|
||||
<footer>{$locals.app.name} v{$locals.app.version} - all rights reserved.</footer>
|
||||
|
@ -3,6 +3,9 @@
|
||||
import NavMenu from "../../components/NavMenu.svelte";
|
||||
import BaseNavMenuLinks from "./BaseNavMenuLinks.svelte";
|
||||
import BaseNavMenuAuth from "./BaseNavMenuAuth.svelte";
|
||||
|
||||
export let noLoginLink = false;
|
||||
export let noLogoLabel = false;
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@ -35,9 +38,9 @@
|
||||
</style>
|
||||
|
||||
<header>
|
||||
<BaseHeaderLogo/>
|
||||
<BaseHeaderLogo noLabel={noLogoLabel}/>
|
||||
<NavMenu>
|
||||
<BaseNavMenuLinks/>
|
||||
<BaseNavMenuAuth/>
|
||||
<BaseNavMenuAuth {noLoginLink}/>
|
||||
</NavMenu>
|
||||
</header>
|
||||
|
@ -1,5 +1,7 @@
|
||||
<script>
|
||||
import {locals} from "../../../ts/stores.js";
|
||||
|
||||
export let noLabel = false;
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@ -23,4 +25,9 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<a href="/" class="logo"><img src="/img/logo.svg" alt="{$locals.app.name} logo"> {$locals.app.name}</a>
|
||||
<a href="/" class="logo">
|
||||
<img src="/img/logo.svg" alt="{$locals.app.name} logo">
|
||||
{#if !noLabel}
|
||||
<span class="label">{$locals.app.name}</span>
|
||||
{/if}
|
||||
</a>
|
||||
|
@ -2,6 +2,8 @@
|
||||
import {locals} from "../../../ts/stores.js";
|
||||
import NavMenuItem from "../../components/NavMenuItem.svelte";
|
||||
import {hasRoute, route} from "../../../../common/Routing";
|
||||
|
||||
export let noLoginLink = false;
|
||||
</script>
|
||||
|
||||
{#if hasRoute('auth')}
|
||||
@ -12,7 +14,7 @@
|
||||
|
||||
<NavMenuItem href={route('account')} icon="user" text={$locals.user.name || 'Account'}/>
|
||||
<NavMenuItem href={route('logout')} icon="log-out" text="Logout" action/>
|
||||
{:else}
|
||||
{:else if !noLoginLink}
|
||||
<NavMenuItem href={route('auth')} icon="log-in" text="Log in / Register"/>
|
||||
{/if}
|
||||
{/if}
|
||||
|
@ -12,13 +12,7 @@
|
||||
import * as stores from '/js/stores.js';
|
||||
const localStore = stores.l;
|
||||
|
||||
const localMap = %locals%;
|
||||
localStore.set((key, args) => {
|
||||
return localMap[args ?
|
||||
`'${key}', \`${args}\``
|
||||
: `'${key}'`];
|
||||
});
|
||||
|
||||
localStore.set(%locals%);
|
||||
setRoutes(%routes%);
|
||||
setPublicUrl(`%publicUrl%`);
|
||||
|
||||
|
@ -3,4 +3,4 @@
|
||||
import Field from "./Field.svelte";
|
||||
</script>
|
||||
|
||||
<Field type="hidden" name="csrf" value={$locals.getCsrfToken()}/>
|
||||
<Field type="hidden" name="csrf" value={$locals.csrfToken}/>
|
||||
|
@ -4,23 +4,29 @@
|
||||
import Message from "../components/Message.svelte";
|
||||
import Icon from "./Icon.svelte";
|
||||
import {getContext} from "svelte";
|
||||
import {dateToDatetimeLocal, dateToIsoString} from "../../ts/datetime-local.js";
|
||||
|
||||
export let type: string;
|
||||
export let name: string;
|
||||
type FieldValue = string | number | Record<string, FieldValue>;
|
||||
export let value: FieldValue | undefined = undefined;
|
||||
export let initialValue: FieldValue | undefined = undefined;
|
||||
export let placeholder: string | undefined = undefined;
|
||||
export let hint: string | undefined = undefined;
|
||||
export let extraData: string[] | undefined = undefined;
|
||||
export let icon: string | undefined = undefined;
|
||||
export let validation: { message: string, value?: string } | undefined = $locals.validation()?.[name];
|
||||
export let validation = $locals.validation?.[name] as { message: string, value?: string } | undefined;
|
||||
|
||||
const formId = getContext('formId');
|
||||
const fieldId = `${formId}-${name}-field`;
|
||||
|
||||
const previousFormData = $locals.previousFormData() || [];
|
||||
const previousFormData = $locals.previousFormData as FieldValue | undefined || [];
|
||||
let previousFieldData = previousFormData[name];
|
||||
if (typeof value === 'number' && previousFieldData) previousFieldData = Number(previousFieldData);
|
||||
|
||||
value = type !== 'hidden' && previousFormData[name] || value || validation?.value || '';
|
||||
value = type !== 'hidden' && previousFieldData || value || initialValue || validation?.value || '';
|
||||
|
||||
$: initialDatetimeLocalValue = type === 'datetime-local' && typeof value === 'string' ? dateToDatetimeLocal(new Date(value)) : undefined;
|
||||
|
||||
function durationValue(f: string): number {
|
||||
if (previousFormData[name]) {
|
||||
@ -61,6 +67,10 @@
|
||||
if (this.type === 'file') {
|
||||
handleFileInput();
|
||||
}
|
||||
|
||||
if (this.type === 'datetime-local') {
|
||||
value = dateToIsoString(new Date(this.value));
|
||||
}
|
||||
}
|
||||
|
||||
let input: HTMLInputElement;
|
||||
@ -398,10 +408,10 @@
|
||||
{/each}
|
||||
</fieldset>
|
||||
{:else if type === 'select'}
|
||||
<select name={name} id={fieldId} {...$$restProps} bind:this={input} on:input={handleInput}>
|
||||
<select name={name} id={fieldId} {...$$restProps} bind:this={input} bind:value={value} on:input={handleInput}>
|
||||
{#each extraData as option}
|
||||
<option value={(option.display === undefined || option.value !== undefined) && (option.value || option)}
|
||||
selected={value === (option.value || option)}>{option.display || option}</option>
|
||||
>{option.display || option}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<Icon name="chevron-down"/>
|
||||
@ -414,6 +424,9 @@
|
||||
</div>
|
||||
{:else if type === 'checkbox'}
|
||||
<input {type} {name} id={fieldId} checked={value === 'on'} {...$$restProps} bind:this={input}>
|
||||
{:else if type === 'datetime-local'}
|
||||
<input {type} bind:this={input} on:input={handleInput} value={initialDatetimeLocalValue}>
|
||||
<input type="hidden" {name} {value}>
|
||||
{:else}
|
||||
<input {type} {name} id={fieldId} {value} {...$$restProps} bind:this={input} on:input={handleInput}
|
||||
tabindex={type === 'file' ? '-1' : undefined}>
|
||||
|
@ -14,6 +14,7 @@
|
||||
export let isBoldSubmit: boolean = true;
|
||||
export let resetButton: boolean = false;
|
||||
export let confirm: string = undefined;
|
||||
export let withFiles: boolean = false;
|
||||
|
||||
const formId = nextAvailableFormId++;
|
||||
setContext('formId', formId);
|
||||
@ -37,7 +38,7 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<form {action} method="POST" id="{formId}-form" on:submit={handleSubmit}>
|
||||
<form {action} method="POST" id="{formId}-form" on:submit={handleSubmit} enctype={withFiles ? 'multipart/form-data' : undefined}>
|
||||
<CsrfTokenField/>
|
||||
<slot/>
|
||||
<div class="form-controls">
|
||||
|
@ -57,7 +57,7 @@ export default class AccountController extends Controller {
|
||||
const nameChangedAt = nameComponent?.getNameChangedAt()?.getTime() || Date.now();
|
||||
const nameChangeRemainingTime = new Date(nameChangedAt + nameChangeWaitPeriod);
|
||||
|
||||
res.render('auth/account/account', {
|
||||
res.formatViewData('auth/account/account', {
|
||||
user_personal_info_fields: user.getPersonalInfoFields(),
|
||||
main_email: await user.mainEmail.get(),
|
||||
emails: await user.emails.get(),
|
||||
|
@ -35,7 +35,7 @@ export default class AuthController extends Controller {
|
||||
|
||||
const userModelFactory = ModelFactory.get(User);
|
||||
const hasUsername = userModelFactory.hasComponent(UserNameComponent);
|
||||
res.render('auth/auth', {
|
||||
res.formatViewData('auth/auth', {
|
||||
auth_methods: authGuard.getAuthMethodNames(),
|
||||
has_username: hasUsername,
|
||||
register_with_password: hasUsername && userModelFactory.hasComponent(UserPasswordComponent),
|
||||
|
@ -147,7 +147,7 @@ export default class MagicLinkController<A extends Application> extends Controll
|
||||
return;
|
||||
}
|
||||
|
||||
res.render('magic_link_lobby', {
|
||||
res.formatViewData('magic_link_lobby', {
|
||||
email: validLink.getOrFail('email'),
|
||||
type: validLink.getOrFail('action_type'),
|
||||
validUntil: validLink.getExpirationDate().getTime(),
|
||||
@ -179,7 +179,7 @@ export default class MagicLinkController<A extends Application> extends Controll
|
||||
}
|
||||
}
|
||||
|
||||
res.render('magic_link', {
|
||||
res.formatViewData('magic_link', {
|
||||
magicLink: magicLink,
|
||||
err: err,
|
||||
success: success && err === null,
|
||||
|
@ -1,4 +1,4 @@
|
||||
export type RouteParams = { [p: string]: string | number } | string[] | string | number;
|
||||
export type RouteParams = { [p: string]: string | number | undefined } | string[] | string | number;
|
||||
export type QueryParamsRecord = Record<string, string | number | boolean | null | undefined>;
|
||||
export type QueryParams = string[][] | QueryParamsRecord | string | URLSearchParams;
|
||||
|
||||
@ -43,7 +43,10 @@ export function route(
|
||||
path = path.replace(/\/+/g, '/');
|
||||
} else {
|
||||
for (const key of Object.keys(params)) {
|
||||
path = path.replace(getRouteParamRegExp(key), params[key].toString());
|
||||
const paramValue = params[key];
|
||||
if (paramValue) {
|
||||
path = path.replace(getRouteParamRegExp(key), paramValue.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,26 +5,10 @@ import {Session, SessionData} from "express-session";
|
||||
import ApplicationComponent from "../ApplicationComponent.js";
|
||||
import {AuthMiddleware} from "../auth/AuthComponent.js";
|
||||
import {BadRequestError} from "../HttpError.js";
|
||||
import FrontendToolsComponent from "./FrontendToolsComponent.js";
|
||||
|
||||
export default class CsrfProtectionComponent extends ApplicationComponent {
|
||||
private static readonly excluders: ((req: Request) => boolean)[] = [];
|
||||
|
||||
public static getCsrfToken(session: Session & Partial<SessionData>): string {
|
||||
if (typeof session.csrf !== 'string') {
|
||||
session.csrf = crypto.randomBytes(64).toString('base64');
|
||||
}
|
||||
return session.csrf;
|
||||
}
|
||||
|
||||
|
||||
public async init(): Promise<void> {
|
||||
const globals = this.getApp().asOptional(FrontendToolsComponent)?.getGlobals();
|
||||
if (globals) {
|
||||
globals.set('getCsrfToken', () => null);
|
||||
}
|
||||
}
|
||||
|
||||
public static addExcluder(excluder: (req: Request) => boolean): void {
|
||||
this.excluders.push(excluder);
|
||||
}
|
||||
@ -36,9 +20,7 @@ export default class CsrfProtectionComponent extends ApplicationComponent {
|
||||
}
|
||||
|
||||
const session = req.getSession();
|
||||
res.locals.getCsrfToken = () => {
|
||||
return CsrfProtectionComponent.getCsrfToken(session);
|
||||
};
|
||||
res.setLazyLocal('csrfToken', () => this.getSessionCsrfToken(session));
|
||||
|
||||
if (!['GET', 'HEAD', 'OPTIONS'].find(s => s === req.method)) {
|
||||
try {
|
||||
@ -58,6 +40,13 @@ export default class CsrfProtectionComponent extends ApplicationComponent {
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
public getSessionCsrfToken(session: Session & Partial<SessionData>): string {
|
||||
if (typeof session.csrf !== 'string') {
|
||||
session.csrf = crypto.randomBytes(64).toString('base64');
|
||||
}
|
||||
return session.csrf;
|
||||
}
|
||||
}
|
||||
|
||||
class InvalidCsrfTokenError extends BadRequestError {
|
||||
|
@ -21,7 +21,7 @@ export default class ExpressAppComponent extends ApplicationComponent {
|
||||
|
||||
public async start(app: Express): Promise<void> {
|
||||
this.server = app.listen(this.port, this.addr, () => {
|
||||
logger.info(`Web server running on http://${this.addr}:${this.port}.`);
|
||||
logger.info(`Web server running on http://${this.addr}:${this.port}`);
|
||||
});
|
||||
|
||||
// Proxy
|
||||
@ -48,6 +48,26 @@ export default class ExpressAppComponent extends ApplicationComponent {
|
||||
if (!middleware) throw new Error('Middleware ' + type.name + ' not present in this request.');
|
||||
return middleware as M;
|
||||
};
|
||||
res.formatViewData = function (
|
||||
viewName: string,
|
||||
data?: Record<string, unknown>,
|
||||
callback?: (err: Error, html: string) => void,
|
||||
) {
|
||||
this.format({
|
||||
html: () => {
|
||||
this.render(viewName, data, callback);
|
||||
},
|
||||
json: () => {
|
||||
if (typeof data === 'undefined') data = {};
|
||||
const serialized = JSON.stringify({...data, viewName}, (key, value) => {
|
||||
if (key.startsWith('_') || typeof value === 'function') return undefined;
|
||||
else return value;
|
||||
});
|
||||
this.contentType('application/json');
|
||||
this.send(serialized);
|
||||
},
|
||||
});
|
||||
};
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
@ -1,39 +1,20 @@
|
||||
import {Router} from "express";
|
||||
|
||||
import ApplicationComponent from "../ApplicationComponent.js";
|
||||
import FrontendToolsComponent from "./FrontendToolsComponent.js";
|
||||
|
||||
export default class FormHelperComponent extends ApplicationComponent {
|
||||
|
||||
public async init(): Promise<void> {
|
||||
const globals = this.getApp().asOptional(FrontendToolsComponent)?.getGlobals();
|
||||
if (globals) {
|
||||
globals.set('validation', () => ({}));
|
||||
globals.set('previousFormData', () => ({}));
|
||||
}
|
||||
}
|
||||
|
||||
public async initRoutes(router: Router): Promise<void> {
|
||||
router.use((req, res, next) => {
|
||||
let _validation: unknown | null;
|
||||
res.locals.validation = () => {
|
||||
if (!_validation) {
|
||||
const v = req.flash('validation');
|
||||
_validation = v.length > 0 ? v[0] : null;
|
||||
}
|
||||
res.setLazyLocal('validation', () => {
|
||||
const validation = req.flash('validation');
|
||||
return validation.length > 0 ? validation[0] : null;
|
||||
});
|
||||
|
||||
return _validation;
|
||||
};
|
||||
|
||||
let _previousFormData: unknown | null = null;
|
||||
res.locals.previousFormData = () => {
|
||||
if (!_previousFormData) {
|
||||
const v = req.flash('previousFormData');
|
||||
_previousFormData = v.length > 0 ? v[0] : null;
|
||||
}
|
||||
|
||||
return _previousFormData;
|
||||
};
|
||||
res.setLazyLocal('previousFormData', () => {
|
||||
const previousFormData = req.flash('previousFormData');
|
||||
return previousFormData.length > 0 ? previousFormData[0] : null;
|
||||
});
|
||||
next();
|
||||
});
|
||||
|
||||
|
@ -1,23 +1,14 @@
|
||||
import config from "config";
|
||||
import {Express, Router} from "express";
|
||||
import path from "path";
|
||||
import util from "util";
|
||||
|
||||
import ApplicationComponent from "../ApplicationComponent.js";
|
||||
import {QueryParams, route, RouteParams} from "../common/Routing.js";
|
||||
import AssetCompiler from "../frontend/AssetCompiler.js";
|
||||
import AssetPreCompiler from "../frontend/AssetPreCompiler.js";
|
||||
import Globals from "../frontend/Globals.js";
|
||||
import ViewEngine from "../frontend/ViewEngine.js";
|
||||
import {logger} from "../Logger.js";
|
||||
import {listFilesRecursively} from "../Utils.js";
|
||||
import FileCache from "../utils/FileCache.js";
|
||||
import LazyLocalsCoreComponent from "./core/LazyLocalsCoreComponent.js";
|
||||
|
||||
export default class FrontendToolsComponent extends ApplicationComponent {
|
||||
private readonly publicDir: string;
|
||||
private readonly publicAssetsCache: FileCache = new FileCache();
|
||||
private readonly assetPreCompilers: AssetPreCompiler[];
|
||||
private readonly globals: Globals = new Globals();
|
||||
|
||||
public constructor(
|
||||
private readonly assetCompiler: AssetCompiler,
|
||||
@ -31,48 +22,17 @@ export default class FrontendToolsComponent extends ApplicationComponent {
|
||||
if (assetPreCompiler.isPublic()) {
|
||||
this.assetCompiler.addExtension(assetPreCompiler.getExtension());
|
||||
}
|
||||
|
||||
assetPreCompiler.setGlobals(this.globals);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async init(): Promise<void> {
|
||||
this.globals.set('route', (
|
||||
routeName: string,
|
||||
params: RouteParams = [],
|
||||
query: QueryParams = '',
|
||||
absolute: boolean = false,
|
||||
) => route(routeName, params, query, absolute));
|
||||
this.globals.set('app_version', this.getApp().getVersion());
|
||||
this.globals.set('core_version', this.getApp().getCoreVersion());
|
||||
this.globals.set('app', config.get('app'));
|
||||
this.globals.set('dump', (val: unknown) => {
|
||||
return util.inspect(val);
|
||||
});
|
||||
this.globals.set('hex', (v: number) => {
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
public async start(app: Express): Promise<void> {
|
||||
// Cache public assets
|
||||
if (config.get<boolean>('asset_cache')) {
|
||||
logger.info('Caching assets from', this.publicDir, '...');
|
||||
for (const file of await listFilesRecursively(this.publicDir)) {
|
||||
await this.publicAssetsCache.load(file);
|
||||
}
|
||||
} else {
|
||||
logger.info('Asset cache disabled.');
|
||||
}
|
||||
|
||||
this.hookPreCompilers();
|
||||
|
||||
// Setup express view engine
|
||||
let main = true;
|
||||
for (const assetPreCompiler of this.assetPreCompilers) {
|
||||
if (assetPreCompiler instanceof ViewEngine) {
|
||||
assetPreCompiler.setup(app, main);
|
||||
assetPreCompiler.setup(app, main, this.getApp().as(LazyLocalsCoreComponent));
|
||||
main = false;
|
||||
}
|
||||
}
|
||||
@ -84,17 +44,9 @@ export default class FrontendToolsComponent extends ApplicationComponent {
|
||||
}
|
||||
}
|
||||
|
||||
public async handleRoutes(router: Router): Promise<void> {
|
||||
router.use((req, res, next) => {
|
||||
res.locals.inlineAsset = (urlPath: string) => {
|
||||
return this.publicAssetsCache.getOrFail(path.join(this.publicDir, urlPath));
|
||||
};
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
// Add request context locals
|
||||
public async initRoutes(router: Router): Promise<void> {
|
||||
router.use((req, res, next) => {
|
||||
// Request context locals
|
||||
res.locals.url = req.url;
|
||||
res.locals.params = req.params;
|
||||
res.locals.query = req.query;
|
||||
@ -126,8 +78,4 @@ export default class FrontendToolsComponent extends ApplicationComponent {
|
||||
this.hookPreCompilers();
|
||||
}
|
||||
}
|
||||
|
||||
public getGlobals(): Globals {
|
||||
return this.globals;
|
||||
}
|
||||
}
|
||||
|
@ -10,10 +10,11 @@ import {logger} from "../Logger.js";
|
||||
import Mail from "../mail/Mail.js";
|
||||
import MailError from "../mail/MailError.js";
|
||||
import SecurityError from "../SecurityError.js";
|
||||
import FrontendToolsComponent from "./FrontendToolsComponent.js";
|
||||
import LazyLocalsCoreComponent from "./core/LazyLocalsCoreComponent.js";
|
||||
|
||||
export default class MailComponent extends ApplicationComponent {
|
||||
private transporter?: Transporter;
|
||||
private readonly additionalLocals: Record<string, unknown> = {};
|
||||
|
||||
public constructor(
|
||||
private readonly viewEngine: MailViewEngine,
|
||||
@ -59,7 +60,7 @@ export default class MailComponent extends ApplicationComponent {
|
||||
logger.info(`Mail ready to be distributed via ${config.get('mail.host')}:${config.get('mail.port')}`);
|
||||
});
|
||||
|
||||
await this.viewEngine.setup(app, false);
|
||||
await this.viewEngine.setup(app, false, this.getApp().as(LazyLocalsCoreComponent));
|
||||
}
|
||||
|
||||
public async stop(): Promise<void> {
|
||||
@ -77,7 +78,6 @@ export default class MailComponent extends ApplicationComponent {
|
||||
|
||||
for (const destEmail of to) {
|
||||
const template = mail.getTemplate();
|
||||
const locals = mail.getData();
|
||||
const options = mail.getOptions();
|
||||
|
||||
// Reset options
|
||||
@ -91,17 +91,17 @@ export default class MailComponent extends ApplicationComponent {
|
||||
};
|
||||
|
||||
// Set locals
|
||||
locals.mail_subject = options.subject;
|
||||
locals.mail_to = options.to;
|
||||
locals.mail_link = config.get<string>('app.public_url') +
|
||||
route('mail', [template.template], locals);
|
||||
Object.assign(locals, this.getApp().as(FrontendToolsComponent).getGlobals().get());
|
||||
const urlLocals = mail.getData();
|
||||
urlLocals.mail_subject = options.subject;
|
||||
urlLocals.mail_to = options.to;
|
||||
urlLocals.mail_link = config.get<string>('app.public_url') +
|
||||
route('mail', [template.template], urlLocals);
|
||||
const locals = {...this.additionalLocals, ...urlLocals};
|
||||
|
||||
// Log
|
||||
logger.debug(`Send mail from ${options.from.address} to ${options.to}`);
|
||||
|
||||
// Render email
|
||||
|
||||
options.html = await this.viewEngine.render('mails/' + template.template + '.mnjk', locals, false);
|
||||
options.text = await this.viewEngine.render('mails/' + template.template + '.mnjk', locals, true);
|
||||
|
||||
@ -112,6 +112,10 @@ export default class MailComponent extends ApplicationComponent {
|
||||
return results;
|
||||
}
|
||||
|
||||
public setAdditionalLocal(key: string, value: unknown): void {
|
||||
this.additionalLocals[key] = value;
|
||||
}
|
||||
|
||||
private getTransporter(): Transporter {
|
||||
if (!this.transporter) throw new MailError('Mail system was not prepared.');
|
||||
return this.transporter;
|
||||
|
@ -3,17 +3,9 @@ import onFinished from "on-finished";
|
||||
|
||||
import ApplicationComponent from "../ApplicationComponent.js";
|
||||
import {logger} from "../Logger.js";
|
||||
import FrontendToolsComponent from "./FrontendToolsComponent.js";
|
||||
import SessionComponent from "./SessionComponent.js";
|
||||
|
||||
export default class PreviousUrlComponent extends ApplicationComponent {
|
||||
public async init(): Promise<void> {
|
||||
const globals = this.getApp().asOptional(FrontendToolsComponent)?.getGlobals();
|
||||
if (globals) {
|
||||
globals.set('getPreviousUrl', () => null);
|
||||
}
|
||||
}
|
||||
|
||||
public async handleRoutes(router: Router): Promise<void> {
|
||||
router.use((req, res, next) => {
|
||||
req.getPreviousUrl = () => {
|
||||
@ -31,7 +23,7 @@ export default class PreviousUrlComponent extends ApplicationComponent {
|
||||
|
||||
return null;
|
||||
};
|
||||
res.locals.getPreviousUrl = req.getPreviousUrl;
|
||||
res.setLazyLocal('previousUrl', () => req.getPreviousUrl());
|
||||
|
||||
req.getIntendedUrl = () => {
|
||||
return req.query.redirect_uri?.toString() || null;
|
||||
|
@ -8,6 +8,7 @@ import CacheProvider from "../CacheProvider.js";
|
||||
import {logger} from "../Logger.js";
|
||||
|
||||
export default class RedisComponent extends ApplicationComponent implements CacheProvider {
|
||||
private readonly prefix: string = config.get('redis.prefix');
|
||||
private redisClient?: RedisClient;
|
||||
private store: Store = new RedisStore(this);
|
||||
|
||||
@ -42,7 +43,7 @@ export default class RedisComponent extends ApplicationComponent implements Cach
|
||||
return;
|
||||
}
|
||||
|
||||
this.redisClient.get(key, (err, val) => {
|
||||
this.redisClient.get(this.prefix + key, (err, val) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
@ -63,7 +64,7 @@ export default class RedisComponent extends ApplicationComponent implements Cach
|
||||
return;
|
||||
}
|
||||
|
||||
this.redisClient.del(key, (err) => {
|
||||
this.redisClient.del(this.prefix + key, (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
@ -81,7 +82,7 @@ export default class RedisComponent extends ApplicationComponent implements Cach
|
||||
return;
|
||||
}
|
||||
|
||||
this.redisClient.psetex(key, ttl, value, (err) => {
|
||||
this.redisClient.psetex(this.prefix + key, ttl, value, (err) => {
|
||||
if (err) return reject(err);
|
||||
resolve();
|
||||
});
|
||||
@ -95,7 +96,7 @@ export default class RedisComponent extends ApplicationComponent implements Cach
|
||||
return;
|
||||
}
|
||||
|
||||
this.redisClient.pexpire(key, ttl, (err) => {
|
||||
this.redisClient.pexpire(this.prefix + key, ttl, (err) => {
|
||||
if (err) return reject(err);
|
||||
resolve();
|
||||
});
|
||||
|
@ -5,7 +5,6 @@ import session from "express-session";
|
||||
|
||||
import ApplicationComponent from "../ApplicationComponent.js";
|
||||
import SecurityError from "../SecurityError.js";
|
||||
import FrontendToolsComponent from "./FrontendToolsComponent.js";
|
||||
import RedisComponent from "./RedisComponent.js";
|
||||
|
||||
export default class SessionComponent extends ApplicationComponent {
|
||||
@ -16,13 +15,6 @@ export default class SessionComponent extends ApplicationComponent {
|
||||
this.storeComponent = storeComponent;
|
||||
}
|
||||
|
||||
public async init(): Promise<void> {
|
||||
const globals = this.getApp().asOptional(FrontendToolsComponent)?.getGlobals();
|
||||
if (globals) {
|
||||
globals.set('flash', () => '');
|
||||
}
|
||||
}
|
||||
|
||||
public async checkSecuritySettings(): Promise<void> {
|
||||
this.checkSecurityConfigField('session.secret');
|
||||
if (!config.get<boolean>('session.cookie.secure')) {
|
||||
@ -68,40 +60,16 @@ export default class SessionComponent extends ApplicationComponent {
|
||||
res.locals.session = session;
|
||||
|
||||
// Views flash function
|
||||
const _flash: FlashStorage = {};
|
||||
res.locals.flash = (key?: string): FlashMessages | unknown[] => {
|
||||
if (key !== undefined) {
|
||||
if (_flash[key] === undefined) _flash[key] = req.flash(key);
|
||||
return _flash[key] || [];
|
||||
}
|
||||
|
||||
if (_flash._messages === undefined) {
|
||||
_flash._messages = {
|
||||
info: req.flash('info'),
|
||||
success: req.flash('success'),
|
||||
warning: req.flash('warning'),
|
||||
error: req.flash('error'),
|
||||
'error-alert': req.flash('error-alert'),
|
||||
};
|
||||
}
|
||||
return _flash._messages;
|
||||
};
|
||||
res.setLazyLocal('flash', () => {
|
||||
return {
|
||||
info: req.flash('info'),
|
||||
success: req.flash('success'),
|
||||
warning: req.flash('warning'),
|
||||
error: req.flash('error'),
|
||||
'error-alert': req.flash('error-alert'),
|
||||
};
|
||||
});
|
||||
next();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export type FlashMessages = {
|
||||
[k: string]: unknown[] | undefined
|
||||
};
|
||||
|
||||
export type DefaultFlashMessages = FlashMessages & {
|
||||
info?: unknown[] | undefined;
|
||||
success?: unknown[] | undefined;
|
||||
warning?: unknown[] | undefined;
|
||||
error?: unknown[] | undefined;
|
||||
};
|
||||
|
||||
type FlashStorage = FlashMessages & {
|
||||
_messages?: DefaultFlashMessages,
|
||||
};
|
||||
|
@ -1,7 +1,7 @@
|
||||
import config from "config";
|
||||
import cookie from "cookie";
|
||||
import cookieParser from "cookie-parser";
|
||||
import {Express, Request} from "express";
|
||||
import {Express, Request, Router} from "express";
|
||||
import {Session} from "express-session";
|
||||
import {WebSocketServer} from "ws";
|
||||
|
||||
@ -10,7 +10,6 @@ import ApplicationComponent from "../ApplicationComponent.js";
|
||||
import {logger} from "../Logger.js";
|
||||
import WebSocketListener from "../WebSocketListener.js";
|
||||
import ExpressAppComponent from "./ExpressAppComponent.js";
|
||||
import FrontendToolsComponent from "./FrontendToolsComponent.js";
|
||||
import RedisComponent from "./RedisComponent.js";
|
||||
|
||||
export default class WebSocketServerComponent extends ApplicationComponent {
|
||||
@ -21,11 +20,13 @@ export default class WebSocketServerComponent extends ApplicationComponent {
|
||||
|
||||
app.require(ExpressAppComponent);
|
||||
app.require(RedisComponent);
|
||||
}
|
||||
|
||||
const globals = app.asOptional(FrontendToolsComponent)?.getGlobals();
|
||||
if (globals) {
|
||||
globals.set('websocketUrl', config.get('app.public_websocket_url'));
|
||||
}
|
||||
public async initRoutes(router: Router): Promise<void> {
|
||||
router.use((req, res, next) => {
|
||||
res.locals.websocketUrl = config.get('app.public_websocket_url');
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
public async start(_app: Express): Promise<void> {
|
||||
|
26
src/components/core/AppLocalsCoreComponents.ts
Normal file
26
src/components/core/AppLocalsCoreComponents.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import config from "config";
|
||||
import {Router} from "express";
|
||||
|
||||
import ApplicationComponent from "../../ApplicationComponent.js";
|
||||
import MailComponent from "../MailComponent.js";
|
||||
|
||||
export default class AppLocalsCoreComponents extends ApplicationComponent {
|
||||
private appLocals: Record<string, unknown> | undefined;
|
||||
|
||||
|
||||
public async init(): Promise<void> {
|
||||
this.appLocals = {
|
||||
...config.get('app'),
|
||||
version: this.getApp().getVersion(),
|
||||
core_version: this.getApp().getCoreVersion(),
|
||||
};
|
||||
this.getApp().as(MailComponent).setAdditionalLocal('app', this.appLocals);
|
||||
}
|
||||
|
||||
public async initRoutes(router: Router): Promise<void> {
|
||||
router.use((req, res, next) => {
|
||||
res.locals.app = this.appLocals;
|
||||
next();
|
||||
});
|
||||
}
|
||||
}
|
30
src/components/core/LazyLocalsCoreComponent.ts
Normal file
30
src/components/core/LazyLocalsCoreComponent.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import {Router} from "express";
|
||||
|
||||
import ApplicationComponent from "../../ApplicationComponent.js";
|
||||
|
||||
export default class LazyLocalsCoreComponent extends ApplicationComponent {
|
||||
public async initRoutes(router: Router): Promise<void> {
|
||||
router.use((req, res, next) => {
|
||||
res.locals._lazyLocals = {};
|
||||
res.setLazyLocal = (key: string, valueProvider: () => unknown) => {
|
||||
res.locals._lazyLocals[key] = valueProvider;
|
||||
};
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
public setupLazyLocals(localsObject: Record<string, unknown>): void {
|
||||
const lazyLocals = localsObject._lazyLocals as Record<string, () => unknown> | undefined;
|
||||
if (!lazyLocals) throw new Error('No _lazyLocals field found on referenced object.');
|
||||
|
||||
for (const lazyLocal of Object.keys(lazyLocals)) {
|
||||
Object.defineProperty(localsObject, lazyLocal, {
|
||||
get: function () {
|
||||
delete this[lazyLocal];
|
||||
return this[lazyLocal] = lazyLocals[lazyLocal]();
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -242,6 +242,9 @@ export default class MysqlConnectionManager {
|
||||
if (args.length > i + 1) {
|
||||
migrationId = parseInt(args[i + 1]);
|
||||
}
|
||||
|
||||
logger.info('Rolling back migration', migrationId);
|
||||
|
||||
await this.prepare(false);
|
||||
await this.rollbackMigration(migrationId);
|
||||
return;
|
||||
|
@ -4,11 +4,9 @@ import path from "path";
|
||||
|
||||
import {logger} from "../Logger.js";
|
||||
import {doesFileExist, listFilesRecursively} from "../Utils.js";
|
||||
import Globals from "./Globals.js";
|
||||
|
||||
export default abstract class AssetPreCompiler {
|
||||
protected readonly assetPaths: string[];
|
||||
private globals?: Globals;
|
||||
private watcher?: FSWatcher;
|
||||
private afterPreCompileHandlers: ((watch: boolean) => Promise<void>)[] = [];
|
||||
private inputChangeHandler?: (restart: boolean) => Promise<void>;
|
||||
@ -57,15 +55,6 @@ export default abstract class AssetPreCompiler {
|
||||
return this.assetPaths;
|
||||
}
|
||||
|
||||
protected getGlobals(): Globals {
|
||||
if (!this.globals) throw new Error('globals field not intialized.');
|
||||
return this.globals;
|
||||
}
|
||||
|
||||
public setGlobals(globals: Globals): void {
|
||||
this.globals = globals;
|
||||
}
|
||||
|
||||
public async stop(): Promise<void> {
|
||||
if (this.watcher) {
|
||||
await this.watcher.close();
|
||||
|
@ -1,11 +0,0 @@
|
||||
export default class Globals {
|
||||
private readonly globals: Record<string, unknown> = {};
|
||||
|
||||
public get(): Record<string, unknown> {
|
||||
return {...this.globals};
|
||||
}
|
||||
|
||||
public set(key: string, value: unknown): void {
|
||||
this.globals[key] = value;
|
||||
}
|
||||
}
|
@ -2,7 +2,6 @@ import "./register_svelte/register_svelte.js";
|
||||
|
||||
import clearModule from "clear-module";
|
||||
import config from "config";
|
||||
import crypto from "crypto";
|
||||
import {promises as fs} from 'fs';
|
||||
import path from "path";
|
||||
import requireFromString from "require-from-string";
|
||||
@ -14,23 +13,10 @@ import {logger} from "../Logger.js";
|
||||
import FileCache from "../utils/FileCache.js";
|
||||
import ViewEngine from "./ViewEngine.js";
|
||||
|
||||
const BACKEND_CODE_PREFIX = '$locals.';
|
||||
const BACKEND_CODE_PREFIX_TEMPORARY_HOLDER = '$$locals$$';
|
||||
const COMPILED_SVELTE_EXTENSION = '.precompiled';
|
||||
|
||||
export default class SvelteViewEngine extends ViewEngine {
|
||||
public static getPreCompileSeparator(canonicalViewName: string): string {
|
||||
return '\n---' +
|
||||
crypto.createHash('sha1')
|
||||
.update(path.basename(path.resolve(canonicalViewName)))
|
||||
.digest('base64') +
|
||||
'---\n';
|
||||
}
|
||||
|
||||
|
||||
private readonly fileCache: FileCache = new FileCache();
|
||||
private readonly dependencyCache: Record<string, Set<string>> = {};
|
||||
private readonly preprocessingCache: Record<string, PreprocessingCacheEntry> = {};
|
||||
private readonly reverseDependencyCache: Record<string, Set<string>> = {};
|
||||
private readonly preprocessingCache: Record<string, string> = {};
|
||||
private readonly cssCache: Record<string, string[] | undefined> = {};
|
||||
|
||||
public constructor(
|
||||
@ -48,8 +34,8 @@ export default class SvelteViewEngine extends ViewEngine {
|
||||
public async onFileRemove(file: string): Promise<void> {
|
||||
const canonicalName = this.toCanonicalName(file);
|
||||
delete this.preprocessingCache[canonicalName];
|
||||
delete this.dependencyCache[canonicalName];
|
||||
Object.values(this.dependencyCache).forEach(set => set.delete(canonicalName));
|
||||
delete this.reverseDependencyCache[canonicalName];
|
||||
Object.values(this.reverseDependencyCache).forEach(set => set.delete(canonicalName));
|
||||
await super.onFileRemove(file);
|
||||
}
|
||||
|
||||
@ -59,34 +45,26 @@ export default class SvelteViewEngine extends ViewEngine {
|
||||
): Promise<string> {
|
||||
const canonicalViewName = this.toCanonicalName(file);
|
||||
|
||||
// View
|
||||
const actualFile = path.join(this.targetDir, canonicalViewName + COMPILED_SVELTE_EXTENSION);
|
||||
const view = await this.fileCache.get(actualFile, !config.get<boolean>('view.cache'));
|
||||
const rootTemplateFile = await this.resolveFileFromCanonicalNameOrFail('templates/svelte_template.html');
|
||||
const rawOutput = await this.fileCache.get(rootTemplateFile, !config.get<boolean>('view.cache'));
|
||||
|
||||
// Root template
|
||||
const templateFile = await this.resolveFileFromCanonicalNameOrFail('templates/svelte_template.html');
|
||||
const rawOutput = await this.fileCache.get(templateFile, !config.get<boolean>('view.cache'));
|
||||
|
||||
// Pre-compiled parts
|
||||
const [
|
||||
backendCalls,
|
||||
locals.isSsr = true;
|
||||
const {
|
||||
head,
|
||||
html,
|
||||
css,
|
||||
] = view.split(SvelteViewEngine.getPreCompileSeparator(canonicalViewName));
|
||||
} = await this.renderSvelteSsr(canonicalViewName, locals);
|
||||
|
||||
const localMap: Record<string, unknown> = this.compileBackendCalls(backendCalls.split('\n'), locals, false);
|
||||
const actualLocals = JSON.stringify(localMap, (key, value) => {
|
||||
if (key.startsWith('_')) return undefined;
|
||||
return typeof value === 'function' ?
|
||||
value.toString() :
|
||||
value;
|
||||
locals.isSsr = false;
|
||||
const serializedLocals = JSON.stringify(locals, (key, value) => {
|
||||
if (key.startsWith('_') || typeof value === 'function') return undefined;
|
||||
return value;
|
||||
});
|
||||
|
||||
// Replaces
|
||||
const replaceMap: Record<string, string> = {
|
||||
canonicalViewName: canonicalViewName,
|
||||
locals: actualLocals,
|
||||
locals: serializedLocals,
|
||||
head: head,
|
||||
html: html,
|
||||
css: css,
|
||||
@ -104,55 +82,17 @@ export default class SvelteViewEngine extends ViewEngine {
|
||||
const targetFile = path.join(this.targetDir, canonicalName);
|
||||
logger.info(canonicalName + ' > ', 'Pre-compiling', canonicalName, '->', targetFile);
|
||||
|
||||
const {backendCalls} = await this.preprocess(canonicalName);
|
||||
await this.preprocessSvelte(canonicalName);
|
||||
|
||||
// Server Side Render (initial HTML and CSS, no-js)
|
||||
const ssr = await this.compileSsr(canonicalName);
|
||||
|
||||
const separator = SvelteViewEngine.getPreCompileSeparator(canonicalName);
|
||||
const finalCode = [
|
||||
[...new Set<string>(backendCalls).values()].join('\n'),
|
||||
ssr.head,
|
||||
ssr.html,
|
||||
ssr.css,
|
||||
].join(separator);
|
||||
|
||||
const swafViewFile = path.join(this.targetDir, canonicalName + COMPILED_SVELTE_EXTENSION);
|
||||
await fs.mkdir(path.dirname(swafViewFile), {recursive: true});
|
||||
await fs.writeFile(swafViewFile, finalCode);
|
||||
|
||||
if (alsoCompileDependents && Object.keys(this.dependencyCache).indexOf(canonicalName) >= 0) {
|
||||
logger.info(canonicalName + ' > ', 'Compiling dependents...');
|
||||
for (const dependent of [...this.dependencyCache[canonicalName]]) {
|
||||
if (alsoCompileDependents && Object.keys(this.reverseDependencyCache).indexOf(canonicalName) >= 0) {
|
||||
logger.info(canonicalName + ' > ', 'Pre-compiling dependents...');
|
||||
for (const dependent of [...this.reverseDependencyCache[canonicalName]]) {
|
||||
await this.preCompile(dependent, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private resolveDependencies(source: string, canonicalViewName: string): string[] {
|
||||
const dependencies: string[] = [];
|
||||
|
||||
for (const match of source.matchAll(/import .+ from ['"](.+?\.svelte)['"];/gm)) {
|
||||
dependencies.push(path.join(path.dirname(canonicalViewName), match[1]));
|
||||
}
|
||||
|
||||
// Clear existing links from cache
|
||||
for (const dependency of Object.keys(this.dependencyCache)) {
|
||||
this.dependencyCache[dependency].delete(canonicalViewName);
|
||||
}
|
||||
|
||||
// Add new links to cache
|
||||
for (const dependency of dependencies) {
|
||||
if (Object.keys(this.dependencyCache).indexOf(dependency) < 0) {
|
||||
this.dependencyCache[dependency] = new Set<string>();
|
||||
}
|
||||
this.dependencyCache[dependency].add(canonicalViewName);
|
||||
}
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
private async preprocess(canonicalName: string): Promise<PreprocessingCacheEntry> {
|
||||
private async preprocessSvelte(canonicalName: string): Promise<string> {
|
||||
// Cache
|
||||
if (Object.keys(this.preprocessingCache).indexOf(canonicalName) >= 0) {
|
||||
return this.preprocessingCache[canonicalName];
|
||||
@ -168,13 +108,10 @@ export default class SvelteViewEngine extends ViewEngine {
|
||||
// Read source file if code was not already provided
|
||||
const code = await this.fileCache.get(file, !config.get<boolean>('view.cache'));
|
||||
|
||||
// Replace backend calls
|
||||
const replacedBackendCalls = await this.replaceBackendCalls(canonicalName, code);
|
||||
|
||||
// Preprocess svelte
|
||||
logger.info(canonicalName + ' > ', 'Svelte preprocessing');
|
||||
const preprocessed = await preprocess(
|
||||
replacedBackendCalls.code,
|
||||
const processed = await preprocess(
|
||||
code,
|
||||
sveltePreprocess({
|
||||
typescript: {
|
||||
tsconfigFile: 'src/assets/views/tsconfig.json',
|
||||
@ -186,95 +123,26 @@ export default class SvelteViewEngine extends ViewEngine {
|
||||
);
|
||||
|
||||
// Write to output file
|
||||
await fs.writeFile(outputFile, preprocessed.code);
|
||||
await fs.writeFile(outputFile, processed.code);
|
||||
|
||||
// Preprocess dependencies
|
||||
const backendCalls: string[] = replacedBackendCalls.backendCalls;
|
||||
for (const dependency of this.resolveDependencies(code, canonicalName)) {
|
||||
backendCalls.push(...(await this.preprocess(dependency)).backendCalls);
|
||||
}
|
||||
this.resolveAndCacheDependencies(processed.code, canonicalName);
|
||||
|
||||
return this.preprocessingCache[canonicalName] = {
|
||||
backendCalls: backendCalls,
|
||||
code: preprocessed.code,
|
||||
};
|
||||
return this.preprocessingCache[canonicalName] = processed.code;
|
||||
}
|
||||
|
||||
private async replaceBackendCalls(canonicalName: string, code: string): Promise<PreprocessingCacheEntry> {
|
||||
logger.info(canonicalName + ' > ', 'Replacing backend calls');
|
||||
|
||||
// Skip replace if there is no swaf export
|
||||
if (!code.match(/import[ \n]+{[ \n]*locals[ \n]*}[ \n]+from[ \n]+["'](\.\.\/)+ts\/stores(\.js)?["']/)) {
|
||||
return {
|
||||
backendCalls: [],
|
||||
code: code,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
let output = code;
|
||||
const backendCalls = new Set<string>();
|
||||
|
||||
let index = 0;
|
||||
while ((index = output.indexOf(BACKEND_CODE_PREFIX, index + 1)) >= 0) {
|
||||
// Escaping
|
||||
if (index > 0 && output[index - 1] === '\\') {
|
||||
const isEscapingEscaped: boolean = index > 1 && output[index - 2] === '\\';
|
||||
output = output.substring(0, index - 1 - (isEscapingEscaped ? 1 : 0)) +
|
||||
output.substring(index, output.length);
|
||||
continue;
|
||||
}
|
||||
|
||||
const startIndex = index + BACKEND_CODE_PREFIX.length;
|
||||
let endIndex = startIndex;
|
||||
let struct = 0;
|
||||
|
||||
while (endIndex < output.length) {
|
||||
if (['(', '[', '{'].indexOf(output[endIndex]) >= 0) struct++;
|
||||
if ([')', ']', '}'].indexOf(output[endIndex]) >= 0) {
|
||||
struct--;
|
||||
if (struct <= 0) {
|
||||
if (struct === 0) endIndex++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ([' ', '\n', '<', '.', '\'', '"', '?', ','].indexOf(output[endIndex]) >= 0 && struct === 0) break;
|
||||
endIndex++;
|
||||
}
|
||||
|
||||
let backendCall = output.substring(startIndex, endIndex);
|
||||
if (backendCall.match(/([^()]+)\((.*?)\)/)) {
|
||||
backendCall = backendCall.replace(/([^()]+)\((.*?)\)/, "'$1', `[$2]`");
|
||||
} else {
|
||||
backendCall = backendCall.replace(/([^()]+)(\(\))?/, "'$1'");
|
||||
}
|
||||
backendCalls.add(backendCall);
|
||||
|
||||
output = output.substring(0, index) +
|
||||
'$locals(' + backendCall.split(BACKEND_CODE_PREFIX).join(BACKEND_CODE_PREFIX_TEMPORARY_HOLDER) + ')' +
|
||||
output.substring(endIndex, output.length);
|
||||
}
|
||||
output = output.split(BACKEND_CODE_PREFIX_TEMPORARY_HOLDER).join(BACKEND_CODE_PREFIX);
|
||||
|
||||
return {
|
||||
backendCalls: [...backendCalls],
|
||||
code: output,
|
||||
};
|
||||
}
|
||||
|
||||
private async compileSsr(canonicalName: string): Promise<{
|
||||
private async renderSvelteSsr(canonicalName: string, locals: {[key: string]: unknown}): Promise<{
|
||||
head: string,
|
||||
css: string,
|
||||
html: string,
|
||||
}> {
|
||||
const targetFile = path.join(this.targetDir, canonicalName);
|
||||
const {backendCalls, code} = await this.preprocess(canonicalName);
|
||||
const code = await this.fileCache.get(targetFile, !config.get<boolean>('view.cache'));
|
||||
|
||||
// Get dependencies css
|
||||
const dependenciesCss: string[] = [];
|
||||
for (const dependency of this.resolveDependencies(code, canonicalName)) {
|
||||
if (this.cssCache[dependency] === undefined) {
|
||||
await this.compileSsr(dependency);
|
||||
for (const dependency of this.resolveAndCacheDependencies(code, canonicalName)) {
|
||||
if (this.cssCache[dependency] === undefined || !config.get<boolean>('view.cache')) {
|
||||
await this.renderSvelteSsr(dependency, locals);
|
||||
}
|
||||
const css = this.cssCache[dependency];
|
||||
if (css === undefined) {
|
||||
@ -295,15 +163,8 @@ export default class SvelteViewEngine extends ViewEngine {
|
||||
});
|
||||
|
||||
// Load locals into locals store
|
||||
const localsModulePath = path.resolve(this.targetDir, "../ts/stores.js");
|
||||
const localsModule = await import(localsModulePath);
|
||||
const locals = this.getGlobals().get();
|
||||
const localMap = this.compileBackendCalls(backendCalls, locals, true);
|
||||
localsModule.locals.set((key: string, args: string) => {
|
||||
return localMap[args ?
|
||||
`'${key}', \`${args}\``
|
||||
: `'${key}'`];
|
||||
});
|
||||
const storesModule = await import(path.resolve(this.targetDir, "../ts/stores.js"));
|
||||
storesModule.locals.set(locals);
|
||||
|
||||
// Load module and render
|
||||
const moduleId = path.resolve(targetFile);
|
||||
@ -324,40 +185,26 @@ export default class SvelteViewEngine extends ViewEngine {
|
||||
};
|
||||
}
|
||||
|
||||
private compileBackendCalls(
|
||||
backendCalls: string[],
|
||||
locals: Record<string, unknown>,
|
||||
isPreRender: boolean,
|
||||
): Record<string, unknown> {
|
||||
locals = {...locals, isPreRender};
|
||||
private resolveAndCacheDependencies(source: string, canonicalViewName: string): string[] {
|
||||
const dependencies: string[] = [];
|
||||
|
||||
const localMap: Record<string, unknown> = {};
|
||||
backendCalls.forEach(code => {
|
||||
const key = code.substring(1, code.indexOf(',') >= 0 ? code.indexOf(',') - 1 : code.length - 1);
|
||||
if (code.indexOf('`[') >= 0) {
|
||||
const args = code.substring(code.indexOf('`[') + 2, code.length - 2)
|
||||
.split(/, *?/)
|
||||
.map(arg => {
|
||||
if (arg.startsWith("'")) return '"' + arg.substring(1, arg.length - 1) + '"';
|
||||
return arg;
|
||||
})
|
||||
.filter(arg => arg.length > 0)
|
||||
.map(arg => {
|
||||
return Function(`"use strict";const $locals = arguments[0];return (${arg});`)(locals);
|
||||
}); // Uses named parameter locals
|
||||
for (const match of source.matchAll(/import .+ from ['"](.+?\.svelte)['"];/gm)) {
|
||||
dependencies.push(path.join(path.dirname(canonicalViewName), match[1]));
|
||||
}
|
||||
|
||||
const f = locals[key];
|
||||
if (typeof f !== 'function') throw new Error(key + ' is not a function.');
|
||||
localMap[`'${key}', \`[${code.substring(code.indexOf('`[') + 2, code.length - 2)}]\``] = f.call(locals, ...args);
|
||||
} else {
|
||||
localMap[`'${key}'`] = locals[key];
|
||||
// Clear existing links from cache
|
||||
for (const dependency of Object.keys(this.reverseDependencyCache)) {
|
||||
this.reverseDependencyCache[dependency].delete(canonicalViewName);
|
||||
}
|
||||
|
||||
// Add new links to cache
|
||||
for (const dependency of dependencies) {
|
||||
if (Object.keys(this.reverseDependencyCache).indexOf(dependency) < 0) {
|
||||
this.reverseDependencyCache[dependency] = new Set<string>();
|
||||
}
|
||||
});
|
||||
return localMap;
|
||||
this.reverseDependencyCache[dependency].add(canonicalViewName);
|
||||
}
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
}
|
||||
|
||||
type PreprocessingCacheEntry = {
|
||||
backendCalls: string[],
|
||||
code: string,
|
||||
};
|
||||
|
@ -1,5 +1,6 @@
|
||||
import {Express} from "express";
|
||||
|
||||
import LazyLocalsCoreComponent from "../components/core/LazyLocalsCoreComponent.js";
|
||||
import AssetPreCompiler from "./AssetPreCompiler.js";
|
||||
|
||||
export default abstract class ViewEngine extends AssetPreCompiler {
|
||||
@ -18,10 +19,11 @@ export default abstract class ViewEngine extends AssetPreCompiler {
|
||||
locals: Record<string, unknown>,
|
||||
): Promise<string>;
|
||||
|
||||
public setup(app: Express, main: boolean): void {
|
||||
public setup(app: Express, main: boolean, lazyLocalsComponent: LazyLocalsCoreComponent): void {
|
||||
app.engine(this.extension, (path, options, callback) => {
|
||||
// Props (locals)
|
||||
const locals = Object.assign(this.getGlobals().get(), options);
|
||||
const locals = {...options};
|
||||
|
||||
lazyLocalsComponent.setupLazyLocals(locals);
|
||||
|
||||
this.render(path, locals)
|
||||
.then(value => callback(null, value))
|
||||
|
@ -53,7 +53,7 @@ export default class BackendController extends Controller {
|
||||
}
|
||||
|
||||
protected async getIndex(req: Request, res: Response): Promise<void> {
|
||||
res.render('backend/index', {
|
||||
res.formatViewData('backend/index', {
|
||||
menu: await Promise.all(BackendController.menu.map(async m => ({
|
||||
link: await m.getLink(),
|
||||
display_string: await m.getDisplayString(),
|
||||
@ -66,7 +66,7 @@ export default class BackendController extends Controller {
|
||||
const accounts = await User.paginate(req, 20, User.select()
|
||||
.where('approved', 0)
|
||||
.with('mainEmail'));
|
||||
res.render('backend/accounts_approval', {
|
||||
res.formatViewData('backend/accounts_approval', {
|
||||
accounts: accounts.map(account => Object.assign({
|
||||
mainEmailStr: account.mainEmail.getOrFail()?.email,
|
||||
created_at_iso: account.created_at?.toISOString(),
|
||||
|
@ -7,8 +7,8 @@ export default class MailController extends Controller {
|
||||
this.get("/mail/:template", this.getMail, 'mail');
|
||||
}
|
||||
|
||||
protected async getMail(request: Request, response: Response): Promise<void> {
|
||||
protected async getMail(request: Request, res: Response): Promise<void> {
|
||||
const template = request.params['template'];
|
||||
response.render(`mails/${template}.mnjk`, request.query);
|
||||
res.formatViewData(`mails/${template}.mnjk`, request.query);
|
||||
}
|
||||
}
|
||||
|
10
src/types/Express.d.ts
vendored
10
src/types/Express.d.ts
vendored
@ -33,6 +33,16 @@ declare global {
|
||||
|
||||
getIntendedUrl(): string | null;
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
setLazyLocal(key: string, valueProvider: () => unknown): void;
|
||||
|
||||
formatViewData(
|
||||
viewName: string,
|
||||
data?: Record<string, unknown>,
|
||||
callback?: (err: Error, html: string) => void,
|
||||
): void;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -142,6 +142,7 @@ describe('Authenticate with email (magic_link)', () => {
|
||||
|
||||
// Authenticate
|
||||
await agent.post('/auth/login?' + querystring.stringify({redirect_uri: '/redirect-uri'}))
|
||||
.accept('json')
|
||||
.set('Cookie', cookies)
|
||||
.send({
|
||||
csrf: csrf,
|
||||
|
@ -12,7 +12,7 @@ useApp(async (addr, port) => {
|
||||
this.use(new class extends Controller {
|
||||
public routes(): void {
|
||||
this.get('/', (req, res) => {
|
||||
res.send(CsrfProtectionComponent.getCsrfToken(req.getSession()));
|
||||
res.send(this.getApp().as(CsrfProtectionComponent).getSessionCsrfToken(req.getSession()));
|
||||
}, 'csrf_test');
|
||||
|
||||
this.post('/', (req, res) => {
|
||||
@ -35,6 +35,7 @@ describe('Test CSRF protection', () => {
|
||||
test('no csrf token should be in session at first', (done) => {
|
||||
const agent = supertest(app.getExpressApp());
|
||||
agent.post('/')
|
||||
.accept('json')
|
||||
.expect(401)
|
||||
.then(res => {
|
||||
expect(res.text).toContain(`You weren't assigned any CSRF token.`);
|
||||
@ -55,6 +56,7 @@ describe('Test CSRF protection', () => {
|
||||
|
||||
const agent = supertest(app.getExpressApp());
|
||||
agent.post('/')
|
||||
.accept('json')
|
||||
.set('Cookie', cookies)
|
||||
.expect(401)
|
||||
.then((res) => {
|
||||
@ -68,6 +70,7 @@ describe('Test CSRF protection', () => {
|
||||
|
||||
const agent = supertest(app.getExpressApp());
|
||||
agent.post('/')
|
||||
.accept('json')
|
||||
.set('Cookie', cookies)
|
||||
.set('Content-Type', 'application/json')
|
||||
.send({csrf: 'not_a_valid_csrf'})
|
||||
|
@ -23,8 +23,10 @@ export async function followMagicLinkFromMail(
|
||||
expect(query).toBeDefined();
|
||||
|
||||
await agent.get('/magic/link?' + query)
|
||||
.accept('json')
|
||||
.expect(200);
|
||||
await agent.get('/magic/lobby')
|
||||
.accept('json')
|
||||
.set('Cookie', cookies)
|
||||
.expect(302)
|
||||
.expect('Location', expectedRedirectUrl);
|
||||
@ -55,10 +57,10 @@ export function authAppProvider(withUsername: boolean = true, approvalMode: bool
|
||||
this.use(new class extends Controller {
|
||||
public routes(): void {
|
||||
this.get('/', (req, res) => {
|
||||
res.render('home');
|
||||
res.formatViewData('home');
|
||||
}, 'home');
|
||||
this.get('/csrf', (req, res) => {
|
||||
res.send(CsrfProtectionComponent.getCsrfToken(req.getSession()));
|
||||
res.send(this.getApp().as(CsrfProtectionComponent).getSessionCsrfToken(req.getSession()));
|
||||
}, 'csrf');
|
||||
this.get('/is-auth', async (req, res) => {
|
||||
const proofs = await this.getApp().as(AuthComponent).getAuthGuard().getProofs(req);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import MailDev, {Mail} from "maildev";
|
||||
|
||||
export const MAIL_SERVER = new MailDev({
|
||||
ip: 'localhost',
|
||||
ip: '127.0.0.1',
|
||||
});
|
||||
|
||||
export async function setupMailServer(): Promise<void> {
|
||||
|
@ -12,7 +12,7 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="render-mode">{$locals.isPreRender ? 'SSR' : 'CSR'}</div>
|
||||
<div class="render-mode">{$locals.isSsr ? 'SSR' : 'CSR'}</div>
|
||||
|
||||
<div class="data-table-container">
|
||||
<table class="data-table">
|
||||
|
484
yarn.lock
484
yarn.lock
@ -535,7 +535,7 @@
|
||||
"@types/yargs" "^16.0.0"
|
||||
chalk "^4.0.0"
|
||||
|
||||
"@mapbox/node-pre-gyp@^1.0.1":
|
||||
"@mapbox/node-pre-gyp@^1.0.7":
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.7.tgz#a26919cac6595662703330d1820a0ca206f45521"
|
||||
integrity sha512-PplSvl4pJ5N3BkVjAdDzpPhVUPdC73JgttkR+LnBx2OORC1GCQsBjUeEuipf9uOaAM1SbxcdZFfR3KDTKm2S0A==
|
||||
@ -905,10 +905,10 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/node@*":
|
||||
version "16.11.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.9.tgz#879be3ad7af29f4c1a5c433421bf99fab7047185"
|
||||
integrity sha512-MKmdASMf3LtPzwLyRrFjtFFZ48cMf8jmX5VRYrDQiJa8Ybu5VAmkqBWqKU8fdCwD8ysw4mQ9nrEHvzg6gunR7A==
|
||||
"@types/node@*", "@types/node@^16.11.10":
|
||||
version "16.11.10"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.10.tgz#2e3ad0a680d96367103d3e670d41c2fed3da61ae"
|
||||
integrity sha512-3aRnHa1KlOEEhJ6+CvyHKK5vE9BcLGjtUpwvqYLRvYNQKMfabu3BwfJaA/SLW8dxe28LsNDjtHwePTuzn3gmOA==
|
||||
|
||||
"@types/nodemailer@^6.4.0":
|
||||
version "6.4.4"
|
||||
@ -974,9 +974,9 @@
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/sass@^1.16.0":
|
||||
version "1.43.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/sass/-/sass-1.43.0.tgz#b4cebe057d887ed66dc6813fd6cbef22043057bb"
|
||||
integrity sha512-DPSXNJ1rYLo88GyF9tuB4bsYGfpKI1a4+wOQmc+LI1SUoocm9QLRSpz0GxxuyjmJsYFIQo/dDlRSSpIXngff+w==
|
||||
version "1.43.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/sass/-/sass-1.43.1.tgz#86bb0168e9e881d7dade6eba16c9ed6d25dc2f68"
|
||||
integrity sha512-BPdoIt1lfJ6B7rw35ncdwBZrAssjcwzI5LByIrYs+tpXlj/CAkuVdRsgZDdP4lq5EjyWzwxZCqAoFyHKFwp32g==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
@ -1277,13 +1277,13 @@ arg@^4.1.0:
|
||||
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
|
||||
|
||||
argon2@^0.28.2:
|
||||
version "0.28.2"
|
||||
resolved "https://registry.yarnpkg.com/argon2/-/argon2-0.28.2.tgz#b583e4ef5b052a83bfe146752844b9fa526dba29"
|
||||
integrity sha512-8oRk3kPlL0lLletENzhpbF9zoZJqvIHwTkjBseMrg1uD4gBMqhqnjJz1z3lEtwT0oqQAEkEwsEpsjaQBBRHcWw==
|
||||
version "0.28.3"
|
||||
resolved "https://registry.yarnpkg.com/argon2/-/argon2-0.28.3.tgz#e5234eccf20a643ffc3b1bbd1aa9e81092e0d8e9"
|
||||
integrity sha512-NkEJOImg+T7nnkx6/Fy8EbjZsF20hbBBKdVP/YUxujuLTAjIODmrFeY4vVpekKwGAGDm6roXxluFQ+CIaoVrbg==
|
||||
dependencies:
|
||||
"@mapbox/node-pre-gyp" "^1.0.1"
|
||||
"@mapbox/node-pre-gyp" "^1.0.7"
|
||||
"@phc/format" "^1.0.0"
|
||||
node-addon-api "^3.0.2"
|
||||
node-addon-api "^4.2.0"
|
||||
opencollective-postinstall "^2.0.3"
|
||||
|
||||
argparse@^1.0.7:
|
||||
@ -1741,9 +1741,9 @@ camelcase@^6.2.0:
|
||||
integrity sha512-tVI4q5jjFV5CavAU8DXfza/TJcZutVKo/5Foskmsqcm0MsL91moHvwiGNnqaa2o6PF/7yT5ikDRcVcl8Rj6LCA==
|
||||
|
||||
caniuse-lite@^1.0.30001280:
|
||||
version "1.0.30001282"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001282.tgz#38c781ee0a90ccfe1fe7fefd00e43f5ffdcb96fd"
|
||||
integrity sha512-YhF/hG6nqBEllymSIjLtR2iWDDnChvhnVJqp+vloyt2tEHFG1yBR+ac2B/rOw0qOK0m0lEXU2dv4E/sMk5P9Kg==
|
||||
version "1.0.30001283"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001283.tgz#8573685bdae4d733ef18f78d44ba0ca5fe9e896b"
|
||||
integrity sha512-9RoKo841j1GQFSJz/nCXOj0sD7tHBtlowjYlrqIUS812x9/emfBLBt6IyMz1zIaYc/eRL8Cs6HPUVi2Hzq4sIg==
|
||||
|
||||
caw@^2.0.0, caw@^2.0.1:
|
||||
version "2.0.1"
|
||||
@ -1838,9 +1838,9 @@ ci-info@^2.0.0:
|
||||
integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==
|
||||
|
||||
ci-info@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.2.0.tgz#2876cb948a498797b5236f0095bc057d0dca38b6"
|
||||
integrity sha512-dVqRX7fLUm8J6FgHJ418XuIgDLZDkYcDFTeL6TA2gt5WlIZUQrrH6EZrNClwT/H0FateUsZkGIOPRrLbP+PR9A==
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.3.0.tgz#b4ed1fb6818dea4803a55c623041f9165d2066b2"
|
||||
integrity sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw==
|
||||
|
||||
cjs-module-lexer@^1.0.0:
|
||||
version "1.2.2"
|
||||
@ -2273,9 +2273,9 @@ debug@2.6.9, debug@^2.6.9:
|
||||
ms "2.0.0"
|
||||
|
||||
debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2:
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
|
||||
integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
|
||||
version "4.3.3"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664"
|
||||
integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==
|
||||
dependencies:
|
||||
ms "2.1.2"
|
||||
|
||||
@ -2624,9 +2624,9 @@ ee-first@1.1.1:
|
||||
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
|
||||
|
||||
electron-to-chromium@^1.3.896:
|
||||
version "1.3.904"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.904.tgz#52a353994faeb0f2a9fab3606b4e0614d1af7b58"
|
||||
integrity sha512-x5uZWXcVNYkTh4JubD7KSC1VMKz0vZwJUqVwY3ihsW0bst1BXDe494Uqbg3Y0fDGVjJqA8vEeGuvO5foyH2+qw==
|
||||
version "1.4.4"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.4.tgz#57311918524c1a26878c330537f967804d43788a"
|
||||
integrity sha512-teHtgwcmVcL46jlFvAaqjyiTLWuMrUQO1JqV303JKB4ysXG6m8fXSFhbjal9st0r9mNskI22AraJZorb1VcLVg==
|
||||
|
||||
emittery@^0.8.1:
|
||||
version "0.8.1"
|
||||
@ -4260,9 +4260,9 @@ is-string@^1.0.5, is-string@^1.0.7:
|
||||
has-tostringtag "^1.0.0"
|
||||
|
||||
is-svg@^4.2.1:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-4.3.1.tgz#8c63ec8c67c8c7f0a8de0a71c8c7d58eccf4406b"
|
||||
integrity sha512-h2CGs+yPUyvkgTJQS9cJzo9lYK06WgRiXUqBBHtglSzVKAuH4/oWsqk7LGfbSa1hGk9QcZ0SyQtVggvBA8LZXA==
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-4.3.2.tgz#a119e9932e1af53f6be1969d1790d6cc5fd947d3"
|
||||
integrity sha512-mM90duy00JGMyjqIVHu9gNTjywdZV+8qNasX8cm/EEYZ53PHDgajvbBwNVvty5dwSAxLUD3p3bdo+7sR/UMrpw==
|
||||
dependencies:
|
||||
fast-xml-parser "^3.19.0"
|
||||
|
||||
@ -5092,9 +5092,9 @@ lru-cache@^6.0.0:
|
||||
yallist "^4.0.0"
|
||||
|
||||
lucide@^0.16.17:
|
||||
version "0.16.17"
|
||||
resolved "https://registry.yarnpkg.com/lucide/-/lucide-0.16.17.tgz#52466954e97ea3efac0191b2452f42932d41fdf7"
|
||||
integrity sha512-zAB+aRo6/naheCMQ8AYNAXCnsxCZQb7pI52taqjcfBDPkH3jJnx+b5UnvR4kncVPw4t7/U4GARH/23PleFgnHQ==
|
||||
version "0.16.19"
|
||||
resolved "https://registry.yarnpkg.com/lucide/-/lucide-0.16.19.tgz#ac79f36dc544f34fc3930a3bac82226c2f7aff18"
|
||||
integrity sha512-OoA7LlDTo4Q65cwEgDZBlsvd3veRz5dnAeJZQnvDSjqldUCocBZOxupbRFSkGjKncII+jColKBQZBTvvdtTnhg==
|
||||
|
||||
magic-string@^0.25.7:
|
||||
version "0.25.7"
|
||||
@ -5289,46 +5289,46 @@ minizlib@^2.1.1:
|
||||
minipass "^3.0.0"
|
||||
yallist "^4.0.0"
|
||||
|
||||
mjml-accordion@4.10.4:
|
||||
version "4.10.4"
|
||||
resolved "https://registry.yarnpkg.com/mjml-accordion/-/mjml-accordion-4.10.4.tgz#f28933f737a54fd3a995e0024f8c85b6b95f6fc4"
|
||||
integrity sha512-C9yyHcQt1yh9c0nnq/VaUiMCScJVY0HGvWgsDhwG6VGoUttPDpGZLHf9aWc4ypkLTrA7rk7PSn1XDmfYCbnFbg==
|
||||
mjml-accordion@4.11.0:
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-accordion/-/mjml-accordion-4.11.0.tgz#91893ed5dcb5f81f8876cc0117d16c9655adf75b"
|
||||
integrity sha512-u6cPMl4z8JeRIq0sGHWfzwE5SqwBhSPHJ8wElhfU7CK6gAoUTz4BIL/03da2whXk0S34n5CSU453JpuWV+3bQQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.14.6"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.10.4"
|
||||
mjml-core "4.11.0"
|
||||
|
||||
mjml-body@4.10.4:
|
||||
version "4.10.4"
|
||||
resolved "https://registry.yarnpkg.com/mjml-body/-/mjml-body-4.10.4.tgz#a0e41960ab0734de3a351041588e38d53ac21fbd"
|
||||
integrity sha512-MN4t9ZR09LXVRmHI/FrSm04gTvfEqasZ+1GAbbmb7G0X/NyDXuh2DZTSlFS9gxmYwDYRQeZbOixv1T4MxpbT2Q==
|
||||
mjml-body@4.11.0:
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-body/-/mjml-body-4.11.0.tgz#486b0e4a96b720d47ad1bac36c70d41915882482"
|
||||
integrity sha512-+SQXOjKOr9IMblmslrUbv+ahF/SkqT7mAuix1P/F9ev3aS+WN7Gy/lyJLaq1zhSQCmzrBDovHez3bJ7NGToU0g==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.14.6"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.10.4"
|
||||
mjml-core "4.11.0"
|
||||
|
||||
mjml-button@4.10.4:
|
||||
version "4.10.4"
|
||||
resolved "https://registry.yarnpkg.com/mjml-button/-/mjml-button-4.10.4.tgz#df6d6aa46c196703c4cafcb5b3e0460baa93f11b"
|
||||
integrity sha512-gGV2X/vrjhfHgQ+e3OH7WcyKnsYZs4PuI7+jl7ajO66veyRoZZteyeAtrEu+NHub0iBbQ/ft12B7yuCf5vgBhw==
|
||||
mjml-button@4.11.0:
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-button/-/mjml-button-4.11.0.tgz#f91791cfedc3ec32a8d64c1cdab2d71be376300b"
|
||||
integrity sha512-v/MwA2Kq2MlHJwrajdAqUihjAQivD8FpkpOAcqdgqI1ffaEDzd6FGFt5qOtVD9BncChQ4a51haOnPd+kbBLlCA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.14.6"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.10.4"
|
||||
mjml-core "4.11.0"
|
||||
|
||||
mjml-carousel@4.10.4:
|
||||
version "4.10.4"
|
||||
resolved "https://registry.yarnpkg.com/mjml-carousel/-/mjml-carousel-4.10.4.tgz#ad964c9c691b6db105b7fd09be916f31f61c08f8"
|
||||
integrity sha512-jwt63Gz27iwnTSHWY3W3jiBlCvKar/6SU/fezLZPdwcEl1dF+kBN1zQYwqO/uXBcxLnar0cg1p0jKU5SfL3XpQ==
|
||||
mjml-carousel@4.11.0:
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-carousel/-/mjml-carousel-4.11.0.tgz#c848c50437be95d9956b22d8610b259e68be9a5a"
|
||||
integrity sha512-/owKKSg3DNes1rauPPhlGCFdZ4zzoxdztPLGOm9TSjkbL5q2cN9NMfzNdTYLnnVG5G5XWX0THjDeeq+bGnHw8w==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.14.6"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.10.4"
|
||||
mjml-core "4.11.0"
|
||||
|
||||
mjml-cli@4.10.4:
|
||||
version "4.10.4"
|
||||
resolved "https://registry.yarnpkg.com/mjml-cli/-/mjml-cli-4.10.4.tgz#aa71e57d4a23a0df8f6b887700c2c31a10d4330a"
|
||||
integrity sha512-FzNuKJ3iqYslejRGJa88IIHtUrUNtox4+bCaroVubvxaLkosi0phyg1g5ndQgAFfX+TKmMslgXnFUsbOxvOeOQ==
|
||||
mjml-cli@4.11.0:
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-cli/-/mjml-cli-4.11.0.tgz#192713b3a97594a9366b8f97c8d8b9572eba147f"
|
||||
integrity sha512-jgkxNY+sY+CwiUlO6VX8cwVjlLBXIzE7X4fXkQ2RjXpbAvsN8uDafST8oL8PDlxNIjOwK6YJySzZSLXSKTF5Zg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.14.6"
|
||||
chokidar "^3.0.0"
|
||||
@ -5336,25 +5336,25 @@ mjml-cli@4.10.4:
|
||||
html-minifier "^4.0.0"
|
||||
js-beautify "^1.6.14"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.10.4"
|
||||
mjml-migrate "4.10.4"
|
||||
mjml-parser-xml "4.10.4"
|
||||
mjml-validator "4.10.4"
|
||||
mjml-core "4.11.0"
|
||||
mjml-migrate "4.11.0"
|
||||
mjml-parser-xml "4.11.0"
|
||||
mjml-validator "4.11.0"
|
||||
yargs "^16.1.0"
|
||||
|
||||
mjml-column@4.10.4:
|
||||
version "4.10.4"
|
||||
resolved "https://registry.yarnpkg.com/mjml-column/-/mjml-column-4.10.4.tgz#53a1b5a34a04f3760129e7ac215e153647116eb2"
|
||||
integrity sha512-XUgrGRsxIWR0UBXI2jBRrGqg/RZSeiFBQSB6+oQ7Jz7VaWHD+zBShJF3FkeKw9oTA7Deq1D9CY8Hd4BJN+7tEQ==
|
||||
mjml-column@4.11.0:
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-column/-/mjml-column-4.11.0.tgz#6fafea6e09e6463a7656cdfe5d3c61bc08dd18fe"
|
||||
integrity sha512-yuupexywYXuXTvxxGLPZw6S/D6fz4b41ZarOkEPW8Pbj7FkfnBTO3QokAS5KapJ9x6sIAVs+cCgUul87dwOBzg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.14.6"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.10.4"
|
||||
mjml-core "4.11.0"
|
||||
|
||||
mjml-core@4.10.4:
|
||||
version "4.10.4"
|
||||
resolved "https://registry.yarnpkg.com/mjml-core/-/mjml-core-4.10.4.tgz#ed165b692498d7d729105fb3ec2cd10aad374f89"
|
||||
integrity sha512-y8qtLr+jzoRxNbiQ2/yLfOMtjsCEKhj7t8KOZmUH4wvP/V8VtzR3+5uPyIhquKEr0FKS3gxPYlErW/9/K8K8MA==
|
||||
mjml-core@4.11.0:
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-core/-/mjml-core-4.11.0.tgz#e673bc70250861e2c687cb8834e9af31b690eea6"
|
||||
integrity sha512-UTI1exu9/0lOF40aacAzC9A7RrJPcay7WYurYsb+X2LbDyfBlSx5ZCtuUTHwLfIz3PL6rSg8yrZ3KtlmhQJ/kQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.14.6"
|
||||
cheerio "1.0.0-rc.10"
|
||||
@ -5363,263 +5363,263 @@ mjml-core@4.10.4:
|
||||
js-beautify "^1.6.14"
|
||||
juice "^7.0.0"
|
||||
lodash "^4.17.21"
|
||||
mjml-migrate "4.10.4"
|
||||
mjml-parser-xml "4.10.4"
|
||||
mjml-validator "4.10.4"
|
||||
mjml-migrate "4.11.0"
|
||||
mjml-parser-xml "4.11.0"
|
||||
mjml-validator "4.11.0"
|
||||
|
||||
mjml-divider@4.10.4:
|
||||
version "4.10.4"
|
||||
resolved "https://registry.yarnpkg.com/mjml-divider/-/mjml-divider-4.10.4.tgz#7c0ced8709be4b8760d1562939ca4ec734cf6e86"
|
||||
integrity sha512-HMAwrSSsnHICr4gosyfdNKqzr6optlOTwT37Rpl15QDBrmJa3ph9cuIH7GhlhIlJB4AIw/dVqbNojvI9tPqphA==
|
||||
mjml-divider@4.11.0:
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-divider/-/mjml-divider-4.11.0.tgz#4eb737f0d369d6736c022cd8bc76e7a27c8dd28f"
|
||||
integrity sha512-jS8Sa7uuFTmThtrcV3FICtbqmA0yJplldaNJwW5upiq7MEXuSwLNzjNeodaTpsVWevf/stE3C4lBA3+V4ascMg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.14.6"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.10.4"
|
||||
mjml-core "4.11.0"
|
||||
|
||||
mjml-group@4.10.4:
|
||||
version "4.10.4"
|
||||
resolved "https://registry.yarnpkg.com/mjml-group/-/mjml-group-4.10.4.tgz#4515f876c919e276c4188deb74f4a231a6e21edc"
|
||||
integrity sha512-BmrByiEFvJG97gph9ItYgtGWhDGg5kE0Zg5oda3+m4OfXeyEb6NzdqlgMM9VgRiyqnFA3P0UnwMqRpidOMNohg==
|
||||
mjml-group@4.11.0:
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-group/-/mjml-group-4.11.0.tgz#fe57cd63f1ac12187124122268820ace785f8b7f"
|
||||
integrity sha512-Yv1ZcPNk4bxOK/eol6bjJvRJBaOCzVY88QO8IfCQifAZuiXhatxwsuwaRKNcTi4cB+IHwl4JguZHgsFRi/Gf/g==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.14.6"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.10.4"
|
||||
mjml-core "4.11.0"
|
||||
|
||||
mjml-head-attributes@4.10.4:
|
||||
version "4.10.4"
|
||||
resolved "https://registry.yarnpkg.com/mjml-head-attributes/-/mjml-head-attributes-4.10.4.tgz#919cc2b33239ca6dad61fa71997a5a6980a533de"
|
||||
integrity sha512-JnL/glaPmtLVEdFashvrXi7qcFpodpt830J+Camabc9LQNGYS/7d4PqhMNAmVI7BAaxBgmhjHGvCeUDkwg8jHg==
|
||||
mjml-head-attributes@4.11.0:
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-head-attributes/-/mjml-head-attributes-4.11.0.tgz#9f493cb3a78deed5d7ac25d7098440ed274be274"
|
||||
integrity sha512-azeDRLGH7cU6PoXLd08E/H3UDHFsblBTcme++OWrj5B21vaFguOBySO2eWFTyWLthy+xst8FftWV46f8BWvciw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.14.6"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.10.4"
|
||||
mjml-core "4.11.0"
|
||||
|
||||
mjml-head-breakpoint@4.10.4:
|
||||
version "4.10.4"
|
||||
resolved "https://registry.yarnpkg.com/mjml-head-breakpoint/-/mjml-head-breakpoint-4.10.4.tgz#43e6d388c7d9fb195ef74cf40e285c4cc25dfb50"
|
||||
integrity sha512-XbHo/1RkGDs5Nh64c1AOfXFIkJpCLuCVQwwPpM0FwKJnUh7jPTykJKHj5FqvQLHNzsNCgpvs4fKchDte77AGsQ==
|
||||
mjml-head-breakpoint@4.11.0:
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-head-breakpoint/-/mjml-head-breakpoint-4.11.0.tgz#fb39f8919dc64085e307b0db67d00a2d3c7512f6"
|
||||
integrity sha512-OQ/WpXHagGoiUmBjoMHuxdigyLQrHNL6+Op0LHO87vN1GH2ap4YLcSSegIsThQLi/I7V4/JFZbzS5jV4xd4SMg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.14.6"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.10.4"
|
||||
mjml-core "4.11.0"
|
||||
|
||||
mjml-head-font@4.10.4:
|
||||
version "4.10.4"
|
||||
resolved "https://registry.yarnpkg.com/mjml-head-font/-/mjml-head-font-4.10.4.tgz#02fadf80df5e8c3a23fc432f7288f786f825f4c1"
|
||||
integrity sha512-P1pXsZtntkpdWcpVe1cyWE9BzJ17tfq4WxtaFtRpnz9L/w3SvTm/OdyJT+GI3LS5D9HoR0dkk6kC9HvPs5aDvw==
|
||||
mjml-head-font@4.11.0:
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-head-font/-/mjml-head-font-4.11.0.tgz#25379dd23b597522dc6a944af21c0cf725a73ee5"
|
||||
integrity sha512-AJQ9lhNzNr0hwPjUID76LNna3U8ycmFH1lyipZOWHTivOVGkSYs8PaboxORfU/QAEDKo/p8LafBAoDPEdSigxA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.14.6"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.10.4"
|
||||
mjml-core "4.11.0"
|
||||
|
||||
mjml-head-html-attributes@4.10.4:
|
||||
version "4.10.4"
|
||||
resolved "https://registry.yarnpkg.com/mjml-head-html-attributes/-/mjml-head-html-attributes-4.10.4.tgz#612c0ea67cd118998da1005eb89f96ef0ba19007"
|
||||
integrity sha512-abF4nNJeXEpx6zE2TwmDTbuDHwe9787ecPQ/5uGK37WbNCbmg2D99XtkdK+96VpdL+1enT5q0ztasdg9ixOBZg==
|
||||
mjml-head-html-attributes@4.11.0:
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-head-html-attributes/-/mjml-head-html-attributes-4.11.0.tgz#ad0b1e3a0496a8e07103d803ac09aeffc0e2aa65"
|
||||
integrity sha512-Gew9EAuqFu9QR3g+8FyE9WHNMszx1ejzo8dRDIuTaBCDEpaIaPVO7G5rGhT7ADqZufeYtRhBXfXjcShPD/a30g==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.14.6"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.10.4"
|
||||
mjml-core "4.11.0"
|
||||
|
||||
mjml-head-preview@4.10.4:
|
||||
version "4.10.4"
|
||||
resolved "https://registry.yarnpkg.com/mjml-head-preview/-/mjml-head-preview-4.10.4.tgz#ba2d8938d88d05c117a8a8b42adf59e85c6d3d4c"
|
||||
integrity sha512-Oe5Ecn+9AnEgo/RWxTEci0OK/7+ft/dUoma0k/H0Fv1CCKcyrMCFPr97A5hEeooGilxU+Jz0/gbt9PXQKpHKLA==
|
||||
mjml-head-preview@4.11.0:
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-head-preview/-/mjml-head-preview-4.11.0.tgz#2dccc5dfae58a9ef3d67c4229909313c77eb9219"
|
||||
integrity sha512-f79+OtwC8CYcQpglGNfm3s74qVecdwLlDm9DD/LJvZwIdaALeIfZF36nz6+rtCLMhND+wxgqxqkmbbuS6AbO+A==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.14.6"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.10.4"
|
||||
mjml-core "4.11.0"
|
||||
|
||||
mjml-head-style@4.10.4:
|
||||
version "4.10.4"
|
||||
resolved "https://registry.yarnpkg.com/mjml-head-style/-/mjml-head-style-4.10.4.tgz#3ae12c2ea254cec51595d05acd5b264f6badd61f"
|
||||
integrity sha512-V3Tg1VKVqMGNLqOGXy7vQk/Rp52iidR56Vtkp5O0hITd072URyOLw1+qL4rPDqaYrbN8keK7p4T8nFL1QySA6w==
|
||||
mjml-head-style@4.11.0:
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-head-style/-/mjml-head-style-4.11.0.tgz#b02f342ecfa946626c9b50e6b49cc8a094815dba"
|
||||
integrity sha512-jw0r9Ld56SkJmDUBx+27xOH+oY2O06FBcKSJIkZLJ/sefbjgJa2/ti2eso/LeGgrmTw8zge7g1pp0Fl+kPjsGA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.14.6"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.10.4"
|
||||
mjml-core "4.11.0"
|
||||
|
||||
mjml-head-title@4.10.4:
|
||||
version "4.10.4"
|
||||
resolved "https://registry.yarnpkg.com/mjml-head-title/-/mjml-head-title-4.10.4.tgz#4dfbdfc3d2f4a415543ab55dd1a9c782239fa71f"
|
||||
integrity sha512-ixVNZdisq/UNUiwiCrzXkJGoIS4GfRoL/rdZLaxfJvccdJxT9LzcNvm/KC0NsWC4kjAvipo1Wo54kmaLwXy/+Q==
|
||||
mjml-head-title@4.11.0:
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-head-title/-/mjml-head-title-4.11.0.tgz#8d322e576675fa28e4425789182982e078c91895"
|
||||
integrity sha512-mjYsKoE9+5LI8V0mK9SAKVKspF19TVius2q5I1FXLRdBmt4Ei87CdrSui+iixGrug0Lwt4P2W3+mK8kVGNnJAQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.14.6"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.10.4"
|
||||
mjml-core "4.11.0"
|
||||
|
||||
mjml-head@4.10.4:
|
||||
version "4.10.4"
|
||||
resolved "https://registry.yarnpkg.com/mjml-head/-/mjml-head-4.10.4.tgz#21f5d67915bb594976af891d6358240170de2169"
|
||||
integrity sha512-hADFAWgptBHnESft9gPt5JuOmlDu+N+F8gNVNjjISQt/k4ZQkyambAAuJvds/Ahc14LOgag6CWLzPQ4B0HPXPA==
|
||||
mjml-head@4.11.0:
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-head/-/mjml-head-4.11.0.tgz#3d3bbbd05beb4153936f7544ea511ef6a8a27428"
|
||||
integrity sha512-KNaSsOlf5FNwHyZQD6YNZN2Eo/o7n+mZISFLyp9MvNFaT3NKIRJDaInD1WjN+w2aHdXAw4sDV3+/9/EET2bh1A==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.14.6"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.10.4"
|
||||
mjml-core "4.11.0"
|
||||
|
||||
mjml-hero@4.10.4:
|
||||
version "4.10.4"
|
||||
resolved "https://registry.yarnpkg.com/mjml-hero/-/mjml-hero-4.10.4.tgz#a0bf6df250e8d28c59849141b98873542b9451f0"
|
||||
integrity sha512-rk10wj7bCVkgN+Bd670BGwE8iO9u4XDHPhyx8h8eZp/PunIST1CGPYZUQ5PVzkPhnJlhI3MOtUYmpd7U1cMRIA==
|
||||
mjml-hero@4.11.0:
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-hero/-/mjml-hero-4.11.0.tgz#feddefdfc8a3a531a742ccbba5abaf571a51d9c3"
|
||||
integrity sha512-tZDCGjQrDICwbsDnLvfVEsQ5+GdrIozB3oO3NxC6m2Eq04VKNBkIqq+QvJyouOxzJ3CZgO2B4rylzxc6YXuHsw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.14.6"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.10.4"
|
||||
mjml-core "4.11.0"
|
||||
|
||||
mjml-image@4.10.4:
|
||||
version "4.10.4"
|
||||
resolved "https://registry.yarnpkg.com/mjml-image/-/mjml-image-4.10.4.tgz#194b2800d1c5570a7d47ab0bad1188e7f54f7ead"
|
||||
integrity sha512-xW2SdVAmTA0ZuzWfCKDYJOMiFfdhiEJDQW1rz1L3NRqI/KemSOthDKlGaXzH/0DobKcYf2N22RSAOSYULDtmmw==
|
||||
mjml-image@4.11.0:
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-image/-/mjml-image-4.11.0.tgz#4a890a5f9747eb9115fc792ca5712367421af756"
|
||||
integrity sha512-qjvKS/x2arDNgKppPecmg69VyXdQbb1CNVqPwowJLyfjioTw4hxQ93suWoBKJJojRjkwauj9IRMC8TR9ffn0HQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.14.6"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.10.4"
|
||||
mjml-core "4.11.0"
|
||||
|
||||
mjml-migrate@4.10.4:
|
||||
version "4.10.4"
|
||||
resolved "https://registry.yarnpkg.com/mjml-migrate/-/mjml-migrate-4.10.4.tgz#d56e5688fbbf216ed5c7fb7132078027a1807d9c"
|
||||
integrity sha512-zBAwwhDRNA67S+U2Zb6zvQMZUW9wKGDdU7fi8tEwSH3YKNXTiXuEFFv4NuvkwPJuwmDSRbi7M2Oc50AkotJw+w==
|
||||
mjml-migrate@4.11.0:
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-migrate/-/mjml-migrate-4.11.0.tgz#266aa92369beb43c51d2e9fd606a06f1d52ebf70"
|
||||
integrity sha512-Y+9U4w9LwlTkfTkHX9GdalIQDO4JQxboG4PM8g7vRKNhKSZDZH8QSr0SNhX0+fMQmjb9b0nztWbMVVvNZfMoPQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.14.6"
|
||||
js-beautify "^1.6.14"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.10.4"
|
||||
mjml-parser-xml "4.10.4"
|
||||
mjml-core "4.11.0"
|
||||
mjml-parser-xml "4.11.0"
|
||||
yargs "^16.1.0"
|
||||
|
||||
mjml-navbar@4.10.4:
|
||||
version "4.10.4"
|
||||
resolved "https://registry.yarnpkg.com/mjml-navbar/-/mjml-navbar-4.10.4.tgz#4028c6bc40268ecd28afec131275775a774119c5"
|
||||
integrity sha512-IZP3o/V4nAG6k2OzUQ7hkWOiiBqVTGXkzLeUFNb5VAkAk67us4gjJu/w/SkRmZii4y8Mhbv99HZuB5G99LxLsA==
|
||||
mjml-navbar@4.11.0:
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-navbar/-/mjml-navbar-4.11.0.tgz#8459ecac109debf4ee744bb7020d886ad0c24bb4"
|
||||
integrity sha512-FEd+8RD6ra8652jXdMbhfhzT1YY8TA30c34qjJ+ULlFgvLH4GNnYRIOHp/Mgd/L38Qf3bhK/rK9ELBidJ36dLQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.14.6"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.10.4"
|
||||
mjml-core "4.11.0"
|
||||
|
||||
mjml-parser-xml@4.10.4:
|
||||
version "4.10.4"
|
||||
resolved "https://registry.yarnpkg.com/mjml-parser-xml/-/mjml-parser-xml-4.10.4.tgz#faf885a58cbba8a74a23e3d12209c4bcf3b952d4"
|
||||
integrity sha512-yN5RtUnRiZuTzbXD+r6e1IfNNyggOQcTIVadCANQjy2JLzn4FjtbXzk64cRLOPaSCKm2+eUqCE5fmOL+DMLCPg==
|
||||
mjml-parser-xml@4.11.0:
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-parser-xml/-/mjml-parser-xml-4.11.0.tgz#384e50834ba50d3060b87e54a094be2c7ed5bc81"
|
||||
integrity sha512-3toQ9UKyfzXWXJ7PlTExBjzGXAzJNORv39sorPv5sG2KJsvPC6NE+e+/1GyqYFhk4JffHoDfd4mQ6w050qYATQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.14.6"
|
||||
detect-node "2.0.4"
|
||||
htmlparser2 "^4.1.0"
|
||||
lodash "^4.17.15"
|
||||
|
||||
mjml-preset-core@4.10.4:
|
||||
version "4.10.4"
|
||||
resolved "https://registry.yarnpkg.com/mjml-preset-core/-/mjml-preset-core-4.10.4.tgz#8273e603f95951e9d1cd035a63f0692a7e21db0d"
|
||||
integrity sha512-FdX63CJbRpg72uEoJVOBTC/i8jd1g31akAheFTtb5/zew3sBIdPvkQGMkxY8Hn1eEZyduJiTc2SCmHiigrqD9g==
|
||||
mjml-preset-core@4.11.0:
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-preset-core/-/mjml-preset-core-4.11.0.tgz#edf93e678d3c955d01400df2fd3e36819bc25428"
|
||||
integrity sha512-HU9ZFzS+A7Dx0B/EbnqYjdcIQQuxpOVRfFReQBlllZYwjWaRUjDIeIjF+cdnCb9QveuSNo4msIG00h/MKPYiGQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.14.6"
|
||||
mjml-accordion "4.10.4"
|
||||
mjml-body "4.10.4"
|
||||
mjml-button "4.10.4"
|
||||
mjml-carousel "4.10.4"
|
||||
mjml-column "4.10.4"
|
||||
mjml-divider "4.10.4"
|
||||
mjml-group "4.10.4"
|
||||
mjml-head "4.10.4"
|
||||
mjml-head-attributes "4.10.4"
|
||||
mjml-head-breakpoint "4.10.4"
|
||||
mjml-head-font "4.10.4"
|
||||
mjml-head-html-attributes "4.10.4"
|
||||
mjml-head-preview "4.10.4"
|
||||
mjml-head-style "4.10.4"
|
||||
mjml-head-title "4.10.4"
|
||||
mjml-hero "4.10.4"
|
||||
mjml-image "4.10.4"
|
||||
mjml-navbar "4.10.4"
|
||||
mjml-raw "4.10.4"
|
||||
mjml-section "4.10.4"
|
||||
mjml-social "4.10.4"
|
||||
mjml-spacer "4.10.4"
|
||||
mjml-table "4.10.4"
|
||||
mjml-text "4.10.4"
|
||||
mjml-wrapper "4.10.4"
|
||||
mjml-accordion "4.11.0"
|
||||
mjml-body "4.11.0"
|
||||
mjml-button "4.11.0"
|
||||
mjml-carousel "4.11.0"
|
||||
mjml-column "4.11.0"
|
||||
mjml-divider "4.11.0"
|
||||
mjml-group "4.11.0"
|
||||
mjml-head "4.11.0"
|
||||
mjml-head-attributes "4.11.0"
|
||||
mjml-head-breakpoint "4.11.0"
|
||||
mjml-head-font "4.11.0"
|
||||
mjml-head-html-attributes "4.11.0"
|
||||
mjml-head-preview "4.11.0"
|
||||
mjml-head-style "4.11.0"
|
||||
mjml-head-title "4.11.0"
|
||||
mjml-hero "4.11.0"
|
||||
mjml-image "4.11.0"
|
||||
mjml-navbar "4.11.0"
|
||||
mjml-raw "4.11.0"
|
||||
mjml-section "4.11.0"
|
||||
mjml-social "4.11.0"
|
||||
mjml-spacer "4.11.0"
|
||||
mjml-table "4.11.0"
|
||||
mjml-text "4.11.0"
|
||||
mjml-wrapper "4.11.0"
|
||||
|
||||
mjml-raw@4.10.4:
|
||||
version "4.10.4"
|
||||
resolved "https://registry.yarnpkg.com/mjml-raw/-/mjml-raw-4.10.4.tgz#0584d55035c4012aabd8590c8c36ab8dfc6bc6b0"
|
||||
integrity sha512-O5HvmpLpLD9YESb3xXp1vMHObpUl7wF2AFcEp2MZBRJyyBQdUPLZsCGld5hQ462NWh/2WZjN/bWcUqgVqpnwyg==
|
||||
mjml-raw@4.11.0:
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-raw/-/mjml-raw-4.11.0.tgz#ccc09dcd0d65731957ab8013cc1f113017360673"
|
||||
integrity sha512-DyUwC/JrE8tF7v9XaKQpQ/yAg5tT2uAWSDI6J5x3t3TNFJmRPd3iorSu8v6t1s3OiekuxfagsDDPLFuzpGwZSw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.14.6"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.10.4"
|
||||
mjml-core "4.11.0"
|
||||
|
||||
mjml-section@4.10.4:
|
||||
version "4.10.4"
|
||||
resolved "https://registry.yarnpkg.com/mjml-section/-/mjml-section-4.10.4.tgz#81d5a527d0cc52201d51609e647697366d59edd9"
|
||||
integrity sha512-8J0gSi+B2Z5ag4ECUbZ1I2/1WE452pdfkwO2wXdD/gGrqkIyWC9WQKGlbT7BHEmDOKalElBTV3YwwFFhbqYWwg==
|
||||
mjml-section@4.11.0:
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-section/-/mjml-section-4.11.0.tgz#2728bfb5de97ec6ea898528e3021016b9c9c2ac6"
|
||||
integrity sha512-OpinxE019Z1symrEFku3UddsvSql2aolcAiOThFuAAXz3+tUuooofGMy/XyyMOuOnktOx/5PMcVFsV84/Jye/g==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.14.6"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.10.4"
|
||||
mjml-core "4.11.0"
|
||||
|
||||
mjml-social@4.10.4:
|
||||
version "4.10.4"
|
||||
resolved "https://registry.yarnpkg.com/mjml-social/-/mjml-social-4.10.4.tgz#8e859f79317184827cd2cecdbc2e2624dae45997"
|
||||
integrity sha512-Tpc0CH8VBa6X66gY81BKZfzF2E6Mj6lE/R1V9h2CHG5cXJSm1TSd27KkiurNMyuEwRLQ51r5X1AlyL4LTrqt2w==
|
||||
mjml-social@4.11.0:
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-social/-/mjml-social-4.11.0.tgz#62bf699eb0f86a3d803f95c4ccbd623b32135a8e"
|
||||
integrity sha512-tM5njGtang0VRWt+XCuMTnZP2IJehUd+kdsC7CnMKQhYI2X4vzEaOBLgXRWvhcT4pSBzEJkXB9sQ+y7JmLO10g==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.14.6"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.10.4"
|
||||
mjml-core "4.11.0"
|
||||
|
||||
mjml-spacer@4.10.4:
|
||||
version "4.10.4"
|
||||
resolved "https://registry.yarnpkg.com/mjml-spacer/-/mjml-spacer-4.10.4.tgz#5ab85acc0900e5bc6fb2309b2904f07421adb301"
|
||||
integrity sha512-NvhOZpPHy34RhruUNZYPJuX6TtKjLYfayuZNZiRaeWnmoqWOtTqWnTqx+85bM2yTuqtAmLVv8lWdJlaxkFnVxQ==
|
||||
mjml-spacer@4.11.0:
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-spacer/-/mjml-spacer-4.11.0.tgz#a7f1f95811a026b10388b66cc5a987a54ca3a361"
|
||||
integrity sha512-9s6PjFgznKEFgFZCSa4vTYVDQ6kRH5ucMHGraoHS4VMZLA8QIaVakHd4Sl0n+SGWCqair4xZ3vYYMhOBKIXmMw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.14.6"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.10.4"
|
||||
mjml-core "4.11.0"
|
||||
|
||||
mjml-table@4.10.4:
|
||||
version "4.10.4"
|
||||
resolved "https://registry.yarnpkg.com/mjml-table/-/mjml-table-4.10.4.tgz#6d472bf0bcdace76f86aeefc5c94f95387691f77"
|
||||
integrity sha512-3MNgG0+W4B5lmnXUEA+XRMjIxpkRDPaCsQQs906CTSXKEN2rTKuiIRtL6ij7kjIWKPFRxpq1sh8+6ueQd5GvRw==
|
||||
mjml-table@4.11.0:
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-table/-/mjml-table-4.11.0.tgz#0675ccbeba5fcf181bfb62fc775eb77e0cd480fb"
|
||||
integrity sha512-ccniRz1MoDqKS6zoNM59xi81M7zy/tkvd/6weyH9XAZiU7ATgOxoBuIMy+d0uSTlKPWNwoi2FqR5YSedxT2YPw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.14.6"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.10.4"
|
||||
mjml-core "4.11.0"
|
||||
|
||||
mjml-text@4.10.4:
|
||||
version "4.10.4"
|
||||
resolved "https://registry.yarnpkg.com/mjml-text/-/mjml-text-4.10.4.tgz#4041dcba8d8e2dad2847c8d62a335595da81c00e"
|
||||
integrity sha512-r53M6bN6tphToWk+3v+yD7mP/5MhCU+OnkKyVASnOSkjB/EeRB8XmPloALJu8FB9sV2vTYEkCHWyVYj3G2Y0DA==
|
||||
mjml-text@4.11.0:
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-text/-/mjml-text-4.11.0.tgz#a61da306b31eb9b20633a2ec7427c7b98f94c090"
|
||||
integrity sha512-xrY5+uepxUg+q5KKuKX2s9KQnnlsMN5aoCt0JSbjt4H9ZwBpqXPD4Z2uJ8eZS33FvnXzUYPNVdKralvXnrsXKA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.14.6"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.10.4"
|
||||
mjml-core "4.11.0"
|
||||
|
||||
mjml-validator@4.10.4:
|
||||
version "4.10.4"
|
||||
resolved "https://registry.yarnpkg.com/mjml-validator/-/mjml-validator-4.10.4.tgz#ab5e3fe0fb148fedbe4fdce8ff7fa6c1224ebd50"
|
||||
integrity sha512-IZbnsA7BSmfUyl7NBLalGSuK0+LazbfSguJimA6/5NyUOkNkUk6dqq6P0ZWslvRbg1aXsrLxgM9s3YJQgf3dvg==
|
||||
mjml-validator@4.11.0:
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-validator/-/mjml-validator-4.11.0.tgz#840b8c54d04f3755057230feefcd8762dcb125d9"
|
||||
integrity sha512-oDL9tHcL4PaCZMwH6T/lLpS7LV9cm9lIwnzJy+y5/S81MGMV6kPr6xXHgS0A01G7sseg6+rHgqBnUgzUob4Ilg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.14.6"
|
||||
|
||||
mjml-wrapper@4.10.4:
|
||||
version "4.10.4"
|
||||
resolved "https://registry.yarnpkg.com/mjml-wrapper/-/mjml-wrapper-4.10.4.tgz#7d4399782df5928e42fc3d33bc957de469142802"
|
||||
integrity sha512-mdjDizjoKQGnoSzuaDDGvjqeRcwZj4ftkqYRTZn/mR9OI8zWSY3CGqi9Q0PO2ZzgmugvzP3siuL+Te6sChWtRQ==
|
||||
mjml-wrapper@4.11.0:
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml-wrapper/-/mjml-wrapper-4.11.0.tgz#379dacf96c3c8a56d9d139a3e514664f8a7322aa"
|
||||
integrity sha512-WXGKxS+DlYalx5ofwq3bttj26BLEcaueeQ+0BcffXIELojUzd0+xTyX09v0QemUCBmKpHB2QaKMGhzYuSkupyg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.14.6"
|
||||
lodash "^4.17.21"
|
||||
mjml-core "4.10.4"
|
||||
mjml-section "4.10.4"
|
||||
mjml-core "4.11.0"
|
||||
mjml-section "4.11.0"
|
||||
|
||||
mjml@^4.6.2:
|
||||
version "4.10.4"
|
||||
resolved "https://registry.yarnpkg.com/mjml/-/mjml-4.10.4.tgz#9ec0ce624c91306e66f61549b453843770254d8f"
|
||||
integrity sha512-8oXXKlXcVi+QDfwz5GIi9r8mT61zWBePp5g6R89fBcQK25xL7pqAvgKR1j1FWpR+JnTkpToK2DU8QH6M/LVVFQ==
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/mjml/-/mjml-4.11.0.tgz#8aa411ccf61e283949fa8ab7acb62a2ed9d74477"
|
||||
integrity sha512-kYuCAds/8F7m7xNEs3TFkrc0jxnNYIkexQIUNByPLQJFoFRltpgXLSdoyreGAniQtDGITPE+p8FIocLUBKsOHg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.14.6"
|
||||
mjml-cli "4.10.4"
|
||||
mjml-core "4.10.4"
|
||||
mjml-migrate "4.10.4"
|
||||
mjml-preset-core "4.10.4"
|
||||
mjml-validator "4.10.4"
|
||||
mjml-cli "4.11.0"
|
||||
mjml-core "4.11.0"
|
||||
mjml-migrate "4.11.0"
|
||||
mjml-preset-core "4.11.0"
|
||||
mjml-validator "4.11.0"
|
||||
|
||||
mkdirp@^0.5.1, mkdirp@~0.5.1:
|
||||
version "0.5.5"
|
||||
@ -5709,10 +5709,10 @@ no-case@^2.2.0:
|
||||
dependencies:
|
||||
lower-case "^1.1.1"
|
||||
|
||||
node-addon-api@^3.0.2:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161"
|
||||
integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==
|
||||
node-addon-api@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.2.0.tgz#117cbb5a959dff0992e1c586ae0393573e4d2a87"
|
||||
integrity sha512-eazsqzwG2lskuzBqCGPi7Ac2UgOoMz8JVOXVhTvvPDYhthvNpefx8jWD8Np7Gv+2Sz0FlPWZk0nJV0z598Wn8Q==
|
||||
|
||||
node-fetch@^2.6.0, node-fetch@^2.6.5:
|
||||
version "2.6.6"
|
||||
@ -5756,9 +5756,9 @@ nodemailer@^3.1.1:
|
||||
integrity sha1-/r+sy0vSc2eEc6MJxstLSi88SOM=
|
||||
|
||||
nodemailer@^6.4.6:
|
||||
version "6.7.1"
|
||||
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.7.1.tgz#09f72f8b375f7b259291757007bcd902c0174c6e"
|
||||
integrity sha512-E1C8G3rnXrGjznwGP1k+OrW5k4rl0XtqTEB19f7vtJAMYwfxZVSsAu2iY5xJkrZsbVYr6PwwAwRmFlakPoFC0A==
|
||||
version "6.7.2"
|
||||
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.7.2.tgz#44b2ad5f7ed71b7067f7a21c4fedabaec62b85e0"
|
||||
integrity sha512-Dz7zVwlef4k5R71fdmxwR8Q39fiboGbu3xgswkzGwczUfjp873rVxt1O46+Fh0j1ORnAC6L9+heI8uUpO6DT7Q==
|
||||
|
||||
nodemon@^2.0.6:
|
||||
version "2.0.15"
|
||||
@ -6732,9 +6732,9 @@ rollup-pluginutils@^2.8.1, rollup-pluginutils@^2.8.2:
|
||||
estree-walker "^0.6.1"
|
||||
|
||||
rollup@^2.42.3:
|
||||
version "2.60.0"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.60.0.tgz#4ee60ab7bdd0356763f87d7099f413e5460fc193"
|
||||
integrity sha512-cHdv9GWd58v58rdseC8e8XIaPUo8a9cgZpnCMMDGZFDZKEODOiPPEQFXLriWr/TjXzhPPmG5bkAztPsOARIcGQ==
|
||||
version "2.60.1"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.60.1.tgz#4b34cd247f09b421f10a3c9286eda2ecf9972079"
|
||||
integrity sha512-akwfnpjY0rXEDSn1UTVfKXJhPsEBu+imi1gqBA1ZkHGydUnkV/fWCC90P7rDaLEW8KTwBcS1G3N4893Ndz+jwg==
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
@ -6785,9 +6785,9 @@ sander@^0.5.0:
|
||||
rimraf "^2.5.2"
|
||||
|
||||
sass@^1.32.12:
|
||||
version "1.43.4"
|
||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.43.4.tgz#68c7d6a1b004bef49af0d9caf750e9b252105d1f"
|
||||
integrity sha512-/ptG7KE9lxpGSYiXn7Ar+lKOv37xfWsZRtFYal2QHNigyVQDx685VFT/h7ejVr+R8w7H4tmUgtulsKl5YpveOg==
|
||||
version "1.43.5"
|
||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.43.5.tgz#25a9d91dd098793ef7229d7b04dd3daae2fc4a65"
|
||||
integrity sha512-WuNm+eAryMgQluL7Mbq9M4EruyGGMyal7Lu58FfnRMVWxgUzIvI7aSn60iNt3kn5yZBMR7G84fAGDcwqOF5JOg==
|
||||
dependencies:
|
||||
chokidar ">=3.0.0 <4.0.0"
|
||||
|
||||
@ -8016,14 +8016,14 @@ write-file-atomic@^3.0.0:
|
||||
typedarray-to-buffer "^3.1.5"
|
||||
|
||||
ws@^7.4.3, ws@^7.4.6:
|
||||
version "7.5.5"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.5.tgz#8b4bc4af518cfabd0473ae4f99144287b33eb881"
|
||||
integrity sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==
|
||||
version "7.5.6"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.6.tgz#e59fc509fb15ddfb65487ee9765c5a51dec5fe7b"
|
||||
integrity sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==
|
||||
|
||||
ws@^8.2.3:
|
||||
version "8.2.3"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba"
|
||||
integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==
|
||||
version "8.3.0"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.3.0.tgz#7185e252c8973a60d57170175ff55fdbd116070d"
|
||||
integrity sha512-Gs5EZtpqZzLvmIM59w4igITU57lrtYVFneaa434VROv4thzJyV6UjIL3D42lslWlI+D4KzLYnxSwtfuiO79sNw==
|
||||
|
||||
ws@~6.1.0:
|
||||
version "6.1.4"
|
||||
|
Loading…
Reference in New Issue
Block a user