chore: Move welcome email to processor (#11939)

* chore: Move welcome email to processor

* fix: Restore welcome email on invite acceptance
This commit is contained in:
Tom Moor
2026-04-02 20:16:47 -04:00
committed by GitHub
parent 12c71f267e
commit b2aad71cb4
5 changed files with 41 additions and 32 deletions
@@ -130,6 +130,7 @@ export default class DeliverWebhookTask extends BaseTask<Props> {
case "users.invite":
case "users.promote":
case "users.demote":
case "users.invite_accepted":
await this.handleUserEvent(subscription, event);
return;
case "documents.create":
+1 -22
View File
@@ -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 () => {
+6 -9
View File
@@ -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) {
@@ -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();
}
}
+2 -1
View File
@@ -162,7 +162,8 @@ export type UserEvent = BaseEvent<User> &
| "users.update"
| "users.suspend"
| "users.activate"
| "users.delete";
| "users.delete"
| "users.invite_accepted";
userId: string;
}
| {