UNPKG

12.9 kBPlain TextView Raw
1import * as express from "express";
2import App from "./app";
3import { app } from "./app";
4import { FileOptions } from "./file.response";
5import RenderResponse from "./render.response";
6import RedirectResponse from "./redirect.response";
7import SkipResponse from "./skip.response";
8import UnauthorizedResponse from "./unauthorized.response";
9import FileResponse from "./file.response";
10import Request from "./request";
11import { LynxControllerMetadata } from "./decorators";
12import Media from "./entities/media.entity";
13import StatusError from "./status-error";
14
15import {
16 createTestAccount,
17 createTransport,
18 getTestMessageUrl,
19 Transporter
20} from "nodemailer";
21
22import { logger } from "./logger";
23import Logger from "./logger";
24
25let mailClient: Transporter;
26let guard = false;
27function 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
66export enum FlashType {
67 primary,
68 secondary,
69 success,
70 danger,
71 warning,
72 info,
73 light,
74 dark
75}
76
77function 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
98export 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 */
107export 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}