0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-04-07 23:01:25 -05:00

refactor(core): inquire when required env not found (#586)

* refactor(core): inquire when required env not found

* refactor(core): add comments for create DB pool
This commit is contained in:
Gao Sun 2022-04-21 16:13:59 +08:00 committed by GitHub
parent 4ba37e7e73
commit 6a62e32fa5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 225 additions and 68 deletions

View file

@ -29,6 +29,7 @@
"got": "^11.8.2",
"i18next": "^20.3.5",
"iconv-lite": "0.6.3",
"inquirer": "^8.2.2",
"jose": "^3.14.3",
"koa": "^2.13.1",
"koa-body": "^4.2.0",
@ -52,6 +53,7 @@
"@shopify/jest-koa-mocks": "^3.0.8",
"@silverhand/eslint-config": "^0.10.2",
"@silverhand/ts-config": "^0.10.2",
"@types/inquirer": "^8.2.1",
"@types/jest": "^27.4.1",
"@types/koa": "^2.13.3",
"@types/koa-logger": "^3.1.1",

View file

@ -0,0 +1,7 @@
import { appendFileSync } from 'fs';
const appendDotEnv = (key: string, value: string) => {
appendFileSync('.env', `${key}=${value}\n`);
};
export default appendDotEnv;

View file

@ -0,0 +1,37 @@
import { assertEnv } from '@silverhand/essentials';
import inquirer from 'inquirer';
import { createPool } from 'slonik';
import { createInterceptors } from 'slonik-interceptor-preset';
import appendDotEnv from './append-dot-env';
const createPoolByEnv = async (isTest: boolean) => {
// Database connection is disabled in unit test environment
if (isTest) {
return;
}
const key = 'DB_URL';
const interceptors = [...createInterceptors()];
try {
const databaseDsn = assertEnv(key);
return createPool(databaseDsn, { interceptors });
} catch (error: unknown) {
const answer = await inquirer.prompt({
name: 'dsn',
message: `No Postgres DSN (${key}) found in env variables. Please input the DSN which points to Logto database:`,
});
if (!answer.dsn) {
throw error;
}
appendDotEnv(key, answer.dsn);
return createPool(answer.dsn, { interceptors });
}
};
export default createPoolByEnv;

View file

@ -1,11 +1,9 @@
import crypto from 'crypto';
import { readFileSync } from 'fs';
import { getEnv, Optional } from '@silverhand/essentials';
import { DatabasePoolType } from 'slonik';
import { assertEnv, getEnv, Optional } from '@silverhand/essentials';
import { nanoid } from 'nanoid';
import { createPool, DatabasePoolType } from 'slonik';
import { createInterceptors } from 'slonik-interceptor-preset';
import { string, number } from 'zod';
import createPoolByEnv from './create-pool-by-env';
import loadOidcValues from './oidc';
import loadPasswordValues from './password';
export enum MountedApps {
Api = 'api',
@ -13,50 +11,21 @@ export enum MountedApps {
Console = 'console',
}
const readPrivateKey = (path: string): Optional<string> => {
try {
return readFileSync(path, 'utf-8');
} catch {}
};
const loadOidcValues = (port: number) => {
const privateKeyPath = getEnv('OIDC_PRIVATE_KEY_PATH', 'oidc-private-key.pem');
const privateKey = crypto.createPrivateKey(readPrivateKey(privateKeyPath) ?? '');
const publicKey = crypto.createPublicKey(privateKey);
return {
privateKeyPath,
privateKey,
publicKey,
issuer: getEnv('OIDC_ISSUER', `http://localhost:${port}/oidc`),
adminResource: getEnv('ADMIN_RESOURCE', 'https://api.logto.io'),
defaultIdTokenTtl: 60 * 60,
defaultRefreshTokenTtl: 14 * 24 * 60 * 60,
};
};
const loadEnvValues = async () => {
const isProduction = getEnv('NODE_ENV') === 'production';
const isTest = getEnv('NODE_ENV') === 'test';
const port = Number(getEnv('PORT', '3001'));
const databaseUrl = isTest ? getEnv('DB_URL') : assertEnv('DB_URL');
return Object.freeze({
isTest,
isProduction,
databaseUrl,
httpsCert: process.env.HTTPS_CERT,
httpsKey: process.env.HTTPS_KEY,
port,
developmentUserId: getEnv('DEVELOPMENT_USER_ID'),
trustingTlsOffloadingProxies: getEnv('TRUSTING_TLS_OFFLOADING_PROXIES') === 'true',
passwordPeppers: string()
.array()
.parse(isTest ? [nanoid()] : JSON.parse(assertEnv('PASSWORD_PEPPERS'))),
passwordIterationCount: number()
.min(100)
.parse(Number(getEnv('PASSWORD_ITERATION_COUNT', '1000'))),
oidc: loadOidcValues(port),
password: await loadPasswordValues(isTest),
oidc: await loadOidcValues(port),
});
};
@ -89,11 +58,7 @@ function createEnvSet() {
load: async () => {
values = await loadEnvValues();
if (!values.isTest) {
const interceptors = [...createInterceptors()];
pool = createPool(values.databaseUrl, { interceptors });
}
pool = await createPoolByEnv(values.isTest);
},
};
}

View file

@ -0,0 +1,56 @@
import crypto, { generateKeyPairSync } from 'crypto';
import { readFileSync, writeFileSync } from 'fs';
import { getEnv } from '@silverhand/essentials';
import inquirer from 'inquirer';
const readPrivateKey = async (path: string): Promise<string> => {
const privateKeyPath = getEnv('OIDC_PRIVATE_KEY_PATH', 'oidc-private-key.pem');
try {
return readFileSync(path, 'utf-8');
} catch (error: unknown) {
const answer = await inquirer.prompt({
type: 'confirm',
name: 'confirm',
message: `No private key found in \`${privateKeyPath}\`, would you like to generate a new one?`,
});
if (!answer.confirm) {
throw error;
}
const { privateKey } = generateKeyPairSync('rsa', {
modulusLength: 4096,
publicKeyEncoding: {
type: 'spki',
format: 'pem',
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem',
},
});
writeFileSync(privateKeyPath, privateKey);
return privateKey;
}
};
const loadOidcValues = async (port: number) => {
const privateKeyPath = getEnv('OIDC_PRIVATE_KEY_PATH', 'oidc-private-key.pem');
const privateKey = crypto.createPrivateKey(await readPrivateKey(privateKeyPath));
const publicKey = crypto.createPublicKey(privateKey);
return Object.freeze({
privateKeyPath,
privateKey,
publicKey,
issuer: getEnv('OIDC_ISSUER', `http://localhost:${port}/oidc`),
adminResource: getEnv('ADMIN_RESOURCE', 'https://api.logto.io'),
defaultIdTokenTtl: 60 * 60,
defaultRefreshTokenTtl: 14 * 24 * 60 * 60,
});
};
export default loadOidcValues;

View file

@ -0,0 +1,50 @@
import { assertEnv, getEnv } from '@silverhand/essentials';
import inquirer from 'inquirer';
import { nanoid } from 'nanoid';
import { number, string } from 'zod';
import appendDotEnv from './append-dot-env';
const loadPeppers = async (isTest: boolean): Promise<string[]> => {
if (isTest) {
return [nanoid()];
}
const key = 'PASSWORD_PEPPERS';
try {
return string()
.array()
.parse(JSON.parse(assertEnv(key)));
} catch (error: unknown) {
if (!(error instanceof Error && error.message === `env variable ${key} not found`)) {
throw error;
}
const answer = await inquirer.prompt({
type: 'confirm',
name: 'confirm',
message: `No password peppers (${key}) found in env variables, would you like to generate a new set and save it into \`.env\`?`,
});
if (!answer.confirm) {
throw error;
}
const peppers = [nanoid(), nanoid(), nanoid()];
appendDotEnv(key, JSON.stringify(peppers));
return peppers;
}
};
const loadPasswordValues = async (isTest: boolean) => {
return Object.freeze({
peppers: await loadPeppers(isTest),
iterationCount: number()
.min(100)
.parse(Number(getEnv('PASSWORD_ITERATION_COUNT', '1000'))),
});
};
export default loadPasswordValues;

View file

@ -51,8 +51,7 @@ describe('koaSpaProxy middleware', () => {
const spy = jest.spyOn(envSet, 'values', 'get').mockReturnValue({
...envSet.values,
isProduction: true,
passwordPeppers: ['foo'],
databaseUrl: 'some_db_url',
password: { peppers: ['foo'], iterationCount: 1000 },
});
const ctx = createContextWithRouteParameters({
@ -70,8 +69,7 @@ describe('koaSpaProxy middleware', () => {
const spy = jest.spyOn(envSet, 'values', 'get').mockReturnValue({
...envSet.values,
isProduction: true,
passwordPeppers: ['foo'],
databaseUrl: 'some_db_url',
password: { peppers: ['foo'], iterationCount: 1000 },
});
const ctx = createContextWithRouteParameters({

View file

@ -24,9 +24,8 @@ export const encryptPassword = (
(accumulator, current) => accumulator + (current.codePointAt(0) ?? 0),
0
);
const peppers = envSet.values.passwordPeppers;
const { peppers, iterationCount } = envSet.values.password;
const pepper = peppers[sum % peppers.length];
const iterationCount = envSet.values.passwordIterationCount;
assertThat(pepper, 'password.pepper_not_found');

81
pnpm-lock.yaml generated
View file

@ -164,6 +164,7 @@ importers:
'@silverhand/eslint-config': ^0.10.2
'@silverhand/essentials': ^1.1.0
'@silverhand/ts-config': ^0.10.2
'@types/inquirer': ^8.2.1
'@types/jest': ^27.4.1
'@types/koa': ^2.13.3
'@types/koa-logger': ^3.1.1
@ -181,6 +182,7 @@ importers:
got: ^11.8.2
i18next: ^20.3.5
iconv-lite: 0.6.3
inquirer: ^8.2.2
jest: ^27.5.1
jest-matcher-specific-error: ^1.0.0
jose: ^3.14.3
@ -220,6 +222,7 @@ importers:
got: 11.8.3
i18next: 20.6.1
iconv-lite: 0.6.3
inquirer: 8.2.2
jose: 3.20.3
koa: 2.13.4
koa-body: 4.2.0
@ -242,6 +245,7 @@ importers:
'@shopify/jest-koa-mocks': 3.0.8
'@silverhand/eslint-config': 0.10.2_3a533fa6cc3da0cf8525ef55d41c4384
'@silverhand/ts-config': 0.10.2_typescript@4.6.2
'@types/inquirer': 8.2.1
'@types/jest': 27.4.1
'@types/koa': 2.13.4
'@types/koa-logger': 3.1.2
@ -5792,6 +5796,13 @@ packages:
'@types/node': 17.0.23
dev: false
/@types/inquirer/8.2.1:
resolution: {integrity: sha512-wKW3SKIUMmltbykg4I5JzCVzUhkuD9trD6efAmYgN2MrSntY0SMRQzEnD3mkyJ/rv9NLbTC7g3hKKE86YwEDLw==}
dependencies:
'@types/through': 0.0.30
rxjs: 7.5.5
dev: true
/@types/istanbul-lib-coverage/2.0.3:
resolution: {integrity: sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==}
dev: true
@ -6066,6 +6077,12 @@ packages:
'@types/superagent': 4.1.15
dev: true
/@types/through/0.0.30:
resolution: {integrity: sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg==}
dependencies:
'@types/node': 17.0.23
dev: true
/@types/unist/2.0.6:
resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==}
@ -6617,7 +6634,6 @@ packages:
engines: {node: '>=8'}
dependencies:
type-fest: 0.21.3
dev: true
/ansi-html-community/0.0.8:
resolution: {integrity: sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==}
@ -7462,7 +7478,6 @@ packages:
/chardet/0.7.0:
resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}
dev: true
/cheerio-select/1.6.0:
resolution: {integrity: sha512-eq0GdBvxVFbqWgmCm7M3XGs1I8oLy/nExUnh6oLqmBditPO9AqQJrkslDpMun/hZ0yyTs8L0m85OHp4ho6Qm9g==}
@ -7584,7 +7599,11 @@ packages:
engines: {node: '>=8'}
dependencies:
restore-cursor: 3.1.0
dev: true
/cli-spinners/2.6.1:
resolution: {integrity: sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==}
engines: {node: '>=6'}
dev: false
/cli-table3/0.6.1:
resolution: {integrity: sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA==}
@ -7606,7 +7625,6 @@ packages:
/cli-width/3.0.0:
resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==}
engines: {node: '>= 10'}
dev: true
/cliui/7.0.4:
resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
@ -7639,7 +7657,6 @@ packages:
/clone/1.0.4:
resolution: {integrity: sha1-2jCcwmPfFZlMaIypAheco8fNfH4=}
engines: {node: '>=0.8'}
dev: true
/clone/2.1.2:
resolution: {integrity: sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=}
@ -8521,7 +8538,6 @@ packages:
resolution: {integrity: sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=}
dependencies:
clone: 1.0.4
dev: true
/defer-to-connect/1.1.3:
resolution: {integrity: sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==}
@ -9583,7 +9599,6 @@ packages:
chardet: 0.7.0
iconv-lite: 0.4.24
tmp: 0.0.33
dev: true
/extsprintf/1.3.0:
resolution: {integrity: sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=}
@ -9711,7 +9726,6 @@ packages:
engines: {node: '>=8'}
dependencies:
escape-string-regexp: 1.0.5
dev: true
/file-entry-cache/6.0.1:
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
@ -11052,6 +11066,26 @@ packages:
through: 2.3.8
dev: true
/inquirer/8.2.2:
resolution: {integrity: sha512-pG7I/si6K/0X7p1qU+rfWnpTE1UIkTONN1wxtzh0d+dHXtT/JG6qBgLxoyHVsQa8cFABxAPh0pD6uUUHiAoaow==}
engines: {node: '>=12.0.0'}
dependencies:
ansi-escapes: 4.3.2
chalk: 4.1.2
cli-cursor: 3.1.0
cli-width: 3.0.0
external-editor: 3.1.0
figures: 3.2.0
lodash: 4.17.21
mute-stream: 0.0.8
ora: 5.4.1
run-async: 2.4.1
rxjs: 7.5.5
string-width: 4.2.3
strip-ansi: 6.0.1
through: 2.3.8
dev: false
/int64-buffer/0.99.1007:
resolution: {integrity: sha512-XDBEu44oSTqlvCSiOZ/0FoUkpWu/vwjJLGSKDabNISPQNZ5wub1FodGHBljRsrR0IXRPq7SslshZYMuA55CgTQ==}
engines: {node: '>= 4.5.0'}
@ -11235,6 +11269,11 @@ packages:
is-path-inside: 3.0.3
dev: false
/is-interactive/1.0.0:
resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==}
engines: {node: '>=8'}
dev: false
/is-js-type/2.0.0:
resolution: {integrity: sha1-c2FwBtZZtOtHKbunR9KHgt8PfiI=}
dependencies:
@ -11398,7 +11437,6 @@ packages:
/is-unicode-supported/0.1.0:
resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
engines: {node: '>=10'}
dev: true
/is-weakref/1.0.2:
resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==}
@ -12670,7 +12708,6 @@ packages:
dependencies:
chalk: 4.1.2
is-unicode-supported: 0.1.0
dev: true
/log-update/4.0.0:
resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==}
@ -13602,7 +13639,6 @@ packages:
/mute-stream/0.0.8:
resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==}
dev: true
/nan/2.15.0:
resolution: {integrity: sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==}
@ -14126,6 +14162,21 @@ packages:
word-wrap: 1.2.3
dev: true
/ora/5.4.1:
resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==}
engines: {node: '>=10'}
dependencies:
bl: 4.1.0
chalk: 4.1.2
cli-cursor: 3.1.0
cli-spinners: 2.6.1
is-interactive: 1.0.0
is-unicode-supported: 0.1.0
log-symbols: 4.1.0
strip-ansi: 6.0.1
wcwidth: 1.0.1
dev: false
/ordered-binary/1.2.4:
resolution: {integrity: sha512-A/csN0d3n+igxBPfUrjbV5GC69LWj2pjZzAAeeHXLukQ4+fytfP4T1Lg0ju7MSPSwq7KtHkGaiwO8URZN5IpLg==}
dev: true
@ -14138,7 +14189,6 @@ packages:
/os-tmpdir/1.0.2:
resolution: {integrity: sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=}
engines: {node: '>=0.10.0'}
dev: true
/osenv/0.1.5:
resolution: {integrity: sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==}
@ -16882,7 +16932,6 @@ packages:
dependencies:
onetime: 5.1.2
signal-exit: 3.0.6
dev: true
/retry/0.12.0:
resolution: {integrity: sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=}
@ -16968,7 +17017,6 @@ packages:
/run-async/2.4.1:
resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==}
engines: {node: '>=0.12.0'}
dev: true
/run-parallel/1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
@ -16992,7 +17040,6 @@ packages:
resolution: {integrity: sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==}
dependencies:
tslib: 2.3.1
dev: false
/sade/1.8.1:
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
@ -18305,7 +18352,6 @@ packages:
/through/2.3.8:
resolution: {integrity: sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=}
dev: true
/through2/2.0.5:
resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==}
@ -18347,7 +18393,6 @@ packages:
engines: {node: '>=0.6.0'}
dependencies:
os-tmpdir: 1.0.2
dev: true
/tmpl/1.0.5:
resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==}
@ -18679,7 +18724,6 @@ packages:
/type-fest/0.21.3:
resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
engines: {node: '>=10'}
dev: true
/type-fest/0.4.1:
resolution: {integrity: sha512-IwzA/LSfD2vC1/YDYMv/zHP4rDF1usCwllsDpbolT3D4fUepIO7f9K70jjmUewU/LmGUKJcwcVtDCpnKk4BPMw==}
@ -19265,7 +19309,6 @@ packages:
resolution: {integrity: sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=}
dependencies:
defaults: 1.0.3
dev: true
/weak-lru-cache/1.2.2:
resolution: {integrity: sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==}