feat(server): send invitation accepted notification (#10421)
This commit is contained in:
@@ -85,3 +85,32 @@ test('should create invitation notification', async t => {
|
|||||||
t.is(spy.firstCall.args[0].body.workspaceId, workspace.id);
|
t.is(spy.firstCall.args[0].body.workspaceId, workspace.id);
|
||||||
t.is(spy.firstCall.args[0].body.createdByUserId, owner.id);
|
t.is(spy.firstCall.args[0].body.createdByUserId, owner.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should create invitation accepted notification when user accepts the invitation', async t => {
|
||||||
|
const { notificationJob, notificationService, models } = t.context;
|
||||||
|
const invite = await models.workspaceUser.set(
|
||||||
|
workspace.id,
|
||||||
|
member.id,
|
||||||
|
WorkspaceRole.Collaborator,
|
||||||
|
WorkspaceMemberStatus.Accepted
|
||||||
|
);
|
||||||
|
const spy = Sinon.spy(notificationService, 'createInvitationAccepted');
|
||||||
|
await notificationJob.sendInvitationAccepted({
|
||||||
|
inviterId: owner.id,
|
||||||
|
inviteId: invite.id,
|
||||||
|
});
|
||||||
|
t.is(spy.callCount, 1);
|
||||||
|
t.is(spy.firstCall.args[0].userId, owner.id);
|
||||||
|
t.is(spy.firstCall.args[0].body.workspaceId, workspace.id);
|
||||||
|
t.is(spy.firstCall.args[0].body.createdByUserId, member.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should ignore send invitation accepted notification when inviteId not exists', async t => {
|
||||||
|
const { notificationJob, notificationService } = t.context;
|
||||||
|
const spy = Sinon.spy(notificationService, 'createInvitationAccepted');
|
||||||
|
await notificationJob.sendInvitationAccepted({
|
||||||
|
inviterId: owner.id,
|
||||||
|
inviteId: `not-exists-${randomUUID()}`,
|
||||||
|
});
|
||||||
|
t.is(spy.callCount, 0);
|
||||||
|
});
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ test('should not create invitation notification if user is already a member', as
|
|||||||
t.is(notification, undefined);
|
t.is(notification, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should create invitation accepted notification', async t => {
|
test('should create invitation accepted notification and email', async t => {
|
||||||
const { notificationService } = t.context;
|
const { notificationService } = t.context;
|
||||||
const inviteId = randomUUID();
|
const inviteId = randomUUID();
|
||||||
const notification = await notificationService.createInvitationAccepted({
|
const notification = await notificationService.createInvitationAccepted({
|
||||||
@@ -140,6 +140,50 @@ test('should create invitation accepted notification', async t => {
|
|||||||
t.is(notification!.body.workspaceId, workspace.id);
|
t.is(notification!.body.workspaceId, workspace.id);
|
||||||
t.is(notification!.body.createdByUserId, member.id);
|
t.is(notification!.body.createdByUserId, member.id);
|
||||||
t.is(notification!.body.inviteId, inviteId);
|
t.is(notification!.body.inviteId, inviteId);
|
||||||
|
|
||||||
|
// should send email
|
||||||
|
const invitationAcceptedMail = t.context.module.mails.last('MemberAccepted');
|
||||||
|
t.is(invitationAcceptedMail.to, owner.email);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not send invitation accepted email if user settings is not receive invitation email', async t => {
|
||||||
|
const { notificationService } = t.context;
|
||||||
|
const inviteId = randomUUID();
|
||||||
|
// should not send email if user settings is not receive invitation email
|
||||||
|
await t.context.models.settings.set(owner.id, {
|
||||||
|
receiveInvitationEmail: false,
|
||||||
|
});
|
||||||
|
const invitationAcceptedMailCount =
|
||||||
|
t.context.module.mails.count('MemberAccepted');
|
||||||
|
const notification = await notificationService.createInvitationAccepted({
|
||||||
|
userId: owner.id,
|
||||||
|
body: {
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
createdByUserId: member.id,
|
||||||
|
inviteId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
t.truthy(notification);
|
||||||
|
// no new invitation accepted email should be sent
|
||||||
|
t.is(
|
||||||
|
t.context.module.mails.count('MemberAccepted'),
|
||||||
|
invitationAcceptedMailCount
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not create invitation accepted notification if user is not an active member', async t => {
|
||||||
|
const { notificationService, models } = t.context;
|
||||||
|
const inviteId = randomUUID();
|
||||||
|
mock.method(models.workspaceUser, 'getActive', async () => null);
|
||||||
|
const notification = await notificationService.createInvitationAccepted({
|
||||||
|
userId: owner.id,
|
||||||
|
body: {
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
createdByUserId: member.id,
|
||||||
|
inviteId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
t.is(notification, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should create invitation blocked notification', async t => {
|
test('should create invitation blocked notification', async t => {
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ declare global {
|
|||||||
inviterId: string;
|
inviterId: string;
|
||||||
inviteId: string;
|
inviteId: string;
|
||||||
};
|
};
|
||||||
|
'notification.sendInvitationAccepted': {
|
||||||
|
inviterId: string;
|
||||||
|
inviteId: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,4 +61,23 @@ export class NotificationJob {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OnJob('notification.sendInvitationAccepted')
|
||||||
|
async sendInvitationAccepted({
|
||||||
|
inviterId,
|
||||||
|
inviteId,
|
||||||
|
}: Jobs['notification.sendInvitationAccepted']) {
|
||||||
|
const invite = await this.models.workspaceUser.getById(inviteId);
|
||||||
|
if (!invite) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.service.createInvitationAccepted({
|
||||||
|
userId: inviterId,
|
||||||
|
body: {
|
||||||
|
workspaceId: invite.workspaceId,
|
||||||
|
createdByUserId: invite.userId,
|
||||||
|
inviteId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,11 +130,49 @@ export class NotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createInvitationAccepted(input: InvitationNotificationCreate) {
|
async createInvitationAccepted(input: InvitationNotificationCreate) {
|
||||||
await this.ensureWorkspaceContentExists(input.body.workspaceId);
|
const workspaceId = input.body.workspaceId;
|
||||||
return await this.models.notification.createInvitation(
|
const userId = input.userId;
|
||||||
|
if (!(await this.isActiveWorkspaceUser(workspaceId, userId))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.ensureWorkspaceContentExists(workspaceId);
|
||||||
|
const notification = await this.models.notification.createInvitation(
|
||||||
input,
|
input,
|
||||||
NotificationType.InvitationAccepted
|
NotificationType.InvitationAccepted
|
||||||
);
|
);
|
||||||
|
await this.sendInvitationAcceptedEmail(input);
|
||||||
|
return notification;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async sendInvitationAcceptedEmail(
|
||||||
|
input: InvitationNotificationCreate
|
||||||
|
) {
|
||||||
|
const inviterUserId = input.userId;
|
||||||
|
const inviteeUserId = input.body.createdByUserId;
|
||||||
|
const workspaceId = input.body.workspaceId;
|
||||||
|
const userSetting = await this.models.settings.get(inviterUserId);
|
||||||
|
if (!userSetting.receiveInvitationEmail) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const inviter = await this.models.user.getWorkspaceUser(inviterUserId);
|
||||||
|
if (!inviter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.mailer.send({
|
||||||
|
name: 'MemberAccepted',
|
||||||
|
to: inviter.email,
|
||||||
|
props: {
|
||||||
|
user: {
|
||||||
|
$$userId: inviteeUserId,
|
||||||
|
},
|
||||||
|
workspace: {
|
||||||
|
$$workspaceId: workspaceId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.logger.log(
|
||||||
|
`Invitation accepted email sent to user ${inviter.id} for workspace ${workspaceId}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createInvitationBlocked(input: InvitationNotificationCreate) {
|
async createInvitationBlocked(input: InvitationNotificationCreate) {
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { getStreamAsBuffer } from 'get-stream';
|
import { getStreamAsBuffer } from 'get-stream';
|
||||||
|
|
||||||
import { Cache, JobQueue, NotFound, OnEvent, URLHelper } from '../../../base';
|
import {
|
||||||
|
Cache,
|
||||||
|
JobQueue,
|
||||||
|
NotFound,
|
||||||
|
OnEvent,
|
||||||
|
URLHelper,
|
||||||
|
UserNotFound,
|
||||||
|
} from '../../../base';
|
||||||
import {
|
import {
|
||||||
DEFAULT_WORKSPACE_AVATAR,
|
DEFAULT_WORKSPACE_AVATAR,
|
||||||
DEFAULT_WORKSPACE_NAME,
|
DEFAULT_WORKSPACE_NAME,
|
||||||
@@ -75,10 +82,9 @@ export class WorkspaceService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendAcceptedEmail(inviteId: string) {
|
async sendInvitationAcceptedNotification(inviteId: string) {
|
||||||
const { workspaceId, inviterUserId, inviteeUserId } =
|
const { workspaceId, inviterUserId, inviteeUserId } =
|
||||||
await this.getInviteInfo(inviteId);
|
await this.getInviteInfo(inviteId);
|
||||||
|
|
||||||
const inviter = inviterUserId
|
const inviter = inviterUserId
|
||||||
? await this.models.user.getWorkspaceUser(inviterUserId)
|
? await this.models.user.getWorkspaceUser(inviterUserId)
|
||||||
: await this.models.workspaceUser.getOwner(workspaceId);
|
: await this.models.workspaceUser.getOwner(workspaceId);
|
||||||
@@ -87,20 +93,12 @@ export class WorkspaceService {
|
|||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
`Inviter or invitee user not found for inviteId: ${inviteId}`
|
`Inviter or invitee user not found for inviteId: ${inviteId}`
|
||||||
);
|
);
|
||||||
return false;
|
throw new UserNotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.mailer.send({
|
await this.queue.add('notification.sendInvitationAccepted', {
|
||||||
name: 'MemberAccepted',
|
inviterId: inviter.id,
|
||||||
to: inviter.email,
|
inviteId,
|
||||||
props: {
|
|
||||||
user: {
|
|
||||||
$$userId: inviteeUserId,
|
|
||||||
},
|
|
||||||
workspace: {
|
|
||||||
$$workspaceId: workspaceId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -582,7 +582,11 @@ export class WorkspaceResolver {
|
|||||||
@CurrentUser() user: CurrentUser | undefined,
|
@CurrentUser() user: CurrentUser | undefined,
|
||||||
@Args('workspaceId') workspaceId: string,
|
@Args('workspaceId') workspaceId: string,
|
||||||
@Args('inviteId') inviteId: string,
|
@Args('inviteId') inviteId: string,
|
||||||
@Args('sendAcceptMail', { nullable: true }) sendAcceptMail: boolean
|
@Args('sendAcceptMail', {
|
||||||
|
nullable: true,
|
||||||
|
deprecationReason: 'never used',
|
||||||
|
})
|
||||||
|
_sendAcceptMail: boolean
|
||||||
) {
|
) {
|
||||||
const lockFlag = `invite:${workspaceId}`;
|
const lockFlag = `invite:${workspaceId}`;
|
||||||
await using lock = await this.mutex.acquire(lockFlag);
|
await using lock = await this.mutex.acquire(lockFlag);
|
||||||
@@ -642,12 +646,8 @@ export class WorkspaceResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sendAcceptMail) {
|
|
||||||
const success = await this.workspaceService.sendAcceptedEmail(inviteId);
|
|
||||||
if (!success) throw new UserNotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.models.workspaceUser.accept(inviteId);
|
await this.models.workspaceUser.accept(inviteId);
|
||||||
|
await this.workspaceService.sendInvitationAcceptedNotification(inviteId);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -850,7 +850,7 @@ type MissingOauthQueryParameterDataType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
acceptInviteById(inviteId: String!, sendAcceptMail: Boolean, workspaceId: String!): Boolean!
|
acceptInviteById(inviteId: String!, sendAcceptMail: Boolean @deprecated(reason: "never used"), workspaceId: String!): Boolean!
|
||||||
activateLicense(license: String!, workspaceId: String!): License!
|
activateLicense(license: String!, workspaceId: String!): License!
|
||||||
|
|
||||||
"""add a category to context"""
|
"""add a category to context"""
|
||||||
|
|||||||
Reference in New Issue
Block a user