Skip to content

Commit

Permalink
feat: add config as params
Browse files Browse the repository at this point in the history
BREAKING_CHANGE: receive config in Service constructor in stead of reading it
from config file.
  • Loading branch information
Trygve Amundsen authored and trygvea committed Jun 5, 2023
1 parent d1e49b8 commit 8e63b4d
Show file tree
Hide file tree
Showing 17 changed files with 199 additions and 222 deletions.
209 changes: 70 additions & 139 deletions lib/config.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,38 @@
import convict from 'convict';
import yaml from 'js-yaml';
import pino from 'pino';
import path, { join } from 'path';
import fs from 'fs';
import os from 'os';
import Sink from '@eik/sink';

/**
* Configuration object
* @typedef {import('@eik/sink')} Sink
* @typedef Config
* @type {object}
* @property {string} name - Name of the application
* @property {('development' | 'production')} env - Applicaton environments
* @property {boolean} metrics - Enable metrics
* @property {object} log - Log configuration
* @property {('trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal')} log.level - Log level to log at
* @property {object} http - Http configuration
* @property {boolean} http.http2 - Enable http2 for the server
* @property {string} http.address - The address the http server should bind to
* @property {number} http.port - The port the http server should bind to
* @property {object} compression - Compression configuration
* @property {boolean} compression.global - Enable global compression for all http routes
* @property {object} jwt - JWT configuration
* @property {string} jwt.secret - Secret used for JWT signing
* @property {object} basicAuth - Basic auth configuration
* @property {('key' | 'disabled')} basicAuth.type - Type of basic auth to use
* @property {string} basicAuth.key - Key used for basic authorization
* @property {object} organization - Organization configuration
* @property {string} organization.name - Organization name - Used as a folder name in the storage of files
* @property {Array.<string>} organization.hostnames - Hostnames the organization maps to
* @property {object | Sink} sink - Sink configuration
* @property {('fs' | 'mem' | 'test')} sink.type - Type of sink to use
* @property {string} sink.path - Absolute path to store files in when using the "fs" sink
* @property {string} notFoundCacheControl - Cache control header value for 404 responses
* @property {string} aliasCacheControl - Cache control header value for alias responses
*/

const CWD = process.cwd();

Expand All @@ -14,156 +43,58 @@ try {
/* empty */
}

convict.addParser({ extension: ['yml', 'yaml'], parse: yaml.load });
/**
* @param {Config} config
* @returns {Config}
*/
const withDefaults = (config) => ({
name: pack.name,
env: 'development',
metrics: true,
notFoundCacheControl: 'public, max-age=5',
aliasCacheControl: '',

convict.addFormat({
name: 'secret-string',
validate: (value) => {
if (typeof value !== 'string') {
throw new Error('Value must be a String');
}
},
coerce: (value) => {
if (path.isAbsolute(value)) {
try {
const file = fs.readFileSync(value);
return file.toString();
} catch (error) {
throw new Error(`Config could not load secret from path: ${value}`);
}
}
return value;
}
});
...config,

const conf = convict({
name: {
doc: 'Name of the apllication',
default: pack.name,
format: String,
},
env: {
doc: 'Applicaton environments',
format: ['development', 'production'],
default: 'development',
env: 'NODE_ENV',
arg: 'node-env',
},
metrics: {
format: Boolean,
default: true,
env: 'METRICS',
},
log: {
level: {
doc: 'Log level to log at',
format: ['trace', 'debug', 'info', 'warn', 'error', 'fatal'],
default: 'info',
env: 'LOG_LEVEL',
arg: 'log-level',
},
level: 'info',
...config.log,
},
http: {
http2: {
doc: 'Enable http2 for the server',
format: Boolean,
default: false,
env: 'HTTP_HTTP2',
},
address: {
doc: 'The address the http server should bind to',
format: String,
default: 'localhost',
env: 'HTTP_ADDRESS',
},
port: {
doc: 'The port the http server should bind to',
format: 'port',
default: 4001,
env: 'HTTP_PORT',
},
http2: false,
address: 'localhost',
port: 4001,
...config.http,
},
compression: {
global: {
doc: 'Enable global compression for all http routes',
format: Boolean,
default: true,
env: 'COMPRESSION_GLOBAL',
},
global: true,
...config.compression,
},
jwt: {
secret: {
doc: 'Secret used for JWT signing',
format: 'secret-string',
default: 'change_me',
env: 'AUTH_JWT_SECRET',
sensitive: true,
},
expire: {
doc: 'Expire time for JWT',
format: String,
default: '60d',
env: 'AUTH_JWT_EXPIRE',
},
secret: 'change_me',
expire: '60d',
...config.jwt,
},
basicAuth: {
type: {
doc: 'Type of basic auth to use',
format: ['key', 'disabled'],
default: 'key',
env: 'BASIC_AUTH_TYPE',
},
key: {
doc: 'Key used for basic authorization',
format: 'secret-string',
default: 'change_me',
env: 'BASIC_AUTH_KEY',
sensitive: true,
},
type: 'key',
key: 'change_me',
...config.basicAuth,
},
organization: {
name: {
doc: 'Organization name - Used as a folder name in the storage of files',
format: String,
default: 'local',
env: 'ORG_NAME',
},
hostnames: {
doc: 'Hostnames the organization maps to',
format: Array,
default: ['localhost', '127.0.0.1'],
env: 'ORG_HOSTNAMES',
},
name: 'local',
hostnames: ['localhost', '127.0.0.1'],
...config.organization,
},
sink: {
type: {
doc: 'Type of sink to use',
format: ['fs', 'mem', 'test'],
default: 'fs',
env: 'SINK_TYPE',
},
path: {
doc: 'Absolute path to store files in when using the "fs" sink',
format: String,
default: path.join(os.tmpdir(), '/eik'),
env: 'SINK_PATH',
},
}
sink:
config.sink instanceof Sink
? config.sink
: {
type: 'fs',
path: path.join(os.tmpdir(), '/eik'),
...config.sink,
},
});

const env = conf.get('env');

const logger = pino({
level: conf.get('log.level'),
name: conf.get('name'),
});

try {
conf.loadFile(path.join(CWD, `/config/${env}.yaml`));
} catch (error) {
logger.error(error);
}

conf.validate();
const DefaultConfig = withDefaults({});

export default conf;
export { DefaultConfig, withDefaults };
61 changes: 31 additions & 30 deletions lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,40 +5,43 @@ import pino from 'pino';
import cors from '@fastify/cors';
import jwt from '@fastify/jwt';
import eik from '@eik/core';
import Sink from '@eik/sink';

import config from './config.js';
import { DefaultConfig, withDefaults } from './config.js';
import * as utils from './utils.js';

const EikService = class EikService {
constructor({ customSink, notFoundCacheControl, aliasCacheControl } = {}) {
this._notFoundCacheControl =
notFoundCacheControl || 'public, max-age=5';
/**
* @param {Config} config
*/
constructor(config) {
const cfg = withDefaults(config);

const logger = pino({
level: config.get('log.level'),
name: config.get('name'),
level: cfg.log?.level,
name: cfg.name,
});

let sink;
if (customSink) {
sink = customSink;
} else if (config.get('sink.type') === 'mem') {
if (cfg.sink instanceof Sink) {
sink = cfg.sink;
} else if (cfg.sink?.type === 'mem') {
logger.info(
`Server is running with a in memory sink. Uploaded files will be lost on restart!`,
);
sink = new eik.sink.MEM();
} else {
logger.info(
`Server is running with the file system sink. Uploaded files will be stored under "${config.get(
'sink.path',
)}"`,
`Server is running with the file system sink. Uploaded files will be stored under "${cfg.sink?.path}"`,
);
sink = new eik.sink.FS();
}

// Transform organization config
const organizations = config
.get('organization.hostnames')
.map((hostname) => [hostname, config.get('organization.name')]);
const organizations = cfg.organization?.hostnames?.map((hostname) => [
hostname,
cfg.organization?.name,
]);

this._versionsGet = new eik.http.VersionsGet({
organizations,
Expand All @@ -55,13 +58,13 @@ const EikService = class EikService {
organizations,
sink,
logger,
cacheControl: aliasCacheControl,
cacheControl: cfg.aliasCacheControl,
});
this._aliasPut = new eik.http.AliasPut({ organizations, sink, logger });
this._authPost = new eik.http.AuthPost({
organizations,
logger,
authKey: config.get('basicAuth.key'),
authKey: cfg.basicAuth?.key,
});
this._pkgLog = new eik.http.PkgLog({ organizations, sink, logger });
this._pkgGet = new eik.http.PkgGet({ organizations, sink, logger });
Expand Down Expand Up @@ -112,34 +115,32 @@ const EikService = class EikService {
});

this.metrics = metrics;
this.config = config;
this.config = cfg;
this.logger = logger;
this.sink = sink;

// Print warnings

if (
config.get('basicAuth.type') === 'key' &&
config.get('basicAuth.key') === config.default('basicAuth.key')
cfg.basicAuth?.type === 'key' &&
cfg.basicAuth.key === DefaultConfig.basicAuth.key
) {
logger.warn(
'Server is running with default basic authorization key configured! For security purposes, it is highly recommended to set a custom value!',
);
}

if (config.get('jwt.secret') === config.default('jwt.secret')) {
if (cfg.jwt?.secret === DefaultConfig.jwt.secret) {
logger.warn(
'Server is running with default jwt secret configured! For security purposes, it is highly recommended to set a custom value!',
);
}

// Print info

const hosts = config.get('organization.hostnames').join(', ');
const hosts = cfg.organization?.hostnames?.join(', ');
logger.info(
`Files for "${hosts}" will be stored in the "${config.get(
'organization.name',
)}" organization space`,
`Files for "${hosts}" will be stored in the "${cfg.organization?.name}" organization space`,
);
}

Expand All @@ -163,7 +164,7 @@ const EikService = class EikService {

// Authentication
app.register(jwt, {
secret: config.get('jwt.secret'),
secret: this.config.jwt?.secret,
messages: {
badRequestErrorMessage:
'Autorization header is malformatted. Format is "Authorization: Bearer [token]"',
Expand Down Expand Up @@ -198,12 +199,12 @@ const EikService = class EikService {

// Compression
app.register(compression, {
global: config.get('compression.global'),
global: this.config.compression?.global,
});

// 404 handling
app.setNotFoundHandler((request, reply) => {
reply.header('cache-control', this._notFoundCacheControl);
reply.header('cache-control', this.config.notFoundCacheControl);
reply.type('text/plain');
reply.code(404);
reply.send('Not found');
Expand All @@ -220,7 +221,7 @@ const EikService = class EikService {
if (error.statusCode === 404) {
reply.header(
'cache-control',
this._notFoundCacheControl,
this.config.notFoundCacheControl,
);
}
reply.send(error);
Expand All @@ -241,7 +242,7 @@ const EikService = class EikService {
const body = JSON.parse(JSON.stringify(outgoing.body));

const token = app.jwt.sign(body, {
expiresIn: config.get('jwt.expire'),
expiresIn: this.config.jwt?.expire,
});

reply.header('cache-control', outgoing.cacheControl);
Expand Down
Loading

0 comments on commit 8e63b4d

Please sign in to comment.