Merge pull request #84 from detleph/dev

Merging Dev into Main
This commit is contained in:
La_Felx
2022-06-10 07:33:01 +02:00
committed by GitHub
14 changed files with 212 additions and 41 deletions
+6
View File
@@ -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
+1 -1
View File
@@ -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
+77 -9
View File
@@ -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;
+32 -7
View File
@@ -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(),
});
+22 -2
View File
@@ -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;
+1 -1
View File
@@ -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";
+10 -9
View File
@@ -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) => {
+10 -1
View File
@@ -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");
}
+32 -4
View File
@@ -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
);
+9 -2
View File
@@ -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 } }),
+7 -1
View File
@@ -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);
+3 -2
View File
@@ -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;
+1 -1
View File
@@ -81,4 +81,4 @@ describe("events", () => {
});
});
});
*/
*/
+1 -1
View File
@@ -15,4 +15,4 @@ describe("mail", () => {
info.accepted.length.should.eq(1);
});
});
*/
*/