@sourceloop/ctrl-plane-tenant-management-service
Version:
Tenant Management microservice for SaaS control plane
165 lines (158 loc) • 5.74 kB
text/typescript
import {inject, service} from '@loopback/core';
import {Transaction, repository} from '@loopback/repository';
import {HttpErrors} from '@loopback/rest';
import {ILogger, LOGGER} from '@sourceloop/core';
import {webhookHandler} from '../../decorators';
import {NotificationType, TenantStatus} from '../../enums';
import {WebhookType} from '../../enums/webhook-types.enum';
import {PermissionKey} from '../../permissions';
import {ResourceRepository, TenantRepository} from '../../repositories';
import {
IWebhookHandler,
ResourceProvisionedWebhookPayload,
ResourceTypes,
WebhookStatus,
} from '../../types';
import {CryptoHelperService} from '../crypto-helper.service';
import {NotificationService} from '../notifications';
import {IPostWebhookHandlerService} from '../../types/i-post-webhook-handler-service.interface';
import {PostWebhookHandlerServiceKey} from '../../keys';
import {SaasTenantRepository} from '../../repositories/saas-tenant.repository';
/**
* Handler for provisioning webhooks.
* This class implements the IWebhookHandler interface and defines the type of webhook it handles
* as `WebhookType.RESOURCES_PROVISIONED`. It provides functionality to handle a webhook payload
* for resource provisioning, updating the status of a tenant and creating resources associated with the tenant.
*/
export class ProvisioningWebhookHandler implements IWebhookHandler {
type: WebhookType = WebhookType.RESOURCES_PROVISIONED;
/**
* Constructs a new instance of the ProvisioningWebhookHandler.
* @param {ResourceRepository} resourceRepository - Repository for managing resources.
* @param {TenantRepository} tenantRepository - Repository for managing tenants.
* @param {NotificationService} notificationService - Service for sending notifications.
* @param {CryptoHelperService} cryptoHelperService - Service for cryptographic operations.
* @param {ILogger} logger - Logger service for logging messages.
*/
constructor(
public resourceRepository: ResourceRepository,
public tenantRepository: TenantRepository,
private notificationService: NotificationService,
private postWebhookHandlerService: IPostWebhookHandlerService<ResourceProvisionedWebhookPayload>,
private cryptoHelperService: CryptoHelperService,
private readonly logger: ILogger,
) {}
/**
* This function handles a webhook payload for resource provisioning, updating the status of a tenant
* and creating resources associated with the tenant.
* @param {ResourceProvisionedWebhookPayload} payload - The payload parameter is of type
* ResourceProvisionedWebhookPayload.
*/
async handle(payload: ResourceProvisionedWebhookPayload): Promise<void> {
const transaction = await this.resourceRepository.beginTransaction();
try {
const existing = await this.tenantRepository.findOne(
{
where: {
id: payload.initiatorId,
status: TenantStatus.PROVISIONING,
},
},
{
transaction,
},
);
if (!existing) {
this.logger.error('Tenant not found or not in provisioning state');
throw new HttpErrors.Unauthorized();
}
if (payload.data.status === WebhookStatus.SUCCESS) {
await this.tenantRepository.updateById(
payload.initiatorId,
{
status: TenantStatus.ACTIVE,
},
{transaction},
);
if (payload.data.resources.length > 0) {
await this.resourceRepository.create({
tenantId: payload.initiatorId,
externalIdentifier: payload.data.resources[0]?.metadata?.bucket,
type: ResourceTypes.BUCKET,
metadata: payload.data.resources[0].metadata,
});
}
await this._sendEmail(transaction, payload);
} else {
await this.tenantRepository.updateById(
payload.initiatorId,
{
status: TenantStatus.PROVISIONFAILED,
},
{transaction},
);
}
await this.postWebhookHandlerService.postWebhookHandler(payload);
await transaction.commit();
} catch (e) {
await transaction.rollback();
this.logger.error(e);
throw e;
}
}
private async _sendEmail(
transaction: Transaction,
payload: ResourceProvisionedWebhookPayload,
) {
const tenant = await this.tenantRepository.findById(
payload.initiatorId,
{
include: [
{
relation: 'contacts',
scope: {
where: {
isPrimary: true,
},
fields: ['email', 'firstName', 'lastName', 'tenantId', 'deleted'],
order: ['createdOn DESC'],
},
},
],
},
{
transaction,
},
);
const email = tenant.contacts?.[0]?.email;
const name = tenant.contacts?.[0]?.firstName;
if (!email) {
this.logger.error(`No email found to notify tenant: ${tenant.id}`);
} else {
const tempToken = this.cryptoHelperService.generateTempTokenForTenant(
tenant,
[
PermissionKey.CreateNotification,
PermissionKey.ViewNotificationTemplate,
],
);
await this.notificationService.send(
email,
NotificationType.WelcomeTenant,
{
name: tenant.name,
user: name,
link: payload.data.appPlaneUrl,
},
tempToken,
);
}
}
}