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