mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 13:50:31 +00:00
fix: invite user service issues
This commit is contained in:
@@ -20,6 +20,7 @@ import { GetUsersService } from './queries/GetUsers.service';
|
||||
import { AcceptInviteUserService } from './commands/AcceptInviteUser.service';
|
||||
import { InviteTenantUserService } from './commands/InviteUser.service';
|
||||
import { UsersInviteController } from './UsersInvite.controller';
|
||||
import { UsersInvitePublicController } from './UsersInvitePublic.controller';
|
||||
import { InjectSystemModel } from '../System/SystemModels/SystemModels.module';
|
||||
import { SendInviteUserMailQueue } from './Users.constants';
|
||||
import InviteSendMainNotificationSubscribe from './subscribers/InviteSendMailNotification.subscriber';
|
||||
@@ -60,6 +61,6 @@ const models = [InjectSystemModel(UserInvite)];
|
||||
SendInviteUsersMailMessage,
|
||||
UsersApplication
|
||||
],
|
||||
controllers: [UsersController, UsersInviteController],
|
||||
controllers: [UsersController, UsersInviteController, UsersInvitePublicController],
|
||||
})
|
||||
export class UsersModule {}
|
||||
|
||||
@@ -1,40 +1,13 @@
|
||||
import { Body, Controller, Get, Param, Patch, Post } from '@nestjs/common';
|
||||
import { Body, Controller, Param, Patch, Post } from '@nestjs/common';
|
||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { UsersApplication } from './Users.application';
|
||||
import { InviteUserDto, SendInviteUserDto } from './dtos/InviteUser.dto';
|
||||
import { SendInviteUserDto } from './dtos/InviteUser.dto';
|
||||
|
||||
@Controller('invite')
|
||||
@ApiTags('Users')
|
||||
export class UsersInviteController {
|
||||
constructor(private readonly usersApplication: UsersApplication) {}
|
||||
|
||||
/**
|
||||
* Accept a user invitation.
|
||||
*/
|
||||
@Post('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('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.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { PublicRoute } from '@/modules/Auth/guards/jwt.guard';
|
||||
import { UsersApplication } from './Users.application';
|
||||
import { InviteUserDto } from './dtos/InviteUser.dto';
|
||||
|
||||
@Controller('invite')
|
||||
@ApiTags('Users')
|
||||
@PublicRoute()
|
||||
export class UsersInvitePublicController {
|
||||
constructor(private readonly usersApplication: UsersApplication) {}
|
||||
|
||||
/**
|
||||
* Accept a user invitation.
|
||||
*/
|
||||
@Post('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('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;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as moment from 'moment';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import {
|
||||
IAcceptInviteEventPayload,
|
||||
ICheckInviteEventPayload,
|
||||
@@ -15,6 +16,11 @@ import { UserInvite } from '../models/InviteUser.model';
|
||||
import { ModelObject } from 'objection';
|
||||
import { InviteUserDto } from '../dtos/InviteUser.dto';
|
||||
|
||||
interface InviteAcceptResponseDto {
|
||||
inviteToken: { email: string, token: string, createdAt: Date };
|
||||
orgName: string
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AcceptInviteUserService {
|
||||
constructor(
|
||||
@@ -27,6 +33,7 @@ export class AcceptInviteUserService {
|
||||
@Inject(UserInvite.name)
|
||||
private readonly userInviteModel: typeof UserInvite,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly cls: ClsService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -62,6 +69,16 @@ export class AcceptInviteUserService {
|
||||
// Clear invite token by the given user id.
|
||||
await this.clearInviteTokensByUserId(inviteToken.userId);
|
||||
|
||||
// Retrieve the tenant to get the organizationId for CLS.
|
||||
const tenant = await this.tenantModel
|
||||
.query()
|
||||
.findById(inviteToken.tenantId);
|
||||
|
||||
// Set CLS values for tenant context before triggering sync events.
|
||||
this.cls.set('tenantId', inviteToken.tenantId);
|
||||
this.cls.set('userId', systemUser.id);
|
||||
this.cls.set('organizationId', tenant.organizationId);
|
||||
|
||||
// Triggers `onUserAcceptInvite` event.
|
||||
await this.eventEmitter.emitAsync(events.inviteUser.acceptInvite, {
|
||||
inviteToken,
|
||||
@@ -77,7 +94,7 @@ export class AcceptInviteUserService {
|
||||
*/
|
||||
public async checkInvite(
|
||||
token: string,
|
||||
): Promise<{ inviteToken: ModelObject<UserInvite>; orgName: string }> {
|
||||
): Promise<InviteAcceptResponseDto> {
|
||||
const inviteToken = await this.getInviteTokenOrThrowError(token);
|
||||
|
||||
// Find the tenant that associated to the given token.
|
||||
@@ -92,7 +109,16 @@ export class AcceptInviteUserService {
|
||||
tenant,
|
||||
} as ICheckInviteEventPayload);
|
||||
|
||||
return { inviteToken, orgName: tenant.metadata.name };
|
||||
// Explicitly convert to plain object to ensure all fields are serialized
|
||||
const result = {
|
||||
inviteToken: {
|
||||
email: inviteToken.email,
|
||||
token: inviteToken.token,
|
||||
createdAt: inviteToken.createdAt,
|
||||
},
|
||||
orgName: tenant.metadata.name,
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,6 +6,7 @@ export class UserInvite extends BaseModel {
|
||||
userId!: number;
|
||||
tenantId!: number;
|
||||
email!: string;
|
||||
createdAt!: Date;
|
||||
|
||||
/**
|
||||
* Table name.
|
||||
@@ -32,4 +33,11 @@ export class UserInvite extends BaseModel {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before inserting a new record.
|
||||
*/
|
||||
$beforeInsert() {
|
||||
this.createdAt = new Date();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { omit } from 'lodash';
|
||||
import { pick } from 'lodash';
|
||||
import * as moment from 'moment';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
@@ -22,13 +22,12 @@ export class SyncTenantAcceptInviteSubscriber {
|
||||
async syncTenantAcceptInvite({
|
||||
inviteToken,
|
||||
user,
|
||||
inviteUserDTO,
|
||||
}: IAcceptInviteEventPayload) {
|
||||
await this.tenantUserModel()
|
||||
.query()
|
||||
.where('systemUserId', inviteToken.userId)
|
||||
.update({
|
||||
...omit(inviteUserDTO, ['password']),
|
||||
...pick(user, ['firstName', 'lastName', 'email', 'active']),
|
||||
inviteAcceptedAt: moment().format('YYYY-MM-DD'),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ export default function InviteAcceptForm() {
|
||||
data: { errors },
|
||||
},
|
||||
}) => {
|
||||
if (errors.find((e) => e.type === 'INVITE.TOKEN.NOT.FOUND')) {
|
||||
if (errors.find((e) => e.type === 'INVITE_TOKEN_INVALID')) {
|
||||
AppToaster.show({
|
||||
message: intl.get('an_unexpected_error_occurred'),
|
||||
intent: Intent.DANGER,
|
||||
@@ -71,14 +71,6 @@ export default function InviteAcceptForm() {
|
||||
phone_number: 'This phone number is used in another account.',
|
||||
});
|
||||
}
|
||||
if (errors.find((e) => e.type === 'INVITE.TOKEN.NOT.FOUND')) {
|
||||
AppToaster.show({
|
||||
message: intl.get('an_unexpected_error_occurred'),
|
||||
intent: Intent.DANGER,
|
||||
position: Position.BOTTOM,
|
||||
});
|
||||
history.push('/auth/login');
|
||||
}
|
||||
setSubmitting(false);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -29,14 +29,22 @@ function InviteAcceptProvider({ token, ...props }) {
|
||||
if (inviteMetaError) { history.push('/auth/login'); }
|
||||
}, [history, inviteMetaError]);
|
||||
|
||||
// Transform the backend response to match frontend expectations.
|
||||
const transformedInviteMeta = inviteMeta
|
||||
? {
|
||||
email: inviteMeta.inviteToken?.email,
|
||||
organizationName: inviteMeta.orgName,
|
||||
}
|
||||
: null;
|
||||
|
||||
// Provider payload.
|
||||
const provider = {
|
||||
token,
|
||||
inviteMeta,
|
||||
inviteMeta: transformedInviteMeta,
|
||||
inviteMetaError,
|
||||
isInviteMetaError,
|
||||
isInviteMetaLoading,
|
||||
inviteAcceptMutate
|
||||
inviteAcceptMutate,
|
||||
};
|
||||
|
||||
if (inviteMetaError) {
|
||||
@@ -45,7 +53,6 @@ function InviteAcceptProvider({ token, ...props }) {
|
||||
|
||||
return (
|
||||
<InviteAcceptLoading isLoading={isInviteMetaLoading}>
|
||||
{ isInviteMetaError }
|
||||
<InviteAcceptContext.Provider value={provider} {...props} />
|
||||
</InviteAcceptLoading>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { useMutation } from 'react-query';
|
||||
import { useRequestQuery } from '../useQueryRequest';
|
||||
import useApiRequest from '../useRequest';
|
||||
import { transformToCamelCase } from '@/utils';
|
||||
|
||||
/**
|
||||
* Authentication invite accept.
|
||||
@@ -22,9 +23,9 @@ export const useAuthInviteAccept = (props) => {
|
||||
export const useInviteMetaByToken = (token, props) => {
|
||||
return useRequestQuery(
|
||||
['INVITE_META', token],
|
||||
{ method: 'get', url: `invite/invited/${token}` },
|
||||
{ method: 'get', url: `invite/check/${token}` },
|
||||
{
|
||||
select: (res) => res.data,
|
||||
select: (res) => transformToCamelCase(res.data),
|
||||
...props
|
||||
}
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user