mirror of
https://github.com/detleph/server.git
synced 2026-09-04 00:26:03 +02:00
@@ -15,4 +15,5 @@ Before Runningthis on you local machine some things have to be setup
|
|||||||
MAILPASSWORD: THE PASSWORD FOR THE MAIL ACCOUNT
|
MAILPASSWORD: THE PASSWORD FOR THE MAIL ACCOUNT
|
||||||
DEV: SWITCH FOR DEV MODE AFFECTS EMAIL SERVER
|
DEV: SWITCH FOR DEV MODE AFFECTS EMAIL SERVER
|
||||||
ALLOW_ORIGIN: ORIGIN OF THE PRODUCTION CLIENT (FOR CORS)
|
ALLOW_ORIGIN: ORIGIN OF THE PRODUCTION CLIENT (FOR CORS)
|
||||||
|
FRONTEND_MAIL_ENDPOINT: THE ENDPOINT THE FRONTEND DOES EMAIL-VERIFICATION
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ if [ "$RECREATE" = true ]; then
|
|||||||
-e DATABASE_URL="postgresql://server:server@postgres:5432/management?schema=public" \
|
-e DATABASE_URL="postgresql://server:server@postgres:5432/management?schema=public" \
|
||||||
-e NODE_ENV="development" \
|
-e NODE_ENV="development" \
|
||||||
-e PORT="${D_PORT}" \
|
-e PORT="${D_PORT}" \
|
||||||
|
-e JWT_SECRET="not_for_production" \
|
||||||
-p "${D_PORT}":"${D_PORT}" \
|
-p "${D_PORT}":"${D_PORT}" \
|
||||||
--entrypoint "/app/scripts/docker-entrypoint.dev.sh" \
|
--entrypoint "/app/scripts/docker-entrypoint.dev.sh" \
|
||||||
node
|
node
|
||||||
|
|||||||
Generated
+33
@@ -10,6 +10,7 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^3.3.0",
|
"@prisma/client": "^3.3.0",
|
||||||
|
"@types/cors": "^2.8.12",
|
||||||
"@types/express-fileupload": "^1.2.1",
|
"@types/express-fileupload": "^1.2.1",
|
||||||
"@types/handlebars": "^4.1.0",
|
"@types/handlebars": "^4.1.0",
|
||||||
"@types/jsonwebtoken": "^8.5.5",
|
"@types/jsonwebtoken": "^8.5.5",
|
||||||
@@ -18,6 +19,7 @@
|
|||||||
"@types/nodemailer": "^6.4.4",
|
"@types/nodemailer": "^6.4.4",
|
||||||
"@types/redis": "^2.8.32",
|
"@types/redis": "^2.8.32",
|
||||||
"argon2": "^0.28.2",
|
"argon2": "^0.28.2",
|
||||||
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"express-async-errors": "^3.1.1",
|
"express-async-errors": "^3.1.1",
|
||||||
@@ -242,6 +244,11 @@
|
|||||||
"integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==",
|
"integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/@types/express": {
|
||||||
"version": "4.17.13",
|
"version": "4.17.13",
|
||||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz",
|
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz",
|
||||||
@@ -989,6 +996,18 @@
|
|||||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/create-require": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||||
@@ -4134,6 +4153,11 @@
|
|||||||
"integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==",
|
"integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==",
|
||||||
"dev": true
|
"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": {
|
"@types/express": {
|
||||||
"version": "4.17.13",
|
"version": "4.17.13",
|
||||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz",
|
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz",
|
||||||
@@ -4757,6 +4781,15 @@
|
|||||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||||
"dev": true
|
"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": {
|
"create-require": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||||
|
|||||||
+12
-6
@@ -34,11 +34,13 @@ model Admin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Discipline {
|
model Discipline {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||||
name String
|
name String
|
||||||
minTeamSize Int
|
briefDescription String
|
||||||
maxTeamSize Int
|
fullDescription String?
|
||||||
|
minTeamSize Int
|
||||||
|
maxTeamSize Int
|
||||||
|
|
||||||
roles RoleSchema[]
|
roles RoleSchema[]
|
||||||
teams Team[]
|
teams Team[]
|
||||||
@@ -64,8 +66,10 @@ model Team {
|
|||||||
pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||||
name String
|
name String
|
||||||
leaderEmail 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
|
disciplineId Int
|
||||||
}
|
}
|
||||||
@@ -79,6 +83,8 @@ model Participant {
|
|||||||
|
|
||||||
group Group @relation(fields: [groupId], references: [id], onDelete: Cascade)
|
group Group @relation(fields: [groupId], references: [id], onDelete: Cascade)
|
||||||
groupId Int
|
groupId Int
|
||||||
|
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||||
|
teamId Int
|
||||||
roles Role[]
|
roles Role[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +127,7 @@ model Group {
|
|||||||
model Media {
|
model Media {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
pid String @unique
|
pid String @unique
|
||||||
description String
|
description String @default("visual")
|
||||||
|
|
||||||
events Event[]
|
events Event[]
|
||||||
disciplines Discipline[]
|
disciplines Discipline[]
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import argon2 from "argon2";
|
|||||||
import { AUTH_ERROR, createInsufficientPermissionsError, DataType, generateInvalidBodyError } from "./common";
|
import { AUTH_ERROR, createInsufficientPermissionsError, DataType, generateInvalidBodyError } from "./common";
|
||||||
import { authClient } from "../lib/redis";
|
import { authClient } from "../lib/redis";
|
||||||
|
|
||||||
|
require("express-async-errors");
|
||||||
|
|
||||||
export const regenerateRevision = async (pid: string) => {
|
export const regenerateRevision = async (pid: string) => {
|
||||||
// TOOO: Add error handling
|
// TOOO: Add error handling
|
||||||
const { revision } = await prisma.admin.update({
|
const { revision } = await prisma.admin.update({
|
||||||
@@ -27,7 +29,9 @@ export const getAllAdmins = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add exception handling
|
// 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({
|
res.status(200).json({
|
||||||
type: "success",
|
type: "success",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import argon2 from "argon2";
|
|||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import { DataType, generateInvalidBodyError } from "./common";
|
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";
|
const TOKEN_EXPIRY = "4 days";
|
||||||
|
|
||||||
export interface AuthJWTPayload {
|
export interface AuthJWTPayload {
|
||||||
@@ -18,6 +18,10 @@ export interface AuthJWTPayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createAdminJWT(admin: Admin & { groups: Group[] }) {
|
function createAdminJWT(admin: Admin & { groups: Group[] }) {
|
||||||
|
if (!JWT_SECRET) {
|
||||||
|
throw new Error("JWT_SECRET not set");
|
||||||
|
}
|
||||||
|
|
||||||
const payload: AuthJWTPayload = {
|
const payload: AuthJWTPayload = {
|
||||||
pid: admin.pid,
|
pid: admin.pid,
|
||||||
name: admin.name,
|
name: admin.name,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { AdminLevel, Prisma } from "@prisma/client";
|
import { AdminLevel, Prisma } from "@prisma/client";
|
||||||
import { PrismaClientKnownRequestError, PrismaClientUnknownRequestError } from "@prisma/client/runtime";
|
import { PrismaClientKnownRequestError, PrismaClientUnknownRequestError } from "@prisma/client/runtime";
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
|
import { ZodError } from "zod";
|
||||||
import prisma from "../lib/prisma";
|
import prisma from "../lib/prisma";
|
||||||
|
|
||||||
interface StringIndexedObject {
|
interface StringIndexedObject {
|
||||||
@@ -23,6 +24,7 @@ export enum DataType {
|
|||||||
NUMBER = "number",
|
NUMBER = "number",
|
||||||
INTEGER = "integer",
|
INTEGER = "integer",
|
||||||
PERMISSION_LEVEL = "'ELEVATED' | 'STANDARD'",
|
PERMISSION_LEVEL = "'ELEVATED' | 'STANDARD'",
|
||||||
|
JOB = "'TEAMLEADER' | 'MEMBER'",
|
||||||
DATETIME = "ISOstring",
|
DATETIME = "ISOstring",
|
||||||
UUID = "string",
|
UUID = "string",
|
||||||
RESULT_SCHEMA = "result_schema",
|
RESULT_SCHEMA = "result_schema",
|
||||||
@@ -32,7 +34,18 @@ interface Body {
|
|||||||
[k: string]: DataType;
|
[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 {
|
return {
|
||||||
type: "error",
|
type: "error",
|
||||||
payload: {
|
payload: {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Request, Response } from "express";
|
|||||||
import prisma from "../lib/prisma";
|
import prisma from "../lib/prisma";
|
||||||
import ForwardableError from "../Middleware/error/ForwardableError";
|
import ForwardableError from "../Middleware/error/ForwardableError";
|
||||||
import NotFoundError from "../Middleware/error/NotFoundError";
|
import NotFoundError from "../Middleware/error/NotFoundError";
|
||||||
|
import { number, z } from "zod";
|
||||||
import {
|
import {
|
||||||
createInsufficientPermissionsError,
|
createInsufficientPermissionsError,
|
||||||
DataType,
|
DataType,
|
||||||
@@ -15,12 +16,30 @@ import {
|
|||||||
|
|
||||||
require("express-async-errors");
|
require("express-async-errors");
|
||||||
|
|
||||||
|
const InitialDisciplineBody = z.object({
|
||||||
|
name: z.string().min(1),
|
||||||
|
minTeamSize: z.number(),
|
||||||
|
maxTeamSize: z.number(),
|
||||||
|
briefDescription: z.string().min(1),
|
||||||
|
fullDescription: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
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.partial({ fullDescription: true }).refine(...disciplineRefiner);
|
||||||
|
const updateDisciplineBody = InitialDisciplineBody.partial().refine(...disciplineRefiner);
|
||||||
|
|
||||||
const basicDiscipline = {
|
const basicDiscipline = {
|
||||||
pid: true,
|
pid: true,
|
||||||
name: true,
|
name: true,
|
||||||
visual: { select: { pid: true } },
|
visual: { select: { pid: true } },
|
||||||
maxTeamSize: true,
|
maxTeamSize: true,
|
||||||
minTeamSize: true,
|
minTeamSize: true,
|
||||||
|
briefDescription: true,
|
||||||
|
fullDescription: true,
|
||||||
event: { select: { pid: true, name: true } },
|
event: { select: { pid: true, name: true } },
|
||||||
roles: { select: { pid: true, name: true } },
|
roles: { select: { pid: true, name: true } },
|
||||||
} as const;
|
} as const;
|
||||||
@@ -117,6 +136,8 @@ interface CreateDisciplineBody {
|
|||||||
name?: string;
|
name?: string;
|
||||||
minTeamSize?: number;
|
minTeamSize?: number;
|
||||||
maxTeamSize?: number;
|
maxTeamSize?: number;
|
||||||
|
briefDescription?: string;
|
||||||
|
fullDescription?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// require: auth(ELEVATED)
|
// require: auth(ELEVATED)
|
||||||
@@ -126,32 +147,36 @@ export const createDiscipline = async (req: Request<{ eventPid: string }, {}, Cr
|
|||||||
return res.status(403).json(createInsufficientPermissionsError());
|
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(
|
return res.status(400).json(
|
||||||
generateInvalidBodyError({
|
generateInvalidBodyError(
|
||||||
name: DataType.STRING,
|
{
|
||||||
minTeamSize: DataType.NUMBER,
|
name: DataType.STRING,
|
||||||
maxTeamSize: DataType.NUMBER,
|
minTeamSize: DataType.NUMBER,
|
||||||
})
|
maxTeamSize: DataType.NUMBER,
|
||||||
|
briefDescription: DataType.STRING,
|
||||||
|
["fullDescription?"]: DataType.STRING,
|
||||||
|
},
|
||||||
|
result.error
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validateName(name)) {
|
const { name, minTeamSize, maxTeamSize, briefDescription, fullDescription } = result.data;
|
||||||
return res.status(400).json(NAME_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const discipline = await prisma.discipline.create({
|
const discipline = await prisma.discipline.create({
|
||||||
data: { name, minTeamSize, maxTeamSize, event: { connect: { pid: req.params.eventPid } } },
|
data: {
|
||||||
select: {
|
name,
|
||||||
pid: true,
|
minTeamSize,
|
||||||
name: true,
|
maxTeamSize,
|
||||||
minTeamSize: true,
|
briefDescription,
|
||||||
maxTeamSize: true,
|
fullDescription,
|
||||||
event: { select: { pid: true, name: true } },
|
event: { connect: { pid: req.params.eventPid } },
|
||||||
},
|
},
|
||||||
|
select: basicDiscipline,
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(201).json({ type: "success", payload: { discipline } });
|
return res.status(201).json({ type: "success", payload: { discipline } });
|
||||||
@@ -163,12 +188,60 @@ export const createDiscipline = async (req: Request<{ eventPid: string }, {}, Cr
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
interface DeleteDisciplineQueryParams {
|
// requires: auth(ELEVATED)
|
||||||
pid: string;
|
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); // 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,
|
||||||
|
briefDescription: DataType.STRING,
|
||||||
|
["fullDescription?"]: DataType.STRING,
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
briefDescription: body.briefDescription,
|
||||||
|
fullDescription: body.fullDescription,
|
||||||
|
},
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// requires: auth(ELEVATED)
|
// requires: auth(ELEVATED)
|
||||||
export const deleteDiscipline = async (req: Request<DeleteDisciplineQueryParams>, res: Response) => {
|
export const deleteDiscipline = async (req: Request<{ pid: string }>, res: Response) => {
|
||||||
if (req.auth?.permission_level !== "ELEVATED") {
|
if (req.auth?.permission_level !== "ELEVATED") {
|
||||||
res.status(403).json(createInsufficientPermissionsError());
|
res.status(403).json(createInsufficientPermissionsError());
|
||||||
}
|
}
|
||||||
@@ -187,61 +260,3 @@ export const deleteDiscipline = async (req: Request<DeleteDisciplineQueryParams>
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
interface visualParams {
|
|
||||||
disciplinePid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface visualBody {
|
|
||||||
mediaPid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const addVisual = async (req: Request<visualParams, {}, visualBody>, 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<visualParams & { pid: string }>, 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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
|
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
|
||||||
import { Request, Response } from "express";
|
import e, { Request, Response } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import prisma from "../lib/prisma";
|
import prisma from "../lib/prisma";
|
||||||
import NotFoundError from "../Middleware/error/NotFoundError";
|
import NotFoundError from "../Middleware/error/NotFoundError";
|
||||||
@@ -8,17 +8,49 @@ import { createInsufficientPermissionsError, DataType, generateError, generateIn
|
|||||||
|
|
||||||
require("express-async-errors");
|
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().min(1),
|
||||||
|
date: dateSchema,
|
||||||
|
briefDescription: z.string(),
|
||||||
|
fullDescription: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const UpdateBody = EventBody.partial();
|
||||||
|
|
||||||
|
const CreateEventBody = EventBody.partial({
|
||||||
|
fullDescription: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const getAllEvents = async (req: Request, res: Response) => {
|
export const getAllEvents = async (req: Request, res: Response) => {
|
||||||
const events = await prisma.event.findMany({
|
const events = await prisma.event.findMany({
|
||||||
select: {
|
select: detailedEvent,
|
||||||
name: true,
|
|
||||||
briefDescription: true,
|
|
||||||
fullDescription: true,
|
|
||||||
visual: { select: { pid: true, description: true } },
|
|
||||||
date: true,
|
|
||||||
pid: true,
|
|
||||||
id: false,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
@@ -45,15 +77,7 @@ export const getEvent = async (req: Request, res: Response) => {
|
|||||||
where: {
|
where: {
|
||||||
pid: eventId,
|
pid: eventId,
|
||||||
},
|
},
|
||||||
select: {
|
select: detailedEvent,
|
||||||
name: true,
|
|
||||||
briefDescription: true,
|
|
||||||
fullDescription: true,
|
|
||||||
date: true,
|
|
||||||
pid: true,
|
|
||||||
id: false,
|
|
||||||
visual: { select: { pid: true, description: true } },
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!event) {
|
if (!event) {
|
||||||
@@ -96,39 +120,33 @@ export const addEvent = async (req: Request, res: Response) => {
|
|||||||
if (req.auth?.permission_level !== "ELEVATED") {
|
if (req.auth?.permission_level !== "ELEVATED") {
|
||||||
res.status(403).json(createInsufficientPermissionsError());
|
res.status(403).json(createInsufficientPermissionsError());
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
typeof req.body.name !== "string" ||
|
const result = CreateEventBody.safeParse(req.body);
|
||||||
typeof req.body.date !== "string" ||
|
|
||||||
typeof req.body.briefDescription !== "string" ||
|
if (result.success === false) {
|
||||||
(req.body.fullDescription && typeof req.body.fullDescription !== "string")
|
|
||||||
) {
|
|
||||||
return res.status(400).json(
|
return res.status(400).json(
|
||||||
generateInvalidBodyError({
|
generateInvalidBodyError(
|
||||||
name: DataType.STRING,
|
{
|
||||||
date: DataType.DATETIME,
|
name: DataType.STRING,
|
||||||
briefDescription: DataType.STRING,
|
date: DataType.DATETIME,
|
||||||
["fullDescription?"]: DataType.STRING,
|
briefDescription: DataType.STRING,
|
||||||
})
|
["fullDescription?"]: DataType.STRING,
|
||||||
|
},
|
||||||
|
result.error
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: Check if date is valid
|
const body = result.data;
|
||||||
|
|
||||||
const event = await prisma.event.create({
|
const event = await prisma.event.create({
|
||||||
data: {
|
data: {
|
||||||
name: req.body.name,
|
name: body.name,
|
||||||
date: req.body.date,
|
date: body.date,
|
||||||
briefDescription: req.body.briefDescription,
|
briefDescription: body.briefDescription,
|
||||||
fullDescription: req.body.fullDescription,
|
fullDescription: body.fullDescription,
|
||||||
},
|
|
||||||
select: {
|
|
||||||
name: true,
|
|
||||||
date: true,
|
|
||||||
pid: true,
|
|
||||||
id: false,
|
|
||||||
briefDescription: true,
|
|
||||||
fullDescription: true,
|
|
||||||
},
|
},
|
||||||
|
select: basicEvent,
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(201).json({
|
res.status(201).json({
|
||||||
@@ -139,15 +157,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) => {
|
export const updateEvent = async (req: Request<{ pid: string }>, res: Response) => {
|
||||||
if (req.auth?.permission_level !== "ELEVATED") {
|
if (req.auth?.permission_level !== "ELEVATED") {
|
||||||
res.status(403).json(createInsufficientPermissionsError());
|
res.status(403).json(createInsufficientPermissionsError());
|
||||||
@@ -159,12 +168,15 @@ export const updateEvent = async (req: Request<{ pid: string }>, res: Response)
|
|||||||
|
|
||||||
if (result.success === false) {
|
if (result.success === false) {
|
||||||
return res.status(400).json(
|
return res.status(400).json(
|
||||||
generateInvalidBodyError({
|
generateInvalidBodyError(
|
||||||
name: DataType.STRING,
|
{
|
||||||
date: DataType.DATETIME,
|
name: DataType.STRING,
|
||||||
briefDescription: DataType.STRING,
|
date: DataType.DATETIME,
|
||||||
["fullDescription?"]: DataType.STRING,
|
briefDescription: DataType.STRING,
|
||||||
})
|
["fullDescription?"]: DataType.STRING,
|
||||||
|
},
|
||||||
|
result.error
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,10 +200,6 @@ export const updateEvent = async (req: Request<{ pid: string }>, res: Response)
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!event) {
|
|
||||||
throw new NotFoundError("event", pid);
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
type: "success",
|
type: "success",
|
||||||
payload: {
|
payload: {
|
||||||
@@ -199,24 +207,8 @@ export const updateEvent = async (req: Request<{ pid: string }>, res: Response)
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") {
|
||||||
return res.status(500).json({
|
throw new NotFoundError("event", pid);
|
||||||
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;
|
throw e;
|
||||||
@@ -241,71 +233,7 @@ export const deleteEvent = async (req: Request<DeleteEventQueryParams>, res: Res
|
|||||||
return res.status(204).end();
|
return res.status(204).end();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") {
|
if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") {
|
||||||
return res.status(404).json(generateError(`The organisation with the ID ${pid} could not be found`));
|
throw new NotFoundError("event", pid);
|
||||||
}
|
|
||||||
|
|
||||||
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<visualParams, {}, visualBody>, 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<visualParams & { pid: string }>, 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;
|
throw e;
|
||||||
|
|||||||
@@ -1,8 +1,28 @@
|
|||||||
import { Admin } from "@prisma/client";
|
import { Admin, Prisma } from "@prisma/client";
|
||||||
import { PrismaClientKnownRequestError, PrismaClientUnknownRequestError } from "@prisma/client/runtime";
|
import { PrismaClientKnownRequestError, PrismaClientUnknownRequestError } from "@prisma/client/runtime";
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
import prisma from "../lib/prisma";
|
import prisma from "../lib/prisma";
|
||||||
import { createInsufficientPermissionsError, generateError, genericError, handleCreateByName } from "./common";
|
import { requireResponsibleForGroups } from "../Middleware/auth/auth";
|
||||||
|
import NotFoundError from "../Middleware/error/NotFoundError";
|
||||||
|
import {
|
||||||
|
createInsufficientPermissionsError,
|
||||||
|
DataType,
|
||||||
|
generateError,
|
||||||
|
generateInvalidBodyError,
|
||||||
|
genericError,
|
||||||
|
handleCreateByName,
|
||||||
|
} from "./common";
|
||||||
|
|
||||||
|
require("express-async-errors");
|
||||||
|
|
||||||
|
const updateGroupBody = z
|
||||||
|
.object({
|
||||||
|
name: z.string().min(1),
|
||||||
|
user_limit: z.number().int().positive(),
|
||||||
|
level: z.number().int().nonnegative(),
|
||||||
|
})
|
||||||
|
.partial();
|
||||||
|
|
||||||
const basicGroup = {
|
const basicGroup = {
|
||||||
pid: true,
|
pid: true,
|
||||||
@@ -10,6 +30,16 @@ const basicGroup = {
|
|||||||
organisation: { select: { pid: true, name: true } },
|
organisation: { select: { pid: true, name: true } },
|
||||||
} as const;
|
} 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) => {
|
export const _getAllGroups = async (res: Response, organisationId: string | undefined) => {
|
||||||
const groups = await prisma.group.findMany({
|
const groups = await prisma.group.findMany({
|
||||||
where: { organisation: { pid: organisationId } },
|
where: { organisation: { pid: organisationId } },
|
||||||
@@ -115,6 +145,52 @@ export const createGroup = async (req: Request<{ organisationPid: string }, {},
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// requires: auth(STANDARD with GROUP permission)
|
||||||
|
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
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = result.data;
|
||||||
|
const { pid } = req.params;
|
||||||
|
|
||||||
|
requireResponsibleForGroups(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: detailedGroup,
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
interface DeleteGroupQueryParams {
|
interface DeleteGroupQueryParams {
|
||||||
pid: string;
|
pid: string;
|
||||||
}
|
}
|
||||||
@@ -127,12 +203,12 @@ export const deleteGroup = async (req: Request<DeleteGroupQueryParams>, res: Res
|
|||||||
const { pid } = req.params;
|
const { pid } = req.params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
prisma.group.delete({ where: { pid } });
|
await prisma.group.delete({ where: { pid } });
|
||||||
|
|
||||||
return res.status(204).end();
|
return res.status(204).end();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") {
|
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;
|
throw e;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Request, response, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import isSvg from "is-svg";
|
import isSvg from "is-svg";
|
||||||
import { fromBuffer as fileTypeFromBuffer } from "file-type";
|
import { fromBuffer as fileTypeFromBuffer } from "file-type";
|
||||||
@@ -8,10 +8,9 @@ import prisma from "../lib/prisma";
|
|||||||
import NotFoundError from "../Middleware/error/NotFoundError";
|
import NotFoundError from "../Middleware/error/NotFoundError";
|
||||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
|
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
|
||||||
import { generateInvalidBodyError, DataType } from "./common";
|
import { generateInvalidBodyError, DataType } from "./common";
|
||||||
import { type } from "os";
|
|
||||||
import { unlink } from "fs/promises";
|
import { unlink } from "fs/promises";
|
||||||
import ForwardableError from "../Middleware/error/ForwardableError";
|
import ForwardableError from "../Middleware/error/ForwardableError";
|
||||||
import SchemaError from "../Middleware/error/SchemaError";
|
import { table } from "console";
|
||||||
|
|
||||||
require("express-async-errors");
|
require("express-async-errors");
|
||||||
|
|
||||||
@@ -99,7 +98,6 @@ export const uploadImage = async (req: Request, res: Response) => {
|
|||||||
const fileName = file.md5 + (fileIsSvg ? ".svg" : "." + fileType?.ext);
|
const fileName = file.md5 + (fileIsSvg ? ".svg" : "." + fileType?.ext);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
//generate record
|
|
||||||
const media = await prisma.media.create({
|
const media = await prisma.media.create({
|
||||||
data: {
|
data: {
|
||||||
pid: fileName,
|
pid: fileName,
|
||||||
@@ -189,3 +187,75 @@ export const deleteMedia = async (req: Request, res: Response) => {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updatedRec = await getPrismaUpdateFKT(tableToUpdate[2])({
|
||||||
|
where: { pid },
|
||||||
|
data: {
|
||||||
|
visual: { connect: { pid: mediaPid } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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") {
|
||||||
|
throw new NotFoundError(tableToUpdate[2], pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ import {
|
|||||||
} from "./common";
|
} from "./common";
|
||||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
|
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
|
import NotFoundError from "../Middleware/error/NotFoundError";
|
||||||
|
|
||||||
|
require("express-async-errors");
|
||||||
|
|
||||||
function validateOranisationName(name: string) {
|
function validateOranisationName(name: string) {
|
||||||
return name.length > 0;
|
return name.length > 0;
|
||||||
@@ -25,7 +28,7 @@ const detailedOrganisation = {
|
|||||||
pid: true,
|
pid: true,
|
||||||
name: true,
|
name: true,
|
||||||
date: true,
|
date: true,
|
||||||
description: true,
|
briefDescription: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
@@ -187,16 +190,12 @@ export const updateOrganisation = async (
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof PrismaClientKnownRequestError) {
|
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") {
|
||||||
if (e.code === "P2025") {
|
throw new NotFoundError("organisation", pid);
|
||||||
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"));
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(500).json(genericError);
|
throw e;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
interface DeleteOrganisationQueryParams {
|
interface DeleteOrganisationQueryParams {
|
||||||
@@ -216,7 +215,7 @@ export const deleteOrganisation = async (req: Request<DeleteOrganisationQueryPar
|
|||||||
const { pid } = req.params;
|
const { pid } = req.params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
prisma.organisation.delete({ where: { pid } });
|
await prisma.organisation.delete({ where: { pid } });
|
||||||
|
|
||||||
res.status(204).end();
|
res.status(204).end();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
import prisma from "../lib/prisma";
|
||||||
|
import { string, z } from "zod";
|
||||||
|
import { Request, Response } from "express";
|
||||||
|
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, 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(),
|
||||||
|
groupPid: z.string().min(1).uuid(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const returnedParticipant = {
|
||||||
|
pid: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
relevance: true,
|
||||||
|
team: {
|
||||||
|
select: {
|
||||||
|
pid: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
group: {
|
||||||
|
select: {
|
||||||
|
pid: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// at: POST api/participants/
|
||||||
|
export const createParticipant = async (req: Request<{ teamPid: string }>, res: Response) => {
|
||||||
|
const { teamPid } = req.params;
|
||||||
|
|
||||||
|
const result = InitialParticipant.safeParse(req.body);
|
||||||
|
|
||||||
|
if (result.success === false) {
|
||||||
|
return res.status(400).json(
|
||||||
|
generateInvalidBodyError(
|
||||||
|
{
|
||||||
|
firstname: DataType.STRING,
|
||||||
|
lastName: DataType.STRING,
|
||||||
|
groupPid: DataType.UUID,
|
||||||
|
},
|
||||||
|
result.error
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const body = result.data;
|
||||||
|
|
||||||
|
if (req.teamleader?.isAuthenticated) {
|
||||||
|
await requireLeaderOfTeam(req.teamleader, teamPid);
|
||||||
|
} else if (req.auth?.permission_level == "STANDARD") {
|
||||||
|
requireResponsibleForGroups(req.auth, body.groupPid);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const discipline = await prisma.team.findUnique({
|
||||||
|
where: { pid: teamPid },
|
||||||
|
select: { discipline: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const maxteamsize = discipline?.discipline.maxTeamSize;
|
||||||
|
|
||||||
|
const userCount = await prisma.participant.count({
|
||||||
|
where: { team: { pid: 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,
|
||||||
|
lastName: body.lastName,
|
||||||
|
relevance: "MEMBER",
|
||||||
|
group: { connect: { pid: body.groupPid } },
|
||||||
|
team: { connect: { pid: teamPid } },
|
||||||
|
},
|
||||||
|
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 '${teamPid}, or group with ID ${body.groupPid}'`));
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// at: PATCH api/participants/:pid/
|
||||||
|
export const updateParticipant = async (req: Request<{ pid: string }>, res: Response) => {
|
||||||
|
const { pid } = req.params;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (result.success === false) {
|
||||||
|
return res.status(400).json(
|
||||||
|
generateInvalidBodyError(
|
||||||
|
{
|
||||||
|
firstname: DataType.STRING,
|
||||||
|
lastName: DataType.STRING,
|
||||||
|
groupPid: DataType.UUID,
|
||||||
|
},
|
||||||
|
result.error
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = result.data;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const participant = await prisma.participant.update({
|
||||||
|
where: { pid },
|
||||||
|
data: {
|
||||||
|
firstName: body.firstName,
|
||||||
|
lastName: body.lastName,
|
||||||
|
...(body.groupPid ? { group: { connect: { pid: body.groupPid } } } : {}),
|
||||||
|
},
|
||||||
|
select: returnedParticipant,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
type: "success",
|
||||||
|
payload: { participant },
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") {
|
||||||
|
return res
|
||||||
|
.status(404)
|
||||||
|
.json(generateError(`Could not find participant '${pid}, or link to group with ID ${body.groupPid}.'`));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// at: DELETE api/participants/:pid/
|
||||||
|
export const deleteParticipant = async (req: Request<{ pid: string }>, res: Response) => {
|
||||||
|
const { pid } = req.params;
|
||||||
|
|
||||||
|
if (req.teamleader?.isAuthenticated) {
|
||||||
|
await requireResponsibleForParticipant(req.teamleader, pid);
|
||||||
|
} else if (req.auth?.permission_level == "STANDARD") {
|
||||||
|
requireResponsibleForGroups(req.auth, await getGroupByParticipantPid(pid));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await prisma.participant.delete({ where: { pid } });
|
||||||
|
|
||||||
|
return res.status(204).end();
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") {
|
||||||
|
throw new NotFoundError("participant", pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getGroupByParticipantPid(partPid: string) {
|
||||||
|
const parti = (await prisma.participant.findUnique({ where: { pid: partPid }, select: { group: true } }))?.group.pid;
|
||||||
|
|
||||||
|
if (!parti) {
|
||||||
|
throw new NotFoundError("participant", partPid);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parti;
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { Prisma } 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 { DataType, generateInvalidBodyError } from "./common";
|
||||||
|
import { getGroupByParticipantPid } from "./participant.controller";
|
||||||
|
import { getGroupsByTeamPid } from "./team.controller";
|
||||||
|
|
||||||
|
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
|
||||||
|
* @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 })), // TODO: Use default score from schema?
|
||||||
|
});
|
||||||
|
|
||||||
|
return roles.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRolesForTeam(req: Request<{ pid: string }>, res: Response) {
|
||||||
|
const pid = req.params.pid;
|
||||||
|
|
||||||
|
if (req.teamleader?.isAuthenticated) {
|
||||||
|
await requireLeaderOfTeam(req.teamleader, pid);
|
||||||
|
} else {
|
||||||
|
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 } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
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 { pid } = req.params;
|
||||||
|
|
||||||
|
const zBody = AssignParticipantToRoleBody.safeParse(req.body);
|
||||||
|
|
||||||
|
if (zBody.success === false) {
|
||||||
|
return res.status(400).json(generateInvalidBodyError({ participantPid: DataType.UUID }, zBody.error));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { participantPid } = zBody.data;
|
||||||
|
|
||||||
|
if (req.teamleader?.isAuthenticated) {
|
||||||
|
requireResponsibleForParticipant(req.teamleader, participantPid);
|
||||||
|
} else {
|
||||||
|
requireResponsibleForGroups(req.auth, await getGroupByParticipantPid(participantPid));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const schema = await prisma.role.update({
|
||||||
|
where: { pid },
|
||||||
|
data: { participant: { connect: { pid: participantPid } } },
|
||||||
|
select: detailedRole,
|
||||||
|
});
|
||||||
|
|
||||||
|
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 } : {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,31 @@
|
|||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
|
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
import prisma from "../lib/prisma";
|
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 NotFoundError from "../Middleware/error/NotFoundError";
|
||||||
import SchemaError from "../Middleware/error/SchemaError";
|
import SchemaError from "../Middleware/error/SchemaError";
|
||||||
import {
|
import {
|
||||||
createInsufficientPermissionsError,
|
createInsufficientPermissionsError,
|
||||||
DataType,
|
DataType,
|
||||||
|
generateError,
|
||||||
generateInvalidBodyError,
|
generateInvalidBodyError,
|
||||||
NAME_ERROR,
|
NAME_ERROR,
|
||||||
validateName,
|
validateName,
|
||||||
} from "./common";
|
} from "./common";
|
||||||
|
|
||||||
|
require("express-async-errors");
|
||||||
|
|
||||||
|
const RoleSchemaBody = z.object({
|
||||||
|
name: z.string().min(1),
|
||||||
|
schema: z.string(PointSchema).or(z.string(DurationSchema)),
|
||||||
|
});
|
||||||
|
|
||||||
|
const UpdateBody = RoleSchemaBody.partial();
|
||||||
|
|
||||||
const roleSchema = {
|
const roleSchema = {
|
||||||
|
pid: true,
|
||||||
name: true,
|
name: true,
|
||||||
schema: true,
|
schema: true,
|
||||||
discipline: { select: { pid: true, name: true } },
|
discipline: { select: { pid: true, name: true } },
|
||||||
@@ -124,58 +136,48 @@ export const createRoleSchema = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
interface visualParams {
|
export const UpdateRoleSchema = async (req: Request<{ pid: string }>, res: Response) => {
|
||||||
schemaPid: string;
|
if (req.auth?.permission_level !== "ELEVATED") {
|
||||||
}
|
return res.status(403).json(createInsufficientPermissionsError());
|
||||||
|
|
||||||
interface visualBody {
|
|
||||||
mediaPid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const addVisual = async (req: Request<visualParams, {}, visualBody>, res: Response) => {
|
|
||||||
if (req.auth?.permission_level != "ELEVATED") {
|
|
||||||
res.status(403).json(createInsufficientPermissionsError());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { schemaPid } = req.params;
|
const { pid } = req.params;
|
||||||
|
|
||||||
const schema = await prisma.roleSchema.update({
|
const result = UpdateBody.safeParse(req.body);
|
||||||
where: { pid: schemaPid },
|
|
||||||
data: {
|
|
||||||
visual: { connect: { pid: req.body.mediaPid } },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!schema) {
|
if (result.success === false) {
|
||||||
throw new NotFoundError("role_schema", schemaPid);
|
return res.status(400).json(
|
||||||
|
generateInvalidBodyError(
|
||||||
|
{
|
||||||
|
name: DataType.STRING,
|
||||||
|
schema: DataType.RESULT_SCHEMA,
|
||||||
|
},
|
||||||
|
result.error
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).json({
|
const body = result.data;
|
||||||
type: "success",
|
|
||||||
payload: {},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteVisual = async (req: Request<visualParams & { pid: string }>, res: Response) => {
|
|
||||||
if (req.auth?.permission_level != "ELEVATED") {
|
|
||||||
res.status(403).json(createInsufficientPermissionsError());
|
|
||||||
}
|
|
||||||
|
|
||||||
const { schemaPid, pid } = req.params;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await prisma.roleSchema.update({
|
const schema = await prisma.roleSchema.update({
|
||||||
where: {
|
where: { pid },
|
||||||
pid: schemaPid,
|
|
||||||
},
|
|
||||||
data: {
|
data: {
|
||||||
visual: { disconnect: { pid } },
|
name: body.name,
|
||||||
|
schema: body.schema,
|
||||||
|
},
|
||||||
|
select: roleSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
type: "success",
|
||||||
|
payload: {
|
||||||
|
schema,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return res.status(204).end();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") {
|
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") {
|
||||||
throw new NotFoundError("role_schema", schemaPid);
|
throw new NotFoundError("discipline", pid);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import prisma from "../lib/prisma";
|
||||||
|
import { DataType, generateInvalidBodyError } from "./common";
|
||||||
|
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";
|
||||||
|
import AuthError from "../Middleware/error/AuthError";
|
||||||
|
import { runInNewContext } from "vm";
|
||||||
|
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
|
||||||
|
|
||||||
|
require("express-async-errors");
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
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 } });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTeam = async (req: Request<{ pid: string }>, res: Response) => {
|
||||||
|
const { pid } = req.params;
|
||||||
|
|
||||||
|
if (req.teamleader?.isAuthenticated) {
|
||||||
|
await requireLeaderOfTeam(req.teamleader, pid);
|
||||||
|
} else if (req.auth?.permission_level == "STANDARD") {
|
||||||
|
requireResponsibleForGroups(req.auth, await getGroupsByTeamPid(pid));
|
||||||
|
}
|
||||||
|
|
||||||
|
const team = await prisma.team.findUnique({
|
||||||
|
where: { pid },
|
||||||
|
select: basicTeam,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!team) {
|
||||||
|
throw new NotFoundError("team", pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({ type: "success", payload: { team } });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateTeam = async (req: Request, res: Response) => {
|
||||||
|
const { pid } = req.params;
|
||||||
|
|
||||||
|
if (req.teamleader?.isAuthenticated) {
|
||||||
|
await requireLeaderOfTeam(req.teamleader, 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);
|
||||||
|
|
||||||
|
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 {
|
||||||
|
const team = await prisma.team.update({
|
||||||
|
where: {
|
||||||
|
pid: pid,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
name: body.teamName,
|
||||||
|
discipline: { connect: { pid: body.disciplineId } },
|
||||||
|
leaderEmail: body.leaderEmail,
|
||||||
|
},
|
||||||
|
select: basicTeam,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({ type: "success", payload: { team } });
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") {
|
||||||
|
throw new NotFoundError("team", pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteTeam = async (req: Request, res: Response) => {
|
||||||
|
const { pid } = req.params;
|
||||||
|
|
||||||
|
if (req.teamleader?.isAuthenticated) {
|
||||||
|
await requireLeaderOfTeam(req.teamleader, pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.auth?.permission_level == "STANDARD") {
|
||||||
|
throw new AuthError("STANDARD Admins are not allowed to delete Teams!");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await prisma.team.delete({ where: { pid } });
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -3,27 +3,173 @@ import prisma from "../lib/prisma";
|
|||||||
import { mailClient } from "../lib/redis";
|
import { mailClient } from "../lib/redis";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { verificationMail } from "../lib/mail";
|
import { verificationMail } from "../lib/mail";
|
||||||
|
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";
|
||||||
|
|
||||||
export const register = async (req: Request, res: Response) => {
|
require("express-async-errors");
|
||||||
//TODO: Implemnt user endpoint and use following code to send verification mail
|
|
||||||
|
|
||||||
const user = {
|
export const TeamBody = z.object({
|
||||||
//Supposed to come from database
|
teamName: z.string().min(1),
|
||||||
id: "10",
|
leaderEmail: z.string().email(),
|
||||||
email: "[email protected]",
|
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 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
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = result.data;
|
||||||
|
|
||||||
|
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, name: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await createRolesForTeam(team.pid);
|
||||||
|
|
||||||
|
const usid = nanoid();
|
||||||
|
|
||||||
|
(await mailClient).set(usid, team.pid);
|
||||||
|
|
||||||
|
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({
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const requestToken = async (req: Request, res: Response) => {
|
||||||
|
const data = z.object({ teamId: z.string().min(1) }).safeParse(req);
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return res.status(404).json(generateError("Team does not exist!"));
|
||||||
|
}
|
||||||
|
|
||||||
const usid = nanoid();
|
const usid = nanoid();
|
||||||
|
|
||||||
(await mailClient).set(usid, user.id);
|
(await mailClient).set(usid, team.pid);
|
||||||
|
|
||||||
verificationMail(user.email, "eventname", usid);
|
verificationMail(team.leaderEmail, team.discipline.name, usid);
|
||||||
|
|
||||||
//Send status code
|
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!" } });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const verifyEmail = async (req: Request, res: Response) => {
|
export const verifyEmail = async (req: Request, res: Response) => {
|
||||||
const { code } = req.body || {};
|
const { code } = req.params || {};
|
||||||
|
|
||||||
if (!(typeof code === "string")) {
|
if (!(typeof code === "string")) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
@@ -45,13 +191,27 @@ export const verifyEmail = async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
prisma.participant.update({
|
try {
|
||||||
where: {
|
const team = await prisma.team.update({
|
||||||
id: parseInt(acc),
|
where: {
|
||||||
},
|
pid: acc,
|
||||||
data: {
|
},
|
||||||
verified: true,
|
data: {
|
||||||
},
|
verified: true,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
mailClient.set(code, "");
|
||||||
|
|
||||||
|
const token = generateTeamleaderJWT(team);
|
||||||
|
|
||||||
|
res.status(200).json({ type: "succes", payload: { token } });
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") {
|
||||||
|
throw new NotFoundError("discipline", acc);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
*/
|
*/
|
||||||
+170
-63
@@ -1,45 +1,99 @@
|
|||||||
/// <reference path="../../custom.d.ts" />
|
/// <reference path="../../custom.d.ts" />
|
||||||
|
|
||||||
import { NextFunction, Request, Response } from "express";
|
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 { authClient } from "../../lib/redis";
|
||||||
import jwt, { JsonWebTokenError, JwtPayload } from "jsonwebtoken";
|
import jwt, { JsonWebTokenError, JwtPayload } from "jsonwebtoken";
|
||||||
import prisma from "../../lib/prisma";
|
import prisma from "../../lib/prisma";
|
||||||
|
import AuthError from "../error/AuthError";
|
||||||
|
import { TeamleaderJWTPayload, _requireTeamleaderAuthentication } from "./teamleaderAuth";
|
||||||
|
|
||||||
const JWT_SECRET = process.env.JWT_SECRET || "secret";
|
require("express-async-errors");
|
||||||
|
|
||||||
const verifyAuthorizationFormat = (authorization: string) => /^Bearer .+$/.test(authorization);
|
const JWT_SECRET = process.env.JWT_SECRET;
|
||||||
|
|
||||||
const getBearerToken = (authorization: string) => authorization.slice(7);
|
export const verifyAuthorizationFormat = (authorization: string) => /^Bearer .+$/.test(authorization);
|
||||||
|
|
||||||
export const requireAuthentication = async (req: Request, res: Response, next: NextFunction) => {
|
export const getBearerToken = (authorization: string) => authorization.slice(7);
|
||||||
const { authorization } = req.headers;
|
|
||||||
|
|
||||||
if (!authorization) {
|
const _requireAdminAuthentication =
|
||||||
return res.status(403).send({
|
(config: { optional?: Boolean; controlled?: Boolean } = { optional: false, controlled: false }) =>
|
||||||
type: "error",
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
payload: {
|
if (!JWT_SECRET) {
|
||||||
message: "The requeset did not include the Authorization header",
|
throw new Error("JWT_SECRET not set");
|
||||||
},
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!verifyAuthorizationFormat(authorization)) {
|
const { authorization } = req.headers;
|
||||||
return res.status(400).send({
|
|
||||||
type: "error",
|
|
||||||
payload: {
|
|
||||||
message: "Malformed Authorization header",
|
|
||||||
format: "Bearer <token>",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let token_payload_: string | JwtPayload;
|
if (!authorization) {
|
||||||
|
if (config.optional) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
return res.status(403).send({
|
||||||
token_payload_ = jwt.verify(getBearerToken(authorization), JWT_SECRET);
|
type: "error",
|
||||||
} catch (e) {
|
payload: {
|
||||||
if (e instanceof JsonWebTokenError) {
|
message: "The requeset did not include the Authorization header",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!verifyAuthorizationFormat(authorization)) {
|
||||||
|
return res.status(400).send({
|
||||||
|
type: "error",
|
||||||
|
payload: {
|
||||||
|
message: "Malformed Authorization header",
|
||||||
|
format: "Bearer <token>",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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({
|
return res.status(403).json({
|
||||||
type: "error",
|
type: "error",
|
||||||
payload: {
|
payload: {
|
||||||
@@ -48,43 +102,96 @@ 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;
|
if (!config.controlled) {
|
||||||
|
next();
|
||||||
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 true;
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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 requireResponsibleForGroups(auth: AuthJWTPayload | undefined, groupPids: string[] | string) {
|
||||||
|
if (auth?.permission_level === "ELEVATED") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import { Team } 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";
|
||||||
|
import { checkTeamExistence } from "../../Controllers/team.controller";
|
||||||
|
|
||||||
|
export interface TeamleaderJWTPayload {
|
||||||
|
team: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET;
|
||||||
|
|
||||||
|
export function generateTeamleaderJWT(teamleader: Team) {
|
||||||
|
if (!JWT_SECRET) {
|
||||||
|
throw new Error("JWT_SECRET not set");
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: TeamleaderJWTPayload = {
|
||||||
|
team: teamleader.pid,
|
||||||
|
};
|
||||||
|
|
||||||
|
return jwt.sign(payload, JWT_SECRET, { expiresIn: "4 days" });
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <token>",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ export function notFoundHandler(req: Request, res: Response) {
|
|||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
type: "error",
|
type: "error",
|
||||||
payload: {
|
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: [
|
_links: [
|
||||||
{
|
{
|
||||||
rel: "root",
|
rel: "root",
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import eventRouter from "./event.routes";
|
import eventRouter from "./event.routes";
|
||||||
import {
|
import {
|
||||||
addVisual,
|
|
||||||
createDiscipline,
|
createDiscipline,
|
||||||
deleteDiscipline,
|
deleteDiscipline,
|
||||||
deleteVisual,
|
|
||||||
getAllDisciplines,
|
getAllDisciplines,
|
||||||
getDiscipline,
|
getDiscipline,
|
||||||
|
updateDiscipline,
|
||||||
} from "../Controllers/discipline.controller";
|
} from "../Controllers/discipline.controller";
|
||||||
import { requireAuthentication } from "../Middleware/auth/auth";
|
import { requireAuthentication } from "../Middleware/auth/auth";
|
||||||
|
|
||||||
@@ -16,20 +15,9 @@ router.get("/", getAllDisciplines); // TODO: Optional auth
|
|||||||
|
|
||||||
router.get("/:pid", getDiscipline);
|
router.get("/:pid", getDiscipline);
|
||||||
|
|
||||||
|
router.patch("/:pid", requireAuthentication, updateDiscipline);
|
||||||
router.delete<"/:pid", { pid: string }>("/:pid", requireAuthentication, deleteDiscipline);
|
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);
|
eventRouter.post("/:eventPid/disciplines", requireAuthentication, createDiscipline);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -1,14 +1,6 @@
|
|||||||
import Express from "express";
|
import Express from "express";
|
||||||
import { string } from "zod";
|
import { string } from "zod";
|
||||||
import {
|
import { addEvent, deleteEvent, getAllEvents, getEvent, updateEvent } from "../Controllers/event.controller";
|
||||||
addEvent,
|
|
||||||
addVisual,
|
|
||||||
deleteEvent,
|
|
||||||
deleteVisual,
|
|
||||||
getAllEvents,
|
|
||||||
getEvent,
|
|
||||||
updateEvent,
|
|
||||||
} from "../Controllers/event.controller";
|
|
||||||
import { requireAuthentication } from "../Middleware/auth/auth";
|
import { requireAuthentication } from "../Middleware/auth/auth";
|
||||||
const router = Express.Router();
|
const router = Express.Router();
|
||||||
|
|
||||||
@@ -22,12 +14,4 @@ router.patch<"/:pid/", { pid: string }>("/:pid/", requireAuthentication, updateE
|
|||||||
|
|
||||||
router.delete<"/:pid/", { pid: string }>("/:pid/", requireAuthentication, deleteEvent);
|
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;
|
export default router;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
getAllGroups,
|
getAllGroups,
|
||||||
getAllGroupsWithParam,
|
getAllGroupsWithParam,
|
||||||
getGroup,
|
getGroup,
|
||||||
|
updateGroup,
|
||||||
} from "../Controllers/group.controllers";
|
} from "../Controllers/group.controllers";
|
||||||
import { requireAuthentication } from "../Middleware/auth/auth";
|
import { requireAuthentication } from "../Middleware/auth/auth";
|
||||||
import organisationRouter from "./organisation.routes";
|
import organisationRouter from "./organisation.routes";
|
||||||
@@ -14,6 +15,7 @@ const router = express.Router();
|
|||||||
router.get("/", getAllGroups);
|
router.get("/", getAllGroups);
|
||||||
router.get("/:pid", getGroup);
|
router.get("/:pid", getGroup);
|
||||||
router.delete<"/:pid", { pid: string }>("/:pid", requireAuthentication, deleteGroup);
|
router.delete<"/:pid", { pid: string }>("/:pid", requireAuthentication, deleteGroup);
|
||||||
|
router.patch("/:pid", requireAuthentication, updateGroup);
|
||||||
|
|
||||||
organisationRouter.get("/:organisationPid/groups", getAllGroupsWithParam);
|
organisationRouter.get("/:organisationPid/groups", getAllGroupsWithParam);
|
||||||
organisationRouter.post("/:organisationPid/groups", requireAuthentication, createGroup);
|
organisationRouter.post("/:organisationPid/groups", requireAuthentication, createGroup);
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import fileUpload from "express-fileupload";
|
import fileUpload from "express-fileupload";
|
||||||
import { requireAuthentication } from "../Middleware/auth/auth";
|
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 eventRouter from "./event.routes";
|
||||||
import disciplineRouter from "./discipline.routes";
|
import disciplineRouter from "./discipline.routes";
|
||||||
import roleSchemaRouter from "./role_schema.routes";
|
import roleSchemaRouter from "./role_schema.routes";
|
||||||
@@ -19,4 +26,28 @@ router.get("/:pid/meta", getMediaMeta);
|
|||||||
|
|
||||||
router.delete("/:pid", requireAuthentication, deleteMedia);
|
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;
|
export default router;
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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();
|
||||||
|
|
||||||
|
teamRouter.post<"/:teamPid/participants", { teamPid: string }>(
|
||||||
|
"/:teamPid/participants",
|
||||||
|
requireConfiguredAuthentication({ optional: false, type: { admin: true, teamleader: true } }),
|
||||||
|
createParticipant
|
||||||
|
);
|
||||||
|
|
||||||
|
router.patch<"/:pid/", { pid: string }>(
|
||||||
|
"/:pid/",
|
||||||
|
requireConfiguredAuthentication({ optional: false, type: { admin: true, teamleader: true } }),
|
||||||
|
updateParticipant
|
||||||
|
);
|
||||||
|
|
||||||
|
router.delete<"/:pid/", { pid: string }>(
|
||||||
|
"/:pid/",
|
||||||
|
requireConfiguredAuthentication({ optional: false, type: { admin: true, teamleader: true } }),
|
||||||
|
deleteParticipant
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
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();
|
||||||
|
|
||||||
|
router.put<"/:pid/participant", { pid: string }>(
|
||||||
|
"/:pid/participant",
|
||||||
|
requireConfiguredAuthentication({ optional: false, type: { admin: true, teamleader: true } }),
|
||||||
|
assignParticipantToRole
|
||||||
|
);
|
||||||
|
|
||||||
|
teamRouter.get<"/:pid/roles", { pid: string }>(
|
||||||
|
"/:pid/roles",
|
||||||
|
requireConfiguredAuthentication({ optional: false, type: { admin: true, teamleader: true } }),
|
||||||
|
getRolesForTeam
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import disciplineRouter from "./discipline.routes";
|
import disciplineRouter from "./discipline.routes";
|
||||||
import {
|
import {
|
||||||
addVisual,
|
|
||||||
createRoleSchema,
|
createRoleSchema,
|
||||||
deleteVisual,
|
|
||||||
getAllRoleSchemas,
|
getAllRoleSchemas,
|
||||||
getAllRoleSchemasWithParam,
|
getAllRoleSchemasWithParam,
|
||||||
getRoleSchema,
|
getRoleSchema,
|
||||||
@@ -20,12 +18,4 @@ disciplineRouter.get("/:disciplinePid/role-schemas", getAllRoleSchemasWithParam)
|
|||||||
|
|
||||||
disciplineRouter.post("/:disciplinePid/role-schemas", requireAuthentication, createRoleSchema);
|
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;
|
export default router;
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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<"/:pid/", { pid: string }>(
|
||||||
|
"/:pid/",
|
||||||
|
requireConfiguredAuthentication({ optional: false, type: { admin: true, teamleader: true } }),
|
||||||
|
getTeam
|
||||||
|
);
|
||||||
|
|
||||||
|
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 } }),
|
||||||
|
deleteTeam
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -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;
|
||||||
+13
-4
@@ -13,6 +13,10 @@ import defaultErrorHandler from "./Middleware/error/handler";
|
|||||||
import logger from "./Middleware/error/logger";
|
import logger from "./Middleware/error/logger";
|
||||||
import debugLogger from "./Middleware/debug/logger";
|
import debugLogger from "./Middleware/debug/logger";
|
||||||
import mediaRouter from "./Routes/media.routes";
|
import mediaRouter from "./Routes/media.routes";
|
||||||
|
import userRouter from "./Routes/user_auth.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";
|
import { notFoundHandler, rootHandler } from "./Middleware/error/defaultRoutes";
|
||||||
|
|
||||||
// Set up async error handling
|
// Set up async error handling
|
||||||
@@ -82,16 +86,21 @@ async function main() {
|
|||||||
app.use("/api/role-schemas", roleSchemaRouter);
|
app.use("/api/role-schemas", roleSchemaRouter);
|
||||||
|
|
||||||
app.use("/api/media", mediaRouter);
|
app.use("/api/media", mediaRouter);
|
||||||
|
|
||||||
|
app.use("/api/users", userRouter);
|
||||||
|
|
||||||
|
app.use("/api/teams", teamRouter);
|
||||||
|
|
||||||
|
app.use("/api/roles", roleRouter);
|
||||||
|
|
||||||
|
app.use("/api/participants", participantRouter);
|
||||||
|
|
||||||
app.get("/", rootHandler);
|
app.get("/", rootHandler);
|
||||||
app.get("/api", rootHandler);
|
app.get("/api", rootHandler);
|
||||||
|
|
||||||
// Error handling
|
// Error handling
|
||||||
app.use(defaultErrorHandler); // This has to be the LAST ROUTE
|
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.use(notFoundHandler);
|
||||||
|
|
||||||
app.listen(process.env.PORT, () => {
|
app.listen(process.env.PORT, () => {
|
||||||
|
|||||||
Vendored
+2
@@ -1,7 +1,9 @@
|
|||||||
import { AuthJWTPayload } from "./Controllers/admin_auth.controller";
|
import { AuthJWTPayload } from "./Controllers/admin_auth.controller";
|
||||||
|
import { TeamleaderJWTPayload } from "./Middleware/auth/teamleaderAuth";
|
||||||
|
|
||||||
declare module "express-serve-static-core" {
|
declare module "express-serve-static-core" {
|
||||||
interface Request {
|
interface Request {
|
||||||
auth?: AuthJWTPayload & { isAuthenticated: boolean };
|
auth?: AuthJWTPayload & { isAuthenticated: boolean };
|
||||||
|
teamleader?: TeamleaderJWTPayload & { isAuthenticated: boolean };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-4
@@ -5,14 +5,21 @@ import nodemailer from "nodemailer";
|
|||||||
import SMTPTransport from "nodemailer/lib/smtp-transport";
|
import SMTPTransport from "nodemailer/lib/smtp-transport";
|
||||||
|
|
||||||
import mjml from "./mjml";
|
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 };
|
export let mailAccount = { user: process.env.MAILUSER + "@mail." + process.env.DOMAIN, pass: process.env.MAILPASSWORD };
|
||||||
|
|
||||||
let transporter =
|
let transporter =
|
||||||
process.env.DEV == "true" || process.env.DOMAIN == undefined
|
process.env.NODE_ENV == "development" || process.env.DOMAIN == undefined
|
||||||
? (async () => {
|
? (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") {
|
if (process.env.NODE_ENV != "test") {
|
||||||
console.log(mailAccount);
|
console.log(mailAccount);
|
||||||
}
|
}
|
||||||
@@ -43,6 +50,8 @@ let transporter =
|
|||||||
);
|
);
|
||||||
|
|
||||||
const sendMail = async (from: string, to: string, subject: string, text?: string, html?: string) => {
|
const sendMail = async (from: string, to: string, subject: string, text?: string, html?: string) => {
|
||||||
|
logger.debug(`Sent email to: ${to}`);
|
||||||
|
|
||||||
return await (
|
return await (
|
||||||
await transporter
|
await transporter
|
||||||
).sendMail({
|
).sendMail({
|
||||||
@@ -57,7 +66,8 @@ const sendMail = async (from: string, to: string, subject: string, text?: string
|
|||||||
export const verificationMail = async (to: string, eventName: string, verificationLink: string) => {
|
export const verificationMail = async (to: string, eventName: string, verificationLink: string) => {
|
||||||
const raw = mjml.getTemplate("emailVerification");
|
const raw = mjml.getTemplate("emailVerification");
|
||||||
|
|
||||||
//TODO: Replace other handlebars with final values
|
// TODO: the process.env.DOMAIN is undefined in Development mode !!
|
||||||
|
verificationLink = (process.env.FRONTEND_MAIL_ENDPOINT ?? "localhost:3000/api/users/verify/") + verificationLink;
|
||||||
const message = Handlebars.compile(raw);
|
const message = Handlebars.compile(raw);
|
||||||
|
|
||||||
const data = { eventName, verificationLink };
|
const data = { eventName, verificationLink };
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const SchemaVersion = z.enum(["1.0"]);
|
|||||||
|
|
||||||
const TimeUnit = z.enum(["days", "hours", "minutes", "seconds", "milliseconds"]);
|
const TimeUnit = z.enum(["days", "hours", "minutes", "seconds", "milliseconds"]);
|
||||||
|
|
||||||
const DurationSchema = z
|
export const DurationSchema = z
|
||||||
.object({
|
.object({
|
||||||
type: z.literal("duration"),
|
type: z.literal("duration"),
|
||||||
min: z.number().int({ message: "min must be an integer (relative to smallestUnit)" }),
|
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" });
|
.refine(({ min, max }) => min < max, { message: "min must be smaller than max" });
|
||||||
|
|
||||||
const PointSchema = z
|
export const PointSchema = z
|
||||||
.object({
|
.object({
|
||||||
type: z.literal("points"),
|
type: z.literal("points"),
|
||||||
min: z.number(),
|
min: z.number(),
|
||||||
|
|||||||
Reference in New Issue
Block a user