mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(cloud): POST /tenants
This commit is contained in:
parent
12377665b4
commit
76a04d97b3
26 changed files with 350 additions and 99 deletions
|
@ -10,6 +10,8 @@ echo Install production dependencies
|
|||
NODE_ENV=production pnpm i
|
||||
|
||||
echo Prune files
|
||||
# Remove cloud in OSS distributions
|
||||
rm -rf packages/cloud
|
||||
# Some node packages use `src` as their dist folder, so ignore them from the rm list in the end
|
||||
find \
|
||||
.git .changeset .changeset-staged .devcontainer .github .husky .parcel-cache .scripts .vscode pnpm-*.yaml *.js \
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { readdir, readFile } from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
import { generateStandardId } from '@logto/core-kit';
|
||||
import {
|
||||
defaultSignInExperience,
|
||||
createDefaultAdminConsoleConfig,
|
||||
|
@ -9,7 +8,7 @@ import {
|
|||
defaultTenantId,
|
||||
adminTenantId,
|
||||
defaultManagementApi,
|
||||
createManagementApiInAdminTenant,
|
||||
createAdminDataInAdminTenant,
|
||||
createMeApiInAdminTenant,
|
||||
} from '@logto/schemas';
|
||||
import { Hooks, Tenants } from '@logto/schemas/models';
|
||||
|
@ -120,13 +119,14 @@ export const seedTables = async (
|
|||
|
||||
await createTenant(connection, adminTenantId);
|
||||
await seedOidcConfigs(connection, adminTenantId);
|
||||
await seedAdminData(connection, createManagementApiInAdminTenant(defaultTenantId));
|
||||
await seedAdminData(connection, createAdminDataInAdminTenant(defaultTenantId));
|
||||
await seedAdminData(connection, createMeApiInAdminTenant());
|
||||
|
||||
await Promise.all([
|
||||
connection.query(insertInto(createDefaultAdminConsoleConfig(), 'logto_configs')),
|
||||
connection.query(insertInto(createDefaultAdminConsoleConfig(defaultTenantId), 'logto_configs')),
|
||||
connection.query(insertInto(defaultSignInExperience, 'sign_in_experiences')),
|
||||
connection.query(insertInto(createDemoAppApplication(generateStandardId()), 'applications')),
|
||||
// TODO: @gao remove demo app
|
||||
connection.query(insertInto(createDemoAppApplication(defaultTenantId), 'applications')),
|
||||
updateDatabaseTimestamp(connection, latestTimestamp),
|
||||
]);
|
||||
};
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { generateStandardId } from '@logto/core-kit';
|
||||
import { CreateRolesScope } from '@logto/schemas';
|
||||
import type { TenantModel, AdminData } from '@logto/schemas';
|
||||
import { createTenantMetadata } from '@logto/shared';
|
||||
import { assert } from '@silverhand/essentials';
|
||||
import type { CommonQueryMethods } from 'slonik';
|
||||
import { sql } from 'slonik';
|
||||
|
@ -11,9 +12,7 @@ import { getDatabaseName } from '../../../queries/database.js';
|
|||
|
||||
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 { parentRole, role, password } = createTenantMetadata(database, tenantId);
|
||||
const tenantModel: TenantModel = { id: tenantId, dbUser: role, dbUserPassword: password };
|
||||
|
||||
await pool.query(insertInto(tenantModel, 'tenants'));
|
||||
|
|
|
@ -19,12 +19,14 @@
|
|||
"start": "NODE_ENV=production node build/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@logto/core-kit": "workspace:*",
|
||||
"@logto/schemas": "workspace:*",
|
||||
"@logto/shared": "workspace:*",
|
||||
"@silverhand/essentials": "2.2.0",
|
||||
"@withtyped/postgres": "^0.7.0",
|
||||
"@withtyped/server": "^0.7.0",
|
||||
"@withtyped/postgres": "^0.8.0",
|
||||
"@withtyped/server": "^0.8.0",
|
||||
"chalk": "^5.0.0",
|
||||
"decamelize": "^6.0.0",
|
||||
"dotenv": "^16.0.0",
|
||||
"find-up": "^6.3.0",
|
||||
"http-proxy": "^1.18.1",
|
||||
|
|
79
packages/cloud/src/libraries/tenants.ts
Normal file
79
packages/cloud/src/libraries/tenants.ts
Normal file
|
@ -0,0 +1,79 @@
|
|||
import { generateStandardId } from '@logto/core-kit';
|
||||
import type { TenantModel } from '@logto/schemas';
|
||||
import {
|
||||
LogtoConfigs,
|
||||
SignInExperiences,
|
||||
createDefaultAdminConsoleConfig,
|
||||
createDefaultSignInExperience,
|
||||
adminTenantId,
|
||||
createAdminData,
|
||||
createAdminDataInAdminTenant,
|
||||
} from '@logto/schemas';
|
||||
import { createTenantMetadata } from '@logto/shared';
|
||||
import type { ZodType } from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { Queries } from '#src/queries/index.js';
|
||||
import { getDatabaseName } from '#src/queries/utils.js';
|
||||
import { insertInto } from '#src/utils/query.js';
|
||||
import { getTenantIdFromManagementApiIndicator } from '#src/utils/tenant.js';
|
||||
|
||||
export type TenantInfo = {
|
||||
id: string;
|
||||
indicator: string;
|
||||
};
|
||||
|
||||
export const tenantInfoGuard: ZodType<TenantInfo> = z.object({
|
||||
id: z.string(),
|
||||
indicator: z.string(),
|
||||
});
|
||||
|
||||
export const createTenantsLibrary = (queries: Queries) => {
|
||||
const { getManagementApiLikeIndicatorsForUser, insertTenant, createTenantRole, insertAdminData } =
|
||||
queries.tenants;
|
||||
const { assignRoleToUser } = queries.users;
|
||||
|
||||
const getAvailableTenants = async (userId: string): Promise<TenantInfo[]> => {
|
||||
const { rows } = await getManagementApiLikeIndicatorsForUser(userId);
|
||||
|
||||
return rows
|
||||
.map(({ indicator }) => ({
|
||||
id: getTenantIdFromManagementApiIndicator(indicator),
|
||||
indicator,
|
||||
}))
|
||||
.filter((tenant): tenant is TenantInfo => Boolean(tenant.id));
|
||||
};
|
||||
|
||||
const createNewTenant = async (forUserId: string): Promise<TenantInfo> => {
|
||||
const { client } = queries;
|
||||
const databaseName = await getDatabaseName(client);
|
||||
const { id: tenantId, parentRole, role, password } = createTenantMetadata(databaseName);
|
||||
|
||||
// TODO: @gao wrap into transaction
|
||||
// Init tenant
|
||||
const tenantModel: TenantModel = { id: tenantId, dbUser: role, dbUserPassword: password };
|
||||
await insertTenant(tenantModel);
|
||||
await createTenantRole(parentRole, role, password);
|
||||
|
||||
// Create admin data set (resource, roles, etc.)
|
||||
const adminDataInAdminTenant = createAdminDataInAdminTenant(tenantId);
|
||||
await insertAdminData(adminDataInAdminTenant);
|
||||
await insertAdminData(createAdminData(tenantId));
|
||||
await assignRoleToUser({
|
||||
id: generateStandardId(),
|
||||
tenantId: adminTenantId,
|
||||
userId: forUserId,
|
||||
roleId: adminDataInAdminTenant.role.id,
|
||||
});
|
||||
|
||||
// Create initial configs
|
||||
await Promise.all([
|
||||
client.query(insertInto(createDefaultAdminConsoleConfig(tenantId), LogtoConfigs.table)),
|
||||
client.query(insertInto(createDefaultSignInExperience(tenantId), SignInExperiences.table)),
|
||||
]);
|
||||
|
||||
return { id: tenantId, indicator: adminDataInAdminTenant.resource.indicator };
|
||||
};
|
||||
|
||||
return { getAvailableTenants, createNewTenant };
|
||||
};
|
|
@ -2,6 +2,7 @@ import assert from 'node:assert';
|
|||
import type { IncomingHttpHeaders } from 'node:http';
|
||||
import path from 'node:path/posix';
|
||||
|
||||
import { tryThat } from '@logto/shared';
|
||||
import type { NextFunction, RequestContext } from '@withtyped/server';
|
||||
import { RequestError } from '@withtyped/server';
|
||||
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
||||
|
@ -57,31 +58,26 @@ export default function withAuth<InputContext extends RequestContext>({
|
|||
return async (context: InputContext, next: NextFunction<WithAuthContext<InputContext>>) => {
|
||||
const [getKey, issuer] = await getJwkSet;
|
||||
|
||||
try {
|
||||
const {
|
||||
payload: { sub, scope },
|
||||
} = await jwtVerify(extractBearerTokenFromHeaders(context.request.headers), getKey, {
|
||||
const {
|
||||
payload: { sub, scope },
|
||||
} = await tryThat(
|
||||
jwtVerify(extractBearerTokenFromHeaders(context.request.headers), getKey, {
|
||||
issuer,
|
||||
audience,
|
||||
});
|
||||
|
||||
assert(sub, new RequestError('"sub" is missing in JWT.', 401));
|
||||
|
||||
const scopes = typeof scope === 'string' ? scope.split(' ') : [];
|
||||
assert(
|
||||
expectScopes.every((scope) => scopes.includes(scope)),
|
||||
new RequestError('Forbidden. Please check your permissions.', 403)
|
||||
);
|
||||
|
||||
await next({ ...context, auth: { id: sub, scopes } });
|
||||
|
||||
return;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof RequestError) {
|
||||
}),
|
||||
(error) => {
|
||||
throw error;
|
||||
}
|
||||
);
|
||||
|
||||
throw new RequestError('Unauthorized.', 401);
|
||||
}
|
||||
assert(sub, new RequestError('"sub" is missing in JWT.', 401));
|
||||
|
||||
const scopes = typeof scope === 'string' ? scope.split(' ') : [];
|
||||
assert(
|
||||
expectScopes.every((scope) => scopes.includes(scope)),
|
||||
new RequestError('Forbidden. Please check your permissions.', 403)
|
||||
);
|
||||
|
||||
return next({ ...context, auth: { id: sub, scopes } });
|
||||
};
|
||||
}
|
||||
|
|
|
@ -3,4 +3,13 @@ import { createQueryClient } from '@withtyped/postgres';
|
|||
import { EnvSet } from '#src/env-set/index.js';
|
||||
import { parseDsn } from '#src/utils/postgres.js';
|
||||
|
||||
export const client = createQueryClient(parseDsn(EnvSet.global.dbUrl));
|
||||
import { createTenantsQueries } from './tenants.js';
|
||||
import { createUsersQueries } from './users.js';
|
||||
|
||||
export class Queries {
|
||||
static default = new Queries();
|
||||
|
||||
public readonly client = createQueryClient(parseDsn(EnvSet.global.dbUrl));
|
||||
public readonly tenants = createTenantsQueries(this.client);
|
||||
public readonly users = createUsersQueries(this.client);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,19 @@
|
|||
import { adminTenantId, getManagementApiResourceIndicator, PredefinedScope } from '@logto/schemas';
|
||||
import assert from 'node:assert';
|
||||
|
||||
import { generateStandardId } from '@logto/core-kit';
|
||||
import type { AdminData, TenantModel } from '@logto/schemas';
|
||||
import {
|
||||
adminTenantId,
|
||||
getManagementApiResourceIndicator,
|
||||
PredefinedScope,
|
||||
CreateRolesScope,
|
||||
} from '@logto/schemas';
|
||||
import type { PostgresQueryClient } from '@withtyped/postgres';
|
||||
import { sql } from '@withtyped/postgres';
|
||||
import { dangerousRaw, id, sql } from '@withtyped/postgres';
|
||||
|
||||
import { insertInto } from '#src/utils/query.js';
|
||||
|
||||
export type TenantsQueries = ReturnType<typeof createTenantsQueries>;
|
||||
|
||||
export const createTenantsQueries = (client: PostgresQueryClient) => {
|
||||
const getManagementApiLikeIndicatorsForUser = async (userId: string) =>
|
||||
|
@ -20,5 +33,43 @@ export const createTenantsQueries = (client: PostgresQueryClient) => {
|
|||
where roles.tenant_id = ${adminTenantId};
|
||||
`);
|
||||
|
||||
return { getManagementApiLikeIndicatorsForUser };
|
||||
const insertTenant = async (tenant: TenantModel) => client.query(insertInto(tenant, 'tenants'));
|
||||
|
||||
const createTenantRole = async (parentRole: string, role: string, password: string) =>
|
||||
client.query(sql`
|
||||
create role ${id(role)} with inherit login
|
||||
password '${dangerousRaw(password)}'
|
||||
in role ${id(parentRole)};
|
||||
`);
|
||||
|
||||
const insertAdminData = async (data: AdminData) => {
|
||||
const { resource, scope, role } = data;
|
||||
|
||||
assert(
|
||||
resource.tenantId && scope.tenantId && role.tenantId,
|
||||
new Error('Tenant ID cannot be empty.')
|
||||
);
|
||||
|
||||
assert(
|
||||
resource.tenantId === scope.tenantId && scope.tenantId === role.tenantId,
|
||||
new Error('All data should have the same tenant ID.')
|
||||
);
|
||||
|
||||
await client.query(insertInto(resource, 'resources'));
|
||||
await client.query(insertInto(scope, 'scopes'));
|
||||
await client.query(insertInto(role, 'roles'));
|
||||
await client.query(
|
||||
insertInto(
|
||||
{
|
||||
id: generateStandardId(),
|
||||
roleId: role.id,
|
||||
scopeId: scope.id,
|
||||
tenantId: resource.tenantId,
|
||||
} satisfies CreateRolesScope,
|
||||
'roles_scopes'
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
return { getManagementApiLikeIndicatorsForUser, insertTenant, createTenantRole, insertAdminData };
|
||||
};
|
||||
|
|
12
packages/cloud/src/queries/users.ts
Normal file
12
packages/cloud/src/queries/users.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import type { UsersRole } from '@logto/schemas';
|
||||
import type { PostgresQueryClient } from '@withtyped/postgres';
|
||||
|
||||
import { insertInto } from '#src/utils/query.js';
|
||||
|
||||
export type UsersQueries = ReturnType<typeof createUsersQueries>;
|
||||
|
||||
export const createUsersQueries = (client: PostgresQueryClient) => {
|
||||
const assignRoleToUser = async (data: UsersRole) => client.query(insertInto(data, 'users_roles'));
|
||||
|
||||
return { assignRoleToUser };
|
||||
};
|
17
packages/cloud/src/queries/utils.ts
Normal file
17
packages/cloud/src/queries/utils.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import type { PostgresQueryClient } from '@withtyped/postgres';
|
||||
import { sql } from '@withtyped/postgres';
|
||||
import { RequestError } from '@withtyped/server';
|
||||
|
||||
export const getDatabaseName = async (client: PostgresQueryClient) => {
|
||||
const {
|
||||
rows: [first],
|
||||
} = await client.query<{ currentDatabase: string }>(sql`
|
||||
select current_database() as "currentDatabase";
|
||||
`);
|
||||
|
||||
if (!first) {
|
||||
throw new RequestError(undefined, 500);
|
||||
}
|
||||
|
||||
return first.currentDatabase.replaceAll('-', '_');
|
||||
};
|
|
@ -1,26 +1,21 @@
|
|||
import { createRouter } from '@withtyped/server';
|
||||
import { z } from 'zod';
|
||||
import { createRouter, RequestError } from '@withtyped/server';
|
||||
|
||||
import { createTenantsLibrary, tenantInfoGuard } from '#src/libraries/tenants.js';
|
||||
import type { WithAuthContext } from '#src/middleware/with-auth.js';
|
||||
import { client } from '#src/queries/index.js';
|
||||
import { createTenantsQueries } from '#src/queries/tenants.js';
|
||||
import { getTenantIdFromManagementApiIndicator } from '#src/utils/tenant.js';
|
||||
import { Queries } from '#src/queries/index.js';
|
||||
|
||||
const { getManagementApiLikeIndicatorsForUser } = createTenantsQueries(client);
|
||||
const { getAvailableTenants, createNewTenant } = createTenantsLibrary(Queries.default);
|
||||
|
||||
export const tenants = createRouter<WithAuthContext, '/tenants'>('/tenants').get(
|
||||
'/',
|
||||
{ response: z.object({ id: z.string(), indicator: z.string() }).array() },
|
||||
async (context, next) => {
|
||||
const { rows } = await getManagementApiLikeIndicatorsForUser(context.auth.id);
|
||||
export const tenants = createRouter<WithAuthContext, '/tenants'>('/tenants')
|
||||
.get('/', { response: tenantInfoGuard.array() }, async (context, next) => {
|
||||
return next({ ...context, json: await getAvailableTenants(context.auth.id) });
|
||||
})
|
||||
.post('/', { response: tenantInfoGuard }, async (context, next) => {
|
||||
const tenants = await getAvailableTenants(context.auth.id);
|
||||
|
||||
const tenants = rows
|
||||
.map(({ indicator }) => ({
|
||||
id: getTenantIdFromManagementApiIndicator(indicator),
|
||||
indicator,
|
||||
}))
|
||||
.filter((tenant): tenant is { id: string; indicator: string } => Boolean(tenant.id));
|
||||
if (tenants.length > 0) {
|
||||
throw new RequestError('The user already has a tenant.', 409);
|
||||
}
|
||||
|
||||
return next({ ...context, json: tenants });
|
||||
}
|
||||
);
|
||||
return next({ ...context, json: await createNewTenant(context.auth.id) });
|
||||
});
|
||||
|
|
13
packages/cloud/src/utils/query.ts
Normal file
13
packages/cloud/src/utils/query.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { id, jsonIfNeeded, sql } from '@withtyped/postgres';
|
||||
import type { JsonObject } from '@withtyped/server';
|
||||
import decamelize from 'decamelize';
|
||||
|
||||
export const insertInto = <T extends JsonObject>(object: T, table: string) => {
|
||||
const entries = Object.entries(object);
|
||||
|
||||
return sql`
|
||||
insert into ${id(table)}
|
||||
(${entries.map(([key]) => id(decamelize(key)))})
|
||||
values (${entries.map(([, value]) => jsonIfNeeded(value))})
|
||||
`;
|
||||
};
|
|
@ -35,8 +35,8 @@
|
|||
"@logto/schemas": "workspace:*",
|
||||
"@logto/shared": "workspace:*",
|
||||
"@silverhand/essentials": "2.2.0",
|
||||
"@withtyped/postgres": "^0.7.0",
|
||||
"@withtyped/server": "^0.7.0",
|
||||
"@withtyped/postgres": "^0.8.0",
|
||||
"@withtyped/server": "^0.8.0",
|
||||
"chalk": "^5.0.0",
|
||||
"clean-deep": "^3.4.0",
|
||||
"date-fns": "^2.29.3",
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
import type { PostgreSql } from '@withtyped/postgres';
|
||||
import type { Transaction } from '@withtyped/server';
|
||||
import { QueryClient } from '@withtyped/server';
|
||||
|
||||
// Consider move to withtyped if everything goes well
|
||||
export class MockQueryClient extends QueryClient<PostgreSql> {
|
||||
async transaction(): Promise<Transaction<PostgreSql>> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
async connect() {
|
||||
console.debug('MockQueryClient connect');
|
||||
}
|
||||
|
|
|
@ -53,6 +53,6 @@
|
|||
},
|
||||
"prettier": "@silverhand/eslint-config/.prettierrc",
|
||||
"dependencies": {
|
||||
"@withtyped/server": "^0.7.0"
|
||||
"@withtyped/server": "^0.8.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,7 +84,7 @@
|
|||
"@logto/language-kit": "workspace:*",
|
||||
"@logto/phrases": "workspace:*",
|
||||
"@logto/phrases-ui": "workspace:*",
|
||||
"@withtyped/server": "^0.7.0",
|
||||
"@withtyped/server": "^0.8.0",
|
||||
"zod": "^3.20.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { generateStandardId } from '@logto/core-kit';
|
||||
|
||||
import type { CreateApplication } from '../db-entries/index.js';
|
||||
import { ApplicationType } from '../db-entries/index.js';
|
||||
import { defaultTenantId } from './tenant.js';
|
||||
|
||||
/**
|
||||
* The fixed application ID for Admin Console.
|
||||
|
@ -11,10 +12,11 @@ export const adminConsoleApplicationId = 'admin-console';
|
|||
|
||||
export const demoAppApplicationId = 'demo-app';
|
||||
|
||||
export const createDemoAppApplication = (secret: string): Readonly<CreateApplication> => ({
|
||||
tenantId: defaultTenantId,
|
||||
/** @deprecated Demo app database entity will be removed soon. */
|
||||
export const createDemoAppApplication = (forTenantId: string): Readonly<CreateApplication> => ({
|
||||
tenantId: forTenantId,
|
||||
id: demoAppApplicationId,
|
||||
secret,
|
||||
secret: generateStandardId(),
|
||||
name: 'Demo App',
|
||||
description: 'Logto demo app.',
|
||||
type: ApplicationType.SPA,
|
||||
|
|
|
@ -2,14 +2,15 @@ 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<{
|
||||
export const createDefaultAdminConsoleConfig = (
|
||||
forTenantId: string
|
||||
): Readonly<{
|
||||
key: AdminConsoleConfigKey;
|
||||
value: AdminConsoleData;
|
||||
}> =>
|
||||
Object.freeze({
|
||||
tenantId: defaultTenantId,
|
||||
tenantId: forTenantId,
|
||||
key: AdminConsoleConfigKey.AdminConsole,
|
||||
value: {
|
||||
language: 'en',
|
||||
|
|
|
@ -14,7 +14,7 @@ export type AdminData = {
|
|||
const defaultResourceId = 'management-api';
|
||||
const defaultScopeAllId = 'management-api-all';
|
||||
|
||||
// Consider combine this with `createManagementApiInAdminTenant()`
|
||||
// Consider combine this with `createAdminData()`
|
||||
/** The fixed Management API Resource for `default` tenant. */
|
||||
export const defaultManagementApi = Object.freeze({
|
||||
resource: {
|
||||
|
@ -47,13 +47,50 @@ export const defaultManagementApi = Object.freeze({
|
|||
},
|
||||
}) satisfies AdminData;
|
||||
|
||||
export const getManagementApiResourceIndicator = (tenantId: string, path = 'api') =>
|
||||
`https://${tenantId}.logto.app/${path}`;
|
||||
export function getManagementApiResourceIndicator<TenantId extends string>(
|
||||
tenantId: TenantId
|
||||
): `https://${TenantId}.logto.app/api`;
|
||||
export function getManagementApiResourceIndicator<TenantId extends string, Path extends string>(
|
||||
tenantId: TenantId,
|
||||
path: Path
|
||||
): `https://${TenantId}.logto.app/${Path}`;
|
||||
|
||||
export const getManagementApiAdminName = (tenantId: string) => `${tenantId}:${UserRole.Admin}`;
|
||||
export function getManagementApiResourceIndicator(tenantId: string, path = 'api') {
|
||||
return `https://${tenantId}.logto.app/${path}`;
|
||||
}
|
||||
|
||||
/** Create a Management API Resource of the given tenant ID for `admin` tenant. */
|
||||
export const createManagementApiInAdminTenant = (tenantId: string): AdminData => {
|
||||
export const getManagementApiAdminName = <TenantId extends string>(tenantId: TenantId) =>
|
||||
`${tenantId}:${UserRole.Admin}` as const;
|
||||
|
||||
/** Create a set of admin data for Management API of the given tenant ID. */
|
||||
export const createAdminData = (tenantId: string): AdminData => {
|
||||
const resourceId = generateStandardId();
|
||||
|
||||
return Object.freeze({
|
||||
resource: {
|
||||
tenantId,
|
||||
id: resourceId,
|
||||
indicator: getManagementApiResourceIndicator(tenantId),
|
||||
name: `Logto Management API`,
|
||||
},
|
||||
scope: {
|
||||
tenantId,
|
||||
id: generateStandardId(),
|
||||
name: PredefinedScope.All,
|
||||
description: 'Default scope for Management API, allows all permissions.',
|
||||
resourceId,
|
||||
},
|
||||
role: {
|
||||
tenantId,
|
||||
id: generateStandardId(),
|
||||
name: UserRole.Admin,
|
||||
description: 'Admin role for Logto.',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/** Create a set of admin data for Management API of the given tenant ID for `admin` tenant. */
|
||||
export const createAdminDataInAdminTenant = (tenantId: string): AdminData => {
|
||||
const resourceId = generateStandardId();
|
||||
|
||||
return Object.freeze({
|
||||
|
|
|
@ -7,8 +7,10 @@ import { defaultTenantId } from './tenant.js';
|
|||
|
||||
const defaultPrimaryColor = '#6139F6';
|
||||
|
||||
export const defaultSignInExperience: Readonly<CreateSignInExperience> = {
|
||||
tenantId: defaultTenantId,
|
||||
export const createDefaultSignInExperience = (
|
||||
forTenantId: string
|
||||
): Readonly<CreateSignInExperience> => ({
|
||||
tenantId: forTenantId,
|
||||
id: 'default',
|
||||
color: {
|
||||
primaryColor: defaultPrimaryColor,
|
||||
|
@ -42,7 +44,10 @@ export const defaultSignInExperience: Readonly<CreateSignInExperience> = {
|
|||
},
|
||||
socialSignInConnectorTargets: [],
|
||||
signInMode: SignInMode.SignInAndRegister,
|
||||
};
|
||||
});
|
||||
|
||||
/** @deprecated Use `createDefaultSignInExperience()` instead. */
|
||||
export const defaultSignInExperience = createDefaultSignInExperience(defaultTenantId);
|
||||
|
||||
export const adminConsoleSignInExperience: CreateSignInExperience = {
|
||||
...defaultSignInExperience,
|
||||
|
|
|
@ -12,8 +12,5 @@ create table sign_in_experiences (
|
|||
sign_up jsonb /* @use SignUp */ not null,
|
||||
social_sign_in_connector_targets jsonb /* @use ConnectorTargets */ not null default '[]'::jsonb,
|
||||
sign_in_mode sign_in_mode not null default 'SignInAndRegister',
|
||||
primary key (id)
|
||||
primary key (tenant_id, id)
|
||||
);
|
||||
|
||||
create index sign_in_experiences__id
|
||||
on sign_in_experiences (tenant_id, id);
|
||||
|
|
|
@ -33,7 +33,6 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@logto/connector-kit": "workspace:*",
|
||||
"@logto/core-kit": "workspace:*",
|
||||
"@silverhand/eslint-config": "2.0.1",
|
||||
"@silverhand/ts-config": "2.0.3",
|
||||
"@types/jest": "^29.1.2",
|
||||
|
@ -55,6 +54,7 @@
|
|||
},
|
||||
"prettier": "@silverhand/eslint-config/.prettierrc",
|
||||
"dependencies": {
|
||||
"@logto/core-kit": "workspace:*",
|
||||
"@logto/schemas": "workspace:*",
|
||||
"@silverhand/essentials": "2.2.0",
|
||||
"find-up": "^6.3.0",
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
export * from './database/index.js';
|
||||
export * from './utils/index.js';
|
||||
export * from './models/index.js';
|
||||
|
|
1
packages/shared/src/models/index.ts
Normal file
1
packages/shared/src/models/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './tenant.js';
|
23
packages/shared/src/models/tenant.ts
Normal file
23
packages/shared/src/models/tenant.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { generateStandardId } from '@logto/core-kit';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
|
||||
// Use lowercase letters for tenant IDs to improve compatibility
|
||||
export const tenantIdAlphabet = '0123456789abcdefghijklmnopqrstuvwxyz';
|
||||
|
||||
export type TenantMetadata = {
|
||||
id: string;
|
||||
parentRole: string;
|
||||
role: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export const createTenantMetadata = (
|
||||
databaseName: string,
|
||||
tenantId = customAlphabet(tenantIdAlphabet)(6)
|
||||
): TenantMetadata => {
|
||||
const parentRole = `logto_tenant_${databaseName}`;
|
||||
const role = `logto_tenant_${databaseName}_${tenantId}`;
|
||||
const password = generateStandardId(32);
|
||||
|
||||
return { id: tenantId, parentRole, role, password };
|
||||
};
|
|
@ -106,6 +106,7 @@ importers:
|
|||
|
||||
packages/cloud:
|
||||
specifiers:
|
||||
'@logto/core-kit': workspace:*
|
||||
'@logto/schemas': workspace:*
|
||||
'@logto/shared': workspace:*
|
||||
'@silverhand/eslint-config': 2.0.1
|
||||
|
@ -114,9 +115,10 @@ importers:
|
|||
'@types/http-proxy': ^1.17.9
|
||||
'@types/mime-types': ^2.1.1
|
||||
'@types/node': ^18.11.18
|
||||
'@withtyped/postgres': ^0.7.0
|
||||
'@withtyped/server': ^0.7.0
|
||||
'@withtyped/postgres': ^0.8.0
|
||||
'@withtyped/server': ^0.8.0
|
||||
chalk: ^5.0.0
|
||||
decamelize: ^6.0.0
|
||||
dotenv: ^16.0.0
|
||||
eslint: ^8.21.0
|
||||
find-up: ^6.3.0
|
||||
|
@ -129,12 +131,14 @@ importers:
|
|||
typescript: ^4.9.4
|
||||
zod: ^3.20.2
|
||||
dependencies:
|
||||
'@logto/core-kit': link:../toolkit/core-kit
|
||||
'@logto/schemas': link:../schemas
|
||||
'@logto/shared': link:../shared
|
||||
'@silverhand/essentials': 2.2.0
|
||||
'@withtyped/postgres': 0.7.0_@withtyped+server@0.7.0
|
||||
'@withtyped/server': 0.7.0
|
||||
'@withtyped/postgres': 0.8.0_@withtyped+server@0.8.0
|
||||
'@withtyped/server': 0.8.0
|
||||
chalk: 5.1.2
|
||||
decamelize: 6.0.0
|
||||
dotenv: 16.0.0
|
||||
find-up: 6.3.0
|
||||
http-proxy: 1.18.1
|
||||
|
@ -331,8 +335,8 @@ importers:
|
|||
'@types/semver': ^7.3.12
|
||||
'@types/sinon': ^10.0.13
|
||||
'@types/supertest': ^2.0.11
|
||||
'@withtyped/postgres': ^0.7.0
|
||||
'@withtyped/server': ^0.7.0
|
||||
'@withtyped/postgres': ^0.8.0
|
||||
'@withtyped/server': ^0.8.0
|
||||
chalk: ^5.0.0
|
||||
clean-deep: ^3.4.0
|
||||
copyfiles: ^2.4.1
|
||||
|
@ -391,8 +395,8 @@ importers:
|
|||
'@logto/schemas': link:../schemas
|
||||
'@logto/shared': link:../shared
|
||||
'@silverhand/essentials': 2.2.0
|
||||
'@withtyped/postgres': 0.7.0_@withtyped+server@0.7.0
|
||||
'@withtyped/server': 0.7.0
|
||||
'@withtyped/postgres': 0.8.0_@withtyped+server@0.8.0
|
||||
'@withtyped/server': 0.8.0
|
||||
chalk: 5.1.2
|
||||
clean-deep: 3.4.0
|
||||
date-fns: 2.29.3
|
||||
|
@ -540,7 +544,7 @@ importers:
|
|||
'@types/jest': ^29.1.2
|
||||
'@types/jest-environment-puppeteer': ^5.0.2
|
||||
'@types/node': ^18.11.18
|
||||
'@withtyped/server': ^0.7.0
|
||||
'@withtyped/server': ^0.8.0
|
||||
dotenv: ^16.0.0
|
||||
eslint: ^8.34.0
|
||||
got: ^12.5.3
|
||||
|
@ -554,7 +558,7 @@ importers:
|
|||
text-encoder: ^0.0.4
|
||||
typescript: ^4.9.4
|
||||
dependencies:
|
||||
'@withtyped/server': 0.7.0
|
||||
'@withtyped/server': 0.8.0
|
||||
devDependencies:
|
||||
'@jest/types': 29.1.2
|
||||
'@logto/connector-kit': link:../toolkit/connector-kit
|
||||
|
@ -647,7 +651,7 @@ importers:
|
|||
'@types/jest': ^29.1.2
|
||||
'@types/node': ^18.11.18
|
||||
'@types/pluralize': ^0.0.29
|
||||
'@withtyped/server': ^0.7.0
|
||||
'@withtyped/server': ^0.8.0
|
||||
camelcase: ^7.0.0
|
||||
eslint: ^8.34.0
|
||||
jest: ^29.1.2
|
||||
|
@ -665,7 +669,7 @@ importers:
|
|||
'@logto/language-kit': link:../toolkit/language-kit
|
||||
'@logto/phrases': link:../phrases
|
||||
'@logto/phrases-ui': link:../phrases-ui
|
||||
'@withtyped/server': 0.7.0
|
||||
'@withtyped/server': 0.8.0
|
||||
zod: 3.20.2
|
||||
devDependencies:
|
||||
'@silverhand/eslint-config': 2.0.1_kjzxg5porcw5dx54sezsklj5cy
|
||||
|
@ -705,6 +709,7 @@ importers:
|
|||
slonik: ^30.0.0
|
||||
typescript: ^4.9.4
|
||||
dependencies:
|
||||
'@logto/core-kit': link:../toolkit/core-kit
|
||||
'@logto/schemas': link:../schemas
|
||||
'@silverhand/essentials': 2.2.0
|
||||
find-up: 6.3.0
|
||||
|
@ -712,7 +717,6 @@ importers:
|
|||
slonik: 30.1.2
|
||||
devDependencies:
|
||||
'@logto/connector-kit': link:../toolkit/connector-kit
|
||||
'@logto/core-kit': link:../toolkit/core-kit
|
||||
'@silverhand/eslint-config': 2.0.1_kjzxg5porcw5dx54sezsklj5cy
|
||||
'@silverhand/ts-config': 2.0.3_typescript@4.9.4
|
||||
'@types/jest': 29.1.2
|
||||
|
@ -4592,21 +4596,21 @@ packages:
|
|||
eslint-visitor-keys: 3.3.0
|
||||
dev: true
|
||||
|
||||
/@withtyped/postgres/0.7.0_@withtyped+server@0.7.0:
|
||||
resolution: {integrity: sha512-D6bI+ols0mtNvaTUp4IzNHAQtbqdakNTZgQ0E0KbVMvdfq0fhOFUaTKANRPjMs4rsyfAETfPzjt3B/Ij47ZiMA==}
|
||||
/@withtyped/postgres/0.8.0_@withtyped+server@0.8.0:
|
||||
resolution: {integrity: sha512-cyhHR1lEV1cs0yDfmHvqR4R52kxgI/JCDd2fizOCzgIE6Z9g3GjYjeVtTl/fTaoDz/QDaXKwfkuxd0N7HSY7uw==}
|
||||
peerDependencies:
|
||||
'@withtyped/server': ^0.7.0
|
||||
'@withtyped/server': ^0.8.0
|
||||
dependencies:
|
||||
'@types/pg': 8.6.6
|
||||
'@withtyped/server': 0.7.0
|
||||
'@withtyped/server': 0.8.0
|
||||
'@withtyped/shared': 0.2.0
|
||||
pg: 8.8.0
|
||||
transitivePeerDependencies:
|
||||
- pg-native
|
||||
dev: false
|
||||
|
||||
/@withtyped/server/0.7.0:
|
||||
resolution: {integrity: sha512-UVW6cOJyOBDfGiSoMg2asYsKqmzL7+UPaYwqW+oxZtvlUabaCekVKTBH3l8tm7zhiOluZg9FD78t0DVuEyQTMw==}
|
||||
/@withtyped/server/0.8.0:
|
||||
resolution: {integrity: sha512-p9gRdEvUNBJ0X15jB4xZutMkzjF19EoBrGjvmaokbuO4+Ub5CSpfV/SOl+7vob8cAgIdWVriZfDGD73ZH0YWJQ==}
|
||||
dependencies:
|
||||
'@withtyped/shared': 0.2.0
|
||||
dev: false
|
||||
|
|
Loading…
Reference in a new issue