Browse Source

email templates

main
Nikky 4 years ago
parent
commit
78e9705663
  1. 0
      config/database.config.js
  2. 4
      config/email.config.js
  3. 6
      config/server.config.js
  4. 5
      src/config/server.config.js
  5. 26
      src/email/templates.js
  6. 10
      src/email/texts.json
  7. 93
      src/email/wrapper - Copy.html
  8. 63
      src/email/wrapper.html
  9. 4
      src/index.js
  10. 4
      src/logic/cache.js
  11. 13
      src/logic/email.js
  12. 4
      src/logic/session.js
  13. 11
      src/misc/email-templates.js
  14. 24
      src/model/user.d.ts
  15. 15
      src/model/user.model.js
  16. 15
      src/route/auth.controller.js
  17. 3
      src/route/user.controller.js
  18. 2
      src/utils/common.utils.js
  19. 57
      src/utils/localization.utils.js
  20. 10
      src/utils/module.utils.js
  21. 2
      src/utils/web.utils.js

0
src/config/database.config.js → config/database.config.js

4
src/config/email.config.js → config/email.config.js

@ -7,5 +7,7 @@ export const MailgunAccount = {
}; };
export const EmailConfig = { export const EmailConfig = {
from: "Black Tide 🌊🖤 <noreply@mail.headpat.network>" from: "Black Tide <noreply@mail.headpat.network>",
upperText: "Greetings from HPN!",
bottomText: "&reg; Black Tide 🌊🖤, 2022"
}; };

6
config/server.config.js

@ -0,0 +1,6 @@
export const ServerConfig = {
port: 3001,
host: '127.0.0.1',
client: 'https://example.com'
};

5
src/config/server.config.js

@ -1,5 +0,0 @@
export const ServerConfig = {
port: 3001,
host: '127.0.0.1'
};

26
src/email/templates.js

@ -0,0 +1,26 @@
import path from 'path';
import { fileURLToPath } from 'url';
import { EmailConfig } from '../../config/email.config.js';
import { minutes } from '../utils/common.utils.js';
import { TemplateFile, TranslationFile } from '../utils/localization.utils.js';
export const RestorePasswordEmail = ({username, locale, url}) => {
const subject = translation.get('email.restore.subject', locale);
const bodyText = translation.get('email.restore.body', locale, {username, url});
return {
subject,
text: bodyText,
html: templates.wrapper.generate({...common, body: bodyText})
};
};
const common = {
title: EmailConfig.upperText,
company: EmailConfig.bottomText
};
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const refreshRate = 60*minutes;
const translation = new TranslationFile(__dirname+'/texts.json', refreshRate);
const templates = {
wrapper: new TemplateFile(__dirname+'/wrapper.html', refreshRate, {html: true})
};

10
src/email/texts.json

@ -0,0 +1,10 @@
{
"en": {
"email.restore.subject": "Account recovery.",
"email.restore.body": "Hello, ${username}.\nWe received a request to reset your password.\nFollow the link below to proceed:\n${url}\nIf you didn't request this change, ignore this email."
},
"ru": {
"email.restore.subject": "Восстановление аккаунта.",
"email.restore.body": "Здравствуйте, ${username}.\nМы получили запрос на сброс пароля на вашем аккаунте.\nНажмите на ссылку для подтверждения:\n${url}\nЕсли это были не вы, проигнорируйте это письмо."
}
}

93
src/email/wrapper - Copy.html

@ -0,0 +1,93 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="x-apple-disable-message-reformatting">
<title></title>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<style>
table, td, div, h1, p {font-family: Arial, sans-serif;}
</style>
</head>
<body style="margin:0;padding:0;">
<table role="presentation" style="width:100%;border-collapse:collapse;border:0;border-spacing:0;background:#ffffff;">
<tr>
<td align="center" style="padding:0;">
<table role="presentation" style="width:602px;border-collapse:collapse;border:1px solid #cccccc;border-spacing:0;text-align:left;">
<tr>
<td align="center" style="padding:40px 0 30px 0;background:#70bbd9;">
<img src="https://assets.codepen.io/210284/h1.png" alt="" width="300" style="height:auto;display:block;" />
</td>
</tr>
<tr>
<td style="padding:36px 30px 42px 30px;">
<table role="presentation" style="width:100%;border-collapse:collapse;border:0;border-spacing:0;">
<tr>
<td style="padding:0 0 36px 0;color:#153643;">
<h1 style="font-size:24px;margin:0 0 20px 0;font-family:Arial,sans-serif;">Creating Email Magic</h1>
<p style="margin:0 0 12px 0;font-size:16px;line-height:24px;font-family:Arial,sans-serif;">Lorem ipsum dolor sit amet, consectetur adipiscing elit. In tempus adipiscing felis, sit amet blandit ipsum volutpat sed. Morbi porttitor, eget accumsan et dictum, nisi libero ultricies ipsum, posuere neque at erat.</p>
<p style="margin:0;font-size:16px;line-height:24px;font-family:Arial,sans-serif;"><a href="http://www.example.com" style="color:#ee4c50;text-decoration:underline;">In tempus felis blandit</a></p>
</td>
</tr>
<tr>
<td style="padding:0;">
<table role="presentation" style="width:100%;border-collapse:collapse;border:0;border-spacing:0;">
<tr>
<td style="width:260px;padding:0;vertical-align:top;color:#153643;">
<p style="margin:0 0 25px 0;font-size:16px;line-height:24px;font-family:Arial,sans-serif;"><img src="https://assets.codepen.io/210284/left.gif" alt="" width="260" style="height:auto;display:block;" /></p>
<p style="margin:0 0 12px 0;font-size:16px;line-height:24px;font-family:Arial,sans-serif;">Lorem ipsum dolor sit amet, consectetur adipiscing elit. In tempus adipiscing felis, sit amet blandit ipsum volutpat sed. Morbi porttitor, eget accumsan dictum, est nisi libero ultricies ipsum, in posuere mauris neque at erat.</p>
<p style="margin:0;font-size:16px;line-height:24px;font-family:Arial,sans-serif;"><a href="http://www.example.com" style="color:#ee4c50;text-decoration:underline;">Blandit ipsum volutpat sed</a></p>
</td>
<td style="width:20px;padding:0;font-size:0;line-height:0;">&nbsp;</td>
<td style="width:260px;padding:0;vertical-align:top;color:#153643;">
<p style="margin:0 0 25px 0;font-size:16px;line-height:24px;font-family:Arial,sans-serif;"><img src="https://assets.codepen.io/210284/right.gif" alt="" width="260" style="height:auto;display:block;" /></p>
<p style="margin:0 0 12px 0;font-size:16px;line-height:24px;font-family:Arial,sans-serif;">Morbi porttitor, eget est accumsan dictum, nisi libero ultricies ipsum, in posuere mauris neque at erat. Lorem ipsum dolor sit amet, consectetur adipiscing elit. In tempus adipiscing felis, sit amet blandit ipsum volutpat sed.</p>
<p style="margin:0;font-size:16px;line-height:24px;font-family:Arial,sans-serif;"><a href="http://www.example.com" style="color:#ee4c50;text-decoration:underline;">In tempus felis blandit</a></p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="padding:30px;background:#ee4c50;">
<table role="presentation" style="width:100%;border-collapse:collapse;border:0;border-spacing:0;font-size:9px;font-family:Arial,sans-serif;">
<tr>
<td style="padding:0;width:50%;" align="left">
<p style="margin:0;font-size:14px;line-height:16px;font-family:Arial,sans-serif;color:#ffffff;">
&reg; Someone, Somewhere 2021<br/><a href="http://www.example.com" style="color:#ffffff;text-decoration:underline;">Unsubscribe</a>
</p>
</td>
<td style="padding:0;width:50%;" align="right">
<table role="presentation" style="border-collapse:collapse;border:0;border-spacing:0;">
<tr>
<td style="padding:0 0 0 10px;width:38px;">
<a href="http://www.twitter.com/" style="color:#ffffff;"><img src="https://assets.codepen.io/210284/tw_1.png" alt="Twitter" width="38" style="height:auto;display:block;border:0;" /></a>
</td>
<td style="padding:0 0 0 10px;width:38px;">
<a href="http://www.facebook.com/" style="color:#ffffff;"><img src="https://assets.codepen.io/210284/fb_1.png" alt="Facebook" width="38" style="height:auto;display:block;border:0;" /></a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

63
src/email/wrapper.html

@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="x-apple-disable-message-reformatting">
<title></title>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<style>
table, td, div, h1, p {font-family: Arial, sans-serif;}
</style>
</head>
<body style="margin:0;padding:0;">
<table role="presentation" style="width:100%;border-collapse:collapse;border:0;border-spacing:0;background:#ffffff;margin-top: 10px;">
<tr>
<td align="center" style="padding:0;">
<table role="presentation" style="width:602px;border-collapse:collapse;border:1px solid #cccccc;border-spacing:0;text-align:left;">
<tr>
<td align="center" style="padding:10px 0 10px 0;background: #5e95f4;background: linear-gradient(135deg, #5e95f4 0%,#4581f1 50%,#2768f3 51%,#1641dc 100%);">
<span style="font-size: 48px;color:#ffffff">${title}</span>
</td>
</tr>
<tr>
<td style="padding:36px 30px 42px 30px;">
${body}
</td>
</tr>
<tr>
<td style="padding:30px;background: #6f6f6f;background: linear-gradient(to bottom, #6f6f6f 0%,#707070 53%,#474747 100%);">
<table role="presentation" style="width:100%;border-collapse:collapse;border:0;border-spacing:0;font-size:9px;font-family:Arial,sans-serif;">
<tr>
<td style="padding:0;width:50%;" align="left">
<p style="margin:0;font-size:14px;line-height:16px;font-family:Arial,sans-serif;color:#ffffff;">
${company}<br/><a href="http://www.example.com" style="color:#ffffff;text-decoration:underline;">Unsubscribe</a>
</p>
</td>
<td style="padding:0;width:50%;" align="right">
<table role="presentation" style="border-collapse:collapse;border:0;border-spacing:0;">
<tr>
<td style="padding:0 0 0 10px;width:38px;">
<a href="https://twitter.com/inque_n" style="color:#ffffff;"><img src="https://assets.codepen.io/210284/tw_1.png" alt="Twitter" width="38" style="height:auto;display:block;border:0;" /></a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

4
src/index.js

@ -3,8 +3,8 @@ import Fastify from 'fastify'
import formBodyPlugin from 'fastify-formbody'; import formBodyPlugin from 'fastify-formbody';
import fastifyMultipart from 'fastify-multipart'; import fastifyMultipart from 'fastify-multipart';
import Sequelize from 'sequelize'; import Sequelize from 'sequelize';
import { DatabaseConfig } from './config/database.config.js'; import { DatabaseConfig } from '../config/database.config.js';
import { ServerConfig } from './config/server.config.js'; import { ServerConfig } from '../config/server.config.js';
import { importAll } from './utils/module.utils.js'; import { importAll } from './utils/module.utils.js';
const app = Fastify({logger: false}); const app = Fastify({logger: false});

4
src/logic/cache.js

@ -1,6 +1,6 @@
/** /**
* A map with a limited number of entries. (unspecified order) * A regular map, except with a limited number of entries.
*/ */
export class CacheTable{ export class CacheTable{
@ -35,7 +35,7 @@ export class CacheTable{
/** /**
* A cache table that keeps track of access within multiple time intervals. * A cache table that keeps track of accesses within multiple time intervals.
*/ */
export class SpamCache extends CacheTable{ export class SpamCache extends CacheTable{

13
src/logic/email.js

@ -1,6 +1,7 @@
import { EmailConfig, MailgunAccount } from "../config/email.config.js"; import { EmailConfig, MailgunAccount } from "../../config/email.config.js";
import Mailgun from "mailgun-js"; import Mailgun from "mailgun-js";
import { RestorePasswordEmail } from "../misc/email-templates.js"; import { RestorePasswordEmail } from "../email/templates.js";
import { ServerConfig } from "../../config/server.config.js";
const mailgun = Mailgun(MailgunAccount); const mailgun = Mailgun(MailgunAccount);
@ -16,15 +17,17 @@ function sendMail(opts){
}); });
} }
export async function sendRestorationLink(user, app){ export async function sendRestorationLink(user){
const template = RestorePasswordEmail({ const template = RestorePasswordEmail({
username: user.username, username: user.username,
url: app.hostname+'/'+user.restoreCode locale: user.locale,
url: ServerConfig.client+'/restore?code='+user.restoreCode
}); });
return await sendMail({ return await sendMail({
from: EmailConfig.from, from: EmailConfig.from,
to: user.email, to: user.email,
subject: template.subject, subject: template.subject,
text: template.text text: template.text,
html: template.html
}); });
} }

4
src/logic/session.js

@ -22,11 +22,11 @@ export class UserSession{
return userCache.get(token); return userCache.get(token);
} }
static onUpdate(user){ static onUpdate(user){
const session = find(user); const session = UserSession.find(user);
if(session) session.user = user; if(session) session.user = user;
} }
static onDestroy(user){ static onDestroy(user){
const session = find(user); const session = UserSession.find(user);
if(session) session.kill(); if(session) session.kill();
} }
} }

11
src/misc/email-templates.js

@ -1,11 +0,0 @@
export const RestorePasswordEmail = ({username, url}) => ({
subject: `Account recovery`,
text: `
Hello, ${username}.
We received a request to reset your password.
Follow the link below to proceed:
${url}
If you didn't request this change, ignore this email.
`
});

24
src/model/user.d.ts

@ -2,18 +2,22 @@ import { model } from "./model"
import { UserRolesEnum } from "./user.model"; import { UserRolesEnum } from "./user.model";
export type UserEntity = model & { export type UserEntity = model & {
uuid: string, uuid: string, /** unique id */
token: string, token: string, /** auth token */
tokenExpiry: Date, tokenExpiry: Date, /** expiration date */
email: string, email: string,
emailConfirmed: boolean, emailConfirmed: boolean,
username: string, username: string,
passwd: string, paswd: string, /** password hash */
role: typeof UserRolesEnum, role: UserRolesEnum,
firstIp: string, firstIp: string,
firstLocale: string, locale: string, /** accepted-language */
bannedUntil: Date, bannedUntil: Date, /** null if not banned */
banReason: string, banReason: string, /** can be null when banned */
restoreCode: string, restoreCode: string, /** sent with the password restore email */
restoreExpiry: Date restoreExpiry: Date, /** subtracted on unsuccessful attempt */
createdAt: Date, /** registration date */
meta: UserMeta /** JSON meta information */
}; };
type UserMeta = any;

15
src/model/user.model.js

@ -15,25 +15,34 @@ const UserEntity = ({db}) => (
paswd: Sequelize.TEXT, paswd: Sequelize.TEXT,
role: {type: Sequelize.ENUM(UserRolesEnum), defaultValue: 'user'}, role: {type: Sequelize.ENUM(UserRolesEnum), defaultValue: 'user'},
firstIp: Sequelize.TEXT, firstIp: Sequelize.TEXT,
firstLocale: Sequelize.TEXT, locale: Sequelize.TEXT,
bannedUntil: Sequelize.DATE, bannedUntil: Sequelize.DATE,
banReason: Sequelize.TEXT, banReason: Sequelize.TEXT,
restoreCode: Sequelize.TEXT, restoreCode: Sequelize.TEXT,
restoreExpiry: Sequelize.DATE restoreExpiry: Sequelize.DATE,
createdAt: {type: Sequelize.DATE, defaultValue: Sequelize.NOW},
meta: Sequelize.JSON
}, { }, {
indexes: [ indexes: [
{ fields: ['token'], unique: true }, { fields: ['token'], unique: true },
{ fields: ['email'], unique: true } { fields: ['email'], unique: true }
], ],
hooks: { hooks: {
//todo: consider whether a hook on select would be plausible
beforeUpdate: UserSession.onUpdate, beforeUpdate: UserSession.onUpdate,
afterDestroy: UserSession.onDestroy afterDestroy: UserSession.onDestroy
} }
}) })
); );
/** @readonly @enum {string} */
export const UserRolesEnum = [ export const UserRolesEnum = [
'user', 'admin', 'moderator' // 'guest',
'user',
'moderator',
'admin'
]; ];
export default UserEntity; export default UserEntity;

15
src/route/auth.controller.js

@ -1,4 +1,4 @@
import { notYet, randomElement, reverseString, hours, minutes } from '../utils/common.utils.js'; import { hasDateHappenedYet, randomElement, reverseString, hours, minutes } from '../utils/common.utils.js';
import { checkStringParam, errorOut, ipAddress, localeFromHeader } from '../utils/web.utils.js'; import { checkStringParam, errorOut, ipAddress, localeFromHeader } from '../utils/web.utils.js';
import { Animals } from '../misc/animals.js'; import { Animals } from '../misc/animals.js';
import { reissueToken, generateToken, newTokenExpiry, hashPassword, doesPasswordMatch, isEndpointAllowedForBannedUsers, isEndpointProtected, generateRestoreCode, restoreValidity, restoreAttempts } from '../logic/security.js'; import { reissueToken, generateToken, newTokenExpiry, hashPassword, doesPasswordMatch, isEndpointAllowedForBannedUsers, isEndpointProtected, generateRestoreCode, restoreValidity, restoreAttempts } from '../logic/security.js';
@ -30,8 +30,8 @@ function AuthController({app, db}){
session = new UserSession(userData, request); session = new UserSession(userData, request);
} }
} }
const tokenNotExpired = notYet(session?.user.tokenExpiry); const tokenNotExpired = hasDateHappenedYet(session?.user.tokenExpiry);
const userNotBanned = (!notYet(session?.user.bannedUntil)) || isEndpointAllowedForBannedUsers(request.url); const userNotBanned = (!hasDateHappenedYet(session?.user.bannedUntil)) || isEndpointAllowedForBannedUsers(request.url);
if(session?.user && tokenNotExpired && userNotBanned){ if(session?.user && tokenNotExpired && userNotBanned){
request.session = session; request.session = session;
}else{ }else{
@ -102,7 +102,8 @@ function AuthController({app, db}){
tokenExpiry: newTokenExpiry(), tokenExpiry: newTokenExpiry(),
paswd: hashPassword(reverseString(paswd)), paswd: hashPassword(reverseString(paswd)),
firstIp: ipAddress(request), firstIp: ipAddress(request),
firstLocale: localeFromHeader(request.headers['accept-language']) locale: localeFromHeader(request.headers['accept-language']),
meta: {}
}; };
await Users.create(newUser); await Users.create(newUser);
//todo: email notify //todo: email notify
@ -117,14 +118,14 @@ function AuthController({app, db}){
const user = await Users.findOne({where: {email: fixedEmail}}); const user = await Users.findOne({where: {email: fixedEmail}});
if(!user) return errorOut(reply, 'restore.not_found'); if(!user) return errorOut(reply, 'restore.not_found');
if(user.restoreExpiry && notYet(user.restoreExpiry)){ if(user.restoreExpiry && hasDateHappenedYet(user.restoreExpiry)){
return errorOut(reply, 'restore.already_sent'); return errorOut(reply, 'restore.already_sent');
} }
const restoreCode = generateRestoreCode(); const restoreCode = generateRestoreCode();
const restoreExpiry = new Date(Date.now() + restoreValidity); const restoreExpiry = new Date(Date.now() + restoreValidity);
await user.update({restoreCode, restoreExpiry}); await user.update({restoreCode, restoreExpiry});
const success = await sendRestorationLink(user, app); const success = await sendRestorationLink(user);
return {success, lifetime: restoreValidity}; return {success, lifetime: restoreValidity};
}); });
@ -144,7 +145,7 @@ function AuthController({app, db}){
const user = await Users.findOne({where: {email: fixedEmail}}); const user = await Users.findOne({where: {email: fixedEmail}});
if(!user) return errorOut(reply, 'restore.not_found'); if(!user) return errorOut(reply, 'restore.not_found');
if((!notYet(user.restoreExpiry)) || (!user.restoreCode)){ if((!hasDateHappenedYet(user.restoreExpiry)) || (!user.restoreCode)){
return errorOut(reply, 'restore.expired'); return errorOut(reply, 'restore.expired');
} }

3
src/route/user.controller.js

@ -1,4 +1,5 @@
import { reissueToken, tokenLifetime, tokenReissue } from '../logic/security.js'; import { reissueToken, tokenLifetime, tokenReissue } from '../logic/security.js';
import { hasDateHappenedYet } from '../utils/common.utils.js';
/** @param {import('./route').props} */ /** @param {import('./route').props} */
@ -11,7 +12,7 @@ function UserController({app, db}){
let newToken; let newToken;
{ // issue a new token if near expired { // issue a new token if near expired
const needsReissue = ((user.tokenExpiry - new Date()) < (tokenLifetime - tokenReissue)); const needsReissue = hasDateHappenedYet(user.tokenExpiry - (tokenLifetime - tokenReissue));
if(needsReissue) newToken = await reissueToken(user); if(needsReissue) newToken = await reissueToken(user);
} }

2
src/utils/common.utils.js

@ -11,7 +11,7 @@ export const minutes = 60*1000;
export const hours = 3600*1000; export const hours = 3600*1000;
export const days = 24*hours; export const days = 24*hours;
export function notYet(date){ export function hasDateHappenedYet(date){
if(!date) return false; if(!date) return false;
return (new Date() <= date); return (new Date() <= date);
} }

57
src/utils/localization.utils.js

@ -0,0 +1,57 @@
import fs from 'fs';
/**
* Plain-text data with auto-refresh.
*/
export class TemplateFile{
constructor(path, interval, opts){
this.path = path;
this.opts = opts;
this.reload();
if(interval){
setInterval(() => this.reload(), interval);
}
}
reload(){
fs.stat(this.path, (err, stats) => {
if(err) throw err;
if((!this.lastModified) || (stats.mtime > this.lastModified)){
this.lastModified = stats.mtime;
fs.readFile(this.path, {encoding: 'utf-8'}, (err, data) => {
if(err) throw err;
this.text = data;
this.onChange();
});
}
});
}
onChange(){ // "virtual"
}
generate(variables, srcText){
return (srcText || this.text).replace(/\${(.*?)}/g, (full, name) => {
const res = variables[name] ?? full;
return this.opts?.html ? res.replace("\n","<br>\n") : res;
});
}
}
/**
* JSON internationalization table.
*/
export class TranslationFile extends TemplateFile{
onChange(){
this.data = JSON.parse(this.text);
}
get(key, locale, variables){
let lang;
for(lang in this.data){
if(locale.substring(0, lang.length) == lang){
break;
}
}
const entry = this.data[lang]?.[key];
return variables ? this.generate(variables, entry) : entry;
}
}

10
src/utils/module.utils.js

@ -2,18 +2,20 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url); const srcRootDirectory = path.dirname(fileURLToPath(import.meta.url))+'/..';
const __dirname = path.dirname(__filename);
function defaultFileFilter(filename){ function defaultFileFilter(filename){
return !!filename.match(/\.js$/); return !!filename.match(/\.js$/);
} }
/**
* Import and invoke all modules in a subfolder
*/
export async function importAll(folder, opts){ export async function importAll(folder, opts){
const controllerPaths = ( const controllerPaths = (
fs.readdirSync(path.join(__dirname, '../'+folder)) fs.readdirSync(path.join(srcRootDirectory, folder))
.filter(defaultFileFilter) .filter(defaultFileFilter)
.map(file => path.join(__dirname, '../'+folder, file)) .map(file => path.join(srcRootDirectory, folder, file))
); );
for(const path of controllerPaths){ for(const path of controllerPaths){
const submodule = await import('file://'+path); const submodule = await import('file://'+path);

2
src/utils/web.utils.js

@ -19,5 +19,5 @@ export function ipAddress(request){
export function localeFromHeader(input){ export function localeFromHeader(input){
//not sure what to do with this yet. //not sure what to do with this yet.
//https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language //https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language
return checkStringParam(input, 1, 64) ? input : 'en'; return checkStringParam(input, 2, 64) ? input : 'en';
} }

Loading…
Cancel
Save