refactor(nestjs): Implement users module

This commit is contained in:
Ahmed Bouhuolia
2025-05-20 17:55:58 +02:00
parent ce058b9416
commit 99fe5a6b0d
48 changed files with 1823 additions and 207 deletions

View File

@@ -0,0 +1,46 @@
import { Injectable } from '@nestjs/common';
import { InjectQueue } from '@nestjs/bull';
import { Queue } from 'bull';
import { events } from '@/common/events/events';
import { OnEvent } from '@nestjs/event-emitter';
import {
IUserInviteTenantSyncedEventPayload,
SendInviteUserMailJobPayload,
} from '../Users.types';
import {
SendInviteUserMailJob,
SendInviteUserMailQueue,
} from '../Users.constants';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
@Injectable()
export default class InviteSendMainNotificationSubscribe {
constructor(
@InjectQueue(SendInviteUserMailQueue)
private readonly sendInviteMailQueue: Queue,
private readonly tenancyContext: TenancyContext,
) {}
/**
* Sends mail notification.
* @param {IUserInvitedEventPayload} payload
*/
@OnEvent(events.inviteUser.sendInviteTenantSynced)
async sendMailNotification({
invite,
user,
}: IUserInviteTenantSyncedEventPayload) {
const tenant = await this.tenancyContext.getTenant();
const authedUser = await this.tenancyContext.getSystemUser();
const organizationId = tenant.organizationId;
const userId = authedUser.id;
this.sendInviteMailQueue.add(SendInviteUserMailJob, {
fromUser: user,
invite,
userId,
organizationId,
} as SendInviteUserMailJobPayload);
}
}

View File

@@ -0,0 +1,29 @@
import {
ITenantUserInactivatedPayload,
ITenantUserActivatedPayload,
ITenantUserDeletedPayload,
ITenantUserEditedPayload,
} from '../Users.types';
import { OnEvent } from '@nestjs/event-emitter';
import { Injectable } from '@nestjs/common';
import { events } from '@/common/events/events';
import { ABILITIES_CACHE } from '@/modules/Roles/TenantAbilities';
@Injectable()
export class PurgeUserAbilityCacheSubscriber {
/**
* Purges authorized user ability once the user mutate.
*/
@OnEvent(events.tenantUser.onEdited)
@OnEvent(events.tenantUser.onActivated)
@OnEvent(events.tenantUser.onInactivated)
purgeAuthorizedUserAbility({
tenantUser,
}:
| ITenantUserInactivatedPayload
| ITenantUserActivatedPayload
| ITenantUserDeletedPayload
| ITenantUserEditedPayload) {
ABILITIES_CACHE.del(tenantUser.systemUserId);
}
}

View File

@@ -0,0 +1,106 @@
import { EventEmitter2, OnEvent } from '@nestjs/event-emitter';
import { Inject, Injectable } from '@nestjs/common';
import {
IUserInvitedEventPayload,
IUserInviteResendEventPayload,
IUserInviteTenantSyncedEventPayload,
} from '../Users.types';
import { events } from '@/common/events/events';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { TenantUser } from '@/modules/Tenancy/TenancyModels/models/TenantUser.model';
import { UserInvite } from '../models/InviteUser.model';
import { SystemUser } from '@/modules/System/models/SystemUser';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
@Injectable()
export class SyncSystemSendInviteSubscriber {
constructor(
@Inject(TenantUser.name)
private readonly tenantUserModel: TenantModelProxy<typeof TenantUser>,
@Inject(SystemUser.name)
private readonly systemUserModel: typeof SystemUser,
@Inject(UserInvite.name)
private readonly inviteModel: typeof UserInvite,
private readonly eventEmitter: EventEmitter2,
private readonly tenancyContext: TenancyContext
) {}
/**
* Syncs send invite to system user.
* @param {IUserInvitedEventPayload} payload -
*/
@OnEvent(events.inviteUser.sendInvite)
async syncSendInviteSystem({ inviteToken, user }: IUserInvitedEventPayload) {
const authorizedUser = await this.tenancyContext.getSystemUser();
const tenantId = authorizedUser.tenantId;
// Creates a new system user.
const systemUser = await this.systemUserModel.query().insert({
email: user.email,
active: user.active,
tenantId,
// Email should be verified since the user got the invite token through email.
verified: true,
});
// Creates a invite user token.
const invite = await this.inviteModel.query().insert({
email: user.email,
tenantId,
userId: systemUser.id,
token: inviteToken,
});
// Links the tenant user with created system user.
await this.tenantUserModel().query().findById(user.id).patch({
systemUserId: systemUser.id,
});
// Triggers `onUserSendInviteTenantSynced` event.
await this.eventEmitter.emitAsync(
events.inviteUser.sendInviteTenantSynced,
{
invite,
user,
} as IUserInviteTenantSyncedEventPayload,
);
}
/**
* Syncs resend invite to system user.
* @param {IUserInviteResendEventPayload} payload -
*/
@OnEvent(events.inviteUser.resendInvite)
async syncResendInviteSystemUser({
inviteToken,
user,
}: IUserInviteResendEventPayload) {
const authorizedUser = await this.tenancyContext.getSystemUser();
const tenantId = authorizedUser.tenantId;
// Clear all invite tokens of the given user id.
await this.clearInviteTokensByUserId(tenantId, user.systemUserId);
const invite = await this.inviteModel.query().insert({
email: user.email,
tenantId,
userId: user.systemUserId,
token: inviteToken,
});
}
/**
* Clear invite tokens of the given user id.
* @param {number} userId - User id.
*/
private clearInviteTokensByUserId = async (tenantId: number, userId: number) => {
await this.inviteModel
.query()
.where({
userId,
tenantId,
})
.delete();
};
}

View File

@@ -0,0 +1,35 @@
import { omit } from 'lodash';
import * as moment from 'moment';
import { Inject, Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { TenantUser } from '@/modules/Tenancy/TenancyModels/models/TenantUser.model';
import { events } from '@/common/events/events';
import { IAcceptInviteEventPayload } from '../Users.types';
@Injectable()
export class SyncTenantAcceptInviteSubscriber {
constructor(
@Inject(TenantUser.name)
private readonly tenantUserModel: TenantModelProxy<typeof TenantUser>,
) {}
/**
* Syncs accept invite to tenant user.
* @param {IAcceptInviteEventPayload} payload -
*/
@OnEvent(events.inviteUser.acceptInvite)
async syncTenantAcceptInvite({
inviteToken,
user,
inviteUserDTO,
}: IAcceptInviteEventPayload) {
await this.tenantUserModel()
.query()
.where('systemUserId', inviteToken.userId)
.update({
...omit(inviteUserDTO, ['password']),
inviteAcceptedAt: moment().format('YYYY-MM-DD'),
});
}
}

View File

@@ -0,0 +1,27 @@
import { events } from '@/common/events/events';
import { SystemUser } from '@/modules/System/models/SystemUser';
import { Inject, Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { ITenantUserDeletedPayload } from '../Users.types';
@Injectable()
export class SyncTenantUserDeleteSubscriber {
constructor(
@Inject(SystemUser.name)
private readonly systemUserModel: typeof SystemUser,
) {}
/**
* Deletes the system user once tenant user be deleted.
* @param {ITenantUserDeletedPayload} payload -
*/
@OnEvent(events.tenantUser.onDeleted)
async syncSystemUserOnceUserDeleted({
tenantUser,
}: ITenantUserDeletedPayload) {
await this.systemUserModel
.query()
.where('id', tenantUser.systemUserId)
.delete();
}
}

View File

@@ -0,0 +1,69 @@
import { pick } from 'lodash';
import { Inject, Injectable } from '@nestjs/common';
import {
ITenantUserActivatedPayload,
ITenantUserEditedPayload,
ITenantUserInactivatedPayload,
} from '../Users.types'
import { OnEvent } from '@nestjs/event-emitter';
import { SystemUser } from '@/modules/System/models/SystemUser';
import { events } from '@/common/events/events';
@Injectable()
export class SyncTenantUserMutateSubscriber {
constructor(
@Inject(SystemUser.name)
private readonly systemUserModel: typeof SystemUser,
) {}
/**
* @param {ITenantUserEditedPayload} payload
*/
@OnEvent(events.tenantUser.onEdited)
async syncSystemUserOnceEdited({ tenantUser }: ITenantUserEditedPayload) {
await this.systemUserModel
.query()
.where('id', tenantUser.systemUserId)
.patch({
...pick(tenantUser, [
'firstName',
'lastName',
'email',
'active',
'phoneNumber',
]),
});
}
/**
* Syncs activate system user.
* @param {ITenantUserInactivatedPayload} payload -
*/
@OnEvent(events.tenantUser.onActivated)
async syncSystemUserOnceActivated({
tenantUser,
}: ITenantUserInactivatedPayload) {
await this.systemUserModel
.query()
.where('id', tenantUser.systemUserId)
.patch({
active: true,
});
}
/**
* Syncs inactivate system user.
* @param {ITenantUserActivatedPayload} payload -
*/
@OnEvent(events.tenantUser.onInactivated)
async syncSystemUserOnceInactivated({
tenantUser,
}: ITenantUserActivatedPayload) {
await this.systemUserModel
.query()
.where('id', tenantUser.systemUserId)
.patch({
active: false,
});
}
}