From 2eb9edee696918ee83c9a57af186b4e6417ab566 Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Mon, 16 May 2022 15:38:22 +0200 Subject: [PATCH 01/31] Only create default admin when in development mode --- src/app.ts | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/src/app.ts b/src/app.ts index 39dcc0f..83da103 100644 --- a/src/app.ts +++ b/src/app.ts @@ -19,21 +19,26 @@ require("dotenv").config(); // Load dotenv config const app = express(); -if (process.env.NODE_ENV === "development") { - logger.info("Using development mode"); -} - async function main() { - // Dev - await prisma.admin.upsert({ - where: { id: 1 }, - create: { - name: "admin", - password: await argon2.hash("test", { type: argon2.argon2id }), - permission_level: "ELEVATED", - }, - update: {}, - }); + if (process.env.NODE_ENV === "development") { + logger.info("Using development mode"); + logger.warning( + "This mode should not be used in any production-near environment as it is significantly less secure than the production mode" + ); + + // TODO: How should you login to the prod server by default? Maybe random password? + await prisma.admin.upsert({ + where: { id: 1 }, + create: { + name: "admin", + password: await argon2.hash("test", { type: argon2.argon2id }), + permission_level: "ELEVATED", + }, + update: {}, + }); + } else { + logger.info("Using production mode"); + } // Todo: Everything From 645baf1f8aa06dffb0cfbe9473b04c0b5b4a0bfe Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Tue, 17 May 2022 15:42:50 +0200 Subject: [PATCH 02/31] Add cors --- README.md | 1 + package.json | 4 +++- src/app.ts | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 24171fb..b4b469e 100644 --- a/README.md +++ b/README.md @@ -14,4 +14,5 @@ Before Runningthis on you local machine some things have to be setup DOMAIN: THE DOMAIN NAME OF THE SERVER MAILPASSWORD: THE PASSWORD FOR THE MAIL ACCOUNT DEV: SWITCH FOR DEV MODE AFFECTS EMAIL SERVER + ALLOW_ORIGIN: ORIGIN OF THE PRODUCTION CLIENT (FOR CORS) ``` diff --git a/package.json b/package.json index d247c39..b21172b 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "homepage": "https://github.com/detleph/server#readme", "dependencies": { "@prisma/client": "^3.3.0", + "@types/cors": "^2.8.12", "@types/handlebars": "^4.1.0", "@types/jsonwebtoken": "^8.5.5", "@types/mjml": "^4.7.0", @@ -27,10 +28,11 @@ "@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", - "handlebars": "^4.7.7", "express-async-errors": "^3.1.1", + "handlebars": "^4.7.7", "jsonwebtoken": "^8.5.1", "mjml": "^4.11.0", "nanoid": "^3.3.3", diff --git a/src/app.ts b/src/app.ts index 83da103..58762fe 100644 --- a/src/app.ts +++ b/src/app.ts @@ -3,6 +3,7 @@ import prisma from "./lib/prisma"; import eventRouter from "./Routes/event.routes"; import adminAuthRouter from "./Routes/admin_auth.routes"; import argon2 from "argon2"; +import cors from "cors"; import adminRouter from "./Routes/admin.routes"; import organisationRouter from "./Routes/organisation.routes"; import groupRouter from "./Routes/group.routes"; @@ -36,8 +37,22 @@ async function main() { }, update: {}, }); + + // Allow all CORS requests + app.use(cors()); } else { logger.info("Using production mode"); + + // Configure cors + app.use( + cors({ + origin: process.env.ALLOW_ORIGIN, + allowedHeaders: ["Content-Type", "Authorization"], + preflightContinue: false, + methods: ["GET", "PUT", "PATCH", "POST", "DELETE"], + optionsSuccessStatus: 204, + }) + ); } // Todo: Everything From 3ec5f924296fc36778da4b66454f5fe3b3580072 Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Tue, 17 May 2022 16:16:26 +0200 Subject: [PATCH 03/31] Add controllers for 404 and the root endpoint --- src/Middleware/error/defaultRoutes.ts | 27 +++++++++++++++++++++++++++ src/app.ts | 5 +++++ 2 files changed, 32 insertions(+) create mode 100644 src/Middleware/error/defaultRoutes.ts diff --git a/src/Middleware/error/defaultRoutes.ts b/src/Middleware/error/defaultRoutes.ts new file mode 100644 index 0000000..4a15997 --- /dev/null +++ b/src/Middleware/error/defaultRoutes.ts @@ -0,0 +1,27 @@ +import { Request, Response } from "express"; + +// Only called when no other route matches +export function notFoundHandler(req: Request, res: Response) { + return res.status(404).json({ + type: "error", + payload: { + message: `The ${req.method} HTTP method is implemented for '${req.path}'`, + _links: [ + { + rel: "root", + href: "/api", + }, + ], + }, + }); +} + +export function rootHandler(req: Request, res: Response) { + return res.status(200).json({ + type: "success", + payload: { + message: "Detleph event API", + detail: "This is the API for the Detleph event system", + }, + }); +} diff --git a/src/app.ts b/src/app.ts index 58762fe..749116d 100644 --- a/src/app.ts +++ b/src/app.ts @@ -12,6 +12,7 @@ import roleSchemaRouter from "./Routes/role_schema.routes"; import defaultErrorHandler from "./Middleware/error/handler"; import logger from "./Middleware/error/logger"; import debugLogger from "./Middleware/debug/logger"; +import { notFoundHandler, rootHandler } from "./Middleware/error/defaultRoutes"; // Set up async error handling require("express-async-errors"); @@ -79,9 +80,13 @@ async function main() { app.use("/api/role-schemas", roleSchemaRouter); + app.get("/", rootHandler); + // Error handling app.use(defaultErrorHandler); // Not working + app.use(notFoundHandler); + app.listen(process.env.PORT, () => { logger.info(`Listening on port ${process.env.PORT}`); }); From 197d1a52a337b7a309e8d614dc6d44436a964ba1 Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Fri, 27 May 2022 22:12:16 +0200 Subject: [PATCH 04/31] Add basic controllers and helper for roles + Directly link participants to teams for sake of query efficiency --- prisma/schema.prisma | 3 + src/Controllers/role.controller.ts | 89 ++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 src/Controllers/role.controller.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9557922..7e7a3e7 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -66,6 +66,7 @@ model Team { leaderEmail String roles Role[] @relation(name: "participants") + participants Participant[] discipline Discipline @relation(fields: [disciplineId], references: [id], onDelete: Cascade) disciplineId Int } @@ -79,6 +80,8 @@ model Participant { group Group @relation(fields: [groupId], references: [id], onDelete: Cascade) groupId Int + team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) + teamId Int roles Role[] } diff --git a/src/Controllers/role.controller.ts b/src/Controllers/role.controller.ts new file mode 100644 index 0000000..f5088c9 --- /dev/null +++ b/src/Controllers/role.controller.ts @@ -0,0 +1,89 @@ +import { Role } from "@prisma/client"; +import { Request, Response } from "express"; +import { z } from "zod"; +import prisma from "../lib/prisma"; +import NotFoundError from "../Middleware/error/NotFoundError"; +import { DataType, generateInvalidBodyError } from "./common"; + +/** + * + * @param teamPid: Pid of the team to add the roles to + * @returns Count of roles added (As roles are helper objects, there should be no need for more) + */ +export async function createRolesForTeam(teamPid: string) { + const schemas = await prisma.roleSchema.findMany({ where: { discipline: { teams: { some: { pid: teamPid } } } } }); + + const teamId = (await prisma.team.findUnique({ where: { pid: teamPid } }))?.id; + + if (!teamId) { + throw new NotFoundError("team", teamPid); + } + + const roles = await prisma.role.createMany({ + data: schemas.map((schema) => ({ schemaId: schema.id, score: "", teamId })), + }); + + return roles.count; +} + +export async function getRolesForTeam(req: Request<{ teamPid: string }>, res: Response) { + const teamPid = req.params.teamPid; + + // TODO: Check if leader of team + const roles = await prisma.role.findMany({ + where: { team: { pid: teamPid } }, + select: { + pid: true, + score: true, + schema: { select: { pid: true } }, + participant: { select: { pid: true, firstName: true, lastName: true } }, + }, + }); + + return res.status(200).json({ + type: "success", + payload: { + roles, + }, + }); +} + +const AssignParticipantToRoleBody = z.object({ + participantPid: z.string().uuid(), +}); + +// requires: auth(leader of the team) +export async function assignParticipantToRole(req: Request<{ pid: string }>, res: Response) { + const zBody = AssignParticipantToRoleBody.safeParse(req.body); + + if (zBody.success === false) { + return res.status(400).json(generateInvalidBodyError({ participant: DataType.UUID })); + } + + const { participantPid } = zBody.data; + const rolePid = req.params.pid; + + const schema = await prisma.role.findFirst({ + where: { pid: rolePid, team: { participants: { some: { pid: participantPid } } } }, + select: { participant: { select: { pid: true, firstName: true, lastName: true } } }, + }); + + if (!schema) { + return res.status(404).json({ + type: "error", + payload: { + message: `No role with the ID '${rolePid}' could be found in the scope of the participant with the ID '${participantPid}'`, + }, + }); + } + + await prisma.role.update({ where: { pid: rolePid }, data: { participant: { connect: { pid: participantPid } } } }); + + return res.status(200).json({ + type: "success", + payload: { + message: `The participant with ID '${participantPid}' was successfully assigned to the role with ID '${rolePid}'`, + ...(schema.participant ? { unassigned: schema.participant } : {}), + }, + }); +} From a9c182cf646e9a18f9c82267c6a4d2de83542b58 Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Fri, 27 May 2022 22:48:16 +0200 Subject: [PATCH 05/31] Add teamleader authentication --- src/Middleware/auth/auth.ts | 4 +- src/Middleware/auth/teamleaderAuth.ts | 75 +++++++++++++++++++++++++++ src/custom.d.ts | 2 + 3 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 src/Middleware/auth/teamleaderAuth.ts diff --git a/src/Middleware/auth/auth.ts b/src/Middleware/auth/auth.ts index 0b66952..6fb22eb 100644 --- a/src/Middleware/auth/auth.ts +++ b/src/Middleware/auth/auth.ts @@ -8,9 +8,9 @@ import prisma from "../../lib/prisma"; const JWT_SECRET = process.env.JWT_SECRET || "secret"; -const verifyAuthorizationFormat = (authorization: string) => /^Bearer .+$/.test(authorization); +export const verifyAuthorizationFormat = (authorization: string) => /^Bearer .+$/.test(authorization); -const getBearerToken = (authorization: string) => authorization.slice(7); +export const getBearerToken = (authorization: string) => authorization.slice(7); export const requireAuthentication = async (req: Request, res: Response, next: NextFunction) => { const { authorization } = req.headers; diff --git a/src/Middleware/auth/teamleaderAuth.ts b/src/Middleware/auth/teamleaderAuth.ts new file mode 100644 index 0000000..489e8d3 --- /dev/null +++ b/src/Middleware/auth/teamleaderAuth.ts @@ -0,0 +1,75 @@ +import { Participant } from "@prisma/client"; +import e, { NextFunction, Request, Response } from "express"; +import jwt, { JsonWebTokenError } from "jsonwebtoken"; +import { getBearerToken, verifyAuthorizationFormat } from "./auth"; + +export interface TeamleaderJWTPayload { + pid: string; + team: string; +} + +const JWT_SECRET = process.env.JWT_SECRET; + +export function generateTeamleaderJWT(teamleader: Participant & { relevance: "TEAMLEADER"; team: { pid: string } }) { + if (!JWT_SECRET) { + throw new Error("JWT_SECRET not set"); + } + + const payload: TeamleaderJWTPayload = { + pid: teamleader.pid, + team: teamleader.team.pid, + }; + + return jwt.sign(payload, JWT_SECRET, { expiresIn: "4 days" }); +} + +export async function requireTeamleaderAuthentication(req: Request, res: Response, next: NextFunction) { + if (!JWT_SECRET) { + throw new Error("JWT_SECRET not set"); + } + + const { authorization } = req.headers; + + if (!authorization) { + return res.status(403).send({ + type: "error", + payload: { + message: + "The request did not include the Authorization header (Only the team leader can perform this operation)", + }, + }); + } + + if (!verifyAuthorizationFormat(authorization)) { + return res.status(400).send({ + type: "error", + payload: { + message: "Malformed Authorization header", + format: "Bearer ", + }, + }); + } + + try { + const token_payload = jwt.verify(getBearerToken(authorization), JWT_SECRET) as TeamleaderJWTPayload; + + req.teamleader = { + isAuthenticated: true, + pid: token_payload.pid, + team: token_payload.team, + }; + + next(); + } catch (e) { + if (e instanceof JsonWebTokenError) { + return res.status(403).json({ + type: "error", + payload: { + message: "Token could not be verified; It might be expired", + }, + }); + } + } + + throw e; +} diff --git a/src/custom.d.ts b/src/custom.d.ts index bb6296a..86509ba 100644 --- a/src/custom.d.ts +++ b/src/custom.d.ts @@ -1,7 +1,9 @@ import { AuthJWTPayload } from "./Controllers/admin_auth.controller"; +import { TeamleaderJWTPayload } from "./Middleware/auth/teamleaderAuth"; declare module "express-serve-static-core" { interface Request { auth?: AuthJWTPayload & { isAuthenticated: boolean }; + teamleader?: TeamleaderJWTPayload & { isAuthenticated: boolean }; } } From 2a33b703bc27e35e8196e3b92528ed4b3fdd1a13 Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Fri, 27 May 2022 23:08:23 +0200 Subject: [PATCH 06/31] Add methods for requiring more specific levels of teamleader auth + Add AuthError --- src/Controllers/admin_auth.controller.ts | 6 +++++- src/Controllers/role.controller.ts | 8 +++++++- src/Middleware/auth/auth.ts | 6 +++++- src/Middleware/auth/teamleaderAuth.ts | 22 ++++++++++++++++++++++ src/Middleware/error/AuthError.ts | 13 +++++++++++++ 5 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 src/Middleware/error/AuthError.ts diff --git a/src/Controllers/admin_auth.controller.ts b/src/Controllers/admin_auth.controller.ts index 760e85d..bb9ed32 100644 --- a/src/Controllers/admin_auth.controller.ts +++ b/src/Controllers/admin_auth.controller.ts @@ -6,7 +6,7 @@ import argon2 from "argon2"; import jwt from "jsonwebtoken"; import { DataType, generateInvalidBodyError } from "./common"; -const JWT_SECRET = process.env.JWT_SECRET || "secret"; +const JWT_SECRET = process.env.JWT_SECRET; const TOKEN_EXPIRY = "4 days"; export interface AuthJWTPayload { @@ -18,6 +18,10 @@ export interface AuthJWTPayload { } function createAdminJWT(admin: Admin & { groups: Group[] }) { + if (!JWT_SECRET) { + throw new Error("JWT_SECRET not set"); + } + const payload: AuthJWTPayload = { pid: admin.pid, name: admin.name, diff --git a/src/Controllers/role.controller.ts b/src/Controllers/role.controller.ts index f5088c9..63721fe 100644 --- a/src/Controllers/role.controller.ts +++ b/src/Controllers/role.controller.ts @@ -2,9 +2,12 @@ import { Role } from "@prisma/client"; import { Request, Response } from "express"; import { z } from "zod"; import prisma from "../lib/prisma"; +import { requireLeaderOfTeam, requireResponsibleForParticipant } from "../Middleware/auth/teamleaderAuth"; import NotFoundError from "../Middleware/error/NotFoundError"; import { DataType, generateInvalidBodyError } from "./common"; +require("express-async-errors"); + /** * * @param teamPid: Pid of the team to add the roles to @@ -29,7 +32,8 @@ export async function createRolesForTeam(teamPid: string) { export async function getRolesForTeam(req: Request<{ teamPid: string }>, res: Response) { const teamPid = req.params.teamPid; - // TODO: Check if leader of team + requireLeaderOfTeam(req.teamleader, teamPid); + const roles = await prisma.role.findMany({ where: { team: { pid: teamPid } }, select: { @@ -63,6 +67,8 @@ export async function assignParticipantToRole(req: Request<{ pid: string }>, res const { participantPid } = zBody.data; const rolePid = req.params.pid; + requireResponsibleForParticipant(req.teamleader, participantPid); + const schema = await prisma.role.findFirst({ where: { pid: rolePid, team: { participants: { some: { pid: participantPid } } } }, select: { participant: { select: { pid: true, firstName: true, lastName: true } } }, diff --git a/src/Middleware/auth/auth.ts b/src/Middleware/auth/auth.ts index 6fb22eb..981c116 100644 --- a/src/Middleware/auth/auth.ts +++ b/src/Middleware/auth/auth.ts @@ -6,13 +6,17 @@ import { authClient } from "../../lib/redis"; import jwt, { JsonWebTokenError, JwtPayload } from "jsonwebtoken"; import prisma from "../../lib/prisma"; -const JWT_SECRET = process.env.JWT_SECRET || "secret"; +const JWT_SECRET = process.env.JWT_SECRET; export const verifyAuthorizationFormat = (authorization: string) => /^Bearer .+$/.test(authorization); export const getBearerToken = (authorization: string) => authorization.slice(7); export const requireAuthentication = async (req: Request, res: Response, next: NextFunction) => { + if (!JWT_SECRET) { + throw new Error("JWT_SECRET not set"); + } + const { authorization } = req.headers; if (!authorization) { diff --git a/src/Middleware/auth/teamleaderAuth.ts b/src/Middleware/auth/teamleaderAuth.ts index 489e8d3..9e8d358 100644 --- a/src/Middleware/auth/teamleaderAuth.ts +++ b/src/Middleware/auth/teamleaderAuth.ts @@ -1,7 +1,9 @@ import { Participant } from "@prisma/client"; import e, { NextFunction, Request, Response } from "express"; import jwt, { JsonWebTokenError } from "jsonwebtoken"; +import AuthError from "../error/AuthError"; import { getBearerToken, verifyAuthorizationFormat } from "./auth"; +import prisma from "../../lib/prisma"; export interface TeamleaderJWTPayload { pid: string; @@ -73,3 +75,23 @@ export async function requireTeamleaderAuthentication(req: Request, res: Respons throw e; } + +export function requireLeaderOfTeam(auth: TeamleaderJWTPayload | undefined, teamPid: string) { + if (auth?.team !== teamPid) { + throw new AuthError("The provided authorization is not valid for the requested team"); + } +} + +export async function requireResponsibleForParticipant(auth: TeamleaderJWTPayload | undefined, participantPid: string) { + if (!auth) { + throw new AuthError("There was an error with your authorization"); + } + + const teamPid = ( + await prisma.participant.findUnique({ where: { pid: participantPid }, select: { team: { select: { pid: true } } } }) + )?.team.pid; + + if (teamPid !== auth.team) { + throw new AuthError("The provided authorization is not valid for the requested participant"); + } +} diff --git a/src/Middleware/error/AuthError.ts b/src/Middleware/error/AuthError.ts new file mode 100644 index 0000000..0faae4c --- /dev/null +++ b/src/Middleware/error/AuthError.ts @@ -0,0 +1,13 @@ +import ForwardableError from "./ForwardableError"; + +export default class AuthError extends ForwardableError { + protected __oid = "AUTH_ERROR"; + + constructor(message?: string) { + super(403, message ?? "The request did not provide sufficient authentication"); + } + + static isAuthError(err: any): err is AuthError { + return err.__oid === "AUTH_ERROR"; + } +} From 55eab7000a1d50cdf6a70a43e3eb82ee647c5d51 Mon Sep 17 00:00:00 2001 From: Laurin <60652077+Flexla54@users.noreply.github.com> Date: Sat, 28 May 2022 16:52:35 +0200 Subject: [PATCH 07/31] add deleteRolesFromTeam --- src/Controllers/role.controller.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Controllers/role.controller.ts b/src/Controllers/role.controller.ts index 63721fe..a326a3e 100644 --- a/src/Controllers/role.controller.ts +++ b/src/Controllers/role.controller.ts @@ -93,3 +93,9 @@ export async function assignParticipantToRole(req: Request<{ pid: string }>, res }, }); } + +export async function deleteRolesFromTeam(teamPid: string){ + await prisma.role.deleteMany({ + where: { team: { pid: teamPid, } } + }); +} \ No newline at end of file From 93d77eeefa22548944a84b0da92bbfd2b5f9ed41 Mon Sep 17 00:00:00 2001 From: Stefan-5422 <32109571+Stefan-5422@users.noreply.github.com> Date: Sat, 28 May 2022 17:14:33 +0200 Subject: [PATCH 08/31] Added user_auth system + 1 Mega commit yay --- package-lock.json | 33 ++++++++++ prisma/schema.prisma | 1 + src/Controllers/user_auth.controller.ts | 82 +++++++++++++++++++++---- src/Middleware/auth/teamleaderAuth.ts | 9 +-- src/Routes/user_auth.routes.ts | 10 +++ src/app.ts | 5 +- src/lib/mail.ts | 17 ++++- 7 files changed, 134 insertions(+), 23 deletions(-) create mode 100644 src/Routes/user_auth.routes.ts 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 }; From 58235b301e2914dfe38cc9c6116d58bf61bc4615 Mon Sep 17 00:00:00 2001 From: Laurin <60652077+Flexla54@users.noreply.github.com> Date: Sat, 28 May 2022 18:27:13 +0200 Subject: [PATCH 09/31] added createRolesForTeam in register --- src/Controllers/role.controller.ts | 2 +- src/Controllers/user_auth.controller.ts | 4 ++++ src/Routes/role.routes.ts | 6 ++++++ 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 src/Routes/role.routes.ts diff --git a/src/Controllers/role.controller.ts b/src/Controllers/role.controller.ts index a326a3e..54f2076 100644 --- a/src/Controllers/role.controller.ts +++ b/src/Controllers/role.controller.ts @@ -98,4 +98,4 @@ export async function deleteRolesFromTeam(teamPid: string){ await prisma.role.deleteMany({ where: { team: { pid: teamPid, } } }); -} \ No newline at end of file +} diff --git a/src/Controllers/user_auth.controller.ts b/src/Controllers/user_auth.controller.ts index e7147ed..e46879a 100644 --- a/src/Controllers/user_auth.controller.ts +++ b/src/Controllers/user_auth.controller.ts @@ -5,6 +5,7 @@ import { nanoid } from "nanoid"; import { verificationMail } from "../lib/mail"; import { DataType, generateInvalidBodyError } from "./common"; import { generateTeamleaderJWT } from "../Middleware/auth/teamleaderAuth"; +import { createRolesForTeam } from "./role.controller"; interface CreateTeamBody { name: string; @@ -38,6 +39,9 @@ export const register = async (req: Request<{}, {}, CreateTeamBody>, res: Respon }, }); + //To do: maybe use returned amount of created use? + createRolesForTeam(team.pid); + const usid = nanoid(); (await mailClient).set(usid, team.pid); diff --git a/src/Routes/role.routes.ts b/src/Routes/role.routes.ts new file mode 100644 index 0000000..72726d5 --- /dev/null +++ b/src/Routes/role.routes.ts @@ -0,0 +1,6 @@ +import Express from "express"; +import { assignParticipantToRole } from "../Controllers/role.controller"; + +const router = Express.Router(); + +router.patch<"/:pid/", { pid: string }>("/:pid/", assignParticipantToRole) \ No newline at end of file From 233182c0ea771450311723a91e6896f7960c3497 Mon Sep 17 00:00:00 2001 From: Laurin <60652077+Flexla54@users.noreply.github.com> Date: Sat, 28 May 2022 18:36:36 +0200 Subject: [PATCH 10/31] added getRolesForTeam to role.routes --- src/Routes/role.routes.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Routes/role.routes.ts b/src/Routes/role.routes.ts index 72726d5..0c91fd1 100644 --- a/src/Routes/role.routes.ts +++ b/src/Routes/role.routes.ts @@ -1,6 +1,9 @@ import Express from "express"; -import { assignParticipantToRole } from "../Controllers/role.controller"; +import { assignParticipantToRole, getRolesForTeam } from "../Controllers/role.controller"; const router = Express.Router(); +//TO DO: maybe transfer getRolesForTeam to team router +router.get<"team/:teamPid/", { teamPid: string }>("team/:teamPid/", getRolesForTeam); + router.patch<"/:pid/", { pid: string }>("/:pid/", assignParticipantToRole) \ No newline at end of file From 0431406328da022907d4cd4f219f289211a79298 Mon Sep 17 00:00:00 2001 From: Laurin <60652077+Flexla54@users.noreply.github.com> Date: Sun, 29 May 2022 00:27:04 +0200 Subject: [PATCH 11/31] added createParticipant, patched DataType --- src/Controllers/common.ts | 1 + src/Controllers/participant.controller.ts | 68 +++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 src/Controllers/participant.controller.ts diff --git a/src/Controllers/common.ts b/src/Controllers/common.ts index 0b97e88..7a6581c 100644 --- a/src/Controllers/common.ts +++ b/src/Controllers/common.ts @@ -23,6 +23,7 @@ export enum DataType { NUMBER = "number", INTEGER = "integer", PERMISSION_LEVEL = "'ELEVATED' | 'STANDARD'", + JOB = "'TEAMLEADER' | 'MEMBER'", DATETIME = "ISOstring", UUID = "string", RESULT_SCHEMA = "result_schema", diff --git a/src/Controllers/participant.controller.ts b/src/Controllers/participant.controller.ts new file mode 100644 index 0000000..8026c65 --- /dev/null +++ b/src/Controllers/participant.controller.ts @@ -0,0 +1,68 @@ +import prisma from "../lib/prisma"; +import { z } from "zod"; +import { Request, Response } from "express"; +import { createInsufficientPermissionsError, DataType, generateError, generateInvalidBodyError } from "./common"; +import { Job } from "@prisma/client"; +import { PrismaClientKnownRequestError } from "@prisma/client/runtime"; + +//TODO: add TeamleaderAuthentification +// discuss wether + +const ParticipantBody = z.object({ + firstName: z.string(), + lastName: z.string(), + groupId: z.string(), + job: z.enum(["TEAMLEADER", "MEMBER"]), +}) + +const returnedParticipant = { + pid: true, + firstName: true, + lastName: true, + relevance: true, + team: { select: { pid: true, } }, + group: { select: { pid: true, } }, +} as const; + +export const createParticipant = async (req: Request<{ pid: string}>, res: Response) => { + //insert TeamleaderAuth + + const result = ParticipantBody.safeParse(req.body); + + if(result.success === false){ + return res.status(400).json( + generateInvalidBodyError({ + firstname: DataType.STRING, + lastName: DataType.STRING, + groupId: DataType.UUID, + job: DataType.JOB, + }) + ); + } + + const body = result.data; + const { pid } = req.params; + + try { + const participant = await prisma.participant.create({ + data: { + firstName: body.firstName, + lastName: body.lastName, + relevance: body.job, + group: { connect: { pid: body.groupId } }, + team: { connect: { pid } }, + }, + select: returnedParticipant, + }); + + return res.status(201).json({ + type: "success", + payload: participant, + }); + } catch (e) { + if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { + return res.status(404).json(generateError(`Could not link to team with ID '${pid}, or group with ID ${body.groupId}'`)); + } + throw e; + } +} \ No newline at end of file From 4bec0d4b5e38ff9bda385fc856c1061ae14c1d4b Mon Sep 17 00:00:00 2001 From: Laurin <60652077+Flexla54@users.noreply.github.com> Date: Sun, 29 May 2022 01:02:13 +0200 Subject: [PATCH 12/31] added updateParticipant --- src/Controllers/participant.controller.ts | 78 ++++++++++++++++++++++- 1 file changed, 75 insertions(+), 3 deletions(-) diff --git a/src/Controllers/participant.controller.ts b/src/Controllers/participant.controller.ts index 8026c65..9eb0047 100644 --- a/src/Controllers/participant.controller.ts +++ b/src/Controllers/participant.controller.ts @@ -2,8 +2,9 @@ import prisma from "../lib/prisma"; import { z } from "zod"; import { Request, Response } from "express"; import { createInsufficientPermissionsError, DataType, generateError, generateInvalidBodyError } from "./common"; -import { Job } from "@prisma/client"; +import { Job, Prisma } from "@prisma/client"; import { PrismaClientKnownRequestError } from "@prisma/client/runtime"; +import NotFoundError from "../Middleware/error/NotFoundError"; //TODO: add TeamleaderAuthentification // discuss wether @@ -13,7 +14,13 @@ const ParticipantBody = z.object({ lastName: z.string(), groupId: z.string(), job: z.enum(["TEAMLEADER", "MEMBER"]), -}) +}); + +const updateParticipantBody = ParticipantBody.pick({ + firstName: true, + lastName: true, + groupId: true, +}).partial(); const returnedParticipant = { pid: true, @@ -65,4 +72,69 @@ export const createParticipant = async (req: Request<{ pid: string}>, res: Respo } throw e; } -} \ No newline at end of file +} + +const updateParticipant = async (req: Request<{ pid: string }>, res: Response) => { + //insert TeamleaderAuth + + const result = updateParticipantBody.safeParse(req.body); + + if(result.success === false){ + return res.status(400).json( + generateInvalidBodyError({ + firstname: DataType.STRING, + lastName: DataType.STRING, + groupId: DataType.UUID, + }) + ); + } + + const body = result.data; + const { pid } = req.params; + + try { + const participant = await prisma.participant.update({ + where: { pid }, + data: { + firstName: body.firstName, + lastName: body.lastName, + group: { connect: { pid: body.groupId, } }, + }, + select: returnedParticipant, + }); + + if(!participant) { + throw new NotFoundError("participant", pid); + } + + res.status(200).json({ + type: "success", + payload: participant, + }); + + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { + return res.status(500).json({ + type: "error", + payload: { + message: `Internal Server error occured. Try again later`, + }, + }); + } + if (e instanceof Prisma.PrismaClientUnknownRequestError) { + return res.status(500).json({ + type: "error", + payload: { + message: "Unknown error occurred with your request. Check if your parameters are correct", + schema: { + firstname: DataType.STRING, + lastName: DataType.STRING, + groupId: DataType.UUID, + }, + }, + }); + } + + throw e; + } +} From b1a0e8be7c96e8a7c4494739d87f255477c907c3 Mon Sep 17 00:00:00 2001 From: Laurin <60652077+Flexla54@users.noreply.github.com> Date: Sun, 29 May 2022 01:14:47 +0200 Subject: [PATCH 13/31] added deleteParticipant --- src/Controllers/event.controller.ts | 2 +- src/Controllers/participant.controller.ts | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/Controllers/event.controller.ts b/src/Controllers/event.controller.ts index 3193c37..42706f3 100644 --- a/src/Controllers/event.controller.ts +++ b/src/Controllers/event.controller.ts @@ -241,7 +241,7 @@ export const deleteEvent = async (req: Request, res: Res return res.status(204).end(); } catch (e) { if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { - return res.status(404).json(generateError(`The organisation with the ID ${pid} could not be found`)); + return res.status(404).json(generateError(`The event with the ID ${pid} could not be found`)); } throw e; diff --git a/src/Controllers/participant.controller.ts b/src/Controllers/participant.controller.ts index 9eb0047..54f4b18 100644 --- a/src/Controllers/participant.controller.ts +++ b/src/Controllers/participant.controller.ts @@ -74,7 +74,7 @@ export const createParticipant = async (req: Request<{ pid: string}>, res: Respo } } -const updateParticipant = async (req: Request<{ pid: string }>, res: Response) => { +export const updateParticipant = async (req: Request<{ pid: string }>, res: Response) => { //insert TeamleaderAuth const result = updateParticipantBody.safeParse(req.body); @@ -138,3 +138,21 @@ const updateParticipant = async (req: Request<{ pid: string }>, res: Response) = throw e; } } + +export const deleteParticipant = async (req: Request<{ pid: string }>, res: Response) => { + //insert TeamleaderAuth + + const { pid } = req.params; + + try { + await prisma.participant.delete({ where: { pid } }); + + return res.status(204).end(); + } catch (e) { + if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { + return res.status(404).json(generateError(`The participant with the ID ${pid} could not be found`)); + } + + throw e; + } +} \ No newline at end of file From cb50e5ac2ef9a9dd4a6221fce658454673a6b011 Mon Sep 17 00:00:00 2001 From: Laurin <60652077+Flexla54@users.noreply.github.com> Date: Sun, 29 May 2022 01:35:57 +0200 Subject: [PATCH 14/31] small fixes --- src/Controllers/participant.controller.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Controllers/participant.controller.ts b/src/Controllers/participant.controller.ts index 54f4b18..f4f6fc1 100644 --- a/src/Controllers/participant.controller.ts +++ b/src/Controllers/participant.controller.ts @@ -7,7 +7,6 @@ import { PrismaClientKnownRequestError } from "@prisma/client/runtime"; import NotFoundError from "../Middleware/error/NotFoundError"; //TODO: add TeamleaderAuthentification -// discuss wether const ParticipantBody = z.object({ firstName: z.string(), @@ -16,10 +15,8 @@ const ParticipantBody = z.object({ job: z.enum(["TEAMLEADER", "MEMBER"]), }); -const updateParticipantBody = ParticipantBody.pick({ - firstName: true, - lastName: true, - groupId: true, +const updateParticipantBody = ParticipantBody.omit({ + job: true, }).partial(); const returnedParticipant = { @@ -27,8 +24,14 @@ const returnedParticipant = { firstName: true, lastName: true, relevance: true, - team: { select: { pid: true, } }, - group: { select: { pid: true, } }, + team: { select: { + pid: true, + name: true, + } }, + group: { select: { + pid: true, + name: true, + } }, } as const; export const createParticipant = async (req: Request<{ pid: string}>, res: Response) => { From e2afde59e1fef16216d9961a45488d45ec0bbb58 Mon Sep 17 00:00:00 2001 From: Laurin <60652077+Flexla54@users.noreply.github.com> Date: Sun, 29 May 2022 02:02:17 +0200 Subject: [PATCH 15/31] added updateDiscipline --- src/Controllers/discipline.controller.ts | 92 ++++++++++++++++++++--- src/Controllers/participant.controller.ts | 2 +- 2 files changed, 81 insertions(+), 13 deletions(-) diff --git a/src/Controllers/discipline.controller.ts b/src/Controllers/discipline.controller.ts index de8abbd..731741d 100644 --- a/src/Controllers/discipline.controller.ts +++ b/src/Controllers/discipline.controller.ts @@ -1,6 +1,7 @@ import { Prisma, Organisation, Admin, AdminLevel, Team } from "@prisma/client"; import { PrismaClientKnownRequestError, PrismaClientUnknownRequestError } from "@prisma/client/runtime"; import { Request, Response } from "express"; +import { z } from "zod"; import prisma from "../lib/prisma"; import ForwardableError from "../Middleware/error/ForwardableError"; import NotFoundError from "../Middleware/error/NotFoundError"; @@ -15,6 +16,14 @@ import { require("express-async-errors"); +const DisciplineBody = z.object({ + name: z.string(), + minTeamSize: z.number(), + maxTeamSize: z.number(), +}) + +const updateDisciplineBody = DisciplineBody.partial(); + const basicDiscipline = { pid: true, name: true, @@ -126,9 +135,9 @@ export const createDiscipline = async (req: Request<{ eventPid: string }, {}, Cr return res.status(403).json(createInsufficientPermissionsError()); } - const { name, minTeamSize, maxTeamSize } = req.body; + const result = DisciplineBody.safeParse(req.body); - if (typeof name !== "string" || typeof minTeamSize !== "number" || typeof maxTeamSize !== "number") { + if(result.success === false) { return res.status(400).json( generateInvalidBodyError({ name: DataType.STRING, @@ -138,6 +147,8 @@ export const createDiscipline = async (req: Request<{ eventPid: string }, {}, Cr ); } + const { name, minTeamSize, maxTeamSize } = result.data; + if (!validateName(name)) { return res.status(400).json(NAME_ERROR); } @@ -145,13 +156,7 @@ export const createDiscipline = async (req: Request<{ eventPid: string }, {}, Cr try { const discipline = await prisma.discipline.create({ data: { name, minTeamSize, maxTeamSize, event: { connect: { pid: req.params.eventPid } } }, - select: { - pid: true, - name: true, - minTeamSize: true, - maxTeamSize: true, - event: { select: { pid: true, name: true } }, - }, + select: basicDiscipline, }); return res.status(201).json({ type: "success", payload: { discipline } }); @@ -163,12 +168,75 @@ export const createDiscipline = async (req: Request<{ eventPid: string }, {}, Cr } }; -interface DeleteDisciplineQueryParams { - pid: string; +export const updateDiscipline = async (req: Request<{ eventPid: string }, {}, CreateDisciplineBody>, res: Response) => { + if (req.auth?.permission_level !== "ELEVATED") { + res.status(403).json(createInsufficientPermissionsError()); + } + + const result = updateDisciplineBody.safeParse(req.body); + + if(result.success === false){ + return res.status(400).json( + generateInvalidBodyError({ + name: DataType.STRING, + minTeamSize: DataType.NUMBER, + maxTeamSize: DataType.NUMBER, + }) + ); + } + + const body = result.data; + const { eventPid } = req.params; + + try { + const discipline = await prisma.discipline.update({ + where: { pid: eventPid }, + data: { + name: body.name, + minTeamSize: body.minTeamSize, + maxTeamSize: body.maxTeamSize, + }, + select: basicDiscipline, + }); + + if(!discipline) { + throw new NotFoundError("discipline", eventPid); + } + + res.status(200).json({ + type: "success", + payload: discipline, + }); + + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { + return res.status(500).json({ + type: "error", + payload: { + message: `Internal Server error occured. Try again later`, + }, + }); + } + if (e instanceof Prisma.PrismaClientUnknownRequestError) { + return res.status(500).json({ + type: "error", + payload: { + message: "Unknown error occurred with your request. Check if your parameters are correct", + schema: { + name: DataType.STRING, + minTeamSize: DataType.NUMBER, + maxTeamSize: DataType.NUMBER, + }, + }, + }); + } + + throw e; + } } // requires: auth(ELEVATED) -export const deleteDiscipline = async (req: Request, res: Response) => { +export const deleteDiscipline = async (req: Request<{ pid: string }>, res: Response) => { if (req.auth?.permission_level !== "ELEVATED") { res.status(403).json(createInsufficientPermissionsError()); } diff --git a/src/Controllers/participant.controller.ts b/src/Controllers/participant.controller.ts index f4f6fc1..bc49308 100644 --- a/src/Controllers/participant.controller.ts +++ b/src/Controllers/participant.controller.ts @@ -158,4 +158,4 @@ export const deleteParticipant = async (req: Request<{ pid: string }>, res: Resp throw e; } -} \ No newline at end of file +} From e6cc0ab058d0b458df6e12fad45f3ea4a6d240be Mon Sep 17 00:00:00 2001 From: Laurin <60652077+Flexla54@users.noreply.github.com> Date: Sun, 29 May 2022 02:20:42 +0200 Subject: [PATCH 16/31] added updateGroup --- src/Controllers/group.controllers.ts | 78 +++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/src/Controllers/group.controllers.ts b/src/Controllers/group.controllers.ts index ee7fb5e..012e6f8 100644 --- a/src/Controllers/group.controllers.ts +++ b/src/Controllers/group.controllers.ts @@ -1,12 +1,21 @@ -import { Admin } from "@prisma/client"; +import { Admin, Prisma } from "@prisma/client"; import { PrismaClientKnownRequestError, PrismaClientUnknownRequestError } from "@prisma/client/runtime"; import { Request, Response } from "express"; +import { z } from "zod"; import prisma from "../lib/prisma"; -import { createInsufficientPermissionsError, generateError, genericError, handleCreateByName } from "./common"; +import NotFoundError from "../Middleware/error/NotFoundError"; +import { createInsufficientPermissionsError, DataType, generateError, generateInvalidBodyError, genericError, handleCreateByName } from "./common"; + +const updateGroupBody = z.object({ + name: z.string(), + user_limit: z.number(), + level: z.number(), +}).partial(); const basicGroup = { pid: true, name: true, + level: true, organisation: { select: { pid: true, name: true } }, } as const; @@ -115,6 +124,71 @@ export const createGroup = async (req: Request<{ organisationPid: string }, {}, ); }; +export const updateGroup = async (req: Request<{ pid: string }>, res: Response) => { + //insert TeamleaderAuth + + const result = updateGroupBody.safeParse(req.body); + + if(result.success === false){ + return res.status(400).json( + generateInvalidBodyError({ + name: DataType.STRING, + user_limit: DataType.NUMBER, + level: DataType.NUMBER, + }) + ); + } + + const body = result.data; + const { pid } = req.params; + + try { + const group = await prisma.group.update({ + where: { pid }, + data: { + name: body.name, + user_limit: body.user_limit, + level: body.level, + }, + select: basicGroup, + }); + + if(!group) { + throw new NotFoundError("group", pid); + } + + res.status(200).json({ + type: "success", + payload: group, + }); + + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { + return res.status(500).json({ + type: "error", + payload: { + message: `Internal Server error occured. Try again later`, + }, + }); + } + if (e instanceof Prisma.PrismaClientUnknownRequestError) { + return res.status(500).json({ + type: "error", + payload: { + message: "Unknown error occurred with your request. Check if your parameters are correct", + schema: { + name: DataType.STRING, + user_limit: DataType.NUMBER, + level: DataType.NUMBER, + }, + }, + }); + } + + throw e; + } +} + interface DeleteGroupQueryParams { pid: string; } From 4fb9f2e49650f0a50d22a9398d179879a8e64351 Mon Sep 17 00:00:00 2001 From: Laurin <60652077+Flexla54@users.noreply.github.com> Date: Sun, 29 May 2022 02:44:30 +0200 Subject: [PATCH 17/31] added updateRoleSchema --- src/Controllers/role_schema.controller.ts | 73 +++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/Controllers/role_schema.controller.ts b/src/Controllers/role_schema.controller.ts index 36265de..62a0dfa 100644 --- a/src/Controllers/role_schema.controller.ts +++ b/src/Controllers/role_schema.controller.ts @@ -1,6 +1,7 @@ import { Prisma } from "@prisma/client"; import { PrismaClientKnownRequestError } from "@prisma/client/runtime"; import { Request, Response } from "express"; +import { z } from "zod"; import prisma from "../lib/prisma"; import { DurationSchemaT, parseSchema, PointSchemaT } from "../lib/result_schema"; import NotFoundError from "../Middleware/error/NotFoundError"; @@ -13,7 +14,15 @@ import { validateName, } from "./common"; +const RoleSchemaBody = z.object({ + name: z.string(), + schema: z.string(), +}); + +const UpdateRoleSchema = RoleSchemaBody.partial(); + const roleSchema = { + pid: true, name: true, schema: true, discipline: { select: { pid: true, name: true } }, @@ -124,6 +133,70 @@ export const createRoleSchema = async ( } }; +export const updateRoleSchema = async (req: Request<{ pid: string }>, res: Response) => { + if (req.auth?.permission_level !== "ELEVATED") { + res.status(403).json(createInsufficientPermissionsError()); + } + + const { pid } = req.params; + + const result = UpdateRoleSchema.safeParse(req.body); + + if (result.success === false) { + return res.status(400).json( + generateInvalidBodyError({ + name: DataType.STRING, + schema: DataType.RESULT_SCHEMA, + }) + ); + } + + const body = result.data; + + try { + const schema = await prisma.roleSchema.update({ + where: { pid }, + data: { + name: body.name, + schema: body.schema, + }, + select: roleSchema, + }); + + if (!schema) { + throw new NotFoundError("schema", pid); + } + + res.status(200).json({ + type: "success", + payload: schema, + }); + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { + return res.status(500).json({ + type: "error", + payload: { + message: `Internal Server error occured. Try again later`, + }, + }); + } + if (e instanceof Prisma.PrismaClientUnknownRequestError) { + return res.status(500).json({ + type: "error", + payload: { + message: "Unknown error occurred with your request. Check if your parameters are correct", + schema: { + name: DataType.STRING, + schema: DataType.RESULT_SCHEMA, + }, + }, + }); + } + + throw e; + } +}; + interface visualParams { schemaPid: string; } From 21deb2259a0832197b302ba5229e778ff4bd4029 Mon Sep 17 00:00:00 2001 From: Laurin <60652077+Flexla54@users.noreply.github.com> Date: Sun, 29 May 2022 02:47:32 +0200 Subject: [PATCH 18/31] added deleteRoleSchema --- src/Controllers/role_schema.controller.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/Controllers/role_schema.controller.ts b/src/Controllers/role_schema.controller.ts index 62a0dfa..2f0d0e5 100644 --- a/src/Controllers/role_schema.controller.ts +++ b/src/Controllers/role_schema.controller.ts @@ -9,6 +9,7 @@ import SchemaError from "../Middleware/error/SchemaError"; import { createInsufficientPermissionsError, DataType, + generateError, generateInvalidBodyError, NAME_ERROR, validateName, @@ -197,6 +198,25 @@ export const updateRoleSchema = async (req: Request<{ pid: string }>, res: Respo } }; +export const deleteRoleSchema = async (req: Request<{ pid: string }>, res: Response) => { + if (req.auth?.permission_level !== "ELEVATED") { + res.status(403).json(createInsufficientPermissionsError()); + } + + const { pid } = req.params; + + try { + await prisma.roleSchema.delete({ where: { pid } }); + + return res.status(204).end(); + } catch (e) { + if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { + return res.status(404).json(generateError(`The RoleSchema with the ID ${pid} could not be found`)); + } + + throw e; + } + interface visualParams { schemaPid: string; } From f560095731e4efd6e99fb8d83c2c0a59c2fb4d6a Mon Sep 17 00:00:00 2001 From: Laurin <60652077+Flexla54@users.noreply.github.com> Date: Sun, 29 May 2022 03:12:32 +0200 Subject: [PATCH 19/31] updated register and createParticipant esp: job --- src/Controllers/participant.controller.ts | 11 ++---- src/Controllers/user_auth.controller.ts | 41 +++++++++++++++++++---- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/src/Controllers/participant.controller.ts b/src/Controllers/participant.controller.ts index bc49308..d5696d1 100644 --- a/src/Controllers/participant.controller.ts +++ b/src/Controllers/participant.controller.ts @@ -12,13 +12,9 @@ const ParticipantBody = z.object({ firstName: z.string(), lastName: z.string(), groupId: z.string(), - job: z.enum(["TEAMLEADER", "MEMBER"]), + //job: z.enum(["TEAMLEADER", "MEMBER"]), }); -const updateParticipantBody = ParticipantBody.omit({ - job: true, -}).partial(); - const returnedParticipant = { pid: true, firstName: true, @@ -45,7 +41,6 @@ export const createParticipant = async (req: Request<{ pid: string}>, res: Respo firstname: DataType.STRING, lastName: DataType.STRING, groupId: DataType.UUID, - job: DataType.JOB, }) ); } @@ -58,7 +53,7 @@ export const createParticipant = async (req: Request<{ pid: string}>, res: Respo data: { firstName: body.firstName, lastName: body.lastName, - relevance: body.job, + relevance: "MEMBER", group: { connect: { pid: body.groupId } }, team: { connect: { pid } }, }, @@ -80,7 +75,7 @@ export const createParticipant = async (req: Request<{ pid: string}>, res: Respo export const updateParticipant = async (req: Request<{ pid: string }>, res: Response) => { //insert TeamleaderAuth - const result = updateParticipantBody.safeParse(req.body); + const result = ParticipantBody.safeParse(req.body); if(result.success === false){ return res.status(400).json( diff --git a/src/Controllers/user_auth.controller.ts b/src/Controllers/user_auth.controller.ts index e46879a..a8034eb 100644 --- a/src/Controllers/user_auth.controller.ts +++ b/src/Controllers/user_auth.controller.ts @@ -6,31 +6,60 @@ import { verificationMail } from "../lib/mail"; import { DataType, generateInvalidBodyError } from "./common"; import { generateTeamleaderJWT } from "../Middleware/auth/teamleaderAuth"; import { createRolesForTeam } from "./role.controller"; +import { z } from "zod"; + +const TeamBody = z.object({ + teamName: z.string(), + leaderEmail: z.string(), + disciplineId: z.string(), + partFirstName: z.string(), + partLastName: z.string(), + partGroupId: z.string(), +}) interface CreateTeamBody { - name: string; + teamName: string; leaderEmail: string; disciplineId: string; + partFirstName: string; + partLastName: string; + partGroupId: string; } export const register = async (req: Request<{}, {}, CreateTeamBody>, res: Response) => { - if (req.body.name == null || req.body.disciplineId == null || req.body.leaderEmail == null) { + + const result = TeamBody.safeParse(req.body); + + if (result.success === false) { res.status(400).json( generateInvalidBodyError({ - name: DataType.STRING, + teamName: DataType.STRING, leaderEmail: DataType.STRING, disciplineId: DataType.STRING, + partFirstName: DataType.STRING, + partLastName: DataType.STRING, + partGroupId: DataType.STRING, }) ); return; } + const body = result.data; + const team = await prisma.team.create({ data: { - leaderEmail: req.body.leaderEmail, - name: req.body.name, + leaderEmail: body.leaderEmail, + name: body.teamName, roles: undefined, - discipline: { connect: { pid: req.body.disciplineId } }, + discipline: { connect: { pid: body.disciplineId } }, + participants: { + create: { + firstName: body.partFirstName, + lastName: body.partLastName, + relevance: "TEAMLEADER", + group: { connect: { pid: body.partGroupId } }, + } + } }, select: { pid: true, From 57671d72b860e7d44af714fdbe92a7e73875f6f2 Mon Sep 17 00:00:00 2001 From: Laurin <60652077+Flexla54@users.noreply.github.com> Date: Sun, 29 May 2022 03:35:12 +0200 Subject: [PATCH 20/31] added updateRoleScore --- src/Controllers/role.controller.ts | 78 +++++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/src/Controllers/role.controller.ts b/src/Controllers/role.controller.ts index 54f2076..086343b 100644 --- a/src/Controllers/role.controller.ts +++ b/src/Controllers/role.controller.ts @@ -1,10 +1,10 @@ -import { Role } from "@prisma/client"; +import { Prisma, Role } from "@prisma/client"; import { Request, Response } from "express"; import { z } from "zod"; import prisma from "../lib/prisma"; import { requireLeaderOfTeam, requireResponsibleForParticipant } from "../Middleware/auth/teamleaderAuth"; import NotFoundError from "../Middleware/error/NotFoundError"; -import { DataType, generateInvalidBodyError } from "./common"; +import { createInsufficientPermissionsError, DataType, generateInvalidBodyError } from "./common"; require("express-async-errors"); @@ -94,6 +94,80 @@ export async function assignParticipantToRole(req: Request<{ pid: string }>, res }); } +export const updateRoleScore = async (req: Request<{ pid: string }, {}, { score: string }>, res: Response) => { + if (req.auth?.permission_level != "ELEVATED") { + res.status(403).json(createInsufficientPermissionsError()); + } + + const { score } = req.body; + + if(typeof score !== "string"){ + res.status(400).json(generateInvalidBodyError({ score: DataType.STRING })); + } + + const { pid } = req.params; + + + + try { + const role = await prisma.role.update({ + where: { pid }, + data: { score }, + select: { + pid: true, + score: true, + schema: { select: { + pid: true, + name: true, + } }, + participant: { select: { + pid: true, + firstName: true, + lastName: true, + } }, + team: { select: { + pid: true, + name: true, + }} + } + }); + + if (!role) { + throw new NotFoundError("event", pid); + } + + res.status(200).json({ + type: "success", + payload: { + role, + }, + }); + + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { + return res.status(500).json({ + type: "error", + payload: { + message: `Internal Server error occured. Try again later`, + }, + }); + } + if (e instanceof Prisma.PrismaClientUnknownRequestError) { + return res.status(500).json({ + type: "error", + payload: { + message: "Unknown error occurred with your request. Check if your parameters are correct", + schema: { + eventId: DataType.UUID, + }, + }, + }); + } + + throw e; + } +} + export async function deleteRolesFromTeam(teamPid: string){ await prisma.role.deleteMany({ where: { team: { pid: teamPid, } } From 7ae0c81bb8a9621d2d6564a2c4aa9132e808159c Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Sun, 29 May 2022 12:37:02 +0200 Subject: [PATCH 21/31] Fix errors and improve error handling + Review code + Fix error with braces --- src/Controllers/common.ts | 14 ++- src/Controllers/discipline.controller.ts | 136 ++++++++++------------ src/Controllers/role_schema.controller.ts | 1 + src/Routes/discipline.routes.ts | 2 + 4 files changed, 78 insertions(+), 75 deletions(-) diff --git a/src/Controllers/common.ts b/src/Controllers/common.ts index 7a6581c..0f76edc 100644 --- a/src/Controllers/common.ts +++ b/src/Controllers/common.ts @@ -1,6 +1,7 @@ import { AdminLevel, Prisma } from "@prisma/client"; import { PrismaClientKnownRequestError, PrismaClientUnknownRequestError } from "@prisma/client/runtime"; import { Request, Response } from "express"; +import { ZodError } from "zod"; import prisma from "../lib/prisma"; interface StringIndexedObject { @@ -33,7 +34,18 @@ interface Body { [k: string]: DataType; } -export function generateInvalidBodyError(body: Body) { +export function generateInvalidBodyError(body: Body, zodError?: ZodError) { + if (zodError instanceof ZodError) { + return { + type: "error", + payload: { + message: "The body of your request did not conform to the requirements", + errors: { body: zodError.format() }, + schema: { body }, + }, + }; + } + return { type: "error", payload: { diff --git a/src/Controllers/discipline.controller.ts b/src/Controllers/discipline.controller.ts index 731741d..40d5494 100644 --- a/src/Controllers/discipline.controller.ts +++ b/src/Controllers/discipline.controller.ts @@ -16,13 +16,19 @@ import { require("express-async-errors"); -const DisciplineBody = z.object({ - name: z.string(), +const InitialDisciplineBody = z.object({ + name: z.string().min(1), minTeamSize: z.number(), maxTeamSize: z.number(), -}) +}); -const updateDisciplineBody = DisciplineBody.partial(); +const disciplineRefiner = [ + (args: any) => (args.minTeamSize && args.maxTeamSize ? args.minTeamSize <= args.maxTeamSize : true), + { message: "The minTeamSize must be smaller or equal to the maxTeamSize" }, +] as const; + +const DisciplineBody = InitialDisciplineBody.refine(...disciplineRefiner); +const updateDisciplineBody = InitialDisciplineBody.partial().refine(...disciplineRefiner); const basicDiscipline = { pid: true, @@ -137,22 +143,21 @@ export const createDiscipline = async (req: Request<{ eventPid: string }, {}, Cr const result = DisciplineBody.safeParse(req.body); - if(result.success === false) { + if (result.success === false) { return res.status(400).json( - generateInvalidBodyError({ - name: DataType.STRING, - minTeamSize: DataType.NUMBER, - maxTeamSize: DataType.NUMBER, - }) + generateInvalidBodyError( + { + name: DataType.STRING, + minTeamSize: DataType.NUMBER, + maxTeamSize: DataType.NUMBER, + }, + result.error + ) ); } const { name, minTeamSize, maxTeamSize } = result.data; - if (!validateName(name)) { - return res.status(400).json(NAME_ERROR); - } - try { const discipline = await prisma.discipline.create({ data: { name, minTeamSize, maxTeamSize, event: { connect: { pid: req.params.eventPid } } }, @@ -168,72 +173,53 @@ export const createDiscipline = async (req: Request<{ eventPid: string }, {}, Cr } }; -export const updateDiscipline = async (req: Request<{ eventPid: string }, {}, CreateDisciplineBody>, res: Response) => { +// requires: auth(ELEVATED) +export const updateDiscipline = async (req: Request<{ pid: string }>, res: Response) => { if (req.auth?.permission_level !== "ELEVATED") { res.status(403).json(createInsufficientPermissionsError()); } - const result = updateDisciplineBody.safeParse(req.body); + const result = updateDisciplineBody.safeParse(req.body); // FIXME: Useres can currently use two requests to forgo min/max team size checking altogether - if(result.success === false){ - return res.status(400).json( - generateInvalidBodyError({ - name: DataType.STRING, - minTeamSize: DataType.NUMBER, - maxTeamSize: DataType.NUMBER, - }) - ); + if (result.success === false) { + return res.status(400).json( + generateInvalidBodyError( + { + name: DataType.STRING, + minTeamSize: DataType.NUMBER, + maxTeamSize: DataType.NUMBER, + }, + result.error + ) + ); + } + + const body = result.data; + const { pid } = req.params; + + try { + const discipline = await prisma.discipline.update({ + where: { pid }, + data: { + name: body.name, + minTeamSize: body.minTeamSize, + maxTeamSize: body.maxTeamSize, + }, + select: basicDiscipline, + }); + + res.status(200).json({ + type: "success", + payload: { discipline }, + }); + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") { + throw new NotFoundError("discipline", pid); } - const body = result.data; - const { eventPid } = req.params; - - try { - const discipline = await prisma.discipline.update({ - where: { pid: eventPid }, - data: { - name: body.name, - minTeamSize: body.minTeamSize, - maxTeamSize: body.maxTeamSize, - }, - select: basicDiscipline, - }); - - if(!discipline) { - throw new NotFoundError("discipline", eventPid); - } - - res.status(200).json({ - type: "success", - payload: discipline, - }); - - } catch (e) { - if (e instanceof Prisma.PrismaClientKnownRequestError) { - return res.status(500).json({ - type: "error", - payload: { - message: `Internal Server error occured. Try again later`, - }, - }); - } - if (e instanceof Prisma.PrismaClientUnknownRequestError) { - return res.status(500).json({ - type: "error", - payload: { - message: "Unknown error occurred with your request. Check if your parameters are correct", - schema: { - name: DataType.STRING, - minTeamSize: DataType.NUMBER, - maxTeamSize: DataType.NUMBER, - }, - }, - }); - } - - throw e; - } -} + throw e; + } +}; // requires: auth(ELEVATED) export const deleteDiscipline = async (req: Request<{ pid: string }>, res: Response) => { @@ -278,6 +264,8 @@ export const addVisual = async (req: Request, res: }, }); + // TODO: This does not work and should be updated in all addVisual-type code segments + // Reason: update throw a PrismaClientKnownRequestError with code P2025 if the record to update could not be found if (!discipline) { throw new NotFoundError("discipline", disciplinePid); } @@ -307,7 +295,7 @@ export const deleteVisual = async (req: Request, return res.status(204).end(); } catch (e) { if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { - throw new NotFoundError("discipline", disciplinePid); + throw new NotFoundError("discipline", disciplinePid); // Refer: Last todo; This is a correct example } throw e; diff --git a/src/Controllers/role_schema.controller.ts b/src/Controllers/role_schema.controller.ts index 2f0d0e5..dafcfe7 100644 --- a/src/Controllers/role_schema.controller.ts +++ b/src/Controllers/role_schema.controller.ts @@ -216,6 +216,7 @@ export const deleteRoleSchema = async (req: Request<{ pid: string }>, res: Respo throw e; } +} interface visualParams { schemaPid: string; diff --git a/src/Routes/discipline.routes.ts b/src/Routes/discipline.routes.ts index 2d97a8e..7aa0289 100644 --- a/src/Routes/discipline.routes.ts +++ b/src/Routes/discipline.routes.ts @@ -7,6 +7,7 @@ import { deleteVisual, getAllDisciplines, getDiscipline, + updateDiscipline, } from "../Controllers/discipline.controller"; import { requireAuthentication } from "../Middleware/auth/auth"; @@ -16,6 +17,7 @@ router.get("/", getAllDisciplines); // TODO: Optional auth router.get("/:pid", getDiscipline); +router.put("/:pid", requireAuthentication, updateDiscipline); router.delete<"/:pid", { pid: string }>("/:pid", requireAuthentication, deleteDiscipline); router.post<"/:disciplinePid/images", { disciplinePid: string }>( From c41bbe9ab4a183755baf4c6a86b0f1402b08be0a Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Sun, 29 May 2022 12:58:28 +0200 Subject: [PATCH 22/31] Add error handling and comment code --- src/Controllers/group.controllers.ts | 59 ++++++++--------------- src/Controllers/participant.controller.ts | 44 +++++------------ 2 files changed, 34 insertions(+), 69 deletions(-) diff --git a/src/Controllers/group.controllers.ts b/src/Controllers/group.controllers.ts index 012e6f8..eaea779 100644 --- a/src/Controllers/group.controllers.ts +++ b/src/Controllers/group.controllers.ts @@ -6,11 +6,13 @@ import prisma from "../lib/prisma"; import NotFoundError from "../Middleware/error/NotFoundError"; import { createInsufficientPermissionsError, DataType, generateError, generateInvalidBodyError, genericError, handleCreateByName } from "./common"; -const updateGroupBody = z.object({ - name: z.string(), - user_limit: z.number(), - level: z.number(), -}).partial(); +const updateGroupBody = z + .object({ + name: z.string().min(1), + user_limit: z.number().int().positive(), + level: z.number().int().nonnegative(), + }) + .partial(); const basicGroup = { pid: true, @@ -131,11 +133,14 @@ export const updateGroup = async (req: Request<{ pid: string }>, res: Response) if(result.success === false){ return res.status(400).json( - generateInvalidBodyError({ - name: DataType.STRING, - user_limit: DataType.NUMBER, - level: DataType.NUMBER, - }) + generateInvalidBodyError( + { + name: DataType.STRING, + user_limit: DataType.NUMBER, + level: DataType.NUMBER, + }, + result.error + ) ); } @@ -153,39 +158,17 @@ export const updateGroup = async (req: Request<{ pid: string }>, res: Response) select: basicGroup, }); - if(!group) { - throw new NotFoundError("group", pid); - } - res.status(200).json({ type: "success", - payload: group, + payload: { group }, }); } catch (e) { - if (e instanceof Prisma.PrismaClientKnownRequestError) { - return res.status(500).json({ - type: "error", - payload: { - message: `Internal Server error occured. Try again later`, - }, - }); - } - if (e instanceof Prisma.PrismaClientUnknownRequestError) { - return res.status(500).json({ - type: "error", - payload: { - message: "Unknown error occurred with your request. Check if your parameters are correct", - schema: { - name: DataType.STRING, - user_limit: DataType.NUMBER, - level: DataType.NUMBER, - }, - }, - }); - } + if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") { + throw new NotFoundError("group", pid) + } - throw e; + throw e; } } @@ -206,7 +189,7 @@ export const deleteGroup = async (req: Request, res: Res return res.status(204).end(); } catch (e) { if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { - return res.status(404).json(generateError(`The group with the ID ${pid} could not be found`)); + throw new NotFoundError("group", pid) } throw e; diff --git a/src/Controllers/participant.controller.ts b/src/Controllers/participant.controller.ts index d5696d1..8a1bb55 100644 --- a/src/Controllers/participant.controller.ts +++ b/src/Controllers/participant.controller.ts @@ -8,10 +8,13 @@ import NotFoundError from "../Middleware/error/NotFoundError"; //TODO: add TeamleaderAuthentification +// REVIEW: All this code should be able to be executed by the teamleader of the team the participant is in AND +// an admin the group of whom overlaps with the team AND an elevated admin + const ParticipantBody = z.object({ firstName: z.string(), lastName: z.string(), - groupId: z.string(), + groupId: z.string().uuid(), //job: z.enum(["TEAMLEADER", "MEMBER"]), }); @@ -30,6 +33,7 @@ const returnedParticipant = { } }, } as const; +// REVIEW: Location of this endpoints (/groups, /teams, /participants, ...?) export const createParticipant = async (req: Request<{ pid: string}>, res: Response) => { //insert TeamleaderAuth @@ -41,7 +45,7 @@ export const createParticipant = async (req: Request<{ pid: string}>, res: Respo firstname: DataType.STRING, lastName: DataType.STRING, groupId: DataType.UUID, - }) + }, result.error) ); } @@ -62,7 +66,7 @@ export const createParticipant = async (req: Request<{ pid: string}>, res: Respo return res.status(201).json({ type: "success", - payload: participant, + payload: { participant }, }); } catch (e) { if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { @@ -75,7 +79,7 @@ export const createParticipant = async (req: Request<{ pid: string}>, res: Respo export const updateParticipant = async (req: Request<{ pid: string }>, res: Response) => { //insert TeamleaderAuth - const result = ParticipantBody.safeParse(req.body); + const result = ParticipantBody.partial().safeParse(req.body); // Should be partial, right? if(result.success === false){ return res.status(400).json( @@ -83,7 +87,7 @@ export const updateParticipant = async (req: Request<{ pid: string }>, res: Resp firstname: DataType.STRING, lastName: DataType.STRING, groupId: DataType.UUID, - }) + }, result.error) ); } @@ -101,38 +105,16 @@ export const updateParticipant = async (req: Request<{ pid: string }>, res: Resp select: returnedParticipant, }); - if(!participant) { - throw new NotFoundError("participant", pid); - } - res.status(200).json({ type: "success", - payload: participant, + payload: { participant }, }); } catch (e) { - if (e instanceof Prisma.PrismaClientKnownRequestError) { - return res.status(500).json({ - type: "error", - payload: { - message: `Internal Server error occured. Try again later`, - }, - }); + if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") { + throw new NotFoundError("participant", pid) } - if (e instanceof Prisma.PrismaClientUnknownRequestError) { - return res.status(500).json({ - type: "error", - payload: { - message: "Unknown error occurred with your request. Check if your parameters are correct", - schema: { - firstname: DataType.STRING, - lastName: DataType.STRING, - groupId: DataType.UUID, - }, - }, - }); - } - + throw e; } } From 197fd9c654beb0b5a492f25e09db9b73c1b8b088 Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Sun, 29 May 2022 13:25:11 +0200 Subject: [PATCH 23/31] Improve validation and error handling + Check code and routes + Comment code --- src/Controllers/role.controller.ts | 31 +++---------------- src/Controllers/role_schema.controller.ts | 37 ++++++----------------- src/Controllers/user_auth.controller.ts | 22 +++++++------- src/Routes/role.routes.ts | 4 +-- 4 files changed, 27 insertions(+), 67 deletions(-) diff --git a/src/Controllers/role.controller.ts b/src/Controllers/role.controller.ts index 086343b..7995368 100644 --- a/src/Controllers/role.controller.ts +++ b/src/Controllers/role.controller.ts @@ -23,7 +23,7 @@ export async function createRolesForTeam(teamPid: string) { } const roles = await prisma.role.createMany({ - data: schemas.map((schema) => ({ schemaId: schema.id, score: "", teamId })), + data: schemas.map((schema) => ({ schemaId: schema.id, score: "", teamId })), // TODO: Use default score from schema? }); return roles.count; @@ -61,7 +61,7 @@ export async function assignParticipantToRole(req: Request<{ pid: string }>, res const zBody = AssignParticipantToRoleBody.safeParse(req.body); if (zBody.success === false) { - return res.status(400).json(generateInvalidBodyError({ participant: DataType.UUID })); + return res.status(400).json(generateInvalidBodyError({ participantPid: DataType.UUID }, zBody.error)); } const { participantPid } = zBody.data; @@ -83,6 +83,7 @@ export async function assignParticipantToRole(req: Request<{ pid: string }>, res }); } + // No error handling should be neccesary as the existence of the role and participant have already been checked above await prisma.role.update({ where: { pid: rolePid }, data: { participant: { connect: { pid: participantPid } } } }); return res.status(200).json({ @@ -107,8 +108,6 @@ export const updateRoleScore = async (req: Request<{ pid: string }, {}, { score: const { pid } = req.params; - - try { const role = await prisma.role.update({ where: { pid }, @@ -132,10 +131,6 @@ export const updateRoleScore = async (req: Request<{ pid: string }, {}, { score: } }); - if (!role) { - throw new NotFoundError("event", pid); - } - res.status(200).json({ type: "success", payload: { @@ -144,24 +139,8 @@ export const updateRoleScore = async (req: Request<{ pid: string }, {}, { score: }); } catch (e) { - if (e instanceof Prisma.PrismaClientKnownRequestError) { - return res.status(500).json({ - type: "error", - payload: { - message: `Internal Server error occured. Try again later`, - }, - }); - } - if (e instanceof Prisma.PrismaClientUnknownRequestError) { - return res.status(500).json({ - type: "error", - payload: { - message: "Unknown error occurred with your request. Check if your parameters are correct", - schema: { - eventId: DataType.UUID, - }, - }, - }); + if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") { + throw new NotFoundError("role", pid) } throw e; diff --git a/src/Controllers/role_schema.controller.ts b/src/Controllers/role_schema.controller.ts index dafcfe7..4b8adda 100644 --- a/src/Controllers/role_schema.controller.ts +++ b/src/Controllers/role_schema.controller.ts @@ -16,7 +16,7 @@ import { } from "./common"; const RoleSchemaBody = z.object({ - name: z.string(), + name: z.string().min(1), schema: z.string(), }); @@ -148,50 +148,31 @@ export const updateRoleSchema = async (req: Request<{ pid: string }>, res: Respo generateInvalidBodyError({ name: DataType.STRING, schema: DataType.RESULT_SCHEMA, - }) + }, result.error) ); } - const body = result.data; + const {name, schema} = result.data; + + const validatedSchema = parseSchema(schema); try { const schema = await prisma.roleSchema.update({ where: { pid }, data: { - name: body.name, - schema: body.schema, + name: name, + schema: validatedSchema, }, select: roleSchema, }); - if (!schema) { - throw new NotFoundError("schema", pid); - } - res.status(200).json({ type: "success", payload: schema, }); } catch (e) { - if (e instanceof Prisma.PrismaClientKnownRequestError) { - return res.status(500).json({ - type: "error", - payload: { - message: `Internal Server error occured. Try again later`, - }, - }); - } - if (e instanceof Prisma.PrismaClientUnknownRequestError) { - return res.status(500).json({ - type: "error", - payload: { - message: "Unknown error occurred with your request. Check if your parameters are correct", - schema: { - name: DataType.STRING, - schema: DataType.RESULT_SCHEMA, - }, - }, - }); + if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") { + throw new NotFoundError("roleSchema", pid) } throw e; diff --git a/src/Controllers/user_auth.controller.ts b/src/Controllers/user_auth.controller.ts index a8034eb..067addf 100644 --- a/src/Controllers/user_auth.controller.ts +++ b/src/Controllers/user_auth.controller.ts @@ -9,12 +9,12 @@ import { createRolesForTeam } from "./role.controller"; import { z } from "zod"; const TeamBody = z.object({ - teamName: z.string(), - leaderEmail: z.string(), - disciplineId: z.string(), - partFirstName: z.string(), - partLastName: z.string(), - partGroupId: z.string(), + teamName: z.string().min(1), + leaderEmail: z.string().email(), + disciplineId: z.string().uuid(), + partFirstName: z.string().min(1), + partLastName: z.string().min(1), + partGroupId: z.string().uuid(), }) interface CreateTeamBody { @@ -26,22 +26,22 @@ interface CreateTeamBody { partGroupId: string; } +// TODO: Some kind of auth (Teamleader probably) export const register = async (req: Request<{}, {}, CreateTeamBody>, res: Response) => { const result = TeamBody.safeParse(req.body); if (result.success === false) { - res.status(400).json( + return res.status(400).json( generateInvalidBodyError({ teamName: DataType.STRING, leaderEmail: DataType.STRING, - disciplineId: DataType.STRING, + disciplineId: DataType.UUID, partFirstName: DataType.STRING, partLastName: DataType.STRING, - partGroupId: DataType.STRING, - }) + partGroupId: DataType.UUID, + }, result.error) ); - return; } const body = result.data; diff --git a/src/Routes/role.routes.ts b/src/Routes/role.routes.ts index 0c91fd1..5bbd03e 100644 --- a/src/Routes/role.routes.ts +++ b/src/Routes/role.routes.ts @@ -3,7 +3,7 @@ import { assignParticipantToRole, getRolesForTeam } from "../Controllers/role.co const router = Express.Router(); -//TO DO: maybe transfer getRolesForTeam to team router +//TO DO: maybe transfer getRolesForTeam to team router -> Seconded router.get<"team/:teamPid/", { teamPid: string }>("team/:teamPid/", getRolesForTeam); -router.patch<"/:pid/", { pid: string }>("/:pid/", assignParticipantToRole) \ No newline at end of file +router.put<"/:pid/participant", { pid: string }>("/:pid/participant", assignParticipantToRole); From cd0cb001065aa25fba9b29bdcc292366920bab91 Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Sun, 29 May 2022 14:47:32 +0200 Subject: [PATCH 24/31] Add requireResponsibleForGroup + Update controllers --- src/Controllers/event.controller.ts | 2 +- src/Controllers/group.controllers.ts | 6 ++++-- src/Middleware/auth/auth.ts | 13 ++++++++++++- src/Routes/discipline.routes.ts | 2 +- src/Routes/group.routes.ts | 2 ++ 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/Controllers/event.controller.ts b/src/Controllers/event.controller.ts index 42706f3..e996351 100644 --- a/src/Controllers/event.controller.ts +++ b/src/Controllers/event.controller.ts @@ -164,7 +164,7 @@ export const updateEvent = async (req: Request<{ pid: string }>, res: Response) date: DataType.DATETIME, briefDescription: DataType.STRING, ["fullDescription?"]: DataType.STRING, - }) + }, result.error) ); } diff --git a/src/Controllers/group.controllers.ts b/src/Controllers/group.controllers.ts index eaea779..b902e95 100644 --- a/src/Controllers/group.controllers.ts +++ b/src/Controllers/group.controllers.ts @@ -3,6 +3,7 @@ import { PrismaClientKnownRequestError, PrismaClientUnknownRequestError } from " import { Request, Response } from "express"; import { z } from "zod"; import prisma from "../lib/prisma"; +import { requireResponsibleForGroup } from "../Middleware/auth/auth"; import NotFoundError from "../Middleware/error/NotFoundError"; import { createInsufficientPermissionsError, DataType, generateError, generateInvalidBodyError, genericError, handleCreateByName } from "./common"; @@ -126,9 +127,8 @@ export const createGroup = async (req: Request<{ organisationPid: string }, {}, ); }; +// requires: auth(STANDARD with GROUP permission) export const updateGroup = async (req: Request<{ pid: string }>, res: Response) => { - //insert TeamleaderAuth - const result = updateGroupBody.safeParse(req.body); if(result.success === false){ @@ -147,6 +147,8 @@ export const updateGroup = async (req: Request<{ pid: string }>, res: Response) const body = result.data; const { pid } = req.params; + requireResponsibleForGroup(req.auth, pid) + try { const group = await prisma.group.update({ where: { pid }, diff --git a/src/Middleware/auth/auth.ts b/src/Middleware/auth/auth.ts index 981c116..339b600 100644 --- a/src/Middleware/auth/auth.ts +++ b/src/Middleware/auth/auth.ts @@ -1,10 +1,11 @@ /// import { NextFunction, Request, Response } from "express"; -import { AuthJWTPayload } from "../../Controllers/admin_auth.controller"; +import { authenticateUser, AuthJWTPayload } from "../../Controllers/admin_auth.controller"; import { authClient } from "../../lib/redis"; import jwt, { JsonWebTokenError, JwtPayload } from "jsonwebtoken"; import prisma from "../../lib/prisma"; +import AuthError from "../error/AuthError"; const JWT_SECRET = process.env.JWT_SECRET; @@ -92,3 +93,13 @@ export const requireAuthentication = async (req: Request, res: Response, next: N next(); }; + +export function requireResponsibleForGroup(auth: AuthJWTPayload | undefined, groupPid: string) { + if (auth?.permission_level === "ELEVATED") { + return; + } + + if (!auth?.groups.includes(groupPid)) { + throw new AuthError("The provided authorization is not valid for the requested operation!"); + } +} diff --git a/src/Routes/discipline.routes.ts b/src/Routes/discipline.routes.ts index 7aa0289..50a2de9 100644 --- a/src/Routes/discipline.routes.ts +++ b/src/Routes/discipline.routes.ts @@ -17,7 +17,7 @@ router.get("/", getAllDisciplines); // TODO: Optional auth router.get("/:pid", getDiscipline); -router.put("/:pid", requireAuthentication, updateDiscipline); +router.patch("/:pid", requireAuthentication, updateDiscipline); router.delete<"/:pid", { pid: string }>("/:pid", requireAuthentication, deleteDiscipline); router.post<"/:disciplinePid/images", { disciplinePid: string }>( diff --git a/src/Routes/group.routes.ts b/src/Routes/group.routes.ts index 4ab9387..ae7e470 100644 --- a/src/Routes/group.routes.ts +++ b/src/Routes/group.routes.ts @@ -5,6 +5,7 @@ import { getAllGroups, getAllGroupsWithParam, getGroup, + updateGroup, } from "../Controllers/group.controllers"; import { requireAuthentication } from "../Middleware/auth/auth"; import organisationRouter from "./organisation.routes"; @@ -14,6 +15,7 @@ const router = express.Router(); router.get("/", getAllGroups); router.get("/:pid", getGroup); router.delete<"/:pid", { pid: string }>("/:pid", requireAuthentication, deleteGroup); +router.patch("/:pid", requireAuthentication, updateGroup); organisationRouter.get("/:organisationPid/groups", getAllGroupsWithParam); organisationRouter.post("/:organisationPid/groups", requireAuthentication, createGroup); From 7dc9dd155888e52f9031f61d2f227adac1e7b3d3 Mon Sep 17 00:00:00 2001 From: Laurin <60652077+Flexla54@users.noreply.github.com> Date: Mon, 30 May 2022 12:21:28 +0200 Subject: [PATCH 25/31] adding team router/cont and participant router --- src/Controllers/participant.controller.ts | 233 ++++++++++++---------- src/Controllers/team.controller.ts | 0 src/Controllers/user_auth.controller.ts | 28 +-- src/Routes/participant.routes.ts | 30 +++ src/Routes/team.routes.ts | 12 ++ 5 files changed, 186 insertions(+), 117 deletions(-) create mode 100644 src/Controllers/team.controller.ts create mode 100644 src/Routes/participant.routes.ts create mode 100644 src/Routes/team.routes.ts diff --git a/src/Controllers/participant.controller.ts b/src/Controllers/participant.controller.ts index 8a1bb55..cfc6e59 100644 --- a/src/Controllers/participant.controller.ts +++ b/src/Controllers/participant.controller.ts @@ -1,10 +1,17 @@ import prisma from "../lib/prisma"; import { z } from "zod"; import { Request, Response } from "express"; -import { createInsufficientPermissionsError, DataType, generateError, generateInvalidBodyError } from "./common"; +import { + AUTH_ERROR, + createInsufficientPermissionsError, + DataType, + generateError, + generateInvalidBodyError, +} from "./common"; import { Job, Prisma } from "@prisma/client"; import { PrismaClientKnownRequestError } from "@prisma/client/runtime"; import NotFoundError from "../Middleware/error/NotFoundError"; +import { requireResponsibleForGroup } from "../Middleware/auth/auth"; //TODO: add TeamleaderAuthentification @@ -12,127 +19,145 @@ import NotFoundError from "../Middleware/error/NotFoundError"; // an admin the group of whom overlaps with the team AND an elevated admin const ParticipantBody = z.object({ - firstName: z.string(), - lastName: z.string(), - groupId: z.string().uuid(), - //job: z.enum(["TEAMLEADER", "MEMBER"]), + firstName: z.string(), + lastName: z.string(), + groupId: z.string().uuid(), + //job: z.enum(["TEAMLEADER", "MEMBER"]), }); const returnedParticipant = { - pid: true, - firstName: true, - lastName: true, - relevance: true, - team: { select: { - pid: true, - name: true, - } }, - group: { select: { - pid: true, - name: true, - } }, + pid: true, + firstName: true, + lastName: true, + relevance: true, + team: { + select: { + pid: true, + name: true, + }, + }, + group: { + select: { + pid: true, + name: true, + }, + }, } as const; // REVIEW: Location of this endpoints (/groups, /teams, /participants, ...?) -export const createParticipant = async (req: Request<{ pid: string}>, res: Response) => { - //insert TeamleaderAuth +export const createParticipant = async (req: Request<{ pid: string }>, res: Response) => { + const result = ParticipantBody.safeParse(req.body); - const result = ParticipantBody.safeParse(req.body); + if (result.success === false) { + return res.status(400).json( + generateInvalidBodyError( + { + firstname: DataType.STRING, + lastName: DataType.STRING, + groupId: DataType.UUID, + }, + result.error + ) + ); + } + const body = result.data; + const { pid } = req.params; - if(result.success === false){ - return res.status(400).json( - generateInvalidBodyError({ - firstname: DataType.STRING, - lastName: DataType.STRING, - groupId: DataType.UUID, - }, result.error) - ); + /* + if (!req.auth?.isAuthenticated || req.teamleader?.team != pid) { + return res.status(500).json(AUTH_ERROR); + } + if (req.teamleader?.team != pid) { + return res.status(500).json(AUTH_ERROR); + } + requireResponsibleForGroup(req.auth, req.body.groupId); + */ + + try { + const participant = await prisma.participant.create({ + data: { + firstName: body.firstName, + lastName: body.lastName, + relevance: "MEMBER", + group: { connect: { pid: body.groupId } }, + team: { connect: { pid } }, + }, + select: returnedParticipant, + }); + + return res.status(201).json({ + type: "success", + payload: { participant }, + }); + } catch (e) { + if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { + return res + .status(404) + .json(generateError(`Could not link to team with ID '${pid}, or group with ID ${body.groupId}'`)); } - - const body = result.data; - const { pid } = req.params; - - try { - const participant = await prisma.participant.create({ - data: { - firstName: body.firstName, - lastName: body.lastName, - relevance: "MEMBER", - group: { connect: { pid: body.groupId } }, - team: { connect: { pid } }, - }, - select: returnedParticipant, - }); - - return res.status(201).json({ - type: "success", - payload: { participant }, - }); - } catch (e) { - if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { - return res.status(404).json(generateError(`Could not link to team with ID '${pid}, or group with ID ${body.groupId}'`)); - } - throw e; - } -} + throw e; + } +}; export const updateParticipant = async (req: Request<{ pid: string }>, res: Response) => { - //insert TeamleaderAuth + //insert TeamleaderAuth - const result = ParticipantBody.partial().safeParse(req.body); // Should be partial, right? + const result = ParticipantBody.partial().safeParse(req.body); // Should be partial, right? - if(result.success === false){ - return res.status(400).json( - generateInvalidBodyError({ - firstname: DataType.STRING, - lastName: DataType.STRING, - groupId: DataType.UUID, - }, result.error) - ); + if (result.success === false) { + return res.status(400).json( + generateInvalidBodyError( + { + firstname: DataType.STRING, + lastName: DataType.STRING, + groupId: DataType.UUID, + }, + result.error + ) + ); + } + + const body = result.data; + const { pid } = req.params; + + try { + const participant = await prisma.participant.update({ + where: { pid }, + data: { + firstName: body.firstName, + lastName: body.lastName, + group: { connect: { pid: body.groupId } }, + }, + select: returnedParticipant, + }); + + res.status(200).json({ + type: "success", + payload: { participant }, + }); + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") { + throw new NotFoundError("participant", pid); } - const body = result.data; - const { pid } = req.params; - - try { - const participant = await prisma.participant.update({ - where: { pid }, - data: { - firstName: body.firstName, - lastName: body.lastName, - group: { connect: { pid: body.groupId, } }, - }, - select: returnedParticipant, - }); - - res.status(200).json({ - type: "success", - payload: { participant }, - }); - - } catch (e) { - if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") { - throw new NotFoundError("participant", pid) - } - - throw e; - } -} + throw e; + } +}; export const deleteParticipant = async (req: Request<{ pid: string }>, res: Response) => { - //insert TeamleaderAuth + //insert TeamleaderAuth - const { pid } = req.params; + const { pid } = req.params; - try { - await prisma.participant.delete({ where: { pid } }); + try { + await prisma.participant.delete({ where: { pid } }); - return res.status(204).end(); - } catch (e) { - if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { - return res.status(404).json(generateError(`The participant with the ID ${pid} could not be found`)); - } - - throw e; + return res.status(204).end(); + } catch (e) { + if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { + return res.status(404).json(generateError(`The participant with the ID ${pid} could not be found`)); } -} + + throw e; + } +}; diff --git a/src/Controllers/team.controller.ts b/src/Controllers/team.controller.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/Controllers/user_auth.controller.ts b/src/Controllers/user_auth.controller.ts index 067addf..405bf97 100644 --- a/src/Controllers/user_auth.controller.ts +++ b/src/Controllers/user_auth.controller.ts @@ -15,7 +15,7 @@ const TeamBody = z.object({ partFirstName: z.string().min(1), partLastName: z.string().min(1), partGroupId: z.string().uuid(), -}) +}); interface CreateTeamBody { teamName: string; @@ -28,19 +28,21 @@ interface CreateTeamBody { // TODO: Some kind of auth (Teamleader probably) export const register = async (req: Request<{}, {}, CreateTeamBody>, res: Response) => { - const result = TeamBody.safeParse(req.body); if (result.success === false) { return res.status(400).json( - generateInvalidBodyError({ - teamName: DataType.STRING, - leaderEmail: DataType.STRING, - disciplineId: DataType.UUID, - partFirstName: DataType.STRING, - partLastName: DataType.STRING, - partGroupId: DataType.UUID, - }, result.error) + generateInvalidBodyError( + { + teamName: DataType.STRING, + leaderEmail: DataType.STRING, + disciplineId: DataType.UUID, + partFirstName: DataType.STRING, + partLastName: DataType.STRING, + partGroupId: DataType.UUID, + }, + result.error + ) ); } @@ -58,8 +60,8 @@ export const register = async (req: Request<{}, {}, CreateTeamBody>, res: Respon lastName: body.partLastName, relevance: "TEAMLEADER", group: { connect: { pid: body.partGroupId } }, - } - } + }, + }, }, select: { pid: true, @@ -68,7 +70,7 @@ export const register = async (req: Request<{}, {}, CreateTeamBody>, res: Respon }, }); - //To do: maybe use returned amount of created use? + //TODO: maybe use returned amount of created use? createRolesForTeam(team.pid); const usid = nanoid(); diff --git a/src/Routes/participant.routes.ts b/src/Routes/participant.routes.ts new file mode 100644 index 0000000..21937a2 --- /dev/null +++ b/src/Routes/participant.routes.ts @@ -0,0 +1,30 @@ +import express from "express"; +import teamRouter from "./team.routes"; +import { createParticipant, deleteParticipant, updateParticipant } from "../Controllers/participant.controller"; +import { requireAuthentication } from "../Middleware/auth/auth"; +import { requireTeamleaderAuthentication } from "../Middleware/auth/teamleaderAuth"; + +const router = express.Router(); + +teamRouter.post<"/:pid/participant/", { pid: string }>( + "/:pid/participant/", + requireAuthentication, + requireTeamleaderAuthentication, + createParticipant +); + +teamRouter.patch<"/:pid/participant/", { pid: string }>( + "/:pid/participant/", + requireAuthentication, + requireTeamleaderAuthentication, + updateParticipant +); + +teamRouter.delete<"/:pid/participant/", { pid: string }>( + "/:pid/participant/", + requireAuthentication, + requireTeamleaderAuthentication, + deleteParticipant +); + +export default router; diff --git a/src/Routes/team.routes.ts b/src/Routes/team.routes.ts new file mode 100644 index 0000000..c05b60c --- /dev/null +++ b/src/Routes/team.routes.ts @@ -0,0 +1,12 @@ +import express from "express"; +import { requireAuthentication } from "../Middleware/auth/auth"; +import { requireTeamleaderAuthentication } from "../Middleware/auth/teamleaderAuth"; +import { register } from "../Controllers/user_auth.controller"; + +const router = express.Router(); + +router.post("/", requireAuthentication, requireTeamleaderAuthentication, register); + +router.delete<"/:pid/", { pid: string }>("/:pid/", requireAuthentication, requireTeamleaderAuthentication, deleteTeam); + +export default router; From eb20ff4965a9a376d9e5317aa8c9f25924598cbb Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Mon, 30 May 2022 18:18:44 +0200 Subject: [PATCH 26/31] Refractor auth logic + Add possibility (_requireAdminAuthentication, _requireTeamleaderAuthentication) to make auth optional + Add possibility to combine multiple auth types into one function --- dev.sh | 1 + src/Middleware/auth/auth.ts | 203 ++++++++++++++++++-------- src/Middleware/auth/teamleaderAuth.ts | 92 +++++++----- 3 files changed, 194 insertions(+), 102 deletions(-) diff --git a/dev.sh b/dev.sh index 7067090..dcd55c1 100755 --- a/dev.sh +++ b/dev.sh @@ -52,6 +52,7 @@ if [ "$RECREATE" = true ]; then -e DATABASE_URL="postgresql://server:server@postgres:5432/management?schema=public" \ -e NODE_ENV="development" \ -e PORT="${D_PORT}" \ + -e JWT_SECRET="not_for_production" \ -p "${D_PORT}":"${D_PORT}" \ --entrypoint "/app/scripts/docker-entrypoint.dev.sh" \ node diff --git a/src/Middleware/auth/auth.ts b/src/Middleware/auth/auth.ts index 339b600..75e20c7 100644 --- a/src/Middleware/auth/auth.ts +++ b/src/Middleware/auth/auth.ts @@ -6,6 +6,9 @@ import { authClient } from "../../lib/redis"; import jwt, { JsonWebTokenError, JwtPayload } from "jsonwebtoken"; import prisma from "../../lib/prisma"; import AuthError from "../error/AuthError"; +import { TeamleaderJWTPayload, _requireTeamleaderAuthentication } from "./teamleaderAuth"; + +require("express-async-errors"); const JWT_SECRET = process.env.JWT_SECRET; @@ -13,38 +16,81 @@ export const verifyAuthorizationFormat = (authorization: string) => /^Bearer .+$ export const getBearerToken = (authorization: string) => authorization.slice(7); -export const requireAuthentication = async (req: Request, res: Response, next: NextFunction) => { - if (!JWT_SECRET) { - throw new Error("JWT_SECRET not set"); - } +const _requireAdminAuthentication = + (config: { optional?: Boolean; controlled?: Boolean } = { optional: false, controlled: false }) => + async (req: Request, res: Response, next: NextFunction) => { + if (!JWT_SECRET) { + throw new Error("JWT_SECRET not set"); + } - const { authorization } = req.headers; + const { authorization } = req.headers; - if (!authorization) { - return res.status(403).send({ - type: "error", - payload: { - message: "The requeset did not include the Authorization header", - }, - }); - } + if (!authorization) { + if (config.optional) { + return false; + } - if (!verifyAuthorizationFormat(authorization)) { - return res.status(400).send({ - type: "error", - payload: { - message: "Malformed Authorization header", - format: "Bearer ", - }, - }); - } + return res.status(403).send({ + type: "error", + payload: { + message: "The requeset did not include the Authorization header", + }, + }); + } - let token_payload_: string | JwtPayload; + if (!verifyAuthorizationFormat(authorization)) { + return res.status(400).send({ + type: "error", + payload: { + message: "Malformed Authorization header", + format: "Bearer ", + }, + }); + } - try { - token_payload_ = jwt.verify(getBearerToken(authorization), JWT_SECRET); - } catch (e) { - if (e instanceof JsonWebTokenError) { + let token_payload_: string | JwtPayload; + + try { + token_payload_ = jwt.verify(getBearerToken(authorization), JWT_SECRET); + } catch (e) { + if (e instanceof JsonWebTokenError) { + return res.status(403).json({ + type: "error", + payload: { + message: "Token could not be verified; It might be expired", + }, + }); + } + + throw e; + } + + const token_payload = token_payload_ as AuthJWTPayload; + + if (!token_payload.permission_level || !token_payload.pid || !token_payload.revision) { + if (typeof (token_payload as unknown as TeamleaderJWTPayload).team === "string") { + return false; + } + + throw new AuthError("The token did not include the required information!"); + } + + const { pid, revision } = token_payload; + + let db_revision = await authClient.get(pid); + + if (db_revision === null) { + // Load the revision ID from the main DB and cache it in redis + const user = await prisma.admin.findUnique({ where: { pid }, select: { revision: true } }); + + if (user) { + db_revision = user.revision.toISOString(); + + await authClient.set(pid, db_revision); + } + } + + if (revision !== db_revision || !revision || !db_revision) { return res.status(403).json({ type: "error", payload: { @@ -53,46 +99,79 @@ export const requireAuthentication = async (req: Request, res: Response, next: N }); } - throw e; - } + req.auth = { + isAuthenticated: true, + pid: token_payload.pid, + name: token_payload.name, + permission_level: token_payload.permission_level, + groups: token_payload.groups, + revision: token_payload.revision, + }; - const token_payload = token_payload_ as AuthJWTPayload; - - const { pid, revision } = token_payload; - - let db_revision = await authClient.get(pid); - - if (db_revision === null) { - // Load the revision ID from the main DB and cache it in redis - const user = await prisma.admin.findUnique({ where: { pid }, select: { revision: true } }); - - if (user) { - db_revision = user.revision.toISOString(); - - await authClient.set(pid, db_revision); + if (!config.controlled) { + next(); } - } - if (revision !== db_revision || !revision || !db_revision) { - return res.status(403).json({ - type: "error", - payload: { - message: "Token could not be verified; It might be expired", - }, - }); - } - - req.auth = { - isAuthenticated: true, - pid: token_payload.pid, - name: token_payload.name, - permission_level: token_payload.permission_level, - groups: token_payload.groups, - revision: token_payload.revision, + return true; }; - next(); -}; +export const requireAuthentication = _requireAdminAuthentication({ optional: false, controlled: false }); + +type AuthType = "admin" | "teamleader"; +interface AuthTypeConfig { + admin?: Boolean; + teamleader?: Boolean; +} + +interface AuthConfiguration { + type: AuthType | AuthTypeConfig; + + optional: Boolean; +} + +function getAuthTypes(type: AuthType | AuthTypeConfig): AuthType[] { + if (typeof type === "string") { + return [type]; + } + + return Object.entries(type) + .filter(([_, value]) => value) + .map(([key, _]) => key as AuthType); +} + +export const requireConfiguredAuthentication = + (config: AuthConfiguration = { optional: false, type: "admin" }) => + async (req: Request, res: Response, next: NextFunction) => { + const types = getAuthTypes(config.type); + const optional = config.optional; + + let adminFinished = false; + let teamleaderFinished = false; + + if (types.includes("admin")) { + adminFinished = Boolean(await _requireAdminAuthentication({ optional: true, controlled: true })(req, res, next)); + + if (adminFinished) { + return next(); + } + } + + if (types.includes("teamleader")) { + teamleaderFinished = Boolean( + _requireTeamleaderAuthentication({ optional: true, controlled: true })(req, res, next) + ); + + if (teamleaderFinished) { + return next(); + } + } + + if (!config.optional) { + throw new AuthError("No sufficient authorization was provided for this operation"); + } + + next(); + }; export function requireResponsibleForGroup(auth: AuthJWTPayload | undefined, groupPid: string) { if (auth?.permission_level === "ELEVATED") { diff --git a/src/Middleware/auth/teamleaderAuth.ts b/src/Middleware/auth/teamleaderAuth.ts index aece12c..61fc82e 100644 --- a/src/Middleware/auth/teamleaderAuth.ts +++ b/src/Middleware/auth/teamleaderAuth.ts @@ -23,55 +23,67 @@ export function generateTeamleaderJWT(teamleader: Team) { return jwt.sign(payload, JWT_SECRET, { expiresIn: "4 days" }); } -export async function requireTeamleaderAuthentication(req: Request, res: Response, next: NextFunction) { - if (!JWT_SECRET) { - throw new Error("JWT_SECRET not set"); - } +export const _requireTeamleaderAuthentication = + (config: { optional: Boolean; controlled: Boolean } = { optional: false, controlled: false }) => + (req: Request, res: Response, next: NextFunction) => { + if (!JWT_SECRET) { + throw new Error("JWT_SECRET not set"); + } - const { authorization } = req.headers; + const { authorization } = req.headers; - if (!authorization) { - return res.status(403).send({ - type: "error", - payload: { - message: - "The request did not include the Authorization header (Only the team leader can perform this operation)", - }, - }); - } + if (!authorization) { + if (config.optional) { + return false; + } - if (!verifyAuthorizationFormat(authorization)) { - return res.status(400).send({ - type: "error", - payload: { - message: "Malformed Authorization header", - format: "Bearer ", - }, - }); - } - - try { - const token_payload = jwt.verify(getBearerToken(authorization), JWT_SECRET) as TeamleaderJWTPayload; - - req.teamleader = { - isAuthenticated: true, - team: token_payload.team, - }; - - next(); - } catch (e) { - if (e instanceof JsonWebTokenError) { - return res.status(403).json({ + return res.status(403).send({ type: "error", payload: { - message: "Token could not be verified; It might be expired", + message: + "The request did not include the Authorization header (Only the team leader can perform this operation)", }, }); } - } - throw e; -} + if (!verifyAuthorizationFormat(authorization)) { + return res.status(400).send({ + type: "error", + payload: { + message: "Malformed Authorization header", + format: "Bearer ", + }, + }); + } + + try { + const token_payload = jwt.verify(getBearerToken(authorization), JWT_SECRET) as TeamleaderJWTPayload; + + req.teamleader = { + isAuthenticated: true, + team: token_payload.team, + }; + + if (!config.controlled) { + next(); + } + + return true; + } catch (e) { + if (e instanceof JsonWebTokenError) { + return res.status(403).json({ + type: "error", + payload: { + message: "Token could not be verified; It might be expired", + }, + }); + } + + throw e; + } + }; + +export const requireTeamleaderAuthentication = _requireTeamleaderAuthentication({ optional: false, controlled: false }); export function requireLeaderOfTeam(auth: TeamleaderJWTPayload | undefined, teamPid: string) { if (auth?.team !== teamPid) { From f80f5b493864231178da31be62f92da5ede5a8bf Mon Sep 17 00:00:00 2001 From: Stefan-5422 <32109571+Stefan-5422@users.noreply.github.com> Date: Mon, 30 May 2022 19:42:35 +0200 Subject: [PATCH 27/31] Remove leftover TODOs --- src/Controllers/user_auth.controller.ts | 1 - src/lib/mail.ts | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/Controllers/user_auth.controller.ts b/src/Controllers/user_auth.controller.ts index 405bf97..831a312 100644 --- a/src/Controllers/user_auth.controller.ts +++ b/src/Controllers/user_auth.controller.ts @@ -108,7 +108,6 @@ export const requestToken = async (req: Request, res: Response) => { 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.params || {}; diff --git a/src/lib/mail.ts b/src/lib/mail.ts index 86b83c8..eda1b55 100644 --- a/src/lib/mail.ts +++ b/src/lib/mail.ts @@ -5,7 +5,6 @@ import nodemailer from "nodemailer"; 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 }; @@ -67,7 +66,6 @@ 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: Set the verification link to the correct endpoint verificationLink = "https://" + (process.env.DOMAIN ?? "localhost:3000") + "/api/users/verify/" + verificationLink; const message = Handlebars.compile(raw); From aa6f0825ac6321591a9d8ac9ca4460839c6acc5a Mon Sep 17 00:00:00 2001 From: Stefan-5422 <32109571+Stefan-5422@users.noreply.github.com> Date: Mon, 30 May 2022 20:57:28 +0200 Subject: [PATCH 28/31] Safe teamLeader code as a cookie so the client can deal with it. --- src/Controllers/user_auth.controller.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Controllers/user_auth.controller.ts b/src/Controllers/user_auth.controller.ts index 831a312..4205a20 100644 --- a/src/Controllers/user_auth.controller.ts +++ b/src/Controllers/user_auth.controller.ts @@ -3,10 +3,10 @@ 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"; +import { createInsufficientPermissionsError, DataType, generateInvalidBodyError } from "./common"; +import { generateTeamleaderJWT, requireLeaderOfTeam } from "../Middleware/auth/teamleaderAuth"; import { createRolesForTeam } from "./role.controller"; -import { z } from "zod"; +import { any, z } from "zod"; const TeamBody = z.object({ teamName: z.string().min(1), @@ -142,5 +142,12 @@ export const verifyEmail = async (req: Request, res: Response) => { 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 + const token = generateTeamleaderJWT(team); + + res.cookie("teamLeaderToken", token, { + path: "/", + maxAge: 1000 * 60 * 60 * 24 * 4, + }); + + res.status(200).json({ type: "succes", payload: { token } }); //TODO: This needs to set a cookie or smth so that the client also gets this info }; From 6f730d121416ce99adb383446bf97a4f49b1dd12 Mon Sep 17 00:00:00 2001 From: Stefan-5422 <32109571+Stefan-5422@users.noreply.github.com> Date: Mon, 30 May 2022 20:58:16 +0200 Subject: [PATCH 29/31] Add team routes --- src/Controllers/team.controller.ts | 98 ++++++++++++++++++++++++++++++ src/Routes/team.routes.ts | 8 ++- src/app.ts | 3 + 3 files changed, 106 insertions(+), 3 deletions(-) diff --git a/src/Controllers/team.controller.ts b/src/Controllers/team.controller.ts index e69de29..1f2a5dd 100644 --- a/src/Controllers/team.controller.ts +++ b/src/Controllers/team.controller.ts @@ -0,0 +1,98 @@ +import { Request, Response } from "express"; +import prisma from "../lib/prisma"; +import { createInsufficientPermissionsError, DataType, generateInvalidBodyError } from "./common"; +import { requireLeaderOfTeam } from "../Middleware/auth/teamleaderAuth"; +import { z } from "zod"; + +const TeamBody = z.object({ + teamName: z.string().min(1), + leaderEmail: z.string().email(), + disciplineId: z.string().uuid(), + partFirstName: z.string().min(1), + partLastName: z.string().min(1), + partGroupId: z.string().uuid(), +}); + +interface CreateTeamBody { + teamName: string; + leaderEmail: string; + disciplineId: string; + partFirstName: string; + partLastName: string; + partGroupId: string; +} + +export const getTeams = async (req: Request, res: Response) => { + const teams = prisma.team.findMany({ select: { pid: true, name: true, disciplineId: true } }); + + res.status(200).json(teams); +}; + +export const getTeam = async (req: Request, res: Response) => { + const { pid } = req.params; + + const team = prisma.team.findUnique({ + where: { pid }, + select: { + disciplineId: true, + name: true, + pid: true, + }, + }); + + res.status(200).json(team); +}; + +export const updateTeam = async (req: Request, res: Response) => { + const result = TeamBody.merge(z.object({ pid: z.string().min(1) })) + .omit({ partGroupId: true, partFirstName: true, partLastName: true }) + .safeParse(req.body); + + if (result.success === false) { + return res.status(400).json( + generateInvalidBodyError( + { + teamName: DataType.STRING, + leaderEmail: DataType.STRING, + disciplineId: DataType.UUID, + }, + result.error + ) + ); + } + + const body = result.data; + + try { + requireLeaderOfTeam(req.teamleader, body.pid); + } catch { + return res.status(401).json(createInsufficientPermissionsError("STANDARD")); + } + + const team = prisma.team.update({ + where: { + pid: body.pid, + }, + data: { + name: body.teamName, + discipline: { connect: { pid: body.disciplineId } }, + leaderEmail: body.leaderEmail, + }, + }); + + res.status(204).json(team); +}; + +export const deleteTeam = async (req: Request, res: Response) => { + const { pid } = req.params; + + try { + requireLeaderOfTeam(req.teamleader, pid); + } catch { + return res.status(401).json(createInsufficientPermissionsError("STANDARD")); + } + + prisma.team.delete({ where: { pid } }); + + res.status(204).json("Welp its gone"); +}; diff --git a/src/Routes/team.routes.ts b/src/Routes/team.routes.ts index c05b60c..a12afb1 100644 --- a/src/Routes/team.routes.ts +++ b/src/Routes/team.routes.ts @@ -1,12 +1,14 @@ import express from "express"; -import { requireAuthentication } from "../Middleware/auth/auth"; +import { requireAuthentication, requireConfiguredAuthentication } from "../Middleware/auth/auth"; import { requireTeamleaderAuthentication } from "../Middleware/auth/teamleaderAuth"; -import { register } from "../Controllers/user_auth.controller"; +import { deleteTeam, getTeam, getTeams, updateTeam } from "../Controllers/team.controller"; const router = express.Router(); -router.post("/", requireAuthentication, requireTeamleaderAuthentication, register); +router.get("/", requireConfiguredAuthentication({ type: "admin", optional: false }), getTeams); +router.get("/:id", getTeam); +router.put("/", requireTeamleaderAuthentication, updateTeam); router.delete<"/:pid/", { pid: string }>("/:pid/", requireAuthentication, requireTeamleaderAuthentication, deleteTeam); export default router; diff --git a/src/app.ts b/src/app.ts index e65ac98..7a41c6c 100644 --- a/src/app.ts +++ b/src/app.ts @@ -14,6 +14,7 @@ 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 TeamRouter from "./Routes/team.routes"; import { notFoundHandler, rootHandler } from "./Middleware/error/defaultRoutes"; // Set up async error handling @@ -86,6 +87,8 @@ async function main() { app.use("/api/users", userRouter); + app.use("/api/teams", TeamRouter); + app.get("/", rootHandler); app.get("/api", rootHandler); From 04c3bc8f872f7266232003249b269cc9bf96edf0 Mon Sep 17 00:00:00 2001 From: Stefan-5422 <32109571+Stefan-5422@users.noreply.github.com> Date: Mon, 30 May 2022 20:58:40 +0200 Subject: [PATCH 30/31] Do some wacky stuff so that mails actually have a chance of working on our server --- src/lib/mail.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/mail.ts b/src/lib/mail.ts index eda1b55..c55fd8e 100644 --- a/src/lib/mail.ts +++ b/src/lib/mail.ts @@ -66,7 +66,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"); - verificationLink = "https://" + (process.env.DOMAIN ?? "localhost:3000") + "/api/users/verify/" + verificationLink; + verificationLink = + "https://" + ("api." + process.env.DOMAIN ?? "localhost:3000/api") + "/users/verify/" + verificationLink; const message = Handlebars.compile(raw); const data = { eventName, verificationLink }; From 5efb3597fd3861bc6c01efb1e90b656042197e5d Mon Sep 17 00:00:00 2001 From: stephan418 Date: Mon, 30 May 2022 19:59:00 +0000 Subject: [PATCH 31/31] [create-pull-request] push formatted files --- src/Controllers/event.controller.ts | 15 +++-- src/Controllers/group.controllers.ts | 76 ++++++++++++----------- src/Controllers/role.controller.ts | 43 +++++++------ src/Controllers/role_schema.controller.ts | 17 ++--- 4 files changed, 84 insertions(+), 67 deletions(-) diff --git a/src/Controllers/event.controller.ts b/src/Controllers/event.controller.ts index e996351..03d0426 100644 --- a/src/Controllers/event.controller.ts +++ b/src/Controllers/event.controller.ts @@ -159,12 +159,15 @@ export const updateEvent = async (req: Request<{ pid: string }>, res: Response) if (result.success === false) { return res.status(400).json( - generateInvalidBodyError({ - name: DataType.STRING, - date: DataType.DATETIME, - briefDescription: DataType.STRING, - ["fullDescription?"]: DataType.STRING, - }, result.error) + generateInvalidBodyError( + { + name: DataType.STRING, + date: DataType.DATETIME, + briefDescription: DataType.STRING, + ["fullDescription?"]: DataType.STRING, + }, + result.error + ) ); } diff --git a/src/Controllers/group.controllers.ts b/src/Controllers/group.controllers.ts index b902e95..686ed7a 100644 --- a/src/Controllers/group.controllers.ts +++ b/src/Controllers/group.controllers.ts @@ -5,7 +5,14 @@ import { z } from "zod"; import prisma from "../lib/prisma"; import { requireResponsibleForGroup } from "../Middleware/auth/auth"; import NotFoundError from "../Middleware/error/NotFoundError"; -import { createInsufficientPermissionsError, DataType, generateError, generateInvalidBodyError, genericError, handleCreateByName } from "./common"; +import { + createInsufficientPermissionsError, + DataType, + generateError, + generateInvalidBodyError, + genericError, + handleCreateByName, +} from "./common"; const updateGroupBody = z .object({ @@ -131,48 +138,47 @@ export const createGroup = async (req: Request<{ organisationPid: string }, {}, export const updateGroup = async (req: Request<{ pid: string }>, res: Response) => { const result = updateGroupBody.safeParse(req.body); - if(result.success === false){ - return res.status(400).json( - generateInvalidBodyError( - { - name: DataType.STRING, - user_limit: DataType.NUMBER, - level: DataType.NUMBER, - }, - result.error - ) - ); + if (result.success === false) { + return res.status(400).json( + generateInvalidBodyError( + { + name: DataType.STRING, + user_limit: DataType.NUMBER, + level: DataType.NUMBER, + }, + result.error + ) + ); } const body = result.data; const { pid } = req.params; - requireResponsibleForGroup(req.auth, pid) + requireResponsibleForGroup(req.auth, pid); try { - const group = await prisma.group.update({ - where: { pid }, - data: { - name: body.name, - user_limit: body.user_limit, - level: body.level, - }, - select: basicGroup, - }); - - res.status(200).json({ - type: "success", - payload: { group }, - }); - + const group = await prisma.group.update({ + where: { pid }, + data: { + name: body.name, + user_limit: body.user_limit, + level: body.level, + }, + select: basicGroup, + }); + + res.status(200).json({ + type: "success", + payload: { group }, + }); } catch (e) { - if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") { - throw new NotFoundError("group", pid) - } - - throw e; + if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") { + throw new NotFoundError("group", pid); + } + + throw e; } -} +}; interface DeleteGroupQueryParams { pid: string; @@ -191,7 +197,7 @@ export const deleteGroup = async (req: Request, res: Res return res.status(204).end(); } catch (e) { if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { - throw new NotFoundError("group", pid) + throw new NotFoundError("group", pid); } throw e; diff --git a/src/Controllers/role.controller.ts b/src/Controllers/role.controller.ts index 7995368..feff325 100644 --- a/src/Controllers/role.controller.ts +++ b/src/Controllers/role.controller.ts @@ -102,7 +102,7 @@ export const updateRoleScore = async (req: Request<{ pid: string }, {}, { score: const { score } = req.body; - if(typeof score !== "string"){ + if (typeof score !== "string") { res.status(400).json(generateInvalidBodyError({ score: DataType.STRING })); } @@ -115,40 +115,45 @@ export const updateRoleScore = async (req: Request<{ pid: string }, {}, { score: select: { pid: true, score: true, - schema: { select: { + schema: { + select: { pid: true, name: true, - } }, - participant: { select: { - pid: true, - firstName: true, - lastName: true, - } }, - team: { select: { - pid: true, - name: true, - }} - } + }, + }, + participant: { + select: { + pid: true, + firstName: true, + lastName: true, + }, + }, + team: { + select: { + pid: true, + name: true, + }, + }, + }, }); - + res.status(200).json({ type: "success", payload: { role, }, }); - } catch (e) { if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") { - throw new NotFoundError("role", pid) + throw new NotFoundError("role", pid); } throw e; } -} +}; -export async function deleteRolesFromTeam(teamPid: string){ +export async function deleteRolesFromTeam(teamPid: string) { await prisma.role.deleteMany({ - where: { team: { pid: teamPid, } } + where: { team: { pid: teamPid } }, }); } diff --git a/src/Controllers/role_schema.controller.ts b/src/Controllers/role_schema.controller.ts index 4b8adda..f47e154 100644 --- a/src/Controllers/role_schema.controller.ts +++ b/src/Controllers/role_schema.controller.ts @@ -145,14 +145,17 @@ export const updateRoleSchema = async (req: Request<{ pid: string }>, res: Respo if (result.success === false) { return res.status(400).json( - generateInvalidBodyError({ - name: DataType.STRING, - schema: DataType.RESULT_SCHEMA, - }, result.error) + generateInvalidBodyError( + { + name: DataType.STRING, + schema: DataType.RESULT_SCHEMA, + }, + result.error + ) ); } - const {name, schema} = result.data; + const { name, schema } = result.data; const validatedSchema = parseSchema(schema); @@ -172,7 +175,7 @@ export const updateRoleSchema = async (req: Request<{ pid: string }>, res: Respo }); } catch (e) { if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") { - throw new NotFoundError("roleSchema", pid) + throw new NotFoundError("roleSchema", pid); } throw e; @@ -197,7 +200,7 @@ export const deleteRoleSchema = async (req: Request<{ pid: string }>, res: Respo throw e; } -} +}; interface visualParams { schemaPid: string;