Skip to content

Commit 15fae66

Browse files
committed
WIP
1 parent 3cfe27b commit 15fae66

23 files changed

+350
-57
lines changed

.env

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ POSTGRESQL_PWD=dml2aWQK
1616
POSTGRESQL_PWD_DECODED=vivid
1717
POSTGRESQL_DB=main
1818
POSTGRESQL_USE_SSL=false
19+
POSTGRESQL_RLS_USER=rls_user
20+
POSTGRESQL_RLS_PWD=dml2aWQK
1921

2022
# fileStorage
2123
FILE_STORAGE_PATH="../uploads"

demo-saas/admin/src/common/apollo/createApolloClient.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ApolloClient, ApolloLink, HttpLink, InMemoryCache } from "@apollo/client";
2+
import { setContext } from "@apollo/client/link/context";
23
import { createErrorDialogApolloLink } from "@comet/admin";
34
import { includeInvisibleContentContext } from "@comet/cms-admin";
45
import fragmentTypes from "@src/fragmentTypes.json";
@@ -9,7 +10,16 @@ export const createApolloClient = (apiUrl: string) => {
910
credentials: "include",
1011
});
1112

12-
const link = ApolloLink.from([createErrorDialogApolloLink(), includeInvisibleContentContext, httpLink]);
13+
const tenantContext = setContext((_, { headers }) => {
14+
return {
15+
headers: {
16+
...headers,
17+
...{ "x-tenant-id": "f9c86c6c-0625-46c0-9be5-bee3a14cc7f4" },
18+
},
19+
};
20+
});
21+
22+
const link = ApolloLink.from([createErrorDialogApolloLink(), includeInvisibleContentContext, tenantContext, httpLink]);
1323

1424
const cache = new InMemoryCache({
1525
possibleTypes: fragmentTypes.possibleTypes,

demo-saas/api/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010
"build": "pnpm clean && pnpm run intl:compile && pnpm api-generator && nest build",
1111
"check-node-version": "check-node-version --node $(cat ../../.nvmrc)",
1212
"clean": "rimraf dist",
13-
"console": "NODE_OPTIONS='--max-old-space-size=256' dotenv -e .env.secrets -e .env.local -e .env -e .env.site-configs -- ts-node --transpile-only -r tsconfig-paths/register src/console.ts",
13+
"console": "NODE_OPTIONS='--max-old-space-size=256' CALLED_FROM_CLI=true dotenv -e .env.secrets -e .env.local -e .env -e .env.site-configs -- ts-node --transpile-only -r tsconfig-paths/register src/console.ts",
1414
"console:prod": "node dist/console.js",
1515
"db:migrate": "pnpm console migrate --",
1616
"db:migrate:prod": "pnpm console:prod migrate --",
17-
"dev:nest": "pnpm clean && pnpm intl:compile && pnpm db:migrate && pnpm console createBlockIndexViews && NODE_OPTIONS='--max-old-space-size=1024' dotenv -e .env.secrets -e .env.local -e .env -e .env.site-configs -- nest start --debug --watch --preserveWatchOutput",
17+
"dev:nest": "pnpm clean && pnpm intl:compile && NODE_OPTIONS='--max-old-space-size=1024' dotenv -e .env.secrets -e .env.local -e .env -e .env.site-configs -- nest start --debug --watch --preserveWatchOutput",
1818
"dev:reload": "rimraf src/reload.ts && chokidar \"node_modules/@comet/cms-api/lib/**\" -s -c \"echo '// change' >> src/reload.ts\"",
1919
"fixtures": "pnpm console fixtures --",
2020
"fixtures:prod": "pnpm console:prod fixtures --",

demo-saas/api/schema.gql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ type Manufacturer {
311311
coordinates: Coordinates
312312
id: ID!
313313
name: String!
314+
tenant: Tenant!
314315
updatedAt: DateTime!
315316
}
316317

@@ -551,6 +552,7 @@ type Product {
551552
status: ProductStatus!
552553
tags: [ProductTag!]!
553554
tagsWithStatus: [ProductToTag!]!
555+
tenant: Tenant!
554556
title: String!
555557
type: ProductType!
556558
updatedAt: DateTime!

demo-saas/api/src/app.module.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,17 @@ import {
1818
UserPermissionsModule,
1919
} from "@comet/cms-api";
2020
import { ApolloDriver, ApolloDriverConfig, ValidationError } from "@nestjs/apollo";
21-
import { DynamicModule, Module } from "@nestjs/common";
22-
import { ModuleRef } from "@nestjs/core";
21+
import { DynamicModule, MiddlewareConsumer, Module } from "@nestjs/common";
22+
import { APP_INTERCEPTOR, ModuleRef } from "@nestjs/core";
2323
import { Enhancer, GraphQLModule } from "@nestjs/graphql";
2424
import { AppPermission } from "@src/auth/app-permission.enum";
2525
import { Config } from "@src/config/config";
2626
import { ConfigModule } from "@src/config/config.module";
2727
import { ContentGenerationService } from "@src/content-generation/content-generation.service";
2828
import { DbModule } from "@src/db/db.module";
2929
import { TranslationModule } from "@src/translation/translation.module";
30-
import { Request } from "express";
30+
import { AsyncLocalStorage } from "async_hooks";
31+
import { NextFunction, Request } from "express";
3132

3233
import { AccessControlService } from "./auth/access-control.service";
3334
import { AuthModule, SYSTEM_USER_NAME } from "./auth/auth.module";
@@ -36,14 +37,23 @@ import { DepartmentsModule } from "./department/departments.module";
3637
import { OpenTelemetryModule } from "./open-telemetry/open-telemetry.module";
3738
import { ProductsModule } from "./products/products.module";
3839
import { StatusModule } from "./status/status.module";
40+
import { TenantInterceptor } from "./tenant/tenant.interceptor";
3941
import { TenantsModule } from "./tenant/tenants.module";
4042

4143
@Module({})
4244
export class AppModule {
45+
constructor(private readonly asyncLocalStorage: AsyncLocalStorage<{ tenantId?: string }>) {}
46+
4347
static forRoot(config: Config): DynamicModule {
4448
const authModule = AuthModule.forRoot(config);
4549

4650
return {
51+
providers: [
52+
{
53+
provide: APP_INTERCEPTOR,
54+
useClass: TenantInterceptor,
55+
},
56+
],
4757
module: AppModule,
4858
imports: [
4959
ConfigModule.forRoot(config),
@@ -83,13 +93,18 @@ export class AppModule {
8393
}),
8494
authModule,
8595
UserPermissionsModule.forRootAsync({
86-
useFactory: (userService: UserService, accessControlService: AccessControlService) => ({
96+
useFactory: (
97+
userService: UserService,
98+
accessControlService: AccessControlService,
99+
asyncLocalStorage: AsyncLocalStorage<{ tenantId?: string }>,
100+
) => ({
87101
availableContentScopes: () => accessControlService.getAvailableContentScopes(),
88102
userService,
89103
accessControlService,
90104
systemUsers: [SYSTEM_USER_NAME],
105+
asyncLocalStorage,
91106
}),
92-
inject: [UserService, AccessControlService],
107+
inject: [UserService, AccessControlService, AsyncLocalStorage<{ tenantId?: string }>],
93108
imports: [authModule],
94109
AppPermission,
95110
}),
@@ -151,4 +166,15 @@ export class AppModule {
151166
],
152167
};
153168
}
169+
170+
configure(consumer: MiddlewareConsumer) {
171+
consumer
172+
.apply((req: Request, res: Response, next: NextFunction) => {
173+
const tenantIdHeader = req.headers["x-tenant-id"];
174+
const tenantId = typeof tenantIdHeader === "string" ? tenantIdHeader : undefined;
175+
const store: { tenantId?: string } = tenantId ? { tenantId } : {};
176+
this.asyncLocalStorage.run(store, () => next());
177+
})
178+
.forRoutes("*path");
179+
}
154180
}

demo-saas/api/src/auth/access-control.service.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,17 @@ import {
77
User,
88
UserPermissions,
99
} from "@comet/cms-api";
10-
import { EntityManager } from "@mikro-orm/core";
10+
import { EntityManager } from "@mikro-orm/postgresql";
1111
import { Injectable } from "@nestjs/common";
1212
import { Department } from "@src/department/entities/department.entity";
13+
import { AsyncLocalStorage } from "async_hooks";
1314

1415
@Injectable()
1516
export class AccessControlService extends AbstractAccessControlService {
16-
constructor(private readonly entityManager: EntityManager) {
17+
constructor(
18+
private readonly entityManager: EntityManager,
19+
private readonly asyncLocalStorage: AsyncLocalStorage<{ tenantId?: string }>,
20+
) {
1721
super();
1822
}
1923

@@ -39,11 +43,19 @@ export class AccessControlService extends AbstractAccessControlService {
3943
}
4044

4145
async getAvailableContentScopes(): Promise<ContentScopeWithLabel[]> {
42-
const departments = await this.entityManager.find(Department, {});
46+
const store = this.asyncLocalStorage.getStore();
4347

44-
return departments.map((department) => ({
45-
scope: { department: department.id },
46-
label: { department: department.name },
47-
}));
48+
return this.entityManager.transactional(async (entityManager) => {
49+
if (store?.tenantId) {
50+
await entityManager.execute(`SELECT set_config('app.tenant', '${store.tenantId}', TRUE)`);
51+
}
52+
53+
const departments = await this.entityManager.findAll(Department, { where: { tenant: { id: store?.tenantId } } });
54+
return departments.map((department) => ({
55+
department: department.id,
56+
scope: { department: department.id },
57+
label: { department: department.name },
58+
}));
59+
});
4860
}
4961
}

demo-saas/api/src/auth/auth.module.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { DynamicModule, Module } from "@nestjs/common";
33
import { APP_GUARD } from "@nestjs/core";
44
import { JwtModule } from "@nestjs/jwt";
55
import { Config } from "@src/config/config";
6+
import { AsyncLocalStorage } from "async_hooks";
67

78
import { AccessControlService } from "./access-control.service";
89
import { UserService } from "./user.service";
@@ -16,6 +17,10 @@ export class AuthModule {
1617
module: AuthModule,
1718
imports: [JwtModule],
1819
providers: [
20+
{
21+
provide: AsyncLocalStorage,
22+
useValue: new AsyncLocalStorage<{ tenantId?: string }>(),
23+
},
1924
createAuthResolver({
2025
postLogoutRedirectUri: config.auth.postLogoutRedirectUri,
2126
endSessionEndpoint: config.auth.idpEndSessionEndpoint,
@@ -49,7 +54,7 @@ export class AuthModule {
4954
],
5055
),
5156
],
52-
exports: [AccessControlService, UserService],
57+
exports: [AccessControlService, UserService, AsyncLocalStorage],
5358
};
5459
}
5560
}

demo-saas/api/src/console.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import { CommandFactory } from "nest-commander";
44

55
import { createConfig } from "./config/config";
66

7+
// Set flag to indicate this is running from CLI (must be set before AppModule is created)
8+
process.env.CALLED_FROM_CLI = "true";
9+
710
const config = createConfig(process.env);
811
const appModule = AppModule.forRoot(config);
912

demo-saas/api/src/db/db.module.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
import { MikroOrmModule } from "@comet/cms-api";
21
import { Module } from "@nestjs/common";
32
import { FixturesModule } from "@src/db/fixtures/fixtures.module";
43

54
import { MigrateCommand } from "./migrate.command";
6-
import { ormConfig } from "./ormconfig";
5+
import { createOrmModules } from "./orm-modules.factory";
76

87
@Module({
9-
imports: [MikroOrmModule.forRoot({ ormConfig }), FixturesModule],
8+
imports: [...createOrmModules(), FixturesModule],
109
providers: [MigrateCommand],
1110
})
1211
export class DbModule {}

demo-saas/api/src/db/fixtures/fixtures.command.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Command, CommandRunner } from "nest-commander";
99

1010
import { FileUploadsFixtureService } from "./generators/file-uploads-fixture.service";
1111
import { ProductsFixtureService } from "./generators/products-fixture.service";
12+
import { TenantFixtureService } from "./generators/tenant-fixture.service";
1213

1314
@Command({
1415
name: "fixtures",
@@ -27,6 +28,7 @@ export class FixturesCommand extends CommandRunner {
2728
private readonly blobStorageBackendService: BlobStorageBackendService,
2829
private readonly productsFixtureService: ProductsFixtureService,
2930
private readonly fileUploadsFixtureService: FileUploadsFixtureService,
31+
private readonly tenantFixtureService: TenantFixtureService,
3032
private readonly orm: MikroORM,
3133
) {
3234
super();
@@ -61,6 +63,7 @@ export class FixturesCommand extends CommandRunner {
6163
this.logger.log("Generate File Uploads...");
6264
await this.fileUploadsFixtureService.generateFileUploads();
6365

66+
await this.tenantFixtureService.generate();
6467
await this.productsFixtureService.generate();
6568

6669
multiBar.stop();

0 commit comments

Comments
 (0)