0
Fork 0
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:
Gao Sun 2023-02-20 14:49:08 +08:00 committed by GitHub
parent 0f3e256a87
commit 86ecd79b36
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 70 additions and 39 deletions

View file

@ -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",

View file

@ -0,0 +1,3 @@
export enum CloudScope {
CreateTenant = 'create:tenant',
}

View file

@ -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');

View file

@ -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 };
}
}

View file

@ -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 {

View file

@ -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

View file

@ -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 };

View file

@ -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`

View file

@ -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) });
});

View file

@ -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: {