diff --git a/package-lock.json b/package-lock.json index fb8ed8f..ec2cb13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@prisma/client": "^3.3.0", + "@types/cors": "^2.8.12", "@types/express-fileupload": "^1.2.1", "@types/handlebars": "^4.1.0", "@types/jsonwebtoken": "^8.5.5", @@ -18,6 +19,7 @@ "@types/nodemailer": "^6.4.4", "@types/redis": "^2.8.32", "argon2": "^0.28.2", + "cors": "^2.8.5", "dotenv": "^10.0.0", "express": "^4.17.1", "express-async-errors": "^3.1.1", @@ -242,6 +244,11 @@ "integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==", "dev": true }, + "node_modules/@types/cors": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", + "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" + }, "node_modules/@types/express": { "version": "4.17.13", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", @@ -989,6 +996,18 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -4134,6 +4153,11 @@ "integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==", "dev": true }, + "@types/cors": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", + "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" + }, "@types/express": { "version": "4.17.13", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", @@ -4757,6 +4781,15 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, "create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7e7a3e7..1d07b9e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -64,6 +64,7 @@ model Team { pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid name String leaderEmail String + verified Boolean @default(false) roles Role[] @relation(name: "participants") participants Participant[] diff --git a/src/Controllers/user_auth.controller.ts b/src/Controllers/user_auth.controller.ts index cd8c94a..e7147ed 100644 --- a/src/Controllers/user_auth.controller.ts +++ b/src/Controllers/user_auth.controller.ts @@ -3,27 +3,79 @@ import prisma from "../lib/prisma"; import { mailClient } from "../lib/redis"; import { nanoid } from "nanoid"; import { verificationMail } from "../lib/mail"; +import { DataType, generateInvalidBodyError } from "./common"; +import { generateTeamleaderJWT } from "../Middleware/auth/teamleaderAuth"; -export const register = async (req: Request, res: Response) => { - //TODO: Implemnt user endpoint and use following code to send verification mail +interface CreateTeamBody { + name: string; + leaderEmail: string; + disciplineId: string; +} - const user = { - //Supposed to come from database - id: "10", - email: "test@test.com", - }; +export const register = async (req: Request<{}, {}, CreateTeamBody>, res: Response) => { + if (req.body.name == null || req.body.disciplineId == null || req.body.leaderEmail == null) { + res.status(400).json( + generateInvalidBodyError({ + name: DataType.STRING, + leaderEmail: DataType.STRING, + disciplineId: DataType.STRING, + }) + ); + return; + } + + const team = await prisma.team.create({ + data: { + leaderEmail: req.body.leaderEmail, + name: req.body.name, + roles: undefined, + discipline: { connect: { pid: req.body.disciplineId } }, + }, + select: { + pid: true, + name: true, + disciplineId: true, + }, + }); const usid = nanoid(); - (await mailClient).set(usid, user.id); + (await mailClient).set(usid, team.pid); - verificationMail(user.email, "eventname", usid); + verificationMail(req.body.leaderEmail, "eventname", usid); - //Send status code + res.status(201).json({ type: "success", payload: { team } }); }; +export const requestToken = async (req: Request, res: Response) => { + const { teamId } = req.body || {}; + + if (!(typeof teamId === "string")) { + return res.status(400).json(generateInvalidBodyError({ teamId: DataType.STRING })); + } + + const team = await prisma.team.findUnique({ + where: { + pid: teamId, + }, + }); + + if (!team) { + return res.status(404).json(); + } + + const usid = nanoid(); + + (await mailClient).set(usid, team.pid); + + verificationMail(team.leaderEmail, "eventname", usid); + + res.status(200).json({ type: "sucess", message: "Email sent!" }); +}; + +//TODO: This should be a get request with the code as a veriable part in the url export const verifyEmail = async (req: Request, res: Response) => { - const { code } = req.body || {}; + const { code } = req.params || {}; if (!(typeof code === "string")) { return res.status(400).json({ @@ -45,12 +97,16 @@ export const verifyEmail = async (req: Request, res: Response) => { }); } - prisma.participant.update({ + const team = await prisma.team.update({ where: { - id: parseInt(acc), + pid: acc, }, data: { verified: true, }, }); + + mailClient.set(code, ""); + + res.status(200).json({ type: "succes", payload: { token: generateTeamleaderJWT(team) } }); //TODO: This needs to set a cookie or smth so that the client also gets this info }; diff --git a/src/Middleware/auth/teamleaderAuth.ts b/src/Middleware/auth/teamleaderAuth.ts index 9e8d358..aece12c 100644 --- a/src/Middleware/auth/teamleaderAuth.ts +++ b/src/Middleware/auth/teamleaderAuth.ts @@ -1,4 +1,4 @@ -import { Participant } from "@prisma/client"; +import { Team } from "@prisma/client"; import e, { NextFunction, Request, Response } from "express"; import jwt, { JsonWebTokenError } from "jsonwebtoken"; import AuthError from "../error/AuthError"; @@ -6,20 +6,18 @@ import { getBearerToken, verifyAuthorizationFormat } from "./auth"; import prisma from "../../lib/prisma"; export interface TeamleaderJWTPayload { - pid: string; team: string; } const JWT_SECRET = process.env.JWT_SECRET; -export function generateTeamleaderJWT(teamleader: Participant & { relevance: "TEAMLEADER"; team: { pid: string } }) { +export function generateTeamleaderJWT(teamleader: Team) { if (!JWT_SECRET) { throw new Error("JWT_SECRET not set"); } const payload: TeamleaderJWTPayload = { - pid: teamleader.pid, - team: teamleader.team.pid, + team: teamleader.pid, }; return jwt.sign(payload, JWT_SECRET, { expiresIn: "4 days" }); @@ -57,7 +55,6 @@ export async function requireTeamleaderAuthentication(req: Request, res: Respons req.teamleader = { isAuthenticated: true, - pid: token_payload.pid, team: token_payload.team, }; diff --git a/src/Routes/user_auth.routes.ts b/src/Routes/user_auth.routes.ts new file mode 100644 index 0000000..568b764 --- /dev/null +++ b/src/Routes/user_auth.routes.ts @@ -0,0 +1,10 @@ +import express from "express"; +import { register, requestToken, verifyEmail } from "../Controllers/user_auth.controller"; + +const router = express.Router(); + +router.post("/", register); +router.get("/verify/:code", verifyEmail); +router.get("/token", requestToken); + +export default router; diff --git a/src/app.ts b/src/app.ts index 5b39331..e65ac98 100644 --- a/src/app.ts +++ b/src/app.ts @@ -13,6 +13,7 @@ import defaultErrorHandler from "./Middleware/error/handler"; import logger from "./Middleware/error/logger"; import debugLogger from "./Middleware/debug/logger"; import mediaRouter from "./Routes/media.routes"; +import userRouter from "./Routes/user_auth.routes"; import { notFoundHandler, rootHandler } from "./Middleware/error/defaultRoutes"; // Set up async error handling @@ -82,7 +83,9 @@ async function main() { app.use("/api/role-schemas", roleSchemaRouter); app.use("/api/media", mediaRouter); - + + app.use("/api/users", userRouter); + app.get("/", rootHandler); app.get("/api", rootHandler); diff --git a/src/lib/mail.ts b/src/lib/mail.ts index 4572b1e..86b83c8 100644 --- a/src/lib/mail.ts +++ b/src/lib/mail.ts @@ -6,13 +6,21 @@ import SMTPTransport from "nodemailer/lib/smtp-transport"; import mjml from "./mjml"; import { randomUUID } from "crypto"; +import logger from "../Middleware/error/logger"; export let mailAccount = { user: process.env.MAILUSER + "@mail." + process.env.DOMAIN, pass: process.env.MAILPASSWORD }; let transporter = - process.env.DEV == "true" || process.env.DOMAIN == undefined + process.env.NODE_ENV == "development" || process.env.DOMAIN == undefined ? (async () => { - mailAccount = await nodemailer.createTestAccount(); + if (process.env.ETHEREAL_EMAIL == undefined || process.env.ETHEREAL_PASSWORD == undefined) { + mailAccount = await nodemailer.createTestAccount(); + } else { + mailAccount = { + user: process.env.ETHEREAL_EMAIL, + pass: process.env.ETHEREAL_PASSWORD, + }; + } if (process.env.NODE_ENV != "test") { console.log(mailAccount); } @@ -43,6 +51,8 @@ let transporter = ); const sendMail = async (from: string, to: string, subject: string, text?: string, html?: string) => { + logger.debug(`Sent email to: ${to}`); + return await ( await transporter ).sendMail({ @@ -57,7 +67,8 @@ const sendMail = async (from: string, to: string, subject: string, text?: string export const verificationMail = async (to: string, eventName: string, verificationLink: string) => { const raw = mjml.getTemplate("emailVerification"); - //TODO: Replace other handlebars with final values + //TODO: Set the verification link to the correct endpoint + verificationLink = "https://" + (process.env.DOMAIN ?? "localhost:3000") + "/api/users/verify/" + verificationLink; const message = Handlebars.compile(raw); const data = { eventName, verificationLink };