From 4555d46902a05bf355befb8cbfedfbbc2276ffaf Mon Sep 17 00:00:00 2001 From: Laurin <60652077+Flexla54@users.noreply.github.com> Date: Wed, 18 May 2022 12:16:47 +0200 Subject: [PATCH 01/55] modified event responses and updated discipline briefDescription + fullDescription --- prisma/schema.prisma | 14 +++-- src/Controllers/discipline.controller.ts | 15 ++++- src/Controllers/event.controller.ts | 71 ++++++++++++++++-------- 3 files changed, 67 insertions(+), 33 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9557922..9d1b73c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -34,11 +34,13 @@ model Admin { } model Discipline { - id Int @id @default(autoincrement()) - pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid - name String - minTeamSize Int - maxTeamSize Int + id Int @id @default(autoincrement()) + pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid + name String + briefDescription String + fullDescription String? + minTeamSize Int + maxTeamSize Int roles RoleSchema[] teams Team[] @@ -121,7 +123,7 @@ model Group { model Media { id Int @id @default(autoincrement()) pid String @unique - description String + description String @default("visual") events Event[] disciplines Discipline[] diff --git a/src/Controllers/discipline.controller.ts b/src/Controllers/discipline.controller.ts index de8abbd..163ba0a 100644 --- a/src/Controllers/discipline.controller.ts +++ b/src/Controllers/discipline.controller.ts @@ -21,6 +21,8 @@ const basicDiscipline = { visual: { select: { pid: true } }, maxTeamSize: true, minTeamSize: true, + briefDescription: true, + fullDescription: true, event: { select: { pid: true, name: true } }, roles: { select: { pid: true, name: true } }, } as const; @@ -117,6 +119,8 @@ interface CreateDisciplineBody { name?: string; minTeamSize?: number; maxTeamSize?: number; + briefDescription: string; + fullDescription: string; } // require: auth(ELEVATED) @@ -126,9 +130,14 @@ export const createDiscipline = async (req: Request<{ eventPid: string }, {}, Cr return res.status(403).json(createInsufficientPermissionsError()); } - const { name, minTeamSize, maxTeamSize } = req.body; + const { name, minTeamSize, maxTeamSize, briefDescription, fullDescription } = req.body; - if (typeof name !== "string" || typeof minTeamSize !== "number" || typeof maxTeamSize !== "number") { + if (typeof name !== "string" || + typeof minTeamSize !== "number" || + typeof maxTeamSize !== "number" || + typeof briefDescription !== "string" || + (fullDescription && typeof fullDescription !== "string") + ) { return res.status(400).json( generateInvalidBodyError({ name: DataType.STRING, @@ -144,7 +153,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 } } }, + data: { name, minTeamSize, maxTeamSize, briefDescription, fullDescription, event: { connect: { pid: req.params.eventPid } } }, select: { pid: true, name: true, diff --git a/src/Controllers/event.controller.ts b/src/Controllers/event.controller.ts index 3193c37..a2de88c 100644 --- a/src/Controllers/event.controller.ts +++ b/src/Controllers/event.controller.ts @@ -1,6 +1,6 @@ import { Prisma } from "@prisma/client"; import { PrismaClientKnownRequestError } from "@prisma/client/runtime"; -import { Request, Response } from "express"; +import e, { Request, Response } from "express"; import { z } from "zod"; import prisma from "../lib/prisma"; import NotFoundError from "../Middleware/error/NotFoundError"; @@ -8,16 +8,39 @@ import { createInsufficientPermissionsError, DataType, generateError, generateIn require("express-async-errors"); +const EventBody = z.object({ + name: z.string(), + date: z.string(), + briefDescription: z.string(), + fullDescription: z.string(), +}); + +const UpdateBody = EventBody.partial(); + +const CreateEventBody = EventBody.partial({ + fullDescription: true, +}); + export const getAllEvents = async (req: Request, res: Response) => { const events = await prisma.event.findMany({ select: { + pid: true, name: true, + date: true, briefDescription: true, fullDescription: true, visual: { select: { pid: true, description: true } }, - date: true, - pid: true, - id: false, + disciplines: { + select: { + pid: true, + name: true, + }}, + organisations: { + select: { + pid: true, + name: true, + } + } }, }); @@ -46,13 +69,25 @@ export const getEvent = async (req: Request, res: Response) => { pid: eventId, }, select: { + pid: true, name: true, + date: true, briefDescription: true, fullDescription: true, - date: true, - pid: true, - id: false, visual: { select: { pid: true, description: true } }, + disciplines: { + select: { + pid: true, + name: true, + briefDescription: true, + fullDescription: true, + }}, + organisations: { + select: { + pid: true, + name: true, + } + } }, }); @@ -96,12 +131,10 @@ export const addEvent = async (req: Request, res: Response) => { if (req.auth?.permission_level !== "ELEVATED") { res.status(403).json(createInsufficientPermissionsError()); } - if ( - typeof req.body.name !== "string" || - typeof req.body.date !== "string" || - typeof req.body.briefDescription !== "string" || - (req.body.fullDescription && typeof req.body.fullDescription !== "string") - ) { + + const result = CreateEventBody.safeParse(req.body); + + if(result.success === false){ return res.status(400).json( generateInvalidBodyError({ name: DataType.STRING, @@ -122,10 +155,9 @@ export const addEvent = async (req: Request, res: Response) => { fullDescription: req.body.fullDescription, }, select: { + pid: true, name: true, date: true, - pid: true, - id: false, briefDescription: true, fullDescription: true, }, @@ -139,15 +171,6 @@ export const addEvent = async (req: Request, res: Response) => { }); }; -const EventBody = z.object({ - name: z.string(), - date: z.string(), - briefDescription: z.string(), - fullDescription: z.string(), -}); - -const UpdateBody = EventBody.partial(); - export const updateEvent = async (req: Request<{ pid: string }>, res: Response) => { if (req.auth?.permission_level !== "ELEVATED") { res.status(403).json(createInsufficientPermissionsError()); From 4e77d3c66dafa09b90ea47a2eced981831ae2795 Mon Sep 17 00:00:00 2001 From: Laurin <60652077+Flexla54@users.noreply.github.com> Date: Sun, 22 May 2022 12:48:37 +0200 Subject: [PATCH 02/55] Event zod add pid/id; rm comented MediaRouter --- src/Controllers/event.controller.ts | 2 ++ src/app.ts | 3 --- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Controllers/event.controller.ts b/src/Controllers/event.controller.ts index a2de88c..7975165 100644 --- a/src/Controllers/event.controller.ts +++ b/src/Controllers/event.controller.ts @@ -9,6 +9,8 @@ import { createInsufficientPermissionsError, DataType, generateError, generateIn require("express-async-errors"); const EventBody = z.object({ + pid: z.string().max(0), + id: z.string().max(0), name: z.string(), date: z.string(), briefDescription: z.string(), diff --git a/src/app.ts b/src/app.ts index ae0c0ec..aed343f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -65,9 +65,6 @@ async function main() { // Error handling app.use(defaultErrorHandler); // This has to be the LAST ROUTE - // Disable the media router for now - // app.use("/api/media", mediaRouter); - app.listen(process.env.PORT, () => { logger.info(`Listening on port ${process.env.PORT}`); }); From cc57c513899684fb97178eb6c2453084b432f09b Mon Sep 17 00:00:00 2001 From: Laurin <60652077+Flexla54@users.noreply.github.com> Date: Tue, 24 May 2022 18:50:30 +0200 Subject: [PATCH 03/55] Added updateDiscipline --- src/Controllers/discipline.controller.ts | 94 +++++++++++++++++++++++- 1 file changed, 92 insertions(+), 2 deletions(-) diff --git a/src/Controllers/discipline.controller.ts b/src/Controllers/discipline.controller.ts index 163ba0a..f6ea601 100644 --- a/src/Controllers/discipline.controller.ts +++ b/src/Controllers/discipline.controller.ts @@ -4,6 +4,7 @@ import { Request, Response } from "express"; import prisma from "../lib/prisma"; import ForwardableError from "../Middleware/error/ForwardableError"; import NotFoundError from "../Middleware/error/NotFoundError"; +import { number, z } from "zod"; import { createInsufficientPermissionsError, DataType, @@ -15,6 +16,17 @@ import { require("express-async-errors"); +const DisciplineBody = z.object({ + name: z.string(), + maxTeamSize: z.number(), + minTeamSize: z.number(), + briefDescription: z.string(), + fullDescription: z.string(), + eventPid: z.string(), +}) + +const UpdateDisciplineBody = DisciplineBody.partial(); + const basicDiscipline = { pid: true, name: true, @@ -115,12 +127,90 @@ export const getDiscipline = async (req: Request, res: }); }; +export const updateDiscipline = async (req: Request<{ pid: string }>, res: Response) => { + if (req.auth?.permission_level !== "ELEVATED") { + res.status(403).json(createInsufficientPermissionsError()); + } + + const { pid } = req.params; + + 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, + briefDescription: DataType.STRING, + ["fullDescription?"]: DataType.STRING, + }) + ); + } + + const body = result.data; + + try { + const discipline = await prisma.discipline.update({ + where: { pid }, + data: { + name: body.name, + minTeamSize: body.minTeamSize, + maxTeamSize: body.maxTeamSize, + briefDescription: body.briefDescription, + fullDescription: body.fullDescription, + }, + select: { + pid: true, + name: true, + minTeamSize: true, + maxTeamSize: true, + briefDescription: true, + fullDescription: true, + }, + }); + + if (!discipline) { + throw new NotFoundError("discipline", pid); + } + + 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: { + eventId: DataType.UUID, + }, + }, + }); + } + + throw e; + } +}; + interface CreateDisciplineBody { name?: string; minTeamSize?: number; maxTeamSize?: number; - briefDescription: string; - fullDescription: string; + briefDescription?: string; + fullDescription?: string; } // require: auth(ELEVATED) 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/55] 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/55] 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/55] 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 4f94bf074c8cc5b21e3c2bfd9d936e12535ddf84 Mon Sep 17 00:00:00 2001 From: Laurin <60652077+Flexla54@users.noreply.github.com> Date: Sat, 28 May 2022 15:50:05 +0200 Subject: [PATCH 07/55] created Role controller/routes --- src/Controllers/role.controller.ts | 0 src/Routes/team.routes.ts | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/Controllers/role.controller.ts create mode 100644 src/Routes/team.routes.ts diff --git a/src/Controllers/role.controller.ts b/src/Controllers/role.controller.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/Routes/team.routes.ts b/src/Routes/team.routes.ts new file mode 100644 index 0000000..e69de29 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 08/55] 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 09/55] 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 10/55] 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 11/55] 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 12/55] 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 13/55] 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 14/55] 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 15/55] 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 16/55] 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 17/55] 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 18/55] 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 19/55] 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 20/55] 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 21/55] 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 22/55] 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 23/55] 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 24/55] 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 25/55] 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 1cb345c316a3a5ed18cdcf8892cdfe2e600aec2a Mon Sep 17 00:00:00 2001 From: Laurin <60652077+Flexla54@users.noreply.github.com> Date: Sun, 29 May 2022 17:15:12 +0200 Subject: [PATCH 26/55] de-duplicated (un-)linkMedia --- src/Controllers/discipline.controller.ts | 58 -------------- src/Controllers/event.controller.ts | 64 --------------- src/Controllers/media.controller.ts | 95 +++++++++++++++++++++++ src/Controllers/role_schema.controller.ts | 58 -------------- src/Routes/discipline.routes.ts | 14 ---- src/Routes/event.routes.ts | 10 --- src/Routes/media.routes.ts | 26 ++++++- src/Routes/role_schema.routes.ts | 10 --- 8 files changed, 120 insertions(+), 215 deletions(-) diff --git a/src/Controllers/discipline.controller.ts b/src/Controllers/discipline.controller.ts index f6ea601..25bd7ae 100644 --- a/src/Controllers/discipline.controller.ts +++ b/src/Controllers/discipline.controller.ts @@ -286,61 +286,3 @@ export const deleteDiscipline = async (req: Request throw e; } }; - -interface visualParams { - disciplinePid: string; -} - -interface visualBody { - mediaPid: string; -} - -export const addVisual = async (req: Request, res: Response) => { - if (req.auth?.permission_level != "ELEVATED") { - res.status(403).json(createInsufficientPermissionsError()); - } - - const { disciplinePid } = req.params; - - const discipline = await prisma.discipline.update({ - where: { pid: disciplinePid }, - data: { - visual: { connect: { pid: req.body.mediaPid } }, - }, - }); - - if (!discipline) { - throw new NotFoundError("discipline", disciplinePid); - } - - return res.status(200).json({ - type: "success", - payload: {}, - }); -}; - -export const deleteVisual = async (req: Request, res: Response) => { - if (req.auth?.permission_level != "ELEVATED") { - res.status(403).json(createInsufficientPermissionsError()); - } - - const { disciplinePid, pid } = req.params; - - try { - await prisma.discipline.update({ - where: { - pid: disciplinePid, - }, - data: { - visual: { disconnect: { pid } }, - }, - }); - return res.status(204).end(); - } catch (e) { - if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { - throw new NotFoundError("discipline", disciplinePid); - } - - throw e; - } -}; diff --git a/src/Controllers/event.controller.ts b/src/Controllers/event.controller.ts index 7975165..70cdc57 100644 --- a/src/Controllers/event.controller.ts +++ b/src/Controllers/event.controller.ts @@ -272,67 +272,3 @@ export const deleteEvent = async (req: Request, res: Res throw e; } }; - -// REVIEW: This code **will** need to be de-duplicated - -interface visualParams { - eventPid: string; -} - -interface visualBody { - mediaPid: string; -} - -export const addVisual = async (req: Request, res: Response) => { - if (req.auth?.permission_level != "ELEVATED") { - res.status(403).json(createInsufficientPermissionsError()); - } - - const { eventPid } = req.params; - - if (typeof req.body.mediaPid !== "string") { - res.status(400).json(generateInvalidBodyError({ mediaPid: DataType.STRING })); - } - - const event = await prisma.event.update({ - where: { pid: eventPid }, - data: { - visual: { connect: { pid: req.body.mediaPid } }, - }, - }); - - if (!event) { - throw new NotFoundError("event", eventPid); - } - - return res.status(200).json({ - type: "success", - payload: {}, - }); -}; - -export const deleteVisual = async (req: Request, res: Response) => { - if (req.auth?.permission_level != "ELEVATED") { - res.status(403).json(createInsufficientPermissionsError()); - } - - const { eventPid, pid } = req.params; - - try { - await prisma.event.update({ - where: { - pid: eventPid, - }, - data: { - visual: { disconnect: { pid } }, - }, - }); - return res.status(204).end(); - } catch (e) { - if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { - throw new NotFoundError("discipline", eventPid); - } - - throw e; - } -}; diff --git a/src/Controllers/media.controller.ts b/src/Controllers/media.controller.ts index eb78bc3..83a34c3 100644 --- a/src/Controllers/media.controller.ts +++ b/src/Controllers/media.controller.ts @@ -12,9 +12,18 @@ import { type } from "os"; import { unlink } from "fs/promises"; import ForwardableError from "../Middleware/error/ForwardableError"; import SchemaError from "../Middleware/error/SchemaError"; +import { z } from "zod"; +import { updateEvent } from "./event.controller"; require("express-async-errors"); +const linkMediaBody = z.object({ + tableToUpdate: z.enum(["EVENT", "ROLE_SCHEMA", "DISCIPLINE"]), + mediaPid: z.string(), +}); + +const unlinkMediaBody = linkMediaBody.omit({ mediaPid: true, }); + function createMediaLinks(fileName: string) { return [{ rel: "self", type: "GET", href: `/api/media/${fileName}` }]; } @@ -189,3 +198,89 @@ export const deleteMedia = async (req: Request, res: Response) => { throw e; } }; + +//TODO: maybe create a function that adds the tableToUpdate based on path +// and call it before calling (un)linkMedia + +export const linkMedia = async ( + req: Request<{ pid: string }, {}, { mediaPid: string, tableToUpdate: string }>, + res: Response) => { + if (req.auth?.permission_level != "ELEVATED") { + res.status(403).json(createInsufficientPermissionsError()); + } + + const zBody = linkMediaBody.safeParse(req.body); + + if(zBody.success === false) { + return res.status(400).json( + generateInvalidBodyError({ + mediaPid: DataType.UUID, + tableToUpdate: DataType.STRING, + }) + ); + } + + const { pid } = req.params; + const { mediaPid, tableToUpdate } = zBody.data; + + const updatedRec = await getPrismaUpdateFKT(tableToUpdate)({ + where: { pid }, + data: { + visual: { connect: { pid: mediaPid } }, + }, + }); + + if (!updatedRec) { + throw new NotFoundError(tableToUpdate, pid); + } + + return res.status(200).json({ + type: "success", + payload: {}, + }); +}; + +export const unlinkMedia = async ( + req: Request<{ pid: string, mediaPid: string }, {}, { tableToUpdate: string }>, + res: Response) => { + if (req.auth?.permission_level != "ELEVATED") { + res.status(403).json(createInsufficientPermissionsError()); + } + + const zBody = linkMediaBody.safeParse(req.body); + + if(zBody.success === false) { + return res.status(400).json( + generateInvalidBodyError({ + mediaPid: DataType.UUID, + tableToUpdate: DataType.STRING, + }) + ); + } + + const { pid } = req.params; + const { mediaPid, tableToUpdate } = zBody.data; + + try { + await getPrismaUpdateFKT(tableToUpdate)({ + where: pid, + data: { visual: { disconnect: { pid: mediaPid } }, + }, + }); + return res.status(204).end(); + } catch (e) { + if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { + throw new NotFoundError(tableToUpdate, pid); + } + + throw e; + } +}; + +function getPrismaUpdateFKT( tableToUpdate: string ): Function { + switch(tableToUpdate) { + case "EVENT": return prisma.event.update; + case "DISCIPLINE": return prisma.discipline.update; + default: return prisma.roleSchema.update; + } +} diff --git a/src/Controllers/role_schema.controller.ts b/src/Controllers/role_schema.controller.ts index 36265de..78951d0 100644 --- a/src/Controllers/role_schema.controller.ts +++ b/src/Controllers/role_schema.controller.ts @@ -123,61 +123,3 @@ export const createRoleSchema = async ( throw e; } }; - -interface visualParams { - schemaPid: string; -} - -interface visualBody { - mediaPid: string; -} - -export const addVisual = async (req: Request, res: Response) => { - if (req.auth?.permission_level != "ELEVATED") { - res.status(403).json(createInsufficientPermissionsError()); - } - - const { schemaPid } = req.params; - - const schema = await prisma.roleSchema.update({ - where: { pid: schemaPid }, - data: { - visual: { connect: { pid: req.body.mediaPid } }, - }, - }); - - if (!schema) { - throw new NotFoundError("role_schema", schemaPid); - } - - return res.status(200).json({ - type: "success", - payload: {}, - }); -}; - -export const deleteVisual = async (req: Request, res: Response) => { - if (req.auth?.permission_level != "ELEVATED") { - res.status(403).json(createInsufficientPermissionsError()); - } - - const { schemaPid, pid } = req.params; - - try { - await prisma.roleSchema.update({ - where: { - pid: schemaPid, - }, - data: { - visual: { disconnect: { pid } }, - }, - }); - return res.status(204).end(); - } catch (e) { - if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { - throw new NotFoundError("role_schema", schemaPid); - } - - throw e; - } -}; diff --git a/src/Routes/discipline.routes.ts b/src/Routes/discipline.routes.ts index 2d97a8e..288cc30 100644 --- a/src/Routes/discipline.routes.ts +++ b/src/Routes/discipline.routes.ts @@ -1,10 +1,8 @@ import express from "express"; import eventRouter from "./event.routes"; import { - addVisual, createDiscipline, deleteDiscipline, - deleteVisual, getAllDisciplines, getDiscipline, } from "../Controllers/discipline.controller"; @@ -18,18 +16,6 @@ router.get("/:pid", getDiscipline); router.delete<"/:pid", { pid: string }>("/:pid", requireAuthentication, deleteDiscipline); -router.post<"/:disciplinePid/images", { disciplinePid: string }>( - "/:disciplinePid/images", - requireAuthentication, - addVisual -); - -router.delete<"/:disciplinePid/images/:pid", { disciplinePid: string; pid: string }>( - "/:disciplinePid/images/:pid", - requireAuthentication, - deleteVisual -); - eventRouter.post("/:eventPid/disciplines", requireAuthentication, createDiscipline); export default router; diff --git a/src/Routes/event.routes.ts b/src/Routes/event.routes.ts index 5e68bd8..7c2d34d 100644 --- a/src/Routes/event.routes.ts +++ b/src/Routes/event.routes.ts @@ -2,9 +2,7 @@ import Express from "express"; import { string } from "zod"; import { addEvent, - addVisual, deleteEvent, - deleteVisual, getAllEvents, getEvent, updateEvent, @@ -22,12 +20,4 @@ router.patch<"/:pid/", { pid: string }>("/:pid/", requireAuthentication, updateE router.delete<"/:pid/", { pid: string }>("/:pid/", requireAuthentication, deleteEvent); -router.post<"/:eventPid/media", { eventPid: string }>("/:eventPid/media", requireAuthentication, addVisual); - -router.delete<"/:eventPid/media/:pid", { eventPid: string; pid: string }>( - "/:eventPid/media/:pid", - requireAuthentication, - deleteVisual -); - export default router; diff --git a/src/Routes/media.routes.ts b/src/Routes/media.routes.ts index f340de1..e4c633a 100644 --- a/src/Routes/media.routes.ts +++ b/src/Routes/media.routes.ts @@ -1,7 +1,7 @@ import express from "express"; import fileUpload from "express-fileupload"; import { requireAuthentication } from "../Middleware/auth/auth"; -import { deleteMedia, getAllMedia, getMediaMeta, uploadImage } from "../Controllers/media.controller"; +import { deleteMedia, getAllMedia, getMediaMeta, linkMedia, unlinkMedia, uploadImage } from "../Controllers/media.controller"; import eventRouter from "./event.routes"; import disciplineRouter from "./discipline.routes"; import roleSchemaRouter from "./role_schema.routes"; @@ -19,4 +19,28 @@ router.get("/:pid/meta", getMediaMeta); router.delete("/:pid", requireAuthentication, deleteMedia); +eventRouter.post<"/:pid/media", { pid: string }>( + "/:pid/media", requireAuthentication, linkMedia +); + +eventRouter.delete<"/:pid/media/:mediaPid", { pid: string, mediaPid: string }>( + "/:pid/media/:mediaPid", requireAuthentication, unlinkMedia +); + +disciplineRouter.post<"/:pid/media", { pid: string }>( + "/:pid/media", requireAuthentication, linkMedia +); + +disciplineRouter.delete<"/:pid/media/:mediaPid", { pid: string, mediaPid: string }>( + "/:pid/media/:mediaPid", requireAuthentication, unlinkMedia +); + +roleSchemaRouter.post<"/:pid/media", { pid: string }>( + "/:pid/media", requireAuthentication, linkMedia +); + +roleSchemaRouter.delete<"/:pid/media/:mediaPid", { pid: string, mediaPid: string }>( + "/:pid/media/:mediaPid", requireAuthentication, unlinkMedia +); + export default router; diff --git a/src/Routes/role_schema.routes.ts b/src/Routes/role_schema.routes.ts index f6ada5f..688dafc 100644 --- a/src/Routes/role_schema.routes.ts +++ b/src/Routes/role_schema.routes.ts @@ -1,9 +1,7 @@ import express from "express"; import disciplineRouter from "./discipline.routes"; import { - addVisual, createRoleSchema, - deleteVisual, getAllRoleSchemas, getAllRoleSchemasWithParam, getRoleSchema, @@ -20,12 +18,4 @@ disciplineRouter.get("/:disciplinePid/role-schemas", getAllRoleSchemasWithParam) disciplineRouter.post("/:disciplinePid/role-schemas", requireAuthentication, createRoleSchema); -router.post<"/:schemaPid/images", { schemaPid: string }>("/:schemaPid/images", requireAuthentication, addVisual); - -router.delete<"/:schemaPid/images/:pid", { schemaPid: string; pid: string }>( - "/:schemaPid/images/:pid", - requireAuthentication, - deleteVisual -); - export default router; From ba1d09d1813cfa4dccf63d715ac4aa70f59c5d78 Mon Sep 17 00:00:00 2001 From: Laurin <60652077+Flexla54@users.noreply.github.com> Date: Sun, 29 May 2022 21:08:40 +0200 Subject: [PATCH 27/55] media bugfixes --- src/Controllers/event.controller.ts | 2 - src/Controllers/media.controller.ts | 67 +++++++++++++---------------- 2 files changed, 29 insertions(+), 40 deletions(-) diff --git a/src/Controllers/event.controller.ts b/src/Controllers/event.controller.ts index 70cdc57..9480148 100644 --- a/src/Controllers/event.controller.ts +++ b/src/Controllers/event.controller.ts @@ -9,8 +9,6 @@ import { createInsufficientPermissionsError, DataType, generateError, generateIn require("express-async-errors"); const EventBody = z.object({ - pid: z.string().max(0), - id: z.string().max(0), name: z.string(), date: z.string(), briefDescription: z.string(), diff --git a/src/Controllers/media.controller.ts b/src/Controllers/media.controller.ts index 83a34c3..22802a7 100644 --- a/src/Controllers/media.controller.ts +++ b/src/Controllers/media.controller.ts @@ -1,4 +1,4 @@ -import { Request, response, Response } from "express"; +import { NextFunction, Request, response, Response } from "express"; import fs from "fs"; import isSvg from "is-svg"; import { fromBuffer as fileTypeFromBuffer } from "file-type"; @@ -17,13 +17,6 @@ import { updateEvent } from "./event.controller"; require("express-async-errors"); -const linkMediaBody = z.object({ - tableToUpdate: z.enum(["EVENT", "ROLE_SCHEMA", "DISCIPLINE"]), - mediaPid: z.string(), -}); - -const unlinkMediaBody = linkMediaBody.omit({ mediaPid: true, }); - function createMediaLinks(fileName: string) { return [{ rel: "self", type: "GET", href: `/api/media/${fileName}` }]; } @@ -203,27 +196,25 @@ export const deleteMedia = async (req: Request, res: Response) => { // and call it before calling (un)linkMedia export const linkMedia = async ( - req: Request<{ pid: string }, {}, { mediaPid: string, tableToUpdate: string }>, + req: Request<{ pid: string }, {}, { mediaPid: string }>, res: Response) => { if (req.auth?.permission_level != "ELEVATED") { res.status(403).json(createInsufficientPermissionsError()); } - const zBody = linkMediaBody.safeParse(req.body); + const { pid } = req.params; + const { mediaPid } = req.body; + const tableToUpdate = req.originalUrl.split("/"); - if(zBody.success === false) { + if(typeof mediaPid !== "string") { return res.status(400).json( generateInvalidBodyError({ mediaPid: DataType.UUID, - tableToUpdate: DataType.STRING, }) ); } - const { pid } = req.params; - const { mediaPid, tableToUpdate } = zBody.data; - - const updatedRec = await getPrismaUpdateFKT(tableToUpdate)({ + const updatedRec = await getPrismaUpdateFKT(tableToUpdate[2])({ where: { pid }, data: { visual: { connect: { pid: mediaPid } }, @@ -231,7 +222,7 @@ export const linkMedia = async ( }); if (!updatedRec) { - throw new NotFoundError(tableToUpdate, pid); + throw new NotFoundError(tableToUpdate[2], pid); } return res.status(200).json({ @@ -241,36 +232,36 @@ export const linkMedia = async ( }; export const unlinkMedia = async ( - req: Request<{ pid: string, mediaPid: string }, {}, { tableToUpdate: string }>, + req: Request<{ pid: string, mediaPid: string }>, res: Response) => { if (req.auth?.permission_level != "ELEVATED") { res.status(403).json(createInsufficientPermissionsError()); } - const zBody = linkMediaBody.safeParse(req.body); - - if(zBody.success === false) { - return res.status(400).json( - generateInvalidBodyError({ - mediaPid: DataType.UUID, - tableToUpdate: DataType.STRING, - }) - ); - } - - const { pid } = req.params; - const { mediaPid, tableToUpdate } = zBody.data; + const { pid, mediaPid } = req.params; + const tableToUpdate = req.originalUrl.split("/"); try { - await getPrismaUpdateFKT(tableToUpdate)({ - where: pid, - data: { visual: { disconnect: { pid: mediaPid } }, - }, + await getPrismaUpdateFKT(tableToUpdate[2])({ + where: { pid }, + data: { visual: { disconnect: { pid: mediaPid } } }, }); return res.status(204).end(); } catch (e) { if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { - throw new NotFoundError(tableToUpdate, pid); + console.log("not found error"); + throw new NotFoundError(tableToUpdate[2], 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: { + eventId: DataType.UUID, + }, + }, + }); } throw e; @@ -279,8 +270,8 @@ export const unlinkMedia = async ( function getPrismaUpdateFKT( tableToUpdate: string ): Function { switch(tableToUpdate) { - case "EVENT": return prisma.event.update; - case "DISCIPLINE": return prisma.discipline.update; + case "events": return prisma.event.update; + case "disciplines": return prisma.discipline.update; default: return prisma.roleSchema.update; } } 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 28/55] 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 29/55] 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 30/55] 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 31/55] 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 32/55] 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 33/55] 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 34/55] [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; From 6edaf890b180dbf3e717260a9e12dd963cb600c1 Mon Sep 17 00:00:00 2001 From: Laurin <60652077+Flexla54@users.noreply.github.com> Date: Tue, 31 May 2022 07:44:26 +0200 Subject: [PATCH 35/55] merged f-endpoints and f-roles --- prisma/schema.prisma | 24 ++-- src/Controllers/discipline.controller.ts | 144 +++++++++++++--------- src/Controllers/event.controller.ts | 135 +++++++------------- src/Controllers/media.controller.ts | 88 ++++++++++++- src/Controllers/role_schema.controller.ts | 126 ------------------- src/Routes/discipline.routes.ts | 14 --- src/Routes/event.routes.ts | 10 -- src/Routes/media.routes.ts | 26 +++- src/Routes/role_schema.routes.ts | 10 -- src/app.ts | 3 - 10 files changed, 255 insertions(+), 325 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1d07b9e..50bae8e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -34,11 +34,13 @@ model Admin { } model Discipline { - id Int @id @default(autoincrement()) - pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid - name String - minTeamSize Int - maxTeamSize Int + id Int @id @default(autoincrement()) + pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid + name String + briefDescription String + fullDescription String? + minTeamSize Int + maxTeamSize Int roles RoleSchema[] teams Team[] @@ -60,15 +62,15 @@ model RoleSchema { } model Team { - id Int @id @default(autoincrement()) - pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid + id Int @id @default(autoincrement()) + pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid name String leaderEmail String verified Boolean @default(false) - roles Role[] @relation(name: "participants") + roles Role[] @relation(name: "participants") participants Participant[] - discipline Discipline @relation(fields: [disciplineId], references: [id], onDelete: Cascade) + discipline Discipline @relation(fields: [disciplineId], references: [id], onDelete: Cascade) disciplineId Int } @@ -81,7 +83,7 @@ model Participant { group Group @relation(fields: [groupId], references: [id], onDelete: Cascade) groupId Int - team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) + team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) teamId Int roles Role[] } @@ -125,7 +127,7 @@ model Group { model Media { id Int @id @default(autoincrement()) pid String @unique - description String + description String @default("visual") events Event[] disciplines Discipline[] diff --git a/src/Controllers/discipline.controller.ts b/src/Controllers/discipline.controller.ts index 40d5494..6251a22 100644 --- a/src/Controllers/discipline.controller.ts +++ b/src/Controllers/discipline.controller.ts @@ -1,10 +1,10 @@ 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"; +import { number, z } from "zod"; import { createInsufficientPermissionsError, DataType, @@ -36,6 +36,8 @@ const basicDiscipline = { visual: { select: { pid: true } }, maxTeamSize: true, minTeamSize: true, + briefDescription: true, + fullDescription: true, event: { select: { pid: true, name: true } }, roles: { select: { pid: true, name: true } }, } as const; @@ -128,10 +130,90 @@ export const getDiscipline = async (req: Request, res: }); }; +export const updateDiscipline = async (req: Request<{ pid: string }>, res: Response) => { + if (req.auth?.permission_level !== "ELEVATED") { + res.status(403).json(createInsufficientPermissionsError()); + } + + const { pid } = req.params; + + 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, + briefDescription: DataType.STRING, + ["fullDescription?"]: DataType.STRING, + }) + ); + } + + const body = result.data; + + try { + const discipline = await prisma.discipline.update({ + where: { pid }, + data: { + name: body.name, + minTeamSize: body.minTeamSize, + maxTeamSize: body.maxTeamSize, + briefDescription: body.briefDescription, + fullDescription: body.fullDescription, + }, + select: { + pid: true, + name: true, + minTeamSize: true, + maxTeamSize: true, + briefDescription: true, + fullDescription: true, + }, + }); + + if (!discipline) { + throw new NotFoundError("discipline", pid); + } + + 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: { + eventId: DataType.UUID, + }, + }, + }); + } + + throw e; + } +}; + interface CreateDisciplineBody { name?: string; minTeamSize?: number; maxTeamSize?: number; + briefDescription?: string; + fullDescription?: string; } // require: auth(ELEVATED) @@ -241,63 +323,3 @@ export const deleteDiscipline = async (req: Request<{ pid: string }>, res: Respo throw e; } }; - -interface visualParams { - disciplinePid: string; -} - -interface visualBody { - mediaPid: string; -} - -export const addVisual = async (req: Request, res: Response) => { - if (req.auth?.permission_level != "ELEVATED") { - res.status(403).json(createInsufficientPermissionsError()); - } - - const { disciplinePid } = req.params; - - const discipline = await prisma.discipline.update({ - where: { pid: disciplinePid }, - data: { - visual: { connect: { pid: req.body.mediaPid } }, - }, - }); - - // 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); - } - - return res.status(200).json({ - type: "success", - payload: {}, - }); -}; - -export const deleteVisual = async (req: Request, res: Response) => { - if (req.auth?.permission_level != "ELEVATED") { - res.status(403).json(createInsufficientPermissionsError()); - } - - const { disciplinePid, pid } = req.params; - - try { - await prisma.discipline.update({ - where: { - pid: disciplinePid, - }, - data: { - visual: { disconnect: { pid } }, - }, - }); - return res.status(204).end(); - } catch (e) { - if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { - throw new NotFoundError("discipline", disciplinePid); // Refer: Last todo; This is a correct example - } - - throw e; - } -}; diff --git a/src/Controllers/event.controller.ts b/src/Controllers/event.controller.ts index 03d0426..1e558c7 100644 --- a/src/Controllers/event.controller.ts +++ b/src/Controllers/event.controller.ts @@ -1,6 +1,6 @@ import { Prisma } from "@prisma/client"; import { PrismaClientKnownRequestError } from "@prisma/client/runtime"; -import { Request, Response } from "express"; +import e, { Request, Response } from "express"; import { z } from "zod"; import prisma from "../lib/prisma"; import NotFoundError from "../Middleware/error/NotFoundError"; @@ -8,16 +8,39 @@ import { createInsufficientPermissionsError, DataType, generateError, generateIn require("express-async-errors"); +const EventBody = z.object({ + name: z.string(), + date: z.string(), + briefDescription: z.string(), + fullDescription: z.string(), +}); + +const UpdateBody = EventBody.partial(); + +const CreateEventBody = EventBody.partial({ + fullDescription: true, +}); + export const getAllEvents = async (req: Request, res: Response) => { const events = await prisma.event.findMany({ select: { + pid: true, name: true, + date: true, briefDescription: true, fullDescription: true, visual: { select: { pid: true, description: true } }, - date: true, - pid: true, - id: false, + disciplines: { + select: { + pid: true, + name: true, + }}, + organisations: { + select: { + pid: true, + name: true, + } + } }, }); @@ -46,13 +69,25 @@ export const getEvent = async (req: Request, res: Response) => { pid: eventId, }, select: { + pid: true, name: true, + date: true, briefDescription: true, fullDescription: true, - date: true, - pid: true, - id: false, visual: { select: { pid: true, description: true } }, + disciplines: { + select: { + pid: true, + name: true, + briefDescription: true, + fullDescription: true, + }}, + organisations: { + select: { + pid: true, + name: true, + } + } }, }); @@ -96,12 +131,10 @@ export const addEvent = async (req: Request, res: Response) => { if (req.auth?.permission_level !== "ELEVATED") { res.status(403).json(createInsufficientPermissionsError()); } - if ( - typeof req.body.name !== "string" || - typeof req.body.date !== "string" || - typeof req.body.briefDescription !== "string" || - (req.body.fullDescription && typeof req.body.fullDescription !== "string") - ) { + + const result = CreateEventBody.safeParse(req.body); + + if(result.success === false){ return res.status(400).json( generateInvalidBodyError({ name: DataType.STRING, @@ -122,10 +155,9 @@ export const addEvent = async (req: Request, res: Response) => { fullDescription: req.body.fullDescription, }, select: { + pid: true, name: true, date: true, - pid: true, - id: false, briefDescription: true, fullDescription: true, }, @@ -139,15 +171,6 @@ export const addEvent = async (req: Request, res: Response) => { }); }; -const EventBody = z.object({ - name: z.string(), - date: z.string(), - briefDescription: z.string(), - fullDescription: z.string(), -}); - -const UpdateBody = EventBody.partial(); - export const updateEvent = async (req: Request<{ pid: string }>, res: Response) => { if (req.auth?.permission_level !== "ELEVATED") { res.status(403).json(createInsufficientPermissionsError()); @@ -250,67 +273,3 @@ export const deleteEvent = async (req: Request, res: Res throw e; } }; - -// REVIEW: This code **will** need to be de-duplicated - -interface visualParams { - eventPid: string; -} - -interface visualBody { - mediaPid: string; -} - -export const addVisual = async (req: Request, res: Response) => { - if (req.auth?.permission_level != "ELEVATED") { - res.status(403).json(createInsufficientPermissionsError()); - } - - const { eventPid } = req.params; - - if (typeof req.body.mediaPid !== "string") { - res.status(400).json(generateInvalidBodyError({ mediaPid: DataType.STRING })); - } - - const event = await prisma.event.update({ - where: { pid: eventPid }, - data: { - visual: { connect: { pid: req.body.mediaPid } }, - }, - }); - - if (!event) { - throw new NotFoundError("event", eventPid); - } - - return res.status(200).json({ - type: "success", - payload: {}, - }); -}; - -export const deleteVisual = async (req: Request, res: Response) => { - if (req.auth?.permission_level != "ELEVATED") { - res.status(403).json(createInsufficientPermissionsError()); - } - - const { eventPid, pid } = req.params; - - try { - await prisma.event.update({ - where: { - pid: eventPid, - }, - data: { - visual: { disconnect: { pid } }, - }, - }); - return res.status(204).end(); - } catch (e) { - if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { - throw new NotFoundError("discipline", eventPid); - } - - throw e; - } -}; diff --git a/src/Controllers/media.controller.ts b/src/Controllers/media.controller.ts index eb78bc3..22802a7 100644 --- a/src/Controllers/media.controller.ts +++ b/src/Controllers/media.controller.ts @@ -1,4 +1,4 @@ -import { Request, response, Response } from "express"; +import { NextFunction, Request, response, Response } from "express"; import fs from "fs"; import isSvg from "is-svg"; import { fromBuffer as fileTypeFromBuffer } from "file-type"; @@ -12,6 +12,8 @@ import { type } from "os"; import { unlink } from "fs/promises"; import ForwardableError from "../Middleware/error/ForwardableError"; import SchemaError from "../Middleware/error/SchemaError"; +import { z } from "zod"; +import { updateEvent } from "./event.controller"; require("express-async-errors"); @@ -189,3 +191,87 @@ export const deleteMedia = async (req: Request, res: Response) => { throw e; } }; + +//TODO: maybe create a function that adds the tableToUpdate based on path +// and call it before calling (un)linkMedia + +export const linkMedia = async ( + req: Request<{ pid: string }, {}, { mediaPid: string }>, + res: Response) => { + if (req.auth?.permission_level != "ELEVATED") { + res.status(403).json(createInsufficientPermissionsError()); + } + + const { pid } = req.params; + const { mediaPid } = req.body; + const tableToUpdate = req.originalUrl.split("/"); + + if(typeof mediaPid !== "string") { + return res.status(400).json( + generateInvalidBodyError({ + mediaPid: DataType.UUID, + }) + ); + } + + const updatedRec = await getPrismaUpdateFKT(tableToUpdate[2])({ + where: { pid }, + data: { + visual: { connect: { pid: mediaPid } }, + }, + }); + + if (!updatedRec) { + throw new NotFoundError(tableToUpdate[2], pid); + } + + return res.status(200).json({ + type: "success", + payload: {}, + }); +}; + +export const unlinkMedia = async ( + req: Request<{ pid: string, mediaPid: string }>, + res: Response) => { + if (req.auth?.permission_level != "ELEVATED") { + res.status(403).json(createInsufficientPermissionsError()); + } + + const { pid, mediaPid } = req.params; + const tableToUpdate = req.originalUrl.split("/"); + + try { + await getPrismaUpdateFKT(tableToUpdate[2])({ + where: { pid }, + data: { visual: { disconnect: { pid: mediaPid } } }, + }); + return res.status(204).end(); + } catch (e) { + if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { + console.log("not found error"); + throw new NotFoundError(tableToUpdate[2], 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: { + eventId: DataType.UUID, + }, + }, + }); + } + + throw e; + } +}; + +function getPrismaUpdateFKT( tableToUpdate: string ): Function { + switch(tableToUpdate) { + case "events": return prisma.event.update; + case "disciplines": return prisma.discipline.update; + default: return prisma.roleSchema.update; + } +} diff --git a/src/Controllers/role_schema.controller.ts b/src/Controllers/role_schema.controller.ts index f47e154..8b05caf 100644 --- a/src/Controllers/role_schema.controller.ts +++ b/src/Controllers/role_schema.controller.ts @@ -133,129 +133,3 @@ export const createRoleSchema = async ( throw e; } }; - -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, - }, - result.error - ) - ); - } - - const { name, schema } = result.data; - - const validatedSchema = parseSchema(schema); - - try { - const schema = await prisma.roleSchema.update({ - where: { pid }, - data: { - name: name, - schema: validatedSchema, - }, - select: roleSchema, - }); - - res.status(200).json({ - type: "success", - payload: schema, - }); - } catch (e) { - if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") { - throw new NotFoundError("roleSchema", pid); - } - - throw e; - } -}; - -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; -} - -interface visualBody { - mediaPid: string; -} - -export const addVisual = async (req: Request, res: Response) => { - if (req.auth?.permission_level != "ELEVATED") { - res.status(403).json(createInsufficientPermissionsError()); - } - - const { schemaPid } = req.params; - - const schema = await prisma.roleSchema.update({ - where: { pid: schemaPid }, - data: { - visual: { connect: { pid: req.body.mediaPid } }, - }, - }); - - if (!schema) { - throw new NotFoundError("role_schema", schemaPid); - } - - return res.status(200).json({ - type: "success", - payload: {}, - }); -}; - -export const deleteVisual = async (req: Request, res: Response) => { - if (req.auth?.permission_level != "ELEVATED") { - res.status(403).json(createInsufficientPermissionsError()); - } - - const { schemaPid, pid } = req.params; - - try { - await prisma.roleSchema.update({ - where: { - pid: schemaPid, - }, - data: { - visual: { disconnect: { pid } }, - }, - }); - return res.status(204).end(); - } catch (e) { - if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { - throw new NotFoundError("role_schema", schemaPid); - } - - throw e; - } -}; diff --git a/src/Routes/discipline.routes.ts b/src/Routes/discipline.routes.ts index 50a2de9..6111c64 100644 --- a/src/Routes/discipline.routes.ts +++ b/src/Routes/discipline.routes.ts @@ -1,10 +1,8 @@ import express from "express"; import eventRouter from "./event.routes"; import { - addVisual, createDiscipline, deleteDiscipline, - deleteVisual, getAllDisciplines, getDiscipline, updateDiscipline, @@ -20,18 +18,6 @@ router.get("/:pid", getDiscipline); router.patch("/:pid", requireAuthentication, updateDiscipline); router.delete<"/:pid", { pid: string }>("/:pid", requireAuthentication, deleteDiscipline); -router.post<"/:disciplinePid/images", { disciplinePid: string }>( - "/:disciplinePid/images", - requireAuthentication, - addVisual -); - -router.delete<"/:disciplinePid/images/:pid", { disciplinePid: string; pid: string }>( - "/:disciplinePid/images/:pid", - requireAuthentication, - deleteVisual -); - eventRouter.post("/:eventPid/disciplines", requireAuthentication, createDiscipline); export default router; diff --git a/src/Routes/event.routes.ts b/src/Routes/event.routes.ts index 5e68bd8..7c2d34d 100644 --- a/src/Routes/event.routes.ts +++ b/src/Routes/event.routes.ts @@ -2,9 +2,7 @@ import Express from "express"; import { string } from "zod"; import { addEvent, - addVisual, deleteEvent, - deleteVisual, getAllEvents, getEvent, updateEvent, @@ -22,12 +20,4 @@ router.patch<"/:pid/", { pid: string }>("/:pid/", requireAuthentication, updateE router.delete<"/:pid/", { pid: string }>("/:pid/", requireAuthentication, deleteEvent); -router.post<"/:eventPid/media", { eventPid: string }>("/:eventPid/media", requireAuthentication, addVisual); - -router.delete<"/:eventPid/media/:pid", { eventPid: string; pid: string }>( - "/:eventPid/media/:pid", - requireAuthentication, - deleteVisual -); - export default router; diff --git a/src/Routes/media.routes.ts b/src/Routes/media.routes.ts index f340de1..e4c633a 100644 --- a/src/Routes/media.routes.ts +++ b/src/Routes/media.routes.ts @@ -1,7 +1,7 @@ import express from "express"; import fileUpload from "express-fileupload"; import { requireAuthentication } from "../Middleware/auth/auth"; -import { deleteMedia, getAllMedia, getMediaMeta, uploadImage } from "../Controllers/media.controller"; +import { deleteMedia, getAllMedia, getMediaMeta, linkMedia, unlinkMedia, uploadImage } from "../Controllers/media.controller"; import eventRouter from "./event.routes"; import disciplineRouter from "./discipline.routes"; import roleSchemaRouter from "./role_schema.routes"; @@ -19,4 +19,28 @@ router.get("/:pid/meta", getMediaMeta); router.delete("/:pid", requireAuthentication, deleteMedia); +eventRouter.post<"/:pid/media", { pid: string }>( + "/:pid/media", requireAuthentication, linkMedia +); + +eventRouter.delete<"/:pid/media/:mediaPid", { pid: string, mediaPid: string }>( + "/:pid/media/:mediaPid", requireAuthentication, unlinkMedia +); + +disciplineRouter.post<"/:pid/media", { pid: string }>( + "/:pid/media", requireAuthentication, linkMedia +); + +disciplineRouter.delete<"/:pid/media/:mediaPid", { pid: string, mediaPid: string }>( + "/:pid/media/:mediaPid", requireAuthentication, unlinkMedia +); + +roleSchemaRouter.post<"/:pid/media", { pid: string }>( + "/:pid/media", requireAuthentication, linkMedia +); + +roleSchemaRouter.delete<"/:pid/media/:mediaPid", { pid: string, mediaPid: string }>( + "/:pid/media/:mediaPid", requireAuthentication, unlinkMedia +); + export default router; diff --git a/src/Routes/role_schema.routes.ts b/src/Routes/role_schema.routes.ts index f6ada5f..688dafc 100644 --- a/src/Routes/role_schema.routes.ts +++ b/src/Routes/role_schema.routes.ts @@ -1,9 +1,7 @@ import express from "express"; import disciplineRouter from "./discipline.routes"; import { - addVisual, createRoleSchema, - deleteVisual, getAllRoleSchemas, getAllRoleSchemasWithParam, getRoleSchema, @@ -20,12 +18,4 @@ disciplineRouter.get("/:disciplinePid/role-schemas", getAllRoleSchemasWithParam) disciplineRouter.post("/:disciplinePid/role-schemas", requireAuthentication, createRoleSchema); -router.post<"/:schemaPid/images", { schemaPid: string }>("/:schemaPid/images", requireAuthentication, addVisual); - -router.delete<"/:schemaPid/images/:pid", { schemaPid: string; pid: string }>( - "/:schemaPid/images/:pid", - requireAuthentication, - deleteVisual -); - export default router; diff --git a/src/app.ts b/src/app.ts index 7a41c6c..9121346 100644 --- a/src/app.ts +++ b/src/app.ts @@ -95,9 +95,6 @@ async function main() { // Error handling app.use(defaultErrorHandler); // This has to be the LAST ROUTE - // Disable the media router for now - // app.use("/api/media", mediaRouter); - app.use(notFoundHandler); app.listen(process.env.PORT, () => { From eff81e32f1b2ce6c30f2128d439c5b16550f1f84 Mon Sep 17 00:00:00 2001 From: Laurin <60652077+Flexla54@users.noreply.github.com> Date: Wed, 1 Jun 2022 14:10:08 +0200 Subject: [PATCH 36/55] Revert "merged f-endpoints and f-roles" This reverts commit 6edaf890b180dbf3e717260a9e12dd963cb600c1. --- prisma/schema.prisma | 24 ++-- src/Controllers/discipline.controller.ts | 144 +++++++++------------- src/Controllers/event.controller.ts | 135 +++++++++++++------- src/Controllers/media.controller.ts | 88 +------------ src/Controllers/role_schema.controller.ts | 126 +++++++++++++++++++ src/Routes/discipline.routes.ts | 14 +++ src/Routes/event.routes.ts | 10 ++ src/Routes/media.routes.ts | 26 +--- src/Routes/role_schema.routes.ts | 10 ++ src/app.ts | 3 + 10 files changed, 325 insertions(+), 255 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 50bae8e..1d07b9e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -34,13 +34,11 @@ model Admin { } model Discipline { - id Int @id @default(autoincrement()) - pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid - name String - briefDescription String - fullDescription String? - minTeamSize Int - maxTeamSize Int + id Int @id @default(autoincrement()) + pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid + name String + minTeamSize Int + maxTeamSize Int roles RoleSchema[] teams Team[] @@ -62,15 +60,15 @@ model RoleSchema { } model Team { - id Int @id @default(autoincrement()) - pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid + id Int @id @default(autoincrement()) + pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid name String leaderEmail String verified Boolean @default(false) - roles Role[] @relation(name: "participants") + roles Role[] @relation(name: "participants") participants Participant[] - discipline Discipline @relation(fields: [disciplineId], references: [id], onDelete: Cascade) + discipline Discipline @relation(fields: [disciplineId], references: [id], onDelete: Cascade) disciplineId Int } @@ -83,7 +81,7 @@ model Participant { group Group @relation(fields: [groupId], references: [id], onDelete: Cascade) groupId Int - team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) + team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) teamId Int roles Role[] } @@ -127,7 +125,7 @@ model Group { model Media { id Int @id @default(autoincrement()) pid String @unique - description String @default("visual") + description String events Event[] disciplines Discipline[] diff --git a/src/Controllers/discipline.controller.ts b/src/Controllers/discipline.controller.ts index 6251a22..40d5494 100644 --- a/src/Controllers/discipline.controller.ts +++ b/src/Controllers/discipline.controller.ts @@ -1,10 +1,10 @@ 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"; -import { number, z } from "zod"; import { createInsufficientPermissionsError, DataType, @@ -36,8 +36,6 @@ const basicDiscipline = { visual: { select: { pid: true } }, maxTeamSize: true, minTeamSize: true, - briefDescription: true, - fullDescription: true, event: { select: { pid: true, name: true } }, roles: { select: { pid: true, name: true } }, } as const; @@ -130,90 +128,10 @@ export const getDiscipline = async (req: Request, res: }); }; -export const updateDiscipline = async (req: Request<{ pid: string }>, res: Response) => { - if (req.auth?.permission_level !== "ELEVATED") { - res.status(403).json(createInsufficientPermissionsError()); - } - - const { pid } = req.params; - - 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, - briefDescription: DataType.STRING, - ["fullDescription?"]: DataType.STRING, - }) - ); - } - - const body = result.data; - - try { - const discipline = await prisma.discipline.update({ - where: { pid }, - data: { - name: body.name, - minTeamSize: body.minTeamSize, - maxTeamSize: body.maxTeamSize, - briefDescription: body.briefDescription, - fullDescription: body.fullDescription, - }, - select: { - pid: true, - name: true, - minTeamSize: true, - maxTeamSize: true, - briefDescription: true, - fullDescription: true, - }, - }); - - if (!discipline) { - throw new NotFoundError("discipline", pid); - } - - 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: { - eventId: DataType.UUID, - }, - }, - }); - } - - throw e; - } -}; - interface CreateDisciplineBody { name?: string; minTeamSize?: number; maxTeamSize?: number; - briefDescription?: string; - fullDescription?: string; } // require: auth(ELEVATED) @@ -323,3 +241,63 @@ export const deleteDiscipline = async (req: Request<{ pid: string }>, res: Respo throw e; } }; + +interface visualParams { + disciplinePid: string; +} + +interface visualBody { + mediaPid: string; +} + +export const addVisual = async (req: Request, res: Response) => { + if (req.auth?.permission_level != "ELEVATED") { + res.status(403).json(createInsufficientPermissionsError()); + } + + const { disciplinePid } = req.params; + + const discipline = await prisma.discipline.update({ + where: { pid: disciplinePid }, + data: { + visual: { connect: { pid: req.body.mediaPid } }, + }, + }); + + // 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); + } + + return res.status(200).json({ + type: "success", + payload: {}, + }); +}; + +export const deleteVisual = async (req: Request, res: Response) => { + if (req.auth?.permission_level != "ELEVATED") { + res.status(403).json(createInsufficientPermissionsError()); + } + + const { disciplinePid, pid } = req.params; + + try { + await prisma.discipline.update({ + where: { + pid: disciplinePid, + }, + data: { + visual: { disconnect: { pid } }, + }, + }); + return res.status(204).end(); + } catch (e) { + if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { + throw new NotFoundError("discipline", disciplinePid); // Refer: Last todo; This is a correct example + } + + throw e; + } +}; diff --git a/src/Controllers/event.controller.ts b/src/Controllers/event.controller.ts index 1e558c7..03d0426 100644 --- a/src/Controllers/event.controller.ts +++ b/src/Controllers/event.controller.ts @@ -1,6 +1,6 @@ import { Prisma } from "@prisma/client"; import { PrismaClientKnownRequestError } from "@prisma/client/runtime"; -import e, { Request, Response } from "express"; +import { Request, Response } from "express"; import { z } from "zod"; import prisma from "../lib/prisma"; import NotFoundError from "../Middleware/error/NotFoundError"; @@ -8,39 +8,16 @@ import { createInsufficientPermissionsError, DataType, generateError, generateIn require("express-async-errors"); -const EventBody = z.object({ - name: z.string(), - date: z.string(), - briefDescription: z.string(), - fullDescription: z.string(), -}); - -const UpdateBody = EventBody.partial(); - -const CreateEventBody = EventBody.partial({ - fullDescription: true, -}); - export const getAllEvents = async (req: Request, res: Response) => { const events = await prisma.event.findMany({ select: { - pid: true, name: true, - date: true, briefDescription: true, fullDescription: true, visual: { select: { pid: true, description: true } }, - disciplines: { - select: { - pid: true, - name: true, - }}, - organisations: { - select: { - pid: true, - name: true, - } - } + date: true, + pid: true, + id: false, }, }); @@ -69,25 +46,13 @@ export const getEvent = async (req: Request, res: Response) => { pid: eventId, }, select: { - pid: true, name: true, - date: true, briefDescription: true, fullDescription: true, + date: true, + pid: true, + id: false, visual: { select: { pid: true, description: true } }, - disciplines: { - select: { - pid: true, - name: true, - briefDescription: true, - fullDescription: true, - }}, - organisations: { - select: { - pid: true, - name: true, - } - } }, }); @@ -131,10 +96,12 @@ export const addEvent = async (req: Request, res: Response) => { if (req.auth?.permission_level !== "ELEVATED") { res.status(403).json(createInsufficientPermissionsError()); } - - const result = CreateEventBody.safeParse(req.body); - - if(result.success === false){ + if ( + typeof req.body.name !== "string" || + typeof req.body.date !== "string" || + typeof req.body.briefDescription !== "string" || + (req.body.fullDescription && typeof req.body.fullDescription !== "string") + ) { return res.status(400).json( generateInvalidBodyError({ name: DataType.STRING, @@ -155,9 +122,10 @@ export const addEvent = async (req: Request, res: Response) => { fullDescription: req.body.fullDescription, }, select: { - pid: true, name: true, date: true, + pid: true, + id: false, briefDescription: true, fullDescription: true, }, @@ -171,6 +139,15 @@ export const addEvent = async (req: Request, res: Response) => { }); }; +const EventBody = z.object({ + name: z.string(), + date: z.string(), + briefDescription: z.string(), + fullDescription: z.string(), +}); + +const UpdateBody = EventBody.partial(); + export const updateEvent = async (req: Request<{ pid: string }>, res: Response) => { if (req.auth?.permission_level !== "ELEVATED") { res.status(403).json(createInsufficientPermissionsError()); @@ -273,3 +250,67 @@ export const deleteEvent = async (req: Request, res: Res throw e; } }; + +// REVIEW: This code **will** need to be de-duplicated + +interface visualParams { + eventPid: string; +} + +interface visualBody { + mediaPid: string; +} + +export const addVisual = async (req: Request, res: Response) => { + if (req.auth?.permission_level != "ELEVATED") { + res.status(403).json(createInsufficientPermissionsError()); + } + + const { eventPid } = req.params; + + if (typeof req.body.mediaPid !== "string") { + res.status(400).json(generateInvalidBodyError({ mediaPid: DataType.STRING })); + } + + const event = await prisma.event.update({ + where: { pid: eventPid }, + data: { + visual: { connect: { pid: req.body.mediaPid } }, + }, + }); + + if (!event) { + throw new NotFoundError("event", eventPid); + } + + return res.status(200).json({ + type: "success", + payload: {}, + }); +}; + +export const deleteVisual = async (req: Request, res: Response) => { + if (req.auth?.permission_level != "ELEVATED") { + res.status(403).json(createInsufficientPermissionsError()); + } + + const { eventPid, pid } = req.params; + + try { + await prisma.event.update({ + where: { + pid: eventPid, + }, + data: { + visual: { disconnect: { pid } }, + }, + }); + return res.status(204).end(); + } catch (e) { + if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { + throw new NotFoundError("discipline", eventPid); + } + + throw e; + } +}; diff --git a/src/Controllers/media.controller.ts b/src/Controllers/media.controller.ts index 22802a7..eb78bc3 100644 --- a/src/Controllers/media.controller.ts +++ b/src/Controllers/media.controller.ts @@ -1,4 +1,4 @@ -import { NextFunction, Request, response, Response } from "express"; +import { Request, response, Response } from "express"; import fs from "fs"; import isSvg from "is-svg"; import { fromBuffer as fileTypeFromBuffer } from "file-type"; @@ -12,8 +12,6 @@ import { type } from "os"; import { unlink } from "fs/promises"; import ForwardableError from "../Middleware/error/ForwardableError"; import SchemaError from "../Middleware/error/SchemaError"; -import { z } from "zod"; -import { updateEvent } from "./event.controller"; require("express-async-errors"); @@ -191,87 +189,3 @@ export const deleteMedia = async (req: Request, res: Response) => { throw e; } }; - -//TODO: maybe create a function that adds the tableToUpdate based on path -// and call it before calling (un)linkMedia - -export const linkMedia = async ( - req: Request<{ pid: string }, {}, { mediaPid: string }>, - res: Response) => { - if (req.auth?.permission_level != "ELEVATED") { - res.status(403).json(createInsufficientPermissionsError()); - } - - const { pid } = req.params; - const { mediaPid } = req.body; - const tableToUpdate = req.originalUrl.split("/"); - - if(typeof mediaPid !== "string") { - return res.status(400).json( - generateInvalidBodyError({ - mediaPid: DataType.UUID, - }) - ); - } - - const updatedRec = await getPrismaUpdateFKT(tableToUpdate[2])({ - where: { pid }, - data: { - visual: { connect: { pid: mediaPid } }, - }, - }); - - if (!updatedRec) { - throw new NotFoundError(tableToUpdate[2], pid); - } - - return res.status(200).json({ - type: "success", - payload: {}, - }); -}; - -export const unlinkMedia = async ( - req: Request<{ pid: string, mediaPid: string }>, - res: Response) => { - if (req.auth?.permission_level != "ELEVATED") { - res.status(403).json(createInsufficientPermissionsError()); - } - - const { pid, mediaPid } = req.params; - const tableToUpdate = req.originalUrl.split("/"); - - try { - await getPrismaUpdateFKT(tableToUpdate[2])({ - where: { pid }, - data: { visual: { disconnect: { pid: mediaPid } } }, - }); - return res.status(204).end(); - } catch (e) { - if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { - console.log("not found error"); - throw new NotFoundError(tableToUpdate[2], 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: { - eventId: DataType.UUID, - }, - }, - }); - } - - throw e; - } -}; - -function getPrismaUpdateFKT( tableToUpdate: string ): Function { - switch(tableToUpdate) { - case "events": return prisma.event.update; - case "disciplines": return prisma.discipline.update; - default: return prisma.roleSchema.update; - } -} diff --git a/src/Controllers/role_schema.controller.ts b/src/Controllers/role_schema.controller.ts index 8b05caf..f47e154 100644 --- a/src/Controllers/role_schema.controller.ts +++ b/src/Controllers/role_schema.controller.ts @@ -133,3 +133,129 @@ export const createRoleSchema = async ( throw e; } }; + +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, + }, + result.error + ) + ); + } + + const { name, schema } = result.data; + + const validatedSchema = parseSchema(schema); + + try { + const schema = await prisma.roleSchema.update({ + where: { pid }, + data: { + name: name, + schema: validatedSchema, + }, + select: roleSchema, + }); + + res.status(200).json({ + type: "success", + payload: schema, + }); + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") { + throw new NotFoundError("roleSchema", pid); + } + + throw e; + } +}; + +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; +} + +interface visualBody { + mediaPid: string; +} + +export const addVisual = async (req: Request, res: Response) => { + if (req.auth?.permission_level != "ELEVATED") { + res.status(403).json(createInsufficientPermissionsError()); + } + + const { schemaPid } = req.params; + + const schema = await prisma.roleSchema.update({ + where: { pid: schemaPid }, + data: { + visual: { connect: { pid: req.body.mediaPid } }, + }, + }); + + if (!schema) { + throw new NotFoundError("role_schema", schemaPid); + } + + return res.status(200).json({ + type: "success", + payload: {}, + }); +}; + +export const deleteVisual = async (req: Request, res: Response) => { + if (req.auth?.permission_level != "ELEVATED") { + res.status(403).json(createInsufficientPermissionsError()); + } + + const { schemaPid, pid } = req.params; + + try { + await prisma.roleSchema.update({ + where: { + pid: schemaPid, + }, + data: { + visual: { disconnect: { pid } }, + }, + }); + return res.status(204).end(); + } catch (e) { + if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { + throw new NotFoundError("role_schema", schemaPid); + } + + throw e; + } +}; diff --git a/src/Routes/discipline.routes.ts b/src/Routes/discipline.routes.ts index 6111c64..50a2de9 100644 --- a/src/Routes/discipline.routes.ts +++ b/src/Routes/discipline.routes.ts @@ -1,8 +1,10 @@ import express from "express"; import eventRouter from "./event.routes"; import { + addVisual, createDiscipline, deleteDiscipline, + deleteVisual, getAllDisciplines, getDiscipline, updateDiscipline, @@ -18,6 +20,18 @@ router.get("/:pid", getDiscipline); router.patch("/:pid", requireAuthentication, updateDiscipline); router.delete<"/:pid", { pid: string }>("/:pid", requireAuthentication, deleteDiscipline); +router.post<"/:disciplinePid/images", { disciplinePid: string }>( + "/:disciplinePid/images", + requireAuthentication, + addVisual +); + +router.delete<"/:disciplinePid/images/:pid", { disciplinePid: string; pid: string }>( + "/:disciplinePid/images/:pid", + requireAuthentication, + deleteVisual +); + eventRouter.post("/:eventPid/disciplines", requireAuthentication, createDiscipline); export default router; diff --git a/src/Routes/event.routes.ts b/src/Routes/event.routes.ts index 7c2d34d..5e68bd8 100644 --- a/src/Routes/event.routes.ts +++ b/src/Routes/event.routes.ts @@ -2,7 +2,9 @@ import Express from "express"; import { string } from "zod"; import { addEvent, + addVisual, deleteEvent, + deleteVisual, getAllEvents, getEvent, updateEvent, @@ -20,4 +22,12 @@ router.patch<"/:pid/", { pid: string }>("/:pid/", requireAuthentication, updateE router.delete<"/:pid/", { pid: string }>("/:pid/", requireAuthentication, deleteEvent); +router.post<"/:eventPid/media", { eventPid: string }>("/:eventPid/media", requireAuthentication, addVisual); + +router.delete<"/:eventPid/media/:pid", { eventPid: string; pid: string }>( + "/:eventPid/media/:pid", + requireAuthentication, + deleteVisual +); + export default router; diff --git a/src/Routes/media.routes.ts b/src/Routes/media.routes.ts index e4c633a..f340de1 100644 --- a/src/Routes/media.routes.ts +++ b/src/Routes/media.routes.ts @@ -1,7 +1,7 @@ import express from "express"; import fileUpload from "express-fileupload"; import { requireAuthentication } from "../Middleware/auth/auth"; -import { deleteMedia, getAllMedia, getMediaMeta, linkMedia, unlinkMedia, uploadImage } from "../Controllers/media.controller"; +import { deleteMedia, getAllMedia, getMediaMeta, uploadImage } from "../Controllers/media.controller"; import eventRouter from "./event.routes"; import disciplineRouter from "./discipline.routes"; import roleSchemaRouter from "./role_schema.routes"; @@ -19,28 +19,4 @@ router.get("/:pid/meta", getMediaMeta); router.delete("/:pid", requireAuthentication, deleteMedia); -eventRouter.post<"/:pid/media", { pid: string }>( - "/:pid/media", requireAuthentication, linkMedia -); - -eventRouter.delete<"/:pid/media/:mediaPid", { pid: string, mediaPid: string }>( - "/:pid/media/:mediaPid", requireAuthentication, unlinkMedia -); - -disciplineRouter.post<"/:pid/media", { pid: string }>( - "/:pid/media", requireAuthentication, linkMedia -); - -disciplineRouter.delete<"/:pid/media/:mediaPid", { pid: string, mediaPid: string }>( - "/:pid/media/:mediaPid", requireAuthentication, unlinkMedia -); - -roleSchemaRouter.post<"/:pid/media", { pid: string }>( - "/:pid/media", requireAuthentication, linkMedia -); - -roleSchemaRouter.delete<"/:pid/media/:mediaPid", { pid: string, mediaPid: string }>( - "/:pid/media/:mediaPid", requireAuthentication, unlinkMedia -); - export default router; diff --git a/src/Routes/role_schema.routes.ts b/src/Routes/role_schema.routes.ts index 688dafc..f6ada5f 100644 --- a/src/Routes/role_schema.routes.ts +++ b/src/Routes/role_schema.routes.ts @@ -1,7 +1,9 @@ import express from "express"; import disciplineRouter from "./discipline.routes"; import { + addVisual, createRoleSchema, + deleteVisual, getAllRoleSchemas, getAllRoleSchemasWithParam, getRoleSchema, @@ -18,4 +20,12 @@ disciplineRouter.get("/:disciplinePid/role-schemas", getAllRoleSchemasWithParam) disciplineRouter.post("/:disciplinePid/role-schemas", requireAuthentication, createRoleSchema); +router.post<"/:schemaPid/images", { schemaPid: string }>("/:schemaPid/images", requireAuthentication, addVisual); + +router.delete<"/:schemaPid/images/:pid", { schemaPid: string; pid: string }>( + "/:schemaPid/images/:pid", + requireAuthentication, + deleteVisual +); + export default router; diff --git a/src/app.ts b/src/app.ts index 9121346..7a41c6c 100644 --- a/src/app.ts +++ b/src/app.ts @@ -95,6 +95,9 @@ async function main() { // Error handling app.use(defaultErrorHandler); // This has to be the LAST ROUTE + // Disable the media router for now + // app.use("/api/media", mediaRouter); + app.use(notFoundHandler); app.listen(process.env.PORT, () => { From 569695245fd787079e1d67a771a68b68e2cfa0ee Mon Sep 17 00:00:00 2001 From: Laurin <60652077+Flexla54@users.noreply.github.com> Date: Thu, 2 Jun 2022 00:09:51 +0200 Subject: [PATCH 37/55] patching complications after merging and review suggestions --- src/Controllers/discipline.controller.ts | 90 ++------------- src/Controllers/event.controller.ts | 122 ++++++++------------- src/Controllers/media.controller.ts | 45 ++------ src/Controllers/organisation.controller.ts | 13 +-- src/Controllers/participant.controller.ts | 32 ++---- src/Controllers/role.controller.ts | 87 ++++----------- src/Controllers/role_schema.controller.ts | 50 ++++++++- src/Controllers/team.controller.ts | 32 ++++-- src/Routes/participant.routes.ts | 21 ++-- src/Routes/role.routes.ts | 2 + 10 files changed, 185 insertions(+), 309 deletions(-) diff --git a/src/Controllers/discipline.controller.ts b/src/Controllers/discipline.controller.ts index bc85a7e..bef8240 100644 --- a/src/Controllers/discipline.controller.ts +++ b/src/Controllers/discipline.controller.ts @@ -1,7 +1,6 @@ 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"; @@ -17,11 +16,12 @@ import { require("express-async-errors"); - const InitialDisciplineBody = z.object({ name: z.string().min(1), minTeamSize: z.number(), maxTeamSize: z.number(), + briefDescription: z.string(), + fullDescription: z.string(), }); const disciplineRefiner = [ @@ -29,7 +29,9 @@ const disciplineRefiner = [ { message: "The minTeamSize must be smaller or equal to the maxTeamSize" }, ] as const; -const DisciplineBody = InitialDisciplineBody.refine(...disciplineRefiner); +const DisciplineBody = InitialDisciplineBody.partial({ briefDescription: true, fullDescription: true }).refine( + ...disciplineRefiner +); const updateDisciplineBody = InitialDisciplineBody.partial().refine(...disciplineRefiner); const basicDiscipline = { @@ -132,84 +134,6 @@ export const getDiscipline = async (req: Request, res: }); }; -export const updateDiscipline = async (req: Request<{ pid: string }>, res: Response) => { - if (req.auth?.permission_level !== "ELEVATED") { - res.status(403).json(createInsufficientPermissionsError()); - } - - const { pid } = req.params; - - 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, - briefDescription: DataType.STRING, - ["fullDescription?"]: DataType.STRING, - }) - ); - } - - const body = result.data; - - try { - const discipline = await prisma.discipline.update({ - where: { pid }, - data: { - name: body.name, - minTeamSize: body.minTeamSize, - maxTeamSize: body.maxTeamSize, - briefDescription: body.briefDescription, - fullDescription: body.fullDescription, - }, - select: { - pid: true, - name: true, - minTeamSize: true, - maxTeamSize: true, - briefDescription: true, - fullDescription: true, - }, - }); - - if (!discipline) { - throw new NotFoundError("discipline", pid); - } - - 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: { - eventId: DataType.UUID, - }, - }, - }); - } - - throw e; - } -}; - interface CreateDisciplineBody { name?: string; minTeamSize?: number; @@ -272,6 +196,8 @@ export const updateDiscipline = async (req: Request<{ pid: string }>, res: Respo name: DataType.STRING, minTeamSize: DataType.NUMBER, maxTeamSize: DataType.NUMBER, + briefDescription: DataType.STRING, + ["fullDescription?"]: DataType.STRING, }, result.error ) @@ -288,6 +214,8 @@ export const updateDiscipline = async (req: Request<{ pid: string }>, res: Respo name: body.name, minTeamSize: body.minTeamSize, maxTeamSize: body.maxTeamSize, + briefDescription: body.briefDescription, + fullDescription: body.fullDescription, }, select: basicDiscipline, }); diff --git a/src/Controllers/event.controller.ts b/src/Controllers/event.controller.ts index 1e558c7..3dfcd84 100644 --- a/src/Controllers/event.controller.ts +++ b/src/Controllers/event.controller.ts @@ -8,9 +8,13 @@ import { createInsufficientPermissionsError, DataType, generateError, generateIn require("express-async-errors"); +export const dateSchema = z.preprocess((arg) => { + if (typeof arg == "string" || arg instanceof Date) return new Date(arg); +}, z.date()); + const EventBody = z.object({ - name: z.string(), - date: z.string(), + name: z.string().min(1), + date: dateSchema, briefDescription: z.string(), fullDescription: z.string(), }); @@ -21,27 +25,32 @@ const CreateEventBody = EventBody.partial({ fullDescription: true, }); -export const getAllEvents = async (req: Request, res: Response) => { - const events = await prisma.event.findMany({ +const basicEvent = { + pid: true, + name: true, + date: true, + briefDescription: true, + fullDescription: true, +} as const; + +const detailedEvent = { + pid: true, + name: true, + date: true, + briefDescription: true, + fullDescription: true, + visual: { select: { pid: true, description: true } }, + disciplines: { select: { pid: true, name: true, - date: true, - briefDescription: true, - fullDescription: true, - visual: { select: { pid: true, description: true } }, - disciplines: { - select: { - pid: true, - name: true, - }}, - organisations: { - select: { - pid: true, - name: true, - } - } }, + }, +} as const; + +export const getAllEvents = async (req: Request, res: Response) => { + const events = await prisma.event.findMany({ + select: detailedEvent, }); res.status(200).json({ @@ -68,27 +77,7 @@ export const getEvent = async (req: Request, res: Response) => { where: { pid: eventId, }, - select: { - pid: true, - name: true, - date: true, - briefDescription: true, - fullDescription: true, - visual: { select: { pid: true, description: true } }, - disciplines: { - select: { - pid: true, - name: true, - briefDescription: true, - fullDescription: true, - }}, - organisations: { - select: { - pid: true, - name: true, - } - } - }, + select: detailedEvent, }); if (!event) { @@ -134,19 +123,20 @@ export const addEvent = async (req: Request, res: Response) => { const result = CreateEventBody.safeParse(req.body); - if(result.success === false){ + if (result.success === false) { return res.status(400).json( - generateInvalidBodyError({ - name: DataType.STRING, - date: DataType.DATETIME, - briefDescription: DataType.STRING, - ["fullDescription?"]: DataType.STRING, - }) + generateInvalidBodyError( + { + name: DataType.STRING, + date: DataType.DATETIME, + briefDescription: DataType.STRING, + ["fullDescription?"]: DataType.STRING, + }, + result.error + ) ); } - //TODO: Check if date is valid - const event = await prisma.event.create({ data: { name: req.body.name, @@ -154,13 +144,7 @@ export const addEvent = async (req: Request, res: Response) => { briefDescription: req.body.briefDescription, fullDescription: req.body.fullDescription, }, - select: { - pid: true, - name: true, - date: true, - briefDescription: true, - fullDescription: true, - }, + select: basicEvent, }); res.status(201).json({ @@ -214,10 +198,6 @@ export const updateEvent = async (req: Request<{ pid: string }>, res: Response) }, }); - if (!event) { - throw new NotFoundError("event", pid); - } - res.status(200).json({ type: "success", payload: { @@ -225,24 +205,8 @@ export const updateEvent = async (req: Request<{ pid: string }>, res: Response) }, }); } 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("discipline", pid); } throw e; @@ -267,7 +231,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 event with the ID ${pid} could not be found`)); + throw new NotFoundError("discipline", pid); } throw e; diff --git a/src/Controllers/media.controller.ts b/src/Controllers/media.controller.ts index 22802a7..11a2cc7 100644 --- a/src/Controllers/media.controller.ts +++ b/src/Controllers/media.controller.ts @@ -1,4 +1,4 @@ -import { NextFunction, Request, response, Response } from "express"; +import { Request, Response } from "express"; import fs from "fs"; import isSvg from "is-svg"; import { fromBuffer as fileTypeFromBuffer } from "file-type"; @@ -8,12 +8,8 @@ import prisma from "../lib/prisma"; import NotFoundError from "../Middleware/error/NotFoundError"; import { PrismaClientKnownRequestError } from "@prisma/client/runtime"; import { generateInvalidBodyError, DataType } from "./common"; -import { type } from "os"; import { unlink } from "fs/promises"; import ForwardableError from "../Middleware/error/ForwardableError"; -import SchemaError from "../Middleware/error/SchemaError"; -import { z } from "zod"; -import { updateEvent } from "./event.controller"; require("express-async-errors"); @@ -101,7 +97,6 @@ export const uploadImage = async (req: Request, res: Response) => { const fileName = file.md5 + (fileIsSvg ? ".svg" : "." + fileType?.ext); try { - //generate record const media = await prisma.media.create({ data: { pid: fileName, @@ -192,12 +187,7 @@ export const deleteMedia = async (req: Request, res: Response) => { } }; -//TODO: maybe create a function that adds the tableToUpdate based on path -// and call it before calling (un)linkMedia - -export const linkMedia = async ( - req: Request<{ pid: string }, {}, { mediaPid: string }>, - res: Response) => { +export const linkMedia = async (req: Request<{ pid: string }, {}, { mediaPid: string }>, res: Response) => { if (req.auth?.permission_level != "ELEVATED") { res.status(403).json(createInsufficientPermissionsError()); } @@ -206,7 +196,7 @@ export const linkMedia = async ( const { mediaPid } = req.body; const tableToUpdate = req.originalUrl.split("/"); - if(typeof mediaPid !== "string") { + if (typeof mediaPid !== "string") { return res.status(400).json( generateInvalidBodyError({ mediaPid: DataType.UUID, @@ -231,9 +221,7 @@ export const linkMedia = async ( }); }; -export const unlinkMedia = async ( - req: Request<{ pid: string, mediaPid: string }>, - res: Response) => { +export const unlinkMedia = async (req: Request<{ pid: string; mediaPid: string }>, res: Response) => { if (req.auth?.permission_level != "ELEVATED") { res.status(403).json(createInsufficientPermissionsError()); } @@ -249,29 +237,20 @@ export const unlinkMedia = async ( return res.status(204).end(); } catch (e) { if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { - console.log("not found error"); throw new NotFoundError(tableToUpdate[2], 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: { - eventId: DataType.UUID, - }, - }, - }); - } throw e; } }; -function getPrismaUpdateFKT( tableToUpdate: string ): Function { - switch(tableToUpdate) { - case "events": return prisma.event.update; - case "disciplines": return prisma.discipline.update; - default: return prisma.roleSchema.update; +function getPrismaUpdateFKT(tableToUpdate: string): Function { + switch (tableToUpdate) { + case "events": + return prisma.event.update; + case "disciplines": + return prisma.discipline.update; + default: + return prisma.roleSchema.update; } } diff --git a/src/Controllers/organisation.controller.ts b/src/Controllers/organisation.controller.ts index 146594e..6007947 100644 --- a/src/Controllers/organisation.controller.ts +++ b/src/Controllers/organisation.controller.ts @@ -12,6 +12,7 @@ import { } from "./common"; import { PrismaClientKnownRequestError } from "@prisma/client/runtime"; import { Prisma } from "@prisma/client"; +import NotFoundError from "../Middleware/error/NotFoundError"; function validateOranisationName(name: string) { return name.length > 0; @@ -187,16 +188,12 @@ export const updateOrganisation = async ( }, }); } catch (e) { - if (e instanceof PrismaClientKnownRequestError) { - if (e.code === "P2025") { - return res.status(404).json(generateError(`The organisation with the ID ${pid} could not be found`)); - } - } else if (e instanceof PrismaClientUnknownRequestError) { - return res.status(400).send(generateError("Unkonwn error occured. This could be due to malformed IDs")); + if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") { + throw new NotFoundError("discipline", pid); } - } - return res.status(500).json(genericError); + throw e; + } }; interface DeleteOrganisationQueryParams { diff --git a/src/Controllers/participant.controller.ts b/src/Controllers/participant.controller.ts index cfc6e59..5c7a338 100644 --- a/src/Controllers/participant.controller.ts +++ b/src/Controllers/participant.controller.ts @@ -11,7 +11,7 @@ import { import { Job, Prisma } from "@prisma/client"; import { PrismaClientKnownRequestError } from "@prisma/client/runtime"; import NotFoundError from "../Middleware/error/NotFoundError"; -import { requireResponsibleForGroup } from "../Middleware/auth/auth"; +import { requireConfiguredAuthentication, requireResponsibleForGroup } from "../Middleware/auth/auth"; //TODO: add TeamleaderAuthentification @@ -44,8 +44,8 @@ const returnedParticipant = { }, } as const; -// REVIEW: Location of this endpoints (/groups, /teams, /participants, ...?) -export const createParticipant = async (req: Request<{ pid: string }>, res: Response) => { +// at: POST api/teams/:teamPid/participant/ +export const createParticipant = async (req: Request<{ teamPid: string }>, res: Response) => { const result = ParticipantBody.safeParse(req.body); if (result.success === false) { @@ -61,17 +61,7 @@ export const createParticipant = async (req: Request<{ pid: string }>, res: Resp ); } const body = result.data; - const { pid } = req.params; - - /* - 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); - */ + const { teamPid } = req.params; try { const participant = await prisma.participant.create({ @@ -80,7 +70,7 @@ export const createParticipant = async (req: Request<{ pid: string }>, res: Resp lastName: body.lastName, relevance: "MEMBER", group: { connect: { pid: body.groupId } }, - team: { connect: { pid } }, + team: { connect: { pid: teamPid } }, }, select: returnedParticipant, }); @@ -93,16 +83,15 @@ export const createParticipant = async (req: Request<{ pid: string }>, res: Resp 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}'`)); + .json(generateError(`Could not link to team with ID '${teamPid}, or group with ID ${body.groupId}'`)); } throw e; } }; +// at: PATCH api/participants/:pid/ export const updateParticipant = async (req: Request<{ pid: string }>, res: Response) => { - //insert TeamleaderAuth - - const result = ParticipantBody.partial().safeParse(req.body); // Should be partial, right? + const result = ParticipantBody.partial().safeParse(req.body); if (result.success === false) { return res.status(400).json( @@ -144,9 +133,8 @@ export const updateParticipant = async (req: Request<{ pid: string }>, res: Resp } }; +// at: DELETE api/participants/:pid/ export const deleteParticipant = async (req: Request<{ pid: string }>, res: Response) => { - //insert TeamleaderAuth - const { pid } = req.params; try { @@ -155,7 +143,7 @@ export const deleteParticipant = async (req: Request<{ pid: string }>, res: Resp 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 new NotFoundError("participant", pid); } throw e; diff --git a/src/Controllers/role.controller.ts b/src/Controllers/role.controller.ts index feff325..a55fd17 100644 --- a/src/Controllers/role.controller.ts +++ b/src/Controllers/role.controller.ts @@ -8,6 +8,30 @@ import { createInsufficientPermissionsError, DataType, generateInvalidBodyError require("express-async-errors"); +const detailedRole = { + pid: true, + score: true, + schema: { + select: { + pid: true, + name: true, + }, + }, + participant: { + select: { + pid: true, + firstName: true, + lastName: true, + }, + }, + team: { + select: { + pid: true, + name: true, + }, + }, +}; + /** * * @param teamPid: Pid of the team to add the roles to @@ -94,66 +118,3 @@ 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, - }, - }, - }, - }); - - res.status(200).json({ - type: "success", - payload: { - role, - }, - }); - } catch (e) { - if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") { - throw new NotFoundError("role", pid); - } - - throw e; - } -}; - -export async function deleteRolesFromTeam(teamPid: string) { - await prisma.role.deleteMany({ - where: { team: { pid: teamPid } }, - }); -} diff --git a/src/Controllers/role_schema.controller.ts b/src/Controllers/role_schema.controller.ts index 8b05caf..f375451 100644 --- a/src/Controllers/role_schema.controller.ts +++ b/src/Controllers/role_schema.controller.ts @@ -20,7 +20,7 @@ const RoleSchemaBody = z.object({ schema: z.string(), }); -const UpdateRoleSchema = RoleSchemaBody.partial(); +const UpdateBody = RoleSchemaBody.partial(); const roleSchema = { pid: true, @@ -133,3 +133,51 @@ export const createRoleSchema = async ( throw e; } }; + +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 = UpdateBody.safeParse(req.body); + + if (result.success === false) { + return res.status(400).json( + generateInvalidBodyError( + { + name: DataType.STRING, + schema: DataType.STRING, + }, + result.error + ) + ); + } + + const body = result.data; + + try { + const schema = await prisma.roleSchema.update({ + where: { pid }, + data: { + name: body.name, + schema: body.schema, + }, + select: roleSchema, + }); + + res.status(200).json({ + type: "success", + payload: { + schema, + }, + }); + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") { + throw new NotFoundError("discipline", pid); + } + + throw e; + } +}; diff --git a/src/Controllers/team.controller.ts b/src/Controllers/team.controller.ts index 1f2a5dd..35d6101 100644 --- a/src/Controllers/team.controller.ts +++ b/src/Controllers/team.controller.ts @@ -3,6 +3,8 @@ import prisma from "../lib/prisma"; import { createInsufficientPermissionsError, DataType, generateInvalidBodyError } from "./common"; import { requireLeaderOfTeam } from "../Middleware/auth/teamleaderAuth"; import { z } from "zod"; +import { Prisma } from "@prisma/client"; +import NotFoundError from "../Middleware/error/NotFoundError"; const TeamBody = z.object({ teamName: z.string().min(1), @@ -69,18 +71,26 @@ export const updateTeam = async (req: Request, res: Response) => { 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, - }, - }); + try { + 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); + res.status(204).json(team); + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") { + throw new NotFoundError("discipline", body.pid); + } + + throw e; + } }; export const deleteTeam = async (req: Request, res: Response) => { diff --git a/src/Routes/participant.routes.ts b/src/Routes/participant.routes.ts index 21937a2..532387a 100644 --- a/src/Routes/participant.routes.ts +++ b/src/Routes/participant.routes.ts @@ -1,29 +1,28 @@ 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"; +import { requireAuthentication, requireConfiguredAuthentication } from "../Middleware/auth/auth"; const router = express.Router(); -teamRouter.post<"/:pid/participant/", { pid: string }>( - "/:pid/participant/", +teamRouter.post<"/:teamPid/participant/", { teamPid: string }>( + "/:teamPid/participant/", requireAuthentication, - requireTeamleaderAuthentication, + requireConfiguredAuthentication({ optional: true, type: { admin: true, teamleader: true } }), createParticipant ); -teamRouter.patch<"/:pid/participant/", { pid: string }>( - "/:pid/participant/", +router.patch<"/:pid/", { pid: string }>( + "/:pid/", requireAuthentication, - requireTeamleaderAuthentication, + requireConfiguredAuthentication({ optional: true, type: { admin: true, teamleader: true } }), updateParticipant ); -teamRouter.delete<"/:pid/participant/", { pid: string }>( - "/:pid/participant/", +router.delete<"/:pid/", { pid: string }>( + "/:pid/", requireAuthentication, - requireTeamleaderAuthentication, + requireConfiguredAuthentication({ optional: true, type: { admin: true, teamleader: true } }), deleteParticipant ); diff --git a/src/Routes/role.routes.ts b/src/Routes/role.routes.ts index 5bbd03e..dff8cc7 100644 --- a/src/Routes/role.routes.ts +++ b/src/Routes/role.routes.ts @@ -7,3 +7,5 @@ const router = Express.Router(); router.get<"team/:teamPid/", { teamPid: string }>("team/:teamPid/", getRolesForTeam); router.put<"/:pid/participant", { pid: string }>("/:pid/participant", assignParticipantToRole); + +export default router; From fc894f76438b65835ae05c8513ac87cba6abc5d7 Mon Sep 17 00:00:00 2001 From: Stefan-5422 <32109571+Stefan-5422@users.noreply.github.com> Date: Thu, 2 Jun 2022 09:40:05 +0200 Subject: [PATCH 38/55] Fix #63, #64; --- src/Controllers/user_auth.controller.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Controllers/user_auth.controller.ts b/src/Controllers/user_auth.controller.ts index 4205a20..04cbf46 100644 --- a/src/Controllers/user_auth.controller.ts +++ b/src/Controllers/user_auth.controller.ts @@ -66,7 +66,7 @@ export const register = async (req: Request<{}, {}, CreateTeamBody>, res: Respon select: { pid: true, name: true, - disciplineId: true, + discipline: { select: { pid: true } }, }, }); @@ -105,7 +105,7 @@ export const requestToken = async (req: Request, res: Response) => { verificationMail(team.leaderEmail, "eventname", usid); - res.status(200).json({ type: "sucess", message: "Email sent!" }); + res.status(200).json({ type: "sucess", payload: { message: "Email sent!" } }); }; export const verifyEmail = async (req: Request, res: Response) => { @@ -149,5 +149,5 @@ export const verifyEmail = async (req: Request, res: Response) => { 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 + res.status(200).json({ type: "succes", payload: { token } }); }; From 80b09ebf5af72902455a17bb8bc47dddad8646a9 Mon Sep 17 00:00:00 2001 From: Stefan-5422 <32109571+Stefan-5422@users.noreply.github.com> Date: Thu, 2 Jun 2022 22:14:41 +0200 Subject: [PATCH 39/55] Fix make code more stable & tackle #66 --- src/Controllers/team.controller.ts | 39 +++++-------------------- src/Controllers/user_auth.controller.ts | 19 +++++++++--- 2 files changed, 22 insertions(+), 36 deletions(-) diff --git a/src/Controllers/team.controller.ts b/src/Controllers/team.controller.ts index 1f2a5dd..481a487 100644 --- a/src/Controllers/team.controller.ts +++ b/src/Controllers/team.controller.ts @@ -3,29 +3,12 @@ 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; -} +import { TeamBody } from "./user_auth.controller"; 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); + res.status(200).json({ type: "success", payload: { teams } }); }; export const getTeam = async (req: Request, res: Response) => { @@ -40,7 +23,7 @@ export const getTeam = async (req: Request, res: Response) => { }, }); - res.status(200).json(team); + res.status(200).json({ type: "success", payload: { team } }); }; export const updateTeam = async (req: Request, res: Response) => { @@ -63,11 +46,7 @@ export const updateTeam = async (req: Request, res: Response) => { const body = result.data; - try { - requireLeaderOfTeam(req.teamleader, body.pid); - } catch { - return res.status(401).json(createInsufficientPermissionsError("STANDARD")); - } + requireLeaderOfTeam(req.teamleader, body.pid); const team = prisma.team.update({ where: { @@ -80,19 +59,15 @@ export const updateTeam = async (req: Request, res: Response) => { }, }); - res.status(204).json(team); + res.status(204).json({ type: "success", payload: { 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")); - } + requireLeaderOfTeam(req.teamleader, pid); prisma.team.delete({ where: { pid } }); - res.status(204).json("Welp its gone"); + res.status(204).json({ type: "success", payload: { message: "Sucesfully deleted team" } }); }; diff --git a/src/Controllers/user_auth.controller.ts b/src/Controllers/user_auth.controller.ts index 04cbf46..51dd6a0 100644 --- a/src/Controllers/user_auth.controller.ts +++ b/src/Controllers/user_auth.controller.ts @@ -3,12 +3,12 @@ import prisma from "../lib/prisma"; import { mailClient } from "../lib/redis"; import { nanoid } from "nanoid"; import { verificationMail } from "../lib/mail"; -import { createInsufficientPermissionsError, DataType, generateInvalidBodyError } from "./common"; +import { createInsufficientPermissionsError, DataType, generateError, generateInvalidBodyError } from "./common"; import { generateTeamleaderJWT, requireLeaderOfTeam } from "../Middleware/auth/teamleaderAuth"; import { createRolesForTeam } from "./role.controller"; import { any, z } from "zod"; -const TeamBody = z.object({ +export const TeamBody = z.object({ teamName: z.string().min(1), leaderEmail: z.string().email(), disciplineId: z.string().uuid(), @@ -26,7 +26,6 @@ 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); @@ -48,6 +47,18 @@ export const register = async (req: Request<{}, {}, CreateTeamBody>, res: Respon const body = result.data; + const group = prisma.group.findUnique({ where: { pid: body.partGroupId } }); + + if (typeof group == null) { + return res.status(404).json(generateError("Specified group was not found!")); + } + + const discipline = prisma.discipline.findUnique({ where: { pid: body.disciplineId } }); + + if (typeof discipline == null) { + return res.status(404).json(generateError("Specified discipline was not found!")); + } + const team = await prisma.team.create({ data: { leaderEmail: body.leaderEmail, @@ -96,7 +107,7 @@ export const requestToken = async (req: Request, res: Response) => { }); if (!team) { - return res.status(404).json(); + return res.status(404).json(generateError("Team does not exist!")); } const usid = nanoid(); From 0f0da062c68499bddd95e0859ba126624ea7f1df Mon Sep 17 00:00:00 2001 From: Laurin <60652077+Flexla54@users.noreply.github.com> Date: Fri, 3 Jun 2022 19:57:43 +0200 Subject: [PATCH 40/55] patched issues found during testing --- src/Controllers/discipline.controller.ts | 12 +++---- src/Controllers/group.controllers.ts | 2 +- src/Controllers/organisation.controller.ts | 2 +- src/Controllers/role.controller.ts | 2 -- src/Controllers/team.controller.ts | 40 ++++++++++++++-------- src/Controllers/user_auth.controller.ts | 6 ++-- src/Middleware/error/defaultRoutes.ts | 2 +- src/Routes/participant.routes.ts | 4 +-- src/Routes/role.routes.ts | 15 ++++++-- src/Routes/team.routes.ts | 14 ++++++-- src/app.ts | 10 ++++-- src/lib/mail.ts | 1 + 12 files changed, 74 insertions(+), 36 deletions(-) diff --git a/src/Controllers/discipline.controller.ts b/src/Controllers/discipline.controller.ts index bef8240..c898603 100644 --- a/src/Controllers/discipline.controller.ts +++ b/src/Controllers/discipline.controller.ts @@ -20,7 +20,7 @@ const InitialDisciplineBody = z.object({ name: z.string().min(1), minTeamSize: z.number(), maxTeamSize: z.number(), - briefDescription: z.string(), + briefDescription: z.string().min(1), fullDescription: z.string(), }); @@ -29,9 +29,7 @@ const disciplineRefiner = [ { message: "The minTeamSize must be smaller or equal to the maxTeamSize" }, ] as const; -const DisciplineBody = InitialDisciplineBody.partial({ briefDescription: true, fullDescription: true }).refine( - ...disciplineRefiner -); +const DisciplineBody = InitialDisciplineBody.partial({ fullDescription: true }).refine(...disciplineRefiner); const updateDisciplineBody = InitialDisciplineBody.partial().refine(...disciplineRefiner); const basicDiscipline = { @@ -158,17 +156,19 @@ export const createDiscipline = async (req: Request<{ eventPid: string }, {}, Cr name: DataType.STRING, minTeamSize: DataType.NUMBER, maxTeamSize: DataType.NUMBER, + briefDescription: DataType.STRING, + ["fullDescription?"]: DataType.STRING, }, result.error ) ); } - const { name, minTeamSize, maxTeamSize } = result.data; + const { name, minTeamSize, maxTeamSize, briefDescription } = result.data; try { const discipline = await prisma.discipline.create({ - data: { name, minTeamSize, maxTeamSize, event: { connect: { pid: req.params.eventPid } } }, + data: { name, minTeamSize, maxTeamSize, briefDescription, event: { connect: { pid: req.params.eventPid } } }, select: basicDiscipline, }); diff --git a/src/Controllers/group.controllers.ts b/src/Controllers/group.controllers.ts index 686ed7a..355a997 100644 --- a/src/Controllers/group.controllers.ts +++ b/src/Controllers/group.controllers.ts @@ -192,7 +192,7 @@ export const deleteGroup = async (req: Request, res: Res const { pid } = req.params; try { - prisma.group.delete({ where: { pid } }); + await prisma.group.delete({ where: { pid } }); return res.status(204).end(); } catch (e) { diff --git a/src/Controllers/organisation.controller.ts b/src/Controllers/organisation.controller.ts index 6007947..5a701fe 100644 --- a/src/Controllers/organisation.controller.ts +++ b/src/Controllers/organisation.controller.ts @@ -213,7 +213,7 @@ export const deleteOrganisation = async (req: Request, 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/Controllers/team.controller.ts b/src/Controllers/team.controller.ts index 35d6101..b49e2cb 100644 --- a/src/Controllers/team.controller.ts +++ b/src/Controllers/team.controller.ts @@ -24,8 +24,28 @@ interface CreateTeamBody { partGroupId: string; } +export const basicTeam = { + pid: true, + name: true, + discipline: { select: { pid: true } }, + roles: { + select: { + pid: true, + schema: { select: { name: true } }, + participant: { select: { pid: true } }, + }, + }, + participants: { + select: { + pid: true, + firstName: true, + lastName: true, + }, + }, +}; + export const getTeams = async (req: Request, res: Response) => { - const teams = prisma.team.findMany({ select: { pid: true, name: true, disciplineId: true } }); + const teams = await prisma.team.findMany({ select: basicTeam }); res.status(200).json(teams); }; @@ -33,13 +53,9 @@ export const getTeams = async (req: Request, res: Response) => { export const getTeam = async (req: Request, res: Response) => { const { pid } = req.params; - const team = prisma.team.findUnique({ + const team = await prisma.team.findUnique({ where: { pid }, - select: { - disciplineId: true, - name: true, - pid: true, - }, + select: basicTeam, }); res.status(200).json(team); @@ -72,7 +88,7 @@ export const updateTeam = async (req: Request, res: Response) => { } try { - const team = prisma.team.update({ + const team = await prisma.team.update({ where: { pid: body.pid, }, @@ -96,13 +112,7 @@ export const updateTeam = async (req: Request, res: Response) => { 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 } }); + await prisma.team.delete({ where: { pid } }); res.status(204).json("Welp its gone"); }; diff --git a/src/Controllers/user_auth.controller.ts b/src/Controllers/user_auth.controller.ts index 4205a20..64bb176 100644 --- a/src/Controllers/user_auth.controller.ts +++ b/src/Controllers/user_auth.controller.ts @@ -7,6 +7,7 @@ import { createInsufficientPermissionsError, DataType, generateInvalidBodyError import { generateTeamleaderJWT, requireLeaderOfTeam } from "../Middleware/auth/teamleaderAuth"; import { createRolesForTeam } from "./role.controller"; import { any, z } from "zod"; +import { basicTeam } from "./team.controller"; const TeamBody = z.object({ teamName: z.string().min(1), @@ -70,13 +71,13 @@ export const register = async (req: Request<{}, {}, CreateTeamBody>, res: Respon }, }); - //TODO: maybe use returned amount of created use? - createRolesForTeam(team.pid); + await createRolesForTeam(team.pid); const usid = nanoid(); (await mailClient).set(usid, team.pid); + // TODO: fix "eventname" verificationMail(req.body.leaderEmail, "eventname", usid); res.status(201).json({ type: "success", payload: { team } }); @@ -103,6 +104,7 @@ export const requestToken = async (req: Request, res: Response) => { (await mailClient).set(usid, team.pid); + // TODO: fix "eventname" verificationMail(team.leaderEmail, "eventname", usid); res.status(200).json({ type: "sucess", message: "Email sent!" }); diff --git a/src/Middleware/error/defaultRoutes.ts b/src/Middleware/error/defaultRoutes.ts index 4a15997..0050727 100644 --- a/src/Middleware/error/defaultRoutes.ts +++ b/src/Middleware/error/defaultRoutes.ts @@ -5,7 +5,7 @@ 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}'`, + message: `The ${req.method} HTTP method is not implemented for '${req.path}'`, _links: [ { rel: "root", diff --git a/src/Routes/participant.routes.ts b/src/Routes/participant.routes.ts index 532387a..fc30af0 100644 --- a/src/Routes/participant.routes.ts +++ b/src/Routes/participant.routes.ts @@ -5,8 +5,8 @@ import { requireAuthentication, requireConfiguredAuthentication } from "../Middl const router = express.Router(); -teamRouter.post<"/:teamPid/participant/", { teamPid: string }>( - "/:teamPid/participant/", +teamRouter.post<"/:teamPid/participants/", { teamPid: string }>( + "/:teamPid/participants/", requireAuthentication, requireConfiguredAuthentication({ optional: true, type: { admin: true, teamleader: true } }), createParticipant diff --git a/src/Routes/role.routes.ts b/src/Routes/role.routes.ts index dff8cc7..c2bc58f 100644 --- a/src/Routes/role.routes.ts +++ b/src/Routes/role.routes.ts @@ -1,11 +1,22 @@ import Express from "express"; import { assignParticipantToRole, getRolesForTeam } from "../Controllers/role.controller"; +import { requireAuthentication, requireConfiguredAuthentication } from "../Middleware/auth/auth"; const router = Express.Router(); //TO DO: maybe transfer getRolesForTeam to team router -> Seconded -router.get<"team/:teamPid/", { teamPid: string }>("team/:teamPid/", getRolesForTeam); +router.get<"team/:teamPid/", { teamPid: string }>( + "team/:teamPid/", + requireAuthentication, + requireConfiguredAuthentication({ optional: true, type: { admin: true, teamleader: true } }), + getRolesForTeam +); -router.put<"/:pid/participant", { pid: string }>("/:pid/participant", assignParticipantToRole); +router.put<"/:pid/participant", { pid: string }>( + "/:pid/participant", + requireAuthentication, + requireConfiguredAuthentication({ optional: true, type: { admin: true, teamleader: true } }), + assignParticipantToRole +); export default router; diff --git a/src/Routes/team.routes.ts b/src/Routes/team.routes.ts index a12afb1..ff1ebac 100644 --- a/src/Routes/team.routes.ts +++ b/src/Routes/team.routes.ts @@ -6,9 +6,19 @@ import { deleteTeam, getTeam, getTeams, updateTeam } from "../Controllers/team.c const router = express.Router(); router.get("/", requireConfiguredAuthentication({ type: "admin", optional: false }), getTeams); -router.get("/:id", getTeam); +router.get( + "/:id", + requireAuthentication, + requireConfiguredAuthentication({ optional: true, type: { admin: true, teamleader: true } }), + getTeam +); router.put("/", requireTeamleaderAuthentication, updateTeam); -router.delete<"/:pid/", { pid: string }>("/:pid/", requireAuthentication, requireTeamleaderAuthentication, deleteTeam); +router.delete<"/:pid/", { pid: string }>( + "/:pid/", + requireAuthentication, + requireConfiguredAuthentication({ optional: true, type: { admin: true, teamleader: true } }), + deleteTeam +); export default router; diff --git a/src/app.ts b/src/app.ts index 9121346..d41ffba 100644 --- a/src/app.ts +++ b/src/app.ts @@ -14,7 +14,9 @@ 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 teamRouter from "./Routes/team.routes"; +import roleRouter from "./Routes/role.routes"; +import participantRouter from "./Routes/participant.routes"; import { notFoundHandler, rootHandler } from "./Middleware/error/defaultRoutes"; // Set up async error handling @@ -87,7 +89,11 @@ async function main() { app.use("/api/users", userRouter); - app.use("/api/teams", TeamRouter); + app.use("/api/teams", teamRouter); + + app.use("/api/roles", roleRouter); + + app.use("/api/participants", participantRouter); app.get("/", rootHandler); app.get("/api", rootHandler); diff --git a/src/lib/mail.ts b/src/lib/mail.ts index c55fd8e..7c96463 100644 --- a/src/lib/mail.ts +++ b/src/lib/mail.ts @@ -66,6 +66,7 @@ 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: the process.env.DOMAIN is undefined in Development mode !! verificationLink = "https://" + ("api." + process.env.DOMAIN ?? "localhost:3000/api") + "/users/verify/" + verificationLink; const message = Handlebars.compile(raw); From b3a1ec5fa531624c827835a6b460301bba8058f1 Mon Sep 17 00:00:00 2001 From: Laurin <60652077+Flexla54@users.noreply.github.com> Date: Fri, 3 Jun 2022 22:25:21 +0200 Subject: [PATCH 41/55] small fixes --- src/Controllers/participant.controller.ts | 25 ++++++++++++----------- src/Routes/participant.routes.ts | 14 +++++-------- src/Routes/role.routes.ts | 6 ++---- src/Routes/team.routes.ts | 6 ++---- 4 files changed, 22 insertions(+), 29 deletions(-) diff --git a/src/Controllers/participant.controller.ts b/src/Controllers/participant.controller.ts index 5c7a338..f5dfaa4 100644 --- a/src/Controllers/participant.controller.ts +++ b/src/Controllers/participant.controller.ts @@ -18,13 +18,14 @@ import { requireConfiguredAuthentication, requireResponsibleForGroup } from "../ // 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({ +const InitialParticipant = z.object({ firstName: z.string(), lastName: z.string(), - groupId: z.string().uuid(), - //job: z.enum(["TEAMLEADER", "MEMBER"]), + groupPid: z.string().uuid(), }); +const ParticipantBody = InitialParticipant.extend({ teamPid: z.string().uuid() }); + const returnedParticipant = { pid: true, firstName: true, @@ -45,7 +46,7 @@ const returnedParticipant = { } as const; // at: POST api/teams/:teamPid/participant/ -export const createParticipant = async (req: Request<{ teamPid: string }>, res: Response) => { +export const createParticipant = async (req: Request, res: Response) => { const result = ParticipantBody.safeParse(req.body); if (result.success === false) { @@ -54,14 +55,14 @@ export const createParticipant = async (req: Request<{ teamPid: string }>, res: { firstname: DataType.STRING, lastName: DataType.STRING, - groupId: DataType.UUID, + groupPid: DataType.UUID, + teamPid: DataType.UUID, }, result.error ) ); } const body = result.data; - const { teamPid } = req.params; try { const participant = await prisma.participant.create({ @@ -69,8 +70,8 @@ export const createParticipant = async (req: Request<{ teamPid: string }>, res: firstName: body.firstName, lastName: body.lastName, relevance: "MEMBER", - group: { connect: { pid: body.groupId } }, - team: { connect: { pid: teamPid } }, + group: { connect: { pid: body.groupPid } }, + team: { connect: { pid: body.teamPid } }, }, select: returnedParticipant, }); @@ -83,7 +84,7 @@ export const createParticipant = async (req: Request<{ teamPid: string }>, res: if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { return res .status(404) - .json(generateError(`Could not link to team with ID '${teamPid}, or group with ID ${body.groupId}'`)); + .json(generateError(`Could not link to team with ID '${body.teamPid}, or group with ID ${body.groupPid}'`)); } throw e; } @@ -91,7 +92,7 @@ export const createParticipant = async (req: Request<{ teamPid: string }>, res: // at: PATCH api/participants/:pid/ export const updateParticipant = async (req: Request<{ pid: string }>, res: Response) => { - const result = ParticipantBody.partial().safeParse(req.body); + const result = InitialParticipant.partial().safeParse(req.body); if (result.success === false) { return res.status(400).json( @@ -99,7 +100,7 @@ export const updateParticipant = async (req: Request<{ pid: string }>, res: Resp { firstname: DataType.STRING, lastName: DataType.STRING, - groupId: DataType.UUID, + groupPid: DataType.UUID, }, result.error ) @@ -115,7 +116,7 @@ export const updateParticipant = async (req: Request<{ pid: string }>, res: Resp data: { firstName: body.firstName, lastName: body.lastName, - group: { connect: { pid: body.groupId } }, + group: { connect: { pid: body.groupPid } }, }, select: returnedParticipant, }); diff --git a/src/Routes/participant.routes.ts b/src/Routes/participant.routes.ts index fc30af0..7ba300f 100644 --- a/src/Routes/participant.routes.ts +++ b/src/Routes/participant.routes.ts @@ -1,28 +1,24 @@ import express from "express"; -import teamRouter from "./team.routes"; import { createParticipant, deleteParticipant, updateParticipant } from "../Controllers/participant.controller"; import { requireAuthentication, requireConfiguredAuthentication } from "../Middleware/auth/auth"; const router = express.Router(); -teamRouter.post<"/:teamPid/participants/", { teamPid: string }>( - "/:teamPid/participants/", - requireAuthentication, - requireConfiguredAuthentication({ optional: true, type: { admin: true, teamleader: true } }), +router.post( + "/", + requireConfiguredAuthentication({ optional: false, type: { admin: true, teamleader: true } }), createParticipant ); router.patch<"/:pid/", { pid: string }>( "/:pid/", - requireAuthentication, - requireConfiguredAuthentication({ optional: true, type: { admin: true, teamleader: true } }), + requireConfiguredAuthentication({ optional: false, type: { admin: true, teamleader: true } }), updateParticipant ); router.delete<"/:pid/", { pid: string }>( "/:pid/", - requireAuthentication, - requireConfiguredAuthentication({ optional: true, type: { admin: true, teamleader: true } }), + requireConfiguredAuthentication({ optional: false, type: { admin: true, teamleader: true } }), deleteParticipant ); diff --git a/src/Routes/role.routes.ts b/src/Routes/role.routes.ts index c2bc58f..0ad57ce 100644 --- a/src/Routes/role.routes.ts +++ b/src/Routes/role.routes.ts @@ -7,15 +7,13 @@ const router = Express.Router(); //TO DO: maybe transfer getRolesForTeam to team router -> Seconded router.get<"team/:teamPid/", { teamPid: string }>( "team/:teamPid/", - requireAuthentication, - requireConfiguredAuthentication({ optional: true, type: { admin: true, teamleader: true } }), + requireConfiguredAuthentication({ optional: false, type: { admin: true, teamleader: true } }), getRolesForTeam ); router.put<"/:pid/participant", { pid: string }>( "/:pid/participant", - requireAuthentication, - requireConfiguredAuthentication({ optional: true, type: { admin: true, teamleader: true } }), + requireConfiguredAuthentication({ optional: false, type: { admin: true, teamleader: true } }), assignParticipantToRole ); diff --git a/src/Routes/team.routes.ts b/src/Routes/team.routes.ts index ff1ebac..a8ffebe 100644 --- a/src/Routes/team.routes.ts +++ b/src/Routes/team.routes.ts @@ -8,16 +8,14 @@ const router = express.Router(); router.get("/", requireConfiguredAuthentication({ type: "admin", optional: false }), getTeams); router.get( "/:id", - requireAuthentication, - requireConfiguredAuthentication({ optional: true, type: { admin: true, teamleader: true } }), + requireConfiguredAuthentication({ optional: false, type: { admin: true, teamleader: true } }), getTeam ); router.put("/", requireTeamleaderAuthentication, updateTeam); router.delete<"/:pid/", { pid: string }>( "/:pid/", - requireAuthentication, - requireConfiguredAuthentication({ optional: true, type: { admin: true, teamleader: true } }), + requireConfiguredAuthentication({ optional: false, type: { admin: true, teamleader: true } }), deleteTeam ); From 619b46a6aa5ab763c6cb9be36c303986205a90f3 Mon Sep 17 00:00:00 2001 From: Laurin <60652077+Flexla54@users.noreply.github.com> Date: Sat, 4 Jun 2022 01:06:09 +0200 Subject: [PATCH 42/55] added leaderOfTeam check --- src/Controllers/participant.controller.ts | 34 ++++++++++++----------- src/Controllers/role.controller.ts | 19 +++++++++---- src/Controllers/team.controller.ts | 28 +++++++++++-------- src/Routes/participant.routes.ts | 2 +- src/Routes/role.routes.ts | 2 +- src/Routes/team.routes.ts | 9 ++++-- 6 files changed, 56 insertions(+), 38 deletions(-) diff --git a/src/Controllers/participant.controller.ts b/src/Controllers/participant.controller.ts index f5dfaa4..2e0a56a 100644 --- a/src/Controllers/participant.controller.ts +++ b/src/Controllers/participant.controller.ts @@ -1,22 +1,11 @@ import prisma from "../lib/prisma"; import { z } from "zod"; import { Request, Response } from "express"; -import { - AUTH_ERROR, - createInsufficientPermissionsError, - DataType, - generateError, - generateInvalidBodyError, -} from "./common"; -import { Job, Prisma } from "@prisma/client"; +import { DataType, generateError, generateInvalidBodyError } from "./common"; +import { Prisma } from "@prisma/client"; import { PrismaClientKnownRequestError } from "@prisma/client/runtime"; import NotFoundError from "../Middleware/error/NotFoundError"; -import { requireConfiguredAuthentication, requireResponsibleForGroup } from "../Middleware/auth/auth"; - -//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 +import { requireLeaderOfTeam } from "../Middleware/auth/teamleaderAuth"; const InitialParticipant = z.object({ firstName: z.string(), @@ -45,7 +34,7 @@ const returnedParticipant = { }, } as const; -// at: POST api/teams/:teamPid/participant/ +// at: POST api/participants/ export const createParticipant = async (req: Request, res: Response) => { const result = ParticipantBody.safeParse(req.body); @@ -64,6 +53,10 @@ export const createParticipant = async (req: Request, res: Response) => { } const body = result.data; + if (req.teamleader?.isAuthenticated) { + requireLeaderOfTeam(req.teamleader, body.teamPid); + } + try { const participant = await prisma.participant.create({ data: { @@ -92,6 +85,12 @@ export const createParticipant = async (req: Request, res: Response) => { // at: PATCH api/participants/:pid/ export const updateParticipant = async (req: Request<{ pid: string }>, res: Response) => { + const { pid } = req.params; + + if (req.teamleader?.isAuthenticated) { + requireLeaderOfTeam(req.teamleader, pid); + } + const result = InitialParticipant.partial().safeParse(req.body); if (result.success === false) { @@ -108,7 +107,6 @@ export const updateParticipant = async (req: Request<{ pid: string }>, res: Resp } const body = result.data; - const { pid } = req.params; try { const participant = await prisma.participant.update({ @@ -138,6 +136,10 @@ export const updateParticipant = async (req: Request<{ pid: string }>, res: Resp export const deleteParticipant = async (req: Request<{ pid: string }>, res: Response) => { const { pid } = req.params; + if (req.teamleader?.isAuthenticated) { + requireLeaderOfTeam(req.teamleader, pid); + } + try { await prisma.participant.delete({ where: { pid } }); diff --git a/src/Controllers/role.controller.ts b/src/Controllers/role.controller.ts index 353965d..294db70 100644 --- a/src/Controllers/role.controller.ts +++ b/src/Controllers/role.controller.ts @@ -56,7 +56,9 @@ export async function createRolesForTeam(teamPid: string) { export async function getRolesForTeam(req: Request<{ teamPid: string }>, res: Response) { const teamPid = req.params.teamPid; - requireLeaderOfTeam(req.teamleader, teamPid); + if (req.teamleader?.isAuthenticated) { + requireLeaderOfTeam(req.teamleader, teamPid); + } const roles = await prisma.role.findMany({ where: { team: { pid: teamPid } }, @@ -82,6 +84,12 @@ const AssignParticipantToRoleBody = z.object({ // requires: auth(leader of the team) export async function assignParticipantToRole(req: Request<{ pid: string }>, res: Response) { + const { pid } = req.params; + + if (req.teamleader?.isAuthenticated) { + requireLeaderOfTeam(req.teamleader, pid); + } + const zBody = AssignParticipantToRoleBody.safeParse(req.body); if (zBody.success === false) { @@ -89,10 +97,9 @@ export async function assignParticipantToRole(req: Request<{ pid: string }>, res } const { participantPid } = zBody.data; - const rolePid = req.params.pid; const schema = await prisma.role.findFirst({ - where: { pid: rolePid, team: { participants: { some: { pid: participantPid } } } }, + where: { pid, team: { participants: { some: { pid: participantPid } } } }, select: { participant: { select: { pid: true, firstName: true, lastName: true } } }, }); @@ -100,18 +107,18 @@ export async function assignParticipantToRole(req: Request<{ pid: string }>, res 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}'`, + message: `No role with the ID '${pid}' could be found in the scope of the participant with the ID '${participantPid}'`, }, }); } // 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 } } } }); + await prisma.role.update({ where: { pid }, 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}'`, + message: `The participant with ID '${participantPid}' was successfully assigned to the role with ID '${pid}'`, ...(schema.participant ? { unassigned: schema.participant } : {}), }, }); diff --git a/src/Controllers/team.controller.ts b/src/Controllers/team.controller.ts index 34e03dc..88d1271 100644 --- a/src/Controllers/team.controller.ts +++ b/src/Controllers/team.controller.ts @@ -1,8 +1,7 @@ import { Request, Response } from "express"; import prisma from "../lib/prisma"; -import { createInsufficientPermissionsError, DataType, generateInvalidBodyError } from "./common"; +import { DataType, generateInvalidBodyError } from "./common"; import { requireLeaderOfTeam } from "../Middleware/auth/teamleaderAuth"; -import { z } from "zod"; import { TeamBody } from "./user_auth.controller"; import { Prisma } from "@prisma/client"; import NotFoundError from "../Middleware/error/NotFoundError"; @@ -36,6 +35,10 @@ export const getTeams = async (req: Request, res: Response) => { export const getTeam = async (req: Request, res: Response) => { const { pid } = req.params; + if (req.teamleader?.isAuthenticated) { + requireLeaderOfTeam(req.teamleader, pid); + } + const team = await prisma.team.findUnique({ where: { pid }, select: basicTeam, @@ -45,9 +48,13 @@ export const getTeam = async (req: Request, res: Response) => { }; 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); + const { pid } = req.params; + + if (req.teamleader?.isAuthenticated) { + requireLeaderOfTeam(req.teamleader, pid); + } + + const result = TeamBody.omit({ partGroupId: true, partFirstName: true, partLastName: true }).safeParse(req.body); if (result.success === false) { return res.status(400).json( @@ -64,12 +71,10 @@ export const updateTeam = async (req: Request, res: Response) => { const body = result.data; - requireLeaderOfTeam(req.teamleader, body.pid); - try { const team = await prisma.team.update({ where: { - pid: body.pid, + pid: pid, }, data: { name: body.teamName, @@ -81,7 +86,7 @@ export const updateTeam = async (req: Request, res: Response) => { res.status(204).json({ type: "success", payload: { team } }); } catch (e) { if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") { - throw new NotFoundError("team", body.pid); + throw new NotFoundError("team", pid); } throw e; @@ -91,8 +96,9 @@ export const updateTeam = async (req: Request, res: Response) => { export const deleteTeam = async (req: Request, res: Response) => { const { pid } = req.params; - // TODO: accept admin auth - requireLeaderOfTeam(req.teamleader, pid); + if (req.teamleader?.isAuthenticated) { + requireLeaderOfTeam(req.teamleader, pid); + } await prisma.team.delete({ where: { pid } }); diff --git a/src/Routes/participant.routes.ts b/src/Routes/participant.routes.ts index 7ba300f..d35fae6 100644 --- a/src/Routes/participant.routes.ts +++ b/src/Routes/participant.routes.ts @@ -1,6 +1,6 @@ import express from "express"; import { createParticipant, deleteParticipant, updateParticipant } from "../Controllers/participant.controller"; -import { requireAuthentication, requireConfiguredAuthentication } from "../Middleware/auth/auth"; +import { requireConfiguredAuthentication } from "../Middleware/auth/auth"; const router = express.Router(); diff --git a/src/Routes/role.routes.ts b/src/Routes/role.routes.ts index 0ad57ce..6ca39ae 100644 --- a/src/Routes/role.routes.ts +++ b/src/Routes/role.routes.ts @@ -1,6 +1,6 @@ import Express from "express"; import { assignParticipantToRole, getRolesForTeam } from "../Controllers/role.controller"; -import { requireAuthentication, requireConfiguredAuthentication } from "../Middleware/auth/auth"; +import { requireConfiguredAuthentication } from "../Middleware/auth/auth"; const router = Express.Router(); diff --git a/src/Routes/team.routes.ts b/src/Routes/team.routes.ts index a8ffebe..b419c1c 100644 --- a/src/Routes/team.routes.ts +++ b/src/Routes/team.routes.ts @@ -1,6 +1,5 @@ import express from "express"; -import { requireAuthentication, requireConfiguredAuthentication } from "../Middleware/auth/auth"; -import { requireTeamleaderAuthentication } from "../Middleware/auth/teamleaderAuth"; +import { requireConfiguredAuthentication } from "../Middleware/auth/auth"; import { deleteTeam, getTeam, getTeams, updateTeam } from "../Controllers/team.controller"; const router = express.Router(); @@ -12,7 +11,11 @@ router.get( getTeam ); -router.put("/", requireTeamleaderAuthentication, updateTeam); +router.put<"/:pid/", { pid: string }>( + "/:pid/", + requireConfiguredAuthentication({ optional: false, type: { admin: true, teamleader: true } }), + updateTeam +); router.delete<"/:pid/", { pid: string }>( "/:pid/", requireConfiguredAuthentication({ optional: false, type: { admin: true, teamleader: true } }), From 1d2794c75c800b88b7f0c5be6318718b2c4fd1d1 Mon Sep 17 00:00:00 2001 From: Laurin <60652077+Flexla54@users.noreply.github.com> Date: Sun, 5 Jun 2022 03:35:37 +0200 Subject: [PATCH 43/55] patched requireLeaderOfTeam usecases --- src/Controllers/participant.controller.ts | 43 ++++++++++++++++++----- src/Controllers/role.controller.ts | 19 +++++----- src/Controllers/team.controller.ts | 6 +++- src/Routes/role.routes.ts | 9 +---- src/Routes/team.routes.ts | 11 ++++-- 5 files changed, 61 insertions(+), 27 deletions(-) diff --git a/src/Controllers/participant.controller.ts b/src/Controllers/participant.controller.ts index 2e0a56a..ee53b5e 100644 --- a/src/Controllers/participant.controller.ts +++ b/src/Controllers/participant.controller.ts @@ -1,5 +1,5 @@ import prisma from "../lib/prisma"; -import { z } from "zod"; +import { string, z } from "zod"; import { Request, Response } from "express"; import { DataType, generateError, generateInvalidBodyError } from "./common"; import { Prisma } from "@prisma/client"; @@ -10,7 +10,7 @@ import { requireLeaderOfTeam } from "../Middleware/auth/teamleaderAuth"; const InitialParticipant = z.object({ firstName: z.string(), lastName: z.string(), - groupPid: z.string().uuid(), + groupPid: z.string().min(1).uuid(), }); const ParticipantBody = InitialParticipant.extend({ teamPid: z.string().uuid() }); @@ -58,6 +58,21 @@ export const createParticipant = async (req: Request, res: Response) => { } try { + const discipline = await prisma.team.findUnique({ + where: { pid: body.teamPid }, + select: { discipline: true } + }); + + const maxteamsize = discipline?.discipline.maxTeamSize; + + const userCount = await prisma.participant.count({ + where: { team: { pid: body.teamPid } } + }); + + if (maxteamsize == userCount) { + return res.status(418).json({ type: "error", payload: "The team has reached the limit of participants!" }); + } + const participant = await prisma.participant.create({ data: { firstName: body.firstName, @@ -69,10 +84,7 @@ export const createParticipant = async (req: Request, res: Response) => { select: returnedParticipant, }); - return res.status(201).json({ - type: "success", - payload: { participant }, - }); + return res.status(201).json({ type: "success", payload: { participant }, }); } catch (e) { if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { return res @@ -86,9 +98,10 @@ export const createParticipant = async (req: Request, res: Response) => { // at: PATCH api/participants/:pid/ export const updateParticipant = async (req: Request<{ pid: string }>, res: Response) => { const { pid } = req.params; + const teamPid = await getTeamPidByParticipantPid(pid); if (req.teamleader?.isAuthenticated) { - requireLeaderOfTeam(req.teamleader, pid); + requireLeaderOfTeam(req.teamleader, teamPid); } const result = InitialParticipant.partial().safeParse(req.body); @@ -135,9 +148,10 @@ export const updateParticipant = async (req: Request<{ pid: string }>, res: Resp // at: DELETE api/participants/:pid/ export const deleteParticipant = async (req: Request<{ pid: string }>, res: Response) => { const { pid } = req.params; + const teamPid = await getTeamPidByParticipantPid(pid); if (req.teamleader?.isAuthenticated) { - requireLeaderOfTeam(req.teamleader, pid); + requireLeaderOfTeam(req.teamleader, teamPid); } try { @@ -152,3 +166,16 @@ export const deleteParticipant = async (req: Request<{ pid: string }>, res: Resp throw e; } }; + +export const getTeamPidByParticipantPid = async function (partPid: string) { + const participant = await prisma.participant.findUnique({ + where: { pid: partPid }, + select: { team: { select: { pid: true } } } + }); + + if (!participant) { + throw new NotFoundError("participant", partPid); + } + + return participant.team.pid; +} diff --git a/src/Controllers/role.controller.ts b/src/Controllers/role.controller.ts index 294db70..250fa97 100644 --- a/src/Controllers/role.controller.ts +++ b/src/Controllers/role.controller.ts @@ -5,6 +5,7 @@ import prisma from "../lib/prisma"; import { requireLeaderOfTeam, requireResponsibleForParticipant } from "../Middleware/auth/teamleaderAuth"; import NotFoundError from "../Middleware/error/NotFoundError"; import { createInsufficientPermissionsError, DataType, generateInvalidBodyError } from "./common"; +import { getTeamPidByParticipantPid } from "./participant.controller"; require("express-async-errors"); @@ -53,15 +54,15 @@ export async function createRolesForTeam(teamPid: string) { return roles.count; } -export async function getRolesForTeam(req: Request<{ teamPid: string }>, res: Response) { - const teamPid = req.params.teamPid; +export async function getRolesForTeam(req: Request<{ pid: string }>, res: Response) { + const pid = req.params.pid; if (req.teamleader?.isAuthenticated) { - requireLeaderOfTeam(req.teamleader, teamPid); + requireLeaderOfTeam(req.teamleader, pid); } const roles = await prisma.role.findMany({ - where: { team: { pid: teamPid } }, + where: { team: { pid } }, select: { pid: true, score: true, @@ -80,16 +81,13 @@ export async function getRolesForTeam(req: Request<{ teamPid: string }>, res: Re const AssignParticipantToRoleBody = z.object({ participantPid: z.string().uuid(), + teamPid: z.string().uuid(), }); // requires: auth(leader of the team) export async function assignParticipantToRole(req: Request<{ pid: string }>, res: Response) { const { pid } = req.params; - if (req.teamleader?.isAuthenticated) { - requireLeaderOfTeam(req.teamleader, pid); - } - const zBody = AssignParticipantToRoleBody.safeParse(req.body); if (zBody.success === false) { @@ -97,6 +95,11 @@ export async function assignParticipantToRole(req: Request<{ pid: string }>, res } const { participantPid } = zBody.data; + const teamPid = await getTeamPidByParticipantPid(pid); + + if (req.teamleader?.isAuthenticated) { + requireLeaderOfTeam(req.teamleader, teamPid); + } const schema = await prisma.role.findFirst({ where: { pid, team: { participants: { some: { pid: participantPid } } } }, diff --git a/src/Controllers/team.controller.ts b/src/Controllers/team.controller.ts index 88d1271..15a8044 100644 --- a/src/Controllers/team.controller.ts +++ b/src/Controllers/team.controller.ts @@ -32,7 +32,7 @@ export const getTeams = async (req: Request, res: Response) => { res.status(200).json({ type: "success", payload: { teams } }); }; -export const getTeam = async (req: Request, res: Response) => { +export const getTeam = async (req: Request<{ pid: string }>, res: Response) => { const { pid } = req.params; if (req.teamleader?.isAuthenticated) { @@ -44,6 +44,10 @@ export const getTeam = async (req: Request, res: Response) => { select: basicTeam, }); + if (!team) { + throw new NotFoundError("team", pid); + } + res.status(200).json({ type: "success", payload: { team } }); }; diff --git a/src/Routes/role.routes.ts b/src/Routes/role.routes.ts index 6ca39ae..b23d118 100644 --- a/src/Routes/role.routes.ts +++ b/src/Routes/role.routes.ts @@ -4,17 +4,10 @@ import { requireConfiguredAuthentication } from "../Middleware/auth/auth"; const router = Express.Router(); -//TO DO: maybe transfer getRolesForTeam to team router -> Seconded -router.get<"team/:teamPid/", { teamPid: string }>( - "team/:teamPid/", - requireConfiguredAuthentication({ optional: false, type: { admin: true, teamleader: true } }), - getRolesForTeam -); - router.put<"/:pid/participant", { pid: string }>( "/:pid/participant", requireConfiguredAuthentication({ optional: false, type: { admin: true, teamleader: true } }), assignParticipantToRole ); -export default router; +export default router; \ No newline at end of file diff --git a/src/Routes/team.routes.ts b/src/Routes/team.routes.ts index b419c1c..a49789a 100644 --- a/src/Routes/team.routes.ts +++ b/src/Routes/team.routes.ts @@ -1,12 +1,13 @@ import express from "express"; import { requireConfiguredAuthentication } from "../Middleware/auth/auth"; import { deleteTeam, getTeam, getTeams, updateTeam } from "../Controllers/team.controller"; +import { getRolesForTeam } from "../Controllers/role.controller"; const router = express.Router(); router.get("/", requireConfiguredAuthentication({ type: "admin", optional: false }), getTeams); -router.get( - "/:id", +router.get<"/:pid/", { pid: string }>( + "/:pid/", requireConfiguredAuthentication({ optional: false, type: { admin: true, teamleader: true } }), getTeam ); @@ -22,4 +23,10 @@ router.delete<"/:pid/", { pid: string }>( deleteTeam ); +router.get<"/:pid/roles", { pid: string }>( + "/:pid/roles", + requireConfiguredAuthentication({ optional: false, type: { admin: true, teamleader: true } }), + getRolesForTeam +); + export default router; From d0a1d4a2f1040cb89da17da7a731c59e5a5bde0f Mon Sep 17 00:00:00 2001 From: Laurin <60652077+Flexla54@users.noreply.github.com> Date: Sun, 5 Jun 2022 21:08:22 +0200 Subject: [PATCH 44/55] small fixes --- src/Controllers/admin.controller.ts | 2 +- src/Controllers/group.controllers.ts | 43 +++++++++++++++++----------- src/Controllers/team.controller.ts | 3 +- 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/src/Controllers/admin.controller.ts b/src/Controllers/admin.controller.ts index cdbc2ed..4048a30 100644 --- a/src/Controllers/admin.controller.ts +++ b/src/Controllers/admin.controller.ts @@ -27,7 +27,7 @@ export const getAllAdmins = async (req: Request, res: Response) => { } // TODO: Add exception handling - const users = await prisma.admin.findMany({ select: { pid: true, name: true, permission_level: true } }); + const users = await prisma.admin.findMany({ select: { pid: true, name: true, permission_level: true, groups: { select: { pid: true } } } }); res.status(200).json({ type: "success", diff --git a/src/Controllers/group.controllers.ts b/src/Controllers/group.controllers.ts index 355a997..2a3c6c3 100644 --- a/src/Controllers/group.controllers.ts +++ b/src/Controllers/group.controllers.ts @@ -25,10 +25,19 @@ const updateGroupBody = z const basicGroup = { pid: true, name: true, - level: true, organisation: { select: { pid: true, name: true } }, } as const; +const detailedGroup = { + pid: true, + name: true, + level: true, + user_limit: true, + organisation: { select: { pid: true, name: true } }, + participants: { select: { pid: true, firstName: true, lastName: true } }, + admins: { select: { pid: true, name: true } }, +} + export const _getAllGroups = async (res: Response, organisationId: string | undefined) => { const groups = await prisma.group.findMany({ where: { organisation: { pid: organisationId } }, @@ -77,12 +86,12 @@ export const getGroup = async (req: Request, res: Response) where: { pid }, select: req.auth?.isAuthenticated ? { - pid: true, - name: true, - organisation: { select: { pid: true, name: true } }, - admins: { select: { pid: true, name: true } }, - participants: { select: { pid: true } }, - } + pid: true, + name: true, + organisation: { select: { pid: true, name: true } }, + admins: { select: { pid: true, name: true } }, + participants: { select: { pid: true } }, + } : basicGroup, }); @@ -101,15 +110,15 @@ export const getGroup = async (req: Request, res: Response) }, ...(req.auth?.isAuthenticated ? { - admins: group.admins?.map((admin) => ({ - ...admin, - _links: [{ rel: "self", type: "GET", href: `/api/admins/${admin.pid}` }], - })), - participants: group.participants?.map((participant) => ({ - ...participant, - _links: [{ rel: "self", type: "GET", href: `/api/participant/${participant.pid}` }], - })), - } + admins: group.admins?.map((admin) => ({ + ...admin, + _links: [{ rel: "self", type: "GET", href: `/api/admins/${admin.pid}` }], + })), + participants: group.participants?.map((participant) => ({ + ...participant, + _links: [{ rel: "self", type: "GET", href: `/api/participant/${participant.pid}` }], + })), + } : {}), }, }, @@ -164,7 +173,7 @@ export const updateGroup = async (req: Request<{ pid: string }>, res: Response) user_limit: body.user_limit, level: body.level, }, - select: basicGroup, + select: detailedGroup, }); res.status(200).json({ diff --git a/src/Controllers/team.controller.ts b/src/Controllers/team.controller.ts index 15a8044..47945e3 100644 --- a/src/Controllers/team.controller.ts +++ b/src/Controllers/team.controller.ts @@ -85,9 +85,10 @@ export const updateTeam = async (req: Request, res: Response) => { discipline: { connect: { pid: body.disciplineId } }, leaderEmail: body.leaderEmail, }, + select: basicTeam, }); - res.status(204).json({ type: "success", payload: { team } }); + res.status(200).json({ type: "success", payload: { team } }); } catch (e) { if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") { throw new NotFoundError("team", pid); From 339ecff938b4317e85870fb24747f81f49000fb6 Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Sun, 5 Jun 2022 21:42:47 +0200 Subject: [PATCH 45/55] FIX: RequireAuthentication now throws on wrong auth type --- src/Middleware/auth/auth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Middleware/auth/auth.ts b/src/Middleware/auth/auth.ts index 75e20c7..949f8bc 100644 --- a/src/Middleware/auth/auth.ts +++ b/src/Middleware/auth/auth.ts @@ -69,7 +69,7 @@ const _requireAdminAuthentication = 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("Teamleader authentication is not supported for this operation!"); } throw new AuthError("The token did not include the required information!"); From 731ab3c87a6148b325037e86162e39bd9e0b29c5 Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Mon, 6 Jun 2022 14:23:46 +0200 Subject: [PATCH 46/55] FIX: fullDescription not being set when creating discipline --- src/Controllers/discipline.controller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Controllers/discipline.controller.ts b/src/Controllers/discipline.controller.ts index c898603..5727202 100644 --- a/src/Controllers/discipline.controller.ts +++ b/src/Controllers/discipline.controller.ts @@ -164,11 +164,11 @@ export const createDiscipline = async (req: Request<{ eventPid: string }, {}, Cr ); } - const { name, minTeamSize, maxTeamSize, briefDescription } = result.data; + const { name, minTeamSize, maxTeamSize, briefDescription, fullDescription } = result.data; try { const discipline = await prisma.discipline.create({ - data: { name, minTeamSize, maxTeamSize, briefDescription, event: { connect: { pid: req.params.eventPid } } }, + data: { name, minTeamSize, maxTeamSize, briefDescription, fullDescription, event: { connect: { pid: req.params.eventPid } } }, select: basicDiscipline, }); From 60100273fbef2bb2696629c536b7f997f14c02bd Mon Sep 17 00:00:00 2001 From: Laurin <60652077+Flexla54@users.noreply.github.com> Date: Mon, 6 Jun 2022 18:34:59 +0200 Subject: [PATCH 47/55] added STANDARD admin responsibility --- src/Controllers/group.controllers.ts | 4 +-- src/Controllers/participant.controller.ts | 35 ++++++++++++---------- src/Controllers/role.controller.ts | 16 ++++++---- src/Controllers/team.controller.ts | 36 +++++++++++++++++++++-- src/Controllers/user_auth.controller.ts | 7 ++--- src/Middleware/auth/auth.ts | 16 ++++++++-- src/Middleware/auth/teamleaderAuth.ts | 3 +- src/Routes/participant.routes.ts | 5 ++-- src/Routes/role.routes.ts | 7 +++++ src/Routes/team.routes.ts | 6 ---- 10 files changed, 92 insertions(+), 43 deletions(-) diff --git a/src/Controllers/group.controllers.ts b/src/Controllers/group.controllers.ts index 2a3c6c3..102ffd5 100644 --- a/src/Controllers/group.controllers.ts +++ b/src/Controllers/group.controllers.ts @@ -3,7 +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 { requireResponsibleForGroups } from "../Middleware/auth/auth"; import NotFoundError from "../Middleware/error/NotFoundError"; import { createInsufficientPermissionsError, @@ -163,7 +163,7 @@ export const updateGroup = async (req: Request<{ pid: string }>, res: Response) const body = result.data; const { pid } = req.params; - requireResponsibleForGroup(req.auth, pid); + requireResponsibleForGroups(req.auth, pid); try { const group = await prisma.group.update({ diff --git a/src/Controllers/participant.controller.ts b/src/Controllers/participant.controller.ts index ee53b5e..2c6d242 100644 --- a/src/Controllers/participant.controller.ts +++ b/src/Controllers/participant.controller.ts @@ -5,7 +5,8 @@ import { DataType, generateError, generateInvalidBodyError } from "./common"; import { Prisma } from "@prisma/client"; import { PrismaClientKnownRequestError } from "@prisma/client/runtime"; import NotFoundError from "../Middleware/error/NotFoundError"; -import { requireLeaderOfTeam } from "../Middleware/auth/teamleaderAuth"; +import { requireLeaderOfTeam, requireResponsibleForParticipant } from "../Middleware/auth/teamleaderAuth"; +import { requireResponsibleForGroups } from "../Middleware/auth/auth"; const InitialParticipant = z.object({ firstName: z.string(), @@ -35,7 +36,9 @@ const returnedParticipant = { } as const; // at: POST api/participants/ -export const createParticipant = async (req: Request, res: Response) => { +export const createParticipant = async (req: Request<{ teamPid: string }>, res: Response) => { + const { teamPid } = req.params; + const result = ParticipantBody.safeParse(req.body); if (result.success === false) { @@ -45,7 +48,6 @@ export const createParticipant = async (req: Request, res: Response) => { firstname: DataType.STRING, lastName: DataType.STRING, groupPid: DataType.UUID, - teamPid: DataType.UUID, }, result.error ) @@ -54,7 +56,9 @@ export const createParticipant = async (req: Request, res: Response) => { const body = result.data; if (req.teamleader?.isAuthenticated) { - requireLeaderOfTeam(req.teamleader, body.teamPid); + await requireLeaderOfTeam(req.teamleader, teamPid); + } else { + requireResponsibleForGroups(req.auth, body.groupPid); } try { @@ -98,10 +102,9 @@ export const createParticipant = async (req: Request, res: Response) => { // at: PATCH api/participants/:pid/ export const updateParticipant = async (req: Request<{ pid: string }>, res: Response) => { const { pid } = req.params; - const teamPid = await getTeamPidByParticipantPid(pid); if (req.teamleader?.isAuthenticated) { - requireLeaderOfTeam(req.teamleader, teamPid); + requireResponsibleForParticipant(req.teamleader, pid); } const result = InitialParticipant.partial().safeParse(req.body); @@ -127,7 +130,7 @@ export const updateParticipant = async (req: Request<{ pid: string }>, res: Resp data: { firstName: body.firstName, lastName: body.lastName, - group: { connect: { pid: body.groupPid } }, + ...(body.groupPid ? { group: { connect: { pid: body.groupPid } } } : {}), }, select: returnedParticipant, }); @@ -148,10 +151,11 @@ export const updateParticipant = async (req: Request<{ pid: string }>, res: Resp // at: DELETE api/participants/:pid/ export const deleteParticipant = async (req: Request<{ pid: string }>, res: Response) => { const { pid } = req.params; - const teamPid = await getTeamPidByParticipantPid(pid); if (req.teamleader?.isAuthenticated) { - requireLeaderOfTeam(req.teamleader, teamPid); + requireResponsibleForParticipant(req.teamleader, pid); + } else { + await requireResponsibleForGroups(req.auth, await getGroupByParticipantPid(pid)); } try { @@ -167,15 +171,14 @@ export const deleteParticipant = async (req: Request<{ pid: string }>, res: Resp } }; -export const getTeamPidByParticipantPid = async function (partPid: string) { - const participant = await prisma.participant.findUnique({ - where: { pid: partPid }, - select: { team: { select: { pid: true } } } - }); +export async function getGroupByParticipantPid(partPid: string) { + const parti = ( + await prisma.participant.findUnique({ where: { pid: partPid }, select: { group: true } }) + )?.group.pid; - if (!participant) { + if (!parti) { throw new NotFoundError("participant", partPid); } - return participant.team.pid; + return parti; } diff --git a/src/Controllers/role.controller.ts b/src/Controllers/role.controller.ts index 250fa97..c9e55bf 100644 --- a/src/Controllers/role.controller.ts +++ b/src/Controllers/role.controller.ts @@ -1,11 +1,12 @@ -import { Prisma, Role } from "@prisma/client"; import { Request, Response } from "express"; import { z } from "zod"; import prisma from "../lib/prisma"; +import { requireResponsibleForGroups } from "../Middleware/auth/auth"; import { requireLeaderOfTeam, requireResponsibleForParticipant } from "../Middleware/auth/teamleaderAuth"; import NotFoundError from "../Middleware/error/NotFoundError"; -import { createInsufficientPermissionsError, DataType, generateInvalidBodyError } from "./common"; -import { getTeamPidByParticipantPid } from "./participant.controller"; +import { DataType, generateInvalidBodyError } from "./common"; +import { getGroupByParticipantPid } from "./participant.controller"; +import { getGroupsByTeamPid } from "./team.controller"; require("express-async-errors"); @@ -58,7 +59,9 @@ export async function getRolesForTeam(req: Request<{ pid: string }>, res: Respon const pid = req.params.pid; if (req.teamleader?.isAuthenticated) { - requireLeaderOfTeam(req.teamleader, pid); + await requireLeaderOfTeam(req.teamleader, pid); + } else { + await requireResponsibleForGroups(req.auth, await getGroupsByTeamPid(pid)); } const roles = await prisma.role.findMany({ @@ -95,10 +98,11 @@ export async function assignParticipantToRole(req: Request<{ pid: string }>, res } const { participantPid } = zBody.data; - const teamPid = await getTeamPidByParticipantPid(pid); if (req.teamleader?.isAuthenticated) { - requireLeaderOfTeam(req.teamleader, teamPid); + requireResponsibleForParticipant(req.teamleader, participantPid); + } else { + await requireResponsibleForGroups(req.auth, await getGroupByParticipantPid(participantPid)); } const schema = await prisma.role.findFirst({ diff --git a/src/Controllers/team.controller.ts b/src/Controllers/team.controller.ts index 47945e3..3db62bd 100644 --- a/src/Controllers/team.controller.ts +++ b/src/Controllers/team.controller.ts @@ -5,6 +5,7 @@ import { requireLeaderOfTeam } from "../Middleware/auth/teamleaderAuth"; import { TeamBody } from "./user_auth.controller"; import { Prisma } from "@prisma/client"; import NotFoundError from "../Middleware/error/NotFoundError"; +import { requireResponsibleForGroups } from "../Middleware/auth/auth"; export const basicTeam = { pid: true, @@ -36,7 +37,9 @@ export const getTeam = async (req: Request<{ pid: string }>, res: Response) => { const { pid } = req.params; if (req.teamleader?.isAuthenticated) { - requireLeaderOfTeam(req.teamleader, pid); + await requireLeaderOfTeam(req.teamleader, pid); + } else { + await requireResponsibleForGroups(req.auth, pid); } const team = await prisma.team.findUnique({ @@ -55,7 +58,9 @@ export const updateTeam = async (req: Request, res: Response) => { const { pid } = req.params; if (req.teamleader?.isAuthenticated) { - requireLeaderOfTeam(req.teamleader, pid); + await requireLeaderOfTeam(req.teamleader, pid); + } else { + await requireResponsibleForGroups(req.auth, await getGroupsByTeamPid(pid)); } const result = TeamBody.omit({ partGroupId: true, partFirstName: true, partLastName: true }).safeParse(req.body); @@ -102,10 +107,35 @@ export const deleteTeam = async (req: Request, res: Response) => { const { pid } = req.params; if (req.teamleader?.isAuthenticated) { - requireLeaderOfTeam(req.teamleader, pid); + await requireLeaderOfTeam(req.teamleader, pid); + } else { + await requireResponsibleForGroups(req.auth, await getGroupsByTeamPid(pid)); } await prisma.team.delete({ where: { pid } }); res.status(204).json({ type: "success", payload: { message: "Sucesfully deleted team" } }); }; + +export async function checkTeamExistence(teamPid: string) { + const teamCount = await prisma.team.count({ + where: { pid: teamPid, } + }); + if (teamCount == 0) { + throw new NotFoundError("team", teamPid); + } +} + +export async function getGroupsByTeamPid(teamPid: string) { + const team = ( + await prisma.team.findUnique({ where: { pid: teamPid }, select: { participants: { select: { group: true } } } }) + ); + + let groups: string[] = []; + + team?.participants.forEach(participant => { + groups.push(participant.group.pid); + }); + + return groups; +} diff --git a/src/Controllers/user_auth.controller.ts b/src/Controllers/user_auth.controller.ts index 89824e4..369f596 100644 --- a/src/Controllers/user_auth.controller.ts +++ b/src/Controllers/user_auth.controller.ts @@ -3,11 +3,10 @@ import prisma from "../lib/prisma"; import { mailClient } from "../lib/redis"; import { nanoid } from "nanoid"; import { verificationMail } from "../lib/mail"; -import { createInsufficientPermissionsError, DataType, generateError, generateInvalidBodyError } from "./common"; -import { generateTeamleaderJWT, requireLeaderOfTeam } from "../Middleware/auth/teamleaderAuth"; +import { DataType, generateError, generateInvalidBodyError } from "./common"; +import { generateTeamleaderJWT } from "../Middleware/auth/teamleaderAuth"; import { createRolesForTeam } from "./role.controller"; -import { any, z } from "zod"; -import { basicTeam } from "./team.controller"; +import { z } from "zod"; export const TeamBody = z.object({ teamName: z.string().min(1), diff --git a/src/Middleware/auth/auth.ts b/src/Middleware/auth/auth.ts index 949f8bc..411b515 100644 --- a/src/Middleware/auth/auth.ts +++ b/src/Middleware/auth/auth.ts @@ -173,12 +173,22 @@ export const requireConfiguredAuthentication = next(); }; -export function requireResponsibleForGroup(auth: AuthJWTPayload | undefined, groupPid: string) { +export function requireResponsibleForGroups(auth: AuthJWTPayload | undefined, groupPids: string[] | 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!"); + if (Array.isArray(groupPids)) { + groupPids.forEach(gr => { + if (auth?.groups.includes(gr)) { + return; + } + + throw new AuthError("The provided authorization is not valid for the requested operation!"); + }); + } else { + if (auth?.groups.includes(groupPids)) { + throw new AuthError("The provided authorization is not valid for the requested operation!"); + } } } diff --git a/src/Middleware/auth/teamleaderAuth.ts b/src/Middleware/auth/teamleaderAuth.ts index 61fc82e..f0a4737 100644 --- a/src/Middleware/auth/teamleaderAuth.ts +++ b/src/Middleware/auth/teamleaderAuth.ts @@ -85,7 +85,8 @@ export const _requireTeamleaderAuthentication = export const requireTeamleaderAuthentication = _requireTeamleaderAuthentication({ optional: false, controlled: false }); -export function requireLeaderOfTeam(auth: TeamleaderJWTPayload | undefined, teamPid: string) { +export async function requireLeaderOfTeam(auth: TeamleaderJWTPayload | undefined, teamPid: string) { + await checkTeamExistence(teamPid); if (auth?.team !== teamPid) { throw new AuthError("The provided authorization is not valid for the requested team"); } diff --git a/src/Routes/participant.routes.ts b/src/Routes/participant.routes.ts index d35fae6..2e76a2e 100644 --- a/src/Routes/participant.routes.ts +++ b/src/Routes/participant.routes.ts @@ -1,11 +1,12 @@ import express from "express"; import { createParticipant, deleteParticipant, updateParticipant } from "../Controllers/participant.controller"; import { requireConfiguredAuthentication } from "../Middleware/auth/auth"; +import teamRouter from "./team.routes"; const router = express.Router(); -router.post( - "/", +teamRouter.post<"/:teamPid/participants", { teamPid: string }>( + "/:teamPid/participants", requireConfiguredAuthentication({ optional: false, type: { admin: true, teamleader: true } }), createParticipant ); diff --git a/src/Routes/role.routes.ts b/src/Routes/role.routes.ts index b23d118..81ee739 100644 --- a/src/Routes/role.routes.ts +++ b/src/Routes/role.routes.ts @@ -1,6 +1,7 @@ import Express from "express"; import { assignParticipantToRole, getRolesForTeam } from "../Controllers/role.controller"; import { requireConfiguredAuthentication } from "../Middleware/auth/auth"; +import teamRouter from "./team.routes"; const router = Express.Router(); @@ -10,4 +11,10 @@ router.put<"/:pid/participant", { pid: string }>( assignParticipantToRole ); +teamRouter.get<"/:pid/roles", { pid: string }>( + "/:pid/roles", + requireConfiguredAuthentication({ optional: false, type: { admin: true, teamleader: true } }), + getRolesForTeam +); + export default router; \ No newline at end of file diff --git a/src/Routes/team.routes.ts b/src/Routes/team.routes.ts index a49789a..7a79ea1 100644 --- a/src/Routes/team.routes.ts +++ b/src/Routes/team.routes.ts @@ -23,10 +23,4 @@ router.delete<"/:pid/", { pid: string }>( deleteTeam ); -router.get<"/:pid/roles", { pid: string }>( - "/:pid/roles", - requireConfiguredAuthentication({ optional: false, type: { admin: true, teamleader: true } }), - getRolesForTeam -); - export default router; From 0b7756d42855d9368568a6898487c09e24c77dcc Mon Sep 17 00:00:00 2001 From: Laurin <60652077+Flexla54@users.noreply.github.com> Date: Mon, 6 Jun 2022 19:31:54 +0200 Subject: [PATCH 48/55] small fixes --- src/Controllers/admin.controller.ts | 2 + src/Controllers/group.controllers.ts | 2 + src/Controllers/organisation.controller.ts | 2 + src/Controllers/participant.controller.ts | 8 +- src/Controllers/role_schema.controller.ts | 2 + src/Controllers/team.controller.ts | 9 +- src/Controllers/user_auth.controller.ts | 2 + src/Middleware/auth/teamleaderAuth.ts | 95 +++++++++++----------- 8 files changed, 70 insertions(+), 52 deletions(-) diff --git a/src/Controllers/admin.controller.ts b/src/Controllers/admin.controller.ts index 4048a30..f942845 100644 --- a/src/Controllers/admin.controller.ts +++ b/src/Controllers/admin.controller.ts @@ -5,6 +5,8 @@ import argon2 from "argon2"; import { AUTH_ERROR, createInsufficientPermissionsError, DataType, generateInvalidBodyError } from "./common"; import { authClient } from "../lib/redis"; +require("express-async-errors"); + export const regenerateRevision = async (pid: string) => { // TOOO: Add error handling const { revision } = await prisma.admin.update({ diff --git a/src/Controllers/group.controllers.ts b/src/Controllers/group.controllers.ts index 102ffd5..6c2c2ea 100644 --- a/src/Controllers/group.controllers.ts +++ b/src/Controllers/group.controllers.ts @@ -14,6 +14,8 @@ import { handleCreateByName, } from "./common"; +require("express-async-errors"); + const updateGroupBody = z .object({ name: z.string().min(1), diff --git a/src/Controllers/organisation.controller.ts b/src/Controllers/organisation.controller.ts index 5a701fe..d21ac13 100644 --- a/src/Controllers/organisation.controller.ts +++ b/src/Controllers/organisation.controller.ts @@ -14,6 +14,8 @@ import { PrismaClientKnownRequestError } from "@prisma/client/runtime"; import { Prisma } from "@prisma/client"; import NotFoundError from "../Middleware/error/NotFoundError"; +require("express-async-errors"); + function validateOranisationName(name: string) { return name.length > 0; } diff --git a/src/Controllers/participant.controller.ts b/src/Controllers/participant.controller.ts index 2c6d242..40d5721 100644 --- a/src/Controllers/participant.controller.ts +++ b/src/Controllers/participant.controller.ts @@ -8,6 +8,8 @@ import NotFoundError from "../Middleware/error/NotFoundError"; import { requireLeaderOfTeam, requireResponsibleForParticipant } from "../Middleware/auth/teamleaderAuth"; import { requireResponsibleForGroups } from "../Middleware/auth/auth"; +require("express-async-errors"); + const InitialParticipant = z.object({ firstName: z.string(), lastName: z.string(), @@ -104,7 +106,7 @@ export const updateParticipant = async (req: Request<{ pid: string }>, res: Resp const { pid } = req.params; if (req.teamleader?.isAuthenticated) { - requireResponsibleForParticipant(req.teamleader, pid); + await requireResponsibleForParticipant(req.teamleader, pid); } const result = InitialParticipant.partial().safeParse(req.body); @@ -153,9 +155,9 @@ export const deleteParticipant = async (req: Request<{ pid: string }>, res: Resp const { pid } = req.params; if (req.teamleader?.isAuthenticated) { - requireResponsibleForParticipant(req.teamleader, pid); + await requireResponsibleForParticipant(req.teamleader, pid); } else { - await requireResponsibleForGroups(req.auth, await getGroupByParticipantPid(pid)); + requireResponsibleForGroups(req.auth, await getGroupByParticipantPid(pid)); } try { diff --git a/src/Controllers/role_schema.controller.ts b/src/Controllers/role_schema.controller.ts index f375451..374a68b 100644 --- a/src/Controllers/role_schema.controller.ts +++ b/src/Controllers/role_schema.controller.ts @@ -15,6 +15,8 @@ import { validateName, } from "./common"; +require("express-async-errors"); + const RoleSchemaBody = z.object({ name: z.string().min(1), schema: z.string(), diff --git a/src/Controllers/team.controller.ts b/src/Controllers/team.controller.ts index 3db62bd..79628e2 100644 --- a/src/Controllers/team.controller.ts +++ b/src/Controllers/team.controller.ts @@ -6,6 +6,9 @@ import { TeamBody } from "./user_auth.controller"; import { Prisma } from "@prisma/client"; import NotFoundError from "../Middleware/error/NotFoundError"; import { requireResponsibleForGroups } from "../Middleware/auth/auth"; +import AuthError from "../Middleware/error/AuthError"; + +require("express-async-errors"); export const basicTeam = { pid: true, @@ -108,8 +111,10 @@ export const deleteTeam = async (req: Request, res: Response) => { if (req.teamleader?.isAuthenticated) { await requireLeaderOfTeam(req.teamleader, pid); - } else { - await requireResponsibleForGroups(req.auth, await getGroupsByTeamPid(pid)); + } + + if (req.auth?.permission_level == "STANDARD") { + throw new AuthError("STANDARD Admins are not allowed to delete Teams!") } await prisma.team.delete({ where: { pid } }); diff --git a/src/Controllers/user_auth.controller.ts b/src/Controllers/user_auth.controller.ts index 369f596..efa67f9 100644 --- a/src/Controllers/user_auth.controller.ts +++ b/src/Controllers/user_auth.controller.ts @@ -8,6 +8,8 @@ import { generateTeamleaderJWT } from "../Middleware/auth/teamleaderAuth"; import { createRolesForTeam } from "./role.controller"; import { z } from "zod"; +require("express-async-errors"); + export const TeamBody = z.object({ teamName: z.string().min(1), leaderEmail: z.string().email(), diff --git a/src/Middleware/auth/teamleaderAuth.ts b/src/Middleware/auth/teamleaderAuth.ts index f0a4737..1052ed8 100644 --- a/src/Middleware/auth/teamleaderAuth.ts +++ b/src/Middleware/auth/teamleaderAuth.ts @@ -4,6 +4,7 @@ import jwt, { JsonWebTokenError } from "jsonwebtoken"; import AuthError from "../error/AuthError"; import { getBearerToken, verifyAuthorizationFormat } from "./auth"; import prisma from "../../lib/prisma"; +import { checkTeamExistence } from "../../Controllers/team.controller"; export interface TeamleaderJWTPayload { team: string; @@ -25,63 +26,63 @@ export function generateTeamleaderJWT(teamleader: Team) { 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; - - if (!authorization) { - if (config.optional) { - return false; + (req: Request, res: Response, next: NextFunction) => { + if (!JWT_SECRET) { + throw new Error("JWT_SECRET not set"); } - 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)", - }, - }); - } + const { authorization } = req.headers; - if (!verifyAuthorizationFormat(authorization)) { - return res.status(400).send({ - type: "error", - payload: { - message: "Malformed Authorization header", - format: "Bearer ", - }, - }); - } + if (!authorization) { + if (config.optional) { + return false; + } - 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({ + 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 }); From d1f41c892727687911b34b417457b07489692ae1 Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Mon, 6 Jun 2022 21:15:15 +0200 Subject: [PATCH 49/55] Only throw when the auth is not controlled --- src/Middleware/auth/auth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Middleware/auth/auth.ts b/src/Middleware/auth/auth.ts index 411b515..40eaedc 100644 --- a/src/Middleware/auth/auth.ts +++ b/src/Middleware/auth/auth.ts @@ -68,7 +68,7 @@ const _requireAdminAuthentication = 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") { + if (typeof (token_payload as unknown as TeamleaderJWTPayload).team === "string" && !config.controlled) { throw new AuthError("Teamleader authentication is not supported for this operation!"); } From 94b2030c8f3c8f9aef5e632256313b50acad4ebb Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Mon, 6 Jun 2022 21:17:44 +0200 Subject: [PATCH 50/55] Return false on controlled --- src/Middleware/auth/auth.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Middleware/auth/auth.ts b/src/Middleware/auth/auth.ts index 40eaedc..ed11793 100644 --- a/src/Middleware/auth/auth.ts +++ b/src/Middleware/auth/auth.ts @@ -68,7 +68,10 @@ const _requireAdminAuthentication = 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" && !config.controlled) { + if (typeof (token_payload as unknown as TeamleaderJWTPayload).team === "string") { + if (config.controlled) { + return false; + } throw new AuthError("Teamleader authentication is not supported for this operation!"); } From 7655eebcd9a1b20cdba13a6ac808c4b429acafd1 Mon Sep 17 00:00:00 2001 From: Laurin <60652077+Flexla54@users.noreply.github.com> Date: Tue, 7 Jun 2022 03:47:37 +0200 Subject: [PATCH 51/55] patched review suggestions and discovered bugs --- src/Controllers/event.controller.ts | 14 +- src/Controllers/media.controller.ts | 30 +-- src/Controllers/organisation.controller.ts | 4 +- src/Controllers/participant.controller.ts | 22 ++- src/Controllers/role.controller.ts | 42 ++--- src/Controllers/role_schema.controller.ts | 8 +- src/Controllers/team.controller.ts | 25 ++- src/Controllers/user_auth.controller.ts | 121 +++++++----- src/Middleware/auth/auth.ts | 210 ++++++++++----------- src/lib/result_schema.ts | 4 +- 10 files changed, 259 insertions(+), 221 deletions(-) diff --git a/src/Controllers/event.controller.ts b/src/Controllers/event.controller.ts index 3dfcd84..03fb8eb 100644 --- a/src/Controllers/event.controller.ts +++ b/src/Controllers/event.controller.ts @@ -137,12 +137,14 @@ export const addEvent = async (req: Request, res: Response) => { ); } + const body = result.data; + const event = await prisma.event.create({ data: { - name: req.body.name, - date: req.body.date, - briefDescription: req.body.briefDescription, - fullDescription: req.body.fullDescription, + name: body.name, + date: body.date, + briefDescription: body.briefDescription, + fullDescription: body.fullDescription, }, select: basicEvent, }); @@ -206,7 +208,7 @@ export const updateEvent = async (req: Request<{ pid: string }>, res: Response) }); } catch (e) { if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") { - throw new NotFoundError("discipline", pid); + throw new NotFoundError("event", pid); } throw e; @@ -231,7 +233,7 @@ export const deleteEvent = async (req: Request, res: Res return res.status(204).end(); } catch (e) { if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { - throw new NotFoundError("discipline", pid); + throw new NotFoundError("event", pid); } throw e; diff --git a/src/Controllers/media.controller.ts b/src/Controllers/media.controller.ts index 11a2cc7..d5b4b36 100644 --- a/src/Controllers/media.controller.ts +++ b/src/Controllers/media.controller.ts @@ -10,6 +10,7 @@ import { PrismaClientKnownRequestError } from "@prisma/client/runtime"; import { generateInvalidBodyError, DataType } from "./common"; import { unlink } from "fs/promises"; import ForwardableError from "../Middleware/error/ForwardableError"; +import { table } from "console"; require("express-async-errors"); @@ -204,21 +205,26 @@ export const linkMedia = async (req: Request<{ pid: string }, {}, { mediaPid: st ); } - const updatedRec = await getPrismaUpdateFKT(tableToUpdate[2])({ - where: { pid }, - data: { - visual: { connect: { pid: mediaPid } }, - }, - }); + try { + const updatedRec = await getPrismaUpdateFKT(tableToUpdate[2])({ + where: { pid }, + data: { + visual: { connect: { pid: mediaPid } }, + }, + }); - if (!updatedRec) { - throw new NotFoundError(tableToUpdate[2], pid); + return res.status(200).json({ + type: "success", + payload: { message: "Linking with the visual was successful" }, + }); + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") { + throw new NotFoundError(tableToUpdate[2], pid); + } + + throw e; } - return res.status(200).json({ - type: "success", - payload: {}, - }); }; export const unlinkMedia = async (req: Request<{ pid: string; mediaPid: string }>, res: Response) => { diff --git a/src/Controllers/organisation.controller.ts b/src/Controllers/organisation.controller.ts index d21ac13..e79d7a3 100644 --- a/src/Controllers/organisation.controller.ts +++ b/src/Controllers/organisation.controller.ts @@ -28,7 +28,7 @@ const detailedOrganisation = { pid: true, name: true, date: true, - description: true, + briefDescription: true, }, }, } as const; @@ -191,7 +191,7 @@ export const updateOrganisation = async ( }); } catch (e) { if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") { - throw new NotFoundError("discipline", pid); + throw new NotFoundError("organisation", pid); } throw e; diff --git a/src/Controllers/participant.controller.ts b/src/Controllers/participant.controller.ts index 40d5721..307dcba 100644 --- a/src/Controllers/participant.controller.ts +++ b/src/Controllers/participant.controller.ts @@ -16,8 +16,6 @@ const InitialParticipant = z.object({ groupPid: z.string().min(1).uuid(), }); -const ParticipantBody = InitialParticipant.extend({ teamPid: z.string().uuid() }); - const returnedParticipant = { pid: true, firstName: true, @@ -41,7 +39,7 @@ const returnedParticipant = { export const createParticipant = async (req: Request<{ teamPid: string }>, res: Response) => { const { teamPid } = req.params; - const result = ParticipantBody.safeParse(req.body); + const result = InitialParticipant.safeParse(req.body); if (result.success === false) { return res.status(400).json( @@ -59,20 +57,20 @@ export const createParticipant = async (req: Request<{ teamPid: string }>, res: if (req.teamleader?.isAuthenticated) { await requireLeaderOfTeam(req.teamleader, teamPid); - } else { + } else if (req.auth?.permission_level == "STANDARD") { requireResponsibleForGroups(req.auth, body.groupPid); } try { const discipline = await prisma.team.findUnique({ - where: { pid: body.teamPid }, + where: { pid: teamPid }, select: { discipline: true } }); const maxteamsize = discipline?.discipline.maxTeamSize; const userCount = await prisma.participant.count({ - where: { team: { pid: body.teamPid } } + where: { team: { pid: teamPid } } }); if (maxteamsize == userCount) { @@ -85,7 +83,7 @@ export const createParticipant = async (req: Request<{ teamPid: string }>, res: lastName: body.lastName, relevance: "MEMBER", group: { connect: { pid: body.groupPid } }, - team: { connect: { pid: body.teamPid } }, + team: { connect: { pid: teamPid } }, }, select: returnedParticipant, }); @@ -95,7 +93,7 @@ export const createParticipant = async (req: Request<{ teamPid: string }>, res: if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { return res .status(404) - .json(generateError(`Could not link to team with ID '${body.teamPid}, or group with ID ${body.groupPid}'`)); + .json(generateError(`Could not link to team with ID '${teamPid}, or group with ID ${body.groupPid}'`)); } throw e; } @@ -107,6 +105,8 @@ export const updateParticipant = async (req: Request<{ pid: string }>, res: Resp if (req.teamleader?.isAuthenticated) { await requireResponsibleForParticipant(req.teamleader, pid); + } else if (req.auth?.permission_level == "STANDARD") { + requireResponsibleForGroups(req.auth, await getGroupByParticipantPid(pid)); } const result = InitialParticipant.partial().safeParse(req.body); @@ -143,7 +143,9 @@ export const updateParticipant = async (req: Request<{ pid: string }>, res: Resp }); } catch (e) { if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") { - throw new NotFoundError("participant", pid); + return res + .status(404) + .json(generateError(`Could not find participant '${pid}, or link to group with ID ${body.groupPid}.'`)); } throw e; @@ -156,7 +158,7 @@ export const deleteParticipant = async (req: Request<{ pid: string }>, res: Resp if (req.teamleader?.isAuthenticated) { await requireResponsibleForParticipant(req.teamleader, pid); - } else { + } else if (req.auth?.permission_level == "STANDARD") { requireResponsibleForGroups(req.auth, await getGroupByParticipantPid(pid)); } diff --git a/src/Controllers/role.controller.ts b/src/Controllers/role.controller.ts index c9e55bf..1c761e6 100644 --- a/src/Controllers/role.controller.ts +++ b/src/Controllers/role.controller.ts @@ -1,3 +1,4 @@ +import { Prisma } from "@prisma/client"; import { Request, Response } from "express"; import { z } from "zod"; import prisma from "../lib/prisma"; @@ -61,7 +62,7 @@ export async function getRolesForTeam(req: Request<{ pid: string }>, res: Respon if (req.teamleader?.isAuthenticated) { await requireLeaderOfTeam(req.teamleader, pid); } else { - await requireResponsibleForGroups(req.auth, await getGroupsByTeamPid(pid)); + requireResponsibleForGroups(req.auth, await getGroupsByTeamPid(pid)); } const roles = await prisma.role.findMany({ @@ -84,7 +85,6 @@ export async function getRolesForTeam(req: Request<{ pid: string }>, res: Respon const AssignParticipantToRoleBody = z.object({ participantPid: z.string().uuid(), - teamPid: z.string().uuid(), }); // requires: auth(leader of the team) @@ -102,31 +102,29 @@ export async function assignParticipantToRole(req: Request<{ pid: string }>, res if (req.teamleader?.isAuthenticated) { requireResponsibleForParticipant(req.teamleader, participantPid); } else { - await requireResponsibleForGroups(req.auth, await getGroupByParticipantPid(participantPid)); + requireResponsibleForGroups(req.auth, await getGroupByParticipantPid(participantPid)); } - const schema = await prisma.role.findFirst({ - where: { pid, team: { participants: { some: { pid: participantPid } } } }, - select: { participant: { select: { pid: true, firstName: true, lastName: true } } }, - }); + try { + const schema = await prisma.role.update({ where: { pid }, data: { participant: { connect: { pid: participantPid } } }, select: detailedRole }); - if (!schema) { - return res.status(404).json({ - type: "error", + return res.status(200).json({ + type: "success", payload: { - message: `No role with the ID '${pid}' could be found in the scope of the participant with the ID '${participantPid}'`, + message: `The participant with ID '${participantPid}' was successfully assigned to the role with ID '${pid}'`, + ...(schema.participant ? { unassigned: schema.participant } : {}), }, }); + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") { + return res.status(404).json({ + type: "error", + payload: { + message: `No role with the ID '${pid}' could be found in the scope of the participant with the ID '${participantPid}'`, + }, + }); + } + + throw e; } - - // 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 }, 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 '${pid}'`, - ...(schema.participant ? { unassigned: schema.participant } : {}), - }, - }); } diff --git a/src/Controllers/role_schema.controller.ts b/src/Controllers/role_schema.controller.ts index 374a68b..99c5369 100644 --- a/src/Controllers/role_schema.controller.ts +++ b/src/Controllers/role_schema.controller.ts @@ -3,7 +3,7 @@ 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 { DurationSchema, parseSchema, PointSchema } from "../lib/result_schema"; import NotFoundError from "../Middleware/error/NotFoundError"; import SchemaError from "../Middleware/error/SchemaError"; import { @@ -19,7 +19,7 @@ require("express-async-errors"); const RoleSchemaBody = z.object({ name: z.string().min(1), - schema: z.string(), + schema: z.string(PointSchema).or(z.string(DurationSchema)), }); const UpdateBody = RoleSchemaBody.partial(); @@ -138,7 +138,7 @@ 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()); + return res.status(403).json(createInsufficientPermissionsError()); } const { pid } = req.params; @@ -150,7 +150,7 @@ export const UpdateRoleSchema = async (req: Request<{ pid: string }>, res: Respo generateInvalidBodyError( { name: DataType.STRING, - schema: DataType.STRING, + schema: DataType.RESULT_SCHEMA, }, result.error ) diff --git a/src/Controllers/team.controller.ts b/src/Controllers/team.controller.ts index 79628e2..16dd84a 100644 --- a/src/Controllers/team.controller.ts +++ b/src/Controllers/team.controller.ts @@ -7,6 +7,8 @@ import { Prisma } from "@prisma/client"; import NotFoundError from "../Middleware/error/NotFoundError"; import { requireResponsibleForGroups } from "../Middleware/auth/auth"; import AuthError from "../Middleware/error/AuthError"; +import { runInNewContext } from "vm"; +import { PrismaClientKnownRequestError } from "@prisma/client/runtime"; require("express-async-errors"); @@ -31,6 +33,9 @@ export const basicTeam = { }; export const getTeams = async (req: Request, res: Response) => { + if (req.auth?.permission_level == "STANDARD") { + throw new AuthError("A STANDARD Admin is not allowed to get all teams!"); + } const teams = await prisma.team.findMany({ select: basicTeam }); res.status(200).json({ type: "success", payload: { teams } }); @@ -41,8 +46,8 @@ export const getTeam = async (req: Request<{ pid: string }>, res: Response) => { if (req.teamleader?.isAuthenticated) { await requireLeaderOfTeam(req.teamleader, pid); - } else { - await requireResponsibleForGroups(req.auth, pid); + } else if (req.auth?.permission_level == "STANDARD") { + requireResponsibleForGroups(req.auth, await getGroupsByTeamPid(pid)); } const team = await prisma.team.findUnique({ @@ -62,8 +67,8 @@ export const updateTeam = async (req: Request, res: Response) => { if (req.teamleader?.isAuthenticated) { await requireLeaderOfTeam(req.teamleader, pid); - } else { - await requireResponsibleForGroups(req.auth, await getGroupsByTeamPid(pid)); + } else if (req.auth?.permission_level == "STANDARD") { + requireResponsibleForGroups(req.auth, await getGroupsByTeamPid(pid)); } const result = TeamBody.omit({ partGroupId: true, partFirstName: true, partLastName: true }).safeParse(req.body); @@ -117,9 +122,17 @@ export const deleteTeam = async (req: Request, res: Response) => { throw new AuthError("STANDARD Admins are not allowed to delete Teams!") } - await prisma.team.delete({ where: { pid } }); + try { + await prisma.team.delete({ where: { pid } }); - res.status(204).json({ type: "success", payload: { message: "Sucesfully deleted team" } }); + res.status(204).json({ type: "success", payload: { message: "Sucesfully deleted team" } }); + } catch (e) { + if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { + throw new NotFoundError("team", pid); + } + + throw e; + } }; export async function checkTeamExistence(teamPid: string) { diff --git a/src/Controllers/user_auth.controller.ts b/src/Controllers/user_auth.controller.ts index efa67f9..8d65606 100644 --- a/src/Controllers/user_auth.controller.ts +++ b/src/Controllers/user_auth.controller.ts @@ -7,6 +7,8 @@ import { DataType, generateError, generateInvalidBodyError } from "./common"; import { generateTeamleaderJWT } from "../Middleware/auth/teamleaderAuth"; import { createRolesForTeam } from "./role.controller"; import { z } from "zod"; +import { PrismaClientKnownRequestError } from "@prisma/client/runtime"; +import NotFoundError from "../Middleware/error/NotFoundError"; require("express-async-errors"); @@ -49,50 +51,55 @@ export const register = async (req: Request<{}, {}, CreateTeamBody>, res: Respon const body = result.data; - const group = prisma.group.findUnique({ where: { pid: body.partGroupId } }); - - if (typeof group == null) { - return res.status(404).json(generateError("Specified group was not found!")); - } - - const discipline = prisma.discipline.findUnique({ where: { pid: body.disciplineId } }); - - if (typeof discipline == null) { - return res.status(404).json(generateError("Specified discipline was not found!")); - } - - const team = await prisma.team.create({ - data: { - leaderEmail: body.leaderEmail, - name: body.teamName, - roles: undefined, - discipline: { connect: { pid: body.disciplineId } }, - participants: { - create: { - firstName: body.partFirstName, - lastName: body.partLastName, - relevance: "TEAMLEADER", - group: { connect: { pid: body.partGroupId } }, + try { + const team = await prisma.team.create({ + data: { + leaderEmail: body.leaderEmail, + name: body.teamName, + roles: undefined, + discipline: { connect: { pid: body.disciplineId } }, + participants: { + create: { + firstName: body.partFirstName, + lastName: body.partLastName, + relevance: "TEAMLEADER", + group: { connect: { pid: body.partGroupId } }, + }, }, }, - }, - select: { - pid: true, - name: true, - discipline: { select: { pid: true } }, - }, - }); + select: { + pid: true, + name: true, + discipline: { select: { pid: true } }, + }, + }); - await createRolesForTeam(team.pid); + await createRolesForTeam(team.pid); - const usid = nanoid(); + const usid = nanoid(); + + (await mailClient).set(usid, team.pid); + + const eventname = await prisma.discipline.findUnique({ where: { pid: body.disciplineId }, select: { event: { select: { name: true, } } } }).event.name; + verificationMail(req.body.leaderEmail, eventname, usid); + + return res.status(201).json({ type: "success", payload: { team } }); + + } catch (e) { + if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { + return res.status(404).json({ + type: "error", + payload: { + message: `Could not connect to discipline with the ID '${body.disciplineId}' or could not connect participant to group with the ID '${body.partGroupId}'`, + }, + }); + } + + throw e; + } - (await mailClient).set(usid, team.pid); - // TODO: fix "eventname" - verificationMail(req.body.leaderEmail, "eventname", usid); - res.status(201).json({ type: "success", payload: { team } }); }; export const requestToken = async (req: Request, res: Response) => { @@ -117,6 +124,7 @@ export const requestToken = async (req: Request, res: Response) => { (await mailClient).set(usid, team.pid); // TODO: fix "eventname" + // let teamName = await prisma.team.findUnique({ where: { pid: teamId }, select: { discipline: { select: { event: { select: { name: true } } } } } }); verificationMail(team.leaderEmail, "eventname", usid); res.status(200).json({ type: "sucess", payload: { message: "Email sent!" } }); @@ -145,23 +153,32 @@ export const verifyEmail = async (req: Request, res: Response) => { }); } - const team = await prisma.team.update({ - where: { - pid: acc, - }, - data: { - verified: true, - }, - }); + try { + const team = await prisma.team.update({ + where: { + pid: acc, + }, + data: { + verified: true, + }, + }); - mailClient.set(code, ""); + mailClient.set(code, ""); - const token = generateTeamleaderJWT(team); + const token = generateTeamleaderJWT(team); - res.cookie("teamLeaderToken", token, { - path: "/", - maxAge: 1000 * 60 * 60 * 24 * 4, - }); + res.cookie("teamLeaderToken", token, { + path: "/", + maxAge: 1000 * 60 * 60 * 24 * 4, + }); + + res.status(200).json({ type: "succes", payload: { token } }); + } catch (e) { + if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { + throw new NotFoundError("discipline", acc); + } + + throw e; + } - res.status(200).json({ type: "succes", payload: { token } }); }; diff --git a/src/Middleware/auth/auth.ts b/src/Middleware/auth/auth.ts index ed11793..5026edb 100644 --- a/src/Middleware/auth/auth.ts +++ b/src/Middleware/auth/auth.ts @@ -18,42 +18,82 @@ export const getBearerToken = (authorization: string) => authorization.slice(7); 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; - - if (!authorization) { - if (config.optional) { - return false; + async (req: Request, res: Response, next: NextFunction) => { + if (!JWT_SECRET) { + throw new Error("JWT_SECRET not set"); } - return res.status(403).send({ - type: "error", - payload: { - message: "The requeset did not include the Authorization header", - }, - }); - } + const { authorization } = req.headers; - if (!verifyAuthorizationFormat(authorization)) { - return res.status(400).send({ - type: "error", - payload: { - message: "Malformed Authorization header", - format: "Bearer ", - }, - }); - } + if (!authorization) { + if (config.optional) { + return false; + } - let token_payload_: string | JwtPayload; + return res.status(403).send({ + type: "error", + payload: { + message: "The requeset did not include the Authorization header", + }, + }); + } - try { - token_payload_ = jwt.verify(getBearerToken(authorization), JWT_SECRET); - } catch (e) { - if (e instanceof JsonWebTokenError) { + if (!verifyAuthorizationFormat(authorization)) { + return res.status(400).send({ + type: "error", + payload: { + message: "Malformed Authorization header", + format: "Bearer ", + }, + }); + } + + 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") { + if (config.controlled) { + return false; + } + throw new AuthError("Teamleader authentication is not supported for this operation!"); + } + + 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: { @@ -62,62 +102,22 @@ const _requireAdminAuthentication = }); } - 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; - - if (!token_payload.permission_level || !token_payload.pid || !token_payload.revision) { - if (typeof (token_payload as unknown as TeamleaderJWTPayload).team === "string") { - if (config.controlled) { - return false; - } - throw new AuthError("Teamleader authentication is not supported for this operation!"); + if (!config.controlled) { + next(); } - 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: { - 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; }; - if (!config.controlled) { - next(); - } - - return true; - }; - export const requireAuthentication = _requireAdminAuthentication({ optional: false, controlled: false }); type AuthType = "admin" | "teamleader"; @@ -144,37 +144,37 @@ function getAuthTypes(type: AuthType | AuthTypeConfig): 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; + async (req: Request, res: Response, next: NextFunction) => { + const types = getAuthTypes(config.type); + const optional = config.optional; - let adminFinished = false; - let teamleaderFinished = false; + let adminFinished = false; + let teamleaderFinished = false; - if (types.includes("admin")) { - adminFinished = Boolean(await _requireAdminAuthentication({ optional: true, controlled: true })(req, res, next)); + if (types.includes("admin")) { + adminFinished = Boolean(await _requireAdminAuthentication({ optional: true, controlled: true })(req, res, next)); - if (adminFinished) { - return next(); + if (adminFinished) { + return next(); + } } - } - if (types.includes("teamleader")) { - teamleaderFinished = Boolean( - _requireTeamleaderAuthentication({ optional: true, controlled: true })(req, res, next) - ); + if (types.includes("teamleader")) { + teamleaderFinished = Boolean( + _requireTeamleaderAuthentication({ optional: true, controlled: true })(req, res, next) + ); - if (teamleaderFinished) { - return next(); + if (teamleaderFinished) { + return next(); + } } - } - if (!config.optional) { - throw new AuthError("No sufficient authorization was provided for this operation"); - } + if (!config.optional) { + throw new AuthError("No sufficient authorization was provided for this operation"); + } - next(); - }; + next(); + }; export function requireResponsibleForGroups(auth: AuthJWTPayload | undefined, groupPids: string[] | string) { if (auth?.permission_level === "ELEVATED") { @@ -190,7 +190,7 @@ export function requireResponsibleForGroups(auth: AuthJWTPayload | undefined, gr throw new AuthError("The provided authorization is not valid for the requested operation!"); }); } else { - if (auth?.groups.includes(groupPids)) { + if (!auth?.groups.includes(groupPids)) { throw new AuthError("The provided authorization is not valid for the requested operation!"); } } diff --git a/src/lib/result_schema.ts b/src/lib/result_schema.ts index 7da23c9..fdb6b62 100644 --- a/src/lib/result_schema.ts +++ b/src/lib/result_schema.ts @@ -7,7 +7,7 @@ const SchemaVersion = z.enum(["1.0"]); const TimeUnit = z.enum(["days", "hours", "minutes", "seconds", "milliseconds"]); -const DurationSchema = z +export const DurationSchema = z .object({ type: z.literal("duration"), min: z.number().int({ message: "min must be an integer (relative to smallestUnit)" }), @@ -17,7 +17,7 @@ const DurationSchema = z }) .refine(({ min, max }) => min < max, { message: "min must be smaller than max" }); -const PointSchema = z +export const PointSchema = z .object({ type: z.literal("points"), min: z.number(), From 6a3612066def7b463683bd7bca87d14b5bc37b10 Mon Sep 17 00:00:00 2001 From: Stefan-5422 <32109571+Stefan-5422@users.noreply.github.com> Date: Mon, 6 Jun 2022 21:58:05 +0000 Subject: [PATCH 52/55] Literally changed a env variable --- README.md | 1 + src/lib/mail.ts | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b4b469e..8afa06d 100644 --- a/README.md +++ b/README.md @@ -15,4 +15,5 @@ Before Runningthis on you local machine some things have to be setup MAILPASSWORD: THE PASSWORD FOR THE MAIL ACCOUNT DEV: SWITCH FOR DEV MODE AFFECTS EMAIL SERVER ALLOW_ORIGIN: ORIGIN OF THE PRODUCTION CLIENT (FOR CORS) + FRONTEND_MAIL_ENDPOINT: THE ENDPOINT THE FRONTEND DOES EMAIL-VERIFICATION ``` diff --git a/src/lib/mail.ts b/src/lib/mail.ts index 7c96463..9b1cf56 100644 --- a/src/lib/mail.ts +++ b/src/lib/mail.ts @@ -67,8 +67,7 @@ export const verificationMail = async (to: string, eventName: string, verificati const raw = mjml.getTemplate("emailVerification"); // TODO: the process.env.DOMAIN is undefined in Development mode !! - verificationLink = - "https://" + ("api." + process.env.DOMAIN ?? "localhost:3000/api") + "/users/verify/" + verificationLink; + verificationLink = (process.env.FRONTEND_MAIL_ENDPOINT ?? "localhost:3000/api/users/verify/") + verificationLink; const message = Handlebars.compile(raw); const data = { eventName, verificationLink }; From 79df68b2cca447f34aba29a96585f8aed4c37c84 Mon Sep 17 00:00:00 2001 From: Stefan-5422 <32109571+Stefan-5422@users.noreply.github.com> Date: Tue, 7 Jun 2022 14:17:52 +0000 Subject: [PATCH 53/55] Address #74 & make email code more solid --- src/Controllers/user_auth.controller.ts | 67 +++++++++++++++++++------ 1 file changed, 52 insertions(+), 15 deletions(-) diff --git a/src/Controllers/user_auth.controller.ts b/src/Controllers/user_auth.controller.ts index 8d65606..a029ff7 100644 --- a/src/Controllers/user_auth.controller.ts +++ b/src/Controllers/user_auth.controller.ts @@ -70,7 +70,7 @@ export const register = async (req: Request<{}, {}, CreateTeamBody>, res: Respon select: { pid: true, name: true, - discipline: { select: { pid: true } }, + discipline: { select: { pid: true, name: true } }, }, }); @@ -80,11 +80,8 @@ export const register = async (req: Request<{}, {}, CreateTeamBody>, res: Respon (await mailClient).set(usid, team.pid); - const eventname = await prisma.discipline.findUnique({ where: { pid: body.disciplineId }, select: { event: { select: { name: true, } } } }).event.name; - verificationMail(req.body.leaderEmail, eventname, usid); - + verificationMail(req.body.leaderEmail, team.discipline.name, usid); return res.status(201).json({ type: "success", payload: { team } }); - } catch (e) { if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { return res.status(404).json({ @@ -97,22 +94,26 @@ export const register = async (req: Request<{}, {}, CreateTeamBody>, res: Respon throw e; } - - - }; export const requestToken = async (req: Request, res: Response) => { - const { teamId } = req.body || {}; + const data = z.object({ teamId: z.string().min(1) }).safeParse(req); - if (!(typeof teamId === "string")) { - return res.status(400).json(generateInvalidBodyError({ teamId: DataType.STRING })); + if (data.success == false) { + return res.status(400).json(generateInvalidBodyError({ teamId: DataType.STRING }, data.error)); } + const { teamId } = data.data; + const team = await prisma.team.findUnique({ where: { pid: teamId, }, + select: { + discipline: { select: { name: true } }, + pid: true, + leaderEmail: true, + }, }); if (!team) { @@ -123,9 +124,46 @@ export const requestToken = async (req: Request, res: Response) => { (await mailClient).set(usid, team.pid); - // TODO: fix "eventname" - // let teamName = await prisma.team.findUnique({ where: { pid: teamId }, select: { discipline: { select: { event: { select: { name: true } } } } } }); - verificationMail(team.leaderEmail, "eventname", usid); + verificationMail(team.leaderEmail, team.discipline.name, usid); + + res.status(200).json({ type: "sucess", payload: { message: "Email sent!" } }); +}; + +export const requestTokenEmail = async (req: Request, res: Response) => { + const data = z.object({ email: z.string().min(1) }).safeParse(req); + + if (data.success == false) { + return res.status(400).json(generateInvalidBodyError({ email: DataType.STRING }, data.error)); + } + + const { email } = data.data; + + const teams = await prisma.team.findMany({ + where: { + leaderEmail: email, + }, + select: { + discipline: { select: { name: true } }, + pid: true, + leaderEmail: true, + }, + }); + + if (!teams) { + return res.status(404).json(generateError("Team does not exist!")); + } + + const team = teams[0]; //REVIEW: maybe a email should be only able to be responsible for one team + + if (!team) { + return res.status(404).json(generateError("Team does not exist!")); + } + + const usid = nanoid(); + + (await mailClient).set(usid, team.pid); + + verificationMail(team.leaderEmail, team.discipline.name, usid); res.status(200).json({ type: "sucess", payload: { message: "Email sent!" } }); }; @@ -180,5 +218,4 @@ export const verifyEmail = async (req: Request, res: Response) => { throw e; } - }; From 05d4c08ec2ebb8f2938cdfeb0418df1275df3e40 Mon Sep 17 00:00:00 2001 From: Stefan-5422 <32109571+Stefan-5422@users.noreply.github.com> Date: Tue, 7 Jun 2022 18:17:30 +0000 Subject: [PATCH 54/55] Removed unecessary cookie --- src/Controllers/user_auth.controller.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Controllers/user_auth.controller.ts b/src/Controllers/user_auth.controller.ts index a029ff7..c2f02ef 100644 --- a/src/Controllers/user_auth.controller.ts +++ b/src/Controllers/user_auth.controller.ts @@ -205,11 +205,6 @@ export const verifyEmail = async (req: Request, res: Response) => { const token = generateTeamleaderJWT(team); - res.cookie("teamLeaderToken", token, { - path: "/", - maxAge: 1000 * 60 * 60 * 24 * 4, - }); - res.status(200).json({ type: "succes", payload: { token } }); } catch (e) { if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { From 1a6c780de5916a4ae9c70172100df137cfb57f53 Mon Sep 17 00:00:00 2001 From: Stefan-5422 Date: Tue, 7 Jun 2022 18:22:55 +0000 Subject: [PATCH 55/55] [create-pull-request] push formatted files --- src/Controllers/admin.controller.ts | 4 +- src/Controllers/discipline.controller.ts | 9 +- src/Controllers/group.controllers.ts | 32 ++-- src/Controllers/media.controller.ts | 1 - src/Controllers/participant.controller.ts | 10 +- src/Controllers/role.controller.ts | 6 +- src/Controllers/team.controller.ts | 13 +- src/Middleware/auth/auth.ts | 210 +++++++++++----------- src/Middleware/auth/teamleaderAuth.ts | 94 +++++----- src/Routes/event.routes.ts | 8 +- src/Routes/media.routes.ts | 43 +++-- src/Routes/role.routes.ts | 2 +- 12 files changed, 222 insertions(+), 210 deletions(-) diff --git a/src/Controllers/admin.controller.ts b/src/Controllers/admin.controller.ts index f942845..cca123f 100644 --- a/src/Controllers/admin.controller.ts +++ b/src/Controllers/admin.controller.ts @@ -29,7 +29,9 @@ export const getAllAdmins = async (req: Request, res: Response) => { } // TODO: Add exception handling - const users = await prisma.admin.findMany({ select: { pid: true, name: true, permission_level: true, groups: { select: { pid: true } } } }); + const users = await prisma.admin.findMany({ + select: { pid: true, name: true, permission_level: true, groups: { select: { pid: true } } }, + }); res.status(200).json({ type: "success", diff --git a/src/Controllers/discipline.controller.ts b/src/Controllers/discipline.controller.ts index 5727202..7caafe0 100644 --- a/src/Controllers/discipline.controller.ts +++ b/src/Controllers/discipline.controller.ts @@ -168,7 +168,14 @@ export const createDiscipline = async (req: Request<{ eventPid: string }, {}, Cr try { const discipline = await prisma.discipline.create({ - data: { name, minTeamSize, maxTeamSize, briefDescription, fullDescription, event: { connect: { pid: req.params.eventPid } } }, + data: { + name, + minTeamSize, + maxTeamSize, + briefDescription, + fullDescription, + event: { connect: { pid: req.params.eventPid } }, + }, select: basicDiscipline, }); diff --git a/src/Controllers/group.controllers.ts b/src/Controllers/group.controllers.ts index 6c2c2ea..c77ac6b 100644 --- a/src/Controllers/group.controllers.ts +++ b/src/Controllers/group.controllers.ts @@ -38,7 +38,7 @@ const detailedGroup = { organisation: { select: { pid: true, name: true } }, participants: { select: { pid: true, firstName: true, lastName: true } }, admins: { select: { pid: true, name: true } }, -} +}; export const _getAllGroups = async (res: Response, organisationId: string | undefined) => { const groups = await prisma.group.findMany({ @@ -88,12 +88,12 @@ export const getGroup = async (req: Request, res: Response) where: { pid }, select: req.auth?.isAuthenticated ? { - pid: true, - name: true, - organisation: { select: { pid: true, name: true } }, - admins: { select: { pid: true, name: true } }, - participants: { select: { pid: true } }, - } + pid: true, + name: true, + organisation: { select: { pid: true, name: true } }, + admins: { select: { pid: true, name: true } }, + participants: { select: { pid: true } }, + } : basicGroup, }); @@ -112,15 +112,15 @@ export const getGroup = async (req: Request, res: Response) }, ...(req.auth?.isAuthenticated ? { - admins: group.admins?.map((admin) => ({ - ...admin, - _links: [{ rel: "self", type: "GET", href: `/api/admins/${admin.pid}` }], - })), - participants: group.participants?.map((participant) => ({ - ...participant, - _links: [{ rel: "self", type: "GET", href: `/api/participant/${participant.pid}` }], - })), - } + admins: group.admins?.map((admin) => ({ + ...admin, + _links: [{ rel: "self", type: "GET", href: `/api/admins/${admin.pid}` }], + })), + participants: group.participants?.map((participant) => ({ + ...participant, + _links: [{ rel: "self", type: "GET", href: `/api/participant/${participant.pid}` }], + })), + } : {}), }, }, diff --git a/src/Controllers/media.controller.ts b/src/Controllers/media.controller.ts index d5b4b36..57f231e 100644 --- a/src/Controllers/media.controller.ts +++ b/src/Controllers/media.controller.ts @@ -224,7 +224,6 @@ export const linkMedia = async (req: Request<{ pid: string }, {}, { mediaPid: st throw e; } - }; export const unlinkMedia = async (req: Request<{ pid: string; mediaPid: string }>, res: Response) => { diff --git a/src/Controllers/participant.controller.ts b/src/Controllers/participant.controller.ts index 307dcba..08bf4db 100644 --- a/src/Controllers/participant.controller.ts +++ b/src/Controllers/participant.controller.ts @@ -64,13 +64,13 @@ export const createParticipant = async (req: Request<{ teamPid: string }>, res: try { const discipline = await prisma.team.findUnique({ where: { pid: teamPid }, - select: { discipline: true } + select: { discipline: true }, }); const maxteamsize = discipline?.discipline.maxTeamSize; const userCount = await prisma.participant.count({ - where: { team: { pid: teamPid } } + where: { team: { pid: teamPid } }, }); if (maxteamsize == userCount) { @@ -88,7 +88,7 @@ export const createParticipant = async (req: Request<{ teamPid: string }>, res: select: returnedParticipant, }); - return res.status(201).json({ type: "success", payload: { participant }, }); + return res.status(201).json({ type: "success", payload: { participant } }); } catch (e) { if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { return res @@ -176,9 +176,7 @@ export const deleteParticipant = async (req: Request<{ pid: string }>, res: Resp }; export async function getGroupByParticipantPid(partPid: string) { - const parti = ( - await prisma.participant.findUnique({ where: { pid: partPid }, select: { group: true } }) - )?.group.pid; + const parti = (await prisma.participant.findUnique({ where: { pid: partPid }, select: { group: true } }))?.group.pid; if (!parti) { throw new NotFoundError("participant", partPid); diff --git a/src/Controllers/role.controller.ts b/src/Controllers/role.controller.ts index 1c761e6..8860f52 100644 --- a/src/Controllers/role.controller.ts +++ b/src/Controllers/role.controller.ts @@ -106,7 +106,11 @@ export async function assignParticipantToRole(req: Request<{ pid: string }>, res } try { - const schema = await prisma.role.update({ where: { pid }, data: { participant: { connect: { pid: participantPid } } }, select: detailedRole }); + const schema = await prisma.role.update({ + where: { pid }, + data: { participant: { connect: { pid: participantPid } } }, + select: detailedRole, + }); return res.status(200).json({ type: "success", diff --git a/src/Controllers/team.controller.ts b/src/Controllers/team.controller.ts index 16dd84a..7b685a0 100644 --- a/src/Controllers/team.controller.ts +++ b/src/Controllers/team.controller.ts @@ -119,7 +119,7 @@ export const deleteTeam = async (req: Request, res: Response) => { } if (req.auth?.permission_level == "STANDARD") { - throw new AuthError("STANDARD Admins are not allowed to delete Teams!") + throw new AuthError("STANDARD Admins are not allowed to delete Teams!"); } try { @@ -137,7 +137,7 @@ export const deleteTeam = async (req: Request, res: Response) => { export async function checkTeamExistence(teamPid: string) { const teamCount = await prisma.team.count({ - where: { pid: teamPid, } + where: { pid: teamPid }, }); if (teamCount == 0) { throw new NotFoundError("team", teamPid); @@ -145,13 +145,14 @@ export async function checkTeamExistence(teamPid: string) { } export async function getGroupsByTeamPid(teamPid: string) { - const team = ( - await prisma.team.findUnique({ where: { pid: teamPid }, select: { participants: { select: { group: true } } } }) - ); + const team = await prisma.team.findUnique({ + where: { pid: teamPid }, + select: { participants: { select: { group: true } } }, + }); let groups: string[] = []; - team?.participants.forEach(participant => { + team?.participants.forEach((participant) => { groups.push(participant.group.pid); }); diff --git a/src/Middleware/auth/auth.ts b/src/Middleware/auth/auth.ts index 5026edb..3ef66a9 100644 --- a/src/Middleware/auth/auth.ts +++ b/src/Middleware/auth/auth.ts @@ -18,82 +18,42 @@ export const getBearerToken = (authorization: string) => authorization.slice(7); 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"); + async (req: Request, res: Response, next: NextFunction) => { + if (!JWT_SECRET) { + throw new Error("JWT_SECRET not set"); + } + + const { authorization } = req.headers; + + if (!authorization) { + if (config.optional) { + return false; } - const { authorization } = req.headers; + 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 ", - }, - }); - } - - 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") { - if (config.controlled) { - return false; - } - throw new AuthError("Teamleader authentication is not supported for this operation!"); - } - - 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) { + try { + token_payload_ = jwt.verify(getBearerToken(authorization), JWT_SECRET); + } catch (e) { + if (e instanceof JsonWebTokenError) { return res.status(403).json({ type: "error", payload: { @@ -102,22 +62,62 @@ const _requireAdminAuthentication = }); } - 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, - }; + throw e; + } - if (!config.controlled) { - next(); + 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") { + if (config.controlled) { + return false; + } + throw new AuthError("Teamleader authentication is not supported for this operation!"); } - return true; + 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: { + 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, }; + if (!config.controlled) { + next(); + } + + return true; + }; + export const requireAuthentication = _requireAdminAuthentication({ optional: false, controlled: false }); type AuthType = "admin" | "teamleader"; @@ -144,37 +144,37 @@ function getAuthTypes(type: AuthType | AuthTypeConfig): 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; + async (req: Request, res: Response, next: NextFunction) => { + const types = getAuthTypes(config.type); + const optional = config.optional; - let adminFinished = false; - let teamleaderFinished = false; + let adminFinished = false; + let teamleaderFinished = false; - if (types.includes("admin")) { - adminFinished = Boolean(await _requireAdminAuthentication({ optional: true, controlled: true })(req, res, next)); + if (types.includes("admin")) { + adminFinished = Boolean(await _requireAdminAuthentication({ optional: true, controlled: true })(req, res, next)); - if (adminFinished) { - return next(); - } + if (adminFinished) { + return next(); } + } - if (types.includes("teamleader")) { - teamleaderFinished = Boolean( - _requireTeamleaderAuthentication({ optional: true, controlled: true })(req, res, next) - ); + if (types.includes("teamleader")) { + teamleaderFinished = Boolean( + _requireTeamleaderAuthentication({ optional: true, controlled: true })(req, res, next) + ); - if (teamleaderFinished) { - return next(); - } + if (teamleaderFinished) { + return next(); } + } - if (!config.optional) { - throw new AuthError("No sufficient authorization was provided for this operation"); - } + if (!config.optional) { + throw new AuthError("No sufficient authorization was provided for this operation"); + } - next(); - }; + next(); + }; export function requireResponsibleForGroups(auth: AuthJWTPayload | undefined, groupPids: string[] | string) { if (auth?.permission_level === "ELEVATED") { @@ -182,7 +182,7 @@ export function requireResponsibleForGroups(auth: AuthJWTPayload | undefined, gr } if (Array.isArray(groupPids)) { - groupPids.forEach(gr => { + groupPids.forEach((gr) => { if (auth?.groups.includes(gr)) { return; } diff --git a/src/Middleware/auth/teamleaderAuth.ts b/src/Middleware/auth/teamleaderAuth.ts index 1052ed8..0af4e3a 100644 --- a/src/Middleware/auth/teamleaderAuth.ts +++ b/src/Middleware/auth/teamleaderAuth.ts @@ -26,63 +26,63 @@ export function generateTeamleaderJWT(teamleader: Team) { 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"); + (req: Request, res: Response, next: NextFunction) => { + if (!JWT_SECRET) { + throw new Error("JWT_SECRET not set"); + } + + const { authorization } = req.headers; + + if (!authorization) { + if (config.optional) { + return false; } - const { authorization } = req.headers; + 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 ", + }, + }); + } - return res.status(403).send({ + 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: - "The request did not include the Authorization header (Only the team leader can perform this operation)", + message: "Token could not be verified; It might be expired", }, }); } - 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; - } - }; + throw e; + } + }; export const requireTeamleaderAuthentication = _requireTeamleaderAuthentication({ optional: false, controlled: false }); diff --git a/src/Routes/event.routes.ts b/src/Routes/event.routes.ts index 7c2d34d..ef6e6e4 100644 --- a/src/Routes/event.routes.ts +++ b/src/Routes/event.routes.ts @@ -1,12 +1,6 @@ import Express from "express"; import { string } from "zod"; -import { - addEvent, - deleteEvent, - getAllEvents, - getEvent, - updateEvent, -} from "../Controllers/event.controller"; +import { addEvent, deleteEvent, getAllEvents, getEvent, updateEvent } from "../Controllers/event.controller"; import { requireAuthentication } from "../Middleware/auth/auth"; const router = Express.Router(); diff --git a/src/Routes/media.routes.ts b/src/Routes/media.routes.ts index e4c633a..6785f7b 100644 --- a/src/Routes/media.routes.ts +++ b/src/Routes/media.routes.ts @@ -1,7 +1,14 @@ import express from "express"; import fileUpload from "express-fileupload"; import { requireAuthentication } from "../Middleware/auth/auth"; -import { deleteMedia, getAllMedia, getMediaMeta, linkMedia, unlinkMedia, uploadImage } from "../Controllers/media.controller"; +import { + deleteMedia, + getAllMedia, + getMediaMeta, + linkMedia, + unlinkMedia, + uploadImage, +} from "../Controllers/media.controller"; import eventRouter from "./event.routes"; import disciplineRouter from "./discipline.routes"; import roleSchemaRouter from "./role_schema.routes"; @@ -19,28 +26,28 @@ router.get("/:pid/meta", getMediaMeta); router.delete("/:pid", requireAuthentication, deleteMedia); -eventRouter.post<"/:pid/media", { pid: string }>( - "/:pid/media", requireAuthentication, linkMedia +eventRouter.post<"/:pid/media", { pid: string }>("/:pid/media", requireAuthentication, linkMedia); + +eventRouter.delete<"/:pid/media/:mediaPid", { pid: string; mediaPid: string }>( + "/:pid/media/:mediaPid", + requireAuthentication, + unlinkMedia ); -eventRouter.delete<"/:pid/media/:mediaPid", { pid: string, mediaPid: string }>( - "/:pid/media/:mediaPid", requireAuthentication, unlinkMedia +disciplineRouter.post<"/:pid/media", { pid: string }>("/:pid/media", requireAuthentication, linkMedia); + +disciplineRouter.delete<"/:pid/media/:mediaPid", { pid: string; mediaPid: string }>( + "/:pid/media/:mediaPid", + requireAuthentication, + unlinkMedia ); -disciplineRouter.post<"/:pid/media", { pid: string }>( - "/:pid/media", requireAuthentication, linkMedia -); +roleSchemaRouter.post<"/:pid/media", { pid: string }>("/:pid/media", requireAuthentication, linkMedia); -disciplineRouter.delete<"/:pid/media/:mediaPid", { pid: string, mediaPid: string }>( - "/:pid/media/:mediaPid", requireAuthentication, unlinkMedia -); - -roleSchemaRouter.post<"/:pid/media", { pid: string }>( - "/:pid/media", requireAuthentication, linkMedia -); - -roleSchemaRouter.delete<"/:pid/media/:mediaPid", { pid: string, mediaPid: string }>( - "/:pid/media/:mediaPid", requireAuthentication, unlinkMedia +roleSchemaRouter.delete<"/:pid/media/:mediaPid", { pid: string; mediaPid: string }>( + "/:pid/media/:mediaPid", + requireAuthentication, + unlinkMedia ); export default router; diff --git a/src/Routes/role.routes.ts b/src/Routes/role.routes.ts index 81ee739..f5df428 100644 --- a/src/Routes/role.routes.ts +++ b/src/Routes/role.routes.ts @@ -17,4 +17,4 @@ teamRouter.get<"/:pid/roles", { pid: string }>( getRolesForTeam ); -export default router; \ No newline at end of file +export default router;