Compare commits

...

2 Commits

Author SHA1 Message Date
Tom Moor cb219a8e5a tsc 2025-02-09 23:29:44 -05:00
Tom Moor a41a007a82 chore: Move welcome email to processor 2025-02-09 22:46:12 -05:00
9 changed files with 110 additions and 51 deletions
-23
View File
@@ -1,9 +1,6 @@
import Router from "koa-router";
import { NotificationEventType } from "@shared/types";
import { parseDomain } from "@shared/utils/domains";
import InviteAcceptedEmail from "@server/emails/templates/InviteAcceptedEmail";
import SigninEmail from "@server/emails/templates/SigninEmail";
import WelcomeEmail from "@server/emails/templates/WelcomeEmail";
import env from "@server/env";
import { AuthorizationError } from "@server/errors";
import { rateLimiter } from "@server/middlewares/rateLimiter";
@@ -117,26 +114,6 @@ router.get(
return ctx.redirect("/?notice=user-suspended");
}
if (user.isInvited) {
await new WelcomeEmail({
to: user.email,
role: user.role,
teamUrl: user.team.url,
}).schedule();
const inviter = await user.$get("invitedBy");
if (
inviter?.subscribedToEventType(NotificationEventType.InviteAccepted)
) {
await new InviteAcceptedEmail({
to: inviter.email,
inviterId: inviter.id,
invitedName: user.name,
teamUrl: user.team.url,
}).schedule();
}
}
// set cookies on response and redirect to team subdomain
await signIn(ctx, "email", {
user,
@@ -25,6 +25,11 @@ describe("WebhookProcessor", () => {
teamId: subscription.teamId,
actorId: signedInUser.id,
ip,
data: {
inviteAccepted: false,
name: signedInUser.name,
service: "google",
},
};
await processor.perform(event);
@@ -49,6 +54,11 @@ describe("WebhookProcessor", () => {
teamId: subscription.teamId,
actorId: signedInUser.id,
ip,
data: {
inviteAccepted: false,
name: signedInUser.name,
service: "google",
},
};
await processor.perform(event);
@@ -75,6 +85,11 @@ describe("WebhookProcessor", () => {
teamId: subscription.teamId,
actorId: signedInUser.id,
ip,
data: {
inviteAccepted: false,
name: signedInUser.name,
service: "google",
},
};
await processor.perform(event);
@@ -34,6 +34,11 @@ describe("DeliverWebhookTask", () => {
teamId: subscription.teamId,
actorId: signedInUser.id,
ip,
data: {
inviteAccepted: false,
name: signedInUser.name,
service: "google",
},
};
await processor.perform({
subscriptionId: subscription.id,
@@ -79,6 +84,11 @@ describe("DeliverWebhookTask", () => {
teamId: subscription.teamId,
actorId: signedInUser.id,
ip,
data: {
inviteAccepted: false,
name: signedInUser.name,
service: "google",
},
};
await processor.perform({
subscriptionId: subscription.id,
@@ -156,6 +166,11 @@ describe("DeliverWebhookTask", () => {
teamId: subscription.teamId,
actorId: signedInUser.id,
ip,
data: {
inviteAccepted: false,
name: signedInUser.name,
service: "google",
},
};
await task.perform({
@@ -204,6 +219,11 @@ describe("DeliverWebhookTask", () => {
teamId: subscription.teamId,
actorId: signedInUser.id,
ip,
data: {
inviteAccepted: false,
name: signedInUser.name,
service: "google",
},
};
await task.perform({
@@ -1,6 +1,5 @@
import { faker } from "@faker-js/faker";
import { v4 as uuidv4 } from "uuid";
import WelcomeEmail from "@server/emails/templates/WelcomeEmail";
import { TeamDomain } from "@server/models";
import Collection from "@server/models/Collection";
import UserAuthentication from "@server/models/UserAuthentication";
@@ -13,7 +12,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().toLowerCase();
const { user, team, isNewTeam, isNewUser } = await accountProvisioner({
ip,
@@ -46,19 +44,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");
const existingTeam = await buildTeam();
const providers = await existingTeam.$get("authenticationProviders");
const authenticationProvider = providers[0];
@@ -97,9 +91,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 () => {
@@ -236,7 +227,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(
@@ -279,7 +269,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: {
@@ -287,12 +276,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"
@@ -328,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: {
@@ -336,8 +321,6 @@ describe("accountProvisioner", () => {
},
});
expect(collectionCount).toEqual(1);
spy.mockRestore();
});
});
-10
View File
@@ -2,7 +2,6 @@ import path from "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,
@@ -160,15 +159,6 @@ async function accountProvisioner({
});
const { isNewUser, user } = result;
// TODO: Move to processor
if (isNewUser) {
await new WelcomeEmail({
to: user.email,
role: user.role,
teamUrl: team.url,
}).schedule();
}
if (isNewUser || isNewTeam) {
let provision = isNewTeam;
@@ -0,0 +1,23 @@
import WelcomeEmail from "@server/emails/templates/WelcomeEmail";
import { User } from "@server/models";
import { Event as TEvent, UserEvent } from "@server/types";
import BaseProcessor from "./BaseProcessor";
export default class UserCreatedProcessor extends BaseProcessor {
static applicableEvents: TEvent["name"][] = ["users.create"];
async perform(event: UserEvent) {
const user = await User.scope("withTeam").findByPk(event.userId, {
rejectOnEmpty: true,
});
if (user.isInvited) {
return;
}
await new WelcomeEmail({
to: user.email,
role: user.role,
teamUrl: user.team.url,
}).schedule();
}
}
@@ -0,0 +1,40 @@
import { NotificationEventType } from "@shared/types";
import InviteAcceptedEmail from "@server/emails/templates/InviteAcceptedEmail";
import WelcomeEmail from "@server/emails/templates/WelcomeEmail";
import { User } from "@server/models";
import { Event as TEvent, UserEvent } from "@server/types";
import BaseProcessor from "./BaseProcessor";
export default class UserSigninProcessor extends BaseProcessor {
static applicableEvents: TEvent["name"][] = ["users.signin"];
async perform(event: UserEvent) {
const inviteAccepted =
"data" in event &&
"inviteAccepted" in event.data &&
event.data.inviteAccepted === true;
if (!inviteAccepted) {
return;
}
const user = await User.scope("withTeam").findByPk(event.userId, {
rejectOnEmpty: true,
});
await new WelcomeEmail({
to: user.email,
role: user.role,
teamUrl: user.team.url,
}).schedule();
const inviter = await user.$get("invitedBy");
if (inviter?.subscribedToEventType(NotificationEventType.InviteAccepted)) {
await new InviteAcceptedEmail({
to: inviter.email,
inviterId: inviter.id,
invitedName: user.name,
teamUrl: user.team.url,
}).schedule();
}
}
}
+9 -1
View File
@@ -141,7 +141,6 @@ export type UserEvent = BaseEvent<User> &
(
| {
name:
| "users.signin"
| "users.signout"
| "users.update"
| "users.suspend"
@@ -164,6 +163,15 @@ export type UserEvent = BaseEvent<User> &
name: string;
};
}
| {
name: "users.signin";
userId: string;
data: {
inviteAccepted: boolean;
name: string;
service: string;
};
}
);
export type UserMembershipEvent = BaseEvent<UserMembership> & {
+3
View File
@@ -59,6 +59,8 @@ export async function signIn(
}
}
const inviteAccepted = user.isInvited;
// update the database when the user last signed in
await user.updateSignedIn(ctx.request.ip);
@@ -70,6 +72,7 @@ export async function signIn(
teamId: team.id,
authType: AuthenticationType.APP,
data: {
inviteAccepted,
name: user.name,
service,
},