Add svelte as a view engine to swaf #33
@ -263,9 +263,21 @@ export default abstract class Application implements Extendable<ApplicationCompo
|
|||||||
await MysqlConnectionManager.migrationCommand(mainCommandArgs);
|
await MysqlConnectionManager.migrationCommand(mainCommandArgs);
|
||||||
await this.stop();
|
await this.stop();
|
||||||
break;
|
break;
|
||||||
case 'pre-compile-views':
|
case 'pre-compile-views': {
|
||||||
await this.as(FrontendToolsComponent).preCompileViews(flags.watch);
|
// Prepare migrations
|
||||||
|
for (const migration of this.getMigrations()) {
|
||||||
|
new migration().registerModels?.();
|
||||||
|
}
|
||||||
|
// Prepare routes
|
||||||
|
for (const controller of this.controllers) {
|
||||||
|
controller.setupRoutes();
|
||||||
|
}
|
||||||
|
|
||||||
|
const frontendToolsComponent = this.as(FrontendToolsComponent);
|
||||||
|
frontendToolsComponent.setupGlobals();
|
||||||
|
await frontendToolsComponent.preCompileViews(flags.watch);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
logger.fatal('Unimplemented main command', mainCommand);
|
logger.fatal('Unimplemented main command', mainCommand);
|
||||||
break;
|
break;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import config from "config";
|
import config from "config";
|
||||||
import {MailTemplate} from "./mail/Mail";
|
import MailTemplate from "./mail/MailTemplate";
|
||||||
|
|
||||||
export const MAGIC_LINK_MAIL = new MailTemplate(
|
export const MAGIC_LINK_MAIL = new MailTemplate(
|
||||||
'magic_link',
|
'magic_link',
|
||||||
|
@ -3,7 +3,6 @@ import Migration, {MigrationType} from "./db/Migration";
|
|||||||
import ExpressAppComponent from "./components/ExpressAppComponent";
|
import ExpressAppComponent from "./components/ExpressAppComponent";
|
||||||
import RedisComponent from "./components/RedisComponent";
|
import RedisComponent from "./components/RedisComponent";
|
||||||
import MysqlComponent from "./components/MysqlComponent";
|
import MysqlComponent from "./components/MysqlComponent";
|
||||||
import NunjucksComponent from "./components/NunjucksComponent";
|
|
||||||
import LogRequestsComponent from "./components/LogRequestsComponent";
|
import LogRequestsComponent from "./components/LogRequestsComponent";
|
||||||
import MailComponent from "./components/MailComponent";
|
import MailComponent from "./components/MailComponent";
|
||||||
import SessionComponent from "./components/SessionComponent";
|
import SessionComponent from "./components/SessionComponent";
|
||||||
@ -34,6 +33,8 @@ import AddNameChangedAtToUsersMigration from "./auth/migrations/AddNameChangedAt
|
|||||||
import BackendController from "./helpers/BackendController";
|
import BackendController from "./helpers/BackendController";
|
||||||
import FrontendToolsComponent from "./components/FrontendToolsComponent";
|
import FrontendToolsComponent from "./components/FrontendToolsComponent";
|
||||||
import SvelteViewEngine from "./frontend/SvelteViewEngine";
|
import SvelteViewEngine from "./frontend/SvelteViewEngine";
|
||||||
|
import MailViewEngine from "./frontend/MailViewEngine";
|
||||||
|
import NunjucksViewEngine from "./frontend/NunjucksViewEngine";
|
||||||
import packageJson = require('./package.json');
|
import packageJson = require('./package.json');
|
||||||
|
|
||||||
export const MIGRATIONS = [
|
export const MIGRATIONS = [
|
||||||
@ -76,13 +77,16 @@ export default class TestApp extends Application {
|
|||||||
this.use(new ServeStaticDirectoryComponent('public'));
|
this.use(new ServeStaticDirectoryComponent('public'));
|
||||||
|
|
||||||
// Dynamic views and routes
|
// Dynamic views and routes
|
||||||
this.use(new NunjucksComponent(['test/views', 'views']));
|
this.use(new FrontendToolsComponent(
|
||||||
this.use(new FrontendToolsComponent(new SvelteViewEngine('build/views', 'public', 'views')));
|
'public',
|
||||||
|
new SvelteViewEngine('build/views', 'public', 'views', 'test/views'),
|
||||||
|
new NunjucksViewEngine('views', 'test/views'),
|
||||||
|
));
|
||||||
this.use(new PreviousUrlComponent());
|
this.use(new PreviousUrlComponent());
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
this.use(new MysqlComponent());
|
this.use(new MysqlComponent());
|
||||||
this.use(new MailComponent());
|
this.use(new MailComponent(new MailViewEngine('views', 'test/views')));
|
||||||
|
|
||||||
// Session
|
// Session
|
||||||
this.use(new RedisComponent());
|
this.use(new RedisComponent());
|
||||||
|
@ -9,11 +9,11 @@ import User from "./models/User";
|
|||||||
import ModelFactory from "../db/ModelFactory";
|
import ModelFactory from "../db/ModelFactory";
|
||||||
import UserEmail from "./models/UserEmail";
|
import UserEmail from "./models/UserEmail";
|
||||||
import MagicLinkController from "./magic_link/MagicLinkController";
|
import MagicLinkController from "./magic_link/MagicLinkController";
|
||||||
import {MailTemplate} from "../mail/Mail";
|
|
||||||
import {ADD_EMAIL_MAIL_TEMPLATE, REMOVE_PASSWORD_MAIL_TEMPLATE} from "../Mails";
|
import {ADD_EMAIL_MAIL_TEMPLATE, REMOVE_PASSWORD_MAIL_TEMPLATE} from "../Mails";
|
||||||
import AuthMagicLinkActionType from "./magic_link/AuthMagicLinkActionType";
|
import AuthMagicLinkActionType from "./magic_link/AuthMagicLinkActionType";
|
||||||
import UserNameComponent from "./models/UserNameComponent";
|
import UserNameComponent from "./models/UserNameComponent";
|
||||||
import Time from "../Time";
|
import Time from "../Time";
|
||||||
|
import MailTemplate from "../mail/MailTemplate";
|
||||||
|
|
||||||
export default class AccountController extends Controller {
|
export default class AccountController extends Controller {
|
||||||
|
|
||||||
|
@ -8,10 +8,10 @@ import Mail from "../mail/Mail";
|
|||||||
import Controller from "../Controller";
|
import Controller from "../Controller";
|
||||||
import config from "config";
|
import config from "config";
|
||||||
import Application from "../Application";
|
import Application from "../Application";
|
||||||
import NunjucksComponent from "../components/NunjucksComponent";
|
|
||||||
import AuthMethod from "./AuthMethod";
|
import AuthMethod from "./AuthMethod";
|
||||||
import {Session, SessionData} from "express-session";
|
import {Session, SessionData} from "express-session";
|
||||||
import UserNameComponent from "./models/UserNameComponent";
|
import UserNameComponent from "./models/UserNameComponent";
|
||||||
|
import MailComponent from "../components/MailComponent";
|
||||||
|
|
||||||
export default class AuthGuard {
|
export default class AuthGuard {
|
||||||
private readonly authMethods: AuthMethod<AuthProof<User>>[];
|
private readonly authMethods: AuthMethod<AuthProof<User>>[];
|
||||||
@ -145,12 +145,12 @@ export default class AuthGuard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (User.isApprovalMode()) {
|
if (User.isApprovalMode()) {
|
||||||
await new Mail(this.app.as(NunjucksComponent).getEnvironment(), PENDING_ACCOUNT_REVIEW_MAIL_TEMPLATE, {
|
await this.app.as(MailComponent).sendMail(new Mail(PENDING_ACCOUNT_REVIEW_MAIL_TEMPLATE, {
|
||||||
username: user.asOptional(UserNameComponent)?.getName() ||
|
username: user.asOptional(UserNameComponent)?.getName() ||
|
||||||
(await user.mainEmail.get())?.getOrFail('email') ||
|
(await user.mainEmail.get())?.getOrFail('email') ||
|
||||||
'Could not find an identifier',
|
'Could not find an identifier',
|
||||||
link: config.get<string>('public_url') + Controller.route('accounts-approval'),
|
link: config.get<string>('public_url') + Controller.route('accounts-approval'),
|
||||||
}).send(config.get<string>('app.contact_email'));
|
}), config.get<string>('app.contact_email'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,12 +8,12 @@ import Controller from "../../Controller";
|
|||||||
import geoip from "geoip-lite";
|
import geoip from "geoip-lite";
|
||||||
import MagicLinkController from "./MagicLinkController";
|
import MagicLinkController from "./MagicLinkController";
|
||||||
import Application from "../../Application";
|
import Application from "../../Application";
|
||||||
import {MailTemplate} from "../../mail/Mail";
|
|
||||||
import AuthMagicLinkActionType from "./AuthMagicLinkActionType";
|
import AuthMagicLinkActionType from "./AuthMagicLinkActionType";
|
||||||
import Validator, {EMAIL_REGEX} from "../../db/Validator";
|
import Validator, {EMAIL_REGEX} from "../../db/Validator";
|
||||||
import ModelFactory from "../../db/ModelFactory";
|
import ModelFactory from "../../db/ModelFactory";
|
||||||
import UserNameComponent from "../models/UserNameComponent";
|
import UserNameComponent from "../models/UserNameComponent";
|
||||||
import {Session} from "express-session";
|
import {Session} from "express-session";
|
||||||
|
import MailTemplate from "../../mail/MailTemplate";
|
||||||
|
|
||||||
export default class MagicLinkAuthMethod implements AuthMethod<MagicLink> {
|
export default class MagicLinkAuthMethod implements AuthMethod<MagicLink> {
|
||||||
public constructor(
|
public constructor(
|
||||||
|
@ -3,12 +3,11 @@ import {Request, Response} from "express";
|
|||||||
import MagicLinkWebSocketListener from "./MagicLinkWebSocketListener";
|
import MagicLinkWebSocketListener from "./MagicLinkWebSocketListener";
|
||||||
import {BadRequestError, NotFoundHttpError} from "../../HttpError";
|
import {BadRequestError, NotFoundHttpError} from "../../HttpError";
|
||||||
import Throttler from "../../Throttler";
|
import Throttler from "../../Throttler";
|
||||||
import Mail, {MailTemplate} from "../../mail/Mail";
|
import Mail from "../../mail/Mail";
|
||||||
import MagicLink from "../models/MagicLink";
|
import MagicLink from "../models/MagicLink";
|
||||||
import config from "config";
|
import config from "config";
|
||||||
import Application from "../../Application";
|
import Application from "../../Application";
|
||||||
import {ParsedUrlQueryInput} from "querystring";
|
import {ParsedUrlQueryInput} from "querystring";
|
||||||
import NunjucksComponent from "../../components/NunjucksComponent";
|
|
||||||
import User from "../models/User";
|
import User from "../models/User";
|
||||||
import AuthComponent, {AuthMiddleware} from "../AuthComponent";
|
import AuthComponent, {AuthMiddleware} from "../AuthComponent";
|
||||||
import {AuthError, PendingApprovalAuthError, RegisterCallback} from "../AuthGuard";
|
import {AuthError, PendingApprovalAuthError, RegisterCallback} from "../AuthGuard";
|
||||||
@ -19,6 +18,8 @@ import UserNameComponent from "../models/UserNameComponent";
|
|||||||
import MagicLinkUserNameComponent from "../models/MagicLinkUserNameComponent";
|
import MagicLinkUserNameComponent from "../models/MagicLinkUserNameComponent";
|
||||||
import {logger} from "../../Logger";
|
import {logger} from "../../Logger";
|
||||||
import UserPasswordComponent from "../password/UserPasswordComponent";
|
import UserPasswordComponent from "../password/UserPasswordComponent";
|
||||||
|
import MailTemplate from "../../mail/MailTemplate";
|
||||||
|
import MailComponent from "../../components/MailComponent";
|
||||||
|
|
||||||
export default class MagicLinkController<A extends Application> extends Controller {
|
export default class MagicLinkController<A extends Application> extends Controller {
|
||||||
public static async sendMagicLink(
|
public static async sendMagicLink(
|
||||||
@ -46,12 +47,12 @@ export default class MagicLinkController<A extends Application> extends Controll
|
|||||||
await link.save();
|
await link.save();
|
||||||
|
|
||||||
// Send email
|
// Send email
|
||||||
await new Mail(app.as(NunjucksComponent).getEnvironment(), mailTemplate, Object.assign(data, {
|
await app.as(MailComponent).sendMail(new Mail(mailTemplate, Object.assign(data, {
|
||||||
link: `${config.get<string>('public_url')}${Controller.route('magic_link', undefined, {
|
link: `${config.get<string>('public_url')}${Controller.route('magic_link', undefined, {
|
||||||
id: link.id,
|
id: link.id,
|
||||||
token: token,
|
token: token,
|
||||||
})}`,
|
})}`,
|
||||||
})).send(email);
|
})), email);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async checkAndAuth(req: Request, res: Response, magicLink: MagicLink): Promise<User | null> {
|
public static async checkAndAuth(req: Request, res: Response, magicLink: MagicLink): Promise<User | null> {
|
||||||
|
@ -7,15 +7,21 @@ import "svelte/register";
|
|||||||
import ViewEngine from "../frontend/ViewEngine";
|
import ViewEngine from "../frontend/ViewEngine";
|
||||||
import {readdirRecursively} from "../Utils";
|
import {readdirRecursively} from "../Utils";
|
||||||
import FileCache from "../utils/FileCache";
|
import FileCache from "../utils/FileCache";
|
||||||
|
import Controller, {RouteParams} from "../Controller";
|
||||||
|
import * as querystring from "querystring";
|
||||||
|
import {ParsedUrlQueryInput} from "querystring";
|
||||||
|
import util from "util";
|
||||||
|
|
||||||
export default class FrontendToolsComponent extends ApplicationComponent {
|
export default class FrontendToolsComponent extends ApplicationComponent {
|
||||||
private readonly publicAssetsCache: FileCache = new FileCache();
|
private readonly publicAssetsCache: FileCache = new FileCache();
|
||||||
|
private readonly viewEngines: ViewEngine[];
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly publicAssetsDir: string,
|
private readonly publicAssetsDir: string,
|
||||||
private readonly viewEngine: ViewEngine,
|
...viewEngines: ViewEngine[]
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
this.viewEngines = viewEngines;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async start(app: Express): Promise<void> {
|
public async start(app: Express): Promise<void> {
|
||||||
@ -31,11 +37,20 @@ export default class FrontendToolsComponent extends ApplicationComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Setup express view engine
|
// Setup express view engine
|
||||||
this.viewEngine.setup(app, true);
|
let main = true;
|
||||||
|
for (const viewEngine of this.viewEngines) {
|
||||||
|
viewEngine.setup(app, main);
|
||||||
|
main = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add util globals
|
||||||
|
this.setupGlobals();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async stop(): Promise<void> {
|
public async stop(): Promise<void> {
|
||||||
await this.viewEngine.stop();
|
for (const viewEngine of this.viewEngines) {
|
||||||
|
await viewEngine.stop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handle(router: Router): Promise<void> {
|
public async handle(router: Router): Promise<void> {
|
||||||
@ -59,6 +74,27 @@ export default class FrontendToolsComponent extends ApplicationComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async preCompileViews(watch: boolean): Promise<void> {
|
public async preCompileViews(watch: boolean): Promise<void> {
|
||||||
await this.viewEngine.preCompileAll(watch);
|
for (const viewEngine of this.viewEngines) {
|
||||||
|
await viewEngine.preCompileAll(watch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public setupGlobals(): void {
|
||||||
|
ViewEngine.setGlobal('route', (
|
||||||
|
route: string,
|
||||||
|
params: RouteParams = [],
|
||||||
|
query: ParsedUrlQueryInput = {},
|
||||||
|
absolute: boolean = false,
|
||||||
|
) => Controller.route(route, params, query, absolute));
|
||||||
|
ViewEngine.setGlobal('app_version', this.getApp().getVersion());
|
||||||
|
ViewEngine.setGlobal('core_version', this.getApp().getCoreVersion());
|
||||||
|
ViewEngine.setGlobal('querystring', querystring);
|
||||||
|
ViewEngine.setGlobal('app', config.get('app'));
|
||||||
|
ViewEngine.setGlobal('dump', (val: unknown) => {
|
||||||
|
return util.inspect(val);
|
||||||
|
});
|
||||||
|
ViewEngine.setGlobal('hex', (v: number) => {
|
||||||
|
return v.toString(16);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,24 @@
|
|||||||
import ApplicationComponent from "../ApplicationComponent";
|
import ApplicationComponent from "../ApplicationComponent";
|
||||||
import {Express} from "express";
|
import {Express} from "express";
|
||||||
import Mail from "../mail/Mail";
|
|
||||||
import config from "config";
|
import config from "config";
|
||||||
import SecurityError from "../SecurityError";
|
import SecurityError from "../SecurityError";
|
||||||
|
import nodemailer, {SentMessageInfo, Transporter} from "nodemailer";
|
||||||
|
import MailError from "../mail/MailError";
|
||||||
|
import util from "util";
|
||||||
|
import {logger} from "../Logger";
|
||||||
|
import Controller from "../Controller";
|
||||||
|
import Mail from "../mail/Mail";
|
||||||
|
import MailViewEngine from "../frontend/MailViewEngine";
|
||||||
|
import ViewEngine from "../frontend/ViewEngine";
|
||||||
|
|
||||||
export default class MailComponent extends ApplicationComponent {
|
export default class MailComponent extends ApplicationComponent {
|
||||||
|
private transporter?: Transporter;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private readonly viewEngine: MailViewEngine,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
public async checkSecuritySettings(): Promise<void> {
|
public async checkSecuritySettings(): Promise<void> {
|
||||||
if (!config.get<boolean>('mail.secure')) {
|
if (!config.get<boolean>('mail.secure')) {
|
||||||
@ -15,12 +29,86 @@ export default class MailComponent extends ApplicationComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async start(_app: Express): Promise<void> {
|
public async start(app: Express): Promise<void> {
|
||||||
await this.prepare('Mail connection', () => Mail.prepare());
|
await this.prepare('Mail connection', async () => {
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
host: config.get('mail.host'),
|
||||||
|
port: config.get('mail.port'),
|
||||||
|
requireTLS: config.get('mail.secure'), // STARTTLS
|
||||||
|
auth: {
|
||||||
|
user: config.get('mail.username'),
|
||||||
|
pass: config.get('mail.password'),
|
||||||
|
},
|
||||||
|
tls: {
|
||||||
|
rejectUnauthorized: !config.get('mail.allow_invalid_tls'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await util.promisify(transporter.verify)();
|
||||||
|
this.transporter = transporter;
|
||||||
|
} catch (e) {
|
||||||
|
throw new MailError('Connection to mail service unsuccessful.', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Mail ready to be distributed via ${config.get('mail.host')}:${config.get('mail.port')}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.viewEngine.setup(app, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async stop(): Promise<void> {
|
public async stop(): Promise<void> {
|
||||||
Mail.end();
|
if (this.transporter) {
|
||||||
|
this.transporter.close();
|
||||||
|
this.transporter = undefined;
|
||||||
|
}
|
||||||
|
await this.viewEngine.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async sendMail(mail: Mail, ...to: string[]): Promise<SentMessageInfo[]> {
|
||||||
|
if (to.length === 0) throw new Error('Cannot send an email to recipient. (to is empty)');
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
for (const destEmail of to) {
|
||||||
|
const template = mail.getTemplate();
|
||||||
|
const locals = mail.getData();
|
||||||
|
const options = mail.getOptions();
|
||||||
|
|
||||||
|
// Reset options
|
||||||
|
options.html = options.text = undefined;
|
||||||
|
|
||||||
|
// Set options
|
||||||
|
options.to = destEmail;
|
||||||
|
options.from = {
|
||||||
|
name: config.get('mail.from_name'),
|
||||||
|
address: config.get('mail.from'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set locals
|
||||||
|
locals.mail_subject = options.subject;
|
||||||
|
locals.mail_to = options.to;
|
||||||
|
locals.mail_link = config.get<string>('public_url') +
|
||||||
|
Controller.route('mail', [template.template], locals);
|
||||||
|
Object.assign(locals, ViewEngine.getGlobals());
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// Send email
|
||||||
|
results.push(await this.getTransporter().sendMail(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTransporter(): Transporter {
|
||||||
|
if (!this.transporter) throw new MailError('Mail system was not prepared.');
|
||||||
|
return this.transporter;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
import nunjucks, {Environment} from "nunjucks";
|
import nunjucks, {Environment} from "nunjucks";
|
||||||
import config from "config";
|
import config from "config";
|
||||||
import {Express, NextFunction, Request, Response, Router} from "express";
|
import {Express} from "express";
|
||||||
import ApplicationComponent from "../ApplicationComponent";
|
import ApplicationComponent from "../ApplicationComponent";
|
||||||
import Controller, {RouteParams} from "../Controller";
|
import Controller, {RouteParams} from "../Controller";
|
||||||
import * as querystring from "querystring";
|
import * as querystring from "querystring";
|
||||||
import {ParsedUrlQueryInput} from "querystring";
|
import {ParsedUrlQueryInput} from "querystring";
|
||||||
import * as util from "util";
|
import * as util from "util";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import Middleware from "../Middleware";
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
* TODO: turn into a proper ViewEngine
|
||||||
|
*/
|
||||||
export default class NunjucksComponent extends ApplicationComponent {
|
export default class NunjucksComponent extends ApplicationComponent {
|
||||||
private readonly viewsPath: string[];
|
private readonly viewsPath: string[];
|
||||||
private environment?: Environment;
|
private environment?: Environment;
|
||||||
|
27
src/frontend/MailViewEngine.ts
Normal file
27
src/frontend/MailViewEngine.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import mjml2html from "mjml";
|
||||||
|
import MailError from "../mail/MailError";
|
||||||
|
import NunjucksViewEngine from "./NunjucksViewEngine";
|
||||||
|
|
||||||
|
export default class MailViewEngine extends NunjucksViewEngine {
|
||||||
|
public getExtension(): string {
|
||||||
|
return 'mnjk';
|
||||||
|
}
|
||||||
|
|
||||||
|
public async render(
|
||||||
|
file: string,
|
||||||
|
locals: Record<string, unknown>,
|
||||||
|
textOnly: boolean = false,
|
||||||
|
): Promise<string> {
|
||||||
|
locals.text = textOnly;
|
||||||
|
const nunjucksResult = await super.render(file, locals);
|
||||||
|
if (textOnly) return nunjucksResult;
|
||||||
|
|
||||||
|
const mjmlResult = mjml2html(nunjucksResult, {});
|
||||||
|
|
||||||
|
if (mjmlResult.errors.length > 0) {
|
||||||
|
throw new MailError(`Error while parsing mail template ${file}: ${JSON.stringify(mjmlResult.errors, null, 4)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mjmlResult.html;
|
||||||
|
}
|
||||||
|
}
|
34
src/frontend/NunjucksViewEngine.ts
Normal file
34
src/frontend/NunjucksViewEngine.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import ViewEngine from "./ViewEngine";
|
||||||
|
import config from "config";
|
||||||
|
import nunjucks, {Environment} from "nunjucks";
|
||||||
|
|
||||||
|
export default class NunjucksViewEngine extends ViewEngine {
|
||||||
|
private readonly environment: Environment;
|
||||||
|
|
||||||
|
public constructor(devWatchedViewDir: string, ...additionalViewPaths: string[]) {
|
||||||
|
super(devWatchedViewDir, ...additionalViewPaths);
|
||||||
|
|
||||||
|
const opts = {
|
||||||
|
autoescape: true,
|
||||||
|
noCache: !config.get('view.cache'),
|
||||||
|
throwOnUndefined: true,
|
||||||
|
};
|
||||||
|
this.environment = new nunjucks.Environment([
|
||||||
|
...this.viewPaths.map(path => new nunjucks.FileSystemLoader(path, opts)),
|
||||||
|
], opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getExtension(): string {
|
||||||
|
return 'njk';
|
||||||
|
}
|
||||||
|
|
||||||
|
public async render(file: string, locals: Record<string, unknown>): Promise<string> {
|
||||||
|
return await new Promise<string>((resolve, reject) => {
|
||||||
|
this.environment.render(file, locals, (err, res) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
else if (res === null) reject('Null response from nunjucks environment.render()');
|
||||||
|
else return resolve(res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -63,8 +63,7 @@ export default class SvelteViewEngine extends ViewEngine {
|
|||||||
public async render(
|
public async render(
|
||||||
file: string,
|
file: string,
|
||||||
locals: Record<string, unknown>,
|
locals: Record<string, unknown>,
|
||||||
callback: (err: Error | null, output?: string) => void,
|
): Promise<string> {
|
||||||
): Promise<void> {
|
|
||||||
const canonicalViewName = this.toCanonicalName(file);
|
const canonicalViewName = this.toCanonicalName(file);
|
||||||
|
|
||||||
// View
|
// View
|
||||||
@ -140,7 +139,7 @@ export default class SvelteViewEngine extends ViewEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
callback(null, output);
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async stop(): Promise<void> {
|
public async stop(): Promise<void> {
|
||||||
|
@ -9,7 +9,7 @@ export default abstract class ViewEngine {
|
|||||||
private static readonly globals: Record<string, unknown> = {};
|
private static readonly globals: Record<string, unknown> = {};
|
||||||
|
|
||||||
public static getGlobals(): Record<string, unknown> {
|
public static getGlobals(): Record<string, unknown> {
|
||||||
return this.globals;
|
return {...this.globals};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static setGlobal(key: string, value: unknown): void {
|
public static setGlobal(key: string, value: unknown): void {
|
||||||
@ -44,21 +44,26 @@ export default abstract class ViewEngine {
|
|||||||
public abstract render(
|
public abstract render(
|
||||||
file: string,
|
file: string,
|
||||||
locals: Record<string, unknown>,
|
locals: Record<string, unknown>,
|
||||||
callback: (err: Error | null, output?: string) => void,
|
): Promise<string>;
|
||||||
): Promise<void>;
|
|
||||||
|
|
||||||
public setup(app: Express, main: boolean): void {
|
public setup(app: Express, main: boolean): void {
|
||||||
app.engine(this.getExtension(), (path, options, callback) => {
|
app.engine(this.getExtension(), (path, options, callback) => {
|
||||||
// Props (locals)
|
// Props (locals)
|
||||||
const locals = Object.assign(ViewEngine.getGlobals(), options);
|
const locals = Object.assign(options, ViewEngine.getGlobals());
|
||||||
|
|
||||||
this.render(path, locals, callback)
|
this.render(path, locals)
|
||||||
|
.then(value => callback(null, value))
|
||||||
.catch(err => callback(err));
|
.catch(err => callback(err));
|
||||||
});
|
});
|
||||||
|
|
||||||
const existingViewPaths = app.get('views');
|
const existingViewPaths = app.get('views');
|
||||||
app.set('views', existingViewPaths ?
|
app.set('views', existingViewPaths ?
|
||||||
[...existingViewPaths, ...this.getViewPaths()] :
|
[...new Set([
|
||||||
|
...typeof existingViewPaths === 'string' ?
|
||||||
|
[existingViewPaths] :
|
||||||
|
existingViewPaths,
|
||||||
|
...this.getViewPaths(),
|
||||||
|
])] :
|
||||||
this.getViewPaths());
|
this.getViewPaths());
|
||||||
|
|
||||||
if (main) {
|
if (main) {
|
||||||
|
@ -8,9 +8,9 @@ import {ACCOUNT_REVIEW_NOTICE_MAIL_TEMPLATE} from "../Mails";
|
|||||||
import UserEmail from "../auth/models/UserEmail";
|
import UserEmail from "../auth/models/UserEmail";
|
||||||
import UserApprovedComponent from "../auth/models/UserApprovedComponent";
|
import UserApprovedComponent from "../auth/models/UserApprovedComponent";
|
||||||
import {RequireAdminMiddleware, RequireAuthMiddleware} from "../auth/AuthComponent";
|
import {RequireAdminMiddleware, RequireAuthMiddleware} from "../auth/AuthComponent";
|
||||||
import NunjucksComponent from "../components/NunjucksComponent";
|
|
||||||
import ModelFactory from "../db/ModelFactory";
|
import ModelFactory from "../db/ModelFactory";
|
||||||
import UserNameComponent from "../auth/models/UserNameComponent";
|
import UserNameComponent from "../auth/models/UserNameComponent";
|
||||||
|
import MailComponent from "../components/MailComponent";
|
||||||
|
|
||||||
export default class BackendController extends Controller {
|
export default class BackendController extends Controller {
|
||||||
private static readonly menu: BackendMenuElement[] = [];
|
private static readonly menu: BackendMenuElement[] = [];
|
||||||
@ -78,10 +78,10 @@ export default class BackendController extends Controller {
|
|||||||
await account.save();
|
await account.save();
|
||||||
|
|
||||||
if (email && email.email) {
|
if (email && email.email) {
|
||||||
await new Mail(this.getApp().as(NunjucksComponent).getEnvironment(), ACCOUNT_REVIEW_NOTICE_MAIL_TEMPLATE, {
|
await this.getApp().as(MailComponent).sendMail(new Mail(ACCOUNT_REVIEW_NOTICE_MAIL_TEMPLATE, {
|
||||||
approved: true,
|
approved: true,
|
||||||
link: config.get<string>('public_url') + Controller.route('auth'),
|
link: config.get<string>('public_url') + Controller.route('auth'),
|
||||||
}).send(email.email);
|
}), email.email);
|
||||||
}
|
}
|
||||||
|
|
||||||
req.flash('success', `Account successfully approved.`);
|
req.flash('success', `Account successfully approved.`);
|
||||||
@ -94,9 +94,9 @@ export default class BackendController extends Controller {
|
|||||||
await account.delete();
|
await account.delete();
|
||||||
|
|
||||||
if (email && email.email) {
|
if (email && email.email) {
|
||||||
await new Mail(this.getApp().as(NunjucksComponent).getEnvironment(), ACCOUNT_REVIEW_NOTICE_MAIL_TEMPLATE, {
|
await this.getApp().as(MailComponent).sendMail(new Mail(ACCOUNT_REVIEW_NOTICE_MAIL_TEMPLATE, {
|
||||||
approved: false,
|
approved: false,
|
||||||
}).send(email.email);
|
}), email.email);
|
||||||
}
|
}
|
||||||
|
|
||||||
req.flash('success', `Account successfully deleted.`);
|
req.flash('success', `Account successfully deleted.`);
|
||||||
|
128
src/mail/Mail.ts
128
src/mail/Mail.ts
@ -1,78 +1,15 @@
|
|||||||
import nodemailer, {SentMessageInfo, Transporter} from "nodemailer";
|
|
||||||
import config from "config";
|
|
||||||
import {Options} from "nodemailer/lib/mailer";
|
import {Options} from "nodemailer/lib/mailer";
|
||||||
import {Environment} from 'nunjucks';
|
|
||||||
import * as util from "util";
|
|
||||||
import {WrappingError} from "../Utils";
|
|
||||||
import mjml2html from "mjml";
|
|
||||||
import {logger} from "../Logger";
|
|
||||||
import Controller from "../Controller";
|
|
||||||
import {ParsedUrlQueryInput} from "querystring";
|
import {ParsedUrlQueryInput} from "querystring";
|
||||||
|
import MailError from "./MailError";
|
||||||
|
import MailTemplate from "./MailTemplate";
|
||||||
|
|
||||||
export default class Mail {
|
export default class Mail {
|
||||||
private static transporter?: Transporter;
|
|
||||||
|
|
||||||
private static getTransporter(): Transporter {
|
|
||||||
if (!this.transporter) throw new MailError('Mail system was not prepared.');
|
|
||||||
return this.transporter;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async prepare(): Promise<void> {
|
|
||||||
const transporter = nodemailer.createTransport({
|
|
||||||
host: config.get('mail.host'),
|
|
||||||
port: config.get('mail.port'),
|
|
||||||
requireTLS: config.get('mail.secure'), // STARTTLS
|
|
||||||
auth: {
|
|
||||||
user: config.get('mail.username'),
|
|
||||||
pass: config.get('mail.password'),
|
|
||||||
},
|
|
||||||
tls: {
|
|
||||||
rejectUnauthorized: !config.get('mail.allow_invalid_tls'),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await util.promisify(transporter.verify)();
|
|
||||||
this.transporter = transporter;
|
|
||||||
} catch (e) {
|
|
||||||
throw new MailError('Connection to mail service unsuccessful.', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`Mail ready to be distributed via ${config.get('mail.host')}:${config.get('mail.port')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static end(): void {
|
|
||||||
if (this.transporter) this.transporter.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static parse(
|
|
||||||
environment: Environment,
|
|
||||||
template: string,
|
|
||||||
data: { [p: string]: unknown },
|
|
||||||
textOnly: boolean,
|
|
||||||
): string {
|
|
||||||
data.text = textOnly;
|
|
||||||
const nunjucksResult = environment.render(template, data);
|
|
||||||
if (textOnly) return nunjucksResult;
|
|
||||||
|
|
||||||
const mjmlResult = mjml2html(nunjucksResult, {});
|
|
||||||
|
|
||||||
if (mjmlResult.errors.length > 0) {
|
|
||||||
throw new MailError(`Error while parsing mail template ${template}: ${JSON.stringify(mjmlResult.errors, null, 4)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return mjmlResult.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly options: Options = {};
|
private readonly options: Options = {};
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly environment: Environment,
|
|
||||||
private readonly template: MailTemplate,
|
private readonly template: MailTemplate,
|
||||||
private readonly data: ParsedUrlQueryInput = {},
|
private readonly data: ParsedUrlQueryInput = {},
|
||||||
) {
|
) {
|
||||||
this.template = template;
|
|
||||||
this.data = data;
|
|
||||||
this.options.subject = this.template.getSubject(data);
|
this.options.subject = this.template.getSubject(data);
|
||||||
|
|
||||||
this.verifyData();
|
this.verifyData();
|
||||||
@ -88,64 +25,15 @@ export default class Mail {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async send(...to: string[]): Promise<SentMessageInfo[]> {
|
public getTemplate(): MailTemplate {
|
||||||
const results = [];
|
return this.template;
|
||||||
|
|
||||||
for (const destEmail of to) {
|
|
||||||
// Reset options
|
|
||||||
this.options.html = this.options.text = undefined;
|
|
||||||
|
|
||||||
// Set options
|
|
||||||
this.options.to = destEmail;
|
|
||||||
this.options.from = {
|
|
||||||
name: config.get('mail.from_name'),
|
|
||||||
address: config.get('mail.from'),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Set data
|
|
||||||
this.data.mail_subject = this.options.subject;
|
|
||||||
this.data.mail_to = this.options.to;
|
|
||||||
this.data.mail_link = config.get<string>('public_url') +
|
|
||||||
Controller.route('mail', [this.template.template], this.data);
|
|
||||||
this.data.app = config.get('app');
|
|
||||||
|
|
||||||
// Log
|
|
||||||
logger.debug(`Send mail from ${this.options.from.address} to ${this.options.to}`);
|
|
||||||
|
|
||||||
// Render email
|
|
||||||
this.options.html = Mail.parse(this.environment, 'mails/' + this.template.template + '.mjml.njk',
|
|
||||||
this.data, false);
|
|
||||||
this.options.text = Mail.parse(this.environment, 'mails/' + this.template.template + '.mjml.njk',
|
|
||||||
this.data, true);
|
|
||||||
|
|
||||||
// Send email
|
|
||||||
results.push(await Mail.getTransporter().sendMail(this.options));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
public getData(): ParsedUrlQueryInput {
|
||||||
}
|
return {...this.data};
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MailTemplate {
|
public getOptions(): Options {
|
||||||
private readonly _template: string;
|
return {...this.options};
|
||||||
private readonly subject: (data: { [p: string]: unknown }) => string;
|
|
||||||
|
|
||||||
public constructor(template: string, subject: (data: { [p: string]: unknown }) => string) {
|
|
||||||
this._template = template;
|
|
||||||
this.subject = subject;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get template(): string {
|
|
||||||
return this._template;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getSubject(data: { [p: string]: unknown }): string {
|
|
||||||
return `${config.get('app.name')} - ${this.subject(data)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MailError extends WrappingError {
|
|
||||||
public constructor(message: string = 'An error occurred while sending mail.', cause?: Error) {
|
|
||||||
super(message, cause);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import {Request, Response} from "express";
|
import {Request, Response} from "express";
|
||||||
import Controller from "../Controller";
|
import Controller from "../Controller";
|
||||||
import Mail from "./Mail";
|
|
||||||
import NunjucksComponent from "../components/NunjucksComponent";
|
|
||||||
|
|
||||||
export default class MailController extends Controller {
|
export default class MailController extends Controller {
|
||||||
public routes(): void {
|
public routes(): void {
|
||||||
@ -10,7 +8,6 @@ export default class MailController extends Controller {
|
|||||||
|
|
||||||
protected async getMail(request: Request, response: Response): Promise<void> {
|
protected async getMail(request: Request, response: Response): Promise<void> {
|
||||||
const template = request.params['template'];
|
const template = request.params['template'];
|
||||||
response.send(Mail.parse(this.getApp().as(NunjucksComponent).getEnvironment(),
|
response.render(`mails/${template}.mnjk`, request.query);
|
||||||
`mails/${template}.mjml.njk`, request.query, false));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
7
src/mail/MailError.ts
Normal file
7
src/mail/MailError.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import {WrappingError} from "../Utils";
|
||||||
|
|
||||||
|
export default class MailError extends WrappingError {
|
||||||
|
public constructor(message: string = 'An error occurred while sending mail.', cause?: Error) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
19
src/mail/MailTemplate.ts
Normal file
19
src/mail/MailTemplate.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import config from "config";
|
||||||
|
|
||||||
|
export default class MailTemplate {
|
||||||
|
private readonly _template: string;
|
||||||
|
private readonly subject: (data: { [p: string]: unknown }) => string;
|
||||||
|
|
||||||
|
public constructor(template: string, subject: (data: { [p: string]: unknown }) => string) {
|
||||||
|
this._template = template;
|
||||||
|
this.subject = subject;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get template(): string {
|
||||||
|
return this._template;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSubject(data: { [p: string]: unknown }): string {
|
||||||
|
return `${config.get('app.name')} - ${this.subject(data)}`;
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'mails/base_layout.mjml.njk' %}
|
{% extends 'mails/base_layout.mnjk' %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<mj-section>
|
<mj-section>
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'mails/base_layout.mjml.njk' %}
|
{% extends 'mails/base_layout.mnjk' %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<mj-section>
|
<mj-section>
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'mails/base_layout.mjml.njk' %}
|
{% extends 'mails/base_layout.mnjk' %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<mj-section>
|
<mj-section>
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'mails/base_layout.mjml.njk' %}
|
{% extends 'mails/base_layout.mnjk' %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<mj-section>
|
<mj-section>
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'mails/base_layout.mjml.njk' %}
|
{% extends 'mails/base_layout.mnjk' %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<mj-section>
|
<mj-section>
|
Loading…
Reference in New Issue
Block a user