feat(ee): add multi-organization workspaces feature
- Add `user_tenants` system DB migration for many-to-many user-to-org relationship - Add backfill migration to populate existing users into join table - Add `UserTenant` Objection.js system model and register globally - Enforce org membership validation in `TenancyGlobalGuard` (security) - Add `modules/ee/Workspaces` with full CRUD: create, list, delete, build-status - Add `CreateUserTenantOnSignupSubscriber` for backward-compatible signup flow - Register `WorkspacesModule` in `AppModule` API endpoints: GET /workspaces - list all orgs user belongs to POST /workspaces - create new org (async build) GET /workspaces/build/:jobId - poll build job status DELETE /workspaces/:orgId - delete org (owner only) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+26
@@ -0,0 +1,26 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.createTable('user_tenants', (table) => {
|
||||
table.bigIncrements('id');
|
||||
table
|
||||
.integer('user_id')
|
||||
.unsigned()
|
||||
.notNullable()
|
||||
.references('id')
|
||||
.inTable('users')
|
||||
.onDelete('CASCADE');
|
||||
table
|
||||
.bigInteger('tenant_id')
|
||||
.unsigned()
|
||||
.notNullable()
|
||||
.references('id')
|
||||
.inTable('tenants')
|
||||
.onDelete('CASCADE');
|
||||
table.string('role', 20).notNullable().defaultTo('owner');
|
||||
table.timestamps();
|
||||
table.unique(['user_id', 'tenant_id']);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.dropTableIfExists('user_tenants');
|
||||
};
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.raw(`
|
||||
INSERT IGNORE INTO user_tenants (user_id, tenant_id, role, created_at, updated_at)
|
||||
SELECT id, tenant_id, 'owner', NOW(), NOW()
|
||||
FROM users
|
||||
WHERE tenant_id IS NOT NULL
|
||||
`);
|
||||
};
|
||||
|
||||
exports.down = function () {
|
||||
// Cannot safely reverse a backfill.
|
||||
return Promise.resolve();
|
||||
};
|
||||
@@ -81,6 +81,7 @@ import { PaymentLinksModule } from '../PaymentLinks/PaymentLinks.module';
|
||||
import { RolesModule } from '../Roles/Roles.module';
|
||||
import { SubscriptionModule } from '../Subscription/Subscription.module';
|
||||
import { OrganizationModule } from '../Organization/Organization.module';
|
||||
import { WorkspacesModule } from '../ee/Workspaces/Workspaces.module';
|
||||
import { TenantDBManagerModule } from '../TenantDBManager/TenantDBManager.module';
|
||||
import { PaymentServicesModule } from '../PaymentServices/PaymentServices.module';
|
||||
import { AuthModule } from '../Auth/Auth.module';
|
||||
@@ -243,6 +244,7 @@ import { AppThrottleModule } from './AppThrottle.module';
|
||||
RolesModule,
|
||||
SubscriptionModule,
|
||||
OrganizationModule,
|
||||
WorkspacesModule,
|
||||
TenantDBManagerModule,
|
||||
PaymentServicesModule,
|
||||
LoopsModule,
|
||||
|
||||
@@ -7,9 +7,10 @@ import { SystemKnexConnection } from '../SystemDB/SystemDB.constants';
|
||||
import { SystemModelsConnection } from './SystemModels.constants';
|
||||
import { SystemUser } from '../models/SystemUser';
|
||||
import { TenantMetadata } from '../models/TenantMetadataModel';
|
||||
import { UserTenant } from '../models/UserTenant.model';
|
||||
import { TenantRepository } from '../repositories/Tenant.repository';
|
||||
|
||||
const models = [SystemUser, PlanSubscription, TenantModel, TenantMetadata];
|
||||
const models = [SystemUser, PlanSubscription, TenantModel, TenantMetadata, UserTenant];
|
||||
|
||||
const modelProviders = models.map((model) => {
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Model } from 'objection';
|
||||
import { BaseModel } from '@/models/Model';
|
||||
import { TenantModel } from './TenantModel';
|
||||
|
||||
export type UserTenantRole = 'owner' | 'member';
|
||||
|
||||
export class UserTenant extends BaseModel {
|
||||
public userId: number;
|
||||
public tenantId: number;
|
||||
public role: UserTenantRole;
|
||||
public tenant: TenantModel;
|
||||
|
||||
static get tableName() {
|
||||
return 'user_tenants';
|
||||
}
|
||||
|
||||
static get relationMappings() {
|
||||
const { SystemUser } = require('./SystemUser');
|
||||
const { TenantModel } = require('./TenantModel');
|
||||
|
||||
return {
|
||||
user: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: SystemUser,
|
||||
join: { from: 'user_tenants.userId', to: 'users.id' },
|
||||
},
|
||||
tenant: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: TenantModel,
|
||||
join: { from: 'user_tenants.tenantId', to: 'tenants.id' },
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
import { isEmpty } from 'lodash';
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
UnauthorizedException,
|
||||
SetMetadata,
|
||||
Inject,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import { IS_PUBLIC_ROUTE } from '../Auth/Auth.constants';
|
||||
import { getAuthApiKey } from '../Auth/Auth.utils';
|
||||
import { UserTenant } from '../System/models/UserTenant.model';
|
||||
import { TenantModel } from '../System/models/TenantModel';
|
||||
|
||||
export const IS_TENANT_AGNOSTIC = 'IS_TENANT_AGNOSTIC';
|
||||
|
||||
@@ -16,33 +19,61 @@ export const TenantAgnosticRoute = () => SetMetadata(IS_TENANT_AGNOSTIC, true);
|
||||
|
||||
@Injectable()
|
||||
export class TenancyGlobalGuard implements CanActivate {
|
||||
constructor(private reflector: Reflector) {}
|
||||
constructor(
|
||||
private reflector: Reflector,
|
||||
private readonly cls: ClsService,
|
||||
|
||||
@Inject(UserTenant.name)
|
||||
private readonly userTenantModel: typeof UserTenant,
|
||||
|
||||
@Inject(TenantModel.name)
|
||||
private readonly tenantModel: typeof TenantModel,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validates the organization ID in the request headers.
|
||||
* @param {ExecutionContext} context
|
||||
* @returns {boolean}
|
||||
* Validates the organization ID in the request headers and enforces
|
||||
* that the authenticated user is a member of the requested organization.
|
||||
*/
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const organizationId = request.headers['organization-id'];
|
||||
const authorization = request.headers['authorization']?.trim();
|
||||
const isAuthApiKey = !!getAuthApiKey(authorization || '');
|
||||
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(
|
||||
IS_PUBLIC_ROUTE,
|
||||
[context.getHandler(), context.getClass()],
|
||||
);
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_ROUTE, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
const isTenantAgnostic = this.reflector.getAllAndOverride<boolean>(
|
||||
IS_TENANT_AGNOSTIC,
|
||||
[context.getHandler(), context.getClass()],
|
||||
);
|
||||
|
||||
if (isPublic || isTenantAgnostic || isAuthApiKey) {
|
||||
return true;
|
||||
}
|
||||
if (!organizationId) {
|
||||
throw new UnauthorizedException('Organization ID is required.');
|
||||
}
|
||||
|
||||
// Validate that the authenticated user is a member of the requested organization.
|
||||
const userId = this.cls.get<number>('userId');
|
||||
|
||||
const tenant = await this.tenantModel.query().findOne({ organizationId });
|
||||
if (!tenant) {
|
||||
throw new UnauthorizedException('Organization not found.');
|
||||
}
|
||||
|
||||
const membership = await this.userTenantModel
|
||||
.query()
|
||||
.findOne({ userId, tenantId: tenant.id });
|
||||
|
||||
if (!membership) {
|
||||
throw new UnauthorizedException(
|
||||
'You do not have access to this organization.',
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpCode,
|
||||
Param,
|
||||
Post,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import { TenantAgnosticRoute } from '@/modules/Tenancy/TenancyGlobal.guard';
|
||||
import { IgnoreUserVerifiedRoute } from '@/modules/Auth/guards/EnsureUserVerified.guard';
|
||||
import { IgnoreTenantInitializedRoute } from '@/modules/Tenancy/EnsureTenantIsInitialized.guard';
|
||||
import { IgnoreTenantSeededRoute } from '@/modules/Tenancy/EnsureTenantIsSeeded.guards';
|
||||
import { IgnoreTenantModelsInitialize } from '@/modules/Tenancy/TenancyInitializeModels.guard';
|
||||
import { CreateWorkspaceService } from './commands/CreateWorkspace.service';
|
||||
import { DeleteWorkspaceService } from './commands/DeleteWorkspace.service';
|
||||
import { GetWorkspacesService } from './queries/GetWorkspaces.service';
|
||||
import { GetWorkspaceBuildJobService } from './queries/GetWorkspaceBuildJob.service';
|
||||
import { CreateWorkspaceDto } from './dtos/CreateWorkspace.dto';
|
||||
import {
|
||||
CreateWorkspaceResponseDto,
|
||||
WorkspaceDto,
|
||||
} from './dtos/WorkspaceResponse.dto';
|
||||
|
||||
@ApiTags('Workspaces')
|
||||
@Controller('workspaces')
|
||||
export class WorkspacesController {
|
||||
constructor(
|
||||
private readonly createWorkspaceService: CreateWorkspaceService,
|
||||
private readonly deleteWorkspaceService: DeleteWorkspaceService,
|
||||
private readonly getWorkspacesService: GetWorkspacesService,
|
||||
private readonly getWorkspaceBuildJobService: GetWorkspaceBuildJobService,
|
||||
private readonly cls: ClsService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Lists all organizations the authenticated user belongs to.
|
||||
* No `organization-id` header required.
|
||||
*/
|
||||
@Get()
|
||||
@TenantAgnosticRoute()
|
||||
@IgnoreUserVerifiedRoute()
|
||||
@ApiOperation({ summary: 'List workspaces the authenticated user belongs to' })
|
||||
async listWorkspaces(): Promise<WorkspaceDto[]> {
|
||||
const userId = this.cls.get<number>('userId');
|
||||
return this.getWorkspacesService.getWorkspaces(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new workspace (organization) for the authenticated user.
|
||||
* The organization database is built asynchronously via a background job.
|
||||
* No `organization-id` header required.
|
||||
*/
|
||||
@Post()
|
||||
@TenantAgnosticRoute()
|
||||
@IgnoreUserVerifiedRoute()
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: 'Create a new workspace' })
|
||||
async createWorkspace(
|
||||
@Body() dto: CreateWorkspaceDto,
|
||||
): Promise<CreateWorkspaceResponseDto> {
|
||||
const userId = this.cls.get<number>('userId');
|
||||
return this.createWorkspaceService.createWorkspace(userId, dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a workspace. Only the workspace owner is permitted to delete it.
|
||||
* Requires `organization-id` header (must match the path param).
|
||||
*/
|
||||
@Delete(':organizationId')
|
||||
@IgnoreTenantInitializedRoute()
|
||||
@IgnoreTenantSeededRoute()
|
||||
@IgnoreTenantModelsInitialize()
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: 'Delete a workspace (owner only)' })
|
||||
async deleteWorkspace(
|
||||
@Param('organizationId') organizationId: string,
|
||||
): Promise<void> {
|
||||
const userId = this.cls.get<number>('userId');
|
||||
await this.deleteWorkspaceService.deleteWorkspace(userId, organizationId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Polls the build job status for a workspace being provisioned.
|
||||
* No `organization-id` header required.
|
||||
*/
|
||||
@Get('build/:buildJobId')
|
||||
@TenantAgnosticRoute()
|
||||
@ApiOperation({ summary: 'Get workspace build job status' })
|
||||
async buildJobStatus(@Param('buildJobId') buildJobId: string) {
|
||||
return this.getWorkspaceBuildJobService.getJobDetails(buildJobId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { WorkspacesController } from './Workspaces.controller';
|
||||
import { CreateWorkspaceService } from './commands/CreateWorkspace.service';
|
||||
import { DeleteWorkspaceService } from './commands/DeleteWorkspace.service';
|
||||
import { GetWorkspacesService } from './queries/GetWorkspaces.service';
|
||||
import { GetWorkspaceBuildJobService } from './queries/GetWorkspaceBuildJob.service';
|
||||
import { CreateUserTenantOnSignupSubscriber } from './subscribers/CreateUserTenantOnSignup.subscriber';
|
||||
import { OrganizationBuildQueue } from '@/modules/Organization/Organization.types';
|
||||
import { InjectSystemModel } from '@/modules/System/SystemModels/SystemModels.module';
|
||||
import { UserTenant } from '@/modules/System/models/UserTenant.model';
|
||||
import { TenantDBManagerModule } from '@/modules/TenantDBManager/TenantDBManager.module';
|
||||
import { GetBuildOrganizationBuildJob } from '@/modules/Organization/commands/GetBuildOrganizationJob.service';
|
||||
import { TenantRepository } from '@/modules/System/repositories/Tenant.repository';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
BullModule.registerQueue({ name: OrganizationBuildQueue }),
|
||||
TenantDBManagerModule,
|
||||
],
|
||||
controllers: [WorkspacesController],
|
||||
providers: [
|
||||
InjectSystemModel(UserTenant),
|
||||
TenantRepository,
|
||||
CreateWorkspaceService,
|
||||
DeleteWorkspaceService,
|
||||
GetWorkspacesService,
|
||||
GetWorkspaceBuildJobService,
|
||||
CreateUserTenantOnSignupSubscriber,
|
||||
GetBuildOrganizationBuildJob,
|
||||
],
|
||||
})
|
||||
export class WorkspacesModule {}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Queue } from 'bullmq';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { InjectQueue } from '@nestjs/bullmq';
|
||||
import { UserTenant } from '@/modules/System/models/UserTenant.model';
|
||||
import { TenantRepository } from '@/modules/System/repositories/Tenant.repository';
|
||||
import {
|
||||
OrganizationBuildQueue,
|
||||
OrganizationBuildQueueJob,
|
||||
OrganizationBuildQueueJobPayload,
|
||||
} from '@/modules/Organization/Organization.types';
|
||||
import { transformBuildDto } from '@/modules/Organization/Organization.utils';
|
||||
import { CreateWorkspaceDto } from '../dtos/CreateWorkspace.dto';
|
||||
import { CreateWorkspaceResponseDto } from '../dtos/WorkspaceResponse.dto';
|
||||
|
||||
@Injectable()
|
||||
export class CreateWorkspaceService {
|
||||
constructor(
|
||||
@Inject(UserTenant.name)
|
||||
private readonly userTenantModel: typeof UserTenant,
|
||||
|
||||
private readonly tenantRepository: TenantRepository,
|
||||
|
||||
@InjectQueue(OrganizationBuildQueue)
|
||||
private readonly organizationBuildQueue: Queue,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Creates a new workspace (organization) for the authenticated user.
|
||||
* - Creates a new tenant row with a unique organizationId.
|
||||
* - Links the user as owner via user_tenants.
|
||||
* - Saves organization metadata.
|
||||
* - Enqueues the tenant database build job.
|
||||
*/
|
||||
async createWorkspace(
|
||||
userId: number,
|
||||
dto: CreateWorkspaceDto,
|
||||
): Promise<CreateWorkspaceResponseDto> {
|
||||
// Create the new tenant row.
|
||||
const tenant = await this.tenantRepository.createWithUniqueOrgId();
|
||||
|
||||
// Link the authenticated user as the owner of this new workspace.
|
||||
await this.userTenantModel.query().insert({
|
||||
userId,
|
||||
tenantId: tenant.id,
|
||||
role: 'owner',
|
||||
});
|
||||
|
||||
// Transform and persist the organization metadata.
|
||||
const transformedDto = transformBuildDto(dto);
|
||||
await this.tenantRepository.saveMetadata(tenant.id, transformedDto);
|
||||
|
||||
// Enqueue the build job using the same queue and processor as the existing flow.
|
||||
const jobMeta = await this.organizationBuildQueue.add(
|
||||
OrganizationBuildQueueJob,
|
||||
{
|
||||
organizationId: tenant.organizationId,
|
||||
userId,
|
||||
buildDto: transformedDto,
|
||||
} as OrganizationBuildQueueJobPayload,
|
||||
);
|
||||
|
||||
// Mark the tenant as currently building.
|
||||
await this.tenantRepository.markAsBuilding(jobMeta.id).findById(tenant.id);
|
||||
|
||||
return {
|
||||
organizationId: tenant.organizationId,
|
||||
jobId: jobMeta.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||
import { UserTenant } from '@/modules/System/models/UserTenant.model';
|
||||
import { TenantModel } from '@/modules/System/models/TenantModel';
|
||||
import { TenantRepository } from '@/modules/System/repositories/Tenant.repository';
|
||||
import { TenantDBManager } from '@/modules/TenantDBManager/TenantDBManager';
|
||||
|
||||
const ERRORS = {
|
||||
WORKSPACE_NOT_FOUND: 'WORKSPACE.NOT_FOUND',
|
||||
NOT_WORKSPACE_OWNER: 'NOT.WORKSPACE.OWNER',
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class DeleteWorkspaceService {
|
||||
constructor(
|
||||
@Inject(UserTenant.name)
|
||||
private readonly userTenantModel: typeof UserTenant,
|
||||
|
||||
@Inject(TenantModel.name)
|
||||
private readonly tenantModel: typeof TenantModel,
|
||||
|
||||
private readonly tenantRepository: TenantRepository,
|
||||
private readonly tenantDBManager: TenantDBManager,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Deletes a workspace (organization). Only the owner of the workspace
|
||||
* is permitted to delete it.
|
||||
* - Drops the physical tenant database.
|
||||
* - Deletes the tenant row (cascades to user_tenants).
|
||||
*/
|
||||
async deleteWorkspace(userId: number, organizationId: string): Promise<void> {
|
||||
const tenant = await this.tenantModel.query().findOne({ organizationId });
|
||||
|
||||
if (!tenant) {
|
||||
throw new ServiceError(ERRORS.WORKSPACE_NOT_FOUND);
|
||||
}
|
||||
const membership = await this.userTenantModel
|
||||
.query()
|
||||
.findOne({ userId, tenantId: tenant.id });
|
||||
|
||||
if (!membership || membership.role !== 'owner') {
|
||||
throw new ServiceError(ERRORS.NOT_WORKSPACE_OWNER);
|
||||
}
|
||||
// Drop the physical tenant database if it exists.
|
||||
await this.tenantDBManager.dropDatabaseIfExists();
|
||||
|
||||
// Delete the tenant row — cascades to user_tenants via FK.
|
||||
await this.tenantModel.query().deleteById(tenant.id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { BuildOrganizationDto } from '@/modules/Organization/dtos/Organization.dto';
|
||||
|
||||
export class CreateWorkspaceDto extends BuildOrganizationDto {}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class WorkspaceMetadataDto {
|
||||
@ApiProperty() name: string;
|
||||
@ApiProperty() baseCurrency: string;
|
||||
@ApiPropertyOptional() industry?: string;
|
||||
@ApiPropertyOptional() location?: string;
|
||||
@ApiPropertyOptional() timezone?: string;
|
||||
@ApiPropertyOptional() language?: string;
|
||||
}
|
||||
|
||||
export class WorkspaceDto {
|
||||
@ApiProperty() organizationId: string;
|
||||
@ApiProperty() isReady: boolean;
|
||||
@ApiProperty() isBuildRunning: boolean;
|
||||
@ApiPropertyOptional() buildJobId?: string;
|
||||
@ApiProperty() role: 'owner' | 'member';
|
||||
@ApiPropertyOptional({ type: WorkspaceMetadataDto })
|
||||
metadata?: WorkspaceMetadataDto;
|
||||
}
|
||||
|
||||
export class CreateWorkspaceResponseDto {
|
||||
@ApiProperty() organizationId: string;
|
||||
@ApiProperty() jobId: string;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { GetBuildOrganizationBuildJob } from '@/modules/Organization/commands/GetBuildOrganizationJob.service';
|
||||
|
||||
@Injectable()
|
||||
export class GetWorkspaceBuildJobService {
|
||||
constructor(
|
||||
private readonly getBuildJobService: GetBuildOrganizationBuildJob,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Returns the current status of a workspace build job.
|
||||
*/
|
||||
getJobDetails(buildJobId: string) {
|
||||
return this.getBuildJobService.getJobDetails(buildJobId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { UserTenant } from '@/modules/System/models/UserTenant.model';
|
||||
import { WorkspaceDto } from '../dtos/WorkspaceResponse.dto';
|
||||
|
||||
@Injectable()
|
||||
export class GetWorkspacesService {
|
||||
constructor(
|
||||
@Inject(UserTenant.name)
|
||||
private readonly userTenantModel: typeof UserTenant,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Returns all workspaces (organizations) the given user belongs to,
|
||||
* including their metadata and build status.
|
||||
*/
|
||||
async getWorkspaces(userId: number): Promise<WorkspaceDto[]> {
|
||||
const memberships = await this.userTenantModel
|
||||
.query()
|
||||
.where('userId', userId)
|
||||
.withGraphFetched('tenant.metadata');
|
||||
|
||||
return memberships.map((m) => ({
|
||||
organizationId: m.tenant.organizationId,
|
||||
isReady: m.tenant.isReady,
|
||||
isBuildRunning: m.tenant.isBuildRunning,
|
||||
buildJobId: m.tenant.buildJobId ?? undefined,
|
||||
role: m.role,
|
||||
metadata: m.tenant.metadata
|
||||
? {
|
||||
name: m.tenant.metadata.name,
|
||||
baseCurrency: m.tenant.metadata.baseCurrency,
|
||||
industry: m.tenant.metadata.industry,
|
||||
location: m.tenant.metadata.location,
|
||||
timezone: m.tenant.metadata.timezone,
|
||||
language: m.tenant.metadata.language,
|
||||
}
|
||||
: undefined,
|
||||
}));
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { events } from '@/common/events/events';
|
||||
import { IAuthSignedUpEventPayload } from '@/modules/Auth/Auth.interfaces';
|
||||
import { UserTenant } from '@/modules/System/models/UserTenant.model';
|
||||
|
||||
@Injectable()
|
||||
export class CreateUserTenantOnSignupSubscriber {
|
||||
constructor(
|
||||
@Inject(UserTenant.name)
|
||||
private readonly userTenantModel: typeof UserTenant,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* On user sign-up, create a user_tenants record linking the new user
|
||||
* to their new organization as the owner.
|
||||
*/
|
||||
@OnEvent(events.auth.signUp)
|
||||
async handleSignUp({ user, tenant }: IAuthSignedUpEventPayload): Promise<void> {
|
||||
await this.userTenantModel.query().insert({
|
||||
userId: user.id,
|
||||
tenantId: tenant.id,
|
||||
role: 'owner',
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user