From 86f16451999bba7964da156005c916940d749b30 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Tue, 12 Apr 2022 20:12:33 -0700 Subject: [PATCH] feat: Automatic invite reminder email (#3354) * feat: Add user flags concept, for tracking bits on a user * feat: Example flag usage for user invite resend abuse * wip * test * fix: Set correct flag --- server/commands/userInviter.ts | 1 + .../emails/templates/InviteReminderEmail.tsx | 70 +++++++++++++++++++ .../migrations/20220409222213-user-flags.js | 2 +- .../20220409225935-user-invited-by.js | 17 +++++ server/models/User.ts | 36 +++++++++- server/queues/tasks/BaseTask.ts | 2 +- .../queues/tasks/InviteReminderTask.test.ts | 37 ++++++++++ server/queues/tasks/InviteReminderTask.ts | 67 ++++++++++++++++++ server/routes/api/users.ts | 4 +- server/routes/api/utils.ts | 3 + server/test/factories.ts | 4 ++ 11 files changed, 237 insertions(+), 6 deletions(-) create mode 100644 server/emails/templates/InviteReminderEmail.tsx create mode 100644 server/migrations/20220409225935-user-invited-by.js create mode 100644 server/queues/tasks/InviteReminderTask.test.ts create mode 100644 server/queues/tasks/InviteReminderTask.ts diff --git a/server/commands/userInviter.ts b/server/commands/userInviter.ts index f81810bbb..017577977 100644 --- a/server/commands/userInviter.ts +++ b/server/commands/userInviter.ts @@ -62,6 +62,7 @@ export default async function userInviter({ service: null, isAdmin: invite.role === "admin", isViewer: invite.role === "viewer", + invitedById: user.id, flags: { [UserFlag.InviteSent]: 1, }, diff --git a/server/emails/templates/InviteReminderEmail.tsx b/server/emails/templates/InviteReminderEmail.tsx new file mode 100644 index 000000000..6dc6a6f61 --- /dev/null +++ b/server/emails/templates/InviteReminderEmail.tsx @@ -0,0 +1,70 @@ +import * as React from "react"; +import BaseEmail from "./BaseEmail"; +import Body from "./components/Body"; +import Button from "./components/Button"; +import EmailTemplate from "./components/EmailLayout"; +import EmptySpace from "./components/EmptySpace"; +import Footer from "./components/Footer"; +import Header from "./components/Header"; +import Heading from "./components/Heading"; + +type Props = { + to: string; + name: string; + actorName: string; + actorEmail: string; + teamName: string; + teamUrl: string; +}; + +/** + * Email sent to an external user when an admin sends them an invite and they + * haven't signed in after a few days. + */ +export default class InviteReminderEmail extends BaseEmail { + protected subject({ actorName, teamName }: Props) { + return `Reminder: ${actorName} invited you to join ${teamName}’s knowledge base`; + } + + protected preview() { + return "Outline is a place for your team to build and share knowledge."; + } + + protected renderAsText({ + teamName, + actorName, + actorEmail, + teamUrl, + }: Props): string { + return ` +This is just a quick reminder that ${actorName} (${actorEmail}) invited you to join them in the ${teamName} team on Outline, a place for your team to build and share knowledge. +We only send a reminder once. + +If you haven't signed up yet, you can do so here: ${teamUrl} +`; + } + + protected render({ teamName, actorName, actorEmail, teamUrl }: Props) { + return ( + +
+ + + Join {teamName} on Outline +

+ This is just a quick reminder that {actorName} ({actorEmail}) + invited you to join them in the {teamName} team on Outline, a place + for your team to build and share knowledge. +

+

If you haven't signed up yet, you can do so here:

+ +

+ +

+ + +