mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-10 18:01:59 +00:00
Compare commits
1 Commits
bugs-bashi
...
users-modu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
985e1dbc01 |
@@ -1,22 +1,21 @@
|
|||||||
// import { Inject, Service } from 'typedi';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
// import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import { Account } from './models/Account.model';
|
||||||
|
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||||
|
|
||||||
// @Service()
|
@Injectable()
|
||||||
// export class MutateBaseCurrencyAccounts {
|
export class MutateBaseCurrencyAccounts {
|
||||||
// @Inject()
|
constructor(
|
||||||
// tenancy: HasTenancyService;
|
@Inject(Account.name)
|
||||||
|
private readonly accountModel: TenantModelProxy<typeof Account>,
|
||||||
|
) {}
|
||||||
|
|
||||||
// /**
|
/**
|
||||||
// * Mutates the all accounts or the organziation.
|
* Mutates the all accounts or the organziation.
|
||||||
// * @param {number} tenantId
|
* @param {string} currencyCode
|
||||||
// * @param {string} currencyCode
|
*/
|
||||||
// */
|
mutateAllAccountsCurrency = async (
|
||||||
// public mutateAllAccountsCurrency = async (
|
currencyCode: string,
|
||||||
// tenantId: number,
|
) => {
|
||||||
// currencyCode: string
|
await Account.query().update({ currencyCode });
|
||||||
// ) => {
|
};
|
||||||
// const { Account } = this.tenancy.models(tenantId);
|
}
|
||||||
|
|
||||||
// await Account.query().update({ currencyCode });
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { events } from '@/common/events/events';
|
||||||
|
import { MutateBaseCurrencyAccounts } from '../MutateBaseCurrencyAccounts';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MutateBaseCurrencyAccountsSubscriber {
|
||||||
|
constructor(
|
||||||
|
public readonly mutateBaseCurrencyAccounts: MutateBaseCurrencyAccounts,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the all accounts currency once the base currency
|
||||||
|
* of the organization is mutated.
|
||||||
|
*/
|
||||||
|
@OnEvent(events.organization.baseCurrencyUpdated)
|
||||||
|
async updateAccountsCurrencyOnBaseCurrencyMutated({
|
||||||
|
organizationDTO,
|
||||||
|
}) {
|
||||||
|
await this.mutateBaseCurrencyAccounts.mutateAllAccountsCurrency(
|
||||||
|
organizationDTO.baseCurrency
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
// import { Service, Inject } from 'typedi';
|
|
||||||
// import events from '@/subscribers/events';
|
|
||||||
// import { MutateBaseCurrencyAccounts } from '../MutateBaseCurrencyAccounts';
|
|
||||||
|
|
||||||
// @Service()
|
|
||||||
// export class MutateBaseCurrencyAccountsSubscriber {
|
|
||||||
// @Inject()
|
|
||||||
// public mutateBaseCurrencyAccounts: MutateBaseCurrencyAccounts;
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * Attaches the events with handles.
|
|
||||||
// * @param bus
|
|
||||||
// */
|
|
||||||
// attach(bus) {
|
|
||||||
// bus.subscribe(
|
|
||||||
// events.organization.baseCurrencyUpdated,
|
|
||||||
// this.updateAccountsCurrencyOnBaseCurrencyMutated
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * Updates the all accounts currency once the base currency
|
|
||||||
// * of the organization is mutated.
|
|
||||||
// */
|
|
||||||
// private updateAccountsCurrencyOnBaseCurrencyMutated = async ({
|
|
||||||
// tenantId,
|
|
||||||
// organizationDTO,
|
|
||||||
// }) => {
|
|
||||||
// await this.mutateBaseCurrencyAccounts.mutateAllAccountsCurrency(
|
|
||||||
// tenantId,
|
|
||||||
// organizationDTO.baseCurrency
|
|
||||||
// );
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
@@ -86,6 +86,8 @@ import { CreditNotesApplyInvoiceModule } from '../CreditNotesApplyInvoice/Credit
|
|||||||
import { ResourceModule } from '../Resource/Resource.module';
|
import { ResourceModule } from '../Resource/Resource.module';
|
||||||
import { ViewsModule } from '../Views/Views.module';
|
import { ViewsModule } from '../Views/Views.module';
|
||||||
import { CurrenciesModule } from '../Currencies/Currencies.module';
|
import { CurrenciesModule } from '../Currencies/Currencies.module';
|
||||||
|
import { MiscellaneousModule } from '../Miscellaneous/Miscellaneous.module';
|
||||||
|
import { UsersModule } from '../UsersModule/Users.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -206,7 +208,9 @@ import { CurrenciesModule } from '../Currencies/Currencies.module';
|
|||||||
ImportModule,
|
ImportModule,
|
||||||
ResourceModule,
|
ResourceModule,
|
||||||
ViewsModule,
|
ViewsModule,
|
||||||
CurrenciesModule
|
CurrenciesModule,
|
||||||
|
MiscellaneousModule,
|
||||||
|
UsersModule
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import { BankingTransactionsModule } from '../BankingTransactions/BankingTransac
|
|||||||
import { GetBankAccountsService } from './queries/GetBankAccounts';
|
import { GetBankAccountsService } from './queries/GetBankAccounts';
|
||||||
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
|
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
|
||||||
import { GetBankAccountSummary } from './queries/GetBankAccountSummary';
|
import { GetBankAccountSummary } from './queries/GetBankAccountSummary';
|
||||||
|
import { MutateBaseCurrencyAccountsSubscriber } from '../Accounts/susbcribers/MutateBaseCurrencyAccounts.subscriber';
|
||||||
|
import { MutateBaseCurrencyAccounts } from '../Accounts/MutateBaseCurrencyAccounts';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -23,7 +25,7 @@ import { GetBankAccountSummary } from './queries/GetBankAccountSummary';
|
|||||||
BankRulesModule,
|
BankRulesModule,
|
||||||
BankingTransactionsRegonizeModule,
|
BankingTransactionsRegonizeModule,
|
||||||
BankingTransactionsModule,
|
BankingTransactionsModule,
|
||||||
DynamicListModule
|
DynamicListModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
DisconnectBankAccountService,
|
DisconnectBankAccountService,
|
||||||
@@ -34,7 +36,9 @@ import { GetBankAccountSummary } from './queries/GetBankAccountSummary';
|
|||||||
DisconnectPlaidItemOnAccountDeleted,
|
DisconnectPlaidItemOnAccountDeleted,
|
||||||
BankAccountsApplication,
|
BankAccountsApplication,
|
||||||
GetBankAccountsService,
|
GetBankAccountsService,
|
||||||
GetBankAccountSummary
|
GetBankAccountSummary,
|
||||||
|
MutateBaseCurrencyAccounts,
|
||||||
|
MutateBaseCurrencyAccountsSubscriber,
|
||||||
],
|
],
|
||||||
exports: [BankAccountsApplication],
|
exports: [BankAccountsApplication],
|
||||||
controllers: [BankAccountsController],
|
controllers: [BankAccountsController],
|
||||||
|
|||||||
@@ -37,15 +37,15 @@ export class CurrenciesController {
|
|||||||
return this.currenciesApp.createCurrency(dto);
|
return this.currenciesApp.createCurrency(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put(':id')
|
@Put(':code')
|
||||||
@ApiOperation({ summary: 'Edit an existing currency' })
|
@ApiOperation({ summary: 'Edit an existing currency' })
|
||||||
@ApiParam({ name: 'id', type: Number, description: 'Currency ID' })
|
@ApiParam({ name: 'id', type: Number, description: 'Currency ID' })
|
||||||
@ApiBody({ type: EditCurrencyDto })
|
@ApiBody({ type: EditCurrencyDto })
|
||||||
@ApiOkResponse({ description: 'The currency has been successfully updated.' })
|
@ApiOkResponse({ description: 'The currency has been successfully updated.' })
|
||||||
@ApiNotFoundResponse({ description: 'Currency not found.' })
|
@ApiNotFoundResponse({ description: 'Currency not found.' })
|
||||||
@ApiBadRequestResponse({ description: 'Invalid input data.' })
|
@ApiBadRequestResponse({ description: 'Invalid input data.' })
|
||||||
edit(@Param('id') id: number, @Body() dto: EditCurrencyDto) {
|
edit(@Param('code') code: string, @Body() dto: EditCurrencyDto) {
|
||||||
return this.currenciesApp.editCurrency(Number(id), dto);
|
return this.currenciesApp.editCurrency(code, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':code')
|
@Delete(':code')
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ export class CurrenciesApplication {
|
|||||||
/**
|
/**
|
||||||
* Edits an existing currency.
|
* Edits an existing currency.
|
||||||
*/
|
*/
|
||||||
public editCurrency(currencyId: number, currencyDTO: EditCurrencyDto) {
|
public editCurrency(currencyCode: string, currencyDTO: EditCurrencyDto) {
|
||||||
return this.editCurrencyService.editCurrency(currencyId, currencyDTO);
|
return this.editCurrencyService.editCurrency(currencyCode, currencyDTO);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -12,22 +12,21 @@ export class EditCurrencyService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Edit details of the given currency.
|
* Edit details of the given currency.
|
||||||
* @param {number} tenantId
|
* @param {number} currencyCode - Currency code.
|
||||||
* @param {number} currencyId
|
* @param {ICurrencyDTO} currencyDTO - Edit currency dto.
|
||||||
* @param {ICurrencyDTO} currencyDTO
|
|
||||||
*/
|
*/
|
||||||
public async editCurrency(
|
public async editCurrency(
|
||||||
currencyId: number,
|
currencyCode: string,
|
||||||
currencyDTO: EditCurrencyDto,
|
currencyDTO: EditCurrencyDto,
|
||||||
): Promise<Currency> {
|
): Promise<Currency> {
|
||||||
const foundCurrency = await this.currencyModel()
|
const foundCurrency = await this.currencyModel()
|
||||||
.query()
|
.query()
|
||||||
.findOne('id', currencyId)
|
.findOne('currencyCode', currencyCode)
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
const currency = await this.currencyModel()
|
const currency = await this.currencyModel()
|
||||||
.query()
|
.query()
|
||||||
.patchAndFetchById(currencyId, {
|
.patchAndFetchById(foundCurrency.id, {
|
||||||
...currencyDTO,
|
...currencyDTO,
|
||||||
});
|
});
|
||||||
return currency;
|
return currency;
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export const DATE_FORMATS = [
|
||||||
|
'MM/DD/YY',
|
||||||
|
'DD/MM/YY',
|
||||||
|
'YY/MM/DD',
|
||||||
|
'MM/DD/yyyy',
|
||||||
|
'DD/MM/yyyy',
|
||||||
|
'yyyy/MM/DD',
|
||||||
|
'DD MMM YYYY',
|
||||||
|
'DD MMMM YYYY',
|
||||||
|
'MMMM DD, YYYY',
|
||||||
|
];
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
import { GetDateFormatsService } from './queries/GetDateFormats.service';
|
||||||
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
@Controller('/')
|
||||||
|
@ApiTags('misc')
|
||||||
|
export class MiscellaneousController {
|
||||||
|
constructor(private readonly getDateFormatsSevice: GetDateFormatsService) {}
|
||||||
|
|
||||||
|
@Get('/date-formats')
|
||||||
|
getDateFormats() {
|
||||||
|
return this.getDateFormatsSevice.getDateFormats();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { GetDateFormatsService } from './queries/GetDateFormats.service';
|
||||||
|
import { MiscellaneousController } from './Miscellaneous.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [GetDateFormatsService],
|
||||||
|
exports: [GetDateFormatsService],
|
||||||
|
controllers: [MiscellaneousController],
|
||||||
|
})
|
||||||
|
export class MiscellaneousModule {}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import * as moment from 'moment';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DATE_FORMATS } from '../Miscellaneous.constants';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GetDateFormatsService {
|
||||||
|
getDateFormats() {
|
||||||
|
return DATE_FORMATS.map((dateFormat) => {
|
||||||
|
return {
|
||||||
|
label: `${moment().format(dateFormat)} [${dateFormat}]`,
|
||||||
|
key: dateFormat,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ import { UpdateOrganizationService } from './commands/UpdateOrganization.service
|
|||||||
import { IgnoreTenantInitializedRoute } from '../Tenancy/EnsureTenantIsInitialized.guard';
|
import { IgnoreTenantInitializedRoute } from '../Tenancy/EnsureTenantIsInitialized.guard';
|
||||||
import { IgnoreTenantSeededRoute } from '../Tenancy/EnsureTenantIsSeeded.guards';
|
import { IgnoreTenantSeededRoute } from '../Tenancy/EnsureTenantIsSeeded.guards';
|
||||||
import { GetBuildOrganizationBuildJob } from './commands/GetBuildOrganizationJob.service';
|
import { GetBuildOrganizationBuildJob } from './commands/GetBuildOrganizationJob.service';
|
||||||
|
import { OrganizationBaseCurrencyLocking } from './Organization/OrganizationBaseCurrencyLocking.service';
|
||||||
|
|
||||||
@ApiTags('Organization')
|
@ApiTags('Organization')
|
||||||
@Controller('organization')
|
@Controller('organization')
|
||||||
@@ -39,6 +40,7 @@ export class OrganizationController {
|
|||||||
private readonly getCurrentOrgService: GetCurrentOrganizationService,
|
private readonly getCurrentOrgService: GetCurrentOrganizationService,
|
||||||
private readonly updateOrganizationService: UpdateOrganizationService,
|
private readonly updateOrganizationService: UpdateOrganizationService,
|
||||||
private readonly getBuildOrganizationJobService: GetBuildOrganizationBuildJob,
|
private readonly getBuildOrganizationJobService: GetBuildOrganizationBuildJob,
|
||||||
|
private readonly orgBaseCurrencyLockingService: OrganizationBaseCurrencyLocking,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post('build')
|
@Post('build')
|
||||||
@@ -81,6 +83,14 @@ export class OrganizationController {
|
|||||||
return { organization };
|
return { organization };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('base-currency-mutate')
|
||||||
|
async baseCurrencyMutate() {
|
||||||
|
const abilities =
|
||||||
|
await this.orgBaseCurrencyLockingService.baseCurrencyMutateLocks();
|
||||||
|
|
||||||
|
return res.status(200).send({ abilities });
|
||||||
|
}
|
||||||
|
|
||||||
@Put()
|
@Put()
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@ApiOperation({ summary: 'Update organization information' })
|
@ApiOperation({ summary: 'Update organization information' })
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ export class OrganizationBaseCurrencyLocking {
|
|||||||
* @returns {Promise<MutateBaseCurrencyLockMeta[]>}
|
* @returns {Promise<MutateBaseCurrencyLockMeta[]>}
|
||||||
*/
|
*/
|
||||||
public async baseCurrencyMutateLocks(
|
public async baseCurrencyMutateLocks(
|
||||||
tenantId: number,
|
|
||||||
): Promise<MutateBaseCurrencyLockMeta[]> {
|
): Promise<MutateBaseCurrencyLockMeta[]> {
|
||||||
const PreventedModels = this.getModelsPreventsMutate(tenantId);
|
const PreventedModels = this.getModelsPreventsMutate(tenantId);
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,12 @@ export class TenantUser extends TenantBaseModel {
|
|||||||
firstName!: string;
|
firstName!: string;
|
||||||
lastName!: string;
|
lastName!: string;
|
||||||
inviteAcceptedAt!: Date;
|
inviteAcceptedAt!: Date;
|
||||||
|
invitedAt!: Date;
|
||||||
roleId!: number;
|
roleId!: number;
|
||||||
|
active!: boolean;
|
||||||
role!: Role;
|
role!: Role;
|
||||||
|
email!: string;
|
||||||
|
systemUserId!: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Table name.
|
* Table name.
|
||||||
|
|||||||
123
packages/server/src/modules/UsersModule/Users.application.ts
Normal file
123
packages/server/src/modules/UsersModule/Users.application.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ActivateUserService } from './commands/ActivateUser.service';
|
||||||
|
import { DeleteUserService } from './commands/DeleteUser.service';
|
||||||
|
import { EditUserService } from './commands/EditUser.service';
|
||||||
|
import { InactivateUserService } from './commands/InactivateUser.service';
|
||||||
|
import { GetUserService } from './queries/GetUser.service';
|
||||||
|
import { AcceptInviteUserService } from './commands/AcceptInviteUser.service';
|
||||||
|
import { EditUserDto } from './dtos/EditUser.dto';
|
||||||
|
import { InviteUserDto } from './dtos/InviteUser.dto';
|
||||||
|
import { GetUsersService } from './queries/GetUsers.service';
|
||||||
|
import { InviteTenantUserService } from './commands/InviteUser.service';
|
||||||
|
import { IUserSendInviteDTO } from './Users.types';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UsersApplication {
|
||||||
|
constructor(
|
||||||
|
private readonly activateUserService: ActivateUserService,
|
||||||
|
private readonly deleteUserService: DeleteUserService,
|
||||||
|
private readonly editUserService: EditUserService,
|
||||||
|
private readonly inactivateUserService: InactivateUserService,
|
||||||
|
private readonly getUserService: GetUserService,
|
||||||
|
private readonly getUsersService: GetUsersService,
|
||||||
|
private readonly acceptInviteUserService: AcceptInviteUserService,
|
||||||
|
private readonly inviteservice: InviteTenantUserService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activates a user.
|
||||||
|
* @param {number} userId - User ID to activate.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async activateUser(userId: number): Promise<void> {
|
||||||
|
return this.activateUserService.activateUser(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inactivates a user.
|
||||||
|
* @param {number} tenantId - Tenant ID.
|
||||||
|
* @param {number} userId - User ID to inactivate.
|
||||||
|
* @param {ModelObject<TenantUser>} authorizedUser - The user performing the action.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async inactivateUser(userId: number): Promise<void> {
|
||||||
|
return this.inactivateUserService.inactivateUser(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edits a user's details.
|
||||||
|
* @param {number} userId - User ID to edit.
|
||||||
|
* @param {IEditUserDTO} editUserDTO - User data to update.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async editUser(userId: number, editUserDTO: EditUserDto): Promise<void> {
|
||||||
|
return this.editUserService.editUser(userId, editUserDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a user (soft delete).
|
||||||
|
* @param {number} tenantId - Tenant ID.
|
||||||
|
* @param {number} userId - User ID to delete.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async deleteUser(userId: number): Promise<void> {
|
||||||
|
return this.deleteUserService.deleteUser(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a user by ID.
|
||||||
|
* @param {number} userId - User ID to retrieve.
|
||||||
|
* @returns {Promise<any>} User details.
|
||||||
|
*/
|
||||||
|
async getUser(userId: number): Promise<any> {
|
||||||
|
return this.getUserService.getUser(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets users list based on the given filter.
|
||||||
|
*/
|
||||||
|
async getUsers() {
|
||||||
|
return this.getUsersService.getUsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accepts a user invitation.
|
||||||
|
* @param {string} token - Invitation token.
|
||||||
|
* @param {IInviteUserInput} inviteUserDTO - User data for accepting the invitation.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async acceptInvite(
|
||||||
|
token: string,
|
||||||
|
inviteUserDTO: InviteUserDto,
|
||||||
|
): Promise<void> {
|
||||||
|
return this.acceptInviteUserService.acceptInvite(token, inviteUserDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if an invitation token is valid.
|
||||||
|
* @param {string} token - Invitation token to validate.
|
||||||
|
* @returns {Promise<{ inviteToken: any; orgName: string }>} Invitation details.
|
||||||
|
*/
|
||||||
|
async checkInvite(
|
||||||
|
token: string,
|
||||||
|
): Promise<{ inviteToken: any; orgName: string }> {
|
||||||
|
return this.acceptInviteUserService.checkInvite(token);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sends an invitation to a new user.
|
||||||
|
* @param {IUserSendInviteDTO} sendInviteDTO - User invitation data.
|
||||||
|
* @returns {Promise<{ invitedUser: ITenantUser }>} The invited user details.
|
||||||
|
*/
|
||||||
|
async sendInvite(sendInviteDTO: IUserSendInviteDTO) {
|
||||||
|
return this.inviteservice.sendInvite(sendInviteDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resends an invitation to an existing user.
|
||||||
|
* @param {number} userId - ID of the user to resend invitation to.
|
||||||
|
* @returns {Promise<{ user: ITenantUser }>} The user details.
|
||||||
|
*/
|
||||||
|
async resendInvite(userId: number) {
|
||||||
|
return this.inviteservice.resendInvite(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
packages/server/src/modules/UsersModule/Users.constants.ts
Normal file
17
packages/server/src/modules/UsersModule/Users.constants.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export const ERRORS = {
|
||||||
|
CANNOT_DELETE_LAST_USER: 'CANNOT_DELETE_LAST_USER',
|
||||||
|
USER_ALREADY_ACTIVE: 'USER_ALREADY_ACTIVE',
|
||||||
|
USER_ALREADY_INACTIVE: 'USER_ALREADY_INACTIVE',
|
||||||
|
EMAIL_ALREADY_EXISTS: 'EMAIL_ALREADY_EXISTS',
|
||||||
|
PHONE_NUMBER_ALREADY_EXIST: 'PHONE_NUMBER_ALREADY_EXIST',
|
||||||
|
USER_NOT_FOUND: 'USER_NOT_FOUND',
|
||||||
|
USER_SAME_THE_AUTHORIZED_USER: 'USER_SAME_THE_AUTHORIZED_USER',
|
||||||
|
CANNOT_AUTHORIZED_USER_MUTATE_ROLE: 'CANNOT_AUTHORIZED_USER_MUTATE_ROLE',
|
||||||
|
|
||||||
|
EMAIL_ALREADY_INVITED: 'EMAIL_ALREADY_INVITED',
|
||||||
|
INVITE_TOKEN_INVALID: 'INVITE_TOKEN_INVALID',
|
||||||
|
PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_EXISTS',
|
||||||
|
EMAIL_EXISTS: 'EMAIL_EXISTS',
|
||||||
|
EMAIL_NOT_EXISTS: 'EMAIL_NOT_EXISTS',
|
||||||
|
USER_RECENTLY_INVITED: 'USER_RECENTLY_INVITED',
|
||||||
|
};
|
||||||
161
packages/server/src/modules/UsersModule/Users.controller.ts
Normal file
161
packages/server/src/modules/UsersModule/Users.controller.ts
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Post,
|
||||||
|
Put,
|
||||||
|
Query,
|
||||||
|
Req,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { UsersApplication } from './Users.application';
|
||||||
|
import { EditUserDto } from './dtos/EditUser.dto';
|
||||||
|
import { InviteUserDto } from './dtos/InviteUser.dto';
|
||||||
|
import { IUserSendInviteDTO } from './Users.types';
|
||||||
|
|
||||||
|
@Controller('users')
|
||||||
|
@ApiTags('users')
|
||||||
|
export class UsersController {
|
||||||
|
constructor(private readonly usersApplication: UsersApplication) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edit details of the given user.
|
||||||
|
*/
|
||||||
|
@Post(':id')
|
||||||
|
@ApiOperation({ summary: 'Edit details of the given user.' })
|
||||||
|
async editUser(
|
||||||
|
@Param('id') userId: number,
|
||||||
|
@Body() editUserDTO: EditUserDto,
|
||||||
|
) {
|
||||||
|
await this.usersApplication.editUser(userId, editUserDTO);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: userId,
|
||||||
|
message: 'The user has been edited successfully.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Soft deleting the given user.
|
||||||
|
*/
|
||||||
|
@Delete(':id')
|
||||||
|
@ApiOperation({ summary: 'Soft deleting the given user.' })
|
||||||
|
async deleteUser(@Param('id') userId: number) {
|
||||||
|
await this.usersApplication.deleteUser(userId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: userId,
|
||||||
|
message: 'The user has been deleted successfully.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve user details of the given user id.
|
||||||
|
*/
|
||||||
|
@Get(':id')
|
||||||
|
@ApiOperation({ summary: 'Retrieve user details of the given user id.' })
|
||||||
|
async getUser(@Param('id') userId: number) {
|
||||||
|
const user = await this.usersApplication.getUser(userId);
|
||||||
|
|
||||||
|
return { user };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the list of users.
|
||||||
|
*/
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: 'Retrieve the list of users.' })
|
||||||
|
async listUsers(
|
||||||
|
@Query('page_size') pageSize?: number,
|
||||||
|
@Query('page') page?: number,
|
||||||
|
) {
|
||||||
|
const users = await this.usersApplication.getUsers();
|
||||||
|
|
||||||
|
return { users };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activate the given user.
|
||||||
|
*/
|
||||||
|
@Put(':id/activate')
|
||||||
|
@ApiOperation({ summary: 'Activate the given user.' })
|
||||||
|
async activateUser(@Param('id') userId: number) {
|
||||||
|
await this.usersApplication.activateUser(userId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: userId,
|
||||||
|
message: 'The user has been activated successfully.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inactivate the given user.
|
||||||
|
*/
|
||||||
|
@Put(':id/inactivate')
|
||||||
|
@ApiOperation({ summary: 'Inactivate the given user.' })
|
||||||
|
async inactivateUser(@Param('id') userId: number) {
|
||||||
|
await this.usersApplication.inactivateUser(userId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: userId,
|
||||||
|
message: 'The user has been inactivated successfully.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accept a user invitation.
|
||||||
|
*/
|
||||||
|
@Post('invite/accept/:token')
|
||||||
|
@ApiOperation({ summary: 'Accept a user invitation.' })
|
||||||
|
async acceptInvite(
|
||||||
|
@Param('token') token: string,
|
||||||
|
@Body() inviteUserDTO: InviteUserDto,
|
||||||
|
) {
|
||||||
|
await this.usersApplication.acceptInvite(token, inviteUserDTO);
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: 'The invitation has been accepted successfully.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an invitation token is valid.
|
||||||
|
*/
|
||||||
|
@Get('invite/check/:token')
|
||||||
|
@ApiOperation({ summary: 'Check if an invitation token is valid.' })
|
||||||
|
async checkInvite(@Param('token') token: string) {
|
||||||
|
const inviteDetails = await this.usersApplication.checkInvite(token);
|
||||||
|
|
||||||
|
return inviteDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an invitation to a new user.
|
||||||
|
*/
|
||||||
|
@Post('invite')
|
||||||
|
@ApiOperation({ summary: 'Send an invitation to a new user.' })
|
||||||
|
async sendInvite(@Body() sendInviteDTO: IUserSendInviteDTO) {
|
||||||
|
const result = await this.usersApplication.sendInvite(sendInviteDTO);
|
||||||
|
|
||||||
|
return {
|
||||||
|
invitedUser: result.invitedUser,
|
||||||
|
message: 'The invitation has been sent successfully.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resend an invitation to an existing user.
|
||||||
|
*/
|
||||||
|
@Post(':id/invite/resend')
|
||||||
|
@ApiOperation({ summary: 'Resend an invitation to an existing user.' })
|
||||||
|
async resendInvite(@Param('id') userId: number) {
|
||||||
|
const result = await this.usersApplication.resendInvite(userId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
user: result.user,
|
||||||
|
message: 'The invitation has been resent successfully.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
35
packages/server/src/modules/UsersModule/Users.module.ts
Normal file
35
packages/server/src/modules/UsersModule/Users.module.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ActivateUserService } from './commands/ActivateUser.service';
|
||||||
|
import { DeleteUserService } from './commands/DeleteUser.service';
|
||||||
|
import { EditUserService } from './commands/EditUser.service';
|
||||||
|
import { InactivateUserService } from './commands/InactivateUser.service';
|
||||||
|
import { GetUserService } from './queries/GetUser.service';
|
||||||
|
import { PurgeUserAbilityCacheSubscriber } from './subscribers/PurgeUserAbilityCache.subscriber';
|
||||||
|
import { SyncTenantUserDeleteSubscriber } from './subscribers/SyncTenantUserDeleted.subscriber';
|
||||||
|
import { SyncTenantUserMutateSubscriber } from './subscribers/SyncTenantUserSaved.subscriber';
|
||||||
|
import { SyncSystemSendInviteSubscriber } from './subscribers/SyncSystemSendInvite.subscriber';
|
||||||
|
import { SyncTenantAcceptInviteSubscriber } from './subscribers/SyncTenantAcceptInvite.subscriber';
|
||||||
|
import { UsersController } from './Users.controller';
|
||||||
|
import { RegisterTenancyModel } from '../Tenancy/TenancyModels/Tenancy.module';
|
||||||
|
import { UserInvite } from './models/InviteUser.model';
|
||||||
|
|
||||||
|
const models = [RegisterTenancyModel(UserInvite)];
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [...models],
|
||||||
|
exports: [...models],
|
||||||
|
providers: [
|
||||||
|
ActivateUserService,
|
||||||
|
DeleteUserService,
|
||||||
|
EditUserService,
|
||||||
|
InactivateUserService,
|
||||||
|
GetUserService,
|
||||||
|
PurgeUserAbilityCacheSubscriber,
|
||||||
|
SyncTenantUserDeleteSubscriber,
|
||||||
|
SyncTenantUserMutateSubscriber,
|
||||||
|
SyncSystemSendInviteSubscriber,
|
||||||
|
SyncTenantAcceptInviteSubscriber,
|
||||||
|
],
|
||||||
|
controllers: [UsersController],
|
||||||
|
})
|
||||||
|
export class UsersModule {}
|
||||||
63
packages/server/src/modules/UsersModule/Users.types.ts
Normal file
63
packages/server/src/modules/UsersModule/Users.types.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { ModelObject } from "objection";
|
||||||
|
import { TenantUser } from "../Tenancy/TenancyModels/models/TenantUser.model";
|
||||||
|
import { EditUserDto } from "./dtos/EditUser.dto";
|
||||||
|
import { UserInvite } from "./models/InviteUser.model";
|
||||||
|
import { SystemUser } from "../System/models/SystemUser";
|
||||||
|
import { InviteUserDto } from "./dtos/InviteUser.dto";
|
||||||
|
import { TenantModel } from "../System/models/TenantModel";
|
||||||
|
|
||||||
|
export interface ITenantUserEditedPayload {
|
||||||
|
userId: number;
|
||||||
|
editUserDTO: EditUserDto;
|
||||||
|
tenantUser: ModelObject<TenantUser>;
|
||||||
|
oldTenantUser: ModelObject<TenantUser>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITenantUserActivatedPayload {
|
||||||
|
userId: number;
|
||||||
|
tenantUser: ModelObject<TenantUser>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITenantUserInactivatedPayload {
|
||||||
|
tenantId: number;
|
||||||
|
userId: number;
|
||||||
|
authorizedUser: ModelObject<SystemUser>;
|
||||||
|
tenantUser: ModelObject<TenantUser>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITenantUserDeletedPayload {
|
||||||
|
userId: number;
|
||||||
|
tenantUser: ModelObject<TenantUser>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUserInvitedEventPayload {
|
||||||
|
inviteToken: string;
|
||||||
|
user: ModelObject<TenantUser>;
|
||||||
|
}
|
||||||
|
export interface IUserInviteTenantSyncedEventPayload {
|
||||||
|
invite: ModelObject<UserInvite>;
|
||||||
|
authorizedUser: ModelObject<SystemUser>;
|
||||||
|
tenantId: number;
|
||||||
|
user: ModelObject<TenantUser>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUserInviteResendEventPayload {
|
||||||
|
inviteToken: string;
|
||||||
|
user: ModelObject<TenantUser>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAcceptInviteEventPayload {
|
||||||
|
inviteToken: ModelObject<UserInvite>;
|
||||||
|
user: ModelObject<SystemUser>;
|
||||||
|
inviteUserDTO: InviteUserDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICheckInviteEventPayload {
|
||||||
|
inviteToken: ModelObject<UserInvite>;
|
||||||
|
tenant: ModelObject<TenantModel>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUserSendInviteDTO {
|
||||||
|
email: string;
|
||||||
|
roleId: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import * as moment from 'moment';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import {
|
||||||
|
IAcceptInviteEventPayload,
|
||||||
|
IInviteUserInput,
|
||||||
|
ICheckInviteEventPayload,
|
||||||
|
IUserInvite,
|
||||||
|
} from '../Users.types';
|
||||||
|
import { SystemUser } from '@/modules/System/models/SystemUser';
|
||||||
|
import { events } from '@/common/events/events';
|
||||||
|
import { hashPassword } from '@/modules/Auth/Auth.utils';
|
||||||
|
import { TenantModel } from '@/modules/System/models/TenantModel';
|
||||||
|
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||||
|
import { ERRORS } from '../Users.constants';
|
||||||
|
import { UserInvite } from '../models/InviteUser.model';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AcceptInviteUserService {
|
||||||
|
constructor(
|
||||||
|
@Inject(SystemUser.name)
|
||||||
|
private readonly systemUserModel: typeof SystemUser,
|
||||||
|
|
||||||
|
@Inject(TenantModel.name)
|
||||||
|
private readonly tenantModel: typeof TenantModel,
|
||||||
|
|
||||||
|
@Inject(UserInvite.name)
|
||||||
|
private readonly userInviteModel: typeof UserInvite,
|
||||||
|
private readonly eventEmitter: EventEmitter2,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accept the received invite.
|
||||||
|
* @param {string} token
|
||||||
|
* @param {IInviteUserInput} inviteUserInput
|
||||||
|
* @throws {ServiceErrors}
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async acceptInvite(
|
||||||
|
token: string,
|
||||||
|
inviteUserDTO: IInviteUserInput,
|
||||||
|
): Promise<void> {
|
||||||
|
// Retrieve the invite token or throw not found error.
|
||||||
|
const inviteToken = await this.getInviteTokenOrThrowError(token);
|
||||||
|
|
||||||
|
// Hash the given password.
|
||||||
|
const hashedPassword = await hashPassword(inviteUserDTO.password);
|
||||||
|
|
||||||
|
// Retrieve the system user.
|
||||||
|
const user = await this.systemUserModel
|
||||||
|
.query()
|
||||||
|
.findOne('email', inviteToken.email);
|
||||||
|
|
||||||
|
// Sets the invited user details after invite accepting.
|
||||||
|
const systemUser = await this.systemUserModel
|
||||||
|
.query()
|
||||||
|
.updateAndFetchById(inviteToken.userId, {
|
||||||
|
...inviteUserDTO,
|
||||||
|
inviteAcceptedAt: moment().format('YYYY-MM-DD'),
|
||||||
|
password: hashedPassword,
|
||||||
|
});
|
||||||
|
// Clear invite token by the given user id.
|
||||||
|
await this.clearInviteTokensByUserId(inviteToken.userId);
|
||||||
|
|
||||||
|
// Triggers `onUserAcceptInvite` event.
|
||||||
|
await this.eventEmitter.emitAsync(events.inviteUser.acceptInvite, {
|
||||||
|
inviteToken,
|
||||||
|
user: systemUser,
|
||||||
|
inviteUserDTO,
|
||||||
|
} as IAcceptInviteEventPayload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the given invite token.
|
||||||
|
* @param {string} token - the given token string.
|
||||||
|
* @throws {ServiceError}
|
||||||
|
*/
|
||||||
|
public async checkInvite(
|
||||||
|
token: string,
|
||||||
|
): Promise<{ inviteToken: IUserInvite; orgName: string }> {
|
||||||
|
const inviteToken = await this.getInviteTokenOrThrowError(token);
|
||||||
|
|
||||||
|
// Find the tenant that associated to the given token.
|
||||||
|
const tenant = await this.tenantModel
|
||||||
|
.query()
|
||||||
|
.findById(inviteToken.tenantId)
|
||||||
|
.withGraphFetched('metadata');
|
||||||
|
|
||||||
|
// Triggers `onUserCheckInvite` event.
|
||||||
|
await this.eventEmitter.emitAsync(events.inviteUser.checkInvite, {
|
||||||
|
inviteToken,
|
||||||
|
tenant,
|
||||||
|
} as ICheckInviteEventPayload);
|
||||||
|
|
||||||
|
return { inviteToken, orgName: tenant.metadata.name };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve invite model from the given token or throw error.
|
||||||
|
* @param {string} token - Then given token string.
|
||||||
|
* @throws {ServiceError}
|
||||||
|
* @returns {Invite}
|
||||||
|
*/
|
||||||
|
private getInviteTokenOrThrowError = async (
|
||||||
|
token: string,
|
||||||
|
): Promise<IUserInvite> => {
|
||||||
|
const inviteToken = await this.userInviteModel
|
||||||
|
.query()
|
||||||
|
.modify('notExpired')
|
||||||
|
.findOne('token', token);
|
||||||
|
|
||||||
|
if (!inviteToken) {
|
||||||
|
throw new ServiceError(ERRORS.INVITE_TOKEN_INVALID);
|
||||||
|
}
|
||||||
|
return inviteToken;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the given user email and phone number uniquine.
|
||||||
|
* @param {IInviteUserInput} inviteUserInput
|
||||||
|
*/
|
||||||
|
private validateUserPhoneNumberNotExists = async (
|
||||||
|
phoneNumber: string,
|
||||||
|
): Promise<void> => {
|
||||||
|
const foundUser = await SystemUser.query().findOne({ phoneNumber });
|
||||||
|
|
||||||
|
if (foundUser) {
|
||||||
|
throw new ServiceError(ERRORS.PHONE_NUMBER_EXISTS);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear invite tokens of the given user id.
|
||||||
|
* @param {number} userId - User id.
|
||||||
|
*/
|
||||||
|
private clearInviteTokensByUserId = async (userId: number) => {
|
||||||
|
await this.userInviteModel.query().where('user_id', userId).delete();
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { events } from '@/common/events/events';
|
||||||
|
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||||
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
|
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||||
|
import { TenantUser } from '@/modules/Tenancy/TenancyModels/models/TenantUser.model';
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { ERRORS } from '../Users.constants';
|
||||||
|
import { ModelObject } from 'objection';
|
||||||
|
import { SystemUser } from '@/modules/System/models/SystemUser';
|
||||||
|
import { ITenantUserActivatedPayload } from '../Users.types';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ActivateUserService {
|
||||||
|
constructor(
|
||||||
|
@Inject(TenantUser.name)
|
||||||
|
private readonly tenantUserModel: TenantModelProxy<typeof TenantUser>,
|
||||||
|
private readonly tenancyContext: TenancyContext,
|
||||||
|
private readonly eventEmitter: EventEmitter2,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activate the given user id.
|
||||||
|
* @param {number} userId - User id.
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async activateUser(userId: number): Promise<void> {
|
||||||
|
const authorizedUser = await this.tenancyContext.getSystemUser();
|
||||||
|
|
||||||
|
// Throw service error if the given user is equals the authorized user.
|
||||||
|
this.throwErrorIfUserSameAuthorizedUser(userId, authorizedUser);
|
||||||
|
|
||||||
|
// Retrieve the user or throw not found service error.
|
||||||
|
const tenantUser = await this.tenantUserModel().query().findById(userId);
|
||||||
|
|
||||||
|
// Throw serivce error if the user is already activated.
|
||||||
|
this.throwErrorIfUserActive(tenantUser);
|
||||||
|
|
||||||
|
// Marks the tenant user as active.
|
||||||
|
await this.tenantUserModel()
|
||||||
|
.query()
|
||||||
|
.findById(userId)
|
||||||
|
.update({ active: true });
|
||||||
|
|
||||||
|
// Triggers `onTenantUserActivated` event.
|
||||||
|
await this.eventEmitter.emitAsync(events.tenantUser.onActivated, {
|
||||||
|
userId,
|
||||||
|
tenantUser,
|
||||||
|
} as ITenantUserActivatedPayload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throws service error in case the user was already active.
|
||||||
|
* @param {ISystemUser} user
|
||||||
|
* @throws {ServiceError}
|
||||||
|
*/
|
||||||
|
private throwErrorIfUserActive(user: ModelObject<TenantUser>) {
|
||||||
|
if (user.active) {
|
||||||
|
throw new ServiceError(ERRORS.USER_ALREADY_ACTIVE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throw service error in case the given user same the authorized user.
|
||||||
|
* @param {number} userId
|
||||||
|
* @param {ModelObject<TenantUser>} authorizedUser
|
||||||
|
*/
|
||||||
|
private throwErrorIfUserSameAuthorizedUser(
|
||||||
|
userId: number,
|
||||||
|
authorizedUser: ModelObject<SystemUser>,
|
||||||
|
) {
|
||||||
|
if (userId === authorizedUser.id) {
|
||||||
|
throw new ServiceError(ERRORS.USER_SAME_THE_AUTHORIZED_USER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
|
import { TenantUser } from '@/modules/Tenancy/TenancyModels/models/TenantUser.model';
|
||||||
|
import { ITenantUserDeletedPayload } from '../Users.types';
|
||||||
|
import { events } from '@/common/events/events';
|
||||||
|
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||||
|
import { ERRORS } from '../Users.constants';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DeleteUserService {
|
||||||
|
constructor(
|
||||||
|
@Inject(TenantUser.name)
|
||||||
|
private readonly tenantUserModel: TenantModelProxy<typeof TenantUser>,
|
||||||
|
private readonly eventEmitter: EventEmitter2,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the given user id.
|
||||||
|
* @param {number} userId - User id.
|
||||||
|
*/
|
||||||
|
async deleteUser(userId: number): Promise<void> {
|
||||||
|
// Retrieve user details or throw not found service error.
|
||||||
|
const tenantUser = await this.tenantUserModel().query().findById(userId);
|
||||||
|
|
||||||
|
// Validate the delete user should not be the last active user.
|
||||||
|
if (tenantUser.isInviteAccepted) {
|
||||||
|
await this.validateNotLastUserDelete();
|
||||||
|
}
|
||||||
|
// Delete user from the storage.
|
||||||
|
await this.tenantUserModel().query().findById(userId).delete();
|
||||||
|
|
||||||
|
// Triggers `onTenantUserDeleted` event.
|
||||||
|
await this.eventEmitter.emitAsync(events.tenantUser.onDeleted, {
|
||||||
|
userId,
|
||||||
|
tenantUser,
|
||||||
|
} as ITenantUserDeletedPayload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the delete user should not be the last user.
|
||||||
|
* @param {number} tenantId
|
||||||
|
*/
|
||||||
|
private async validateNotLastUserDelete() {
|
||||||
|
const inviteAcceptedUsers = await this.tenantUserModel()
|
||||||
|
.query()
|
||||||
|
.select(['id'])
|
||||||
|
.whereNotNull('invite_accepted_at');
|
||||||
|
|
||||||
|
if (inviteAcceptedUsers.length === 1) {
|
||||||
|
throw new ServiceError(ERRORS.CANNOT_DELETE_LAST_USER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { events } from '@/common/events/events';
|
||||||
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
|
import { TenantUser } from '@/modules/Tenancy/TenancyModels/models/TenantUser.model';
|
||||||
|
import { ITenantUserEditedPayload } from '../Users.types';
|
||||||
|
import { EditUserDto } from '../dtos/EditUser.dto';
|
||||||
|
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||||
|
import { ERRORS } from '../Users.constants';
|
||||||
|
import { ModelObject } from 'objection';
|
||||||
|
import { SystemUser } from '@/modules/System/models/SystemUser';
|
||||||
|
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class EditUserService {
|
||||||
|
constructor(
|
||||||
|
@Inject(TenantUser.name)
|
||||||
|
private readonly tenantUserModel: TenantModelProxy<typeof TenantUser>,
|
||||||
|
private readonly eventEmitter: EventEmitter2,
|
||||||
|
private readonly tenancyContext: TenancyContext,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new user.
|
||||||
|
* @param {number} userId - User id.
|
||||||
|
* @param {IUserDTO} editUserDTO - Edit user DTO.
|
||||||
|
* @return {Promise<ISystemUser>}
|
||||||
|
*/
|
||||||
|
public async editUser(
|
||||||
|
userId: number,
|
||||||
|
editUserDTO: EditUserDto,
|
||||||
|
): Promise<any> {
|
||||||
|
const { email } = editUserDTO;
|
||||||
|
const authorizedUser = await this.tenancyContext.getSystemUser();
|
||||||
|
|
||||||
|
// Retrieve the tenant user or throw not found service error.
|
||||||
|
const oldTenantUser = await this.tenantUserModel()
|
||||||
|
.query()
|
||||||
|
.findById(userId)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
// Validate cannot mutate the authorized user.
|
||||||
|
this.validateMutateRoleNotAuthorizedUser(
|
||||||
|
oldTenantUser,
|
||||||
|
editUserDTO,
|
||||||
|
authorizedUser,
|
||||||
|
);
|
||||||
|
// Validate user email should be unique.
|
||||||
|
await this.validateUserEmailUniquiness(email, userId);
|
||||||
|
|
||||||
|
// Updates the tenant user.
|
||||||
|
const tenantUser = await this.tenantUserModel()
|
||||||
|
.query()
|
||||||
|
.updateAndFetchById(userId, {
|
||||||
|
...editUserDTO,
|
||||||
|
});
|
||||||
|
// Triggers `onTenantUserEdited` event.
|
||||||
|
await this.eventEmitter.emitAsync(events.tenantUser.onEdited, {
|
||||||
|
userId,
|
||||||
|
editUserDTO,
|
||||||
|
tenantUser,
|
||||||
|
oldTenantUser,
|
||||||
|
} as ITenantUserEditedPayload);
|
||||||
|
|
||||||
|
return tenantUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the given user email should be unique in the storage.
|
||||||
|
* @param {string} email - User email.
|
||||||
|
* @param {number} userId - User id.
|
||||||
|
*/
|
||||||
|
async validateUserEmailUniquiness(email: string, userId: number) {
|
||||||
|
const userByEmail = await this.tenantUserModel()
|
||||||
|
.query()
|
||||||
|
.findOne('email', email)
|
||||||
|
.whereNot('id', userId);
|
||||||
|
|
||||||
|
if (userByEmail) {
|
||||||
|
throw new ServiceError(ERRORS.EMAIL_ALREADY_EXISTS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the authorized user cannot mutate its role.
|
||||||
|
* @param {ITenantUser} oldTenantUser - Old tenant user.
|
||||||
|
* @param {IEditUserDTO} editUserDTO - Edit user dto.
|
||||||
|
* @param {ISystemUser} authorizedUser - Authorized user.
|
||||||
|
*/
|
||||||
|
validateMutateRoleNotAuthorizedUser(
|
||||||
|
oldTenantUser: ModelObject<TenantUser>,
|
||||||
|
editUserDTO: EditUserDto,
|
||||||
|
authorizedUser: ModelObject<SystemUser>,
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
authorizedUser.id === oldTenantUser.systemUserId &&
|
||||||
|
editUserDTO.roleId !== oldTenantUser.roleId
|
||||||
|
) {
|
||||||
|
throw new ServiceError(ERRORS.CANNOT_AUTHORIZED_USER_MUTATE_ROLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||||
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
|
import { TenantUser } from '@/modules/Tenancy/TenancyModels/models/TenantUser.model';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { ModelObject } from 'objection';
|
||||||
|
import { ERRORS } from '../Users.constants';
|
||||||
|
import { ITenantUserInactivatedPayload } from '../Users.types';
|
||||||
|
import { events } from '@/common/events/events';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class InactivateUserService {
|
||||||
|
constructor(
|
||||||
|
private readonly tenantUserModel: TenantModelProxy<typeof TenantUser>,
|
||||||
|
private readonly eventEmitter: EventEmitter2,
|
||||||
|
) {}
|
||||||
|
/**
|
||||||
|
* Inactivate the given user id.
|
||||||
|
* @param {number} userId
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async inactivateUser(userId: number): Promise<void> {
|
||||||
|
// Throw service error if the given user is equals the authorized user.
|
||||||
|
this.throwErrorIfUserSameAuthorizedUser(userId, authorizedUser);
|
||||||
|
|
||||||
|
// Retrieve the user or throw not found service error.
|
||||||
|
const tenantUser = await this.getTenantUserOrThrowError(tenantId, userId);
|
||||||
|
|
||||||
|
// Throw serivce error if the user is already inactivated.
|
||||||
|
this.throwErrorIfUserInactive(tenantUser);
|
||||||
|
|
||||||
|
// Marks the tenant user as active.
|
||||||
|
await this.tenantUserModel()
|
||||||
|
.query()
|
||||||
|
.findById(userId)
|
||||||
|
.update({ active: true });
|
||||||
|
|
||||||
|
// Triggers `onTenantUserActivated` event.
|
||||||
|
await this.eventEmitter.emitAsync(events.tenantUser.onInactivated, {
|
||||||
|
tenantId,
|
||||||
|
userId,
|
||||||
|
authorizedUser,
|
||||||
|
tenantUser,
|
||||||
|
} as ITenantUserInactivatedPayload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throw service error in case the given user same the authorized user.
|
||||||
|
* @param {number} userId
|
||||||
|
* @param {ModelObject<TenantUser>} authorizedUser
|
||||||
|
*/
|
||||||
|
private throwErrorIfUserSameAuthorizedUser(
|
||||||
|
userId: number,
|
||||||
|
authorizedUser: ModelObject<TenantUser>,
|
||||||
|
) {
|
||||||
|
if (userId === authorizedUser.id) {
|
||||||
|
throw new ServiceError(ERRORS.USER_SAME_THE_AUTHORIZED_USER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throws service error in case the user was already inactive.
|
||||||
|
* @param {ModelObject<TenantUser>} user
|
||||||
|
* @throws {ServiceError}
|
||||||
|
*/
|
||||||
|
private throwErrorIfUserInactive(user: ModelObject<TenantUser>) {
|
||||||
|
if (!user.active) {
|
||||||
|
throw new ServiceError(ERRORS.USER_ALREADY_INACTIVE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import * as uniqid from 'uniqid';
|
||||||
|
import * as moment from 'moment';
|
||||||
|
import {
|
||||||
|
IUserSendInviteDTO,
|
||||||
|
IUserInvitedEventPayload,
|
||||||
|
IUserInviteResendEventPayload,
|
||||||
|
} from '../Users.types';
|
||||||
|
import { ERRORS } from '../Users.constants';
|
||||||
|
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||||
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
|
import { TenantUser } from '@/modules/Tenancy/TenancyModels/models/TenantUser.model';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { events } from '@/common/events/events';
|
||||||
|
import { Role } from '@/modules/Roles/models/Role.model';
|
||||||
|
import { ModelObject } from 'objection';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class InviteTenantUserService {
|
||||||
|
constructor(
|
||||||
|
private readonly eventEmitter: EventEmitter2,
|
||||||
|
|
||||||
|
@Inject(TenantUser.name)
|
||||||
|
private readonly tenantUserModel: TenantModelProxy<typeof TenantUser>,
|
||||||
|
|
||||||
|
@Inject(Role.name)
|
||||||
|
private readonly roleModel: TenantModelProxy<typeof Role>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends invite mail to the given email from the given tenant and user.
|
||||||
|
* @param {string} email -
|
||||||
|
* @param {IUser} authorizedUser -
|
||||||
|
* @return {Promise<IUserInvite>}
|
||||||
|
*/
|
||||||
|
public async sendInvite(sendInviteDTO: IUserSendInviteDTO): Promise<{
|
||||||
|
invitedUser: TenantUser;
|
||||||
|
}> {
|
||||||
|
// Get the given role or throw not found service error.
|
||||||
|
const role = await this.roleModel().query().findById(sendInviteDTO.roleId);
|
||||||
|
|
||||||
|
// Validates the given email not exists on the storage.
|
||||||
|
await this.validateUserEmailNotExists(sendInviteDTO.email);
|
||||||
|
|
||||||
|
// Generates a new invite token.
|
||||||
|
const inviteToken = uniqid();
|
||||||
|
|
||||||
|
// Creates and fetches a tenant user.
|
||||||
|
const user = await this.tenantUserModel().query().insertAndFetch({
|
||||||
|
email: sendInviteDTO.email,
|
||||||
|
roleId: sendInviteDTO.roleId,
|
||||||
|
active: true,
|
||||||
|
invitedAt: new Date(),
|
||||||
|
});
|
||||||
|
// Triggers `onUserSendInvite` event.
|
||||||
|
await this.eventEmitter.emitAsync(events.inviteUser.sendInvite, {
|
||||||
|
inviteToken,
|
||||||
|
user,
|
||||||
|
} as IUserInvitedEventPayload);
|
||||||
|
|
||||||
|
return { invitedUser: user };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-send user invite.
|
||||||
|
* @param {number} tenantId -
|
||||||
|
* @param {string} email -
|
||||||
|
* @return {Promise<{ invite: IUserInvite }>}
|
||||||
|
*/
|
||||||
|
public async resendInvite(userId: number): Promise<{ user: ModelObject<TenantUser> }> {
|
||||||
|
// Retrieve the user by id or throw not found service error.
|
||||||
|
const user = await this.getUserByIdOrThrowError(userId);
|
||||||
|
|
||||||
|
// Validate the user is not invited recently.
|
||||||
|
this.validateUserInviteThrottle(user);
|
||||||
|
|
||||||
|
// Validate the given user is not accepted yet.
|
||||||
|
this.validateInviteUserNotAccept(user);
|
||||||
|
|
||||||
|
// Generates a new invite token.
|
||||||
|
const inviteToken = uniqid();
|
||||||
|
|
||||||
|
// Triggers `onUserSendInvite` event.
|
||||||
|
await this.eventEmitter.emitAsync(events.inviteUser.resendInvite, {
|
||||||
|
user,
|
||||||
|
inviteToken,
|
||||||
|
} as IUserInviteResendEventPayload);
|
||||||
|
|
||||||
|
return { user };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the given user has no active invite token.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} userId - User id.
|
||||||
|
*/
|
||||||
|
private validateInviteUserNotAccept = (user: ModelObject<TenantUser>) => {
|
||||||
|
// Throw the error if the one invite tokens is still active.
|
||||||
|
if (user.inviteAcceptedAt) {
|
||||||
|
throw new ServiceError(ERRORS.USER_RECENTLY_INVITED);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates user invite is not invited recently before specific time point.
|
||||||
|
* @param {ITenantUser} user
|
||||||
|
*/
|
||||||
|
private validateUserInviteThrottle = (user: ModelObject<TenantUser>) => {
|
||||||
|
const PARSE_FORMAT = 'M/D/YYYY, H:mm:ss A';
|
||||||
|
const beforeTime = moment().subtract(5, 'minutes');
|
||||||
|
|
||||||
|
if (moment(user.invitedAt, PARSE_FORMAT).isAfter(beforeTime)) {
|
||||||
|
throw new ServiceError(ERRORS.USER_RECENTLY_INVITED);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the given user by id or throw not found service error.
|
||||||
|
* @param {number} userId - User id.
|
||||||
|
*/
|
||||||
|
private getUserByIdOrThrowError = async (
|
||||||
|
userId: number,
|
||||||
|
): Promise<TenantUser> => {
|
||||||
|
// Retrieve the tenant user.
|
||||||
|
const user = await this.tenantUserModel().query().findById(userId);
|
||||||
|
|
||||||
|
// Throw if the user not found.
|
||||||
|
if (!user) {
|
||||||
|
throw new ServiceError(ERRORS.USER_NOT_FOUND);
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throws error in case the given user email not exists on the storage.
|
||||||
|
* @param {string} email
|
||||||
|
* @throws {ServiceError}
|
||||||
|
*/
|
||||||
|
private async validateUserEmailNotExists(email: string): Promise<void> {
|
||||||
|
const foundUser = await this.tenantUserModel()
|
||||||
|
.query()
|
||||||
|
.findOne('email', email);
|
||||||
|
|
||||||
|
if (foundUser) {
|
||||||
|
throw new ServiceError(ERRORS.EMAIL_EXISTS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { Mail } from '@/modules/Mail/Mail';
|
||||||
|
import { MailTransporter } from '@/modules/Mail/MailTransporter.service';
|
||||||
|
import { SystemUser } from '@/modules/System/models/SystemUser';
|
||||||
|
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { ModelObject } from 'objection';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SendInviteUsersMailMessage {
|
||||||
|
constructor(
|
||||||
|
private readonly mailTransporter: MailTransporter,
|
||||||
|
private readonly tenancyContext: TenancyContext,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
) {}
|
||||||
|
/**
|
||||||
|
* Sends invite mail to the given email.
|
||||||
|
* @param user
|
||||||
|
* @param invite
|
||||||
|
*/
|
||||||
|
async sendInviteMail(fromUser: ModelObject<SystemUser>, invite: any) {
|
||||||
|
const tenant = await this.tenancyContext.getTenant(true);
|
||||||
|
const root = path.join(global.__views_dir, '/images/bigcapital.png');
|
||||||
|
const baseURL = this.configService.get('baseURL');
|
||||||
|
|
||||||
|
const mail = new Mail()
|
||||||
|
.setSubject(`${fromUser.firstName} has invited you to join a Bigcapital`)
|
||||||
|
.setView('mail/UserInvite.html')
|
||||||
|
.setTo(invite.email)
|
||||||
|
.setAttachments([
|
||||||
|
{
|
||||||
|
filename: 'bigcapital.png',
|
||||||
|
path: root,
|
||||||
|
cid: 'bigcapital_logo',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.setData({
|
||||||
|
root,
|
||||||
|
acceptUrl: `${baseURL}/auth/invite/${invite.token}/accept`,
|
||||||
|
fullName: `${fromUser.firstName} ${fromUser.lastName}`,
|
||||||
|
firstName: fromUser.firstName,
|
||||||
|
lastName: fromUser.lastName,
|
||||||
|
email: fromUser.email,
|
||||||
|
organizationName: tenant.metadata.name,
|
||||||
|
});
|
||||||
|
this.mailTransporter.send(mail);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
packages/server/src/modules/UsersModule/dtos/EditUser.dto.ts
Normal file
16
packages/server/src/modules/UsersModule/dtos/EditUser.dto.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { IsEmail, IsNotEmpty } from 'class-validator';
|
||||||
|
|
||||||
|
export class EditUserDto {
|
||||||
|
@IsNotEmpty()
|
||||||
|
firstName: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
lastName: string;
|
||||||
|
|
||||||
|
@IsEmail()
|
||||||
|
@IsNotEmpty()
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
roleId: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class InviteUserDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
firstName: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
lastName: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { Container } from 'typedi';
|
||||||
|
import { Ability } from '@casl/ability';
|
||||||
|
import LruCache from 'lru-cache';
|
||||||
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
import { IRole, IRolePremission, ISystemUser } from '@/interfaces';
|
||||||
|
|
||||||
|
// store abilities of 1000 most active users
|
||||||
|
export const ABILITIES_CACHE = new LruCache(1000);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve ability for the given role.
|
||||||
|
* @param {} role
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function getAbilityForRole(role) {
|
||||||
|
const rules = getAbilitiesRolesConds(role);
|
||||||
|
return new Ability(rules);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve abilities of the given role.
|
||||||
|
* @param {IRole} role
|
||||||
|
* @returns {}
|
||||||
|
*/
|
||||||
|
function getAbilitiesRolesConds(role: IRole) {
|
||||||
|
switch (role.slug) {
|
||||||
|
case 'admin': // predefined role.
|
||||||
|
return getSuperAdminRules();
|
||||||
|
default:
|
||||||
|
return getRulesFromRolePermissions(role.permissions || []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the super admin rules.
|
||||||
|
* @returns {}
|
||||||
|
*/
|
||||||
|
function getSuperAdminRules() {
|
||||||
|
return [{ action: 'manage', subject: 'all' }];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve CASL rules from role permissions.
|
||||||
|
* @param {IRolePremission[]} permissions -
|
||||||
|
* @returns {}
|
||||||
|
*/
|
||||||
|
function getRulesFromRolePermissions(permissions: IRolePremission[]) {
|
||||||
|
return permissions
|
||||||
|
.filter((permission: IRolePremission) => permission.value)
|
||||||
|
.map((permission: IRolePremission) => {
|
||||||
|
return {
|
||||||
|
action: permission.ability,
|
||||||
|
subject: permission.subject,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve ability for user.
|
||||||
|
* @param {ISystemUser} user
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @returns {}
|
||||||
|
*/
|
||||||
|
async function getAbilityForUser(user: ISystemUser, tenantId: number) {
|
||||||
|
const tenancy = Container.get(HasTenancyService);
|
||||||
|
const { User } = tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const tenantUser = await User.query()
|
||||||
|
.findOne('systemUserId', user.id)
|
||||||
|
.withGraphFetched('role.permissions');
|
||||||
|
|
||||||
|
return getAbilityForRole(tenantUser.role);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Request} request -
|
||||||
|
* @param {Response} response -
|
||||||
|
* @param {NextFunction} next -
|
||||||
|
*/
|
||||||
|
export default async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const { tenantId, user } = req;
|
||||||
|
|
||||||
|
if (ABILITIES_CACHE.has(req.user.id)) {
|
||||||
|
req.ability = ABILITIES_CACHE.get(req.user.id);
|
||||||
|
} else {
|
||||||
|
req.ability = await getAbilityForUser(req.user, tenantId);
|
||||||
|
ABILITIES_CACHE.set(req.user.id, req.ability);
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
};
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
import { BaseModel } from '@/models/Model';
|
||||||
|
|
||||||
|
export class UserInvite extends BaseModel {
|
||||||
|
token!: string;
|
||||||
|
userId!: number;
|
||||||
|
tenantId!: number;
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table name.
|
||||||
|
*/
|
||||||
|
static get tableName() {
|
||||||
|
return 'user_invites';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamps columns.
|
||||||
|
*/
|
||||||
|
get timestamps() {
|
||||||
|
return ['createdAt'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model modifiers.
|
||||||
|
*/
|
||||||
|
static get modifiers() {
|
||||||
|
return {
|
||||||
|
notExpired(query) {
|
||||||
|
const comp = moment().subtract(24, 'hours').toMySqlDateTime();
|
||||||
|
query.where('created_at', '>=', comp);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { SystemUser } from '@/modules/System/models/SystemUser';
|
||||||
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
|
import { TenantUser } from '@/modules/Tenancy/TenancyModels/models/TenantUser.model';
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GetUserService {
|
||||||
|
constructor(
|
||||||
|
@Inject(TenantUser.name)
|
||||||
|
private readonly tenantUserModel: TenantModelProxy<typeof TenantUser>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the given user details.
|
||||||
|
* @param {number} tenantId - Tenant id.
|
||||||
|
* @param {number} userId - User id.
|
||||||
|
*/
|
||||||
|
public async getUser(userId: number) {
|
||||||
|
// Retrieve the system user.
|
||||||
|
const user = await this.tenantUserModel().query().findById(userId);
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
|
import { TenantUser } from '@/modules/Tenancy/TenancyModels/models/TenantUser.model';
|
||||||
|
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
||||||
|
import { Inject } from '@nestjs/common';
|
||||||
|
import { UserTransformer } from './User.transformer';
|
||||||
|
|
||||||
|
export class GetUsersService {
|
||||||
|
constructor(
|
||||||
|
@Inject(TenantUser.name)
|
||||||
|
private readonly tenantUserModel: TenantModelProxy<typeof TenantUser>,
|
||||||
|
private readonly transformerInjectable: TransformerInjectable,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve users list based on the given filter.
|
||||||
|
* @param {object} filter
|
||||||
|
*/
|
||||||
|
public async getUsers() {
|
||||||
|
const users = await this.tenantUserModel().query().withGraphFetched('role');
|
||||||
|
|
||||||
|
return this.transformerInjectable.transform(
|
||||||
|
users,
|
||||||
|
new UserTransformer(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { Transformer } from "@/modules/Transformer/Transformer";
|
||||||
|
|
||||||
|
export class UserTransformer extends Transformer {
|
||||||
|
/**
|
||||||
|
* Exclude these attributes from user object.
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
public excludeAttributes = (): string[] => {
|
||||||
|
return ['role'];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Includeded attributes.
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
public includeAttributes = (): string[] => {
|
||||||
|
return ['roleName', 'roleDescription', 'roleSlug'];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the localized role name if is predefined or stored name.
|
||||||
|
* @param role
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
public roleName(user) {
|
||||||
|
return user.role.predefined
|
||||||
|
? this.context.i18n.__(user.role.name)
|
||||||
|
: user.role.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the localized role description if is predefined or stored description.
|
||||||
|
* @param user
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
public roleDescription(user) {
|
||||||
|
return user.role.predefined
|
||||||
|
? this.context.i18n.__(user.role.description)
|
||||||
|
: user.role.description;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the role slug.
|
||||||
|
* @param user
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
public roleSlug(user) {
|
||||||
|
return user.role.slug;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { events } from '@/common/events/events';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
|
import { IUserInviteTenantSyncedEventPayload } from '../Users.types';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class InviteSendMainNotificationSubscribe {
|
||||||
|
/**
|
||||||
|
* Sends mail notification.
|
||||||
|
* @param {IUserInvitedEventPayload} payload
|
||||||
|
*/
|
||||||
|
@OnEvent(events.inviteUser.sendInviteTenantSynced)
|
||||||
|
private sendMailNotification(
|
||||||
|
payload: IUserInviteTenantSyncedEventPayload
|
||||||
|
) {
|
||||||
|
const { invite, authorizedUser, tenantId } = payload;
|
||||||
|
|
||||||
|
this.agenda.now('user-invite-mail', {
|
||||||
|
invite,
|
||||||
|
authorizedUser,
|
||||||
|
tenantId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import {
|
||||||
|
ITenantUserInactivatedPayload,
|
||||||
|
ITenantUserActivatedPayload,
|
||||||
|
ITenantUserDeletedPayload,
|
||||||
|
ITenantUserEditedPayload,
|
||||||
|
} from '../Users.types';
|
||||||
|
import { ABILITIES_CACHE } from '../../api/middleware/AuthorizationMiddleware';
|
||||||
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { events } from '@/common/events/events';
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
@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,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Syncs send invite to system user.
|
||||||
|
* @param {IUserInvitedEventPayload} payload -
|
||||||
|
*/
|
||||||
|
@OnEvent(events.inviteUser.sendInvite)
|
||||||
|
async syncSendInviteSystem({
|
||||||
|
inviteToken,
|
||||||
|
user,
|
||||||
|
tenantId,
|
||||||
|
authorizedUser,
|
||||||
|
}: IUserInvitedEventPayload) {
|
||||||
|
// 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,
|
||||||
|
tenantId,
|
||||||
|
user,
|
||||||
|
authorizedUser,
|
||||||
|
} as IUserInviteTenantSyncedEventPayload,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Syncs resend invite to system user.
|
||||||
|
* @param {IUserInviteResendEventPayload} payload -
|
||||||
|
*/
|
||||||
|
@OnEvent(events.inviteUser.resendInvite)
|
||||||
|
async syncResendInviteSystemUser({
|
||||||
|
inviteToken,
|
||||||
|
authorizedUser,
|
||||||
|
tenantId,
|
||||||
|
user,
|
||||||
|
}: IUserInviteResendEventPayload) {
|
||||||
|
// Clear all invite tokens of the given user id.
|
||||||
|
await this.clearInviteTokensByUserId(user.systemUserId, tenantId);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
userId: number,
|
||||||
|
tenantId: number,
|
||||||
|
) => {
|
||||||
|
await this.inviteModel
|
||||||
|
.query()
|
||||||
|
.where({
|
||||||
|
userId,
|
||||||
|
tenantId,
|
||||||
|
})
|
||||||
|
.delete();
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,9 +7,9 @@ import { useRequestQuery } from '../useQueryRequest';
|
|||||||
export function useDateFormats(props = {}) {
|
export function useDateFormats(props = {}) {
|
||||||
return useRequestQuery(
|
return useRequestQuery(
|
||||||
['DATE_FORMATS'],
|
['DATE_FORMATS'],
|
||||||
{ method: 'get', url: `/date_formats` },
|
{ method: 'get', url: `/date-formats` },
|
||||||
{
|
{
|
||||||
select: (res) => res.data.data,
|
select: (res) => res.data,
|
||||||
defaultData: [],
|
defaultData: [],
|
||||||
...props,
|
...props,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ function useSettingsQuery(key, query, props) {
|
|||||||
key,
|
key,
|
||||||
{ method: 'get', url: 'settings', params: query },
|
{ method: 'get', url: 'settings', params: query },
|
||||||
{
|
{
|
||||||
select: (res) => res.data.settings,
|
select: (res) => res.data,
|
||||||
defaultData: [],
|
defaultData: [],
|
||||||
...props,
|
...props,
|
||||||
},
|
},
|
||||||
@@ -170,7 +170,7 @@ export function useSettingSMSNotifications(props) {
|
|||||||
[t.SETTING_SMS_NOTIFICATIONS],
|
[t.SETTING_SMS_NOTIFICATIONS],
|
||||||
{ method: 'get', url: `settings/sms-notifications` },
|
{ method: 'get', url: `settings/sms-notifications` },
|
||||||
{
|
{
|
||||||
select: (res) => res.data.notifications,
|
select: (res) => res.data,
|
||||||
defaultData: [],
|
defaultData: [],
|
||||||
...props,
|
...props,
|
||||||
},
|
},
|
||||||
@@ -188,7 +188,7 @@ export function useSettingSMSNotification(key, props) {
|
|||||||
url: `settings/sms-notification/${key}`,
|
url: `settings/sms-notification/${key}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
select: (res) => res.data.notification,
|
select: (res) => res.data,
|
||||||
defaultData: {
|
defaultData: {
|
||||||
smsNotification: [],
|
smsNotification: [],
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user