diff --git a/plugins/webhooks/server/tasks/DeliverWebhookTask.ts b/plugins/webhooks/server/tasks/DeliverWebhookTask.ts index 883c740927..3f07aa0062 100644 --- a/plugins/webhooks/server/tasks/DeliverWebhookTask.ts +++ b/plugins/webhooks/server/tasks/DeliverWebhookTask.ts @@ -130,6 +130,7 @@ export default class DeliverWebhookTask extends BaseTask { case "users.invite": case "users.promote": case "users.demote": + case "users.invite_accepted": await this.handleUserEvent(subscription, event); return; case "documents.create": diff --git a/server/commands/accountProvisioner.test.ts b/server/commands/accountProvisioner.test.ts index 0d24074609..4d1e33baed 100644 --- a/server/commands/accountProvisioner.test.ts +++ b/server/commands/accountProvisioner.test.ts @@ -1,6 +1,5 @@ import { faker } from "@faker-js/faker"; import { randomUUID } from "node:crypto"; -import WelcomeEmail from "@server/emails/templates/WelcomeEmail"; import { TeamDomain } from "@server/models"; import Collection from "@server/models/Collection"; import UserAuthentication from "@server/models/UserAuthentication"; @@ -15,7 +14,6 @@ describe("accountProvisioner", () => { describe("hosted", () => { it("should create a new user and team", async () => { - const spy = jest.spyOn(WelcomeEmail.prototype, "schedule"); const email = faker.internet.email(); const { user, team, isNewTeam, isNewUser } = await accountProvisioner( ctx, @@ -50,19 +48,15 @@ describe("accountProvisioner", () => { expect(user.email).toEqual(email); expect(isNewUser).toEqual(true); expect(isNewTeam).toEqual(true); - expect(spy).toHaveBeenCalled(); const collectionCount = await Collection.count({ where: { teamId: team.id, }, }); expect(collectionCount).toEqual(1); - - spy.mockRestore(); }); - it("should update exising user and authentication", async () => { - const spy = jest.spyOn(WelcomeEmail.prototype, "schedule"); + it("should update existing user and authentication", async () => { const existingTeam = await buildTeam(); const providers = await existingTeam.$get("authenticationProviders"); const authenticationProvider = providers[0]; @@ -100,9 +94,6 @@ describe("accountProvisioner", () => { expect(user.email).toEqual(newEmail); expect(isNewTeam).toEqual(false); expect(isNewUser).toEqual(false); - expect(spy).not.toHaveBeenCalled(); - - spy.mockRestore(); }); it("should allow authentication by email matching", async () => { @@ -283,7 +274,6 @@ describe("accountProvisioner", () => { }); it("should create a new user in an existing team when the domain is allowed", async () => { - const spy = jest.spyOn(WelcomeEmail.prototype, "schedule"); const team = await buildTeam(); const admin = await buildAdmin({ teamId: team.id }); const authenticationProviders = await team.$get( @@ -324,7 +314,6 @@ describe("accountProvisioner", () => { expect(auth.scopes[0]).toEqual("read"); expect(user.email).toEqual(email); expect(isNewUser).toEqual(true); - expect(spy).toHaveBeenCalled(); // should provision welcome collection const collectionCount = await Collection.count({ where: { @@ -332,12 +321,9 @@ describe("accountProvisioner", () => { }, }); expect(collectionCount).toEqual(1); - - spy.mockRestore(); }); it("should create a new user in an existing team", async () => { - const spy = jest.spyOn(WelcomeEmail.prototype, "schedule"); const team = await buildTeam(); const authenticationProviders = await team.$get( "authenticationProviders" @@ -372,7 +358,6 @@ describe("accountProvisioner", () => { expect(auth.scopes[0]).toEqual("read"); expect(user.email).toEqual(email); expect(isNewUser).toEqual(true); - expect(spy).toHaveBeenCalled(); // should provision welcome collection const collectionCount = await Collection.count({ where: { @@ -380,12 +365,9 @@ describe("accountProvisioner", () => { }, }); expect(collectionCount).toEqual(1); - - spy.mockRestore(); }); it("should handle emails with capital letters correctly", async () => { - const spy = jest.spyOn(WelcomeEmail.prototype, "schedule"); const email = "Jenny.Tester@EXAMPLE.COM"; const params = { @@ -418,7 +400,6 @@ describe("accountProvisioner", () => { expect(user.email).toEqual(email); expect(isNewUser).toEqual(true); expect(isNewTeam).toEqual(true); - expect(spy).toHaveBeenCalled(); // Test that we can find the user again const existing = await accountProvisioner(ctx, params); @@ -427,8 +408,6 @@ describe("accountProvisioner", () => { expect(existing.isNewTeam).toEqual(false); expect(existing.isNewUser).toEqual(false); expect(existing.user.id).toEqual(user.id); - - spy.mockRestore(); }); it("should allow connecting a new authentication provider while logged in", async () => { diff --git a/server/commands/accountProvisioner.ts b/server/commands/accountProvisioner.ts index e5fc6c1e87..dfac2db306 100644 --- a/server/commands/accountProvisioner.ts +++ b/server/commands/accountProvisioner.ts @@ -2,7 +2,6 @@ import path from "node:path"; import { readFile } from "fs-extra"; import invariant from "invariant"; import { CollectionPermission, UserRole } from "@shared/types"; -import WelcomeEmail from "@server/emails/templates/WelcomeEmail"; import env from "@server/env"; import { InvalidAuthenticationError, @@ -15,6 +14,7 @@ import { AuthenticationProvider, Collection, Document, + Event, Team, } from "@server/models"; import { DocumentHelper } from "@server/models/helpers/DocumentHelper"; @@ -194,14 +194,11 @@ async function accountProvisioner( }); const { isNewUser, user } = result; - // TODO: Move to processor - if (isNewUser) { - await new WelcomeEmail({ - to: user.email, - language: user.language, - role: user.role, - teamUrl: team.url, - }).schedule(); + if (isNewUser && user.isInvited) { + await Event.createFromContext(ctx, { + name: "users.invite_accepted", + userId: user.id, + }); } if (isNewUser || isNewTeam) { diff --git a/server/queues/processors/UserCreatedProcessor.ts b/server/queues/processors/UserCreatedProcessor.ts new file mode 100644 index 0000000000..3fe934a5e6 --- /dev/null +++ b/server/queues/processors/UserCreatedProcessor.ts @@ -0,0 +1,31 @@ +import WelcomeEmail from "@server/emails/templates/WelcomeEmail"; +import { Team, User } from "@server/models"; +import type { Event, UserEvent } from "@server/types"; +import BaseProcessor from "./BaseProcessor"; + +export default class UserCreatedProcessor extends BaseProcessor { + static applicableEvents: Event["name"][] = [ + "users.create", + "users.invite_accepted", + ]; + + async perform(event: UserEvent) { + const [user, team] = await Promise.all([ + User.findByPk(event.userId, { rejectOnEmpty: true }), + Team.findByPk(event.teamId, { rejectOnEmpty: true }), + ]); + + // Invited users receive an InviteEmail at invite time, and a WelcomeEmail + // when they accept the invite and sign in for the first time. + if (event.name === "users.create" && user.isInvited) { + return; + } + + await new WelcomeEmail({ + to: user.email, + language: user.language, + role: user.role, + teamUrl: team.url, + }).schedule(); + } +} diff --git a/server/types.ts b/server/types.ts index 0306cd0d9d..f5d6f77fae 100644 --- a/server/types.ts +++ b/server/types.ts @@ -162,7 +162,8 @@ export type UserEvent = BaseEvent & | "users.update" | "users.suspend" | "users.activate" - | "users.delete"; + | "users.delete" + | "users.invite_accepted"; userId: string; } | {