@logto/cli 1.0.0-beta.16 → 1.0.0-beta.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/logto.js +2 -0
- package/lib/commands/connector/add.js +8 -10
- package/lib/commands/connector/index.js +9 -14
- package/lib/commands/connector/list.js +10 -16
- package/lib/commands/connector/remove.js +13 -18
- package/lib/commands/connector/utils.d.ts +1 -1
- package/lib/commands/connector/utils.js +100 -86
- package/lib/commands/database/alteration/index.d.ts +1 -2
- package/lib/commands/database/alteration/index.js +36 -98
- package/lib/commands/database/alteration/index.test.js +36 -57
- package/lib/commands/database/alteration/meta-url.d.ts +1 -0
- package/lib/commands/database/alteration/meta-url.js +4 -0
- package/lib/commands/database/alteration/type.d.ts +1 -1
- package/lib/commands/database/alteration/type.js +1 -2
- package/lib/commands/database/alteration/utils.d.ts +4 -0
- package/lib/commands/database/alteration/utils.js +39 -0
- package/lib/commands/database/alteration/version.d.ts +1 -1
- package/lib/commands/database/alteration/version.js +18 -25
- package/lib/commands/database/config.js +44 -49
- package/lib/commands/database/index.js +7 -12
- package/lib/commands/database/seed/index.d.ts +1 -1
- package/lib/commands/database/seed/index.js +59 -55
- package/lib/commands/database/seed/oidc-config.js +15 -18
- package/lib/commands/database/utilities.js +6 -11
- package/lib/commands/install/index.d.ts +1 -1
- package/lib/commands/install/index.js +14 -28
- package/lib/commands/install/utils.d.ts +0 -1
- package/lib/commands/install/utils.js +59 -106
- package/lib/constants.js +1 -4
- package/lib/database.js +33 -43
- package/lib/index.js +18 -23
- package/lib/meta-url.d.ts +1 -0
- package/lib/meta-url.js +4 -0
- package/lib/package-json.d.ts +7 -3
- package/lib/package-json.js +28 -27
- package/lib/queries/logto-config.js +18 -26
- package/lib/queries/logto-config.test.js +29 -30
- package/lib/test-utilities.d.ts +2 -2
- package/lib/test-utilities.js +1 -5
- package/lib/utilities.d.ts +5 -5
- package/lib/utilities.js +49 -64
- package/package.json +27 -23
- package/bin/logto +0 -2
|
@@ -1,102 +1,106 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const logto_config_1 = require("../../../queries/logto-config");
|
|
17
|
-
const utilities_1 = require("../../../utilities");
|
|
18
|
-
const alteration_1 = require("../alteration");
|
|
19
|
-
const oidc_config_1 = require("./oidc-config");
|
|
1
|
+
import { readdir, readFile } from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { generateStandardId } from '@logto/core-kit';
|
|
4
|
+
import { logtoConfigGuards, LogtoOidcConfigKey, managementResource, defaultSignInExperience, createDefaultSetting, createDemoAppApplication, defaultRole, } from '@logto/schemas';
|
|
5
|
+
import { Hooks } from '@logto/schemas/models';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { sql } from 'slonik';
|
|
8
|
+
import { raw } from 'slonik-sql-tag-raw';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import { createPoolAndDatabaseIfNeeded, insertInto } from '../../../database.js';
|
|
11
|
+
import { getRowsByKeys, doesConfigsTableExist, updateDatabaseTimestamp, updateValueByKey, } from '../../../queries/logto-config.js';
|
|
12
|
+
import { getPathInModule, log, oraPromise } from '../../../utilities.js';
|
|
13
|
+
import { getLatestAlterationTimestamp } from '../alteration/index.js';
|
|
14
|
+
import { getAlterationDirectory } from '../alteration/utils.js';
|
|
15
|
+
import { oidcConfigReaders } from './oidc-config.js';
|
|
20
16
|
const createTables = async (connection) => {
|
|
21
|
-
const tableDirectory =
|
|
22
|
-
const directoryFiles = await
|
|
17
|
+
const tableDirectory = getPathInModule('@logto/schemas', 'tables');
|
|
18
|
+
const directoryFiles = await readdir(tableDirectory);
|
|
23
19
|
const tableFiles = directoryFiles.filter((file) => file.endsWith('.sql'));
|
|
24
20
|
const queries = await Promise.all(tableFiles.map(async (file) => [
|
|
25
21
|
file,
|
|
26
|
-
await
|
|
22
|
+
await readFile(path.join(tableDirectory, file), 'utf8'),
|
|
27
23
|
]));
|
|
28
24
|
// Await in loop is intended for better error handling
|
|
29
25
|
for (const [, query] of queries) {
|
|
30
26
|
// eslint-disable-next-line no-await-in-loop
|
|
31
|
-
await connection.query(
|
|
27
|
+
await connection.query(sql `${raw(query)}`);
|
|
28
|
+
}
|
|
29
|
+
for (const table of [Hooks]) {
|
|
30
|
+
// eslint-disable-next-line no-await-in-loop
|
|
31
|
+
await connection.query(sql `${raw(table.raw)}`);
|
|
32
32
|
}
|
|
33
33
|
};
|
|
34
|
-
const seedTables = async (connection) => {
|
|
35
|
-
const { managementResource, defaultSignInExperience, createDefaultSetting, createDemoAppApplication, defaultRole, } = schemas_1.seeds;
|
|
34
|
+
const seedTables = async (connection, latestTimestamp) => {
|
|
36
35
|
await Promise.all([
|
|
37
|
-
connection.query(
|
|
38
|
-
connection.query(
|
|
39
|
-
connection.query(
|
|
40
|
-
connection.query(
|
|
41
|
-
connection.query(
|
|
42
|
-
|
|
36
|
+
connection.query(insertInto(managementResource, 'resources')),
|
|
37
|
+
connection.query(insertInto(createDefaultSetting(), 'settings')),
|
|
38
|
+
connection.query(insertInto(defaultSignInExperience, 'sign_in_experiences')),
|
|
39
|
+
connection.query(insertInto(createDemoAppApplication(generateStandardId()), 'applications')),
|
|
40
|
+
connection.query(insertInto(defaultRole, 'roles')),
|
|
41
|
+
updateDatabaseTimestamp(connection, latestTimestamp),
|
|
43
42
|
]);
|
|
44
43
|
};
|
|
45
44
|
const seedOidcConfigs = async (pool) => {
|
|
46
|
-
const configGuard =
|
|
47
|
-
key:
|
|
48
|
-
value:
|
|
45
|
+
const configGuard = z.object({
|
|
46
|
+
key: z.nativeEnum(LogtoOidcConfigKey),
|
|
47
|
+
value: z.unknown(),
|
|
49
48
|
});
|
|
50
|
-
const { rows } = await
|
|
49
|
+
const { rows } = await getRowsByKeys(pool, Object.values(LogtoOidcConfigKey));
|
|
51
50
|
// Filter out valid keys that hold a valid value
|
|
52
51
|
const result = await Promise.all(rows.map(async (row) => {
|
|
53
52
|
try {
|
|
54
53
|
const { key, value } = await configGuard.parseAsync(row);
|
|
55
|
-
await
|
|
54
|
+
await logtoConfigGuards[key].parseAsync(value);
|
|
56
55
|
return key;
|
|
57
56
|
}
|
|
58
57
|
catch { }
|
|
59
58
|
}));
|
|
60
59
|
const existingKeys = new Set(result.filter(Boolean));
|
|
61
|
-
const validOptions = Object.values(
|
|
60
|
+
const validOptions = Object.values(LogtoOidcConfigKey).filter((key) => {
|
|
62
61
|
const included = existingKeys.has(key);
|
|
63
62
|
if (included) {
|
|
64
|
-
|
|
63
|
+
log.info(`Key ${chalk.green(key)} exists, skipping`);
|
|
65
64
|
}
|
|
66
65
|
return !included;
|
|
67
66
|
});
|
|
68
67
|
// The awaits in loop is intended since we'd like to log info in sequence
|
|
69
68
|
/* eslint-disable no-await-in-loop */
|
|
70
69
|
for (const key of validOptions) {
|
|
71
|
-
const { value, fromEnv } = await
|
|
70
|
+
const { value, fromEnv } = await oidcConfigReaders[key]();
|
|
72
71
|
if (fromEnv) {
|
|
73
|
-
|
|
72
|
+
log.info(`Read config ${chalk.green(key)} from env`);
|
|
74
73
|
}
|
|
75
74
|
else {
|
|
76
|
-
|
|
75
|
+
log.info(`Generated config ${chalk.green(key)}`);
|
|
77
76
|
}
|
|
78
|
-
await
|
|
77
|
+
await updateValueByKey(pool, key, value);
|
|
79
78
|
}
|
|
80
79
|
/* eslint-enable no-await-in-loop */
|
|
81
|
-
|
|
80
|
+
log.succeed('Seed OIDC config');
|
|
82
81
|
};
|
|
83
82
|
const seedChoices = Object.freeze(['all', 'oidc']);
|
|
84
|
-
const seedByPool = async (pool, type) => {
|
|
83
|
+
export const seedByPool = async (pool, type) => {
|
|
85
84
|
await pool.transaction(async (connection) => {
|
|
86
85
|
if (type !== 'oidc') {
|
|
87
|
-
|
|
86
|
+
// Check alteration scripts available in order to insert correct timestamp
|
|
87
|
+
const latestTimestamp = await getLatestAlterationTimestamp();
|
|
88
|
+
if (latestTimestamp < 1) {
|
|
89
|
+
throw new Error(`No alteration script found when seeding the database.\n` +
|
|
90
|
+
`Please check \`${getAlterationDirectory()}\` to see if there are alteration scripts available.\n`);
|
|
91
|
+
}
|
|
92
|
+
await oraPromise(createTables(connection), {
|
|
88
93
|
text: 'Create tables',
|
|
89
|
-
prefixText:
|
|
94
|
+
prefixText: chalk.blue('[info]'),
|
|
90
95
|
});
|
|
91
|
-
await
|
|
96
|
+
await oraPromise(seedTables(connection, latestTimestamp), {
|
|
92
97
|
text: 'Seed data',
|
|
93
|
-
prefixText:
|
|
98
|
+
prefixText: chalk.blue('[info]'),
|
|
94
99
|
});
|
|
95
100
|
}
|
|
96
101
|
await seedOidcConfigs(connection);
|
|
97
102
|
});
|
|
98
103
|
};
|
|
99
|
-
exports.seedByPool = seedByPool;
|
|
100
104
|
const seed = {
|
|
101
105
|
command: 'seed [type]',
|
|
102
106
|
describe: 'Create database then seed tables and data',
|
|
@@ -113,9 +117,9 @@ const seed = {
|
|
|
113
117
|
default: 'all',
|
|
114
118
|
}),
|
|
115
119
|
handler: async ({ type, swe }) => {
|
|
116
|
-
const pool = await
|
|
117
|
-
if (swe && (await
|
|
118
|
-
|
|
120
|
+
const pool = await createPoolAndDatabaseIfNeeded();
|
|
121
|
+
if (swe && (await doesConfigsTableExist(pool))) {
|
|
122
|
+
log.info('Seeding skipped');
|
|
119
123
|
await pool.end();
|
|
120
124
|
return;
|
|
121
125
|
}
|
|
@@ -123,16 +127,16 @@ const seed = {
|
|
|
123
127
|
// Cannot avoid `as` since the official type definition of `yargs` doesn't work.
|
|
124
128
|
// The value of `type` can be ensured, so it's safe to use `as` here.
|
|
125
129
|
// eslint-disable-next-line no-restricted-syntax
|
|
126
|
-
await
|
|
130
|
+
await seedByPool(pool, type);
|
|
127
131
|
}
|
|
128
132
|
catch (error) {
|
|
129
133
|
console.error(error);
|
|
130
134
|
console.log();
|
|
131
|
-
|
|
135
|
+
log.warn('Error ocurred during seeding your database.\n\n' +
|
|
132
136
|
' Nothing has changed since the seeding process was in a transaction.\n' +
|
|
133
137
|
' Try to fix the error and seed again.');
|
|
134
138
|
}
|
|
135
139
|
await pool.end();
|
|
136
140
|
},
|
|
137
141
|
};
|
|
138
|
-
|
|
142
|
+
export default seed;
|
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const schemas_1 = require("@logto/schemas");
|
|
6
|
-
const essentials_1 = require("@silverhand/essentials");
|
|
7
|
-
const utilities_1 = require("../utilities");
|
|
1
|
+
import { readFile } from 'fs/promises';
|
|
2
|
+
import { LogtoOidcConfigKey } from '@logto/schemas';
|
|
3
|
+
import { getEnv, getEnvAsStringArray } from '@silverhand/essentials';
|
|
4
|
+
import { generateOidcCookieKey, generateOidcPrivateKey } from '../utilities.js';
|
|
8
5
|
const isBase64FormatPrivateKey = (key) => !key.includes('-');
|
|
9
6
|
/**
|
|
10
7
|
* Each config reader will do the following things in order:
|
|
11
8
|
* 1. Try to read value from env (mimic the behavior from the original core)
|
|
12
9
|
* 2. Generate value if #1 doesn't work
|
|
13
10
|
*/
|
|
14
|
-
|
|
11
|
+
export const oidcConfigReaders = {
|
|
15
12
|
/**
|
|
16
13
|
* Try to read private keys with the following order:
|
|
17
14
|
*
|
|
@@ -22,9 +19,9 @@ exports.oidcConfigReaders = {
|
|
|
22
19
|
* @returns The private keys for OIDC provider.
|
|
23
20
|
* @throws An error when failed to read a private key.
|
|
24
21
|
*/
|
|
25
|
-
[
|
|
22
|
+
[LogtoOidcConfigKey.PrivateKeys]: async () => {
|
|
26
23
|
// Direct keys in env
|
|
27
|
-
const privateKeys =
|
|
24
|
+
const privateKeys = getEnvAsStringArray('OIDC_PRIVATE_KEYS');
|
|
28
25
|
if (privateKeys.length > 0) {
|
|
29
26
|
return {
|
|
30
27
|
value: privateKeys.map((key) => {
|
|
@@ -37,26 +34,26 @@ exports.oidcConfigReaders = {
|
|
|
37
34
|
};
|
|
38
35
|
}
|
|
39
36
|
// Read keys from files
|
|
40
|
-
const privateKeyPaths =
|
|
37
|
+
const privateKeyPaths = getEnvAsStringArray('OIDC_PRIVATE_KEY_PATHS');
|
|
41
38
|
if (privateKeyPaths.length > 0) {
|
|
42
39
|
return {
|
|
43
|
-
value: await Promise.all(privateKeyPaths.map(async (path) =>
|
|
40
|
+
value: await Promise.all(privateKeyPaths.map(async (path) => readFile(path, 'utf8'))),
|
|
44
41
|
fromEnv: true,
|
|
45
42
|
};
|
|
46
43
|
}
|
|
47
44
|
return {
|
|
48
|
-
value: [await
|
|
45
|
+
value: [await generateOidcPrivateKey()],
|
|
49
46
|
fromEnv: false,
|
|
50
47
|
};
|
|
51
48
|
},
|
|
52
|
-
[
|
|
49
|
+
[LogtoOidcConfigKey.CookieKeys]: async () => {
|
|
53
50
|
const envKey = 'OIDC_COOKIE_KEYS';
|
|
54
|
-
const keys =
|
|
55
|
-
return { value: keys.length > 0 ? keys : [
|
|
51
|
+
const keys = getEnvAsStringArray(envKey);
|
|
52
|
+
return { value: keys.length > 0 ? keys : [generateOidcCookieKey()], fromEnv: keys.length > 0 };
|
|
56
53
|
},
|
|
57
|
-
[
|
|
54
|
+
[LogtoOidcConfigKey.RefreshTokenReuseInterval]: async () => {
|
|
58
55
|
const envKey = 'OIDC_REFRESH_TOKEN_REUSE_INTERVAL';
|
|
59
|
-
const raw = Number(
|
|
56
|
+
const raw = Number(getEnv(envKey));
|
|
60
57
|
const value = Math.max(3, raw || 0);
|
|
61
58
|
return { value, fromEnv: raw === value };
|
|
62
59
|
},
|
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const nanoid_1 = require("nanoid");
|
|
7
|
-
const generateOidcPrivateKey = async () => {
|
|
8
|
-
const { privateKey } = await (0, util_1.promisify)(crypto_1.generateKeyPair)('rsa', {
|
|
1
|
+
import { generateKeyPair } from 'crypto';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { nanoid } from 'nanoid';
|
|
4
|
+
export const generateOidcPrivateKey = async () => {
|
|
5
|
+
const { privateKey } = await promisify(generateKeyPair)('rsa', {
|
|
9
6
|
modulusLength: 4096,
|
|
10
7
|
publicKeyEncoding: {
|
|
11
8
|
type: 'spki',
|
|
@@ -18,6 +15,4 @@ const generateOidcPrivateKey = async () => {
|
|
|
18
15
|
});
|
|
19
16
|
return privateKey;
|
|
20
17
|
};
|
|
21
|
-
|
|
22
|
-
const generateOidcCookieKey = () => (0, nanoid_1.nanoid)();
|
|
23
|
-
exports.generateOidcCookieKey = generateOidcCookieKey;
|
|
18
|
+
export const generateOidcCookieKey = () => nanoid();
|
|
@@ -1,41 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
const chalk_1 = __importDefault(require("chalk"));
|
|
7
|
-
const database_1 = require("../../database");
|
|
8
|
-
const utilities_1 = require("../../utilities");
|
|
9
|
-
const utils_1 = require("../connector/utils");
|
|
10
|
-
const utils_2 = require("./utils");
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { getDatabaseUrlFromConfig } from '../../database.js';
|
|
3
|
+
import { log } from '../../utilities.js';
|
|
4
|
+
import { validateNodeVersion, inquireInstancePath, validateDatabase, downloadRelease, seedDatabase, createEnv, logFinale, decompress, isUrl, } from './utils.js';
|
|
11
5
|
const installLogto = async ({ path, skipSeed, officialConnectors, downloadUrl }) => {
|
|
12
|
-
|
|
6
|
+
validateNodeVersion();
|
|
13
7
|
// Get instance path
|
|
14
|
-
const instancePath = await
|
|
8
|
+
const instancePath = await inquireInstancePath(path);
|
|
15
9
|
// Validate if user has a valid database
|
|
16
|
-
await
|
|
10
|
+
await validateDatabase();
|
|
17
11
|
// Download and decompress
|
|
18
|
-
const tarPath = !downloadUrl ||
|
|
19
|
-
await
|
|
12
|
+
const tarPath = !downloadUrl || isUrl(downloadUrl) ? await downloadRelease(downloadUrl) : downloadUrl;
|
|
13
|
+
await decompress(instancePath, tarPath);
|
|
20
14
|
// Seed database
|
|
21
15
|
if (skipSeed) {
|
|
22
|
-
|
|
16
|
+
log.info(`Skipped database seeding.\n\n' + ' You can use the ${chalk.green('db seed')} command to seed database when ready.\n`);
|
|
23
17
|
}
|
|
24
18
|
else {
|
|
25
|
-
await
|
|
19
|
+
await seedDatabase(instancePath);
|
|
26
20
|
}
|
|
27
21
|
// Save to dot env
|
|
28
|
-
await
|
|
29
|
-
// Add official connectors
|
|
30
|
-
if (await (0, utils_2.inquireOfficialConnectors)(officialConnectors)) {
|
|
31
|
-
await (0, utils_1.addOfficialConnectors)(instancePath);
|
|
32
|
-
}
|
|
33
|
-
else {
|
|
34
|
-
utilities_1.log.info('Skipped adding official connectors.\n\n' +
|
|
35
|
-
` You can use the ${chalk_1.default.green('connector add')} command to add connectors at any time.\n`);
|
|
36
|
-
}
|
|
22
|
+
await createEnv(instancePath, await getDatabaseUrlFromConfig());
|
|
37
23
|
// Finale
|
|
38
|
-
|
|
24
|
+
logFinale(instancePath);
|
|
39
25
|
};
|
|
40
26
|
const install = {
|
|
41
27
|
command: ['init', 'i', 'install'],
|
|
@@ -68,4 +54,4 @@ const install = {
|
|
|
68
54
|
await installLogto({ path: p, skipSeed: ss, officialConnectors: oc, downloadUrl: du });
|
|
69
55
|
},
|
|
70
56
|
};
|
|
71
|
-
|
|
57
|
+
export default install;
|
|
@@ -7,5 +7,4 @@ export declare const decompress: (toPath: string, tarPath: string) => Promise<vo
|
|
|
7
7
|
export declare const seedDatabase: (instancePath: string) => Promise<void>;
|
|
8
8
|
export declare const createEnv: (instancePath: string, databaseUrl: string) => Promise<void>;
|
|
9
9
|
export declare const logFinale: (instancePath: string) => void;
|
|
10
|
-
export declare const inquireOfficialConnectors: (initialAnswer?: boolean) => Promise<boolean>;
|
|
11
10
|
export declare const isUrl: (string: string) => boolean;
|
|
@@ -1,94 +1,63 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
-
if (mod && mod.__esModule) return mod;
|
|
20
|
-
var result = {};
|
|
21
|
-
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
-
__setModuleDefault(result, mod);
|
|
23
|
-
return result;
|
|
24
|
-
};
|
|
25
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
-
};
|
|
28
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
-
exports.isUrl = exports.inquireOfficialConnectors = exports.logFinale = exports.createEnv = exports.seedDatabase = exports.decompress = exports.downloadRelease = exports.validateDatabase = exports.inquireInstancePath = exports.validateNodeVersion = exports.defaultPath = void 0;
|
|
30
|
-
const child_process_1 = require("child_process");
|
|
31
|
-
const fs_1 = require("fs");
|
|
32
|
-
const promises_1 = require("fs/promises");
|
|
33
|
-
const os_1 = __importDefault(require("os"));
|
|
34
|
-
const path_1 = __importDefault(require("path"));
|
|
35
|
-
const essentials_1 = require("@silverhand/essentials");
|
|
36
|
-
const chalk_1 = __importDefault(require("chalk"));
|
|
37
|
-
const fs_extra_1 = require("fs-extra");
|
|
38
|
-
const inquirer_1 = __importDefault(require("inquirer"));
|
|
39
|
-
const semver = __importStar(require("semver"));
|
|
40
|
-
const tar_1 = __importDefault(require("tar"));
|
|
41
|
-
const database_1 = require("../../database");
|
|
42
|
-
const utilities_1 = require("../../utilities");
|
|
43
|
-
const seed_1 = require("../database/seed");
|
|
44
|
-
exports.defaultPath = path_1.default.join(os_1.default.homedir(), 'logto');
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { assert } from '@silverhand/essentials';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import inquirer from 'inquirer';
|
|
9
|
+
import * as semver from 'semver';
|
|
10
|
+
import tar from 'tar';
|
|
11
|
+
import { createPoolAndDatabaseIfNeeded } from '../../database.js';
|
|
12
|
+
import { packageJson } from '../../package-json.js';
|
|
13
|
+
import { cliConfig, ConfigKey, downloadFile, isTty, log, oraPromise, safeExecSync, } from '../../utilities.js';
|
|
14
|
+
import { seedByPool } from '../database/seed/index.js';
|
|
15
|
+
export const defaultPath = path.join(os.homedir(), 'logto');
|
|
45
16
|
const pgRequired = new semver.SemVer('14.0.0');
|
|
46
|
-
const validateNodeVersion = () => {
|
|
17
|
+
export const validateNodeVersion = () => {
|
|
47
18
|
const required = [new semver.SemVer('16.13.0'), new semver.SemVer('18.12.0')];
|
|
48
19
|
const requiredVersionString = required.map((version) => '^' + version.version).join(' || ');
|
|
49
|
-
const current = new semver.SemVer(
|
|
20
|
+
const current = new semver.SemVer(execSync('node -v', { encoding: 'utf8', stdio: 'pipe' }));
|
|
50
21
|
if (required.every((version) => version.major !== current.major)) {
|
|
51
|
-
|
|
22
|
+
log.error(`Logto requires NodeJS ${requiredVersionString}, but ${current.version} found.`);
|
|
52
23
|
}
|
|
53
24
|
if (required.some((version) => version.major === current.major && version.compare(current) > 0)) {
|
|
54
|
-
|
|
25
|
+
log.warn(`Logto is tested under NodeJS ${requiredVersionString}, but version ${current.version} found.`);
|
|
55
26
|
}
|
|
56
27
|
};
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
? `The path ${chalk_1.default.green(value)} already exists, please try another.`
|
|
28
|
+
const validatePath = (value) => existsSync(path.resolve(value))
|
|
29
|
+
? `The path ${chalk.green(value)} already exists, please try another.`
|
|
60
30
|
: true;
|
|
61
|
-
const inquireInstancePath = async (initialPath) => {
|
|
62
|
-
if (!
|
|
63
|
-
|
|
31
|
+
export const inquireInstancePath = async (initialPath) => {
|
|
32
|
+
if (!isTty()) {
|
|
33
|
+
assert(initialPath, new Error('Path is missing'));
|
|
64
34
|
return initialPath;
|
|
65
35
|
}
|
|
66
|
-
const { instancePath } = await
|
|
36
|
+
const { instancePath } = await inquirer.prompt({
|
|
67
37
|
name: 'instancePath',
|
|
68
38
|
message: 'Where should we create your Logto instance?',
|
|
69
39
|
type: 'input',
|
|
70
|
-
default:
|
|
40
|
+
default: defaultPath,
|
|
71
41
|
filter: (value) => value.trim(),
|
|
72
42
|
validate: validatePath,
|
|
73
43
|
}, { instancePath: initialPath });
|
|
74
44
|
// Validate for initialPath
|
|
75
45
|
const validated = validatePath(instancePath);
|
|
76
46
|
if (validated !== true) {
|
|
77
|
-
|
|
47
|
+
log.error(validated);
|
|
78
48
|
}
|
|
79
49
|
return instancePath;
|
|
80
50
|
};
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (utilities_1.cliConfig.has(utilities_1.ConfigKey.DatabaseUrl) || !(0, utilities_1.isTty)()) {
|
|
51
|
+
export const validateDatabase = async () => {
|
|
52
|
+
if (cliConfig.has(ConfigKey.DatabaseUrl) || !isTty()) {
|
|
84
53
|
return;
|
|
85
54
|
}
|
|
86
|
-
const { hasPostgresUrl } = await
|
|
55
|
+
const { hasPostgresUrl } = await inquirer.prompt({
|
|
87
56
|
name: 'hasPostgresUrl',
|
|
88
57
|
message: `Logto requires PostgreSQL >=${pgRequired.version} but cannot find in the current environment.\n Do you have a remote PostgreSQL instance ready?`,
|
|
89
58
|
type: 'confirm',
|
|
90
59
|
when: () => {
|
|
91
|
-
const pgOutput =
|
|
60
|
+
const pgOutput = safeExecSync('postgres --version') ?? '';
|
|
92
61
|
// Filter out all brackets in the output since Homebrew will append `(Homebrew)`.
|
|
93
62
|
const pgArray = pgOutput.split(' ').filter((value) => !value.startsWith('('));
|
|
94
63
|
const pgCurrent = semver.coerce(pgArray[pgArray.length - 1]);
|
|
@@ -96,74 +65,59 @@ const validateDatabase = async () => {
|
|
|
96
65
|
},
|
|
97
66
|
});
|
|
98
67
|
if (hasPostgresUrl === false) {
|
|
99
|
-
|
|
68
|
+
log.error('Logto requires a Postgres instance to run.');
|
|
100
69
|
}
|
|
101
70
|
};
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
71
|
+
export const downloadRelease = async (url) => {
|
|
72
|
+
const tarFilePath = path.resolve(os.tmpdir(), './logto.tar.gz');
|
|
73
|
+
const from = url ??
|
|
74
|
+
`https://github.com/logto-io/logto/releases/download/v${packageJson.version}/logto.tar.gz`;
|
|
75
|
+
log.info(`Download Logto from ${from}`);
|
|
76
|
+
log.info(`Target ${tarFilePath}`);
|
|
77
|
+
await downloadFile(from, tarFilePath);
|
|
107
78
|
return tarFilePath;
|
|
108
79
|
};
|
|
109
|
-
|
|
110
|
-
const decompress = async (toPath, tarPath) => {
|
|
80
|
+
export const decompress = async (toPath, tarPath) => {
|
|
111
81
|
const run = async () => {
|
|
112
82
|
try {
|
|
113
|
-
await
|
|
114
|
-
await
|
|
83
|
+
await fs.mkdir(toPath);
|
|
84
|
+
await tar.extract({ file: tarPath, cwd: toPath, strip: 1 });
|
|
115
85
|
}
|
|
116
86
|
catch (error) {
|
|
117
|
-
|
|
87
|
+
log.error(error);
|
|
118
88
|
}
|
|
119
89
|
};
|
|
120
|
-
return
|
|
90
|
+
return oraPromise(run(), {
|
|
121
91
|
text: `Decompress to ${toPath}`,
|
|
122
|
-
prefixText:
|
|
92
|
+
prefixText: chalk.blue('[info]'),
|
|
123
93
|
}, true);
|
|
124
94
|
};
|
|
125
|
-
|
|
126
|
-
const seedDatabase = async (instancePath) => {
|
|
95
|
+
export const seedDatabase = async (instancePath) => {
|
|
127
96
|
try {
|
|
128
|
-
const pool = await
|
|
129
|
-
await
|
|
97
|
+
const pool = await createPoolAndDatabaseIfNeeded();
|
|
98
|
+
await seedByPool(pool, 'all');
|
|
130
99
|
await pool.end();
|
|
131
100
|
}
|
|
132
101
|
catch (error) {
|
|
133
102
|
console.error(error);
|
|
134
|
-
await (
|
|
103
|
+
await oraPromise(fs.rm(instancePath, { force: true, recursive: true }), {
|
|
135
104
|
text: 'Clean up',
|
|
136
|
-
prefixText:
|
|
105
|
+
prefixText: chalk.blue('[info]'),
|
|
137
106
|
});
|
|
138
|
-
|
|
139
|
-
` To skip the database seeding, append ${
|
|
107
|
+
log.error('Error occurred during seeding your Logto database. Nothing has changed since the seeding process was in a transaction.\n\n' +
|
|
108
|
+
` To skip the database seeding, append ${chalk.green('--skip-seed')} to the command options.`);
|
|
140
109
|
}
|
|
141
110
|
};
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
encoding: 'utf8',
|
|
147
|
-
});
|
|
148
|
-
utilities_1.log.info(`Saved database URL to ${chalk_1.default.blue(dotEnvPath)}`);
|
|
111
|
+
export const createEnv = async (instancePath, databaseUrl) => {
|
|
112
|
+
const dotEnvPath = path.resolve(instancePath, '.env');
|
|
113
|
+
await fs.writeFile(dotEnvPath, `DB_URL=${databaseUrl}`, 'utf8');
|
|
114
|
+
log.info(`Saved database URL to ${chalk.blue(dotEnvPath)}`);
|
|
149
115
|
};
|
|
150
|
-
|
|
151
|
-
const logFinale = (instancePath) => {
|
|
116
|
+
export const logFinale = (instancePath) => {
|
|
152
117
|
const startCommand = `cd ${instancePath} && npm start`;
|
|
153
|
-
|
|
154
|
-
};
|
|
155
|
-
exports.logFinale = logFinale;
|
|
156
|
-
const inquireOfficialConnectors = async (initialAnswer) => {
|
|
157
|
-
const { value } = await inquirer_1.default.prompt({
|
|
158
|
-
name: 'value',
|
|
159
|
-
message: 'Do you want to add official connectors?',
|
|
160
|
-
type: 'confirm',
|
|
161
|
-
default: true,
|
|
162
|
-
}, { value: initialAnswer });
|
|
163
|
-
return value;
|
|
118
|
+
log.info(`Use the command below to start Logto. Happy hacking!\n\n ${chalk.green(startCommand)}`);
|
|
164
119
|
};
|
|
165
|
-
|
|
166
|
-
const isUrl = (string) => {
|
|
120
|
+
export const isUrl = (string) => {
|
|
167
121
|
try {
|
|
168
122
|
// On purpose to test
|
|
169
123
|
// eslint-disable-next-line no-new
|
|
@@ -174,4 +128,3 @@ const isUrl = (string) => {
|
|
|
174
128
|
return false;
|
|
175
129
|
}
|
|
176
130
|
};
|
|
177
|
-
exports.isUrl = isUrl;
|
package/lib/constants.js
CHANGED