mirror of
https://github.com/PretendoNetwork/account.git
synced 2026-09-30 22:28:00 -05:00
Merge pull request #51 from PretendoNetwork/feature-better-config
[Feature] Better config
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -63,4 +63,5 @@ t.js
|
||||
p.js
|
||||
config.json
|
||||
certs
|
||||
/cdn
|
||||
/cdn
|
||||
dump.rdb
|
||||
@@ -24,15 +24,18 @@
|
||||
},
|
||||
"from": "Company Name <user@company.net>"
|
||||
},
|
||||
"aws": {
|
||||
"spaces": {
|
||||
"key": "ACCESS_KEY",
|
||||
"secret": "ACCESS_SECRET"
|
||||
}
|
||||
"s3": {
|
||||
"endpoint": "nyc3.digitaloceanspaces.com",
|
||||
"key": "ACCESS_KEY",
|
||||
"secret": "ACCESS_SECRET"
|
||||
},
|
||||
"hcaptcha": {
|
||||
"secret": "0x0000000000000000000000000000000000000000"
|
||||
},
|
||||
"cdn_base": "https://example.com",
|
||||
"cdn": {
|
||||
"base_url": "https://local-cdn.example.com",
|
||||
"subdomain": "local-cdn",
|
||||
"disk_path": "/home/jon/pretend-cdn"
|
||||
},
|
||||
"website_base": "https://example.com"
|
||||
}
|
||||
21
example.env
Normal file
21
example.env
Normal file
@@ -0,0 +1,21 @@
|
||||
PN_ACT_PREFER_ENV_CONFIG=true # Load config from ENV instead of config.json
|
||||
PN_ACT_CONFIG_HTTP_PORT=7070
|
||||
PN_ACT_CONFIG_MONGO_URI=mongodb://localhost:27017
|
||||
PN_ACT_CONFIG_MONGO_DB_NAME=database_name
|
||||
PN_ACT_CONFIG_MONGOOSE_OPTION_useNewUrlParser=true
|
||||
PN_ACT_CONFIG_MONGOOSE_OPTION_useUnifiedTopology=true
|
||||
PN_ACT_CONFIG_REDIS_URL=redis://localhost:6379
|
||||
PN_ACT_CONFIG_EMAIL_HOST=smtp.gmail.com
|
||||
PN_ACT_CONFIG_EMAIL_PORT=587
|
||||
PN_ACT_CONFIG_EMAIL_SECURE=false
|
||||
PN_ACT_CONFIG_EMAIL_USERNAME=username
|
||||
PN_ACT_CONFIG_EMAIL_PASSWORD=password
|
||||
PN_ACT_CONFIG_EMAIL_FROM=Company Name <user@company.net>
|
||||
PN_ACT_CONFIG_S3_ENDPOINT=nyc3.digitaloceanspaces.com
|
||||
PN_ACT_CONFIG_S3_ACCESS_KEY=ACCESS_KEY
|
||||
PN_ACT_CONFIG_S3_ACCESS_SECRET=ACCESS_SECRET
|
||||
PN_ACT_CONFIG_HCAPTCHA_SECRET=0x0000000000000000000000000000000000000000
|
||||
PN_ACT_CONFIG_CDN_BASE_URL=https://local-cdn.example.com
|
||||
PN_ACT_CONFIG_CDN_SUBDOMAIN=local-cdn
|
||||
PN_ACT_CONFIG_CDN_DISK_PATH=/home/jon/pretend-cdn
|
||||
PN_ACT_CONFIG_WEBSITE_BASE=https://example.com
|
||||
30
package-lock.json
generated
30
package-lock.json
generated
@@ -14,6 +14,7 @@
|
||||
"colors": "^1.4.0",
|
||||
"cors": "^2.8.5",
|
||||
"dicer": "^0.2.5",
|
||||
"dotenv": "^16.0.3",
|
||||
"email-validator": "^2.0.4",
|
||||
"express": "^4.17.1",
|
||||
"express-form-data": "^2.0.17",
|
||||
@@ -26,6 +27,8 @@
|
||||
"image-pixels": "^1.1.1",
|
||||
"joi": "^17.6.1",
|
||||
"kaitai-struct": "^0.9.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.set": "^4.3.2",
|
||||
"mii-js": "github:PretendoNetwork/mii-js",
|
||||
"moment": "^2.24.0",
|
||||
"moment-timezone": "^0.5.27",
|
||||
@@ -1083,6 +1086,14 @@
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
|
||||
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
|
||||
"integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/dtype": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dtype/-/dtype-2.0.0.tgz",
|
||||
@@ -2044,7 +2055,12 @@
|
||||
"node_modules/lodash.get": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
|
||||
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="
|
||||
},
|
||||
"node_modules/lodash.set": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
|
||||
"integrity": "sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg=="
|
||||
},
|
||||
"node_modules/lowercase-keys": {
|
||||
"version": "2.0.0",
|
||||
@@ -4540,6 +4556,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"dotenv": {
|
||||
"version": "16.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
|
||||
"integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ=="
|
||||
},
|
||||
"dtype": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dtype/-/dtype-2.0.0.tgz",
|
||||
@@ -5334,7 +5355,12 @@
|
||||
"lodash.get": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
|
||||
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="
|
||||
},
|
||||
"lodash.set": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
|
||||
"integrity": "sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg=="
|
||||
},
|
||||
"lowercase-keys": {
|
||||
"version": "2.0.0",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"colors": "^1.4.0",
|
||||
"cors": "^2.8.5",
|
||||
"dicer": "^0.2.5",
|
||||
"dotenv": "^16.0.3",
|
||||
"email-validator": "^2.0.4",
|
||||
"express": "^4.17.1",
|
||||
"express-form-data": "^2.0.17",
|
||||
@@ -37,6 +38,8 @@
|
||||
"image-pixels": "^1.1.1",
|
||||
"joi": "^17.6.1",
|
||||
"kaitai-struct": "^0.9.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.set": "^4.3.2",
|
||||
"mii-js": "github:PretendoNetwork/mii-js",
|
||||
"moment": "^2.24.0",
|
||||
"moment-timezone": "^0.5.27",
|
||||
|
||||
74
src/cache.js
74
src/cache.js
@@ -1,30 +1,45 @@
|
||||
const fs = require('fs-extra');
|
||||
const redis = require('redis');
|
||||
const config = require('../config.json');
|
||||
const { config, disabledFeatures } = require('./config-manager');
|
||||
let client;
|
||||
|
||||
const memoryCache = {};
|
||||
|
||||
const SERVICE_CERTS_BASE = `${__dirname}/../certs/service`;
|
||||
const NEX_CERTS_BASE = `${__dirname}/../certs/nex`;
|
||||
const LOCAL_CDN_BASE = `${__dirname}/../cdn`;
|
||||
|
||||
async function connect() {
|
||||
client = redis.createClient(config.redis.client);
|
||||
client.on('error', (err) => console.log('Redis Client Error', err));
|
||||
if (!disabledFeatures.redis) {
|
||||
client = redis.createClient(config.redis.client);
|
||||
client.on('error', (err) => console.log('Redis Client Error', err));
|
||||
|
||||
await client.connect();
|
||||
await client.connect();
|
||||
}
|
||||
}
|
||||
|
||||
async function setCachedFile(type, name, fileName, value) {
|
||||
await client.set(`${type}:${name}:${fileName}`, value);
|
||||
async function setCachedFile(fileName, value) {
|
||||
if (disabledFeatures.redis) {
|
||||
memoryCache[fileName] = value;
|
||||
} else {
|
||||
await client.set(fileName, value);
|
||||
}
|
||||
}
|
||||
|
||||
async function getCachedFile(type, name, fileName, encoding) {
|
||||
const cachedFile = await client.get(`${type}:${name}:${fileName}`);
|
||||
async function getCachedFile(fileName, encoding) {
|
||||
let cachedFile;
|
||||
|
||||
if (disabledFeatures.redis) {
|
||||
cachedFile = memoryCache[fileName];
|
||||
} else {
|
||||
cachedFile = await client.get(fileName);
|
||||
}
|
||||
|
||||
if (cachedFile !== null) {
|
||||
return Buffer.from(cachedFile, encoding);
|
||||
} else {
|
||||
return cachedFile;
|
||||
cachedFile = Buffer.from(cachedFile, encoding);
|
||||
}
|
||||
|
||||
return cachedFile;
|
||||
}
|
||||
|
||||
// NEX server cache functions
|
||||
@@ -76,19 +91,19 @@ async function getNEXAESKey(name, encoding) {
|
||||
}
|
||||
|
||||
async function setNEXPublicKey(name, value) {
|
||||
await setCachedFile('nex', name, 'public_key', value);
|
||||
await setCachedFile(`nex:${name}:public_key`, value);
|
||||
}
|
||||
|
||||
async function setNEXPrivateKey(name, value) {
|
||||
await setCachedFile('nex', name, 'private_key', value);
|
||||
await setCachedFile(`nex:${name}:private_key`, value);
|
||||
}
|
||||
|
||||
async function setNEXSecretKey(name, value) {
|
||||
await setCachedFile('nex', name, 'secret_key', value);
|
||||
await setCachedFile(`nex:${name}:secret_key`, value);
|
||||
}
|
||||
|
||||
async function setNEXAESKey(name, value) {
|
||||
await setCachedFile('nex', name, 'aes_key', value);
|
||||
await setCachedFile(`nex:${name}:aes_key`, value);
|
||||
}
|
||||
|
||||
// 3rd party service cache functions
|
||||
@@ -140,19 +155,38 @@ async function getServiceAESKey(name, encoding) {
|
||||
}
|
||||
|
||||
async function setServicePublicKey(name, value) {
|
||||
await setCachedFile('service', name, 'public_key', value);
|
||||
await setCachedFile(`service:${name}:public_key`, value);
|
||||
}
|
||||
|
||||
async function setServicePrivateKey(name, value) {
|
||||
await setCachedFile('service', name, 'private_key', value);
|
||||
await setCachedFile(`service:${name}:private_key`, value);
|
||||
}
|
||||
|
||||
async function setServiceSecretKey(name, value) {
|
||||
await setCachedFile('service', name, 'secret_key', value);
|
||||
await setCachedFile(`service:${name}:secret_key`, value);
|
||||
}
|
||||
|
||||
async function setServiceAESKey(name, value) {
|
||||
await setCachedFile('service', name, 'aes_key', value);
|
||||
await setCachedFile(`service:${name}:aes_key`, value);
|
||||
}
|
||||
|
||||
// Local CDN cache functions
|
||||
|
||||
async function getLocalCDNFile(name, encoding) {
|
||||
let file = await getCachedFile(`local_cdn:${name}`, encoding);
|
||||
|
||||
if (file === null) {
|
||||
if (await fs.pathExists(`${LOCAL_CDN_BASE}/${name}`)) {
|
||||
file = await fs.readFile(`${LOCAL_CDN_BASE}/${name}`, { encoding });
|
||||
await setLocalCDNFile(name, file);
|
||||
}
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
async function setLocalCDNFile(name, value) {
|
||||
await setCachedFile(`local_cdn:${name}`, value);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
@@ -173,4 +207,6 @@ module.exports = {
|
||||
setServicePrivateKey,
|
||||
setServiceSecretKey,
|
||||
setServiceAESKey,
|
||||
getLocalCDNFile,
|
||||
setLocalCDNFile
|
||||
};
|
||||
339
src/config-manager.js
Normal file
339
src/config-manager.js
Normal file
@@ -0,0 +1,339 @@
|
||||
const fs = require('fs-extra');
|
||||
const get = require('lodash.get');
|
||||
const set = require('lodash.set');
|
||||
const logger = require('../logger');
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
/**
|
||||
* @typedef {Object} Config
|
||||
* @property {object} http HTTP server settings
|
||||
* @property {number} http.port HTTP port the server will listen on
|
||||
* @property {object} mongoose Mongose connection settings
|
||||
* @property {string} mongoose.uri URI Mongoose will connect to
|
||||
* @property {string} mongoose.database MongoDB database name
|
||||
* @property {object} mongoose.options MongoDB connection options
|
||||
* @property {object} [redis] redis settings
|
||||
* @property {string} [redis.client] redis client settings
|
||||
* @property {string} [redis.client.url] redis server URL
|
||||
* @property {object} [email] node-mailer client settings
|
||||
* @property {string} [email.host] SMTP server address
|
||||
* @property {number} [email.port] SMTP server port
|
||||
* @property {boolean} [email.secure] Secure SMTP
|
||||
* @property {string} [email.from] Email 'from' name/address
|
||||
* @property {object} [email.auth] Email authentication settings
|
||||
* @property {string} [email.auth.user] Email username
|
||||
* @property {string} [email.auth.pass] Email password
|
||||
* @property {object} [s3] s3 client settings
|
||||
* @property {object} [s3.endpoint] s3 endpoint URL
|
||||
* @property {string} [s3.key] s3 access key
|
||||
* @property {string} [s3.secret] s3 access secret
|
||||
* @property {object} [hcaptcha] hCaptcha settings
|
||||
* @property {string} [hcaptcha.secret] hCaptcha secret
|
||||
* @property {object} cdn CDN config settings
|
||||
* @property {object} [cdn.subdomain] Subdomain used for serving CDN contents when s3 is disabled
|
||||
* @property {string} [cdn.disk_path] Fully qualified file system path for storing and reading local CDN contents
|
||||
* @property {string} cdn.base_url Base URL for CDN server
|
||||
* @property {string} website_base Base URL for service website (used with emails)
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {Config}
|
||||
*/
|
||||
let config = {};
|
||||
|
||||
|
||||
/**
|
||||
* @typedef {Object} DisabledFeatures
|
||||
* @property {boolean} redis true if redis is disabled
|
||||
* @property {boolean} email true if email sending is disabled
|
||||
* @property {boolean} captcha true if captcha verification is disabled
|
||||
* @property {boolean} s3 true if s3 services is disabled
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {DisabledFeatures}
|
||||
*/
|
||||
const disabledFeatures = {
|
||||
redis: false,
|
||||
email: false,
|
||||
captcha: false,
|
||||
s3: false
|
||||
};
|
||||
|
||||
const requiredFields = [
|
||||
['http.port', 'PN_ACT_CONFIG_HTTP_PORT', Number],
|
||||
['mongoose.uri', 'PN_ACT_CONFIG_MONGO_URI'],
|
||||
['mongoose.database', 'PN_ACT_CONFIG_MONGO_DB_NAME'],
|
||||
['cdn.base_url', 'PN_ACT_CONFIG_CDN_BASE_URL']
|
||||
];
|
||||
|
||||
function configure() {
|
||||
const usingEnv = process.env.PN_ACT_PREFER_ENV_CONFIG === 'true';
|
||||
|
||||
if (usingEnv) {
|
||||
logger.info('Loading config from environment variable');
|
||||
|
||||
config = {
|
||||
http: {
|
||||
port: Number(process.env.PN_ACT_CONFIG_HTTP_PORT)
|
||||
},
|
||||
mongoose: {
|
||||
uri: process.env.PN_ACT_CONFIG_MONGO_URI,
|
||||
database: process.env.PN_ACT_CONFIG_MONGO_DB_NAME,
|
||||
options: Object.keys(process.env)
|
||||
.filter(key => key.startsWith('PN_ACT_CONFIG_MONGOOSE_OPTION_'))
|
||||
.reduce((obj, key) => {
|
||||
obj[key.split('_').pop()] = process.env[key];
|
||||
return obj;
|
||||
}, {})
|
||||
},
|
||||
redis: {
|
||||
client: {
|
||||
url: process.env.PN_ACT_CONFIG_REDIS_URL
|
||||
}
|
||||
},
|
||||
email: {
|
||||
host: process.env.PN_ACT_CONFIG_EMAIL_HOST,
|
||||
port: Number(process.env.PN_ACT_CONFIG_EMAIL_PORT),
|
||||
secure: Boolean(process.env.PN_ACT_CONFIG_EMAIL_SECURE),
|
||||
auth: {
|
||||
user: process.env.PN_ACT_CONFIG_EMAIL_USERNAME,
|
||||
pass: process.env.PN_ACT_CONFIG_EMAIL_PASSWORD
|
||||
},
|
||||
from: process.env.PN_ACT_CONFIG_EMAIL_FROM
|
||||
},
|
||||
s3: {
|
||||
endpoint: process.env.PN_ACT_CONFIG_S3_ENDPOINT,
|
||||
key: process.env.PN_ACT_CONFIG_S3_ACCESS_KEY,
|
||||
secret: process.env.PN_ACT_CONFIG_S3_ACCESS_SECRET
|
||||
},
|
||||
hcaptcha: {
|
||||
secret: process.env.PN_ACT_CONFIG_HCAPTCHA_SECRET
|
||||
},
|
||||
cdn: {
|
||||
subdomain: process.env.PN_ACT_CONFIG_CDN_BASE,
|
||||
disk_path: process.env.PN_ACT_CONFIG_CDN_BASE,
|
||||
base_url: process.env.PN_ACT_CONFIG_CDN_BASE
|
||||
},
|
||||
website_base: process.env.PN_ACT_CONFIG_WEBSITE_BASE
|
||||
};
|
||||
} else {
|
||||
logger.info('Loading config from config.json');
|
||||
|
||||
if (!fs.pathExistsSync(`${__dirname}/../config.json`)) {
|
||||
logger.error('Failed to locate config.json file');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
config = require(`${__dirname}/../config.json`);
|
||||
}
|
||||
|
||||
logger.info('Config loaded, checking integrity');
|
||||
|
||||
// * Check for required settings
|
||||
for (const requiredField of requiredFields) {
|
||||
const [keyPath, env, convertType] = requiredField;
|
||||
|
||||
const configValue = get(config, keyPath);
|
||||
const envValue = get(process.env, keyPath);
|
||||
|
||||
if (!configValue || (typeof configValue === 'string' && configValue.trim() === '')) {
|
||||
if (!envValue || envValue.trim() === '') {
|
||||
logger.error(`Failed to locate required field ${keyPath}. Set ${keyPath} in config.json or the ${env} environment variable`);
|
||||
|
||||
process.exit(0);
|
||||
} else {
|
||||
logger.info(`${keyPath} not found in config, using environment variable ${env}`);
|
||||
|
||||
const newValue = envValue;
|
||||
|
||||
set(config, keyPath, convertType ? convertType(newValue) : newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// * Check for optional settings
|
||||
|
||||
const redisCheck = get(config, 'redis.client.url');
|
||||
|
||||
if (!redisCheck || redisCheck.trim() === '') {
|
||||
if (usingEnv) {
|
||||
logger.warn('Failed to find Redis connection url. Disabling feature and using in-memory cache. To enable feature set the PN_ACT_CONFIG_REDIS_URL environment variable');
|
||||
|
||||
} else {
|
||||
logger.warn('Failed to find Redis connection url. Disabling feature and using in-memory cache. To enable feature set redis.client.url in your config.json');
|
||||
|
||||
}
|
||||
|
||||
disabledFeatures.redis = true;
|
||||
}
|
||||
|
||||
const emailHostCheck = get(config, 'email.host');
|
||||
|
||||
if (!emailHostCheck || emailHostCheck.trim() === '') {
|
||||
if (usingEnv) {
|
||||
logger.warn('Failed to find email SMTP host. Disabling feature. To enable feature set the PN_ACT_CONFIG_EMAIL_HOST environment variable');
|
||||
} else {
|
||||
logger.warn('Failed to find email SMTP host. Disabling feature. To enable feature set email.host in your config.json');
|
||||
}
|
||||
|
||||
|
||||
disabledFeatures.email = true;
|
||||
}
|
||||
|
||||
const emailPortCheck = get(config, 'email.port');
|
||||
|
||||
if (!emailPortCheck) {
|
||||
if (usingEnv) {
|
||||
logger.warn('Failed to find email SMTP port. Disabling feature. To enable feature set the PN_ACT_CONFIG_EMAIL_PORT environment variable');
|
||||
} else {
|
||||
logger.warn('Failed to find email SMTP port. Disabling feature. To enable feature set email.port in your config.json');
|
||||
}
|
||||
|
||||
disabledFeatures.email = true;
|
||||
}
|
||||
|
||||
const emailSecureCheck = get(config, 'email.secure');
|
||||
|
||||
if (emailSecureCheck === undefined) {
|
||||
if (usingEnv) {
|
||||
logger.warn('Failed to find email SMTP secure flag. Disabling feature. To enable feature set the PN_ACT_CONFIG_EMAIL_SECURE environment variable');
|
||||
} else {
|
||||
|
||||
logger.warn('Failed to find email SMTP secure flag. Disabling feature. To enable feature set email.secure in your config.json');
|
||||
}
|
||||
|
||||
disabledFeatures.email = true;
|
||||
}
|
||||
|
||||
const emailUsernameCheck = get(config, 'email.auth.user');
|
||||
|
||||
if (!emailUsernameCheck || emailUsernameCheck.trim() === '') {
|
||||
if (usingEnv) {
|
||||
logger.warn('Failed to find email account username. Disabling feature. To enable feature set the auth.user environment variable');
|
||||
} else {
|
||||
|
||||
logger.warn('Failed to find email account username. Disabling feature. To enable feature set email.auth.user in your config.json');
|
||||
}
|
||||
|
||||
disabledFeatures.email = true;
|
||||
}
|
||||
|
||||
const emailPasswordCheck = get(config, 'email.auth.pass');
|
||||
|
||||
if (!emailPasswordCheck || emailPasswordCheck.trim() === '') {
|
||||
if (usingEnv) {
|
||||
logger.warn('Failed to find email account password. Disabling feature. To enable feature set the PN_ACT_CONFIG_EMAIL_PASSWORD environment variable');
|
||||
} else {
|
||||
|
||||
logger.warn('Failed to find email account password. Disabling feature. To enable feature set email.auth.pass in your config.json');
|
||||
}
|
||||
|
||||
disabledFeatures.email = true;
|
||||
}
|
||||
|
||||
const emailFromCheck = get(config, 'email.from');
|
||||
|
||||
if (!emailFromCheck || emailFromCheck.trim() === '') {
|
||||
if (usingEnv) {
|
||||
logger.warn('Failed to find email from config. Disabling feature. To enable feature set the PN_ACT_CONFIG_EMAIL_FROM environment variable');
|
||||
} else {
|
||||
|
||||
logger.warn('Failed to find email from config. Disabling feature. To enable feature set email.from in your config.json');
|
||||
}
|
||||
|
||||
disabledFeatures.email = true;
|
||||
}
|
||||
|
||||
if (!disabledFeatures.email) {
|
||||
const websiteBaseCheck = get(config, 'website_base');
|
||||
|
||||
if (!websiteBaseCheck || websiteBaseCheck.trim() === '') {
|
||||
if (usingEnv) {
|
||||
logger.error('Email sending is enabled and no website base was configured. Set the PN_ACT_CONFIG_WEBSITE_BASE environment variable');
|
||||
} else {
|
||||
logger.error('Email sending is enabled and no website base was configured. Set website_base in your config.json');
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
const captchaSecretCheck = get(config, 'hcaptcha.secret');
|
||||
|
||||
if (!captchaSecretCheck || captchaSecretCheck.trim() === '') {
|
||||
if (usingEnv) {
|
||||
logger.warn('Failed to find captcha secret config. Disabling feature. To enable feature set the PN_ACT_CONFIG_HCAPTCHA_SECRET environment variable');
|
||||
} else {
|
||||
logger.warn('Failed to find captcha secret config. Disabling feature. To enable feature set hcaptcha.secret in your config.json');
|
||||
}
|
||||
|
||||
disabledFeatures.captcha = true;
|
||||
}
|
||||
|
||||
const s3EndpointCheck = get(config, 's3.endpoint');
|
||||
|
||||
if (!s3EndpointCheck || s3EndpointCheck.trim() === '') {
|
||||
if (usingEnv) {
|
||||
logger.warn('Failed to find s3 endpoint config. Disabling feature. To enable feature set the PN_ACT_CONFIG_S3_ENDPOINT environment variable');
|
||||
} else {
|
||||
logger.warn('Failed to find s3 endpoint config. Disabling feature. To enable feature set s3.endpoint in your config.json');
|
||||
}
|
||||
|
||||
disabledFeatures.s3 = true;
|
||||
} else {
|
||||
}
|
||||
|
||||
const s3AccessKeyCheck = get(config, 's3.key');
|
||||
|
||||
if (!s3AccessKeyCheck || s3AccessKeyCheck.trim() === '') {
|
||||
if (usingEnv) {
|
||||
logger.warn('Failed to find s3 access key config. Disabling feature. To enable feature set the PN_ACT_CONFIG_S3_ACCESS_KEY environment variable');
|
||||
} else {
|
||||
logger.warn('Failed to find s3 access key config. Disabling feature. To enable feature set s3.key in your config.json');
|
||||
}
|
||||
|
||||
disabledFeatures.s3 = true;
|
||||
}
|
||||
|
||||
const s3SecretKeyCheck = get(config, 's3.secret');
|
||||
|
||||
if (!s3SecretKeyCheck || s3SecretKeyCheck.trim() === '') {
|
||||
if (usingEnv) {
|
||||
logger.warn('Failed to find s3 secret key config. Disabling feature. To enable feature set the PN_ACT_CONFIG_S3_ACCESS_SECRET environment variable');
|
||||
} else {
|
||||
logger.warn('Failed to find s3 secret key config. Disabling feature. To enable feature set s3.secret in your config.json');
|
||||
}
|
||||
|
||||
disabledFeatures.s3 = true;
|
||||
}
|
||||
|
||||
if (disabledFeatures.s3) {
|
||||
const cdnSubdomainCheck = get(config, 'cdn.subdomain');
|
||||
|
||||
if (!cdnSubdomainCheck || cdnSubdomainCheck.trim() === '') {
|
||||
if (usingEnv) {
|
||||
logger.error('s3 file storage is disabled and no CDN subdomain was set. Set the PN_ACT_CONFIG_CDN_SUBDOMAIN environment variable');
|
||||
} else {
|
||||
logger.error('s3 file storage is disabled and no CDN subdomain was set. Set cdn.subdomain in your config.json');
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (disabledFeatures.redis) {
|
||||
logger.warn('Both s3 and Redis are disabled. Large CDN files will use the in-memory cache, which may result in high memory use. Please enable s3 if you\'re running a production server.');
|
||||
}
|
||||
|
||||
logger.warn(`s3 file storage disabled. Using disk-based file storage. Please ensure cdn.base_url config or PN_ACT_CONFIG_CDN_BASE env variable is set to point to this server with the subdomain being ${config.cdn.subdomain}`);
|
||||
}
|
||||
|
||||
module.exports.config = config;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
configure,
|
||||
config,
|
||||
disabledFeatures
|
||||
};
|
||||
@@ -5,7 +5,7 @@ const util = require('./util');
|
||||
const { PNID } = require('./models/pnid');
|
||||
const { Server } = require('./models/server');
|
||||
const logger = require('../logger');
|
||||
const config = require('../config.json');
|
||||
const { config } = require('./config-manager');
|
||||
const { uri, database, options } = config.mongoose;
|
||||
|
||||
// TODO: Extend this later with more settings
|
||||
@@ -172,9 +172,9 @@ async function getUserProfileJSONByPID(pid) {
|
||||
mii_image: {
|
||||
// Images MUST be loaded over HTTPS or console ignores them
|
||||
// Bunny CDN is the only CDN which seems to support TLS 1.0/1.1 (required)
|
||||
cached_url: `${config.cdn_base}/mii/${user.pid}/standard.tga`,
|
||||
cached_url: `${config.cdn.base_url}/mii/${user.pid}/standard.tga`,
|
||||
id: user.get('mii.image_id'),
|
||||
url: `${config.cdn_base}/mii/${user.pid}/standard.tga`,
|
||||
url: `${config.cdn.base_url}/mii/${user.pid}/standard.tga`,
|
||||
type: 'standard'
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
const nodemailer = require('nodemailer');
|
||||
const config = require('../config.json');
|
||||
const { config, disabledFeatures } = require('./config-manager');
|
||||
|
||||
const transporter = nodemailer.createTransport(config.email);
|
||||
let transporter;
|
||||
|
||||
if (!disabledFeatures.email) {
|
||||
transporter = nodemailer.createTransport(config.email);
|
||||
}
|
||||
|
||||
async function sendMail(options) {
|
||||
options.from = config.email.from;
|
||||
if (!disabledFeatures.email) {
|
||||
options.from = config.email.from;
|
||||
|
||||
await transporter.sendMail(options);
|
||||
await transporter.sendMail(options);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
process.title = 'Pretendo - Account';
|
||||
|
||||
const configManager = require('./config-manager');
|
||||
|
||||
configManager.configure();
|
||||
|
||||
const express = require('express');
|
||||
const morgan = require('morgan');
|
||||
const xmlparser = require('./middleware/xml-parser');
|
||||
@@ -7,7 +11,8 @@ const cache = require('./cache');
|
||||
const database = require('./database');
|
||||
const util = require('./util');
|
||||
const logger = require('../logger');
|
||||
const config = require('../config.json');
|
||||
|
||||
const { config } = configManager;
|
||||
|
||||
const { http: { port } } = config;
|
||||
const app = express();
|
||||
@@ -17,10 +22,9 @@ const nnid = require('./services/nnid');
|
||||
const nasc = require('./services/nasc');
|
||||
const datastore = require('./services/datastore');
|
||||
const api = require('./services/api');
|
||||
const localcdn = require('./services/local-cdn');
|
||||
|
||||
// START APPLICATION
|
||||
app.set('etag', false);
|
||||
app.disable('x-powered-by');
|
||||
|
||||
// Create router
|
||||
logger.info('Setting up Middleware');
|
||||
@@ -37,6 +41,7 @@ app.use(nnid);
|
||||
app.use(nasc);
|
||||
app.use(datastore);
|
||||
app.use(api);
|
||||
app.use(localcdn);
|
||||
|
||||
// 404 handler
|
||||
logger.info('Creating 404 status handler');
|
||||
|
||||
@@ -12,7 +12,7 @@ const database = require('../../../../database');
|
||||
const cache = require('../../../../cache');
|
||||
const util = require('../../../../util');
|
||||
const logger = require('../../../../../logger');
|
||||
const config = require('../../../../../config.json');
|
||||
const { config, disabledFeatures } = require('../../../../config-manager');
|
||||
|
||||
const PNID_VALID_CHARACTERS_REGEX = /^[\w\-\.]*$/gm;
|
||||
const PNID_PUNCTUATION_START_REGEX = /^[\_\-\.]/gm;
|
||||
@@ -42,22 +42,24 @@ router.post('/', async (request, response) => {
|
||||
const passwordConfirm = body.password_confirm?.trim();
|
||||
const hCaptchaResponse = body.hCaptchaResponse?.trim();
|
||||
|
||||
if (!hCaptchaResponse || hCaptchaResponse === '') {
|
||||
return response.status(400).json({
|
||||
app: 'api',
|
||||
status: 400,
|
||||
error: 'Must fill in captcha'
|
||||
});
|
||||
}
|
||||
if (!disabledFeatures.captcha) {
|
||||
if (!hCaptchaResponse || hCaptchaResponse === '') {
|
||||
return response.status(400).json({
|
||||
app: 'api',
|
||||
status: 400,
|
||||
error: 'Must fill in captcha'
|
||||
});
|
||||
}
|
||||
|
||||
const captchaVerify = await hcaptcha.verify(config.hcaptcha.secret, hCaptchaResponse);
|
||||
const captchaVerify = await hcaptcha.verify(config.hcaptcha.secret, hCaptchaResponse);
|
||||
|
||||
if (!captchaVerify.success) {
|
||||
return response.status(400).json({
|
||||
app: 'api',
|
||||
status: 400,
|
||||
error: 'Captcha verification failed'
|
||||
});
|
||||
if (!captchaVerify.success) {
|
||||
return response.status(400).json({
|
||||
app: 'api',
|
||||
status: 400,
|
||||
error: 'Captcha verification failed'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!email || email === '') {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const router = require('express').Router();
|
||||
const joi = require('joi');
|
||||
const { PNID } = require('../../../../models/pnid');
|
||||
const config = require('../../../../../config.json');
|
||||
const { config } = require('../../../../config-manager');
|
||||
|
||||
// TODO: Extend this later with more settings
|
||||
const userSchema = joi.object({
|
||||
@@ -47,7 +47,7 @@ router.get('/', async (request, response) => {
|
||||
mii: {
|
||||
data: pnid.get('mii.data'),
|
||||
name: pnid.get('mii.name'),
|
||||
image_url: `${config.cdn_base}/mii/${pnid.get('pid')}/normal_face.png`
|
||||
image_url: `${config.cdn.base_url}/mii/${pnid.get('pid')}/normal_face.png`
|
||||
},
|
||||
flags: {
|
||||
marketing: pnid.get('flags.marketing')
|
||||
@@ -111,7 +111,7 @@ router.post('/', async (request, response) => {
|
||||
mii: {
|
||||
data: pnid.get('mii.data'),
|
||||
name: pnid.get('mii.name'),
|
||||
image_url: `${config.cdn_base}/mii/${pnid.get('pid')}/normal_face.png`
|
||||
image_url: `${config.cdn.base_url}/mii/${pnid.get('pid')}/normal_face.png`
|
||||
},
|
||||
flags: {
|
||||
marketing: pnid.get('flags.marketing')
|
||||
|
||||
30
src/services/local-cdn/index.js
Normal file
30
src/services/local-cdn/index.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const express = require('express');
|
||||
const subdomain = require('express-subdomain');
|
||||
const logger = require('../../../logger');
|
||||
const { config, disabledFeatures } = require('../../config-manager');
|
||||
|
||||
if (!disabledFeatures.s3) {
|
||||
// * s3 enabled, no need for this
|
||||
|
||||
module.exports = express.Router();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const routes = require('./routes');
|
||||
|
||||
// Router to handle the subdomain
|
||||
const localcdn = express.Router();
|
||||
|
||||
// Setup routes
|
||||
logger.info('[LOCAL-CDN] Applying imported routes');
|
||||
localcdn.use(routes.GET);
|
||||
|
||||
// Main router for endpoints
|
||||
const router = express.Router();
|
||||
|
||||
// Create subdomains
|
||||
logger.info(`[LOCAL-CDN] Creating '${config.cdn.subdomain}' subdomain`);
|
||||
router.use(subdomain(config.cdn.subdomain, localcdn));
|
||||
|
||||
module.exports = router;
|
||||
16
src/services/local-cdn/routes/get.js
Normal file
16
src/services/local-cdn/routes/get.js
Normal file
@@ -0,0 +1,16 @@
|
||||
const router = require('express').Router();
|
||||
const cache = require('../../../cache');
|
||||
|
||||
router.get('/*', async (request, response) => {
|
||||
const filePath = request.params[0];
|
||||
|
||||
const file = await cache.getLocalCDNFile(filePath);
|
||||
|
||||
if (file) {
|
||||
response.send(file);
|
||||
} else {
|
||||
response.sendStatus(404);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
3
src/services/local-cdn/routes/index.js
Normal file
3
src/services/local-cdn/routes/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
GET: require('./get.js'),
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
const router = require('express').Router();
|
||||
const xmlbuilder = require('xmlbuilder');
|
||||
const { PNID } = require('../../../models/pnid');
|
||||
const config = require('../../../../config.json');
|
||||
const { config } = require('../../../config-manager');
|
||||
|
||||
/**
|
||||
* [GET]
|
||||
@@ -20,51 +20,51 @@ router.get('/', async (request, response) => {
|
||||
|
||||
const miiImages = [
|
||||
{
|
||||
cached_url: `${config.cdn_base}/mii/${user.pid}/normal_face.png`,
|
||||
cached_url: `${config.cdn.base_url}/mii/${user.pid}/normal_face.png`,
|
||||
id: mii.id,
|
||||
url: `${config.cdn_base}/mii/${user.pid}/normal_face.png`,
|
||||
url: `${config.cdn.base_url}/mii/${user.pid}/normal_face.png`,
|
||||
type: 'standard'
|
||||
},
|
||||
{
|
||||
cached_url: `${config.cdn_base}/mii/${user.pid}/frustrated.png`,
|
||||
cached_url: `${config.cdn.base_url}/mii/${user.pid}/frustrated.png`,
|
||||
id: mii.id,
|
||||
url: `${config.cdn_base}/mii/${user.pid}/frustrated.png`,
|
||||
url: `${config.cdn.base_url}/mii/${user.pid}/frustrated.png`,
|
||||
type: 'frustrated_face'
|
||||
},
|
||||
{
|
||||
cached_url: `${config.cdn_base}/mii/${user.pid}/smile_open_mouth.png`,
|
||||
cached_url: `${config.cdn.base_url}/mii/${user.pid}/smile_open_mouth.png`,
|
||||
id: mii.id,
|
||||
url: `${config.cdn_base}/mii/${user.pid}/smile_open_mouth.png`,
|
||||
url: `${config.cdn.base_url}/mii/${user.pid}/smile_open_mouth.png`,
|
||||
type: 'happy_face'
|
||||
},
|
||||
{
|
||||
cached_url: `${config.cdn_base}/mii/${user.pid}/wink_left.png`,
|
||||
cached_url: `${config.cdn.base_url}/mii/${user.pid}/wink_left.png`,
|
||||
id: mii.id,
|
||||
url: `${config.cdn_base}/mii/${user.pid}/wink_left.png`,
|
||||
url: `${config.cdn.base_url}/mii/${user.pid}/wink_left.png`,
|
||||
type: 'like_face'
|
||||
},
|
||||
{
|
||||
cached_url: `${config.cdn_base}/mii/${user.pid}/normal_face.png`,
|
||||
cached_url: `${config.cdn.base_url}/mii/${user.pid}/normal_face.png`,
|
||||
id: mii.id,
|
||||
url: `${config.cdn_base}/mii/${user.pid}/normal_face.png`,
|
||||
url: `${config.cdn.base_url}/mii/${user.pid}/normal_face.png`,
|
||||
type: 'normal_face'
|
||||
},
|
||||
{
|
||||
cached_url: `${config.cdn_base}/mii/${user.pid}/sorrow.png`,
|
||||
cached_url: `${config.cdn.base_url}/mii/${user.pid}/sorrow.png`,
|
||||
id: mii.id,
|
||||
url: `${config.cdn_base}/mii/${user.pid}/sorrow.png`,
|
||||
url: `${config.cdn.base_url}/mii/${user.pid}/sorrow.png`,
|
||||
type: 'puzzled_face'
|
||||
},
|
||||
{
|
||||
cached_url: `${config.cdn_base}/mii/${user.pid}/surprised_open_mouth.png`,
|
||||
cached_url: `${config.cdn.base_url}/mii/${user.pid}/surprised_open_mouth.png`,
|
||||
id: mii.id,
|
||||
url: `${config.cdn_base}/mii/${user.pid}/surprised_open_mouth.png`,
|
||||
url: `${config.cdn.base_url}/mii/${user.pid}/surprised_open_mouth.png`,
|
||||
type: 'surprised_face'
|
||||
},
|
||||
{
|
||||
cached_url: `${config.cdn_base}/mii/${user.pid}/body.png`,
|
||||
cached_url: `${config.cdn.base_url}/mii/${user.pid}/body.png`,
|
||||
id: mii.id,
|
||||
url: `${config.cdn_base}/mii/${user.pid}/body.png`,
|
||||
url: `${config.cdn.base_url}/mii/${user.pid}/body.png`,
|
||||
type: 'whole_body'
|
||||
}
|
||||
];
|
||||
|
||||
49
src/util.js
49
src/util.js
@@ -1,16 +1,21 @@
|
||||
const crypto = require('crypto');
|
||||
const path = require('path');
|
||||
const NodeRSA = require('node-rsa');
|
||||
const aws = require('aws-sdk');
|
||||
const fs = require('fs-extra');
|
||||
const mailer = require('./mailer');
|
||||
const cache = require('./cache');
|
||||
const config = require('../config.json');
|
||||
const { config, disabledFeatures } = require('./config-manager');
|
||||
|
||||
const spacesEndpoint = new aws.Endpoint('nyc3.digitaloceanspaces.com');
|
||||
const s3 = new aws.S3({
|
||||
endpoint: spacesEndpoint,
|
||||
accessKeyId: config.aws.spaces.key,
|
||||
secretAccessKey: config.aws.spaces.secret
|
||||
});
|
||||
let s3;
|
||||
|
||||
if (!disabledFeatures.s3) {
|
||||
s3 = new aws.S3({
|
||||
endpoint: new aws.Endpoint(config.s3.endpoint),
|
||||
accessKeyId: config.s3.key,
|
||||
secretAccessKey: config.s3.secret
|
||||
});
|
||||
}
|
||||
|
||||
function nintendoPasswordHash(password, pid) {
|
||||
const pidBuffer = Buffer.alloc(4);
|
||||
@@ -203,20 +208,32 @@ function unpackToken(token) {
|
||||
function fullUrl(request) {
|
||||
const protocol = request.protocol;
|
||||
const host = request.host;
|
||||
const path = request.originalUrl;
|
||||
const opath = request.originalUrl;
|
||||
|
||||
return `${protocol}://${host}${path}`;
|
||||
return `${protocol}://${host}${opath}`;
|
||||
}
|
||||
|
||||
async function uploadCDNAsset(bucket, key, data, acl) {
|
||||
const awsPutParams = {
|
||||
Body: data,
|
||||
Key: key,
|
||||
Bucket: bucket,
|
||||
ACL: acl
|
||||
};
|
||||
if (disabledFeatures.s3) {
|
||||
await writeLocalCDNFile(key, data);
|
||||
} else {
|
||||
const awsPutParams = {
|
||||
Body: data,
|
||||
Key: key,
|
||||
Bucket: bucket,
|
||||
ACL: acl
|
||||
};
|
||||
|
||||
await s3.putObject(awsPutParams).promise();
|
||||
await s3.putObject(awsPutParams).promise();
|
||||
}
|
||||
}
|
||||
|
||||
async function writeLocalCDNFile(key, data) {
|
||||
const filePath = config.cdn.disk_path;
|
||||
const folder = path.dirname(filePath);
|
||||
|
||||
await fs.ensureDir(folder);
|
||||
await fs.writeFile(filePath, data);
|
||||
}
|
||||
|
||||
function nascError(errorCode) {
|
||||
|
||||
Reference in New Issue
Block a user