1 | import * as express from "express";
|
2 | import App from "./app";
|
3 | import { app } from "./app";
|
4 | import { FileOptions } from "./file.response";
|
5 | import RenderResponse from "./render.response";
|
6 | import RedirectResponse from "./redirect.response";
|
7 | import SkipResponse from "./skip.response";
|
8 | import UnauthorizedResponse from "./unauthorized.response";
|
9 | import FileResponse from "./file.response";
|
10 | import Request from "./request";
|
11 | import { LynxControllerMetadata } from "./decorators";
|
12 | import Media from "./entities/media.entity";
|
13 | import StatusError from "./status-error";
|
14 |
|
15 | import {
|
16 | createTestAccount,
|
17 | createTransport,
|
18 | getTestMessageUrl,
|
19 | Transporter
|
20 | } from "nodemailer";
|
21 |
|
22 | import { logger } from "./logger";
|
23 | import Logger from "./logger";
|
24 |
|
25 | let mailClient: Transporter;
|
26 | let guard = false;
|
27 | function syncronizedInit() {
|
28 | if (guard) return;
|
29 | guard = true;
|
30 | if (!mailClient) {
|
31 | if (!app.config.mailer.host) {
|
32 | try {
|
33 | createTestAccount((err, account) => {
|
34 | if (err) {
|
35 | logger.error(err);
|
36 | guard = false;
|
37 | return;
|
38 | }
|
39 | mailClient = createTransport({
|
40 | host: "smtp.ethereal.email",
|
41 | port: 587,
|
42 | secure: false, // true for 465, false for other ports
|
43 | auth: {
|
44 | user: account.user, // generated ethereal user
|
45 | pass: account.pass // generated ethereal password
|
46 | }
|
47 | });
|
48 | guard = false;
|
49 | });
|
50 | } catch (e) {
|
51 | guard = false;
|
52 | logger.error(e);
|
53 | }
|
54 | } else {
|
55 | try {
|
56 | mailClient = createTransport(app.config.mailer);
|
57 | guard = false;
|
58 | } catch (e) {
|
59 | guard = false;
|
60 | logger.error(e);
|
61 | }
|
62 | }
|
63 | }
|
64 | }
|
65 |
|
66 | export enum FlashType {
|
67 | primary,
|
68 | secondary,
|
69 | success,
|
70 | danger,
|
71 | warning,
|
72 | info,
|
73 | light,
|
74 | dark
|
75 | }
|
76 |
|
77 | function mapFlashTypeToString(type: FlashType): string {
|
78 | switch (type) {
|
79 | case FlashType.primary:
|
80 | return "primary";
|
81 | case FlashType.secondary:
|
82 | return "secondary";
|
83 | case FlashType.success:
|
84 | return "success";
|
85 | case FlashType.danger:
|
86 | return "danger";
|
87 | case FlashType.warning:
|
88 | return "warning";
|
89 | case FlashType.info:
|
90 | return "info";
|
91 | case FlashType.light:
|
92 | return "light";
|
93 | case FlashType.dark:
|
94 | return "dark";
|
95 | }
|
96 | }
|
97 |
|
98 | export interface FlashMessage {
|
99 | type: FlashType;
|
100 | message: string;
|
101 | }
|
102 |
|
103 | /**
|
104 | * This class defines the basic class for any controllers. It implements a lot
|
105 | * of utility methods in order to correctly generate any response.
|
106 | */
|
107 | export class BaseController {
|
108 | public app: App;
|
109 | private _metadata: LynxControllerMetadata;
|
110 | public logger: Logger = logger;
|
111 |
|
112 | get metadata(): LynxControllerMetadata {
|
113 | return this._metadata;
|
114 | }
|
115 |
|
116 | constructor(app: App) {
|
117 | this.app = app;
|
118 | syncronizedInit();
|
119 | }
|
120 |
|
121 | /**
|
122 | * This method is called only when the constructed has been completed.
|
123 | * Since this method is async, it can be used to perform some initialization
|
124 | * that needed the use of the await keyword. */
|
125 | async postConstructor() {}
|
126 |
|
127 | /**
|
128 | * Add a value to the current request context.
|
129 | * Any variable added with this method will available in the template context
|
130 | * thought the @method render method.
|
131 | * @param req the current Request
|
132 | * @param key the key of the value to add
|
133 | * @param value the value to add
|
134 | */
|
135 | public addToContext(req: Request, key: string, value: any) {
|
136 | if (!req.lynxContext) {
|
137 | req.lynxContext = {};
|
138 | }
|
139 | req.lynxContext[key] = value;
|
140 | }
|
141 |
|
142 | /**
|
143 | * Utility method to generate an error with a status code.
|
144 | * This method should be used instead of the usual throw new Error(msg).
|
145 | * In this way, a proper HTTP status code can be used (for example, 404 or 500),
|
146 | * instead of the default 400.
|
147 | * @param status the http status code to return
|
148 | * @param message the error message
|
149 | * @return a new @type StatusError object
|
150 | */
|
151 | public error(status: number, message: string): StatusError {
|
152 | let err = new StatusError(message);
|
153 | err.statusCode = status;
|
154 | return err;
|
155 | }
|
156 |
|
157 | /**
|
158 | * This method generate an url to a route starting from the route name and
|
159 | * optionally its parameters.
|
160 | * If a parameter not is used to generate the route url, it will be appended
|
161 | * as a query parameter.
|
162 | * @param name the name of the route
|
163 | * @param parameters a plain object containing the paramters for the route.
|
164 | */
|
165 | public route(name: string, parameters?: any): string {
|
166 | return this.app.route(name, parameters);
|
167 | }
|
168 |
|
169 | /**
|
170 | * Generate a web page starting from a template and using a generated context.
|
171 | * @param view the name of the view
|
172 | * @param req the request object
|
173 | * @param context a plain object containing any necessary data needed by the view
|
174 | */
|
175 | public render(view: string, req: Request, context?: any): RenderResponse {
|
176 | if (!view.endsWith(".njk")) {
|
177 | view = view + ".njk";
|
178 | }
|
179 | if (!context) {
|
180 | context = {};
|
181 | }
|
182 | context.req = req;
|
183 | context.flash = (req.session as any).sessionFlash;
|
184 | for (let key in req.lynxContext) {
|
185 | context[key] = req.lynxContext[key];
|
186 | }
|
187 | delete (req.session as any).sessionFlash;
|
188 | return new RenderResponse(view, context);
|
189 | }
|
190 |
|
191 | /**
|
192 | * Redirect the current route to another
|
193 | * @param routeName the new of the target route
|
194 | * @param routeParams a plain object containing the paramters for the route.
|
195 | */
|
196 | public redirect(routeName: string, routeParams?: any): RedirectResponse {
|
197 | return new RedirectResponse(this.route(routeName, routeParams));
|
198 | }
|
199 |
|
200 | /**
|
201 | * Add a flash message in the current request.
|
202 | * @param msg the FlashMessage to be included
|
203 | * @param req the request
|
204 | */
|
205 | public addFlashMessage(msg: FlashMessage, req: Request) {
|
206 | let session = req.session as any;
|
207 | if (!session.sessionFlash) {
|
208 | session.sessionFlash = [];
|
209 | }
|
210 | session.sessionFlash.push({
|
211 | type: mapFlashTypeToString(msg.type),
|
212 | message: this.tr(msg.message, req)
|
213 | });
|
214 | }
|
215 |
|
216 | /**
|
217 | * Add a success flash message in the current request.
|
218 | * @param msg the string (can be localized) of the message
|
219 | * @param req the request
|
220 | */
|
221 | public addSuccessMessage(msg: string, req: Request) {
|
222 | this.addFlashMessage({ type: FlashType.success, message: msg }, req);
|
223 | }
|
224 |
|
225 | /**
|
226 | * Add an error flash message in the current request.
|
227 | * @param msg the string (can be localized) of the message
|
228 | * @param req the request
|
229 | */
|
230 | public addErrorMessage(msg: string, req: Request) {
|
231 | this.addFlashMessage({ type: FlashType.danger, message: msg }, req);
|
232 | }
|
233 |
|
234 | /**
|
235 | * Generate a response suitable to file download. This method can also be
|
236 | * used to generate images of specific dimensions.
|
237 | * @param path the string path of the file, or a Media object to be downloaded
|
238 | * @param options options to correctly generate the output file
|
239 | */
|
240 | public download(path: string | Media, options?: FileOptions): FileResponse {
|
241 | let f: FileResponse;
|
242 | if (path instanceof Media) {
|
243 | if (path.isDirectory) {
|
244 | throw new Error("unable to download a directory");
|
245 | }
|
246 | f = new FileResponse(path.fileName);
|
247 | f.contentType = path.mimetype;
|
248 | if (path.originalName) {
|
249 | f.fileName = path.originalName;
|
250 | }
|
251 | } else {
|
252 | f = new FileResponse(path);
|
253 | }
|
254 | if (options) {
|
255 | f.options = options;
|
256 | }
|
257 | return f;
|
258 | }
|
259 |
|
260 | /**
|
261 | * Generate an unauthorized response.
|
262 | */
|
263 | public unauthorized(): UnauthorizedResponse {
|
264 | return new UnauthorizedResponse();
|
265 | }
|
266 |
|
267 | /**
|
268 | * Generate a skip resopnse. In this particuar case, the original Express `next()`
|
269 | * will be executed, causing the controller chain to continue its execution.
|
270 | */
|
271 | public next(): SkipResponse {
|
272 | return new SkipResponse();
|
273 | }
|
274 |
|
275 | /**
|
276 | * Utility method to send emails from a controller.
|
277 | * This method is similar to the `sendMail` method, but define a lower level API.
|
278 | * Indeed, it directly accepts the text and the html of the email, and not the templates urls.
|
279 | * @param dest the email destination (can also be an array of addresses)
|
280 | * @param subject the subject of the email
|
281 | * @param text the text version of the email
|
282 | * @param html the html version of the email
|
283 | */
|
284 | public async sendRawMail(
|
285 | dest: string | string[],
|
286 | subject: string,
|
287 | text: string,
|
288 | html: string,
|
289 | ) {
|
290 | let mailOptions = {
|
291 | from: this.app.config.mailer.sender, // sender address
|
292 | to: dest,
|
293 | subject: subject, // Subject line
|
294 | text: text, // plain text body
|
295 | html: html // html body
|
296 | };
|
297 | try {
|
298 | let result = await mailClient.sendMail(mailOptions);
|
299 | if (result) {
|
300 | logger.debug("Preview URL: %s", getTestMessageUrl(result));
|
301 | }
|
302 | return true;
|
303 | } catch (e) {
|
304 | logger.error(e);
|
305 | }
|
306 | return false;
|
307 | }
|
308 |
|
309 | /**
|
310 | * Utility method to send an email from a controller. This method is async,
|
311 | * so use the await keyword (or eventually a promise) to correctly read the
|
312 | * return value.
|
313 | * This method uses the template engine to compile the email.
|
314 | * NOTE: internally, this method uses the `sendRawMail` method.
|
315 | * @param req the current request
|
316 | * @param dest the email destination (can also be an array of addresses)
|
317 | * @param subjectTemplateString the subject of the email, that can also be a string template
|
318 | * @param textTemplate the text version of the email, referencing a path in the view folders
|
319 | * @param htmlTemplate the html version of the email, referencing a path in the view folders
|
320 | * @param context a plain object containing any necessary data needed by the view
|
321 | */
|
322 | public async sendMail(
|
323 | req: express.Request,
|
324 | dest: string | string[],
|
325 | subjectTemplateString: string,
|
326 | textTemplate: string,
|
327 | htmlTemplate: string,
|
328 | context: any
|
329 | ): Promise<boolean> {
|
330 | if (!context) {
|
331 | context = {};
|
332 | }
|
333 | context.req = req;
|
334 |
|
335 | let subject = this.app.nunjucksEnvironment.renderString(
|
336 | subjectTemplateString,
|
337 | context
|
338 | );
|
339 |
|
340 | if (!textTemplate.endsWith(".njk")) {
|
341 | textTemplate += ".njk";
|
342 | }
|
343 | if (!htmlTemplate.endsWith(".njk")) {
|
344 | htmlTemplate += ".njk";
|
345 | }
|
346 | let text = this.app.nunjucksEnvironment.render(textTemplate, context);
|
347 | let html = this.app.nunjucksEnvironment.render(htmlTemplate, context);
|
348 |
|
349 | return this.sendRawMail(dest, subject, text, html);
|
350 | }
|
351 |
|
352 | /**
|
353 | * Utility method to obtain a translated string.
|
354 | * @param str the string key to be translated
|
355 | * @param req the original request
|
356 | */
|
357 | public tr(str: string, req: Request): string {
|
358 | return this.app.translate(str, req);
|
359 | }
|
360 |
|
361 | /**
|
362 | * Utility method to obtain a translated string, formatted with parameters.
|
363 | * Each parameter should be encoded as {0}, {1}, etc...
|
364 | * @param str the string key to be translated
|
365 | * @param req the original request
|
366 | * @param args the arguments to format the string
|
367 | */
|
368 | public trFormat(str: string, req: Request, ...args: any): string {
|
369 | let translated = this.tr(str, req);
|
370 | return this.format(translated, args);
|
371 | }
|
372 |
|
373 | private format(fmt: string, ...args: any) {
|
374 | if (!fmt.match(/^(?:(?:(?:[^{}]|(?:\{\{)|(?:\}\}))+)|(?:\{[0-9]+\}))+$/)) {
|
375 | throw new Error('invalid format string.');
|
376 | }
|
377 | return fmt.replace(/((?:[^{}]|(?:\{\{)|(?:\}\}))+)|(?:\{([0-9]+)\})/g, (_, str, index) => {
|
378 | if (str) {
|
379 | return str.replace(/(?:{{)|(?:}})/g, (m:string[]) => m[0]);
|
380 | } else {
|
381 | if (index >= args.length) {
|
382 | throw new Error('argument index is out of range in format');
|
383 | }
|
384 | return args[index];
|
385 | }
|
386 | });
|
387 | }
|
388 | }
|