Skip to content

Improved configuration #148

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Sep 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions config/compatibility.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ store:
# secretKey is the same as STORE_SECRET_KEY; must be specified if env doesn't
secretKey: YOUR_SECRET_KEY
# forcePathStyle is the same as STORE_FORCE_PATH_STYLE; defaults to false
forcePathStyle: true
# proxyAttachments is the same as DIRECT_RESPONSE_ATTACHMENT; defaults to false
proxyAttachments: true

# baseUrl is the same as BASE_URL; not required
baseUrl: https://example.com
# Server configuration
server:
# useSecureConfig is the same as COOKIE_SECURE; if in production (which is for end-users), it will be true, else it'll be false
useSecureConfig: true
# baseUrl is the same as BASE_URL; not required
baseUrl: https://example.com
75 changes: 43 additions & 32 deletions libs/server/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import yaml from 'js-yaml';
import { getEnv } from 'libs/shared/env';
import * as env from 'libs/shared/env';
import { existsSync, readFileSync } from 'fs';

export type BasicUser = { username: string; password: string };
Expand All @@ -26,33 +26,39 @@ export interface S3StoreConfiguration {
region: string;
forcePathStyle: boolean;
prefix: string;
proxyAttachments: boolean;
}

export type StoreConfiguration = S3StoreConfiguration;

export interface ServerConfiguration {
useSecureCookies: boolean;
baseUrl?: string;
}

export interface Configuration {
auth: AuthConfiguration;
store: StoreConfiguration;
baseUrl?: string;
server: ServerConfiguration;
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
let loaded: Configuration | undefined = undefined;

export function loadConfig() {
const configFile = String(getEnv('CONFIG_FILE', './notea.yml'));
const configFile = env.getEnvRaw('CONFIG_FILE', false) ?? './notea.yml';

let baseConfig: Configuration = {} as Configuration;
if (existsSync(configFile)) {
const data = readFileSync(configFile, 'utf-8');
baseConfig = yaml.load(data) as Configuration;
}

const disablePassword = getEnv<boolean>('DISABLE_PASSWORD', undefined);
const disablePassword = env.parseBool(env.getEnvRaw('DISABLE_PASSWORD', false), false);

let auth: AuthConfiguration;
if (disablePassword === undefined || !disablePassword) {
const envPassword = getEnv<string>('PASSWORD', undefined, false);
if (!disablePassword) {
const envPassword = env.getEnvRaw('PASSWORD', false);
if (baseConfig.auth === undefined) {
if (envPassword === undefined) {
throw new Error('Authentication undefined');
Expand Down Expand Up @@ -98,47 +104,52 @@ export function loadConfig() {
// for now, this works
{
store.detectCredentials ??= true;
store.accessKey = getEnv<string>(
store.accessKey = env.getEnvRaw(
'STORE_ACCESS_KEY',
store.accessKey,
!store.detectCredentials
)?.toString();
store.secretKey = getEnv<string>(
!store.detectCredentials || !store.accessKey
) ?? store.accessKey;
store.secretKey = env.getEnvRaw(
'STORE_SECRET_KEY',
store.secretKey,
!store.detectCredentials
)?.toString();
store.bucket = getEnv<string>(
!store.detectCredentials || !store.secretKey
) ?? store.secretKey;
store.bucket = env.getEnvRaw(
'STORE_BUCKET',
store.bucket ?? 'notea',
false
).toString();
store.forcePathStyle = getEnv<boolean>(
) ?? 'notea';
store.forcePathStyle = env.parseBool(env.getEnvRaw(
'STORE_FORCE_PATH_STYLE',
store.forcePathStyle ?? false,
!store.forcePathStyle
);
store.endpoint = getEnv<string>(
)) ?? store.forcePathStyle;
store.endpoint = env.getEnvRaw(
'STORE_END_POINT',
store.endpoint,
!store.endpoint
);
store.region = getEnv<string>(
store.endpoint == null
) ?? store.endpoint;
store.region = env.getEnvRaw(
'STORE_REGION',
store.region ?? 'us-east-1',
false
).toString();
store.prefix = getEnv<string>(
) ?? store.region ?? 'us-east-1';
store.prefix = env.getEnvRaw(
'STORE_PREFIX',
store.prefix ?? '',
false
);
false,
) ?? store.prefix ?? '';
store.proxyAttachments = env.parseBool(env.getEnvRaw('DIRECT_RESPONSE_ATTACHMENT', false), store.proxyAttachments ?? false);
}

let server: ServerConfiguration;
if (!baseConfig.server) {
server = {} as ServerConfiguration;
} else {
server = baseConfig.server;
}
{
server.useSecureCookies = env.parseBool(env.getEnvRaw('COOKIE_SECURE', false), process.env.NODE_ENV === 'production');
server.baseUrl = env.getEnvRaw('BASE_URL', false) ?? baseConfig.server.baseUrl;
}

loaded = {
auth,
store,
baseUrl: getEnv<string>('BASE_URL')?.toString() ?? baseConfig.baseUrl,
server
};
}

Expand Down
4 changes: 2 additions & 2 deletions libs/server/middlewares/csrf.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import Tokens from 'csrf';
import { CSRF_HEADER_KEY } from 'libs/shared/const';
import { getEnv } from 'libs/shared/env';
import md5 from 'md5';
import { ApiNext, ApiRequest, ApiResponse, SSRMiddleware } from '../connect';
import { BasicAuthConfiguration, config } from 'libs/server/config';

const tokens = new Tokens();

// generate CSRF secret
const csrfSecret = md5('CSRF' + getEnv('PASSWORD'));
const csrfSecret = md5('CSRF' + (config().auth as BasicAuthConfiguration).password);

export const getCsrfToken = () => tokens.create(csrfSecret);

Expand Down
2 changes: 1 addition & 1 deletion libs/server/middlewares/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const applyNote: (id: string) => SSRMiddleware =
req.props = {
...req.props,
...props,
baseURL: config()?.baseUrl || 'http://' + req.headers.host,
baseURL: config().server.baseUrl ?? 'http://' + req.headers.host,
};

next();
Expand Down
9 changes: 3 additions & 6 deletions libs/server/middlewares/session.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import { ironSession } from 'next-iron-session';
import md5 from 'md5';
import { getEnv } from 'libs/shared/env';
import { BasicAuthConfiguration, config } from 'libs/server/config';

const sessionOptions = {
cookieName: 'notea-auth',
password: md5('notea' + getEnv('PASSWORD')),
password: md5('notea' + (config().auth as BasicAuthConfiguration).password), // NOTE(tecc): in the future, if this field becomes null, it will be an issue
// if your localhost is served on http:// then disable the secure flag
cookieOptions: {
secure: getEnv<boolean>(
'COOKIE_SECURE',
process.env.NODE_ENV === 'production'
),
secure: config().server.useSecureCookies,
},
};

Expand Down
45 changes: 45 additions & 0 deletions libs/shared/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ type AllowedEnvs =
| 'STORE_PREFIX'
| 'CONFIG_FILE';

/**
* @deprecated This function should not be used. Prefer the `config()` system.
*/
export function getEnv<T>(
env: AllowedEnvs,
defaultValue?: any,
Expand Down Expand Up @@ -44,3 +47,45 @@ export function getEnv<T>(

return result as unknown as T;
}

export function getEnvRaw(env: AllowedEnvs, required: true): string;
export function getEnvRaw(env: AllowedEnvs, required?: false): string | undefined;
export function getEnvRaw(env: AllowedEnvs, required?: boolean): string | undefined;
export function getEnvRaw(env: AllowedEnvs, required: boolean = false): string | undefined {
const value = process.env[env];

if (value == null) {
if (required) {
throw new Error(`[Notea] ${env} is undefined`);
} else {
return undefined;
}
}

return String(value).toLocaleLowerCase();
}

export function parseBool(str: string, invalid?: boolean): boolean;
export function parseBool(str: null | undefined): undefined;
export function parseBool(str: string | null | undefined, invalid: boolean): boolean;
export function parseBool(str: string | null | undefined, invalid?: boolean): boolean | undefined;
export function parseBool(str: string | null | undefined, invalid?: boolean): boolean | undefined {
if (str == null) {
return invalid ?? undefined;
}
switch (str.toLowerCase()) {
case "true":
case "1":
case "yes":
case "on":
return true;
case "false":
case "0":
case "no":
case "off":
return false;
default:
if (invalid == null) throw new Error("Invalid boolean: " + str);
else return invalid;
}
}
21 changes: 9 additions & 12 deletions pages/api/file/[...file].ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { api } from 'libs/server/connect';
import { useReferrer } from 'libs/server/middlewares/referrer';
import { useStore } from 'libs/server/middlewares/store';
import { getPathFileByName } from 'libs/server/note-path';
import { getEnv } from 'libs/shared/env';
import { config } from 'libs/server/config';

// On aliyun `X-Amz-Expires` must be less than 604800 seconds
const expires = 86400;
Expand All @@ -24,9 +24,7 @@ export default api()
`public, max-age=${expires}, s-maxage=${expires}, stale-while-revalidate=${expires}`
);

const directed = getEnv<boolean>('DIRECT_RESPONSE_ATTACHMENT', false);

if (directed) {
if (config().store.proxyAttachments) {
const { buffer, contentType } =
await req.state.store.getObjectAndMeta(objectPath);

Expand All @@ -35,15 +33,14 @@ export default api()
}

res.send(buffer);
return;
}
} else {
const signUrl = await req.state.store.getSignUrl(objectPath, expires);

const signUrl = await req.state.store.getSignUrl(objectPath, expires);
if (signUrl) {
res.redirect(signUrl);
return;
}

if (signUrl) {
res.redirect(signUrl);
return;
res.redirect('/404');
}

res.redirect('/404');
});