Add svelte as a view engine to swaf #33
@ -6,6 +6,7 @@ import * as path from "path";
|
||||
|
||||
import ApplicationComponent from "./ApplicationComponent.js";
|
||||
import CacheProvider from "./CacheProvider.js";
|
||||
import {route, setPublicUrl} from "./common/Routing.js";
|
||||
import FrontendToolsComponent from "./components/FrontendToolsComponent.js";
|
||||
import LogRequestsComponent from "./components/LogRequestsComponent.js";
|
||||
import RedisComponent from "./components/RedisComponent.js";
|
||||
@ -37,6 +38,8 @@ export default abstract class Application implements Extendable<ApplicationCompo
|
||||
protected constructor(version: string, ignoreCommandLine: boolean = false) {
|
||||
this.version = version;
|
||||
this.ignoreCommandLine = ignoreCommandLine;
|
||||
|
||||
setPublicUrl(config.get<string>('app.public_url'));
|
||||
}
|
||||
|
||||
protected abstract getMigrations(): MigrationType<Migration>[];
|
||||
@ -163,7 +166,7 @@ export default abstract class Application implements Extendable<ApplicationCompo
|
||||
},
|
||||
html: () => {
|
||||
req.flash('validation', bag.getMessages());
|
||||
res.redirect(req.getPreviousUrl() || Controller.route('home'));
|
||||
res.redirect(req.getPreviousUrl() || route('home'));
|
||||
},
|
||||
});
|
||||
return;
|
||||
|
@ -1,55 +1,13 @@
|
||||
import config from "config";
|
||||
import express, {IRouter, RequestHandler, Router} from "express";
|
||||
import {PathParams} from "express-serve-static-core";
|
||||
import * as querystring from "querystring";
|
||||
import {ParsedUrlQueryInput} from "querystring";
|
||||
|
||||
import Application from "./Application.js";
|
||||
import {registerRoute} from "./common/Routing.js";
|
||||
import FileUploadMiddleware from "./FileUploadMiddleware.js";
|
||||
import {logger} from "./Logger.js";
|
||||
import Middleware, {MiddlewareType} from "./Middleware.js";
|
||||
|
||||
export default abstract class Controller {
|
||||
/**
|
||||
* TODO: this should not be static, it should actually be bound to an app instance.
|
||||
*/
|
||||
private static readonly routes: { [p: string]: string | undefined } = {};
|
||||
|
||||
public static route(
|
||||
route: string,
|
||||
params: RouteParams = [],
|
||||
query: ParsedUrlQueryInput = {},
|
||||
absolute: boolean = false,
|
||||
): string {
|
||||
let path = this.routes[route];
|
||||
if (path === undefined) throw new Error(`Unknown route for name ${route}.`);
|
||||
|
||||
const regExp = this.getRouteParamRegExp('[a-zA-Z0-9_-]+', 'g');
|
||||
if (typeof params === 'string' || typeof params === 'number') {
|
||||
path = path.replace(regExp, '' + params);
|
||||
} else if (Array.isArray(params)) {
|
||||
let i = 0;
|
||||
for (const match of path.matchAll(regExp)) {
|
||||
if (match.length > 0) {
|
||||
path = path.replace(match[0], typeof params[i] !== 'undefined' ? params[i] : '');
|
||||
}
|
||||
i++;
|
||||
}
|
||||
path = path.replace(/\/+/g, '/');
|
||||
} else {
|
||||
for (const key of Object.keys(params)) {
|
||||
path = path.replace(this.getRouteParamRegExp(key), params[key].toString());
|
||||
}
|
||||
}
|
||||
|
||||
const queryStr = querystring.stringify(query);
|
||||
return `${absolute ? config.get<string>('public_url') : ''}${path}` + (queryStr.length > 0 ? '?' + queryStr : '');
|
||||
}
|
||||
|
||||
private static getRouteParamRegExp(key: string, flags?: string): RegExp {
|
||||
return new RegExp(`:${key}(\\(.+?\\))?\\??`, flags);
|
||||
}
|
||||
|
||||
private readonly router: Router = express.Router();
|
||||
private readonly fileUploadFormRouter: Router = express.Router();
|
||||
private app?: Application;
|
||||
@ -183,13 +141,15 @@ export default abstract class Controller {
|
||||
routePath = (prefix !== '/' ? prefix : '') + path;
|
||||
}
|
||||
|
||||
if (!Controller.routes[routeName]) {
|
||||
if (typeof routePath === 'string') {
|
||||
logger.info(`Route ${routeName} has path ${routePath}`);
|
||||
Controller.routes[routeName] = routePath;
|
||||
} else {
|
||||
if (typeof routePath !== 'string') {
|
||||
logger.warn(`Cannot assign path to route ${routeName}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (registerRoute(routeName, routePath)) {
|
||||
logger.info(`Route ${routeName} has path ${routePath}`);
|
||||
} else {
|
||||
logger.warn(`Couldn't register ${routeName} for path ${routePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -202,5 +162,3 @@ export default abstract class Controller {
|
||||
this.app = app;
|
||||
}
|
||||
}
|
||||
|
||||
export type RouteParams = { [p: string]: string | number } | string[] | string | number;
|
||||
|
@ -6,6 +6,9 @@
|
||||
<style>%css%</style>
|
||||
<script type="module" defer>
|
||||
import View from '/js/views/%canonicalViewName%.js';
|
||||
import * as Routing from '/js/Routing.js';
|
||||
const setRoutes = Routing.R.setRoutes;
|
||||
const setPublicUrl = Routing.R.setPublicUrl;
|
||||
import * as stores from '/js/stores.js';
|
||||
const localStore = stores.s.locals;
|
||||
|
||||
@ -16,6 +19,9 @@
|
||||
: `'${key}'`];
|
||||
});
|
||||
|
||||
setRoutes(%routes%);
|
||||
setPublicUrl(%publicUrl%);
|
||||
|
||||
new View({
|
||||
hydrate: true,
|
||||
target: document.body,
|
||||
|
@ -1,7 +1,8 @@
|
||||
import config from "config";
|
||||
import {Request, Response} from "express";
|
||||
|
||||
import Time from "../common/Time.js";
|
||||
import {route} from "../common/Routing.js";
|
||||
import {Time} from "../common/Time.js";
|
||||
import Controller from "../Controller.js";
|
||||
import ModelFactory from "../db/ModelFactory.js";
|
||||
import Validator, {EMAIL_REGEX, InvalidFormatValidationError} from "../db/Validator.js";
|
||||
@ -81,14 +82,14 @@ export default class AccountController extends Controller {
|
||||
const nameChangedAt = userNameComponent.getNameChangedAt()?.getTime() || Date.now();
|
||||
const nameChangeWaitPeriod = config.get<number>('auth.name_change_wait_period');
|
||||
req.flash('error', `Your can't change your name until ${new Date(nameChangedAt + nameChangeWaitPeriod)}.`);
|
||||
res.redirect(Controller.route('account'));
|
||||
res.redirect(route('account'));
|
||||
return;
|
||||
}
|
||||
|
||||
await user.save();
|
||||
|
||||
req.flash('success', `Your name was successfully changed to ${req.body.name}.`);
|
||||
res.redirect(Controller.route('account'));
|
||||
res.redirect(route('account'));
|
||||
}
|
||||
|
||||
protected async postChangePassword(req: Request, res: Response): Promise<void> {
|
||||
@ -101,7 +102,7 @@ export default class AccountController extends Controller {
|
||||
const passwordComponent = user.as(UserPasswordComponent);
|
||||
if (passwordComponent.hasPassword() && !await passwordComponent.verifyPassword(req.body.current_password)) {
|
||||
req.flash('error', 'Invalid current password.');
|
||||
res.redirect(Controller.route('account'));
|
||||
res.redirect(route('account'));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -109,7 +110,7 @@ export default class AccountController extends Controller {
|
||||
await user.save();
|
||||
|
||||
req.flash('success', 'Password changed successfully.');
|
||||
res.redirect(Controller.route('account'));
|
||||
res.redirect(route('account'));
|
||||
}
|
||||
|
||||
protected async postRemovePassword(req: Request, res: Response): Promise<void> {
|
||||
@ -117,7 +118,7 @@ export default class AccountController extends Controller {
|
||||
const mainEmail = await user.mainEmail.get();
|
||||
if (!mainEmail || !mainEmail.email) {
|
||||
req.flash('error', 'You can\'t remove your password without adding an email address first.');
|
||||
res.redirect(Controller.route('account'));
|
||||
res.redirect(route('account'));
|
||||
|
||||
return;
|
||||
}
|
||||
@ -126,14 +127,14 @@ export default class AccountController extends Controller {
|
||||
this.getApp(),
|
||||
req.getSession().id,
|
||||
AuthMagicLinkActionType.REMOVE_PASSWORD,
|
||||
Controller.route('account'),
|
||||
route('account'),
|
||||
mainEmail.email,
|
||||
this.removePasswordMailTemplate,
|
||||
{},
|
||||
);
|
||||
|
||||
res.redirect(Controller.route('magic_link_lobby', undefined, {
|
||||
redirect_uri: Controller.route('account'),
|
||||
res.redirect(route('magic_link_lobby', undefined, {
|
||||
redirect_uri: route('account'),
|
||||
}));
|
||||
}
|
||||
|
||||
@ -156,7 +157,7 @@ export default class AccountController extends Controller {
|
||||
this.getApp(),
|
||||
req.getSession().id,
|
||||
AuthMagicLinkActionType.ADD_EMAIL,
|
||||
Controller.route('account'),
|
||||
route('account'),
|
||||
email,
|
||||
this.addEmailMailTemplate,
|
||||
{
|
||||
@ -164,8 +165,8 @@ export default class AccountController extends Controller {
|
||||
},
|
||||
);
|
||||
|
||||
res.redirect(Controller.route('magic_link_lobby', undefined, {
|
||||
redirect_uri: Controller.route('account'),
|
||||
res.redirect(route('magic_link_lobby', undefined, {
|
||||
redirect_uri: route('account'),
|
||||
}));
|
||||
}
|
||||
|
||||
@ -188,7 +189,7 @@ export default class AccountController extends Controller {
|
||||
await user.save();
|
||||
|
||||
req.flash('success', 'This email was successfully set as your main address.');
|
||||
res.redirect(Controller.route('account'));
|
||||
res.redirect(route('account'));
|
||||
}
|
||||
|
||||
protected async postRemoveEmail(req: Request, res: Response): Promise<void> {
|
||||
@ -208,6 +209,6 @@ export default class AccountController extends Controller {
|
||||
await userEmail.delete();
|
||||
|
||||
req.flash('success', 'This email was successfully removed from your account.');
|
||||
res.redirect(Controller.route('account'));
|
||||
res.redirect(route('account'));
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import {NextFunction, Request, Response} from "express";
|
||||
|
||||
import Application from "../Application.js";
|
||||
import ApplicationComponent from "../ApplicationComponent.js";
|
||||
import Controller from "../Controller.js";
|
||||
import {route} from "../common/Routing.js";
|
||||
import {ForbiddenHttpError} from "../HttpError.js";
|
||||
import Middleware from "../Middleware.js";
|
||||
import AuthGuard from "./AuthGuard.js";
|
||||
@ -66,7 +66,7 @@ export class RequireRequestAuthMiddleware extends Middleware {
|
||||
}
|
||||
|
||||
req.flash('error', `You must be logged in to access ${req.url}.`);
|
||||
res.redirect(Controller.route('auth', undefined, {
|
||||
res.redirect(route('auth', undefined, {
|
||||
redirect_uri: req.url,
|
||||
}));
|
||||
}
|
||||
@ -102,7 +102,7 @@ export class RequireAuthMiddleware extends Middleware {
|
||||
}
|
||||
|
||||
req.flash('error', `You must be logged in to access ${req.url}.`);
|
||||
res.redirect(Controller.route('auth', undefined, {
|
||||
res.redirect(route('auth', undefined, {
|
||||
redirect_uri: req.url,
|
||||
}));
|
||||
}
|
||||
@ -117,7 +117,7 @@ export class RequireGuestMiddleware extends Middleware {
|
||||
protected async handle(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
const proofs = await req.as(AuthMiddleware).getAuthGuard().getProofsForSession(req.getSession());
|
||||
if (proofs.length > 0) {
|
||||
res.redirect(Controller.route('home'));
|
||||
res.redirect(route('home'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -4,8 +4,8 @@ import {Session, SessionData} from "express-session";
|
||||
import {Connection} from "mysql";
|
||||
|
||||
import Application from "../Application.js";
|
||||
import {route} from "../common/Routing.js";
|
||||
import MailComponent from "../components/MailComponent.js";
|
||||
import Controller from "../Controller.js";
|
||||
import MysqlConnectionManager from "../db/MysqlConnectionManager.js";
|
||||
import Mail from "../mail/Mail.js";
|
||||
import {PENDING_ACCOUNT_REVIEW_MAIL_TEMPLATE} from "../Mails.js";
|
||||
@ -150,7 +150,7 @@ export default class AuthGuard {
|
||||
username: user.asOptional(UserNameComponent)?.getName() ||
|
||||
(await user.mainEmail.get())?.getOrFail('email') ||
|
||||
'Could not find an identifier',
|
||||
link: config.get<string>('public_url') + Controller.route('accounts-approval'),
|
||||
link: route('accounts-approval', {}, {}, true),
|
||||
}), config.get<string>('app.contact_email'));
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import {Session} from "express-session";
|
||||
import geoip from "geoip-lite";
|
||||
|
||||
import Application from "../../Application.js";
|
||||
import Controller from "../../Controller.js";
|
||||
import {route} from "../../common/Routing.js";
|
||||
import ModelFactory from "../../db/ModelFactory.js";
|
||||
import {WhereTest} from "../../db/ModelQuery.js";
|
||||
import Validator, {EMAIL_REGEX} from "../../db/Validator.js";
|
||||
@ -55,7 +55,7 @@ export default class MagicLinkAuthMethod implements AuthMethod<MagicLink> {
|
||||
|
||||
if (pendingLink) {
|
||||
if (await pendingLink.isValid()) {
|
||||
res.redirect(Controller.route('magic_link_lobby', undefined, {
|
||||
res.redirect(route('magic_link_lobby', undefined, {
|
||||
redirect_uri: req.getIntendedUrl() || pendingLink.original_url || undefined,
|
||||
}));
|
||||
return true;
|
||||
@ -105,7 +105,7 @@ export default class MagicLinkAuthMethod implements AuthMethod<MagicLink> {
|
||||
this.app,
|
||||
req.getSession().id,
|
||||
actionType,
|
||||
Controller.route('auth', undefined, {
|
||||
route('auth', undefined, {
|
||||
redirect_uri: req.getIntendedUrl() || undefined,
|
||||
}),
|
||||
email,
|
||||
@ -120,7 +120,7 @@ export default class MagicLinkAuthMethod implements AuthMethod<MagicLink> {
|
||||
},
|
||||
);
|
||||
|
||||
res.redirect(Controller.route('magic_link_lobby', undefined, {
|
||||
res.redirect(route('magic_link_lobby', undefined, {
|
||||
redirect_uri: req.getIntendedUrl(),
|
||||
}));
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import config from "config";
|
||||
import {Request, Response} from "express";
|
||||
import {ParsedUrlQueryInput} from "querystring";
|
||||
|
||||
import Application from "../../Application.js";
|
||||
import {QueryParamsRecord, route} from "../../common/Routing.js";
|
||||
import MailComponent from "../../components/MailComponent.js";
|
||||
import Controller from "../../Controller.js";
|
||||
import {QueryVariable} from "../../db/MysqlConnectionManager.js";
|
||||
@ -30,7 +30,7 @@ export default class MagicLinkController<A extends Application> extends Controll
|
||||
original_url: string,
|
||||
email: string,
|
||||
mailTemplate: MailTemplate,
|
||||
data: ParsedUrlQueryInput,
|
||||
data: QueryParamsRecord,
|
||||
magicLinkData: Record<string, QueryVariable> = {},
|
||||
): Promise<void> {
|
||||
Throttler.throttle('magic_link', process.env.NODE_ENV === 'test' ? 10 : 2, MagicLink.validityPeriod(),
|
||||
@ -49,10 +49,10 @@ export default class MagicLinkController<A extends Application> extends Controll
|
||||
|
||||
// Send email
|
||||
await app.as(MailComponent).sendMail(new Mail(mailTemplate, Object.assign(data, {
|
||||
link: `${config.get<string>('public_url')}${Controller.route('magic_link', undefined, {
|
||||
link: `${route('magic_link', undefined, {
|
||||
id: link.id,
|
||||
token: token,
|
||||
})}`,
|
||||
}, true)}`,
|
||||
})), email);
|
||||
}
|
||||
|
||||
@ -90,7 +90,7 @@ export default class MagicLinkController<A extends Application> extends Controll
|
||||
} catch (e) {
|
||||
if (e instanceof PendingApprovalAuthError) {
|
||||
req.flash('warning', `Your account is pending review. You'll receive an email once you're approved.`);
|
||||
res.redirect(Controller.route('auth'));
|
||||
res.redirect(route('auth'));
|
||||
return null;
|
||||
} else {
|
||||
throw e;
|
||||
@ -191,7 +191,7 @@ export default class MagicLinkController<A extends Application> extends Controll
|
||||
// Auth success
|
||||
const name = user.asOptional(UserNameComponent)?.getName();
|
||||
req.flash('success', `Authentication success. Welcome${name ? `, ${name}` : ''}`);
|
||||
res.redirect(req.getIntendedUrl() || Controller.route('home'));
|
||||
res.redirect(req.getIntendedUrl() || route('home'));
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -211,7 +211,7 @@ export default class MagicLinkController<A extends Application> extends Controll
|
||||
if (await UserEmail.select().with('user').where('email', email).first()) {
|
||||
req.flash('error', 'An account already exists with this email address.' +
|
||||
' Please first remove it there before adding it here.');
|
||||
res.redirect(Controller.route('account'));
|
||||
res.redirect(route('account'));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -228,7 +228,7 @@ export default class MagicLinkController<A extends Application> extends Controll
|
||||
}
|
||||
|
||||
req.flash('success', `Email address ${userEmail.email} successfully added.`);
|
||||
res.redirect(Controller.route('account'));
|
||||
res.redirect(route('account'));
|
||||
break;
|
||||
}
|
||||
|
||||
@ -250,7 +250,7 @@ export default class MagicLinkController<A extends Application> extends Controll
|
||||
}
|
||||
|
||||
req.flash('success', `Password successfully removed.`);
|
||||
res.redirect(Controller.route('account'));
|
||||
res.redirect(route('account'));
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@ import {Request, Response} from "express";
|
||||
import {Session} from "express-session";
|
||||
|
||||
import Application from "../../Application.js";
|
||||
import Controller from "../../Controller.js";
|
||||
import {route} from "../../common/Routing.js";
|
||||
import ModelFactory from "../../db/ModelFactory.js";
|
||||
import Validator, {InvalidFormatValidationError} from "../../db/Validator.js";
|
||||
import {ServerError} from "../../HttpError.js";
|
||||
@ -73,7 +73,7 @@ export default class PasswordAuthMethod implements AuthMethod<PasswordAuthProof>
|
||||
|
||||
if (e instanceof PendingApprovalAuthError) {
|
||||
req.flash('error', 'Your account is still being reviewed.');
|
||||
res.redirect(Controller.route('auth'));
|
||||
res.redirect(route('auth'));
|
||||
return;
|
||||
} else {
|
||||
const err = new InvalidFormatValidationError('Invalid password.');
|
||||
@ -86,7 +86,7 @@ export default class PasswordAuthMethod implements AuthMethod<PasswordAuthProof>
|
||||
}
|
||||
|
||||
req.flash('success', `Welcome, ${user.name}.`);
|
||||
res.redirect(req.getIntendedUrl() || Controller.route('home'));
|
||||
res.redirect(req.getIntendedUrl() || route('home'));
|
||||
}
|
||||
|
||||
public async attemptRegister(req: Request, res: Response, identifier: string): Promise<void> {
|
||||
@ -124,7 +124,7 @@ export default class PasswordAuthMethod implements AuthMethod<PasswordAuthProof>
|
||||
} catch (e) {
|
||||
if (e instanceof PendingApprovalAuthError) {
|
||||
req.flash('info', `Your account was successfully created and is pending review from an administrator.`);
|
||||
res.redirect(Controller.route('auth'));
|
||||
res.redirect(route('auth'));
|
||||
return;
|
||||
} else {
|
||||
throw e;
|
||||
@ -134,7 +134,7 @@ export default class PasswordAuthMethod implements AuthMethod<PasswordAuthProof>
|
||||
const user = await passwordAuthProof.getResource();
|
||||
|
||||
req.flash('success', `Your account was successfully created! Welcome, ${user?.as(UserNameComponent).getName()}.`);
|
||||
res.redirect(req.getIntendedUrl() || Controller.route('home'));
|
||||
res.redirect(req.getIntendedUrl() || route('home'));
|
||||
}
|
||||
|
||||
}
|
||||
|
68
src/common/Routing.ts
Normal file
68
src/common/Routing.ts
Normal file
@ -0,0 +1,68 @@
|
||||
export type RouteParams = { [p: string]: string | number } | string[] | string | number;
|
||||
export type QueryParamsRecord = Record<string, string | number | boolean | null | undefined>;
|
||||
export type QueryParams = string[][] | QueryParamsRecord | string | URLSearchParams;
|
||||
|
||||
export let routes: Record<string, string | undefined> = {};
|
||||
export function setRoutes(newRoutes: Record<string, string | undefined>): void {
|
||||
routes = newRoutes;
|
||||
}
|
||||
export function registerRoute(routeName: string, routePath: string): boolean {
|
||||
if (!routes[routeName]) {
|
||||
routes[routeName] = routePath;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export let publicUrl: string;
|
||||
export function setPublicUrl(newPublicUrl: string): void {
|
||||
publicUrl = newPublicUrl;
|
||||
}
|
||||
|
||||
export function route(
|
||||
route: string,
|
||||
params: RouteParams = [],
|
||||
query: QueryParams = '',
|
||||
absolute: boolean = false,
|
||||
): string {
|
||||
let path = routes[route];
|
||||
if (path === undefined) throw new Error(`Unknown route for name ${route}.`);
|
||||
|
||||
const regExp = getRouteParamRegExp('[a-zA-Z0-9_-]+', 'g');
|
||||
if (typeof params === 'string' || typeof params === 'number') {
|
||||
path = path.replace(regExp, '' + params);
|
||||
} else if (Array.isArray(params)) {
|
||||
let i = 0;
|
||||
for (const match of path.matchAll(regExp)) {
|
||||
if (match.length > 0) {
|
||||
path = path.replace(match[0], typeof params[i] !== 'undefined' ? params[i] : '');
|
||||
}
|
||||
i++;
|
||||
}
|
||||
path = path.replace(/\/+/g, '/');
|
||||
} else {
|
||||
for (const key of Object.keys(params)) {
|
||||
path = path.replace(getRouteParamRegExp(key), params[key].toString());
|
||||
}
|
||||
}
|
||||
|
||||
let filteredQuery: string[][] | Record<string, string> | string | URLSearchParams;
|
||||
if (typeof query !== "string" && !(query instanceof URLSearchParams) && !Array.isArray(query)) {
|
||||
for (const key of Object.keys(query)) {
|
||||
if (query[key] === undefined || query[key] === null) delete query[key];
|
||||
else if (typeof query[key] === "number") query[key] = (query[key] as number).toString();
|
||||
else if (typeof query[key] === "boolean") query[key] = query[key] ? '1' : '0';
|
||||
}
|
||||
filteredQuery = query as Record<string, string>;
|
||||
} else {
|
||||
filteredQuery = query;
|
||||
}
|
||||
|
||||
const queryStr = new URLSearchParams(filteredQuery).toString();
|
||||
return `${absolute ? publicUrl : ''}${path}` + (queryStr.length > 0 ? '?' + queryStr : '');
|
||||
}
|
||||
|
||||
export function getRouteParamRegExp(key: string, flags?: string): RegExp {
|
||||
return new RegExp(`:${key}(\\(.+?\\))?\\??`, flags);
|
||||
}
|
@ -7,7 +7,7 @@ export class TimeUnit {
|
||||
}
|
||||
}
|
||||
|
||||
export default class Time {
|
||||
export class Time {
|
||||
public static readonly UNITS = {
|
||||
MILLISECOND: {
|
||||
milliseconds: 1,
|
||||
@ -89,5 +89,4 @@ export default class Time {
|
||||
|
||||
return 'now';
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,11 +1,10 @@
|
||||
import config from "config";
|
||||
import {Express, Router} from "express";
|
||||
import path from "path";
|
||||
import {ParsedUrlQueryInput} from "querystring";
|
||||
import util from "util";
|
||||
|
||||
import ApplicationComponent from "../ApplicationComponent.js";
|
||||
import Controller, {RouteParams} from "../Controller.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";
|
||||
@ -40,11 +39,11 @@ export default class FrontendToolsComponent extends ApplicationComponent {
|
||||
|
||||
public async init(): Promise<void> {
|
||||
this.globals.set('route', (
|
||||
route: string,
|
||||
routeName: string,
|
||||
params: RouteParams = [],
|
||||
query: ParsedUrlQueryInput = {},
|
||||
query: QueryParams = '',
|
||||
absolute: boolean = false,
|
||||
) => Controller.route(route, params, query, absolute));
|
||||
) => 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'));
|
||||
|
@ -4,9 +4,8 @@ import nodemailer, {SentMessageInfo, Transporter} from "nodemailer";
|
||||
import util from "util";
|
||||
|
||||
import ApplicationComponent from "../ApplicationComponent.js";
|
||||
import Controller from "../Controller.js";
|
||||
import {route} from "../common/Routing.js";
|
||||
import MailViewEngine from "../frontend/MailViewEngine.js";
|
||||
import ViewEngine from "../frontend/ViewEngine.js";
|
||||
import {logger} from "../Logger.js";
|
||||
import Mail from "../mail/Mail.js";
|
||||
import MailError from "../mail/MailError.js";
|
||||
@ -91,7 +90,7 @@ export default class MailComponent extends ApplicationComponent {
|
||||
locals.mail_subject = options.subject;
|
||||
locals.mail_to = options.to;
|
||||
locals.mail_link = config.get<string>('public_url') +
|
||||
Controller.route('mail', [template.template], locals);
|
||||
route('mail', [template.template], locals);
|
||||
Object.assign(locals, this.getApp().as(FrontendToolsComponent).getGlobals().get());
|
||||
|
||||
// Log
|
||||
|
@ -9,6 +9,7 @@ import requireFromString from "require-from-string";
|
||||
import {compile, preprocess} from "svelte/compiler";
|
||||
import {sveltePreprocess} from "svelte-preprocess/dist/autoProcess.js";
|
||||
|
||||
import {publicUrl, routes} from "../common/Routing.js";
|
||||
import {logger} from "../Logger.js";
|
||||
import FileCache from "../utils/FileCache.js";
|
||||
import ViewEngine from "./ViewEngine.js";
|
||||
@ -89,6 +90,9 @@ export default class SvelteViewEngine extends ViewEngine {
|
||||
head: head,
|
||||
html: html,
|
||||
css: css,
|
||||
|
||||
routes: JSON.stringify(routes),
|
||||
publicUrl: publicUrl,
|
||||
};
|
||||
return rawOutput.replace(
|
||||
new RegExp(Object.keys(replaceMap).map(str => `%${str}%`).join('|'), 'g'),
|
||||
|
@ -1,4 +1,3 @@
|
||||
import config from "config";
|
||||
import {Request, Response} from "express";
|
||||
|
||||
import {RequireAdminMiddleware, RequireAuthMiddleware} from "../auth/AuthComponent.js";
|
||||
@ -6,6 +5,7 @@ import User from "../auth/models/User.js";
|
||||
import UserApprovedComponent from "../auth/models/UserApprovedComponent.js";
|
||||
import UserEmail from "../auth/models/UserEmail.js";
|
||||
import UserNameComponent from "../auth/models/UserNameComponent.js";
|
||||
import {route} from "../common/Routing.js";
|
||||
import MailComponent from "../components/MailComponent.js";
|
||||
import Controller from "../Controller.js";
|
||||
import ModelFactory from "../db/ModelFactory.js";
|
||||
@ -24,7 +24,7 @@ export default class BackendController extends Controller {
|
||||
super();
|
||||
if (User.isApprovalMode()) {
|
||||
BackendController.registerMenuElement({
|
||||
getLink: async () => Controller.route('accounts-approval'),
|
||||
getLink: async () => route('accounts-approval'),
|
||||
getDisplayString: async () => {
|
||||
const pendingUsersCount = (await User.select()
|
||||
.where('approved', false)
|
||||
@ -81,12 +81,12 @@ export default class BackendController extends Controller {
|
||||
if (email && email.email) {
|
||||
await this.getApp().as(MailComponent).sendMail(new Mail(ACCOUNT_REVIEW_NOTICE_MAIL_TEMPLATE, {
|
||||
approved: true,
|
||||
link: config.get<string>('public_url') + Controller.route('auth'),
|
||||
link: route('auth', {}, {}, true),
|
||||
}), email.email);
|
||||
}
|
||||
|
||||
req.flash('success', `Account successfully approved.`);
|
||||
res.redirect(Controller.route('accounts-approval'));
|
||||
res.redirect(route('accounts-approval'));
|
||||
}
|
||||
|
||||
protected async postRejectAccount(req: Request, res: Response): Promise<void> {
|
||||
@ -101,7 +101,7 @@ export default class BackendController extends Controller {
|
||||
}
|
||||
|
||||
req.flash('success', `Account successfully deleted.`);
|
||||
res.redirect(Controller.route('accounts-approval'));
|
||||
res.redirect(route('accounts-approval'));
|
||||
}
|
||||
|
||||
protected async accountRequest(req: Request): Promise<{
|
||||
@ -122,7 +122,7 @@ export default class BackendController extends Controller {
|
||||
|
||||
export interface BackendMenuElement {
|
||||
/**
|
||||
* Returns the link of this menu element (usually using {@code Controller.route})
|
||||
* Returns the link of this menu element (usually using {@code route})
|
||||
*/
|
||||
getLink(): Promise<string>;
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import {Options} from "nodemailer/lib/mailer";
|
||||
import {ParsedUrlQueryInput} from "querystring";
|
||||
|
||||
import {QueryParams, QueryParamsRecord} from "../common/Routing.js";
|
||||
import MailError from "./MailError.js";
|
||||
import MailTemplate from "./MailTemplate.js";
|
||||
|
||||
@ -9,7 +10,7 @@ export default class Mail {
|
||||
|
||||
public constructor(
|
||||
private readonly template: MailTemplate,
|
||||
private readonly data: ParsedUrlQueryInput = {},
|
||||
private readonly data: QueryParamsRecord = {},
|
||||
) {
|
||||
this.options.subject = this.template.getSubject(data);
|
||||
|
||||
@ -30,7 +31,7 @@ export default class Mail {
|
||||
return this.template;
|
||||
}
|
||||
|
||||
public getData(): ParsedUrlQueryInput {
|
||||
public getData(): QueryParamsRecord {
|
||||
return {...this.data};
|
||||
}
|
||||
|
||||
|
@ -1,35 +0,0 @@
|
||||
import Controller from "../src/Controller.js";
|
||||
|
||||
describe('Controller.route()', () => {
|
||||
new class extends Controller {
|
||||
public routes(): void {
|
||||
const emptyHandler = () => {
|
||||
//
|
||||
};
|
||||
this.get('/test/no-param', emptyHandler, 'test.no-param');
|
||||
this.get('/test/no-param/', emptyHandler, 'test.no-param-slash');
|
||||
this.get('/test/one-param/:param', emptyHandler, 'test.one-param');
|
||||
this.get('/test/two-params/:param1/:param2', emptyHandler, 'test.two-params');
|
||||
this.get('/test/paginated/:page([0-9]+)?', emptyHandler, 'test.paginated');
|
||||
this.get('/test/slug/:slug([a-zA-Z0-9]+)?', emptyHandler, 'test.slug');
|
||||
}
|
||||
}().setupRoutes();
|
||||
|
||||
test('Empty params', () => {
|
||||
expect(Controller.route('test.no-param')).toBe('/test/no-param');
|
||||
expect(Controller.route('test.no-param-slash')).toBe('/test/no-param/');
|
||||
expect(Controller.route('test.one-param')).toBe('/test/one-param/');
|
||||
expect(Controller.route('test.two-params')).toBe('/test/two-params/');
|
||||
expect(Controller.route('test.paginated')).toBe('/test/paginated/');
|
||||
expect(Controller.route('test.slug')).toBe('/test/slug/');
|
||||
});
|
||||
|
||||
test('Populated params', () => {
|
||||
expect(Controller.route('test.no-param')).toBe('/test/no-param');
|
||||
expect(Controller.route('test.no-param-slash')).toBe('/test/no-param/');
|
||||
expect(Controller.route('test.one-param', {param: 'value'})).toBe('/test/one-param/value');
|
||||
expect(Controller.route('test.two-params', {param1: 'value1', param2: 'value2'})).toBe('/test/two-params/value1/value2');
|
||||
expect(Controller.route('test.paginated', {page: 5})).toBe('/test/paginated/5');
|
||||
expect(Controller.route('test.slug', {slug: 'abc'})).toBe('/test/slug/abc');
|
||||
});
|
||||
});
|
36
test/common/Routing.test.ts
Normal file
36
test/common/Routing.test.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import {route} from "../../src/common/Routing.js";
|
||||
import Controller from "../../src/Controller.js";
|
||||
|
||||
describe('route()', () => {
|
||||
new class extends Controller {
|
||||
public routes(): void {
|
||||
const emptyHandler = () => {
|
||||
//
|
||||
};
|
||||
this.get('/test/no-param', emptyHandler, 'test.no-param');
|
||||
this.get('/test/no-param/', emptyHandler, 'test.no-param-slash');
|
||||
this.get('/test/one-param/:param', emptyHandler, 'test.one-param');
|
||||
this.get('/test/two-params/:param1/:param2', emptyHandler, 'test.two-params');
|
||||
this.get('/test/paginated/:page([0-9]+)?', emptyHandler, 'test.paginated');
|
||||
this.get('/test/slug/:slug([a-zA-Z0-9]+)?', emptyHandler, 'test.slug');
|
||||
}
|
||||
}().setupRoutes();
|
||||
|
||||
test('Empty params', () => {
|
||||
expect(route('test.no-param')).toBe('/test/no-param');
|
||||
expect(route('test.no-param-slash')).toBe('/test/no-param/');
|
||||
expect(route('test.one-param')).toBe('/test/one-param/');
|
||||
expect(route('test.two-params')).toBe('/test/two-params/');
|
||||
expect(route('test.paginated')).toBe('/test/paginated/');
|
||||
expect(route('test.slug')).toBe('/test/slug/');
|
||||
});
|
||||
|
||||
test('Populated params', () => {
|
||||
expect(route('test.no-param')).toBe('/test/no-param');
|
||||
expect(route('test.no-param-slash')).toBe('/test/no-param/');
|
||||
expect(route('test.one-param', {param: 'value'})).toBe('/test/one-param/value');
|
||||
expect(route('test.two-params', {param1: 'value1', param2: 'value2'})).toBe('/test/two-params/value1/value2');
|
||||
expect(route('test.paginated', {page: 5})).toBe('/test/paginated/5');
|
||||
expect(route('test.slug', {slug: 'abc'})).toBe('/test/slug/abc');
|
||||
});
|
||||
});
|
@ -21,7 +21,8 @@
|
||||
"src/types"
|
||||
],
|
||||
"lib": [
|
||||
"es2020"
|
||||
"es2020",
|
||||
"dom"
|
||||
],
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
|
Loading…
Reference in New Issue
Block a user