Skip to content

Commit

Permalink
Account activity emails
Browse files Browse the repository at this point in the history
  • Loading branch information
benjie committed Mar 25, 2020
1 parent 9c7c241 commit 6ce053a
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 8 deletions.
9 changes: 5 additions & 4 deletions @app/db/migrations/committed/000001.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
--! Previous: -
--! Hash: sha1:76d7519b0aa7d6eeb883c47cb33585c91ea776f7
--! Hash: sha1:af3861c450adf4a6d08a62fd910fa2b0e8f394fc

drop schema if exists app_public cascade;

Expand Down Expand Up @@ -46,10 +46,11 @@ comment on function app_private.tg__add_job() is
create function app_private.tg__add_audit_job() returns trigger as $$
declare
v_user_id uuid;
v_type text = TG_ARGV[0];
v_user_id_attribute text = TG_ARGV[1];
v_extra_attribute1 text = TG_ARGV[2];
v_extra_attribute2 text = TG_ARGV[2];
v_extra_attribute3 text = TG_ARGV[2];
v_extra_attribute2 text = TG_ARGV[3];
v_extra_attribute3 text = TG_ARGV[4];
v_extra1 text;
v_extra2 text;
v_extra3 text;
Expand Down Expand Up @@ -82,7 +83,7 @@ begin
perform graphile_worker.add_job(
'user__audit',
json_build_object(
'type', tg_argv[0],
'type', v_type,
'user_id', v_user_id,
'extra1', v_extra1,
'extra2', v_extra2,
Expand Down
2 changes: 1 addition & 1 deletion @app/worker/src/tasks/send_email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const isDev = process.env.NODE_ENV !== "production";
export interface SendEmailPayload {
options: {
from?: string;
to: string;
to: string | string[];
subject: string;
};
template: string;
Expand Down
151 changes: 151 additions & 0 deletions @app/worker/src/tasks/user__audit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { projectName } from "@app/config";
import { Task } from "graphile-worker";

import { SendEmailPayload } from "./send_email";

/* For tracking account actions */

type AccountAction =
| "linked_account" //
| "unlinked_account" //
| "changed_password" //
| "reset_password" //
| "added_email" //
| "removed_email"; //

type UserAuditPayload =
| {
type: "added_email";
user_id: string;
current_user_id: string;

/** id */
extra1: string;

/** email */
extra2: string;
}
| {
type: "removed_email";
user_id: string;
current_user_id: string;

/** id */
extra1: string;

/** email */
extra2: string;
}
| {
type: "linked_account";
user_id: string;
current_user_id: string;

/** service */
extra1: string;

/** identifier */
extra2: string;
}
| {
type: "unlinked_account";
user_id: string;
current_user_id: string;

/** service */
extra1: string;

/** identifier */
extra2: string;
}
| {
type: "reset_password";
user_id: string;
current_user_id: string;
}
| {
type: "change_password";
user_id: string;
current_user_id: string;
};

const task: Task = async (rawPayload, { addJob, withPgClient }) => {
const payload: UserAuditPayload = rawPayload as any;
let subject: string;
let actionDescription: string;
switch (payload.type) {
case "added_email": {
subject = `You added an email to your account`;
actionDescription = `You added the email '${payload.extra2}' to your account.`;
break;
}
case "removed_email": {
subject = `You removed an email from your account`;
actionDescription = `You removed the email '${payload.extra2}' from your account.`;
break;
}
case "linked_account": {
subject = `You linked a third-party OAuth provider to your account`;
actionDescription = `You linked a third-party OAuth provider ('${payload.extra1}') to your account.`;
break;
}
case "unlinked_account": {
subject = `You removed a link between your account and a third-party OAuth provider`;
actionDescription = `You removed a link between your account and a third-party OAuth provider ('${payload.extra1}').`;
break;
}
case "reset_password": {
subject = `You reset your password`;
actionDescription = `You reset your password.`;
break;
}
case "change_password": {
subject = `You changed your password`;
actionDescription = `You changed your password.`;
break;
}
default: {
// Ensure we've handled all cases above
const neverPayload: never = payload;
console.error(
`Audit action '${(neverPayload as any).type}' not understood; ignoring.`
);
return;
}
}

const { rows: userEmails } = await withPgClient((client) =>
client.query<{
id: string;
user_id: string;
email: string;
is_verified: boolean;
is_primary: boolean;
created_at: Date;
updated_at: Date;
}>(
"select * from app_public.user_emails where user_id = $1 and is_verified is true",
[payload.user_id]
)
);

if (userEmails.length === 0) {
throw new Error("Could not find emails for this user");
}

const emails = userEmails.map((e) => e.email);

const sendEmailPayload: SendEmailPayload = {
options: {
to: emails,
subject: `[${projectName}] ${subject}`,
},
template: "account_activity.mjml",
variables: {
actionDescription,
},
};
await addJob("send_email", sendEmailPayload);
};

export default task;
45 changes: 45 additions & 0 deletions @app/worker/templates/account_activity.mjml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<mjml>
<mj-head>
<mj-attributes>
<mj-class name="background" background-color="#f5faff" font-size="13px" />
<mj-class name="bold-text" align="center" font-size="20px" font-family="Helvetica" font-weight="bold" />
<mj-class name="text" align="center" font-size="16px" font-family="Helvetica" />
<mj-class name="small-text" align="center" font-size="13px" font-family="Helvetica" />
<mj-class name="blue-section" background-color="#4680b4" />
<mj-class name="white-section" background-color="#fff" />
</mj-attributes>
</mj-head>

<mj-body mj-class="background">
<mj-section padding-bottom="20px"></mj-section>

<mj-section mj-class="blue-section">
<mj-column width="100%">
<mj-text mj-class="bold-text" color="#FFF" padding-left="25px" padding-right="25px" padding-bottom="28px" padding-top="28px">
[[actionDescription]]
</mj-text>
<mj-text mj-class="text" color="#FFF" padding-left="25px" padding-right="25px" padding-bottom="28px" padding-top="28px">
This email is purely for your information and security, if you performed the action above then no further action is necessary.
</mj-text>
</mj-column>
</mj-section>

<mj-section mj-class="blue-section">
<mj-column width="100%">
<mj-text mj-class="text" color="#FFF" padding-left="25px" padding-right="25px" padding-bottom="20px" padding-top="20px">
Best,<br/>
The [[projectName]] Team
</mj-text>
</mj-column>
</mj-section>

<mj-section>
<mj-column width="100%">
<mj-text mj-class="small-text" color="#777" padding-left="25px" padding-right="25px" padding-bottom="20px" padding-top="20px">
[[legalText]]
</mj-text>
</mj-column>
</mj-section>

</mj-body>
</mjml>
7 changes: 4 additions & 3 deletions data/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -498,10 +498,11 @@ CREATE FUNCTION app_private.tg__add_audit_job() RETURNS trigger
AS $_$
declare
v_user_id uuid;
v_type text = TG_ARGV[0];
v_user_id_attribute text = TG_ARGV[1];
v_extra_attribute1 text = TG_ARGV[2];
v_extra_attribute2 text = TG_ARGV[2];
v_extra_attribute3 text = TG_ARGV[2];
v_extra_attribute2 text = TG_ARGV[3];
v_extra_attribute3 text = TG_ARGV[4];
v_extra1 text;
v_extra2 text;
v_extra3 text;
Expand Down Expand Up @@ -534,7 +535,7 @@ begin
perform graphile_worker.add_job(
'user__audit',
json_build_object(
'type', tg_argv[0],
'type', v_type,
'user_id', v_user_id,
'extra1', v_extra1,
'extra2', v_extra2,
Expand Down

0 comments on commit 6ce053a

Please sign in to comment.