Skip to content

Commit 7572a60

Browse files
FEAT: add tenant creation api
1 parent 65513d8 commit 7572a60

File tree

11 files changed

+142
-4
lines changed

11 files changed

+142
-4
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
2+
import { ConfigService } from '@nestjs/config';
3+
import { GqlExecutionContext } from '@nestjs/graphql';
4+
5+
@Injectable()
6+
export class AuthKeyGuard implements CanActivate {
7+
constructor(private configService: ConfigService) {}
8+
9+
canActivate(context: ExecutionContext): boolean {
10+
const ctx = GqlExecutionContext.create(context).getContext();
11+
if (ctx) {
12+
const authKeyInHeader = ctx.headers['x-api-key'];
13+
if (authKeyInHeader) {
14+
const secretKey = this.configService.get('AUTH_KEY') as string;
15+
return secretKey === authKeyInHeader;
16+
}
17+
}
18+
return false;
19+
}
20+
}

src/authorization/authorization.module.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ import { UserService } from './service/user.service';
5151
import { UserServiceInterface } from './service/user.service.interface';
5252
import { UserCacheService } from './service/usercache.service';
5353
import { UserCacheServiceInterface } from './service/usercache.service.interface';
54+
import Tenant from './entity/tenant.entity';
55+
import { TenantResolver } from './resolver/tenant.resolver';
56+
import TenantService from './service/tenant.service';
57+
import { TenantRepository } from './repository/tenant.repository';
58+
import { TenantServiceInterface } from './service/tenant.service.interface';
5459

5560
@Module({
5661
imports: [
@@ -66,6 +71,7 @@ import { UserCacheServiceInterface } from './service/usercache.service.interface
6671
Role,
6772
GroupRole,
6873
RolePermission,
74+
Tenant,
6975
]),
7076
RedisCacheModule,
7177
],
@@ -91,6 +97,8 @@ import { UserCacheServiceInterface } from './service/usercache.service.interface
9197
UserGroupRepository,
9298
EntityPermissionRepository,
9399
LoggerService,
100+
TenantResolver,
101+
TenantRepository,
94102
{
95103
provide: EntityServiceInterface,
96104
useClass: EntityService,
@@ -127,6 +135,10 @@ import { UserCacheServiceInterface } from './service/usercache.service.interface
127135
provide: UserCacheServiceInterface,
128136
useClass: UserCacheService,
129137
},
138+
{
139+
provide: TenantServiceInterface,
140+
useClass: TenantService,
141+
},
130142
],
131143
exports: [
132144
{
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
1+
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
22
import BaseEntity from './base.entity';
33

44
@Entity()
5-
@Index('tenant_name_unique_idx', { synchronize: false })
65
class Tenant extends BaseEntity {
76
@PrimaryGeneratedColumn('uuid')
87
public id!: string;
98

109
@Column()
1110
public name!: string;
11+
12+
@Column({ unique: true })
13+
public domain!: string;
1214
}
1315

1416
export default Tenant;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { NotFoundException } from '@nestjs/common';
2+
3+
export class TenantNotFoundException extends NotFoundException {
4+
constructor(tenantDomain: string) {
5+
super(`Tenant with domain ${tenantDomain} not found`);
6+
}
7+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
input NewTenantInput {
2+
name: String!
3+
domain: String!
4+
}
5+
6+
type Tenant {
7+
id: ID!
8+
name: String!
9+
domain: String!
10+
}
11+
12+
type Mutation {
13+
createTenant(input: NewTenantInput!): Tenant
14+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { DataSource } from 'typeorm';
3+
4+
import { BaseRepository } from './base.repository';
5+
import Tenant from '../entity/tenant.entity';
6+
7+
@Injectable()
8+
export class TenantRepository extends BaseRepository<Tenant> {
9+
constructor(private dataSource: DataSource) {
10+
super(Tenant, dataSource);
11+
}
12+
13+
async getTenantByDomain(domain: string) {
14+
return this.findOneBy({ domain });
15+
}
16+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Args, Mutation, Resolver } from '@nestjs/graphql';
2+
import { NewTenantInput } from '../../schema/graphql.schema';
3+
import Tenant from '../entity/tenant.entity';
4+
import { Inject, UseGuards } from '@nestjs/common';
5+
import { AuthKeyGuard } from '../../authentication/authKey.guard';
6+
import { TenantServiceInterface } from '../service/tenant.service.interface';
7+
8+
@Resolver('Tenant')
9+
export class TenantResolver {
10+
constructor(
11+
@Inject(TenantServiceInterface)
12+
private tenantService: TenantServiceInterface,
13+
) {}
14+
15+
@UseGuards(AuthKeyGuard)
16+
@Mutation()
17+
async createTenant(
18+
@Args('input') tenantInput: NewTenantInput,
19+
): Promise<Tenant> {
20+
return this.tenantService.createTenant(tenantInput);
21+
}
22+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { NewTenantInput } from 'src/schema/graphql.schema';
2+
import Tenant from '../entity/tenant.entity';
3+
4+
export interface TenantServiceInterface {
5+
getTenantByDomain(domain: string): Promise<Tenant>;
6+
7+
createTenant(tenant: NewTenantInput): Promise<Tenant>;
8+
}
9+
10+
export const TenantServiceInterface = Symbol('TenantServiceInterface');
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Injectable } from '@nestjs/common';
2+
3+
import Tenant from '../entity/tenant.entity';
4+
import { TenantNotFoundException } from '../exception/tenant.exception';
5+
import { TenantRepository } from '../repository/tenant.repository';
6+
import { NewTenantInput } from '../../schema/graphql.schema';
7+
8+
@Injectable()
9+
export default class TenantService {
10+
constructor(private tenantRepository: TenantRepository) {}
11+
12+
async getTenantByDomain(domain: string): Promise<Tenant> {
13+
const tenant = await this.tenantRepository.getTenantByDomain(domain);
14+
if (!tenant) {
15+
throw new TenantNotFoundException(domain);
16+
}
17+
return tenant;
18+
}
19+
20+
async createTenant(tenant: NewTenantInput): Promise<Tenant> {
21+
return this.tenantRepository.save(tenant);
22+
}
23+
}

src/migrations/1733833844028-MultiTenantFeature.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ export class MultiTenantFeature1733833844028 implements MigrationInterface {
55

66
public async up(queryRunner: QueryRunner): Promise<void> {
77
await queryRunner.query(
8-
`CREATE TABLE "tenant" ("deleted_at" TIMESTAMP, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, CONSTRAINT "PK_da8c6efd67bb301e810e56ac139" PRIMARY KEY ("id"))`,
8+
`CREATE TABLE "tenant" ("deleted_at" TIMESTAMP, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, "domain" character varying NOT NULL, CONSTRAINT "UQ_97b9c4dae58b30f5bd875f241ab" UNIQUE ("domain"), CONSTRAINT "PK_da8c6efd67bb301e810e56ac139" PRIMARY KEY ("id"))`,
99
);
1010
const tenants = await queryRunner.query(
11-
`INSERT INTO "tenant" ("name") VALUES ('Default Tenant') RETURNING id`,
11+
`INSERT INTO "tenant" ("name", "domain") VALUES ('Default Tenant', 'default.domain') RETURNING id`,
1212
);
1313
const tenantId = tenants[0].id;
1414
await queryRunner.query(

0 commit comments

Comments
 (0)