Skip to content

Commit b2aad71

Browse files
authored
chore: Move welcome email to processor (outline#11939)
* chore: Move welcome email to processor * fix: Restore welcome email on invite acceptance
1 parent 12c71f2 commit b2aad71

File tree

5 files changed

+41
-32
lines changed

5 files changed

+41
-32
lines changed

plugins/webhooks/server/tasks/DeliverWebhookTask.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ export default class DeliverWebhookTask extends BaseTask<Props> {
130130
case "users.invite":
131131
case "users.promote":
132132
case "users.demote":
133+
case "users.invite_accepted":
133134
await this.handleUserEvent(subscription, event);
134135
return;
135136
case "documents.create":

server/commands/accountProvisioner.test.ts

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { faker } from "@faker-js/faker";
22
import { randomUUID } from "node:crypto";
3-
import WelcomeEmail from "@server/emails/templates/WelcomeEmail";
43
import { TeamDomain } from "@server/models";
54
import Collection from "@server/models/Collection";
65
import UserAuthentication from "@server/models/UserAuthentication";
@@ -15,7 +14,6 @@ describe("accountProvisioner", () => {
1514

1615
describe("hosted", () => {
1716
it("should create a new user and team", async () => {
18-
const spy = jest.spyOn(WelcomeEmail.prototype, "schedule");
1917
const email = faker.internet.email();
2018
const { user, team, isNewTeam, isNewUser } = await accountProvisioner(
2119
ctx,
@@ -50,19 +48,15 @@ describe("accountProvisioner", () => {
5048
expect(user.email).toEqual(email);
5149
expect(isNewUser).toEqual(true);
5250
expect(isNewTeam).toEqual(true);
53-
expect(spy).toHaveBeenCalled();
5451
const collectionCount = await Collection.count({
5552
where: {
5653
teamId: team.id,
5754
},
5855
});
5956
expect(collectionCount).toEqual(1);
60-
61-
spy.mockRestore();
6257
});
6358

64-
it("should update exising user and authentication", async () => {
65-
const spy = jest.spyOn(WelcomeEmail.prototype, "schedule");
59+
it("should update existing user and authentication", async () => {
6660
const existingTeam = await buildTeam();
6761
const providers = await existingTeam.$get("authenticationProviders");
6862
const authenticationProvider = providers[0];
@@ -100,9 +94,6 @@ describe("accountProvisioner", () => {
10094
expect(user.email).toEqual(newEmail);
10195
expect(isNewTeam).toEqual(false);
10296
expect(isNewUser).toEqual(false);
103-
expect(spy).not.toHaveBeenCalled();
104-
105-
spy.mockRestore();
10697
});
10798

10899
it("should allow authentication by email matching", async () => {
@@ -283,7 +274,6 @@ describe("accountProvisioner", () => {
283274
});
284275

285276
it("should create a new user in an existing team when the domain is allowed", async () => {
286-
const spy = jest.spyOn(WelcomeEmail.prototype, "schedule");
287277
const team = await buildTeam();
288278
const admin = await buildAdmin({ teamId: team.id });
289279
const authenticationProviders = await team.$get(
@@ -324,20 +314,16 @@ describe("accountProvisioner", () => {
324314
expect(auth.scopes[0]).toEqual("read");
325315
expect(user.email).toEqual(email);
326316
expect(isNewUser).toEqual(true);
327-
expect(spy).toHaveBeenCalled();
328317
// should provision welcome collection
329318
const collectionCount = await Collection.count({
330319
where: {
331320
teamId: team.id,
332321
},
333322
});
334323
expect(collectionCount).toEqual(1);
335-
336-
spy.mockRestore();
337324
});
338325

339326
it("should create a new user in an existing team", async () => {
340-
const spy = jest.spyOn(WelcomeEmail.prototype, "schedule");
341327
const team = await buildTeam();
342328
const authenticationProviders = await team.$get(
343329
"authenticationProviders"
@@ -372,20 +358,16 @@ describe("accountProvisioner", () => {
372358
expect(auth.scopes[0]).toEqual("read");
373359
expect(user.email).toEqual(email);
374360
expect(isNewUser).toEqual(true);
375-
expect(spy).toHaveBeenCalled();
376361
// should provision welcome collection
377362
const collectionCount = await Collection.count({
378363
where: {
379364
teamId: team.id,
380365
},
381366
});
382367
expect(collectionCount).toEqual(1);
383-
384-
spy.mockRestore();
385368
});
386369

387370
it("should handle emails with capital letters correctly", async () => {
388-
const spy = jest.spyOn(WelcomeEmail.prototype, "schedule");
389371
const email = "Jenny.Tester@EXAMPLE.COM";
390372

391373
const params = {
@@ -418,7 +400,6 @@ describe("accountProvisioner", () => {
418400
expect(user.email).toEqual(email);
419401
expect(isNewUser).toEqual(true);
420402
expect(isNewTeam).toEqual(true);
421-
expect(spy).toHaveBeenCalled();
422403

423404
// Test that we can find the user again
424405
const existing = await accountProvisioner(ctx, params);
@@ -427,8 +408,6 @@ describe("accountProvisioner", () => {
427408
expect(existing.isNewTeam).toEqual(false);
428409
expect(existing.isNewUser).toEqual(false);
429410
expect(existing.user.id).toEqual(user.id);
430-
431-
spy.mockRestore();
432411
});
433412

434413
it("should allow connecting a new authentication provider while logged in", async () => {

server/commands/accountProvisioner.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import path from "node:path";
22
import { readFile } from "fs-extra";
33
import invariant from "invariant";
44
import { CollectionPermission, UserRole } from "@shared/types";
5-
import WelcomeEmail from "@server/emails/templates/WelcomeEmail";
65
import env from "@server/env";
76
import {
87
InvalidAuthenticationError,
@@ -15,6 +14,7 @@ import {
1514
AuthenticationProvider,
1615
Collection,
1716
Document,
17+
Event,
1818
Team,
1919
} from "@server/models";
2020
import { DocumentHelper } from "@server/models/helpers/DocumentHelper";
@@ -194,14 +194,11 @@ async function accountProvisioner(
194194
});
195195
const { isNewUser, user } = result;
196196

197-
// TODO: Move to processor
198-
if (isNewUser) {
199-
await new WelcomeEmail({
200-
to: user.email,
201-
language: user.language,
202-
role: user.role,
203-
teamUrl: team.url,
204-
}).schedule();
197+
if (isNewUser && user.isInvited) {
198+
await Event.createFromContext(ctx, {
199+
name: "users.invite_accepted",
200+
userId: user.id,
201+
});
205202
}
206203

207204
if (isNewUser || isNewTeam) {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import WelcomeEmail from "@server/emails/templates/WelcomeEmail";
2+
import { Team, User } from "@server/models";
3+
import type { Event, UserEvent } from "@server/types";
4+
import BaseProcessor from "./BaseProcessor";
5+
6+
export default class UserCreatedProcessor extends BaseProcessor {
7+
static applicableEvents: Event["name"][] = [
8+
"users.create",
9+
"users.invite_accepted",
10+
];
11+
12+
async perform(event: UserEvent) {
13+
const [user, team] = await Promise.all([
14+
User.findByPk(event.userId, { rejectOnEmpty: true }),
15+
Team.findByPk(event.teamId, { rejectOnEmpty: true }),
16+
]);
17+
18+
// Invited users receive an InviteEmail at invite time, and a WelcomeEmail
19+
// when they accept the invite and sign in for the first time.
20+
if (event.name === "users.create" && user.isInvited) {
21+
return;
22+
}
23+
24+
await new WelcomeEmail({
25+
to: user.email,
26+
language: user.language,
27+
role: user.role,
28+
teamUrl: team.url,
29+
}).schedule();
30+
}
31+
}

server/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,8 @@ export type UserEvent = BaseEvent<User> &
162162
| "users.update"
163163
| "users.suspend"
164164
| "users.activate"
165-
| "users.delete";
165+
| "users.delete"
166+
| "users.invite_accepted";
166167
userId: string;
167168
}
168169
| {

0 commit comments

Comments
 (0)