Skip to content

Commit fed0dc8

Browse files
committed
WIP
1 parent 5de2dbc commit fed0dc8

20 files changed

+322
-56
lines changed

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": "2f4e657d-1857-42dd-b53c-92d32990935f" },
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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
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
@@ -317,6 +317,7 @@ type Manufacturer {
317317
coordinates: Coordinates
318318
id: ID!
319319
name: String!
320+
tenant: Tenant!
320321
updatedAt: DateTime!
321322
}
322323

@@ -557,6 +558,7 @@ type Product {
557558
status: ProductStatus!
558559
tags: [ProductTag!]!
559560
tagsWithStatus: [ProductToTag!]!
561+
tenant: Tenant!
560562
title: String!
561563
type: ProductType!
562564
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: 22 additions & 8 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

@@ -34,12 +38,22 @@ export class AccessControlService extends AbstractAccessControlService {
3438
}
3539

3640
async getAvailableContentScopes(): Promise<ContentScopeWithLabel[]> {
37-
const departments = await this.entityManager.find(Department, {});
41+
const store = this.asyncLocalStorage.getStore();
42+
console.log("store", store);
3843

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

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/db/db.module.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,42 @@ import { Module } from "@nestjs/common";
33
import { FixturesModule } from "@src/db/fixtures/fixtures.module";
44

55
import { MigrateCommand } from "./migrate.command";
6-
import { ormConfig } from "./ormconfig";
6+
import { createOrmConfig } from "./ormconfig";
77

88
@Module({
9-
imports: [MikroOrmModule.forRoot({ ormConfig }), FixturesModule],
9+
imports: [
10+
// MikroOrmModule.forRoot({
11+
// ormConfig: createOrmConfig({ user: "app_user", password: "secret" }),
12+
// }),
13+
// MikroOrmModule.forRoot({
14+
// ormConfig: createOrmConfig({
15+
// user: process.env.POSTGRESQL_USER,
16+
// password: Buffer.from(process.env.POSTGRESQL_PWD ?? "", "base64")
17+
// .toString("utf-8")
18+
// .trim(),
19+
// contextName: "admin",
20+
// }),
21+
// }),
22+
23+
MikroOrmModule.forRoot({
24+
ormConfig: createOrmConfig({
25+
user: process.env.POSTGRESQL_USER,
26+
password: Buffer.from(process.env.POSTGRESQL_PWD ?? "", "base64")
27+
.toString("utf-8")
28+
.trim(),
29+
}),
30+
}),
31+
MikroOrmModule.forRoot({
32+
ormConfig: createOrmConfig({
33+
user: process.env.POSTGRESQL_USER,
34+
password: Buffer.from(process.env.POSTGRESQL_PWD ?? "", "base64")
35+
.toString("utf-8")
36+
.trim(),
37+
contextName: "admin",
38+
}),
39+
}),
40+
FixturesModule,
41+
],
1042
providers: [MigrateCommand],
1143
})
1244
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();

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ import { ProductCategoryType } from "@src/products/entities/product-category-typ
1010

1111
import { FileUploadsFixtureService } from "./generators/file-uploads-fixture.service";
1212
import { ProductsFixtureService } from "./generators/products-fixture.service";
13+
import { TenantFixtureService } from "./generators/tenant-fixture.service";
1314

1415
@Module({
1516
imports: [ConfigModule, MikroOrmModule.forFeature([Product, ProductCategory, ProductCategoryType, Manufacturer, AttachedDocument])],
16-
providers: [FixturesCommand, FileUploadsFixtureService, ProductsFixtureService],
17+
providers: [FixturesCommand, FileUploadsFixtureService, ProductsFixtureService, TenantFixtureService],
1718
})
1819
export class FixturesModule {}

demo-saas/api/src/db/fixtures/generators/products-fixture.service.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Product, ProductStatus } from "@src/products/entities/product.entity";
88
import { ProductCategory } from "@src/products/entities/product-category.entity";
99
import { ProductCategoryType } from "@src/products/entities/product-category-type.entity";
1010
import { ProductType } from "@src/products/entities/product-type.enum";
11+
import { Tenant } from "@src/tenant/entities/tenant.entity";
1112
import { format } from "date-fns";
1213

1314
@Injectable()
@@ -26,6 +27,7 @@ export class ProductsFixtureService {
2627
this.logger.log("Generating manufacturers...");
2728

2829
const manufacturers: Manufacturer[] = [];
30+
const tenants = await this.entityManager.findAll(Tenant, {});
2931

3032
for (let i = 0; i < 10; i++) {
3133
const manufacturer = this.manufacturersRepository.create({
@@ -55,6 +57,7 @@ export class ProductsFixtureService {
5557
country: faker.location.country(),
5658
},
5759
},
60+
tenant: faker.helpers.arrayElement(tenants),
5861
});
5962

6063
this.entityManager.persist(manufacturer);
@@ -93,6 +96,7 @@ export class ProductsFixtureService {
9396
for (let i = 0; i < 100; i++) {
9497
const title = faker.commerce.productName();
9598

99+
const manufacturer = faker.helpers.arrayElement(manufacturers);
96100
const product = this.productsRepository.create({
97101
id: faker.string.uuid(),
98102
title: faker.commerce.productName(),
@@ -110,7 +114,8 @@ export class ProductsFixtureService {
110114
attachedBlocks: [{ type: "pixelImage", props: {} }],
111115
activeType: "pixelImage",
112116
}).transformToBlockData(),
113-
manufacturer: faker.helpers.arrayElement(manufacturers),
117+
manufacturer,
118+
tenant: manufacturer.tenant,
114119
});
115120

116121
this.entityManager.persist(product);

0 commit comments

Comments
 (0)