mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
refactor(cloud): wrap create tenant in transaction (#3151)
This commit is contained in:
parent
0f3e256a87
commit
86ecd79b36
10 changed files with 70 additions and 39 deletions
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@logto/cloud",
|
||||
"version": "0.1.0",
|
||||
"description": "☁️ Logto Cloud service.",
|
||||
"description": "Logto Cloud service.",
|
||||
"main": "build/index.js",
|
||||
"author": "Silverhand Inc. <contact@silverhand.io>",
|
||||
"license": "Elastic-2.0",
|
||||
|
|
3
packages/cloud/src/consts/rbac.ts
Normal file
3
packages/cloud/src/consts/rbac.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export enum CloudScope {
|
||||
CreateTenant = 'create:tenant',
|
||||
}
|
|
@ -4,13 +4,13 @@ import createServer, { compose, withRequest } from '@withtyped/server';
|
|||
import dotenv from 'dotenv';
|
||||
import { findUp } from 'find-up';
|
||||
|
||||
import withAuth from './middleware/with-auth.js';
|
||||
import withHttpProxy from './middleware/with-http-proxy.js';
|
||||
import withPathname from './middleware/with-pathname.js';
|
||||
import withSpa from './middleware/with-spa.js';
|
||||
|
||||
dotenv.config({ path: await findUp('.env', {}) });
|
||||
|
||||
const { default: withAuth } = await import('./middleware/with-auth.js');
|
||||
const { default: withHttpProxy } = await import('./middleware/with-http-proxy.js');
|
||||
const { default: withPathname } = await import('./middleware/with-pathname.js');
|
||||
const { default: withSpa } = await import('./middleware/with-spa.js');
|
||||
|
||||
const { EnvSet } = await import('./env-set/index.js');
|
||||
const { default: router } = await import('./routes/index.js');
|
||||
|
||||
|
|
|
@ -14,6 +14,8 @@ import type { ZodType } from 'zod';
|
|||
import { z } from 'zod';
|
||||
|
||||
import type { Queries } from '#src/queries/index.js';
|
||||
import { createTenantsQueries } from '#src/queries/tenants.js';
|
||||
import { createUsersQueries } from '#src/queries/users.js';
|
||||
import { getDatabaseName } from '#src/queries/utils.js';
|
||||
import { insertInto } from '#src/utils/query.js';
|
||||
import { getTenantIdFromManagementApiIndicator } from '#src/utils/tenant.js';
|
||||
|
@ -28,12 +30,11 @@ export const tenantInfoGuard: ZodType<TenantInfo> = z.object({
|
|||
indicator: z.string(),
|
||||
});
|
||||
|
||||
export const createTenantsLibrary = (queries: Queries) => {
|
||||
const { getManagementApiLikeIndicatorsForUser, insertTenant, createTenantRole, insertAdminData } =
|
||||
queries.tenants;
|
||||
const { assignRoleToUser } = queries.users;
|
||||
export class TenantsLibrary {
|
||||
constructor(public readonly queries: Queries) {}
|
||||
|
||||
const getAvailableTenants = async (userId: string): Promise<TenantInfo[]> => {
|
||||
async getAvailableTenants(userId: string): Promise<TenantInfo[]> {
|
||||
const { getManagementApiLikeIndicatorsForUser } = this.queries.tenants;
|
||||
const { rows } = await getManagementApiLikeIndicatorsForUser(userId);
|
||||
|
||||
return rows
|
||||
|
@ -42,24 +43,30 @@ export const createTenantsLibrary = (queries: Queries) => {
|
|||
indicator,
|
||||
}))
|
||||
.filter((tenant): tenant is TenantInfo => Boolean(tenant.id));
|
||||
};
|
||||
}
|
||||
|
||||
const createNewTenant = async (forUserId: string): Promise<TenantInfo> => {
|
||||
const { client } = queries;
|
||||
const databaseName = await getDatabaseName(client);
|
||||
async createNewTenant(forUserId: string): Promise<TenantInfo> {
|
||||
const databaseName = await getDatabaseName(this.queries.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);
|
||||
const transaction = await this.queries.client.transaction();
|
||||
const tenants = createTenantsQueries(transaction);
|
||||
const users = createUsersQueries(transaction);
|
||||
|
||||
// Start
|
||||
await transaction.start();
|
||||
|
||||
// Init tenant
|
||||
await tenants.insertTenant(tenantModel);
|
||||
await tenants.createTenantRole(parentRole, role, password);
|
||||
|
||||
// Create admin data set (resource, roles, etc.)
|
||||
const adminDataInAdminTenant = createAdminDataInAdminTenant(tenantId);
|
||||
await insertAdminData(adminDataInAdminTenant);
|
||||
await insertAdminData(createAdminData(tenantId));
|
||||
await assignRoleToUser({
|
||||
await tenants.insertAdminData(adminDataInAdminTenant);
|
||||
await tenants.insertAdminData(createAdminData(tenantId));
|
||||
await users.assignRoleToUser({
|
||||
id: generateStandardId(),
|
||||
tenantId: adminTenantId,
|
||||
userId: forUserId,
|
||||
|
@ -68,12 +75,15 @@ export const createTenantsLibrary = (queries: Queries) => {
|
|||
|
||||
// Create initial configs
|
||||
await Promise.all([
|
||||
client.query(insertInto(createDefaultAdminConsoleConfig(tenantId), LogtoConfigs.table)),
|
||||
client.query(insertInto(createDefaultSignInExperience(tenantId), SignInExperiences.table)),
|
||||
transaction.query(insertInto(createDefaultAdminConsoleConfig(tenantId), LogtoConfigs.table)),
|
||||
transaction.query(
|
||||
insertInto(createDefaultSignInExperience(tenantId), SignInExperiences.table)
|
||||
),
|
||||
]);
|
||||
|
||||
return { id: tenantId, indicator: adminDataInAdminTenant.resource.indicator };
|
||||
};
|
||||
// End
|
||||
await transaction.end();
|
||||
|
||||
return { getAvailableTenants, createNewTenant };
|
||||
};
|
||||
return { id: tenantId, indicator: adminDataInAdminTenant.resource.indicator };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@ import { RequestError } from '@withtyped/server';
|
|||
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { EnvSet } from '#src/env-set/index.js';
|
||||
|
||||
const bearerTokenIdentifier = 'Bearer';
|
||||
|
||||
export const extractBearerTokenFromHeaders = ({ authorization }: IncomingHttpHeaders) => {
|
||||
|
@ -56,6 +58,14 @@ export default function withAuth<InputContext extends RequestContext>({
|
|||
})();
|
||||
|
||||
return async (context: InputContext, next: NextFunction<WithAuthContext<InputContext>>) => {
|
||||
const userId = context.request.headers['development-user-id']?.toString();
|
||||
|
||||
if (!EnvSet.isProduction && userId) {
|
||||
console.log(`Found dev user ID ${userId}, skip token validation.`);
|
||||
|
||||
return next({ ...context, auth: { id: userId, scopes: expectScopes } });
|
||||
}
|
||||
|
||||
const [getKey, issuer] = await getJwkSet;
|
||||
|
||||
const {
|
||||
|
|
|
@ -8,14 +8,15 @@ import {
|
|||
PredefinedScope,
|
||||
CreateRolesScope,
|
||||
} from '@logto/schemas';
|
||||
import type { PostgresQueryClient } from '@withtyped/postgres';
|
||||
import type { PostgreSql } from '@withtyped/postgres';
|
||||
import { dangerousRaw, id, sql } from '@withtyped/postgres';
|
||||
import type { Queryable } from '@withtyped/server';
|
||||
|
||||
import { insertInto } from '#src/utils/query.js';
|
||||
|
||||
export type TenantsQueries = ReturnType<typeof createTenantsQueries>;
|
||||
|
||||
export const createTenantsQueries = (client: PostgresQueryClient) => {
|
||||
export const createTenantsQueries = (client: Queryable<PostgreSql>) => {
|
||||
const getManagementApiLikeIndicatorsForUser = async (userId: string) =>
|
||||
client.query<{ indicator: string }>(sql`
|
||||
select resources.indicator from roles
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import type { UsersRole } from '@logto/schemas';
|
||||
import type { PostgresQueryClient } from '@withtyped/postgres';
|
||||
import type { PostgreSql } from '@withtyped/postgres';
|
||||
import type { Queryable } from '@withtyped/server';
|
||||
|
||||
import { insertInto } from '#src/utils/query.js';
|
||||
|
||||
export type UsersQueries = ReturnType<typeof createUsersQueries>;
|
||||
|
||||
export const createUsersQueries = (client: PostgresQueryClient) => {
|
||||
export const createUsersQueries = (client: Queryable<PostgreSql>) => {
|
||||
const assignRoleToUser = async (data: UsersRole) => client.query(insertInto(data, 'users_roles'));
|
||||
|
||||
return { assignRoleToUser };
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import type { PostgresQueryClient } from '@withtyped/postgres';
|
||||
import type { PostgreSql } from '@withtyped/postgres';
|
||||
import { sql } from '@withtyped/postgres';
|
||||
import type { Queryable } from '@withtyped/server';
|
||||
import { RequestError } from '@withtyped/server';
|
||||
|
||||
export const getDatabaseName = async (client: PostgresQueryClient) => {
|
||||
export const getDatabaseName = async (client: Queryable<PostgreSql>) => {
|
||||
const {
|
||||
rows: [first],
|
||||
} = await client.query<{ currentDatabase: string }>(sql`
|
||||
|
|
|
@ -1,21 +1,26 @@
|
|||
import { createRouter, RequestError } from '@withtyped/server';
|
||||
|
||||
import { createTenantsLibrary, tenantInfoGuard } from '#src/libraries/tenants.js';
|
||||
import { CloudScope } from '#src/consts/rbac.js';
|
||||
import { tenantInfoGuard, TenantsLibrary } from '#src/libraries/tenants.js';
|
||||
import type { WithAuthContext } from '#src/middleware/with-auth.js';
|
||||
import { Queries } from '#src/queries/index.js';
|
||||
|
||||
const { getAvailableTenants, createNewTenant } = createTenantsLibrary(Queries.default);
|
||||
const library = new TenantsLibrary(Queries.default);
|
||||
|
||||
export const tenants = createRouter<WithAuthContext, '/tenants'>('/tenants')
|
||||
.get('/', { response: tenantInfoGuard.array() }, async (context, next) => {
|
||||
return next({ ...context, json: await getAvailableTenants(context.auth.id) });
|
||||
return next({ ...context, json: await library.getAvailableTenants(context.auth.id) });
|
||||
})
|
||||
.post('/', { response: tenantInfoGuard }, async (context, next) => {
|
||||
const tenants = await getAvailableTenants(context.auth.id);
|
||||
if (!context.auth.scopes.includes(CloudScope.CreateTenant)) {
|
||||
throw new RequestError('Forbidden due to lack of permission.', 403);
|
||||
}
|
||||
|
||||
const tenants = await library.getAvailableTenants(context.auth.id);
|
||||
|
||||
if (tenants.length > 0) {
|
||||
throw new RequestError('The user already has a tenant.', 409);
|
||||
}
|
||||
|
||||
return next({ ...context, json: await createNewTenant(context.auth.id) });
|
||||
return next({ ...context, json: await library.createNewTenant(context.auth.id) });
|
||||
});
|
||||
|
|
|
@ -14,7 +14,7 @@ export type AdminData = {
|
|||
const defaultResourceId = 'management-api';
|
||||
const defaultScopeAllId = 'management-api-all';
|
||||
|
||||
// Consider combine this with `createAdminData()`
|
||||
// Consider combining this with `createAdminData()`
|
||||
/** The fixed Management API Resource for `default` tenant. */
|
||||
export const defaultManagementApi = Object.freeze({
|
||||
resource: {
|
||||
|
|
Loading…
Reference in a new issue