Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add AWS SES helper + simple template. #5

Merged
merged 3 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@ SERVER_HOST=0.0.0.0
SERVER_PORT=4000
JWT_SECRET=OR5t0v7yk3z7qw77

AWS_SES_REGION=
AWS_SES_ID=
AWS_SES_SECRET=
AWS_SES_SOURCE=

APP_URL=http://0.0.0.0:3000
12 changes: 11 additions & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,27 @@ jobs:
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1

- name: Set up environment variables
- name: Environment
run: |
touch .env
echo "NODE_ENV=${{ vars.NODE_ENV }}" >> .env
echo "SERVER_HOST=${{ vars.HOST }}" >> .env
echo "SERVER_PORT=${{ vars.PORT }}" >> .env
echo "JWT_SECRET=${{ vars.JWT_SECRET }}" >> .env
echo "MONGODB_URI=${{ vars.MONGODB_URI }}" >> .env
echo "AWS_SES_REGION=${{ vars.AWS_REGION }}" >> .env
echo "AWS_SES_ID=${{ secrets.AWS_SES_ID }}" >> .env
echo "AWS_SES_SECRET=${{ secrets.AWS_SES_SECRET }}" >> .env
echo "AWS_SES_SOURCE=${{ vars.APAWS_SES_SOURCEP_URL }}" >> .env
echo "APP_URL=${{ vars.APP_URL }}" >> .env
echo "" >> .env

- name: Format and Lint
run: yarn format && yarn lint

- name: Test
run: yarn test

- name: Docker Build
id: build-image
env:
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"start": "node dist/index.js"
},
"dependencies": {
"@aws-sdk/client-ses": "^3.632.0",
"@typegoose/typegoose": "^12.3.1",
"axios": "^1.7.4",
"bcrypt": "^5.1.1",
Expand Down
7 changes: 6 additions & 1 deletion src/controllers/users.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Request, Response } from "express";
import { UserModel } from "@/models/User";
import { sendVerificationEmail } from "@/helpers/aws/ses";

export const createUser = async (req: Request, res: Response) => {
try {
Expand All @@ -9,7 +10,11 @@ export const createUser = async (req: Request, res: Response) => {
}

const newUser = await UserModel.createUser(email, password, name);
if (newUser) return res.status(200).send(newUser);
if (newUser) {
await sendVerificationEmail(email, newUser.verifyToken!);
newUser.verifyToken = undefined;
return res.status(200).send(newUser);
}
} catch (e) {
console.log(`[ERROR][createUser] ${JSON.stringify(e)}`);
}
Expand Down
10 changes: 10 additions & 0 deletions src/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ const APP_URL = process.env.APP_URL || `http://${SERVER_HOST}:3000`;

const JWT_SECRET = process.env.JWT_SECRET || "OR5t0v7yk3z7qw77";

// AWS
const AWS_SES_REGION = process.env.AWS_SES_REGION || "us-east-1";
const AWS_SES_ID = process.env.AWS_SES_ID || "123123123";
const AWS_SES_SECRET = process.env.AWS_SES_SECRET || "123123123";
const AWS_SES_SOURCE = process.env.AWS_SES_SOURCE || "email@polkadot.education";

// DEBUG/TEST
const DEBUG = parseInt(process.env.DEBUG || "0");

Expand All @@ -23,5 +29,9 @@ export const env = {
SERVER_PORT,
APP_URL,
JWT_SECRET,
AWS_SES_REGION,
AWS_SES_SECRET,
AWS_SES_ID,
AWS_SES_SOURCE,
DEBUG,
};
51 changes: 51 additions & 0 deletions src/helpers/aws/ses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { env } from "@/environment";

import { SESClient, SendEmailCommand } from "@aws-sdk/client-ses";

import verificationLink from "./templates/verificationLink";

const sesClient = new SESClient({
credentials: {
secretAccessKey: String(env.AWS_SES_SECRET),
accessKeyId: String(env.AWS_SES_ID),
},
apiVersion: "2010-12-01",
region: String(env.AWS_SES_REGION),
});

const createSendEmailCommand = (
toAddresses: string[],
subject: string,
html: string,
fromAddress: string = env.AWS_SES_SOURCE,
) => {
const from = `Polkadot Education <${fromAddress}>`;
return new SendEmailCommand({
Destination: {
ToAddresses: toAddresses,
},
Message: {
Body: {
Html: {
Charset: "UTF-8",
Data: html,
},
},
Subject: {
Charset: "UTF-8",
Data: subject,
},
},
Source: from,
});
};

export const sendVerificationEmail = async (to: string, token: string) => {
const html = verificationLink.replace("{{VERIFICATION_LINK}}", `${env.APP_URL}/verify/${token}`);
const sendEmailCommand = createSendEmailCommand([to], "Verify your Account", html);
try {
return await sesClient.send(sendEmailCommand);
} catch (e) {
console.log(`[ERROR][sendVerificationEmail] ${JSON.stringify(e)}`);
}
};
13 changes: 13 additions & 0 deletions src/helpers/aws/templates/verificationLink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export default `<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml">
<body>
<h1>Polkadot Education</h1>
<p>
Verify your account's email
<a href="{{VERIFICATION_LINK}}" target="_blank">
here
</a>
.
</p>
<body>
</html>`;
4 changes: 2 additions & 2 deletions src/middlewares/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { env } from "@/environment";
import { UserModel } from "@/models/User";

const authMiddleware = async (req: Request, res: Response, next: NextFunction) => {
// Uncomment here to test endpoints without authentication (app's jwt)
// if (env.NODE_ENV === "local") return next();
// Comment here to test endpoints with authentication (app's jwt)
if (["local", "test"].includes(env.NODE_ENV)) return next();
const error = {
status: 401,
error: "You are not authorized to perform this action.",
Expand Down
6 changes: 6 additions & 0 deletions src/models/User.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DocumentType, ReturnModelType, getModelForClass, prop } from "@typegoose/typegoose";
import moment from "moment";
import jwt from "jsonwebtoken";
import { randomBytes } from "crypto";
import * as bcrypt from "bcrypt";

import { env } from "@/environment";
Expand All @@ -17,6 +18,9 @@ class User extends BaseModel {
@prop({ required: true, type: String })
public name: string;

@prop({ required: false, type: String })
public verifyToken?: string;

@prop({ required: false, type: Date })
public lastActivity: Date;

Expand Down Expand Up @@ -45,12 +49,14 @@ class User extends BaseModel {
email: email.toLowerCase(),
password: await this.hashPassword(password),
name,
verifyToken: randomBytes(16).toString("hex"),
lastActivity: new Date(),
});
return {
userId: user._id,
email: user.email,
name: user.name,
verifyToken: user.verifyToken,
lastActivity: user.lastActivity,
};
} catch (e) {
Expand Down
4 changes: 2 additions & 2 deletions src/routes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Express } from "express";
import { Express, Request, Response } from "express";

// Controllers
import { createUser, deleteUser, getUser, loginUser } from "@/controllers/users";
Expand All @@ -9,7 +9,7 @@ import corsConfig from "./cors.config";

const router = (app: Express) => {
// Health check
app.get("/status", (_req: Request, resp: Response) => resp.status(200).json({ type: "success" }));
app.get("/status", (_req: Request, res: Response) => res.status(200).json({ type: "success" }));

// Users
app.post("/user", createUser);
Expand Down
1 change: 1 addition & 0 deletions src/types/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export type UserInfo = {
email: string;
name: string;
lastActivity: Date;
verifyToken?: string;
createdAt?: Date;
updatedAt?: Date;
};
Loading
Loading