mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
refactor: seed data for multi-tenancy 2
This commit is contained in:
parent
9775db7af8
commit
a76ce24bee
29 changed files with 181 additions and 98 deletions
|
@ -114,6 +114,19 @@ const manifests = [
|
||||||
|
|
||||||
assert.deepStrictEqual(...manifests);
|
assert.deepStrictEqual(...manifests);
|
||||||
|
|
||||||
|
const autoCompare = (a, b) => {
|
||||||
|
if (typeof a !== typeof b) {
|
||||||
|
return (typeof a).localeCompare(typeof b);
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(a).localeCompare(String(b));
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildSortByKeys = (keys) => (a, b) => {
|
||||||
|
const found = keys.find((key) => a[key] !== b[key]);
|
||||||
|
return found ? autoCompare(a[found], b[found]) : 0;
|
||||||
|
};
|
||||||
|
|
||||||
const queryDatabaseData = async (database) => {
|
const queryDatabaseData = async (database) => {
|
||||||
const pool = new pg.Pool({ database, user: 'postgres', password: 'postgres' });
|
const pool = new pg.Pool({ database, user: 'postgres', password: 'postgres' });
|
||||||
const result = await Promise.all(manifests[0].tables
|
const result = await Promise.all(manifests[0].tables
|
||||||
|
@ -122,16 +135,11 @@ const queryDatabaseData = async (database) => {
|
||||||
|
|
||||||
// check config rows except the value column
|
// check config rows except the value column
|
||||||
if (['logto_configs', '_logto_configs', 'systems'].includes(table_name)) {
|
if (['logto_configs', '_logto_configs', 'systems'].includes(table_name)) {
|
||||||
return [table_name, omitArray(rows, 'value').sort((a, b) => {
|
const data = omitArray(rows, 'value');
|
||||||
if (a.tenant_id === b.tenant_id) {
|
return [table_name, data.sort(buildSortByKeys(Object.keys(data[0] ?? {})))];
|
||||||
return a.key.localeCompare(b.key);
|
|
||||||
}
|
|
||||||
|
|
||||||
return a.tenant_id.localeCompare(b.tenant_id);
|
|
||||||
})];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [table_name, omitArray(
|
const data = omitArray(
|
||||||
rows,
|
rows,
|
||||||
'id',
|
'id',
|
||||||
'resource_id',
|
'resource_id',
|
||||||
|
@ -142,7 +150,9 @@ const queryDatabaseData = async (database) => {
|
||||||
'secret',
|
'secret',
|
||||||
'db_user',
|
'db_user',
|
||||||
'db_user_password'
|
'db_user_password'
|
||||||
)];
|
);
|
||||||
|
|
||||||
|
return [table_name, data.sort(buildSortByKeys(Object.keys(data[0] ?? {})))];
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
31
packages/cli/src/commands/database/seed/cloud.ts
Normal file
31
packages/cli/src/commands/database/seed/cloud.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { adminConsoleApplicationId, adminTenantId, defaultTenantId } from '@logto/schemas';
|
||||||
|
import { appendPath, GlobalValues } from '@logto/shared';
|
||||||
|
import type { CommonQueryMethods } from 'slonik';
|
||||||
|
import { sql } from 'slonik';
|
||||||
|
|
||||||
|
import { log } from '../../../utils.js';
|
||||||
|
|
||||||
|
export const appendAdminConsoleRedirectUris = async (pool: CommonQueryMethods) => {
|
||||||
|
const redirectUris = new GlobalValues().cloudUrlSet
|
||||||
|
.deduplicated()
|
||||||
|
.map((endpoint) => appendPath(endpoint, defaultTenantId, 'callback'));
|
||||||
|
|
||||||
|
const metadataKey = sql.identifier(['oidc_client_metadata']);
|
||||||
|
|
||||||
|
// Copied from packages/cloud/src/queries/tenants.ts
|
||||||
|
// Can be merged into the original once we remove slonik
|
||||||
|
await pool.query(sql`
|
||||||
|
update applications
|
||||||
|
set ${metadataKey} = jsonb_set(
|
||||||
|
${metadataKey},
|
||||||
|
'{redirectUris}',
|
||||||
|
(select jsonb_agg(distinct value) from jsonb_array_elements(
|
||||||
|
${metadataKey}->'redirectUris' || ${sql.jsonb(redirectUris.map(String))}
|
||||||
|
))
|
||||||
|
)
|
||||||
|
where id = ${adminConsoleApplicationId}
|
||||||
|
and tenant_id = ${adminTenantId}
|
||||||
|
`);
|
||||||
|
|
||||||
|
log.succeed('Appended initial Redirect URIs to Admin Console:', redirectUris.map(String));
|
||||||
|
};
|
|
@ -7,38 +7,33 @@ import { doesConfigsTableExist } from '../../../queries/logto-config.js';
|
||||||
import { log, oraPromise } from '../../../utils.js';
|
import { log, oraPromise } from '../../../utils.js';
|
||||||
import { getLatestAlterationTimestamp } from '../alteration/index.js';
|
import { getLatestAlterationTimestamp } from '../alteration/index.js';
|
||||||
import { getAlterationDirectory } from '../alteration/utils.js';
|
import { getAlterationDirectory } from '../alteration/utils.js';
|
||||||
import { createTables, seedTables } from './tables.js';
|
import { createTables, seedCloud, seedTables } from './tables.js';
|
||||||
|
|
||||||
const seedChoices = Object.freeze(['all', 'oidc'] as const);
|
export const seedByPool = async (pool: DatabasePool, cloud = false) => {
|
||||||
|
|
||||||
type SeedChoice = (typeof seedChoices)[number];
|
|
||||||
|
|
||||||
export const seedByPool = async (pool: DatabasePool, type: SeedChoice) => {
|
|
||||||
await pool.transaction(async (connection) => {
|
await pool.transaction(async (connection) => {
|
||||||
if (type !== 'oidc') {
|
// Check alteration scripts available in order to insert correct timestamp
|
||||||
// Check alteration scripts available in order to insert correct timestamp
|
const latestTimestamp = await getLatestAlterationTimestamp();
|
||||||
const latestTimestamp = await getLatestAlterationTimestamp();
|
|
||||||
|
|
||||||
if (latestTimestamp < 1) {
|
if (latestTimestamp < 1) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`No alteration script found when seeding the database.\n` +
|
`No alteration script found when seeding the database.\n` +
|
||||||
`Please check \`${getAlterationDirectory()}\` to see if there are alteration scripts available.\n`
|
`Please check \`${getAlterationDirectory()}\` to see if there are alteration scripts available.\n`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await oraPromise(createTables(connection), {
|
await oraPromise(createTables(connection), {
|
||||||
text: 'Create tables',
|
text: 'Create tables',
|
||||||
prefixText: chalk.blue('[info]'),
|
prefixText: chalk.blue('[info]'),
|
||||||
});
|
});
|
||||||
await oraPromise(seedTables(connection, latestTimestamp), {
|
await seedTables(connection, latestTimestamp);
|
||||||
text: 'Seed data',
|
|
||||||
prefixText: chalk.blue('[info]'),
|
if (cloud) {
|
||||||
});
|
await seedCloud(connection);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const seed: CommandModule<Record<string, unknown>, { type: string; swe?: boolean }> = {
|
const seed: CommandModule<Record<string, unknown>, { swe?: boolean; cloud?: boolean }> = {
|
||||||
command: 'seed [type]',
|
command: 'seed [type]',
|
||||||
describe: 'Create database then seed tables and data',
|
describe: 'Create database then seed tables and data',
|
||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
|
@ -48,13 +43,12 @@ const seed: CommandModule<Record<string, unknown>, { type: string; swe?: boolean
|
||||||
alias: 'skip-when-exists',
|
alias: 'skip-when-exists',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
})
|
})
|
||||||
.positional('type', {
|
.option('cloud', {
|
||||||
describe: 'Optional seed type',
|
describe: 'Seed additional cloud data',
|
||||||
type: 'string',
|
type: 'boolean',
|
||||||
choices: seedChoices,
|
hidden: true,
|
||||||
default: 'all',
|
|
||||||
}),
|
}),
|
||||||
handler: async ({ type, swe }) => {
|
handler: async ({ swe, cloud }) => {
|
||||||
const pool = await createPoolAndDatabaseIfNeeded();
|
const pool = await createPoolAndDatabaseIfNeeded();
|
||||||
|
|
||||||
if (swe && (await doesConfigsTableExist(pool))) {
|
if (swe && (await doesConfigsTableExist(pool))) {
|
||||||
|
@ -65,10 +59,7 @@ const seed: CommandModule<Record<string, unknown>, { type: string; swe?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Cannot avoid `as` since the official type definition of `yargs` doesn't work.
|
await seedByPool(pool, cloud);
|
||||||
// The value of `type` can be ensured, so it's safe to use `as` here.
|
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
|
||||||
await seedByPool(pool, type as SeedChoice);
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
console.log();
|
console.log();
|
||||||
|
|
|
@ -21,7 +21,8 @@ import { raw } from 'slonik-sql-tag-raw';
|
||||||
import { insertInto } from '../../../database.js';
|
import { insertInto } from '../../../database.js';
|
||||||
import { getDatabaseName } from '../../../queries/database.js';
|
import { getDatabaseName } from '../../../queries/database.js';
|
||||||
import { updateDatabaseTimestamp } from '../../../queries/system.js';
|
import { updateDatabaseTimestamp } from '../../../queries/system.js';
|
||||||
import { getPathInModule } from '../../../utils.js';
|
import { getPathInModule, log } from '../../../utils.js';
|
||||||
|
import { appendAdminConsoleRedirectUris } from './cloud.js';
|
||||||
import { seedOidcConfigs } from './oidc-config.js';
|
import { seedOidcConfigs } from './oidc-config.js';
|
||||||
import { createTenant, seedAdminData } from './tenant.js';
|
import { createTenant, seedAdminData } from './tenant.js';
|
||||||
|
|
||||||
|
@ -136,4 +137,10 @@ export const seedTables = async (
|
||||||
connection.query(insertInto(createDefaultAdminConsoleApplication(), 'applications')),
|
connection.query(insertInto(createDefaultAdminConsoleApplication(), 'applications')),
|
||||||
updateDatabaseTimestamp(connection, latestTimestamp),
|
updateDatabaseTimestamp(connection, latestTimestamp),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
log.succeed('Seed data');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const seedCloud = async (connection: DatabaseTransactionConnection) => {
|
||||||
|
await appendAdminConsoleRedirectUris(connection);
|
||||||
};
|
};
|
||||||
|
|
|
@ -136,7 +136,7 @@ export const decompress = async (toPath: string, tarPath: string) => {
|
||||||
export const seedDatabase = async (instancePath: string) => {
|
export const seedDatabase = async (instancePath: string) => {
|
||||||
try {
|
try {
|
||||||
const pool = await createPoolAndDatabaseIfNeeded();
|
const pool = await createPoolAndDatabaseIfNeeded();
|
||||||
await seedByPool(pool, 'all');
|
await seedByPool(pool);
|
||||||
await pool.end();
|
await pool.end();
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
|
@ -6,9 +6,10 @@
|
||||||
],
|
],
|
||||||
"watch": [
|
"watch": [
|
||||||
"./src/",
|
"./src/",
|
||||||
|
"../core/src/",
|
||||||
"./node_modules/",
|
"./node_modules/",
|
||||||
"../../.env"
|
"../../.env"
|
||||||
],
|
],
|
||||||
"ext": "json,js,jsx,ts,tsx",
|
"ext": "json,js,jsx,ts,tsx",
|
||||||
"delay": 500
|
"delay": 1000
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
"chalk": "^5.0.0",
|
"chalk": "^5.0.0",
|
||||||
"decamelize": "^6.0.0",
|
"decamelize": "^6.0.0",
|
||||||
"dotenv": "^16.0.0",
|
"dotenv": "^16.0.0",
|
||||||
|
"fetch-retry": "^5.0.4",
|
||||||
"find-up": "^6.3.0",
|
"find-up": "^6.3.0",
|
||||||
"http-proxy": "^1.18.1",
|
"http-proxy": "^1.18.1",
|
||||||
"jose": "^4.11.0",
|
"jose": "^4.11.0",
|
||||||
|
|
|
@ -5,6 +5,7 @@ import path from 'node:path/posix';
|
||||||
import { tryThat } from '@logto/shared';
|
import { tryThat } from '@logto/shared';
|
||||||
import type { NextFunction, RequestContext } from '@withtyped/server';
|
import type { NextFunction, RequestContext } from '@withtyped/server';
|
||||||
import { RequestError } from '@withtyped/server';
|
import { RequestError } from '@withtyped/server';
|
||||||
|
import fetchRetry from 'fetch-retry';
|
||||||
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
@ -46,9 +47,13 @@ export default function withAuth<InputContext extends RequestContext>({
|
||||||
audience,
|
audience,
|
||||||
scopes: expectScopes = [],
|
scopes: expectScopes = [],
|
||||||
}: WithAuthConfig) {
|
}: WithAuthConfig) {
|
||||||
|
const fetch = fetchRetry(global.fetch);
|
||||||
const getJwkSet = (async () => {
|
const getJwkSet = (async () => {
|
||||||
const fetched = await fetch(
|
const fetched = await fetch(
|
||||||
new URL(path.join(endpoint.pathname, 'oidc/.well-known/openid-configuration'), endpoint)
|
new Request(
|
||||||
|
new URL(path.join(endpoint.pathname, 'oidc/.well-known/openid-configuration'), endpoint)
|
||||||
|
),
|
||||||
|
{ retries: 5, retryDelay: (attempt) => 2 ** attempt * 1000 }
|
||||||
);
|
);
|
||||||
const { jwks_uri: jwksUri, issuer } = z
|
const { jwks_uri: jwksUri, issuer } = z
|
||||||
.object({ jwks_uri: z.string(), issuer: z.string() })
|
.object({ jwks_uri: z.string(), issuer: z.string() })
|
||||||
|
|
|
@ -81,7 +81,9 @@ export const createTenantsQueries = (client: Queryable<PostgreSql>) => {
|
||||||
set ${metadataKey} = jsonb_set(
|
set ${metadataKey} = jsonb_set(
|
||||||
${metadataKey},
|
${metadataKey},
|
||||||
'{redirectUris}',
|
'{redirectUris}',
|
||||||
${metadataKey}->'redirectUris' || ${jsonb(urls.map(String))}
|
(select jsonb_agg(distinct value) from jsonb_array_elements(
|
||||||
|
${metadataKey}->'redirectUris' || ${jsonb(urls.map(String))}
|
||||||
|
))
|
||||||
)
|
)
|
||||||
where id = ${adminConsoleApplicationId}
|
where id = ${adminConsoleApplicationId}
|
||||||
and tenant_id = ${adminTenantId}
|
and tenant_id = ${adminTenantId}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { GlobalValues, appendPath } from '@logto/shared';
|
||||||
import type { Optional } from '@silverhand/essentials';
|
import type { Optional } from '@silverhand/essentials';
|
||||||
import type { PostgreSql } from '@withtyped/postgres';
|
import type { PostgreSql } from '@withtyped/postgres';
|
||||||
import type { QueryClient } from '@withtyped/server';
|
import type { QueryClient } from '@withtyped/server';
|
||||||
|
@ -5,9 +6,7 @@ import type { DatabasePool } from 'slonik';
|
||||||
|
|
||||||
import { createLogtoConfigLibrary } from '#src/libraries/logto-config.js';
|
import { createLogtoConfigLibrary } from '#src/libraries/logto-config.js';
|
||||||
import { createLogtoConfigQueries } from '#src/queries/logto-config.js';
|
import { createLogtoConfigQueries } from '#src/queries/logto-config.js';
|
||||||
import { appendPath } from '#src/utils/url.js';
|
|
||||||
|
|
||||||
import GlobalValues from './GlobalValues.js';
|
|
||||||
import createPool from './create-pool.js';
|
import createPool from './create-pool.js';
|
||||||
import createQueryClient from './create-query-client.js';
|
import createQueryClient from './create-query-client.js';
|
||||||
import loadOidcValues from './oidc.js';
|
import loadOidcValues from './oidc.js';
|
||||||
|
|
|
@ -1,28 +1,5 @@
|
||||||
import chalk from 'chalk';
|
|
||||||
|
|
||||||
export const throwNotLoadedError = () => {
|
export const throwNotLoadedError = () => {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'The env set is not loaded. Make sure to call `await envSet.load()` before using it.'
|
'The env set is not loaded. Make sure to call `await envSet.load()` before using it.'
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const throwErrorWithDsnMessage = (error: unknown) => {
|
|
||||||
const key = 'DB_URL';
|
|
||||||
|
|
||||||
if (error instanceof Error && error.message === `env variable ${key} not found`) {
|
|
||||||
console.error(
|
|
||||||
`${chalk.red('[error]')} No Postgres DSN (${chalk.green(key)}) found in env variables.\n\n` +
|
|
||||||
` Either provide it in your env, or add it to the ${chalk.blue(
|
|
||||||
'.env'
|
|
||||||
)} file in the Logto project root.\n\n` +
|
|
||||||
` If you want to set up a new Logto database, run ${chalk.green(
|
|
||||||
'npm run cli db seed'
|
|
||||||
)} before setting env ${chalk.green(key)}.\n\n` +
|
|
||||||
` Visit ${chalk.blue(
|
|
||||||
'https://docs.logto.io/docs/references/core/configuration'
|
|
||||||
)} for more info about setting up env.\n`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import { adminTenantId } from '@logto/schemas';
|
import { adminTenantId } from '@logto/schemas';
|
||||||
|
import type { GlobalValues } from '@logto/shared';
|
||||||
import type { Optional } from '@silverhand/essentials';
|
import type { Optional } from '@silverhand/essentials';
|
||||||
import { deduplicate, trySafe } from '@silverhand/essentials';
|
import { deduplicate, trySafe } from '@silverhand/essentials';
|
||||||
|
|
||||||
import type GlobalValues from './GlobalValues.js';
|
|
||||||
|
|
||||||
export const getTenantEndpoint = (
|
export const getTenantEndpoint = (
|
||||||
id: string,
|
id: string,
|
||||||
{ urlSet, adminUrlSet, isDomainBasedMultiTenancy, isPathBasedMultiTenancy }: GlobalValues
|
{ urlSet, adminUrlSet, isDomainBasedMultiTenancy, isPathBasedMultiTenancy }: GlobalValues
|
||||||
|
|
|
@ -7,13 +7,12 @@ import {
|
||||||
LogtoOidcConfigKey,
|
LogtoOidcConfigKey,
|
||||||
LogtoConfigs,
|
LogtoConfigs,
|
||||||
} from '@logto/schemas';
|
} from '@logto/schemas';
|
||||||
import { convertToIdentifiers } from '@logto/shared';
|
import { convertToIdentifiers, appendPath } from '@logto/shared';
|
||||||
import type { JWK } from 'jose';
|
import type { JWK } from 'jose';
|
||||||
import { sql } from 'slonik';
|
import { sql } from 'slonik';
|
||||||
|
|
||||||
import { EnvSet, getTenantEndpoint } from '#src/env-set/index.js';
|
import { EnvSet, getTenantEndpoint } from '#src/env-set/index.js';
|
||||||
import { exportJWK } from '#src/utils/jwks.js';
|
import { exportJWK } from '#src/utils/jwks.js';
|
||||||
import { appendPath } from '#src/utils/url.js';
|
|
||||||
|
|
||||||
const { table, fields } = convertToIdentifiers(LogtoConfigs);
|
const { table, fields } = convertToIdentifiers(LogtoConfigs);
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
|
import { GlobalValues, UrlSet } from '@logto/shared';
|
||||||
import { createMockUtils } from '@logto/shared/esm';
|
import { createMockUtils } from '@logto/shared/esm';
|
||||||
import type { RequestMethod } from 'node-mocks-http';
|
import type { RequestMethod } from 'node-mocks-http';
|
||||||
|
|
||||||
import GlobalValues from '#src/env-set/GlobalValues.js';
|
|
||||||
import UrlSet from '#src/env-set/UrlSet.js';
|
|
||||||
import createMockContext from '#src/test-utils/jest-koa-mocks/create-mock-context.js';
|
import createMockContext from '#src/test-utils/jest-koa-mocks/create-mock-context.js';
|
||||||
|
|
||||||
const { jest } = import.meta;
|
const { jest } = import.meta;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import cors from '@koa/cors';
|
import cors from '@koa/cors';
|
||||||
|
import type { UrlSet } from '@logto/shared';
|
||||||
import type { MiddlewareType } from 'koa';
|
import type { MiddlewareType } from 'koa';
|
||||||
|
|
||||||
import type UrlSet from '#src/env-set/UrlSet.js';
|
|
||||||
import { EnvSet } from '#src/env-set/index.js';
|
import { EnvSet } from '#src/env-set/index.js';
|
||||||
|
|
||||||
export default function koaCors<StateT, ContextT, ResponseBodyT>(
|
export default function koaCors<StateT, ContextT, ResponseBodyT>(
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
|
import { appendPath } from '@logto/shared';
|
||||||
import type { MiddlewareType } from 'koa';
|
import type { MiddlewareType } from 'koa';
|
||||||
import type { IRouterParamContext } from 'koa-router';
|
import type { IRouterParamContext } from 'koa-router';
|
||||||
import type Provider from 'oidc-provider';
|
import type Provider from 'oidc-provider';
|
||||||
|
|
||||||
import { EnvSet } from '#src/env-set/index.js';
|
import { EnvSet } from '#src/env-set/index.js';
|
||||||
import { appendPath } from '#src/utils/url.js';
|
|
||||||
|
|
||||||
// Need To Align With UI
|
// Need To Align With UI
|
||||||
export const sessionNotFoundPath = '/unknown-session';
|
export const sessionNotFoundPath = '/unknown-session';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type { CreateApplication } from '@logto/schemas';
|
import type { CreateApplication } from '@logto/schemas';
|
||||||
import { ApplicationType, adminConsoleApplicationId, demoAppApplicationId } from '@logto/schemas';
|
import { ApplicationType, adminConsoleApplicationId, demoAppApplicationId } from '@logto/schemas';
|
||||||
import { tryThat } from '@logto/shared';
|
import { tryThat, appendPath } from '@logto/shared';
|
||||||
import { addSeconds } from 'date-fns';
|
import { addSeconds } from 'date-fns';
|
||||||
import type { AdapterFactory, AllClientMetadata } from 'oidc-provider';
|
import type { AdapterFactory, AllClientMetadata } from 'oidc-provider';
|
||||||
import { errors } from 'oidc-provider';
|
import { errors } from 'oidc-provider';
|
||||||
|
@ -9,7 +9,6 @@ import snakecaseKeys from 'snakecase-keys';
|
||||||
import { EnvSet } from '#src/env-set/index.js';
|
import { EnvSet } from '#src/env-set/index.js';
|
||||||
import { getTenantUrls } from '#src/env-set/utils.js';
|
import { getTenantUrls } from '#src/env-set/utils.js';
|
||||||
import type Queries from '#src/tenants/Queries.js';
|
import type Queries from '#src/tenants/Queries.js';
|
||||||
import { appendPath } from '#src/utils/url.js';
|
|
||||||
|
|
||||||
import { getConstantClientMetadata } from './utils.js';
|
import { getConstantClientMetadata } from './utils.js';
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { adminTenantId, defaultTenantId } from '@logto/schemas';
|
import { adminTenantId, defaultTenantId } from '@logto/schemas';
|
||||||
|
import { GlobalValues } from '@logto/shared';
|
||||||
import { createMockUtils } from '@logto/shared/esm';
|
import { createMockUtils } from '@logto/shared/esm';
|
||||||
|
|
||||||
import GlobalValues from '#src/env-set/GlobalValues.js';
|
|
||||||
|
|
||||||
const { jest } = import.meta;
|
const { jest } = import.meta;
|
||||||
|
|
||||||
const { mockEsmWithActual } = createMockUtils(jest);
|
const { mockEsmWithActual } = createMockUtils(jest);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { adminTenantId, defaultTenantId } from '@logto/schemas';
|
import { adminTenantId, defaultTenantId } from '@logto/schemas';
|
||||||
|
import type { UrlSet } from '@logto/shared';
|
||||||
import { conditionalString } from '@silverhand/essentials';
|
import { conditionalString } from '@silverhand/essentials';
|
||||||
|
|
||||||
import type UrlSet from '#src/env-set/UrlSet.js';
|
|
||||||
import { EnvSet, getTenantEndpoint } from '#src/env-set/index.js';
|
import { EnvSet, getTenantEndpoint } from '#src/env-set/index.js';
|
||||||
|
|
||||||
const normalizePathname = (pathname: string) =>
|
const normalizePathname = (pathname: string) =>
|
||||||
|
|
|
@ -15,6 +15,11 @@ const addApiData = async (pool: CommonQueryMethods) => {
|
||||||
resourceId: generateStandardId(),
|
resourceId: generateStandardId(),
|
||||||
scopeId: generateStandardId(),
|
scopeId: generateStandardId(),
|
||||||
};
|
};
|
||||||
|
const adminRole = {
|
||||||
|
id: generateStandardId(),
|
||||||
|
name: 'admin:admin',
|
||||||
|
description: 'Admin role for Logto.',
|
||||||
|
};
|
||||||
|
|
||||||
await pool.query(sql`
|
await pool.query(sql`
|
||||||
insert into resources (tenant_id, id, indicator, name)
|
insert into resources (tenant_id, id, indicator, name)
|
||||||
|
@ -27,7 +32,7 @@ const addApiData = async (pool: CommonQueryMethods) => {
|
||||||
${adminTenantId},
|
${adminTenantId},
|
||||||
${cloudApi.resourceId},
|
${cloudApi.resourceId},
|
||||||
'https://cloud.logto.io/api',
|
'https://cloud.logto.io/api',
|
||||||
'Logto Management API for tenant admin'
|
'Logto Cloud API'
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
await pool.query(sql`
|
await pool.query(sql`
|
||||||
|
@ -37,17 +42,26 @@ const addApiData = async (pool: CommonQueryMethods) => {
|
||||||
${adminApi.scopeId},
|
${adminApi.scopeId},
|
||||||
'all',
|
'all',
|
||||||
'Default scope for Management API, allows all permissions.',
|
'Default scope for Management API, allows all permissions.',
|
||||||
${adminApi.scopeId}
|
${adminApi.resourceId}
|
||||||
), (
|
), (
|
||||||
${adminTenantId},
|
${adminTenantId},
|
||||||
${cloudApi.scopeId},
|
${cloudApi.scopeId},
|
||||||
'create:tenant',
|
'create:tenant',
|
||||||
'Allow creating new tenants.',
|
'Allow creating new tenants.',
|
||||||
${cloudApi.scopeId}
|
${cloudApi.resourceId}
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
await pool.query(sql`
|
||||||
|
insert into roles (tenant_id, id, name, description)
|
||||||
|
values (
|
||||||
|
${adminTenantId},
|
||||||
|
${adminRole.id},
|
||||||
|
${adminRole.name},
|
||||||
|
${adminRole.description}
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const { id: roleId } = await pool.one<{ id: string }>(sql`
|
const { id: userRoleId } = await pool.one<{ id: string }>(sql`
|
||||||
select id from roles
|
select id from roles
|
||||||
where tenant_id = ${adminTenantId}
|
where tenant_id = ${adminTenantId}
|
||||||
and name = 'user'
|
and name = 'user'
|
||||||
|
@ -58,13 +72,13 @@ const addApiData = async (pool: CommonQueryMethods) => {
|
||||||
values (
|
values (
|
||||||
${adminTenantId},
|
${adminTenantId},
|
||||||
${generateStandardId()},
|
${generateStandardId()},
|
||||||
${roleId},
|
${userRoleId},
|
||||||
${adminApi.scopeId}
|
${cloudApi.scopeId}
|
||||||
), (
|
), (
|
||||||
${adminTenantId},
|
${adminTenantId},
|
||||||
${generateStandardId()},
|
${generateStandardId()},
|
||||||
${roleId},
|
${adminRole.id},
|
||||||
${cloudApi.scopeId}
|
${adminApi.scopeId}
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
};
|
};
|
||||||
|
@ -93,9 +107,19 @@ const alteration: AlterationScript = {
|
||||||
},
|
},
|
||||||
down: async (pool) => {
|
down: async (pool) => {
|
||||||
await pool.query(sql`
|
await pool.query(sql`
|
||||||
delete from applications
|
delete from resources
|
||||||
where tenant_id = 'admin'
|
where tenant_id = ${adminTenantId}
|
||||||
and id = 'admin-console';
|
and indicator in ('https://admin.logto.app/api', 'https://cloud.logto.io/api');
|
||||||
|
`);
|
||||||
|
await pool.query(sql`
|
||||||
|
delete from roles
|
||||||
|
where tenant_id = ${adminTenantId}
|
||||||
|
and name = 'admin:admin';
|
||||||
|
`);
|
||||||
|
await pool.query(sql`
|
||||||
|
delete from logto_configs
|
||||||
|
where tenant_id = ${adminTenantId}
|
||||||
|
and key = 'adminConsole';
|
||||||
`);
|
`);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -57,6 +57,7 @@
|
||||||
"@logto/core-kit": "workspace:*",
|
"@logto/core-kit": "workspace:*",
|
||||||
"@logto/schemas": "workspace:*",
|
"@logto/schemas": "workspace:*",
|
||||||
"@silverhand/essentials": "2.2.0",
|
"@silverhand/essentials": "2.2.0",
|
||||||
|
"chalk": "^5.0.0",
|
||||||
"find-up": "^6.3.0",
|
"find-up": "^6.3.0",
|
||||||
"nanoid": "^4.0.0",
|
"nanoid": "^4.0.0",
|
||||||
"slonik": "^30.0.0"
|
"slonik": "^30.0.0"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { tryThat } from '@logto/shared';
|
|
||||||
import { assertEnv, getEnv, getEnvAsStringArray, yes } from '@silverhand/essentials';
|
import { assertEnv, getEnv, getEnvAsStringArray, yes } from '@silverhand/essentials';
|
||||||
|
|
||||||
|
import { tryThat } from '../utils/index.js';
|
||||||
import UrlSet from './UrlSet.js';
|
import UrlSet from './UrlSet.js';
|
||||||
import { throwErrorWithDsnMessage } from './throw-errors.js';
|
import { throwErrorWithDsnMessage } from './throw-errors.js';
|
||||||
|
|
28
packages/shared/src/env/throw-errors.ts
vendored
Normal file
28
packages/shared/src/env/throw-errors.ts
vendored
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import chalk from 'chalk';
|
||||||
|
|
||||||
|
export const throwNotLoadedError = () => {
|
||||||
|
throw new Error(
|
||||||
|
'The env set is not loaded. Make sure to call `await envSet.load()` before using it.'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const throwErrorWithDsnMessage = (error: unknown) => {
|
||||||
|
const key = 'DB_URL';
|
||||||
|
|
||||||
|
if (error instanceof Error && error.message === `env variable ${key} not found`) {
|
||||||
|
console.error(
|
||||||
|
`${chalk.red('[error]')} No Postgres DSN (${chalk.green(key)}) found in env variables.\n\n` +
|
||||||
|
` Either provide it in your env, or add it to the ${chalk.blue(
|
||||||
|
'.env'
|
||||||
|
)} file in the Logto project root.\n\n` +
|
||||||
|
` If you want to set up a new Logto database, run ${chalk.green(
|
||||||
|
'npm run cli db seed'
|
||||||
|
)} before setting env ${chalk.green(key)}.\n\n` +
|
||||||
|
` Visit ${chalk.blue(
|
||||||
|
'https://docs.logto.io/docs/references/core/configuration'
|
||||||
|
)} for more info about setting up env.\n`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
};
|
|
@ -1,3 +1,5 @@
|
||||||
export * from './database/index.js';
|
export * from './database/index.js';
|
||||||
export * from './utils/index.js';
|
export * from './utils/index.js';
|
||||||
export * from './models/index.js';
|
export * from './models/index.js';
|
||||||
|
export { default as UrlSet } from './env/UrlSet.js';
|
||||||
|
export { default as GlobalValues } from './env/GlobalValues.js';
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
export * from './function.js';
|
export * from './function.js';
|
||||||
export * from './object.js';
|
export * from './object.js';
|
||||||
export { default as findPackage } from './find-package.js';
|
export { default as findPackage } from './find-package.js';
|
||||||
|
export * from './url.js';
|
||||||
|
|
|
@ -122,6 +122,7 @@ importers:
|
||||||
decamelize: ^6.0.0
|
decamelize: ^6.0.0
|
||||||
dotenv: ^16.0.0
|
dotenv: ^16.0.0
|
||||||
eslint: ^8.21.0
|
eslint: ^8.21.0
|
||||||
|
fetch-retry: ^5.0.4
|
||||||
find-up: ^6.3.0
|
find-up: ^6.3.0
|
||||||
http-proxy: ^1.18.1
|
http-proxy: ^1.18.1
|
||||||
jose: ^4.11.0
|
jose: ^4.11.0
|
||||||
|
@ -142,6 +143,7 @@ importers:
|
||||||
chalk: 5.1.2
|
chalk: 5.1.2
|
||||||
decamelize: 6.0.0
|
decamelize: 6.0.0
|
||||||
dotenv: 16.0.0
|
dotenv: 16.0.0
|
||||||
|
fetch-retry: 5.0.4
|
||||||
find-up: 6.3.0
|
find-up: 6.3.0
|
||||||
http-proxy: 1.18.1
|
http-proxy: 1.18.1
|
||||||
jose: 4.11.1
|
jose: 4.11.1
|
||||||
|
@ -702,6 +704,7 @@ importers:
|
||||||
'@silverhand/ts-config': 2.0.3
|
'@silverhand/ts-config': 2.0.3
|
||||||
'@types/jest': ^29.1.2
|
'@types/jest': ^29.1.2
|
||||||
'@types/node': ^18.11.18
|
'@types/node': ^18.11.18
|
||||||
|
chalk: ^5.0.0
|
||||||
eslint: ^8.34.0
|
eslint: ^8.34.0
|
||||||
find-up: ^6.3.0
|
find-up: ^6.3.0
|
||||||
jest: ^29.1.2
|
jest: ^29.1.2
|
||||||
|
@ -714,6 +717,7 @@ importers:
|
||||||
'@logto/core-kit': link:../toolkit/core-kit
|
'@logto/core-kit': link:../toolkit/core-kit
|
||||||
'@logto/schemas': link:../schemas
|
'@logto/schemas': link:../schemas
|
||||||
'@silverhand/essentials': 2.2.0
|
'@silverhand/essentials': 2.2.0
|
||||||
|
chalk: 5.1.2
|
||||||
find-up: 6.3.0
|
find-up: 6.3.0
|
||||||
nanoid: 4.0.0
|
nanoid: 4.0.0
|
||||||
slonik: 30.1.2
|
slonik: 30.1.2
|
||||||
|
@ -7125,6 +7129,10 @@ packages:
|
||||||
web-streams-polyfill: 3.2.1
|
web-streams-polyfill: 3.2.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/fetch-retry/5.0.4:
|
||||||
|
resolution: {integrity: sha512-LXcdgpdcVedccGg0AZqg+S8lX/FCdwXD92WNZ5k5qsb0irRhSFsBOpcJt7oevyqT2/C2nEE0zSFNdBEpj3YOSw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/figures/5.0.0:
|
/figures/5.0.0:
|
||||||
resolution: {integrity: sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==}
|
resolution: {integrity: sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
Loading…
Reference in a new issue