Merge pull request #3 from GithubOrgProjectPiza/feature-mailserver

Feature mailserver
This commit is contained in:
Stefan
2022-04-02 19:18:24 +02:00
committed by GitHub
17 changed files with 536 additions and 3757 deletions
+3
View File
@@ -102,3 +102,6 @@ dist
# TernJS port file # TernJS port file
.tern-port .tern-port
# Compiled mjml templates
.hbs
+25
View File
@@ -0,0 +1,25 @@
version: "3.4"
services:
mail:
image: boky/postfix
restart: always
environment:
- ALLOWED_SENDER_DOMAINS=mail.${DOMAIN}
ports:
- "587:587"
container_name: "postfix"
volumes:
- ./host/keys/:/etc/opendkim/keys
postgres:
image: postgres
restart: always
environment:
- POSTGRES_USER=server
- POSTGRES_PASSWORD=${DATABASE_PASSWORD}
ports:
- "5432:5432"
redis:
image: redis
restart: always
ports:
- "6379:6379"
-3745
View File
File diff suppressed because it is too large Load Diff
+16 -2
View File
@@ -20,11 +20,25 @@
"homepage": "https://github.com/GithubOrgProjectPiza/server#readme", "homepage": "https://github.com/GithubOrgProjectPiza/server#readme",
"dependencies": { "dependencies": {
"@prisma/client": "^3.10.0", "@prisma/client": "^3.10.0",
"@types/argon2": "^0.15.0",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"axios": "^0.26.1", "@types/handlebars": "^4.1.0",
"@types/jsonwebtoken": "^8.5.8",
"@types/mjml": "^4.7.0",
"@types/nodemailer": "^6.4.4",
"@types/redis": "^4.0.11",
"argon2": "^0.28.5",
"dotenv": "^16.0.0",
"express": "^4.17.3", "express": "^4.17.3",
"handlebars": "^4.7.7",
"jsonwebtoken": "^8.5.1",
"mjml": "^4.12.0",
"nanoid": "^3.3.2",
"nodemailer": "^6.7.2",
"prisma": "^3.10.0",
"redis": "^4.0.4",
"axios": "^0.26.1",
"node-fetch": "^3.2.3", "node-fetch": "^3.2.3",
"prisma": "^3.11.1",
"ts-node": "^10.5.0", "ts-node": "^10.5.0",
"typescript": "^4.5.5" "typescript": "^4.5.5"
}, },
+6
View File
@@ -16,6 +16,7 @@ model User {
passwordHash String passwordHash String
passwordSalt String passwordSalt String
role Role @default(USER) role Role @default(USER)
status Status @default(UNVERIFIED)
organization Organization @relation(fields: [organizationId], references: [id]) organization Organization @relation(fields: [organizationId], references: [id])
organizationId Int organizationId Int
orders Order[] orders Order[]
@@ -63,3 +64,8 @@ enum Role {
USER USER
ADMIN ADMIN
} }
enum Status {
UNVERIFIED
VERIFIED
ENABLED
}
+32
View File
@@ -0,0 +1,32 @@
<mjml>
<mj-body>
<mj-section>
<mj-column>
<mj-text font-size="20px" color="#6FB98F" font-family="helvetica" align="center">
πzza verification Email
</mj-text>
<mj-divider border-color="#6FB98F"></mj-divider>
<mj-text font-size="20px" color="#333" font-family="helvetica" align="center"
>Vielen Dank für deine Registrierung!<br />
Bitte klicke nun auf diesen Button,<br />
um den Konto zu bestätigen.</mj-text
>
<mj-button
background-color="#6FB98F"
font-family="helvetica"
font-size="20px"
border-radius="10px"
href="{{link}}"
>Hier Clicken</mj-button
>
<mj-text font-size="20px" color="#333" font-family="helvetica" align="center"
>Mit Freundlichen grüßen dein πzza Team</mj-text
>
</mj-column>
</mj-section>
</mj-body>
</mjml>
+32
View File
@@ -0,0 +1,32 @@
#! /bin/bash
#load .env file
[[ -f .env ]] || { echo "`tput setaf 1`✗`tput sgr0` Could not find .env file are you in the correct directory (you should be in the server directory)"; exit 1;}
set -o allexport
[[ -f .env ]] && source .env
set +o allexport
#static variables
NC='tput sgr0' #No Color
GREEN='tput setaf 2'
RED='tput setaf 1'
#check dependencies
command -v opendkim-genkey >/dev/null 2>&1 || { echo >&2 "`$RED`✗`$NC` Opendkim not installed install with your favorite package manager (opendkim-tools | opendkim-utils)"; exit 1;}
#generate dkim keys
mkdir -p ./host/keys
echo "`$GREEN`✓`$NC` Created folder"
cd ./host/keys
opendkim-genkey -b 2048 -h rsa-sha256 -r -v --subdomains -s mail -d mail.$DOMAIN
sed -i 's/h=rsa-sha256/sha256/' mail.txt
mv mail.private mail.$DOMAIN.private
mv mail.txt mail.$DOMAIN.txt
echo "`$GREEN`✓`$NC` Generated dkim keys"
exit 0
+142
View File
@@ -0,0 +1,142 @@
import express from "express";
import { Request, Response } from "express";
import prisma from "../lib/prisma.lib";
import { Role, Status } from "@prisma/client";
import { createDefaultError, createDefaultSucces } from "../lib/standardResponse";
import argon2 from "argon2";
import jwt from "jsonwebtoken";
import { emailClient } from "../lib/redis.lib";
import { nanoid } from "nanoid";
import { verificationMail } from "../lib/mail";
const JWT_SECRET = process.env.jwtsecret || "secret";
interface registerBody {
name: string;
password: string;
role: string;
organization?: number;
}
export interface authJwt {
id: number;
role: string;
}
const ROLES: readonly string[] = [Role.ADMIN, Role.USER];
export const register = async (req: Request, res: Response) => {
const { name, password, role, organization } = req.body || {};
if (
!(
typeof name === "string" &&
typeof password === "string" &&
ROLES.includes(role) &&
typeof organization === "number"
)
) {
return res.status(400).json(
createDefaultError("Request body does not match required parameters", {
name: "string",
password: "string",
organization: "int",
})
);
}
const hash = await argon2.hash("salty" + password + "salty");
if (!(await prisma.organization.findUnique({ where: { id: organization } }))) {
return res.status(404).json(createDefaultError(`Could not find organization with id ${organization}`, {}));
}
if (role ? ("USER" as Role) === Role.ADMIN : Role) {
if (req.auth?.role !== Role.ADMIN) {
return res.status(401).json(createDefaultError(`Unauthorized`, {}));
}
}
const user = await prisma.user.create({
data: {
email: name,
passwordSalt: "salty",
passwordHash: hash,
role: role as Role,
status: role === Role.ADMIN ? Status.UNVERIFIED : Status.ENABLED,
organization: { connect: { id: organization } || undefined },
},
select: {
id: true,
email: true,
role: true,
},
});
const usid = nanoid();
(await emailClient).set(usid, user.id);
verificationMail(name, usid);
res.status(201).json(createDefaultSucces("User created succesfully", user));
};
export const authenthicate = async (req: Request, res: Response) => {
const { name, password } = req.body || {};
if (!(typeof name === "string" && typeof password === "string")) {
return res.status(400).json(
createDefaultError("Request body does not match required parameters", {
name: "string",
password: "string",
})
);
}
const hash = await argon2.hash("salty" + password + "salty");
const user = await prisma.user.findUnique({ where: { email: name } });
if (!user) {
return res.status(403).json(createDefaultError("Username or Password wrong", {}));
}
if (user.passwordHash !== hash) {
return res.status(403).json(createDefaultError("Username or Password wrong", {}));
}
const userjwt: authJwt = {
id: user.id,
role: user.role,
};
req.headers.authorization = jwt.sign(userjwt, JWT_SECRET);
};
export const verifyEmail = async (req: Request, res: Response) => {
const { code } = req.body || {};
if (!(typeof code === "string")) {
return res.status(400).json(
createDefaultError("Request body does not match required parameters", {
code: "string",
})
);
}
const acc: number | undefined = emailClient.get(code);
if (!acc) {
return res.status(404).json(createDefaultError("The code is invalid", {}));
}
const user = prisma.user.update({
where: {
id: acc,
},
data: {
status: Status.VERIFIED,
},
});
};
+7
View File
@@ -0,0 +1,7 @@
import { authJwt } from "./controller/auth.controller";
declare module "express-serve-static-core" {
interface Request {
auth?: authJwt & { isAuthenthicated: boolean };
}
}
+29 -10
View File
@@ -1,23 +1,42 @@
import express from "express"; import express, { NextFunction, Request, Response } from "express";
const app = express(); import dotenv from "dotenv";
import adminRoutes from "./routes/auth.route";
const testRoutes = require("./routes/testrouter.route"); const testRoutes = require("./routes/testrouter.route");
const ordersRoutes = require("./routes/orders.route"); const ordersRoutes = require("./routes/orders.route");
const organizationsRoutes = require("./routes/organisations.route"); const organizationsRoutes = require("./routes/organisations.route");
const pizzasRoutes = require("./routes/pizzas.route"); const pizzasRoutes = require("./routes/pizzas.route");
const restaurantsRoutes = require("./routes/restaurants.route"); const restaurantsRoutes = require("./routes/restaurants.route");
dotenv.config();
const app = express();
// Bodyparser and urlencoded to parse post request bodies
app.use(express.urlencoded({ extended: true }));
app.use(express.json()); app.use(express.json());
app.use("/tests", testRoutes);
app.use("/order", ordersRoutes); app.use((err: any, req: Request, res: Response, next: NextFunction) => {
app.use("/organization", organizationsRoutes); if (err) {
app.use("/pizza", pizzasRoutes); res.status(400).send({
app.use("/restaurant", restaurantsRoutes); type: "error",
payload: "The body of your request did not contain valid data",
});
} else {
next();
}
});
async function main() { async function main() {
app.get("/", (req, res) => {
res.send("Hello World!"); app.use("/api/auth", adminRoutes);
}); app.use(express.json());
app.use("/tests", testRoutes);
app.use("/order", ordersRoutes);
app.use("/organization", organizationsRoutes);
app.use("/pizza", pizzasRoutes);
app.use("/restaurant", restaurantsRoutes);
app.listen(3000, () => { app.listen(3000, () => {
console.log("Server is running on port 3000"); console.log("Server is running on port 3000");
+73
View File
@@ -0,0 +1,73 @@
import Handlebars, { template } from "handlebars";
import nodemailer from "nodemailer";
import SMTPTransport from "nodemailer/lib/smtp-transport";
import mjml from "./mjml";
export let mailAccount = {
user: process.env.MAILUSER + "@mail." + process.env.DOMAIN,
pass: process.env.MAILPASSWORD || "password",
};
let transporter =
process.env.DEV == "true" || process.env.DOMAIN == undefined
? (async () => {
mailAccount = await nodemailer.createTestAccount();
//mailAccount = {
// user: "",
// pass: "",
//};
if (process.env.NODE_ENV != "test") {
console.log(mailAccount);
}
return nodemailer.createTransport({
host: "smtp.ethereal.email",
port: 587,
secure: false, // true for 465, false for other ports
auth: {
user: mailAccount.user, // generated ethereal user
pass: mailAccount.pass, // generated ethereal password
},
});
})()
: nodemailer.createTransport(
new SMTPTransport({
host: "localhost",
port: 587,
secure: false,
auth: {
user: mailAccount.user,
pass: mailAccount.pass,
},
tls: {
rejectUnauthorized: false,
secureProtocol: "TLSv1_method",
},
})
);
const sendMail = async (from: string, to: string, subject: string, text?: string, html?: string) => {
return await (
await transporter
).sendMail({
from: from,
to: to,
subject: subject,
text: text,
html: html,
});
};
export const verificationMail = async (to: string, id: string) => {
const raw = mjml.getTemplate("verify");
//TODO: Replace other handlebars with final values
const message = Handlebars.compile(raw);
const data = {};
const compiled = message(data);
sendMail(mailAccount.user, to, "Verify Email", undefined, compiled);
};
export default { sendMail };
+33
View File
@@ -0,0 +1,33 @@
import fs from "fs";
import path from "path";
import mjml from "mjml";
const template_folder = __dirname + "/../../resources/email/templates/";
const compileTemplates = () => {
console.log("Compiling templates: ");
fs.readdir(template_folder, (err, files) => {
if (err) console.log(err);
files.forEach((file) => {
if (path.extname(file) !== ".mjml") return;
let content = fs.readFileSync(template_folder + file);
let mjmlres = mjml(content.toString());
let hbs = template_folder + file.replace(".mjml", ".hbs");
fs.writeFileSync(hbs, mjmlres.html);
});
});
};
compileTemplates(); // This will compile all templates when this file is first included
const getTemplate = (name: string): string => {
let file = "";
file = fs.readFileSync(path.join(template_folder, name + ".hbs")).toString();
return file;
};
export default { getTemplate };
+1
View File
@@ -6,3 +6,4 @@ let prisma: PrismaClient;
prisma = new PrismaClient(); prisma = new PrismaClient();
export default prisma; export default prisma;
+19
View File
@@ -0,0 +1,19 @@
import redis, { createClient } from "redis";
import { promisify } from "util";
function createAsyncClient(client: redis.RedisClientType) {
return {
get: promisify(client.get).bind(client),
set: promisify(client.set).bind(client),
};
}
/*----E-mail verify----*/
const email: redis.RedisClientType = createClient({
url: "redis://localhost:6379",
});
email.connect();
export const emailClient = createAsyncClient(email);
+16
View File
@@ -0,0 +1,16 @@
export const createDefaultError = (message: string, context: any) => ({
"type": "error",
"payload": {
"message": message,
"context": context,
}
})
export const createDefaultSucces = (message: string, context: any) => ({
"type": "success",
"payload": {
"message": message,
"context": context,
}
})
+93
View File
@@ -0,0 +1,93 @@
/// <reference path="../custom.d.ts" />
import { NextFunction, Request, Response } from "express";
import { createDefaultError } from "../lib/standardResponse";
import jwt, { JwtPayload } from "jsonwebtoken";
import { authJwt } from "../controller/auth.controller";
import prisma from "../lib/prisma.lib";
import { emailClient } from "../lib/redis.lib";
const verifyAuthorizationFormat = (authorization: string) => /^Bearer .+$/.test(authorization);
const getBearerToken = (authorization: string) => authorization.slice(7);
const JWT_SECRET = process.env.jwtsecret || "secret";
export const getAuthenthication = async (req: Request, res: Response, next: NextFunction) => {
const { authorization } = req.headers;
if (!authorization) {
next();
return;
}
if (!verifyAuthorizationFormat(authorization)) {
return res.status(400).json(createDefaultError("Malformed Bearer Token", { format: "Bearer <token>" }));
}
let payload_: string | JwtPayload;
try {
payload_ = jwt.verify(getBearerToken(authorization), JWT_SECRET);
} catch (e) {
return res.status(403).json(createDefaultError("The Token could not be verified, it might be expiered", null));
}
const payload = payload_ as authJwt;
const { id } = payload;
const user = await prisma.user.findUnique({
where: { id },
select: {
role: true,
},
});
req.auth = {
isAuthenthicated: true,
id,
role: user?.role || "user",
};
next();
};
export const requireAuthentication = async (req: Request, res: Response, next: NextFunction) => {
const { authorization } = req.headers;
if (!authorization) {
return res.status(403).json(createDefaultError("The request did not contain a authorization header", null));
}
if (!verifyAuthorizationFormat(authorization)) {
return res.status(400).json(createDefaultError("Malformed Bearer Token", { format: "Bearer <token>" }));
}
let payload_: string | JwtPayload;
try {
payload_ = jwt.verify(getBearerToken(authorization), JWT_SECRET);
} catch (e) {
return res.status(403).json(createDefaultError("The Token could not be verified, it might be expiered", null));
}
const payload = payload_ as authJwt;
const { id } = payload;
const user = await prisma.user.findUnique({
where: { id },
select: {
role: true,
},
});
req.auth = {
isAuthenthicated: true,
id,
role: user?.role || "USER",
};
next();
};
+9
View File
@@ -0,0 +1,9 @@
import { Router } from "express";
import { register } from "../controller/auth.controller";
import { getAuthenthication } from "../middleware/auth.middle";
const router = Router();
router.post("/register", getAuthenthication, register);
export default router;