mirror of
https://github.com/logto-io/logto.git
synced 2025-04-07 23:01:25 -05:00
refactor!: add admin tenant
This commit is contained in:
parent
1d7f22debf
commit
2af6fd114a
42 changed files with 496 additions and 337 deletions
|
@ -1,5 +1,10 @@
|
|||
import type { LogtoConfigKey } from '@logto/schemas';
|
||||
import { LogtoOidcConfigKey, logtoConfigGuards, logtoConfigKeys } from '@logto/schemas';
|
||||
import {
|
||||
defaultTenantId,
|
||||
LogtoOidcConfigKey,
|
||||
logtoConfigGuards,
|
||||
logtoConfigKeys,
|
||||
} from '@logto/schemas';
|
||||
import { deduplicate, noop } from '@silverhand/essentials';
|
||||
import chalk from 'chalk';
|
||||
import type { CommandModule } from 'yargs';
|
||||
|
@ -45,7 +50,7 @@ const validateRotateKey: ValidateRotateKeyFunction = (key) => {
|
|||
}
|
||||
};
|
||||
|
||||
const getConfig: CommandModule<unknown, { key: string; keys: string[] }> = {
|
||||
const getConfig: CommandModule<unknown, { key: string; keys: string[]; tenantId: string }> = {
|
||||
command: 'get <key> [keys...]',
|
||||
describe: 'Get config value(s) of the given key(s) in Logto database',
|
||||
builder: (yargs) =>
|
||||
|
@ -60,13 +65,18 @@ const getConfig: CommandModule<unknown, { key: string; keys: string[] }> = {
|
|||
type: 'string',
|
||||
array: true,
|
||||
default: [],
|
||||
})
|
||||
.option('tenantId', {
|
||||
describe: 'The tenant to operate',
|
||||
type: 'string',
|
||||
default: defaultTenantId,
|
||||
}),
|
||||
handler: async ({ key, keys }) => {
|
||||
handler: async ({ key, keys, tenantId }) => {
|
||||
const queryKeys = deduplicate([key, ...keys]);
|
||||
validateKeys(queryKeys);
|
||||
|
||||
const pool = await createPoolFromConfig();
|
||||
const { rows } = await getRowsByKeys(pool, queryKeys);
|
||||
const { rows } = await getRowsByKeys(pool, tenantId, queryKeys);
|
||||
await pool.end();
|
||||
|
||||
console.log(
|
||||
|
@ -85,7 +95,7 @@ const getConfig: CommandModule<unknown, { key: string; keys: string[] }> = {
|
|||
},
|
||||
};
|
||||
|
||||
const setConfig: CommandModule<unknown, { key: string; value: string }> = {
|
||||
const setConfig: CommandModule<unknown, { key: string; value: string; tenantId: string }> = {
|
||||
command: 'set <key> <value>',
|
||||
describe: 'Set config value of the given key in Logto database',
|
||||
builder: (yargs) =>
|
||||
|
@ -99,35 +109,46 @@ const setConfig: CommandModule<unknown, { key: string; value: string }> = {
|
|||
describe: 'The value to set, should be a valid JSON string',
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
})
|
||||
.option('tenantId', {
|
||||
describe: 'The tenant to operate',
|
||||
type: 'string',
|
||||
default: defaultTenantId,
|
||||
}),
|
||||
handler: async ({ key, value }) => {
|
||||
handler: async ({ key, value, tenantId }) => {
|
||||
validateKeys(key);
|
||||
|
||||
const guarded = logtoConfigGuards[key].parse(JSON.parse(value));
|
||||
|
||||
const pool = await createPoolFromConfig();
|
||||
await updateValueByKey(pool, key, guarded);
|
||||
await updateValueByKey(pool, tenantId, key, guarded);
|
||||
await pool.end();
|
||||
|
||||
log.info(`Update ${chalk.green(key)} succeeded`);
|
||||
},
|
||||
};
|
||||
|
||||
const rotateConfig: CommandModule<unknown, { key: string }> = {
|
||||
const rotateConfig: CommandModule<unknown, { key: string; tenantId: string }> = {
|
||||
command: 'rotate <key>',
|
||||
describe:
|
||||
'Generate a new private or secret key for the given config key and prepend to the key array',
|
||||
builder: (yargs) =>
|
||||
yargs.positional('key', {
|
||||
describe: `The key to rotate, one of ${chalk.green(validRotateKeys.join(', '))}`,
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
}),
|
||||
handler: async ({ key }) => {
|
||||
yargs
|
||||
.positional('key', {
|
||||
describe: `The key to rotate, one of ${chalk.green(validRotateKeys.join(', '))}`,
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
})
|
||||
.option('tenantId', {
|
||||
describe: 'The tenant to operate',
|
||||
type: 'string',
|
||||
default: defaultTenantId,
|
||||
}),
|
||||
handler: async ({ key, tenantId }) => {
|
||||
validateRotateKey(key);
|
||||
|
||||
const pool = await createPoolFromConfig();
|
||||
const { rows } = await getRowsByKeys(pool, [key]);
|
||||
const { rows } = await getRowsByKeys(pool, tenantId, [key]);
|
||||
|
||||
if (!rows[0]) {
|
||||
log.warn('No key found, create a new one');
|
||||
|
@ -147,14 +168,14 @@ const rotateConfig: CommandModule<unknown, { key: string }> = {
|
|||
}
|
||||
};
|
||||
const rotated = await getValue();
|
||||
await updateValueByKey(pool, key, rotated);
|
||||
await updateValueByKey(pool, tenantId, key, rotated);
|
||||
await pool.end();
|
||||
|
||||
log.info(`Rotate ${chalk.green(key)} succeeded, now it has ${rotated.length} keys`);
|
||||
},
|
||||
};
|
||||
|
||||
const trimConfig: CommandModule<unknown, { key: string; length: number }> = {
|
||||
const trimConfig: CommandModule<unknown, { key: string; length: number; tenantId: string }> = {
|
||||
command: 'trim <key> [length]',
|
||||
describe: 'Remove the last [length] number of private or secret keys for the given config key',
|
||||
builder: (yargs) =>
|
||||
|
@ -169,8 +190,13 @@ const trimConfig: CommandModule<unknown, { key: string; length: number }> = {
|
|||
type: 'number',
|
||||
default: 1,
|
||||
demandOption: true,
|
||||
})
|
||||
.option('tenantId', {
|
||||
describe: 'The tenant to operate',
|
||||
type: 'string',
|
||||
default: defaultTenantId,
|
||||
}),
|
||||
handler: async ({ key, length }) => {
|
||||
handler: async ({ key, length, tenantId }) => {
|
||||
validateRotateKey(key);
|
||||
|
||||
if (length < 1) {
|
||||
|
@ -178,7 +204,7 @@ const trimConfig: CommandModule<unknown, { key: string; length: number }> = {
|
|||
}
|
||||
|
||||
const pool = await createPoolFromConfig();
|
||||
const { rows } = await getRowsByKeys(pool, [key]);
|
||||
const { rows } = await getRowsByKeys(pool, tenantId, [key]);
|
||||
|
||||
if (!rows[0]) {
|
||||
log.warn('No key found, create a new one');
|
||||
|
@ -195,7 +221,7 @@ const trimConfig: CommandModule<unknown, { key: string; length: number }> = {
|
|||
return value.slice(0, -length);
|
||||
};
|
||||
const trimmed = await getValue();
|
||||
await updateValueByKey(pool, key, trimmed);
|
||||
await updateValueByKey(pool, tenantId, key, trimmed);
|
||||
await pool.end();
|
||||
|
||||
log.info(`Trim ${chalk.green(key)} succeeded, now it has ${trimmed.length} keys`);
|
||||
|
|
|
@ -1,68 +1,14 @@
|
|||
import { logtoConfigGuards, LogtoOidcConfigKey } from '@logto/schemas';
|
||||
import chalk from 'chalk';
|
||||
import type { DatabasePool, DatabaseTransactionConnection } from 'slonik';
|
||||
import type { DatabasePool } from 'slonik';
|
||||
import type { CommandModule } from 'yargs';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { createPoolAndDatabaseIfNeeded } from '../../../database.js';
|
||||
import {
|
||||
getRowsByKeys,
|
||||
doesConfigsTableExist,
|
||||
updateValueByKey,
|
||||
} from '../../../queries/logto-config.js';
|
||||
import { doesConfigsTableExist } from '../../../queries/logto-config.js';
|
||||
import { log, oraPromise } from '../../../utilities.js';
|
||||
import { getLatestAlterationTimestamp } from '../alteration/index.js';
|
||||
import { getAlterationDirectory } from '../alteration/utils.js';
|
||||
import { oidcConfigReaders } from './oidc-config.js';
|
||||
import { createTables, seedTables } from './tables.js';
|
||||
|
||||
const seedOidcConfigs = async (pool: DatabaseTransactionConnection) => {
|
||||
const configGuard = z.object({
|
||||
key: z.nativeEnum(LogtoOidcConfigKey),
|
||||
value: z.unknown(),
|
||||
});
|
||||
const { rows } = await getRowsByKeys(pool, Object.values(LogtoOidcConfigKey));
|
||||
// Filter out valid keys that hold a valid value
|
||||
const result = await Promise.all(
|
||||
rows.map<Promise<LogtoOidcConfigKey | undefined>>(async (row) => {
|
||||
try {
|
||||
const { key, value } = await configGuard.parseAsync(row);
|
||||
await logtoConfigGuards[key].parseAsync(value);
|
||||
|
||||
return key;
|
||||
} catch {}
|
||||
})
|
||||
);
|
||||
const existingKeys = new Set(result.filter(Boolean));
|
||||
|
||||
const validOptions = Object.values(LogtoOidcConfigKey).filter((key) => {
|
||||
const included = existingKeys.has(key);
|
||||
|
||||
if (included) {
|
||||
log.info(`Key ${chalk.green(key)} exists, skipping`);
|
||||
}
|
||||
|
||||
return !included;
|
||||
});
|
||||
|
||||
// The awaits in loop is intended since we'd like to log info in sequence
|
||||
/* eslint-disable no-await-in-loop */
|
||||
for (const key of validOptions) {
|
||||
const { value, fromEnv } = await oidcConfigReaders[key]();
|
||||
|
||||
if (fromEnv) {
|
||||
log.info(`Read config ${chalk.green(key)} from env`);
|
||||
} else {
|
||||
log.info(`Generated config ${chalk.green(key)}`);
|
||||
}
|
||||
|
||||
await updateValueByKey(pool, key, value);
|
||||
}
|
||||
/* eslint-enable no-await-in-loop */
|
||||
|
||||
log.succeed('Seed OIDC config');
|
||||
};
|
||||
|
||||
const seedChoices = Object.freeze(['all', 'oidc'] as const);
|
||||
|
||||
type SeedChoice = typeof seedChoices[number];
|
||||
|
@ -89,8 +35,6 @@ export const seedByPool = async (pool: DatabasePool, type: SeedChoice) => {
|
|||
prefixText: chalk.blue('[info]'),
|
||||
});
|
||||
}
|
||||
|
||||
await seedOidcConfigs(connection);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -1,13 +1,66 @@
|
|||
import { readFile } from 'fs/promises';
|
||||
|
||||
import type { LogtoOidcConfigType } from '@logto/schemas';
|
||||
import { LogtoOidcConfigKey } from '@logto/schemas';
|
||||
import { LogtoOidcConfigKey, logtoConfigGuards } from '@logto/schemas';
|
||||
import { getEnvAsStringArray } from '@silverhand/essentials';
|
||||
import chalk from 'chalk';
|
||||
import type { DatabaseTransactionConnection } from 'slonik';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getRowsByKeys, updateValueByKey } from '../../../queries/logto-config.js';
|
||||
import { log } from '../../../utilities.js';
|
||||
import { generateOidcCookieKey, generateOidcPrivateKey } from '../utilities.js';
|
||||
|
||||
const isBase64FormatPrivateKey = (key: string) => !key.includes('-');
|
||||
|
||||
export const seedOidcConfigs = async (pool: DatabaseTransactionConnection, tenantId: string) => {
|
||||
const tenantPrefix = `[${tenantId}]`;
|
||||
const configGuard = z.object({
|
||||
key: z.nativeEnum(LogtoOidcConfigKey),
|
||||
value: z.unknown(),
|
||||
});
|
||||
const { rows } = await getRowsByKeys(pool, tenantId, Object.values(LogtoOidcConfigKey));
|
||||
// Filter out valid keys that hold a valid value
|
||||
const result = await Promise.all(
|
||||
rows.map<Promise<LogtoOidcConfigKey | undefined>>(async (row) => {
|
||||
try {
|
||||
const { key, value } = await configGuard.parseAsync(row);
|
||||
await logtoConfigGuards[key].parseAsync(value);
|
||||
|
||||
return key;
|
||||
} catch {}
|
||||
})
|
||||
);
|
||||
const existingKeys = new Set(result.filter(Boolean));
|
||||
|
||||
const validOptions = Object.values(LogtoOidcConfigKey).filter((key) => {
|
||||
const included = existingKeys.has(key);
|
||||
|
||||
if (included) {
|
||||
log.info(tenantPrefix, `Key ${chalk.green(key)} exists, skipping`);
|
||||
}
|
||||
|
||||
return !included;
|
||||
});
|
||||
|
||||
// The awaits in loop is intended since we'd like to log info in sequence
|
||||
/* eslint-disable no-await-in-loop */
|
||||
for (const key of validOptions) {
|
||||
const { value, fromEnv } = await oidcConfigReaders[key]();
|
||||
|
||||
if (fromEnv) {
|
||||
log.info(tenantPrefix, `Read config ${chalk.green(key)} from env`);
|
||||
} else {
|
||||
log.info(tenantPrefix, `Generated config ${chalk.green(key)}`);
|
||||
}
|
||||
|
||||
await updateValueByKey(pool, tenantId, key, value);
|
||||
}
|
||||
/* eslint-enable no-await-in-loop */
|
||||
|
||||
log.succeed(tenantPrefix, 'Seed OIDC config');
|
||||
};
|
||||
|
||||
/**
|
||||
* Each config reader will do the following things in order:
|
||||
* 1. Try to read value from env (mimic the behavior from the original core)
|
||||
|
|
|
@ -3,14 +3,13 @@ import path from 'path';
|
|||
|
||||
import { generateStandardId } from '@logto/core-kit';
|
||||
import {
|
||||
managementResource,
|
||||
defaultSignInExperience,
|
||||
createDefaultAdminConsoleConfig,
|
||||
createDemoAppApplication,
|
||||
defaultRole,
|
||||
managementResourceScope,
|
||||
defaultRoleScopeRelation,
|
||||
defaultTenantId,
|
||||
adminTenantId,
|
||||
defaultManagementApi,
|
||||
createManagementApiInAdminTenant,
|
||||
} from '@logto/schemas';
|
||||
import { Hooks, Tenants } from '@logto/schemas/models';
|
||||
import type { DatabaseTransactionConnection } from 'slonik';
|
||||
|
@ -21,7 +20,8 @@ import { insertInto } from '../../../database.js';
|
|||
import { getDatabaseName } from '../../../queries/database.js';
|
||||
import { updateDatabaseTimestamp } from '../../../queries/system.js';
|
||||
import { getPathInModule } from '../../../utilities.js';
|
||||
import { createTenant } from './tenant.js';
|
||||
import { seedOidcConfigs } from './oidc-config.js';
|
||||
import { createTenant, seedAdminData } from './tenant.js';
|
||||
|
||||
const getExplicitOrder = (query: string) => {
|
||||
const matched = /\/\*\s*init_order\s*=\s*([\d.]+)\s*\*\//.exec(query)?.[1];
|
||||
|
@ -114,14 +114,17 @@ export const seedTables = async (
|
|||
latestTimestamp: number
|
||||
) => {
|
||||
await createTenant(connection, defaultTenantId);
|
||||
await seedOidcConfigs(connection, defaultTenantId);
|
||||
await seedAdminData(connection, defaultManagementApi);
|
||||
|
||||
await createTenant(connection, adminTenantId);
|
||||
await seedOidcConfigs(connection, adminTenantId);
|
||||
await seedAdminData(connection, createManagementApiInAdminTenant(defaultTenantId));
|
||||
|
||||
await Promise.all([
|
||||
connection.query(insertInto(managementResource, 'resources')),
|
||||
connection.query(insertInto(managementResourceScope, 'scopes')),
|
||||
connection.query(insertInto(createDefaultAdminConsoleConfig(), 'logto_configs')),
|
||||
connection.query(insertInto(defaultSignInExperience, 'sign_in_experiences')),
|
||||
connection.query(insertInto(createDemoAppApplication(generateStandardId()), 'applications')),
|
||||
connection.query(insertInto(defaultRole, 'roles')),
|
||||
connection.query(insertInto(defaultRoleScopeRelation, 'roles_scopes')),
|
||||
updateDatabaseTimestamp(connection, latestTimestamp),
|
||||
]);
|
||||
};
|
||||
|
|
|
@ -1,23 +1,49 @@
|
|||
import { generateStandardId } from '@logto/core-kit';
|
||||
import type { TenantModel } from '@logto/schemas';
|
||||
import type { DatabaseTransactionConnection } from 'slonik';
|
||||
import { CreateRolesScope } from '@logto/schemas';
|
||||
import type { TenantModel, AdminData } from '@logto/schemas';
|
||||
import { assert } from '@silverhand/essentials';
|
||||
import type { CommonQueryMethods } from 'slonik';
|
||||
import { sql } from 'slonik';
|
||||
import { raw } from 'slonik-sql-tag-raw';
|
||||
|
||||
import { insertInto } from '../../../database.js';
|
||||
import { getDatabaseName } from '../../../queries/database.js';
|
||||
|
||||
export const createTenant = async (connection: DatabaseTransactionConnection, tenantId: string) => {
|
||||
const database = await getDatabaseName(connection, true);
|
||||
export const createTenant = async (pool: CommonQueryMethods, tenantId: string) => {
|
||||
const database = await getDatabaseName(pool, true);
|
||||
const parentRole = `logto_tenant_${database}`;
|
||||
const role = `logto_tenant_${database}_${tenantId}`;
|
||||
const password = generateStandardId(32);
|
||||
const tenantModel: TenantModel = { id: tenantId, dbUser: role, dbUserPassword: password };
|
||||
|
||||
await connection.query(insertInto(tenantModel, 'tenants'));
|
||||
await connection.query(sql`
|
||||
await pool.query(insertInto(tenantModel, 'tenants'));
|
||||
await pool.query(sql`
|
||||
create role ${sql.identifier([role])} with inherit login
|
||||
password '${raw(password)}'
|
||||
in role ${sql.identifier([parentRole])};
|
||||
`);
|
||||
};
|
||||
|
||||
export const seedAdminData = async (pool: CommonQueryMethods, data: AdminData) => {
|
||||
const { resource, scope, role } = data;
|
||||
|
||||
assert(
|
||||
resource.tenantId === scope.tenantId && scope.tenantId === role.tenantId,
|
||||
new Error('All data should have the same tenant ID')
|
||||
);
|
||||
|
||||
await pool.query(insertInto(resource, 'resources'));
|
||||
await pool.query(insertInto(scope, 'scopes'));
|
||||
await pool.query(insertInto(role, 'roles'));
|
||||
await pool.query(
|
||||
insertInto(
|
||||
{
|
||||
id: generateStandardId(),
|
||||
roleId: role.id,
|
||||
scopeId: scope.id,
|
||||
tenantId: resource.tenantId,
|
||||
} satisfies CreateRolesScope,
|
||||
'roles_scopes'
|
||||
)
|
||||
);
|
||||
};
|
||||
|
|
|
@ -16,21 +16,27 @@ export const doesConfigsTableExist = async (pool: CommonQueryMethods) => {
|
|||
return Boolean(rows[0]?.regclass);
|
||||
};
|
||||
|
||||
export const getRowsByKeys = async (pool: CommonQueryMethods, keys: LogtoConfigKey[]) =>
|
||||
export const getRowsByKeys = async (
|
||||
pool: CommonQueryMethods,
|
||||
tenantId: string,
|
||||
keys: LogtoConfigKey[]
|
||||
) =>
|
||||
pool.query<LogtoConfig>(sql`
|
||||
select ${sql.join([fields.key, fields.value], sql`,`)} from ${table}
|
||||
where ${fields.key} in (${sql.join(keys, sql`,`)})
|
||||
where ${fields.tenantId} = ${tenantId}
|
||||
and ${fields.key} in (${sql.join(keys, sql`,`)})
|
||||
`);
|
||||
|
||||
export const updateValueByKey = async <T extends LogtoConfigKey>(
|
||||
pool: CommonQueryMethods,
|
||||
tenantId: string,
|
||||
key: T,
|
||||
value: z.infer<typeof logtoConfigGuards[T]>
|
||||
) =>
|
||||
pool.query(
|
||||
sql`
|
||||
insert into ${table} (${fields.key}, ${fields.value})
|
||||
values (${key}, ${sql.jsonb(value)})
|
||||
insert into ${table} (${fields.tenantId}, ${fields.key}, ${fields.value})
|
||||
values (${tenantId}, ${key}, ${sql.jsonb(value)})
|
||||
on conflict (${fields.tenantId}, ${fields.key})
|
||||
do update set ${fields.value}=excluded.${fields.value}
|
||||
`
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
import { UserScope } from '@logto/core-kit';
|
||||
import { LogtoProvider } from '@logto/react';
|
||||
import {
|
||||
adminConsoleApplicationId,
|
||||
managementResource,
|
||||
managementResourceScope,
|
||||
} from '@logto/schemas';
|
||||
import { adminConsoleApplicationId } from '@logto/schemas';
|
||||
import { useContext } from 'react';
|
||||
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
|
||||
import { SWRConfig } from 'swr';
|
||||
|
||||
|
@ -14,7 +11,9 @@ import './scss/overlayscrollbars.scss';
|
|||
|
||||
// eslint-disable-next-line import/no-unassigned-import
|
||||
import '@fontsource/roboto-mono';
|
||||
import AppLoading from '@/components/AppLoading';
|
||||
import Toast from '@/components/Toast';
|
||||
import { managementApi } from '@/consts/management-api';
|
||||
import AppBoundary from '@/containers/AppBoundary';
|
||||
import AppLayout from '@/containers/AppLayout';
|
||||
import ErrorBoundary from '@/containers/ErrorBoundary';
|
||||
|
@ -48,6 +47,7 @@ import {
|
|||
UserDetailsTabs,
|
||||
} from './consts/page-tabs';
|
||||
import AppContent from './containers/AppContent';
|
||||
import AppEndpointProvider, { AppEndpointContext } from './containers/AppEndpointProvider';
|
||||
import ApiResourcePermissions from './pages/ApiResourceDetails/ApiResourcePermissions';
|
||||
import ApiResourceSettings from './pages/ApiResourceDetails/ApiResourceSettings';
|
||||
import CloudPreview from './pages/CloudPreview';
|
||||
|
@ -63,6 +63,11 @@ void initI18n();
|
|||
|
||||
const Main = () => {
|
||||
const swrOptions = useSwrOptions();
|
||||
const { endpoint } = useContext(AppEndpointContext);
|
||||
|
||||
if (!endpoint) {
|
||||
return <AppLoading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
|
@ -154,16 +159,18 @@ const Main = () => {
|
|||
|
||||
const App = () => (
|
||||
<BrowserRouter basename={getBasename('console', '5002')}>
|
||||
<LogtoProvider
|
||||
config={{
|
||||
endpoint: window.location.origin,
|
||||
appId: adminConsoleApplicationId,
|
||||
resources: [managementResource.indicator],
|
||||
scopes: [UserScope.Identities, UserScope.CustomData, managementResourceScope.name],
|
||||
}}
|
||||
>
|
||||
<Main />
|
||||
</LogtoProvider>
|
||||
<AppEndpointProvider>
|
||||
<LogtoProvider
|
||||
config={{
|
||||
endpoint: window.location.origin,
|
||||
appId: adminConsoleApplicationId,
|
||||
resources: [managementApi.indicator],
|
||||
scopes: [UserScope.Identities, UserScope.CustomData, managementApi.scopeAll],
|
||||
}}
|
||||
>
|
||||
<Main />
|
||||
</LogtoProvider>
|
||||
</AppEndpointProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
export * from './applications';
|
||||
export * from './connectors';
|
||||
export * from './logs';
|
||||
export * from './management-api';
|
||||
|
||||
export const themeStorageKey = 'logto:admin_console:theme';
|
||||
export const requestTimeout = 20_000;
|
||||
|
|
10
packages/console/src/consts/management-api.ts
Normal file
10
packages/console/src/consts/management-api.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import {
|
||||
defaultTenantId,
|
||||
getManagementApiResourceIndicator,
|
||||
managementApiScopeAll,
|
||||
} from '@logto/schemas';
|
||||
|
||||
export const managementApi = Object.freeze({
|
||||
indicator: getManagementApiResourceIndicator(defaultTenantId),
|
||||
scopeAll: managementApiScopeAll,
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
import ky from 'ky';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useMemo, useEffect, createContext, useState } from 'react';
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const AppEndpointContext = createContext<{ endpoint?: URL }>({});
|
||||
|
||||
const AppEndpointProvider = ({ children }: Props) => {
|
||||
const [endpoint, setEndpoint] = useState<URL>();
|
||||
const memorizedContext = useMemo(() => ({ endpoint }), [endpoint]);
|
||||
|
||||
useEffect(() => {
|
||||
const getEndpoint = async () => {
|
||||
const { app } = await ky
|
||||
.get(new URL('api/.well-known/endpoints', window.location.origin))
|
||||
.json<{ app: string }>();
|
||||
setEndpoint(new URL(app));
|
||||
};
|
||||
|
||||
void getEndpoint();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AppEndpointContext.Provider value={memorizedContext}>{children}</AppEndpointContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppEndpointProvider;
|
|
@ -1,12 +1,12 @@
|
|||
import { useLogto } from '@logto/react';
|
||||
import type { RequestErrorBody } from '@logto/schemas';
|
||||
import { managementResource } from '@logto/schemas';
|
||||
import ky from 'ky';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useContext, useMemo } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { requestTimeout } from '@/consts';
|
||||
import { managementApi, requestTimeout } from '@/consts';
|
||||
import { AppEndpointContext } from '@/containers/AppEndpointProvider';
|
||||
|
||||
export class RequestError extends Error {
|
||||
status: number;
|
||||
|
@ -24,6 +24,7 @@ type Props = {
|
|||
};
|
||||
|
||||
const useApi = ({ hideErrorToast }: Props = {}) => {
|
||||
const { endpoint } = useContext(AppEndpointContext);
|
||||
const { isAuthenticated, getAccessToken } = useLogto();
|
||||
const { t, i18n } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
|
@ -44,7 +45,7 @@ const useApi = ({ hideErrorToast }: Props = {}) => {
|
|||
const api = useMemo(
|
||||
() =>
|
||||
ky.create({
|
||||
prefixUrl: window.location.origin,
|
||||
prefixUrl: endpoint,
|
||||
timeout: requestTimeout,
|
||||
hooks: {
|
||||
beforeError: hideErrorToast
|
||||
|
@ -59,7 +60,7 @@ const useApi = ({ hideErrorToast }: Props = {}) => {
|
|||
beforeRequest: [
|
||||
async (request) => {
|
||||
if (isAuthenticated) {
|
||||
const accessToken = await getAccessToken(managementResource.indicator);
|
||||
const accessToken = await getAccessToken(managementApi.indicator);
|
||||
request.headers.set('Authorization', `Bearer ${accessToken ?? ''}`);
|
||||
request.headers.set('Accept-Language', i18n.language);
|
||||
}
|
||||
|
@ -67,7 +68,7 @@ const useApi = ({ hideErrorToast }: Props = {}) => {
|
|||
],
|
||||
},
|
||||
}),
|
||||
[hideErrorToast, toastError, isAuthenticated, getAccessToken, i18n.language]
|
||||
[endpoint, hideErrorToast, toastError, isAuthenticated, getAccessToken, i18n.language]
|
||||
);
|
||||
|
||||
return api;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { Resource } from '@logto/schemas';
|
||||
import { AppearanceMode, managementResource } from '@logto/schemas';
|
||||
import { defaultManagementApi, AppearanceMode } from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
@ -39,7 +39,7 @@ const ApiResourceDetails = () => {
|
|||
const Icon = theme === AppearanceMode.LightMode ? ApiResource : ApiResourceDark;
|
||||
|
||||
const isOnPermissionPage = pathname.endsWith(ApiResourceDetailsTabs.Permissions);
|
||||
const isLogtoManagementApiResource = data?.id === managementResource.id;
|
||||
const isLogtoManagementApiResource = data?.indicator === defaultManagementApi.resource.indicator;
|
||||
|
||||
const [isDeleteFormOpen, setIsDeleteFormOpen] = useState(false);
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
"test:report": "codecov -F core"
|
||||
},
|
||||
"dependencies": {
|
||||
"@koa/cors": "^4.0.0",
|
||||
"@logto/cli": "workspace:*",
|
||||
"@logto/connector-kit": "workspace:*",
|
||||
"@logto/core-kit": "workspace:*",
|
||||
|
@ -84,6 +85,7 @@
|
|||
"@types/koa-logger": "^3.1.1",
|
||||
"@types/koa-mount": "^4.0.0",
|
||||
"@types/koa-send": "^4.1.3",
|
||||
"@types/koa__cors": "^3.3.0",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/oidc-provider": "^8.0.0",
|
||||
"@types/semver": "^7.3.12",
|
||||
|
|
|
@ -6,7 +6,8 @@ import chalk from 'chalk';
|
|||
import type Koa from 'koa';
|
||||
|
||||
import { EnvSet } from '#src/env-set/index.js';
|
||||
import { defaultTenant, tenantPool } from '#src/tenants/index.js';
|
||||
import { tenantPool } from '#src/tenants/index.js';
|
||||
import { getTenantId } from '#src/utils/tenant.js';
|
||||
|
||||
const logListening = (type: 'core' | 'admin' = 'core') => {
|
||||
const urlSet = type === 'core' ? EnvSet.values.urlSet : EnvSet.values.adminUrlSet;
|
||||
|
@ -16,24 +17,9 @@ const logListening = (type: 'core' | 'admin' = 'core') => {
|
|||
}
|
||||
};
|
||||
|
||||
const getTenantId = () => {
|
||||
const { isDomainBasedMultiTenancy, isProduction, isIntegrationTest, developmentTenantId } =
|
||||
EnvSet.values;
|
||||
|
||||
if (!isDomainBasedMultiTenancy) {
|
||||
if ((!isProduction || isIntegrationTest) && developmentTenantId) {
|
||||
return developmentTenantId;
|
||||
}
|
||||
|
||||
return defaultTenant;
|
||||
}
|
||||
|
||||
throw new Error('Not implemented');
|
||||
};
|
||||
|
||||
export default async function initApp(app: Koa): Promise<void> {
|
||||
app.use(async (ctx, next) => {
|
||||
const tenantId = getTenantId();
|
||||
const tenantId = getTenantId(ctx.URL);
|
||||
|
||||
if (!tenantId) {
|
||||
ctx.status = 404;
|
||||
|
|
|
@ -4,6 +4,7 @@ import type { QueryClient } from '@withtyped/server';
|
|||
import type { DatabasePool } from 'slonik';
|
||||
|
||||
import { createLogtoConfigLibrary } from '#src/libraries/logto-config.js';
|
||||
import { createLogtoConfigQueries } from '#src/queries/logto-config.js';
|
||||
import { appendPath } from '#src/utils/url.js';
|
||||
|
||||
import GlobalValues from './GlobalValues.js';
|
||||
|
@ -82,7 +83,7 @@ export class EnvSet {
|
|||
this.#pool = pool;
|
||||
this.#queryClient = createQueryClient(this.databaseUrl, EnvSet.isTest);
|
||||
|
||||
const { getOidcConfigs } = createLogtoConfigLibrary(pool);
|
||||
const { getOidcConfigs } = createLogtoConfigLibrary(createLogtoConfigQueries(pool));
|
||||
|
||||
const oidcConfigs = await getOidcConfigs();
|
||||
this.#oidc = await loadOidcValues(
|
||||
|
|
|
@ -8,10 +8,10 @@ export const createApplicationLibrary = (queries: Queries) => {
|
|||
const {
|
||||
applicationsRoles: { findApplicationsRolesByApplicationId },
|
||||
rolesScopes: { findRolesScopesByRoleIds },
|
||||
scopes: { findScopesByIdsAndResourceId },
|
||||
scopes: { findScopesByIdsAndResourceIndicator },
|
||||
} = queries;
|
||||
|
||||
const findApplicationScopesForResourceId = async (
|
||||
const findApplicationScopesForResourceIndicator = async (
|
||||
applicationId: string,
|
||||
resourceId: string
|
||||
): Promise<readonly Scope[]> => {
|
||||
|
@ -19,7 +19,7 @@ export const createApplicationLibrary = (queries: Queries) => {
|
|||
const rolesScopes = await findRolesScopesByRoleIds(
|
||||
applicationsRoles.map(({ roleId }) => roleId)
|
||||
);
|
||||
const scopes = await findScopesByIdsAndResourceId(
|
||||
const scopes = await findScopesByIdsAndResourceIndicator(
|
||||
rolesScopes.map(({ scopeId }) => scopeId),
|
||||
resourceId
|
||||
);
|
||||
|
@ -28,6 +28,6 @@ export const createApplicationLibrary = (queries: Queries) => {
|
|||
};
|
||||
|
||||
return {
|
||||
findApplicationScopesForResourceId,
|
||||
findApplicationScopesForResourceIndicator,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { getRowsByKeys } from '@logto/cli/lib/queries/logto-config.js';
|
||||
import type { LogtoOidcConfigType } from '@logto/schemas';
|
||||
import { logtoOidcConfigGuard, LogtoOidcConfigKey } from '@logto/schemas';
|
||||
import chalk from 'chalk';
|
||||
import type { CommonQueryMethods } from 'slonik';
|
||||
import { z, ZodError } from 'zod';
|
||||
|
||||
export const createLogtoConfigLibrary = (pool: CommonQueryMethods) => {
|
||||
import type Queries from '#src/tenants/Queries.js';
|
||||
|
||||
export const createLogtoConfigLibrary = ({ getRowsByKeys }: Queries['logtoConfigs']) => {
|
||||
const getOidcConfigs = async (): Promise<LogtoOidcConfigType> => {
|
||||
try {
|
||||
const { rows } = await getRowsByKeys(pool, Object.values(LogtoOidcConfigKey));
|
||||
const { rows } = await getRowsByKeys(Object.values(LogtoOidcConfigKey));
|
||||
|
||||
return z
|
||||
.object(logtoOidcConfigGuard)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { builtInLanguages } from '@logto/phrases-ui';
|
||||
import type { Branding, LanguageInfo, SignInExperience } from '@logto/schemas';
|
||||
import {
|
||||
defaultTenantId,
|
||||
adminTenantId,
|
||||
SignInMode,
|
||||
ConnectorType,
|
||||
BrandingStyle,
|
||||
|
@ -72,26 +72,27 @@ export const createSignInExperienceLibrary = (
|
|||
const getSignInExperienceForApplication = async (
|
||||
applicationId?: string
|
||||
): Promise<SignInExperience & { notification?: string }> => {
|
||||
const signInExperience = await findDefaultSignInExperience();
|
||||
|
||||
// Hard code Admin Console sign-in methods settings.
|
||||
if (applicationId === adminConsoleApplicationId) {
|
||||
return {
|
||||
// If we need to hard code, it implies Logto is running in the single-tenant mode;
|
||||
// Thus we can hard code Tenant ID as well.
|
||||
tenantId: defaultTenantId,
|
||||
...adminConsoleSignInExperience,
|
||||
tenantId: adminTenantId,
|
||||
branding: {
|
||||
...adminConsoleSignInExperience.branding,
|
||||
slogan: i18next.t('admin_console.welcome.title'),
|
||||
},
|
||||
termsOfUseUrl: signInExperience.termsOfUseUrl,
|
||||
languageInfo: signInExperience.languageInfo,
|
||||
termsOfUseUrl: null,
|
||||
languageInfo: {
|
||||
autoDetect: true,
|
||||
fallbackLanguage: 'en',
|
||||
},
|
||||
signInMode: (await hasActiveUsers()) ? SignInMode.SignIn : SignInMode.Register,
|
||||
socialSignInConnectorTargets: [],
|
||||
};
|
||||
}
|
||||
|
||||
const signInExperience = await findDefaultSignInExperience();
|
||||
|
||||
// Insert Demo App Notification
|
||||
if (applicationId === demoAppApplicationId) {
|
||||
const { socialSignInConnectorTargets } = signInExperience;
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { UsersPasswordEncryptionMethod } from '@logto/schemas';
|
||||
import { createMockPool } from 'slonik';
|
||||
|
||||
import { mockResource, mockRole, mockScope } from '#src/__mocks__/index.js';
|
||||
import { mockUser } from '#src/__mocks__/user.js';
|
||||
|
@ -7,17 +6,13 @@ import { MockQueries } from '#src/test-utils/tenant.js';
|
|||
|
||||
const { jest } = import.meta;
|
||||
|
||||
const pool = createMockPool({
|
||||
query: jest.fn(),
|
||||
});
|
||||
|
||||
const { encryptUserPassword, createUserLibrary } = await import('./user.js');
|
||||
|
||||
const hasUserWithId = jest.fn();
|
||||
const queries = new MockQueries({
|
||||
users: { hasUserWithId },
|
||||
roles: { findRolesByRoleIds: async () => [mockRole] },
|
||||
scopes: { findScopesByIdsAndResourceId: async () => [mockScope] },
|
||||
scopes: { findScopesByIdsAndResourceIndicator: async () => [mockScope] },
|
||||
usersRoles: { findUsersRolesByUserId: async () => [] },
|
||||
rolesScopes: { findRolesScopesByRoleIds: async () => [] },
|
||||
});
|
||||
|
@ -73,12 +68,12 @@ describe('encryptUserPassword()', () => {
|
|||
});
|
||||
|
||||
describe('findUserScopesForResourceId()', () => {
|
||||
const { findUserScopesForResourceId } = createUserLibrary(queries);
|
||||
const { findUserScopesForResourceIndicator } = createUserLibrary(queries);
|
||||
|
||||
it('returns scopes that the user has access', async () => {
|
||||
await expect(findUserScopesForResourceId(mockUser.id, mockResource.id)).resolves.toEqual([
|
||||
mockScope,
|
||||
]);
|
||||
await expect(
|
||||
findUserScopesForResourceIndicator(mockUser.id, mockResource.indicator)
|
||||
).resolves.toEqual([mockScope]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { buildIdGenerator, generateStandardId } from '@logto/core-kit';
|
||||
import type { User, CreateUser, Scope } from '@logto/schemas';
|
||||
import { Users, UsersPasswordEncryptionMethod, defaultRole } from '@logto/schemas';
|
||||
import { Users, UsersPasswordEncryptionMethod } from '@logto/schemas';
|
||||
import type { OmitAutoSetFields } from '@logto/shared';
|
||||
import type { Nullable } from '@silverhand/essentials';
|
||||
import { deduplicate } from '@silverhand/essentials';
|
||||
|
@ -15,7 +15,6 @@ import assertThat from '#src/utils/assert-that.js';
|
|||
import { encryptPassword } from '#src/utils/password.js';
|
||||
|
||||
const userId = buildIdGenerator(12);
|
||||
const roleId = buildIdGenerator(21);
|
||||
|
||||
export const encryptUserPassword = async (
|
||||
password: string
|
||||
|
@ -51,18 +50,11 @@ export type UserLibrary = ReturnType<typeof createUserLibrary>;
|
|||
export const createUserLibrary = (queries: Queries) => {
|
||||
const {
|
||||
pool,
|
||||
roles: { findRolesByRoleNames, insertRoles, findRoleByRoleName, findRolesByRoleIds },
|
||||
users: {
|
||||
hasUser,
|
||||
hasUserWithEmail,
|
||||
hasUserWithId,
|
||||
hasUserWithPhone,
|
||||
findUsersByIds,
|
||||
findUserById,
|
||||
},
|
||||
roles: { findRolesByRoleNames, findRoleByRoleName, findRolesByRoleIds },
|
||||
users: { hasUser, hasUserWithEmail, hasUserWithId, hasUserWithPhone, findUsersByIds },
|
||||
usersRoles: { insertUsersRoles, findUsersRolesByRoleId, findUsersRolesByUserId },
|
||||
rolesScopes: { findRolesScopesByRoleIds },
|
||||
scopes: { findScopesByIdsAndResourceId },
|
||||
scopes: { findScopesByIdsAndResourceIndicator },
|
||||
} = queries;
|
||||
|
||||
const generateUserId = async (retries = 500) =>
|
||||
|
@ -83,11 +75,8 @@ export const createUserLibrary = (queries: Queries) => {
|
|||
returning: true,
|
||||
});
|
||||
|
||||
const insertUser = async (data: OmitAutoSetFields<CreateUser>, isAdmin = false) => {
|
||||
const roleNames = deduplicate([
|
||||
...EnvSet.values.userDefaultRoleNames,
|
||||
...(isAdmin ? [defaultRole.name] : []),
|
||||
]);
|
||||
const insertUser = async (data: OmitAutoSetFields<CreateUser>, additionalRoleNames: string[]) => {
|
||||
const roleNames = deduplicate([...EnvSet.values.userDefaultRoleNames, ...additionalRoleNames]);
|
||||
const roles = await findRolesByRoleNames(roleNames);
|
||||
|
||||
assertThat(roles.length === roleNames.length, 'role.default_role_missing');
|
||||
|
@ -142,15 +131,16 @@ export const createUserLibrary = (queries: Queries) => {
|
|||
return findUsersByIds(usersRoles.map(({ userId }) => userId));
|
||||
};
|
||||
|
||||
const findUserScopesForResourceId = async (
|
||||
const findUserScopesForResourceIndicator = async (
|
||||
userId: string,
|
||||
resourceId: string
|
||||
resourceIndicator: string
|
||||
): Promise<readonly Scope[]> => {
|
||||
const usersRoles = await findUsersRolesByUserId(userId);
|
||||
const rolesScopes = await findRolesScopesByRoleIds(usersRoles.map(({ roleId }) => roleId));
|
||||
const scopes = await findScopesByIdsAndResourceId(
|
||||
|
||||
const scopes = await findScopesByIdsAndResourceIndicator(
|
||||
rolesScopes.map(({ scopeId }) => scopeId),
|
||||
resourceId
|
||||
resourceIndicator
|
||||
);
|
||||
|
||||
return scopes;
|
||||
|
@ -168,7 +158,7 @@ export const createUserLibrary = (queries: Queries) => {
|
|||
insertUser,
|
||||
checkIdentifierCollision,
|
||||
findUsersByRoleName,
|
||||
findUserScopesForResourceId,
|
||||
findUserScopesForResourceIndicator,
|
||||
findUserRoles,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { managementResourceScope, UserRole } from '@logto/schemas';
|
||||
import { defaultManagementApi } from '@logto/schemas';
|
||||
import { createMockUtils, pickDefault } from '@logto/shared/esm';
|
||||
import type { Context } from 'koa';
|
||||
import type { IRouterParamContext } from 'koa-router';
|
||||
|
@ -17,7 +17,7 @@ const { mockEsm } = createMockUtils(jest);
|
|||
const { jwtVerify } = mockEsm('jose', () => ({
|
||||
jwtVerify: jest
|
||||
.fn()
|
||||
.mockReturnValue({ payload: { sub: 'fooUser', scope: managementResourceScope.name } }),
|
||||
.mockReturnValue({ payload: { sub: 'fooUser', scope: defaultManagementApi.scope.name } }),
|
||||
}));
|
||||
|
||||
const koaAuth = await pickDefault(import('./koa-auth.js'));
|
||||
|
@ -181,9 +181,7 @@ describe('koaAuth middleware', () => {
|
|||
},
|
||||
};
|
||||
|
||||
await expect(koaAuth(mockEnvSet, UserRole.Admin)(ctx, next)).rejects.toMatchError(
|
||||
forbiddenError
|
||||
);
|
||||
await expect(koaAuth(mockEnvSet)(ctx, next)).rejects.toMatchError(forbiddenError);
|
||||
});
|
||||
|
||||
it('expect to throw if jwt scope does not include management resource scope', async () => {
|
||||
|
@ -198,9 +196,7 @@ describe('koaAuth middleware', () => {
|
|||
},
|
||||
};
|
||||
|
||||
await expect(koaAuth(mockEnvSet, UserRole.Admin)(ctx, next)).rejects.toMatchError(
|
||||
forbiddenError
|
||||
);
|
||||
await expect(koaAuth(mockEnvSet)(ctx, next)).rejects.toMatchError(forbiddenError);
|
||||
});
|
||||
|
||||
it('expect to throw unauthorized error if unknown error occurs', async () => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { IncomingHttpHeaders } from 'http';
|
||||
|
||||
import { managementResource, managementResourceScope } from '@logto/schemas';
|
||||
import { defaultManagementApi } from '@logto/schemas';
|
||||
import type { Optional } from '@silverhand/essentials';
|
||||
import { jwtVerify } from 'jose';
|
||||
import type { MiddlewareType, Request } from 'koa';
|
||||
|
@ -54,7 +54,9 @@ export const verifyBearerTokenFromRequest = async (
|
|||
const userId = request.headers['development-user-id']?.toString() ?? developmentUserId;
|
||||
|
||||
if ((!isProduction || isIntegrationTest) && userId) {
|
||||
return { sub: userId, clientId: undefined, scopes: [managementResourceScope.name] };
|
||||
console.log(`Found dev user ID ${userId}, skip token validation.`);
|
||||
|
||||
return { sub: userId, clientId: undefined, scopes: [defaultManagementApi.scope.name] };
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -79,22 +81,19 @@ export const verifyBearerTokenFromRequest = async (
|
|||
};
|
||||
|
||||
export default function koaAuth<StateT, ContextT extends IRouterParamContext, ResponseBodyT>(
|
||||
envSet: EnvSet,
|
||||
forScope?: string
|
||||
envSet: EnvSet
|
||||
): MiddlewareType<StateT, WithAuthContext<ContextT>, ResponseBodyT> {
|
||||
return async (ctx, next) => {
|
||||
const { sub, clientId, scopes } = await verifyBearerTokenFromRequest(
|
||||
envSet,
|
||||
ctx.request,
|
||||
managementResource.indicator
|
||||
defaultManagementApi.resource.indicator
|
||||
);
|
||||
|
||||
if (forScope) {
|
||||
assertThat(
|
||||
scopes.includes(forScope),
|
||||
new RequestError({ code: 'auth.forbidden', status: 403 })
|
||||
);
|
||||
}
|
||||
assertThat(
|
||||
scopes.includes(defaultManagementApi.scope.name),
|
||||
new RequestError({ code: 'auth.forbidden', status: 403 })
|
||||
);
|
||||
|
||||
ctx.auth = {
|
||||
type: sub === clientId ? 'app' : 'user',
|
||||
|
|
|
@ -34,8 +34,8 @@ export default function initOidc(envSet: EnvSet, queries: Queries, libraries: Li
|
|||
resources: { findResourceByIndicator },
|
||||
users: { findUserById },
|
||||
} = queries;
|
||||
const { findUserScopesForResourceId } = libraries.users;
|
||||
const { findApplicationScopesForResourceId } = libraries.applications;
|
||||
const { findUserScopesForResourceIndicator } = libraries.users;
|
||||
const { findApplicationScopesForResourceIndicator } = libraries.applications;
|
||||
const logoutSource = readFileSync('static/html/logout.html', 'utf8');
|
||||
|
||||
const cookieConfig = Object.freeze({
|
||||
|
@ -90,7 +90,7 @@ export default function initOidc(envSet: EnvSet, queries: Queries, libraries: Li
|
|||
throw new errors.InvalidTarget();
|
||||
}
|
||||
|
||||
const { accessTokenTtl: accessTokenTTL, id } = resourceServer;
|
||||
const { accessTokenTtl: accessTokenTTL } = resourceServer;
|
||||
const result = {
|
||||
accessTokenFormat: 'jwt',
|
||||
accessTokenTTL,
|
||||
|
@ -103,7 +103,7 @@ export default function initOidc(envSet: EnvSet, queries: Queries, libraries: Li
|
|||
const userId = ctx.oidc.session?.accountId;
|
||||
|
||||
if (userId) {
|
||||
const scopes = await findUserScopesForResourceId(userId, id);
|
||||
const scopes = await findUserScopesForResourceIndicator(userId, indicator);
|
||||
|
||||
return {
|
||||
...result,
|
||||
|
@ -115,7 +115,7 @@ export default function initOidc(envSet: EnvSet, queries: Queries, libraries: Li
|
|||
|
||||
// Machine to machine app
|
||||
if (clientId) {
|
||||
const scopes = await findApplicationScopesForResourceId(clientId, id);
|
||||
const scopes = await findApplicationScopesForResourceIndicator(clientId, indicator);
|
||||
|
||||
return {
|
||||
...result,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { AdminConsoleData } from '@logto/schemas';
|
||||
import type { AdminConsoleData, LogtoConfig, LogtoConfigKey } from '@logto/schemas';
|
||||
import { AdminConsoleConfigKey, LogtoConfigs } from '@logto/schemas';
|
||||
import { convertToIdentifiers } from '@logto/shared';
|
||||
import type { CommonQueryMethods } from 'slonik';
|
||||
|
@ -21,5 +21,11 @@ export const createLogtoConfigQueries = (pool: CommonQueryMethods) => {
|
|||
returning ${fields.value}
|
||||
`);
|
||||
|
||||
return { getAdminConsoleConfig, updateAdminConsoleConfig };
|
||||
const getRowsByKeys = async (keys: LogtoConfigKey[]) =>
|
||||
pool.query<LogtoConfig>(sql`
|
||||
select ${sql.join([fields.key, fields.value], sql`,`)} from ${table}
|
||||
where ${fields.key} in (${sql.join(keys, sql`,`)})
|
||||
`);
|
||||
|
||||
return { getAdminConsoleConfig, updateAdminConsoleConfig, getRowsByKeys };
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { CreateRole, Role } from '@logto/schemas';
|
||||
import { adminRoleId, SearchJointMode, Roles } from '@logto/schemas';
|
||||
import { defaultManagementApi, SearchJointMode, Roles } from '@logto/schemas';
|
||||
import type { OmitAutoSetFields } from '@logto/shared';
|
||||
import { conditionalArraySql, conditionalSql, convertToIdentifiers } from '@logto/shared';
|
||||
import type { CommonQueryMethods } from 'slonik';
|
||||
|
@ -34,7 +34,7 @@ export const createRolesQueries = (pool: CommonQueryMethods) => {
|
|||
pool.one<{ count: number }>(sql`
|
||||
select count(*)
|
||||
from ${table}
|
||||
where ${fields.id}<>${adminRoleId}
|
||||
where ${fields.id}<>${defaultManagementApi.role.id}
|
||||
${conditionalArraySql(
|
||||
excludeRoleIds,
|
||||
(value) => sql`and ${fields.id} not in (${sql.join(value, sql`, `)})`
|
||||
|
@ -57,7 +57,7 @@ export const createRolesQueries = (pool: CommonQueryMethods) => {
|
|||
sql`
|
||||
select ${sql.join(Object.values(fields), sql`, `)}
|
||||
from ${table}
|
||||
where ${fields.id}<>${adminRoleId}
|
||||
where ${fields.id}<>${defaultManagementApi.role.id}
|
||||
${conditionalArraySql(
|
||||
excludeRoleIds,
|
||||
(value) => sql`and ${fields.id} not in (${sql.join(value, sql`, `)})`
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { CreateScope, Scope } from '@logto/schemas';
|
||||
import { Scopes } from '@logto/schemas';
|
||||
import { Resources, Scopes } from '@logto/schemas';
|
||||
import type { OmitAutoSetFields } from '@logto/shared';
|
||||
import { conditionalSql, convertToIdentifiers } from '@logto/shared';
|
||||
import type { CommonQueryMethods } from 'slonik';
|
||||
|
@ -12,7 +12,8 @@ import { DeletionError } from '#src/errors/SlonikError/index.js';
|
|||
import type { Search } from '#src/utils/search.js';
|
||||
import { buildConditionsFromSearch } from '#src/utils/search.js';
|
||||
|
||||
const { table, fields } = convertToIdentifiers(Scopes);
|
||||
const { table, fields } = convertToIdentifiers(Scopes, true);
|
||||
const resources = convertToIdentifiers(Resources, true);
|
||||
|
||||
const buildResourceConditions = (search: Search) => {
|
||||
const hasSearch = search.matches.length > 0;
|
||||
|
@ -99,16 +100,18 @@ export const createScopeQueries = (pool: CommonQueryMethods) => {
|
|||
`)
|
||||
: [];
|
||||
|
||||
const findScopesByIdsAndResourceId = async (
|
||||
const findScopesByIdsAndResourceIndicator = async (
|
||||
scopeIds: string[],
|
||||
resourceId: string
|
||||
resourceIndicator: string
|
||||
): Promise<readonly Scope[]> =>
|
||||
scopeIds.length > 0
|
||||
? pool.any<Scope>(sql`
|
||||
select ${sql.join(Object.values(fields), sql`, `)}
|
||||
from ${table}
|
||||
inner join ${resources.table}
|
||||
on ${resources.fields.id} = ${fields.resourceId}
|
||||
where ${fields.id} in (${sql.join(scopeIds, sql`, `)})
|
||||
and ${fields.resourceId} = ${resourceId}
|
||||
and ${resources.fields.indicator} = ${resourceIndicator}
|
||||
`)
|
||||
: [];
|
||||
|
||||
|
@ -152,7 +155,7 @@ export const createScopeQueries = (pool: CommonQueryMethods) => {
|
|||
findScopesByResourceId,
|
||||
findScopesByResourceIds,
|
||||
findScopesByIds,
|
||||
findScopesByIdsAndResourceId,
|
||||
findScopesByIdsAndResourceIndicator,
|
||||
insertScope,
|
||||
findScopeById,
|
||||
updateScope,
|
||||
|
|
|
@ -140,12 +140,11 @@ export default function adminUserRoutes<T extends AuthedRouter>(
|
|||
primaryEmail: string().regex(emailRegEx),
|
||||
username: string().regex(usernameRegEx),
|
||||
password: string().regex(passwordRegEx),
|
||||
isAdmin: boolean(),
|
||||
name: string(),
|
||||
}).partial(),
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { primaryEmail, primaryPhone, username, password, name, isAdmin } = ctx.guard.body;
|
||||
const { primaryEmail, primaryPhone, username, password, name } = ctx.guard.body;
|
||||
|
||||
assertThat(
|
||||
!username || !(await hasUser(username)),
|
||||
|
@ -177,7 +176,7 @@ export default function adminUserRoutes<T extends AuthedRouter>(
|
|||
name,
|
||||
...conditional(password && (await encryptUserPassword(password))),
|
||||
},
|
||||
isAdmin
|
||||
[]
|
||||
);
|
||||
|
||||
ctx.body = pick(user, ...userInfoSelectFields);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { generateStandardId, buildIdGenerator } from '@logto/core-kit';
|
||||
import { adminRoleId, Applications } from '@logto/schemas';
|
||||
import { defaultManagementApi, Applications } from '@logto/schemas';
|
||||
import { boolean, object, string } from 'zod';
|
||||
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
|
@ -76,7 +76,7 @@ export default function applicationRoutes<T extends AuthedRouter>(
|
|||
|
||||
ctx.body = {
|
||||
...application,
|
||||
isAdmin: applicationsRoles.some(({ roleId }) => roleId === adminRoleId),
|
||||
isAdmin: applicationsRoles.some(({ roleId }) => roleId === defaultManagementApi.role.id),
|
||||
};
|
||||
|
||||
return next();
|
||||
|
@ -107,14 +107,16 @@ export default function applicationRoutes<T extends AuthedRouter>(
|
|||
// FIXME @sijie temp solution to set admin access to machine to machine app
|
||||
if (isAdmin !== undefined) {
|
||||
const applicationsRoles = await findApplicationsRolesByApplicationId(id);
|
||||
const originalIsAdmin = applicationsRoles.some(({ roleId }) => roleId === adminRoleId);
|
||||
const originalIsAdmin = applicationsRoles.some(
|
||||
({ roleId }) => roleId === defaultManagementApi.role.id
|
||||
);
|
||||
|
||||
if (isAdmin && !originalIsAdmin) {
|
||||
await insertApplicationsRoles([
|
||||
{ id: generateStandardId(), applicationId: id, roleId: adminRoleId },
|
||||
{ id: generateStandardId(), applicationId: id, roleId: defaultManagementApi.role.id },
|
||||
]);
|
||||
} else if (!isAdmin && originalIsAdmin) {
|
||||
await deleteApplicationRole(id, adminRoleId);
|
||||
await deleteApplicationRole(id, defaultManagementApi.role.id);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { managementResourceScope } from '@logto/schemas';
|
||||
import cors from '@koa/cors';
|
||||
import Koa from 'koa';
|
||||
import Router from 'koa-router';
|
||||
|
||||
import { EnvSet } from '#src/env-set/index.js';
|
||||
import type TenantContext from '#src/tenants/TenantContext.js';
|
||||
|
||||
import koaAuth from '../middleware/koa-auth.js';
|
||||
|
@ -33,7 +34,7 @@ const createRouters = (tenant: TenantContext) => {
|
|||
interactionRoutes(interactionRouter, tenant);
|
||||
|
||||
const managementRouter: AuthedRouter = new Router();
|
||||
managementRouter.use(koaAuth(tenant.envSet, managementResourceScope.name));
|
||||
managementRouter.use(koaAuth(tenant.envSet));
|
||||
applicationRoutes(managementRouter, tenant);
|
||||
logtoConfigRoutes(managementRouter, tenant);
|
||||
connectorRoutes(managementRouter, tenant);
|
||||
|
@ -64,6 +65,19 @@ const createRouters = (tenant: TenantContext) => {
|
|||
export default function initRouter(tenant: TenantContext): Koa {
|
||||
const apisApp = new Koa();
|
||||
|
||||
apisApp.use(
|
||||
cors({
|
||||
origin: (ctx) => {
|
||||
const { origin } = ctx.request.headers;
|
||||
|
||||
return origin &&
|
||||
EnvSet.values.adminUrlSet.deduplicated().some((value) => new URL(value).origin === origin)
|
||||
? origin
|
||||
: '';
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
for (const router of createRouters(tenant)) {
|
||||
apisApp.use(router.routes()).use(router.allowedMethods());
|
||||
}
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
import type { User, Profile } from '@logto/schemas';
|
||||
import { InteractionEvent, adminConsoleApplicationId } from '@logto/schemas';
|
||||
import {
|
||||
getManagementApiAdminName,
|
||||
defaultTenantId,
|
||||
adminTenantId,
|
||||
InteractionEvent,
|
||||
adminConsoleApplicationId,
|
||||
} from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
|
||||
import type { ConnectorLibrary } from '#src/libraries/connector.js';
|
||||
|
@ -7,6 +13,7 @@ import { assignInteractionResults } from '#src/libraries/session.js';
|
|||
import { encryptUserPassword } from '#src/libraries/user.js';
|
||||
import type { LogEntry } from '#src/middleware/koa-audit-log.js';
|
||||
import type TenantContext from '#src/tenants/TenantContext.js';
|
||||
import { getTenantId } from '#src/utils/tenant.js';
|
||||
|
||||
import type { WithInteractionDetailsContext } from '../middleware/koa-interaction-details.js';
|
||||
import type {
|
||||
|
@ -157,14 +164,16 @@ export default async function submitInteraction(
|
|||
const { client_id } = ctx.interactionDetails.params;
|
||||
|
||||
const createAdminUser =
|
||||
String(client_id) === adminConsoleApplicationId && !(await hasActiveUsers());
|
||||
getTenantId(ctx.URL) === adminTenantId &&
|
||||
String(client_id) === adminConsoleApplicationId &&
|
||||
!(await hasActiveUsers());
|
||||
|
||||
await insertUser(
|
||||
{
|
||||
id,
|
||||
...upsertProfile,
|
||||
},
|
||||
createAdminUser
|
||||
createAdminUser ? [getManagementApiAdminName(defaultTenantId)] : []
|
||||
);
|
||||
|
||||
await assignInteractionResults(ctx, provider, { login: { accountId: id } });
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import {
|
||||
adminConsoleApplicationId,
|
||||
managementResourceId,
|
||||
managementResourceScope,
|
||||
defaultTenantId,
|
||||
getManagementApiResourceIndicator,
|
||||
managementApiScopeAll,
|
||||
} from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import type Router from 'koa-router';
|
||||
|
@ -35,13 +36,13 @@ export default function consentRoutes<T>(
|
|||
|
||||
// Block non-admin user from consenting to admin console
|
||||
if (String(client_id) === adminConsoleApplicationId) {
|
||||
const scopes = await libraries.users.findUserScopesForResourceId(
|
||||
const scopes = await libraries.users.findUserScopesForResourceIndicator(
|
||||
accountId,
|
||||
managementResourceId
|
||||
getManagementApiResourceIndicator(defaultTenantId)
|
||||
);
|
||||
|
||||
assertThat(
|
||||
scopes.some(({ name }) => name === managementResourceScope.name),
|
||||
scopes.some(({ name }) => name === managementApiScopeAll),
|
||||
new RequestError({ code: 'auth.forbidden', status: 401 })
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { ConnectorType } from '@logto/connector-kit';
|
|||
import { adminConsoleApplicationId } from '@logto/schemas';
|
||||
import etag from 'etag';
|
||||
|
||||
import { EnvSet } from '#src/env-set/index.js';
|
||||
import { getApplicationIdFromInteraction } from '#src/libraries/session.js';
|
||||
|
||||
import type { AnonymousRouter, RouterInitArgs } from './types.js';
|
||||
|
@ -15,6 +16,15 @@ export default function wellKnownRoutes<T extends AnonymousRouter>(
|
|||
connectors: { getLogtoConnectors },
|
||||
} = libraries;
|
||||
|
||||
router.get('/.well-known/endpoints', async (ctx, next) => {
|
||||
ctx.body = {
|
||||
console: EnvSet.values.adminUrlSet.endpoint,
|
||||
app: EnvSet.values.urlSet.endpoint,
|
||||
};
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
router.get(
|
||||
'/.well-known/sign-in-exp',
|
||||
async (ctx, next) => {
|
||||
|
|
33
packages/core/src/utils/tenant.ts
Normal file
33
packages/core/src/utils/tenant.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { adminTenantId, defaultTenantId } from '@logto/schemas';
|
||||
|
||||
import { EnvSet } from '#src/env-set/index.js';
|
||||
|
||||
export const getTenantId = (url: URL) => {
|
||||
const {
|
||||
isDomainBasedMultiTenancy,
|
||||
isProduction,
|
||||
isIntegrationTest,
|
||||
developmentTenantId,
|
||||
urlSet,
|
||||
adminUrlSet,
|
||||
} = EnvSet.values;
|
||||
|
||||
if ((!isProduction || isIntegrationTest) && developmentTenantId) {
|
||||
return developmentTenantId;
|
||||
}
|
||||
|
||||
const urlString = url.toString();
|
||||
|
||||
if (adminUrlSet.deduplicated().some((value) => urlString.startsWith(value))) {
|
||||
return adminTenantId;
|
||||
}
|
||||
|
||||
if (
|
||||
!isDomainBasedMultiTenancy ||
|
||||
(!urlSet.isLocalhostDisabled && urlString.startsWith(urlSet.localhostUrl))
|
||||
) {
|
||||
return defaultTenantId;
|
||||
}
|
||||
|
||||
return new RegExp(urlSet.endpoint.replace('*', '([^.]*)')).exec(urlString)?.[1];
|
||||
};
|
|
@ -1,35 +0,0 @@
|
|||
import type { ArbitraryObject, UserInfo } from '@logto/schemas';
|
||||
|
||||
import api from './api.js';
|
||||
|
||||
export const getCurrentUserInfo = (userId: string) =>
|
||||
api.get(`me`, { headers: { 'development-user-id': userId } }).json<UserInfo>();
|
||||
|
||||
export const getCurrentUserCustomData = (userId: string) =>
|
||||
api
|
||||
.get('me/custom-data', {
|
||||
headers: {
|
||||
'development-user-id': userId,
|
||||
},
|
||||
})
|
||||
.json<ArbitraryObject>();
|
||||
|
||||
export const updateCurrentUserCustomData = (userId: string, payload: Record<string, unknown>) =>
|
||||
api.patch('me/custom-data', {
|
||||
headers: {
|
||||
'development-user-id': userId,
|
||||
},
|
||||
json: {
|
||||
customData: payload,
|
||||
},
|
||||
});
|
||||
|
||||
export const changeCurrentUserPassword = (userId: string, password: string) =>
|
||||
api.patch('me/password', {
|
||||
headers: {
|
||||
'development-user-id': userId,
|
||||
},
|
||||
json: {
|
||||
password,
|
||||
},
|
||||
});
|
|
@ -1,7 +1,5 @@
|
|||
export * from './application.js';
|
||||
export * from './resource.js';
|
||||
export * from './management-api.js';
|
||||
export * from './logto-config.js';
|
||||
export * from './sign-in-experience.js';
|
||||
export * from './roles.js';
|
||||
export * from './scope.js';
|
||||
export * from './tenant.js';
|
||||
|
|
|
@ -2,12 +2,14 @@ import { CreateLogtoConfig } from '../db-entries/index.js';
|
|||
import { AppearanceMode } from '../foundations/index.js';
|
||||
import type { AdminConsoleData } from '../types/index.js';
|
||||
import { AdminConsoleConfigKey } from '../types/index.js';
|
||||
import { defaultTenantId } from './tenant.js';
|
||||
|
||||
export const createDefaultAdminConsoleConfig = (): Readonly<{
|
||||
key: AdminConsoleConfigKey;
|
||||
value: AdminConsoleData;
|
||||
}> =>
|
||||
Object.freeze({
|
||||
tenantId: defaultTenantId,
|
||||
key: AdminConsoleConfigKey.AdminConsole,
|
||||
value: {
|
||||
language: 'en',
|
||||
|
|
82
packages/schemas/src/seeds/management-api.ts
Normal file
82
packages/schemas/src/seeds/management-api.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
import { generateStandardId } from '@logto/core-kit';
|
||||
|
||||
import type { CreateResource, CreateRole, CreateScope } from '../db-entries/index.js';
|
||||
import { UserRole } from '../types/index.js';
|
||||
import { adminTenantId, defaultTenantId } from './tenant.js';
|
||||
|
||||
export type AdminData = {
|
||||
resource: CreateResource;
|
||||
scope: CreateScope;
|
||||
role: CreateRole;
|
||||
};
|
||||
|
||||
export const managementApiScopeAll = 'all';
|
||||
|
||||
// Consider remove the dependency of IDs
|
||||
const defaultResourceId = 'management-api';
|
||||
const defaultScopeAllId = 'management-api-all';
|
||||
|
||||
// Consider combine this with `createManagementApiInAdminTenant()`
|
||||
/** The fixed Management API Resource for `default` tenant. */
|
||||
export const defaultManagementApi = Object.freeze({
|
||||
resource: {
|
||||
tenantId: defaultTenantId,
|
||||
/** @deprecated You should not rely on this constant. Change to something else. */
|
||||
id: defaultResourceId,
|
||||
/**
|
||||
* The fixed resource indicator for Management APIs.
|
||||
*
|
||||
* Admin Console requires the access token of this resource to be functional.
|
||||
*/
|
||||
indicator: 'https://logto.app/api',
|
||||
name: 'Logto Management API',
|
||||
},
|
||||
scope: {
|
||||
tenantId: defaultTenantId,
|
||||
/** @deprecated You should not rely on this constant. Change to something else. */
|
||||
id: defaultScopeAllId,
|
||||
name: managementApiScopeAll,
|
||||
description: 'Default scope for Management API, allows all permissions.',
|
||||
/** @deprecated You should not rely on this constant. Change to something else. */
|
||||
resourceId: defaultResourceId,
|
||||
},
|
||||
role: {
|
||||
tenantId: defaultTenantId,
|
||||
/** @deprecated You should not rely on this constant. Change to something else. */
|
||||
id: 'admin-role',
|
||||
name: UserRole.Admin,
|
||||
description: 'Admin role for Logto.',
|
||||
},
|
||||
}) satisfies AdminData;
|
||||
|
||||
export const getManagementApiResourceIndicator = (tenantId: string) =>
|
||||
`https://${tenantId}.logto.app/api`;
|
||||
|
||||
export const getManagementApiAdminName = (tenantId: string) => `${tenantId}:${UserRole.Admin}`;
|
||||
|
||||
/** Create a Management API Resource of the given tenant ID for `admin` tenant. */
|
||||
export const createManagementApiInAdminTenant = (tenantId: string): AdminData => {
|
||||
const resourceId = generateStandardId();
|
||||
|
||||
return Object.freeze({
|
||||
resource: {
|
||||
tenantId: adminTenantId,
|
||||
id: resourceId,
|
||||
indicator: getManagementApiResourceIndicator(tenantId),
|
||||
name: `Logto Management API for tenant ${tenantId}`,
|
||||
},
|
||||
scope: {
|
||||
tenantId: adminTenantId,
|
||||
id: generateStandardId(),
|
||||
name: managementApiScopeAll,
|
||||
description: 'Default scope for Management API, allows all permissions.',
|
||||
resourceId,
|
||||
},
|
||||
role: {
|
||||
tenantId: adminTenantId,
|
||||
id: generateStandardId(),
|
||||
name: getManagementApiAdminName(tenantId),
|
||||
description: 'Admin role for Logto.',
|
||||
},
|
||||
});
|
||||
};
|
|
@ -1,16 +0,0 @@
|
|||
import type { CreateResource } from '../db-entries/index.js';
|
||||
import { defaultTenantId } from './tenant.js';
|
||||
|
||||
export const managementResourceId = 'management-api';
|
||||
|
||||
export const managementResource: Readonly<CreateResource> = Object.freeze({
|
||||
tenantId: defaultTenantId,
|
||||
id: managementResourceId,
|
||||
/**
|
||||
* The fixed resource indicator for Management APIs.
|
||||
*
|
||||
* Admin Console requires the access token of this resource to be functional.
|
||||
*/
|
||||
indicator: 'https://api.logto.io',
|
||||
name: 'Logto Management API',
|
||||
});
|
|
@ -1,24 +0,0 @@
|
|||
import type { CreateRole, CreateRolesScope } from '../db-entries/index.js';
|
||||
import { UserRole } from '../types/index.js';
|
||||
import { managementApiScopeAll } from './scope.js';
|
||||
import { defaultTenantId } from './tenant.js';
|
||||
|
||||
export const adminRoleId = 'admin-role';
|
||||
export const adminRoleScopeId = 'admin-role-scope';
|
||||
|
||||
/**
|
||||
* Default Admin Role for Admin Console.
|
||||
*/
|
||||
export const defaultRole: Readonly<CreateRole> = {
|
||||
tenantId: defaultTenantId,
|
||||
id: adminRoleId,
|
||||
name: UserRole.Admin,
|
||||
description: 'Admin role for Logto.',
|
||||
};
|
||||
|
||||
export const defaultRoleScopeRelation: Readonly<CreateRolesScope> = {
|
||||
id: adminRoleScopeId,
|
||||
tenantId: defaultTenantId,
|
||||
roleId: adminRoleId,
|
||||
scopeId: managementApiScopeAll,
|
||||
};
|
|
@ -1,13 +0,0 @@
|
|||
import type { CreateScope } from '../db-entries/index.js';
|
||||
import { managementResourceId } from './resource.js';
|
||||
import { defaultTenantId } from './tenant.js';
|
||||
|
||||
export const managementApiScopeAll = 'management-api-all';
|
||||
|
||||
export const managementResourceScope: Readonly<CreateScope> = Object.freeze({
|
||||
tenantId: defaultTenantId,
|
||||
id: managementApiScopeAll,
|
||||
name: 'all',
|
||||
description: 'Default scope for Management API, allows all permissions.',
|
||||
resourceId: managementResourceId,
|
||||
});
|
|
@ -2,14 +2,14 @@
|
|||
|
||||
create function set_tenant_id() returns trigger as
|
||||
$$ begin
|
||||
select tenants.id into new.tenant_id
|
||||
from tenants
|
||||
where ('tenant_user_' || tenants.id) = current_user;
|
||||
|
||||
if new.tenant_id is null then
|
||||
new.tenant_id := 'default';
|
||||
if new.tenant_id is not null then
|
||||
return new;
|
||||
end if;
|
||||
|
||||
select tenants.id into new.tenant_id
|
||||
from tenants
|
||||
where tenants.db_user = current_user;
|
||||
|
||||
return new;
|
||||
end; $$ language plpgsql;
|
||||
|
||||
|
|
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
|
@ -250,6 +250,7 @@ importers:
|
|||
|
||||
packages/core:
|
||||
specifiers:
|
||||
'@koa/cors': ^4.0.0
|
||||
'@logto/cli': workspace:*
|
||||
'@logto/connector-kit': workspace:*
|
||||
'@logto/core-kit': workspace:*
|
||||
|
@ -271,6 +272,7 @@ importers:
|
|||
'@types/koa-logger': ^3.1.1
|
||||
'@types/koa-mount': ^4.0.0
|
||||
'@types/koa-send': ^4.1.3
|
||||
'@types/koa__cors': ^3.3.0
|
||||
'@types/node': ^18.11.18
|
||||
'@types/oidc-provider': ^8.0.0
|
||||
'@types/semver': ^7.3.12
|
||||
|
@ -326,6 +328,7 @@ importers:
|
|||
typescript: ^4.9.4
|
||||
zod: ^3.20.2
|
||||
dependencies:
|
||||
'@koa/cors': 4.0.0
|
||||
'@logto/cli': link:../cli
|
||||
'@logto/connector-kit': link:../toolkit/connector-kit
|
||||
'@logto/core-kit': link:../toolkit/core-kit
|
||||
|
@ -384,6 +387,7 @@ importers:
|
|||
'@types/koa-logger': 3.1.2
|
||||
'@types/koa-mount': 4.0.1
|
||||
'@types/koa-send': 4.1.3
|
||||
'@types/koa__cors': 3.3.0
|
||||
'@types/node': 18.11.18
|
||||
'@types/oidc-provider': 8.0.0
|
||||
'@types/semver': 7.3.12
|
||||
|
@ -4099,6 +4103,12 @@ packages:
|
|||
'@types/node': 18.11.18
|
||||
dev: true
|
||||
|
||||
/@types/koa__cors/3.3.0:
|
||||
resolution: {integrity: sha512-FUN8YxcBakIs+walVe3+HcNP+Bxd0SB8BJHBWkglZ5C1XQWljlKcEFDG/dPiCIqwVCUbc5X0nYDlH62uEhdHMA==}
|
||||
dependencies:
|
||||
'@types/koa': 2.13.4
|
||||
dev: true
|
||||
|
||||
/@types/mdast/3.0.10:
|
||||
resolution: {integrity: sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==}
|
||||
dependencies:
|
||||
|
|
Loading…
Add table
Reference in a new issue