diff --git a/docker-compose.yml b/docker-compose.yml index b706db3..54a831b 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,3 +35,9 @@ services: - DATABASE_URL=postgresql://server:${DATABASE_PASSWORD}@postgres:5432/management?schema=public - DATABASE_USER=server - DATABASE_PASSWORD=${DATABASE_PASSWORD} + - JWT_SECRET=${JWT_SECRET} + - DOMAIN=${DOMAIN} + - MAILUSER=${MAILUSER} + - MAILPASSWORD=${MAILPASSWORD} + - FRONTEND_MAIL_ENDPOINT=https://josport.at/authteamleader/ + - ALLOW_ORIGIN=https://josport.at diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 79ea5f2..cef0fb2 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -37,7 +37,7 @@ model Discipline { id Int @id @default(autoincrement()) pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid name String - briefDescription String + briefDescription String @default("description") fullDescription String? minTeamSize Int maxTeamSize Int diff --git a/src/Controllers/participant.controller.ts b/src/Controllers/participant.controller.ts index 08bf4db..b1960c7 100644 --- a/src/Controllers/participant.controller.ts +++ b/src/Controllers/participant.controller.ts @@ -1,12 +1,17 @@ import prisma from "../lib/prisma"; import { string, z } from "zod"; import { Request, Response } from "express"; -import { DataType, generateError, generateInvalidBodyError } from "./common"; +import { DataType, generateError, generateInvalidBodyError, createInsufficientPermissionsError } from "./common"; import { Prisma } from "@prisma/client"; import { PrismaClientKnownRequestError } from "@prisma/client/runtime"; import NotFoundError from "../Middleware/error/NotFoundError"; import { requireLeaderOfTeam, requireResponsibleForParticipant } from "../Middleware/auth/teamleaderAuth"; import { requireResponsibleForGroups } from "../Middleware/auth/auth"; +import { requireConfiguredAuthentication } from "../Middleware/auth/auth"; +import { isTeamleaderJWTPayload, TeamleaderJWTPayload } from "../Middleware/auth/teamleaderAuth"; +import { AuthJWTPayload } from "./admin_auth.controller"; +import AuthError from "../Middleware/error/AuthError"; +import { getGroupsByTeamPid } from "./team.controller"; require("express-async-errors"); @@ -16,17 +21,11 @@ const InitialParticipant = z.object({ groupPid: z.string().min(1).uuid(), }); -const returnedParticipant = { +const basicParticipant = { pid: true, firstName: true, lastName: true, relevance: true, - team: { - select: { - pid: true, - name: true, - }, - }, group: { select: { pid: true, @@ -35,7 +34,76 @@ const returnedParticipant = { }, } as const; -// at: POST api/participants/ +const returnedParticipant = { + ...basicParticipant, + team: { + select: { + pid: true, + name: true, + }, + }, +} as const; + +const _getAllParticipants = async (res: Response, req: Request, teamPid?: string) => { + if (req.teamleader?.isAuthenticated) { + await requireLeaderOfTeam(req.teamleader, teamPid); + } else if (req.auth?.permission_level == "STANDARD") { + if (!teamPid) { + throw new AuthError("A STANDARD Admin is not allowed to fetch all Participants!"); + } else { + requireResponsibleForGroups(req.auth, await getGroupsByTeamPid(teamPid)); + } + } + + const participants = await prisma.participant.findMany({ + where: { team: { pid: teamPid } }, + select: basicParticipant, + }); + + return res.status(200).json({ + type: "success", + payload: { + participants, + }, + }); +}; + +export const getAllParticipants = async (req: Request<{}, {}, {}, { teamPid?: string }>, res: Response) => { + return _getAllParticipants(res, req, req.query.teamPid); +}; + +export const getAllParticipantsParams = async (req: Request<{ teamPid: string }>, res: Response) => { + return _getAllParticipants(res, req, req.params.teamPid); +}; + +export const getParticipantForRole = async (req: Request<{ rolePid: string }>, res: Response) => { + const participant = await prisma.participant.findFirst({ + where: { roles: { some: { pid: req.params.rolePid } } }, + select: returnedParticipant, + }); + + if (!participant) { + return res.status(404).json({ + type: "error", + payload: { + message: `Could not find a participant for the role with the ID '${req.params.rolePid}'`, + }, + }); + } + + if (req.teamleader?.isAuthenticated) { + await requireLeaderOfTeam(req.teamleader, participant?.team.pid); + } else if (req.auth?.permission_level == "STANDARD") { + requireResponsibleForGroups(req.auth, await getGroupsByTeamPid(participant?.team.pid)); + } + + return res.status(200).json({ + type: "success", + payload: { participant }, + }); +}; + +// at: POST api/teams/:teamPid/participant/ export const createParticipant = async (req: Request<{ teamPid: string }>, res: Response) => { const { teamPid } = req.params; diff --git a/src/Controllers/role.controller.ts b/src/Controllers/role.controller.ts index 8860f52..7fe04fb 100644 --- a/src/Controllers/role.controller.ts +++ b/src/Controllers/role.controller.ts @@ -11,6 +11,13 @@ import { getGroupsByTeamPid } from "./team.controller"; require("express-async-errors"); +const basicRole = { + pid: true, + score: true, + schema: { select: { pid: true } }, + participant: { select: { pid: true, firstName: true, lastName: true } }, +}; + const detailedRole = { pid: true, score: true, @@ -61,18 +68,13 @@ export async function getRolesForTeam(req: Request<{ pid: string }>, res: Respon if (req.teamleader?.isAuthenticated) { await requireLeaderOfTeam(req.teamleader, pid); - } else { + } else if (req.auth?.permission_level == "STANDARD") { requireResponsibleForGroups(req.auth, await getGroupsByTeamPid(pid)); } const roles = await prisma.role.findMany({ where: { team: { pid } }, - select: { - pid: true, - score: true, - schema: { select: { pid: true } }, - participant: { select: { pid: true, firstName: true, lastName: true } }, - }, + select: basicRole, }); return res.status(200).json({ @@ -83,6 +85,29 @@ export async function getRolesForTeam(req: Request<{ pid: string }>, res: Respon }); } +export async function getRole(req: Request<{ rolePid: string }>, res: Response) { + const rolePid = req.params.rolePid; + + const role = await prisma.role.findUnique({ where: { pid: rolePid }, select: detailedRole }); + + if (!role) { + throw new NotFoundError("role", rolePid); + } + + if (req.teamleader?.isAuthenticated) { + await requireLeaderOfTeam(req.teamleader, role.team.pid); + } else if (req.auth?.permission_level == "STANDARD" && role.participant?.pid !== undefined) { + requireResponsibleForGroups(req.auth, await getGroupByParticipantPid(role.participant?.pid)); + } + + return res.status(200).json({ + type: "success", + payload: { + role, + }, + }); +} + const AssignParticipantToRoleBody = z.object({ participantPid: z.string().uuid(), }); diff --git a/src/Controllers/role_schema.controller.ts b/src/Controllers/role_schema.controller.ts index 99c5369..f9a9445 100644 --- a/src/Controllers/role_schema.controller.ts +++ b/src/Controllers/role_schema.controller.ts @@ -136,7 +136,7 @@ export const createRoleSchema = async ( } }; -export const UpdateRoleSchema = async (req: Request<{ pid: string }>, res: Response) => { +export const updateRoleSchema = async (req: Request<{ pid: string }>, res: Response) => { if (req.auth?.permission_level !== "ELEVATED") { return res.status(403).json(createInsufficientPermissionsError()); } @@ -177,7 +177,27 @@ export const UpdateRoleSchema = async (req: Request<{ pid: string }>, res: Respo }); } catch (e) { if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") { - throw new NotFoundError("discipline", pid); + throw new NotFoundError("role-schema", pid); + } + + throw e; + } +}; + +export const deleteRoleSchema = async (req: Request<{ pid: string }>, res: Response) => { + if (req.auth?.permission_level !== "ELEVATED") { + return 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") { + throw new NotFoundError("role-schema", pid); } throw e; diff --git a/src/Controllers/team.controller.ts b/src/Controllers/team.controller.ts index 7b685a0..be0a66e 100644 --- a/src/Controllers/team.controller.ts +++ b/src/Controllers/team.controller.ts @@ -1,6 +1,6 @@ import { Request, Response } from "express"; import prisma from "../lib/prisma"; -import { DataType, generateInvalidBodyError } from "./common"; +import { createInsufficientPermissionsError, DataType, generateInvalidBodyError } from "./common"; import { requireLeaderOfTeam } from "../Middleware/auth/teamleaderAuth"; import { TeamBody } from "./user_auth.controller"; import { Prisma } from "@prisma/client"; diff --git a/src/Controllers/user_auth.controller.ts b/src/Controllers/user_auth.controller.ts index c2f02ef..268e1cc 100644 --- a/src/Controllers/user_auth.controller.ts +++ b/src/Controllers/user_auth.controller.ts @@ -97,7 +97,7 @@ export const register = async (req: Request<{}, {}, CreateTeamBody>, res: Respon }; export const requestToken = async (req: Request, res: Response) => { - const data = z.object({ teamId: z.string().min(1) }).safeParse(req); + const data = z.object({ teamId: z.string().min(1) }).safeParse(req.body); if (data.success == false) { return res.status(400).json(generateInvalidBodyError({ teamId: DataType.STRING }, data.error)); @@ -130,7 +130,7 @@ export const requestToken = async (req: Request, res: Response) => { }; export const requestTokenEmail = async (req: Request, res: Response) => { - const data = z.object({ email: z.string().min(1) }).safeParse(req); + const data = z.object({ email: z.string().email() }).safeParse(req.body); if (data.success == false) { return res.status(400).json(generateInvalidBodyError({ email: DataType.STRING }, data.error)); @@ -149,23 +149,24 @@ export const requestTokenEmail = async (req: Request, res: Response) => { }, }); - if (!teams) { - return res.status(404).json(generateError("Team does not exist!")); + if (teams.length <= 0) { + // REVIEW: Potential for time-based attacks + return res + .status(200) + .json({ type: "sucess", payload: { message: "If a team with the provided email exist, the token was sent!" } }); } 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!" } }); + res + .status(200) + .json({ type: "sucess", payload: { message: "If a team with the provided email exist, the token was sent!" } }); }; export const verifyEmail = async (req: Request, res: Response) => { diff --git a/src/Middleware/auth/teamleaderAuth.ts b/src/Middleware/auth/teamleaderAuth.ts index 0af4e3a..9b5c745 100644 --- a/src/Middleware/auth/teamleaderAuth.ts +++ b/src/Middleware/auth/teamleaderAuth.ts @@ -10,6 +10,10 @@ export interface TeamleaderJWTPayload { team: string; } +export function isTeamleaderJWTPayload(payload: any): payload is TeamleaderJWTPayload { + return typeof payload.team === "string"; +} + const JWT_SECRET = process.env.JWT_SECRET; export function generateTeamleaderJWT(teamleader: Team) { @@ -86,8 +90,13 @@ export const _requireTeamleaderAuthentication = export const requireTeamleaderAuthentication = _requireTeamleaderAuthentication({ optional: false, controlled: false }); -export async function requireLeaderOfTeam(auth: TeamleaderJWTPayload | undefined, teamPid: string) { +export async function requireLeaderOfTeam(auth: TeamleaderJWTPayload | undefined, teamPid?: string) { + if (!teamPid) { + throw new AuthError("Could not match IDs"); + } + 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 2e76a2e..7e113db 100644 --- a/src/Routes/participant.routes.ts +++ b/src/Routes/participant.routes.ts @@ -1,12 +1,40 @@ import express from "express"; -import { createParticipant, deleteParticipant, updateParticipant } from "../Controllers/participant.controller"; -import { requireConfiguredAuthentication } from "../Middleware/auth/auth"; import teamRouter from "./team.routes"; +import roleRouter from "./role.routes"; +import { + createParticipant, + deleteParticipant, + getAllParticipants, + getAllParticipantsParams, + getParticipantForRole, + updateParticipant, +} from "../Controllers/participant.controller"; +import { requireAuthentication, requireConfiguredAuthentication } from "../Middleware/auth/auth"; + +require("express-async-errors"); const router = express.Router(); -teamRouter.post<"/:teamPid/participants", { teamPid: string }>( - "/:teamPid/participants", +router.get( + "/", + requireConfiguredAuthentication({ optional: false, type: { admin: true, teamleader: true } }), + getAllParticipants +); + +teamRouter.get( + "/:teamPid/particpants", + requireConfiguredAuthentication({ type: { admin: true, teamleader: true }, optional: false }), + getAllParticipantsParams +); + +roleRouter.get( + "/:rolePid/participant", + requireConfiguredAuthentication({ type: { admin: true, teamleader: true }, optional: false }), + getParticipantForRole +); + +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 f5df428..f070e32 100644 --- a/src/Routes/role.routes.ts +++ b/src/Routes/role.routes.ts @@ -1,10 +1,17 @@ import Express from "express"; -import { assignParticipantToRole, getRolesForTeam } from "../Controllers/role.controller"; -import { requireConfiguredAuthentication } from "../Middleware/auth/auth"; +import { assignParticipantToRole, getRole, getRolesForTeam } from "../Controllers/role.controller"; +import { requireAuthentication, requireConfiguredAuthentication } from "../Middleware/auth/auth"; +import { requireTeamleaderAuthentication } from "../Middleware/auth/teamleaderAuth"; import teamRouter from "./team.routes"; const router = Express.Router(); +router.get( + "/:rolePid", + requireConfiguredAuthentication({ type: { admin: true, teamleader: true }, optional: false }), + getRole +); + router.put<"/:pid/participant", { pid: string }>( "/:pid/participant", requireConfiguredAuthentication({ optional: false, type: { admin: true, teamleader: true } }), diff --git a/src/Routes/role_schema.routes.ts b/src/Routes/role_schema.routes.ts index 688dafc..e2f4b5c 100644 --- a/src/Routes/role_schema.routes.ts +++ b/src/Routes/role_schema.routes.ts @@ -2,11 +2,13 @@ import express from "express"; import disciplineRouter from "./discipline.routes"; import { createRoleSchema, + deleteRoleSchema, getAllRoleSchemas, getAllRoleSchemasWithParam, getRoleSchema, + updateRoleSchema, } from "../Controllers/role_schema.controller"; -import { requireAuthentication } from "../Middleware/auth/auth"; +import { requireAuthentication, requireConfiguredAuthentication } from "../Middleware/auth/auth"; const router = express.Router(); @@ -14,6 +16,10 @@ router.get("/", getAllRoleSchemas); router.get("/:pid", getRoleSchema); +router.patch("/:pid", requireConfiguredAuthentication({ optional: false, type: "admin" }), updateRoleSchema); + +router.delete("/:pid", requireConfiguredAuthentication({ optional: false, type: "admin" }), deleteRoleSchema); + disciplineRouter.get("/:disciplinePid/role-schemas", getAllRoleSchemasWithParam); disciplineRouter.post("/:disciplinePid/role-schemas", requireAuthentication, createRoleSchema); diff --git a/src/Routes/user_auth.routes.ts b/src/Routes/user_auth.routes.ts index 568b764..12028f0 100644 --- a/src/Routes/user_auth.routes.ts +++ b/src/Routes/user_auth.routes.ts @@ -1,10 +1,11 @@ import express from "express"; -import { register, requestToken, verifyEmail } from "../Controllers/user_auth.controller"; +import { register, requestToken, requestTokenEmail, verifyEmail } from "../Controllers/user_auth.controller"; const router = express.Router(); router.post("/", register); router.get("/verify/:code", verifyEmail); -router.get("/token", requestToken); +router.post("/token", requestToken); +router.post("/email-token", requestTokenEmail); export default router; diff --git a/src/Tests/events.test.ts b/src/Tests/events.test.ts index ef4dae5..08c8985 100644 --- a/src/Tests/events.test.ts +++ b/src/Tests/events.test.ts @@ -81,4 +81,4 @@ describe("events", () => { }); }); }); -*/ \ No newline at end of file +*/ diff --git a/src/Tests/mail.test.ts b/src/Tests/mail.test.ts index 3102622..fccddc0 100644 --- a/src/Tests/mail.test.ts +++ b/src/Tests/mail.test.ts @@ -15,4 +15,4 @@ describe("mail", () => { info.accepted.length.should.eq(1); }); }); -*/ \ No newline at end of file +*/