mirror of
https://github.com/detleph/server.git
synced 2026-09-04 08:36:06 +02:00
Merge branch 'feature-endpoints' into feature-roles
This commit is contained in:
@@ -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[]
|
||||||
@@ -125,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[]
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Prisma, Organisation, Admin, AdminLevel, Team } from "@prisma/client";
|
import { Prisma, Organisation, Admin, AdminLevel, Team } 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 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,
|
||||||
@@ -20,6 +20,8 @@ const InitialDisciplineBody = z.object({
|
|||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
minTeamSize: z.number(),
|
minTeamSize: z.number(),
|
||||||
maxTeamSize: z.number(),
|
maxTeamSize: z.number(),
|
||||||
|
briefDescription: z.string().min(1),
|
||||||
|
fullDescription: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const disciplineRefiner = [
|
const disciplineRefiner = [
|
||||||
@@ -27,7 +29,7 @@ const disciplineRefiner = [
|
|||||||
{ message: "The minTeamSize must be smaller or equal to the maxTeamSize" },
|
{ message: "The minTeamSize must be smaller or equal to the maxTeamSize" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const DisciplineBody = InitialDisciplineBody.refine(...disciplineRefiner);
|
const DisciplineBody = InitialDisciplineBody.partial({ fullDescription: true }).refine(...disciplineRefiner);
|
||||||
const updateDisciplineBody = InitialDisciplineBody.partial().refine(...disciplineRefiner);
|
const updateDisciplineBody = InitialDisciplineBody.partial().refine(...disciplineRefiner);
|
||||||
|
|
||||||
const basicDiscipline = {
|
const basicDiscipline = {
|
||||||
@@ -36,6 +38,8 @@ const basicDiscipline = {
|
|||||||
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;
|
||||||
@@ -132,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)
|
||||||
@@ -150,17 +156,19 @@ export const createDiscipline = async (req: Request<{ eventPid: string }, {}, Cr
|
|||||||
name: DataType.STRING,
|
name: DataType.STRING,
|
||||||
minTeamSize: DataType.NUMBER,
|
minTeamSize: DataType.NUMBER,
|
||||||
maxTeamSize: DataType.NUMBER,
|
maxTeamSize: DataType.NUMBER,
|
||||||
|
briefDescription: DataType.STRING,
|
||||||
|
["fullDescription?"]: DataType.STRING,
|
||||||
},
|
},
|
||||||
result.error
|
result.error
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name, minTeamSize, maxTeamSize } = result.data;
|
const { name, minTeamSize, maxTeamSize, briefDescription } = result.data;
|
||||||
|
|
||||||
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: { name, minTeamSize, maxTeamSize, briefDescription, event: { connect: { pid: req.params.eventPid } } },
|
||||||
select: basicDiscipline,
|
select: basicDiscipline,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -188,6 +196,8 @@ export const updateDiscipline = async (req: Request<{ pid: string }>, res: Respo
|
|||||||
name: DataType.STRING,
|
name: DataType.STRING,
|
||||||
minTeamSize: DataType.NUMBER,
|
minTeamSize: DataType.NUMBER,
|
||||||
maxTeamSize: DataType.NUMBER,
|
maxTeamSize: DataType.NUMBER,
|
||||||
|
briefDescription: DataType.STRING,
|
||||||
|
["fullDescription?"]: DataType.STRING,
|
||||||
},
|
},
|
||||||
result.error
|
result.error
|
||||||
)
|
)
|
||||||
@@ -204,6 +214,8 @@ export const updateDiscipline = async (req: Request<{ pid: string }>, res: Respo
|
|||||||
name: body.name,
|
name: body.name,
|
||||||
minTeamSize: body.minTeamSize,
|
minTeamSize: body.minTeamSize,
|
||||||
maxTeamSize: body.maxTeamSize,
|
maxTeamSize: body.maxTeamSize,
|
||||||
|
briefDescription: body.briefDescription,
|
||||||
|
fullDescription: body.fullDescription,
|
||||||
},
|
},
|
||||||
select: basicDiscipline,
|
select: basicDiscipline,
|
||||||
});
|
});
|
||||||
@@ -241,63 +253,3 @@ export const deleteDiscipline = async (req: Request<{ pid: string }>, res: Respo
|
|||||||
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 } },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: This does not work and should be updated in all addVisual-type code segments
|
|
||||||
// Reason: update throw a PrismaClientKnownRequestError with code P2025 if the record to update could not be found
|
|
||||||
if (!discipline) {
|
|
||||||
throw new NotFoundError("discipline", disciplinePid);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).json({
|
|
||||||
type: "success",
|
|
||||||
payload: {},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteVisual = async (req: Request<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); // Refer: Last todo; This is a correct example
|
|
||||||
}
|
|
||||||
|
|
||||||
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,24 +120,23 @@ 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 event = await prisma.event.create({
|
const event = await prisma.event.create({
|
||||||
data: {
|
data: {
|
||||||
name: req.body.name,
|
name: req.body.name,
|
||||||
@@ -121,14 +144,7 @@ export const addEvent = async (req: Request, res: Response) => {
|
|||||||
briefDescription: req.body.briefDescription,
|
briefDescription: req.body.briefDescription,
|
||||||
fullDescription: req.body.fullDescription,
|
fullDescription: req.body.fullDescription,
|
||||||
},
|
},
|
||||||
select: {
|
select: basicEvent,
|
||||||
name: true,
|
|
||||||
date: true,
|
|
||||||
pid: true,
|
|
||||||
id: false,
|
|
||||||
briefDescription: true,
|
|
||||||
fullDescription: true,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(201).json({
|
res.status(201).json({
|
||||||
@@ -139,15 +155,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());
|
||||||
@@ -191,10 +198,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: {
|
||||||
@@ -202,24 +205,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("discipline", 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;
|
||||||
@@ -244,71 +231,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 event with the ID ${pid} could not be found`));
|
throw new NotFoundError("discipline", 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;
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ 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) {
|
||||||
|
|||||||
@@ -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,8 @@ 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";
|
|
||||||
|
|
||||||
require("express-async-errors");
|
require("express-async-errors");
|
||||||
|
|
||||||
@@ -99,7 +97,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 +186,71 @@ 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,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedRec = await getPrismaUpdateFKT(tableToUpdate[2])({
|
||||||
|
where: { pid },
|
||||||
|
data: {
|
||||||
|
visual: { connect: { pid: mediaPid } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!updatedRec) {
|
||||||
|
throw new NotFoundError(tableToUpdate[2], pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
type: "success",
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const unlinkMedia = async (req: Request<{ pid: string; mediaPid: string }>, res: Response) => {
|
||||||
|
if (req.auth?.permission_level != "ELEVATED") {
|
||||||
|
res.status(403).json(createInsufficientPermissionsError());
|
||||||
|
}
|
||||||
|
|
||||||
|
const { pid, mediaPid } = req.params;
|
||||||
|
const tableToUpdate = req.originalUrl.split("/");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await getPrismaUpdateFKT(tableToUpdate[2])({
|
||||||
|
where: { pid },
|
||||||
|
data: { visual: { disconnect: { pid: mediaPid } } },
|
||||||
|
});
|
||||||
|
return res.status(204).end();
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") {
|
||||||
|
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,7 @@ 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";
|
||||||
|
|
||||||
function validateOranisationName(name: string) {
|
function validateOranisationName(name: string) {
|
||||||
return name.length > 0;
|
return name.length > 0;
|
||||||
@@ -187,16 +188,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("discipline", 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 +213,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) {
|
||||||
|
|||||||
@@ -11,20 +11,21 @@ import {
|
|||||||
import { Job, Prisma } from "@prisma/client";
|
import { Job, Prisma } from "@prisma/client";
|
||||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
|
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
|
||||||
import NotFoundError from "../Middleware/error/NotFoundError";
|
import NotFoundError from "../Middleware/error/NotFoundError";
|
||||||
import { requireResponsibleForGroup } from "../Middleware/auth/auth";
|
import { requireConfiguredAuthentication, requireResponsibleForGroup } from "../Middleware/auth/auth";
|
||||||
|
|
||||||
//TODO: add TeamleaderAuthentification
|
//TODO: add TeamleaderAuthentification
|
||||||
|
|
||||||
// REVIEW: All this code should be able to be executed by the teamleader of the team the participant is in AND
|
// REVIEW: All this code should be able to be executed by the teamleader of the team the participant is in AND
|
||||||
// an admin the group of whom overlaps with the team AND an elevated admin
|
// an admin the group of whom overlaps with the team AND an elevated admin
|
||||||
|
|
||||||
const ParticipantBody = z.object({
|
const InitialParticipant = z.object({
|
||||||
firstName: z.string(),
|
firstName: z.string(),
|
||||||
lastName: z.string(),
|
lastName: z.string(),
|
||||||
groupId: z.string().uuid(),
|
groupPid: z.string().uuid(),
|
||||||
//job: z.enum(["TEAMLEADER", "MEMBER"]),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ParticipantBody = InitialParticipant.extend({ teamPid: z.string().uuid() });
|
||||||
|
|
||||||
const returnedParticipant = {
|
const returnedParticipant = {
|
||||||
pid: true,
|
pid: true,
|
||||||
firstName: true,
|
firstName: true,
|
||||||
@@ -44,8 +45,8 @@ const returnedParticipant = {
|
|||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// REVIEW: Location of this endpoints (/groups, /teams, /participants, ...?)
|
// at: POST api/teams/:teamPid/participant/
|
||||||
export const createParticipant = async (req: Request<{ pid: string }>, res: Response) => {
|
export const createParticipant = async (req: Request, res: Response) => {
|
||||||
const result = ParticipantBody.safeParse(req.body);
|
const result = ParticipantBody.safeParse(req.body);
|
||||||
|
|
||||||
if (result.success === false) {
|
if (result.success === false) {
|
||||||
@@ -54,24 +55,14 @@ export const createParticipant = async (req: Request<{ pid: string }>, res: Resp
|
|||||||
{
|
{
|
||||||
firstname: DataType.STRING,
|
firstname: DataType.STRING,
|
||||||
lastName: DataType.STRING,
|
lastName: DataType.STRING,
|
||||||
groupId: DataType.UUID,
|
groupPid: DataType.UUID,
|
||||||
|
teamPid: DataType.UUID,
|
||||||
},
|
},
|
||||||
result.error
|
result.error
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const body = result.data;
|
const body = result.data;
|
||||||
const { pid } = req.params;
|
|
||||||
|
|
||||||
/*
|
|
||||||
if (!req.auth?.isAuthenticated || req.teamleader?.team != pid) {
|
|
||||||
return res.status(500).json(AUTH_ERROR);
|
|
||||||
}
|
|
||||||
if (req.teamleader?.team != pid) {
|
|
||||||
return res.status(500).json(AUTH_ERROR);
|
|
||||||
}
|
|
||||||
requireResponsibleForGroup(req.auth, req.body.groupId);
|
|
||||||
*/
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const participant = await prisma.participant.create({
|
const participant = await prisma.participant.create({
|
||||||
@@ -79,8 +70,8 @@ export const createParticipant = async (req: Request<{ pid: string }>, res: Resp
|
|||||||
firstName: body.firstName,
|
firstName: body.firstName,
|
||||||
lastName: body.lastName,
|
lastName: body.lastName,
|
||||||
relevance: "MEMBER",
|
relevance: "MEMBER",
|
||||||
group: { connect: { pid: body.groupId } },
|
group: { connect: { pid: body.groupPid } },
|
||||||
team: { connect: { pid } },
|
team: { connect: { pid: body.teamPid } },
|
||||||
},
|
},
|
||||||
select: returnedParticipant,
|
select: returnedParticipant,
|
||||||
});
|
});
|
||||||
@@ -93,16 +84,15 @@ export const createParticipant = async (req: Request<{ pid: string }>, res: Resp
|
|||||||
if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") {
|
if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") {
|
||||||
return res
|
return res
|
||||||
.status(404)
|
.status(404)
|
||||||
.json(generateError(`Could not link to team with ID '${pid}, or group with ID ${body.groupId}'`));
|
.json(generateError(`Could not link to team with ID '${body.teamPid}, or group with ID ${body.groupPid}'`));
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// at: PATCH api/participants/:pid/
|
||||||
export const updateParticipant = async (req: Request<{ pid: string }>, res: Response) => {
|
export const updateParticipant = async (req: Request<{ pid: string }>, res: Response) => {
|
||||||
//insert TeamleaderAuth
|
const result = InitialParticipant.partial().safeParse(req.body);
|
||||||
|
|
||||||
const result = ParticipantBody.partial().safeParse(req.body); // Should be partial, right?
|
|
||||||
|
|
||||||
if (result.success === false) {
|
if (result.success === false) {
|
||||||
return res.status(400).json(
|
return res.status(400).json(
|
||||||
@@ -110,7 +100,7 @@ export const updateParticipant = async (req: Request<{ pid: string }>, res: Resp
|
|||||||
{
|
{
|
||||||
firstname: DataType.STRING,
|
firstname: DataType.STRING,
|
||||||
lastName: DataType.STRING,
|
lastName: DataType.STRING,
|
||||||
groupId: DataType.UUID,
|
groupPid: DataType.UUID,
|
||||||
},
|
},
|
||||||
result.error
|
result.error
|
||||||
)
|
)
|
||||||
@@ -126,7 +116,7 @@ export const updateParticipant = async (req: Request<{ pid: string }>, res: Resp
|
|||||||
data: {
|
data: {
|
||||||
firstName: body.firstName,
|
firstName: body.firstName,
|
||||||
lastName: body.lastName,
|
lastName: body.lastName,
|
||||||
group: { connect: { pid: body.groupId } },
|
group: { connect: { pid: body.groupPid } },
|
||||||
},
|
},
|
||||||
select: returnedParticipant,
|
select: returnedParticipant,
|
||||||
});
|
});
|
||||||
@@ -144,9 +134,8 @@ export const updateParticipant = async (req: Request<{ pid: string }>, res: Resp
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// at: DELETE api/participants/:pid/
|
||||||
export const deleteParticipant = async (req: Request<{ pid: string }>, res: Response) => {
|
export const deleteParticipant = async (req: Request<{ pid: string }>, res: Response) => {
|
||||||
//insert TeamleaderAuth
|
|
||||||
|
|
||||||
const { pid } = req.params;
|
const { pid } = req.params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -155,7 +144,7 @@ export const deleteParticipant = async (req: Request<{ pid: string }>, res: Resp
|
|||||||
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 participant with the ID ${pid} could not be found`));
|
throw new NotFoundError("participant", pid);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
|
|||||||
@@ -8,6 +8,30 @@ import { createInsufficientPermissionsError, DataType, generateInvalidBodyError
|
|||||||
|
|
||||||
require("express-async-errors");
|
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
|
* @param teamPid: Pid of the team to add the roles to
|
||||||
@@ -67,8 +91,6 @@ export async function assignParticipantToRole(req: Request<{ pid: string }>, res
|
|||||||
const { participantPid } = zBody.data;
|
const { participantPid } = zBody.data;
|
||||||
const rolePid = req.params.pid;
|
const rolePid = req.params.pid;
|
||||||
|
|
||||||
requireResponsibleForParticipant(req.teamleader, participantPid);
|
|
||||||
|
|
||||||
const schema = await prisma.role.findFirst({
|
const schema = await prisma.role.findFirst({
|
||||||
where: { pid: rolePid, team: { participants: { some: { pid: participantPid } } } },
|
where: { pid: rolePid, team: { participants: { some: { pid: participantPid } } } },
|
||||||
select: { participant: { select: { pid: true, firstName: true, lastName: true } } },
|
select: { participant: { select: { pid: true, firstName: true, lastName: true } } },
|
||||||
@@ -94,66 +116,3 @@ export async function assignParticipantToRole(req: Request<{ pid: string }>, res
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updateRoleScore = async (req: Request<{ pid: string }, {}, { score: string }>, res: Response) => {
|
|
||||||
if (req.auth?.permission_level != "ELEVATED") {
|
|
||||||
res.status(403).json(createInsufficientPermissionsError());
|
|
||||||
}
|
|
||||||
|
|
||||||
const { score } = req.body;
|
|
||||||
|
|
||||||
if (typeof score !== "string") {
|
|
||||||
res.status(400).json(generateInvalidBodyError({ score: DataType.STRING }));
|
|
||||||
}
|
|
||||||
|
|
||||||
const { pid } = req.params;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const role = await prisma.role.update({
|
|
||||||
where: { pid },
|
|
||||||
data: { score },
|
|
||||||
select: {
|
|
||||||
pid: true,
|
|
||||||
score: true,
|
|
||||||
schema: {
|
|
||||||
select: {
|
|
||||||
pid: true,
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
participant: {
|
|
||||||
select: {
|
|
||||||
pid: true,
|
|
||||||
firstName: true,
|
|
||||||
lastName: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
team: {
|
|
||||||
select: {
|
|
||||||
pid: true,
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(200).json({
|
|
||||||
type: "success",
|
|
||||||
payload: {
|
|
||||||
role,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") {
|
|
||||||
throw new NotFoundError("role", pid);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function deleteRolesFromTeam(teamPid: string) {
|
|
||||||
await prisma.role.deleteMany({
|
|
||||||
where: { team: { pid: teamPid } },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const RoleSchemaBody = z.object({
|
|||||||
schema: z.string(),
|
schema: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const UpdateRoleSchema = RoleSchemaBody.partial();
|
const UpdateBody = RoleSchemaBody.partial();
|
||||||
|
|
||||||
const roleSchema = {
|
const roleSchema = {
|
||||||
pid: true,
|
pid: true,
|
||||||
@@ -134,126 +134,48 @@ export const createRoleSchema = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateRoleSchema = async (req: Request<{ pid: string }>, res: Response) => {
|
export const UpdateRoleSchema = async (req: Request<{ pid: string }>, res: Response) => {
|
||||||
if (req.auth?.permission_level !== "ELEVATED") {
|
if (req.auth?.permission_level !== "ELEVATED") {
|
||||||
res.status(403).json(createInsufficientPermissionsError());
|
res.status(403).json(createInsufficientPermissionsError());
|
||||||
}
|
}
|
||||||
|
|
||||||
const { pid } = req.params;
|
const { pid } = req.params;
|
||||||
|
|
||||||
const result = UpdateRoleSchema.safeParse(req.body);
|
const result = UpdateBody.safeParse(req.body);
|
||||||
|
|
||||||
if (result.success === false) {
|
if (result.success === false) {
|
||||||
return res.status(400).json(
|
return res.status(400).json(
|
||||||
generateInvalidBodyError(
|
generateInvalidBodyError(
|
||||||
{
|
{
|
||||||
name: DataType.STRING,
|
name: DataType.STRING,
|
||||||
schema: DataType.RESULT_SCHEMA,
|
schema: DataType.STRING,
|
||||||
},
|
},
|
||||||
result.error
|
result.error
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name, schema } = result.data;
|
const body = result.data;
|
||||||
|
|
||||||
const validatedSchema = parseSchema(schema);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const schema = await prisma.roleSchema.update({
|
const schema = await prisma.roleSchema.update({
|
||||||
where: { pid },
|
where: { pid },
|
||||||
data: {
|
data: {
|
||||||
name: name,
|
name: body.name,
|
||||||
schema: validatedSchema,
|
schema: body.schema,
|
||||||
},
|
},
|
||||||
select: roleSchema,
|
select: roleSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
type: "success",
|
type: "success",
|
||||||
payload: schema,
|
payload: {
|
||||||
|
schema,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") {
|
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") {
|
||||||
throw new NotFoundError("roleSchema", pid);
|
throw new NotFoundError("discipline", pid);
|
||||||
}
|
|
||||||
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteRoleSchema = async (req: Request<{ pid: string }>, res: Response) => {
|
|
||||||
if (req.auth?.permission_level !== "ELEVATED") {
|
|
||||||
res.status(403).json(createInsufficientPermissionsError());
|
|
||||||
}
|
|
||||||
|
|
||||||
const { pid } = req.params;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await prisma.roleSchema.delete({ where: { pid } });
|
|
||||||
|
|
||||||
return res.status(204).end();
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") {
|
|
||||||
return res.status(404).json(generateError(`The RoleSchema with the ID ${pid} could not be found`));
|
|
||||||
}
|
|
||||||
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
interface visualParams {
|
|
||||||
schemaPid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface visualBody {
|
|
||||||
mediaPid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const addVisual = async (req: Request<visualParams, {}, visualBody>, res: Response) => {
|
|
||||||
if (req.auth?.permission_level != "ELEVATED") {
|
|
||||||
res.status(403).json(createInsufficientPermissionsError());
|
|
||||||
}
|
|
||||||
|
|
||||||
const { schemaPid } = req.params;
|
|
||||||
|
|
||||||
const schema = await prisma.roleSchema.update({
|
|
||||||
where: { pid: schemaPid },
|
|
||||||
data: {
|
|
||||||
visual: { connect: { pid: req.body.mediaPid } },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!schema) {
|
|
||||||
throw new NotFoundError("role_schema", schemaPid);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).json({
|
|
||||||
type: "success",
|
|
||||||
payload: {},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteVisual = async (req: Request<visualParams & { pid: string }>, res: Response) => {
|
|
||||||
if (req.auth?.permission_level != "ELEVATED") {
|
|
||||||
res.status(403).json(createInsufficientPermissionsError());
|
|
||||||
}
|
|
||||||
|
|
||||||
const { schemaPid, pid } = req.params;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await prisma.roleSchema.update({
|
|
||||||
where: {
|
|
||||||
pid: schemaPid,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
visual: { disconnect: { pid } },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return res.status(204).end();
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") {
|
|
||||||
throw new NotFoundError("role_schema", schemaPid);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
|
|||||||
@@ -4,9 +4,31 @@ import { createInsufficientPermissionsError, DataType, generateInvalidBodyError
|
|||||||
import { requireLeaderOfTeam } from "../Middleware/auth/teamleaderAuth";
|
import { requireLeaderOfTeam } from "../Middleware/auth/teamleaderAuth";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { TeamBody } from "./user_auth.controller";
|
import { TeamBody } from "./user_auth.controller";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
import NotFoundError from "../Middleware/error/NotFoundError";
|
||||||
|
|
||||||
|
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) => {
|
export const getTeams = async (req: Request, res: Response) => {
|
||||||
const teams = prisma.team.findMany({ select: { pid: true, name: true, disciplineId: true } });
|
const teams = await prisma.team.findMany({ select: basicTeam });
|
||||||
|
|
||||||
res.status(200).json({ type: "success", payload: { teams } });
|
res.status(200).json({ type: "success", payload: { teams } });
|
||||||
};
|
};
|
||||||
@@ -14,13 +36,9 @@ export const getTeams = async (req: Request, res: Response) => {
|
|||||||
export const getTeam = async (req: Request, res: Response) => {
|
export const getTeam = async (req: Request, res: Response) => {
|
||||||
const { pid } = req.params;
|
const { pid } = req.params;
|
||||||
|
|
||||||
const team = prisma.team.findUnique({
|
const team = await prisma.team.findUnique({
|
||||||
where: { pid },
|
where: { pid },
|
||||||
select: {
|
select: basicTeam,
|
||||||
disciplineId: true,
|
|
||||||
name: true,
|
|
||||||
pid: true,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(200).json({ type: "success", payload: { team } });
|
res.status(200).json({ type: "success", payload: { team } });
|
||||||
@@ -48,26 +66,35 @@ export const updateTeam = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
requireLeaderOfTeam(req.teamleader, body.pid);
|
requireLeaderOfTeam(req.teamleader, body.pid);
|
||||||
|
|
||||||
const team = prisma.team.update({
|
try {
|
||||||
where: {
|
const team = await prisma.team.update({
|
||||||
pid: body.pid,
|
where: {
|
||||||
},
|
pid: body.pid,
|
||||||
data: {
|
},
|
||||||
name: body.teamName,
|
data: {
|
||||||
discipline: { connect: { pid: body.disciplineId } },
|
name: body.teamName,
|
||||||
leaderEmail: body.leaderEmail,
|
discipline: { connect: { pid: body.disciplineId } },
|
||||||
},
|
leaderEmail: body.leaderEmail,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
res.status(204).json({ type: "success", payload: { team } });
|
res.status(204).json({ type: "success", payload: { team } });
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") {
|
||||||
|
throw new NotFoundError("team", body.pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteTeam = async (req: Request, res: Response) => {
|
export const deleteTeam = async (req: Request, res: Response) => {
|
||||||
const { pid } = req.params;
|
const { pid } = req.params;
|
||||||
|
|
||||||
|
// TODO: accept admin auth
|
||||||
requireLeaderOfTeam(req.teamleader, pid);
|
requireLeaderOfTeam(req.teamleader, pid);
|
||||||
|
|
||||||
prisma.team.delete({ where: { pid } });
|
await prisma.team.delete({ where: { pid } });
|
||||||
|
|
||||||
res.status(204).json({ type: "success", payload: { message: "Sucesfully deleted team" } });
|
res.status(204).json({ type: "success", payload: { message: "Sucesfully deleted team" } });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { createInsufficientPermissionsError, DataType, generateError, generateIn
|
|||||||
import { generateTeamleaderJWT, requireLeaderOfTeam } from "../Middleware/auth/teamleaderAuth";
|
import { generateTeamleaderJWT, requireLeaderOfTeam } from "../Middleware/auth/teamleaderAuth";
|
||||||
import { createRolesForTeam } from "./role.controller";
|
import { createRolesForTeam } from "./role.controller";
|
||||||
import { any, z } from "zod";
|
import { any, z } from "zod";
|
||||||
|
import { basicTeam } from "./team.controller";
|
||||||
|
|
||||||
export const TeamBody = z.object({
|
export const TeamBody = z.object({
|
||||||
teamName: z.string().min(1),
|
teamName: z.string().min(1),
|
||||||
@@ -81,13 +82,13 @@ export const register = async (req: Request<{}, {}, CreateTeamBody>, res: Respon
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
//TODO: maybe use returned amount of created use?
|
await createRolesForTeam(team.pid);
|
||||||
createRolesForTeam(team.pid);
|
|
||||||
|
|
||||||
const usid = nanoid();
|
const usid = nanoid();
|
||||||
|
|
||||||
(await mailClient).set(usid, team.pid);
|
(await mailClient).set(usid, team.pid);
|
||||||
|
|
||||||
|
// TODO: fix "eventname"
|
||||||
verificationMail(req.body.leaderEmail, "eventname", usid);
|
verificationMail(req.body.leaderEmail, "eventname", usid);
|
||||||
|
|
||||||
res.status(201).json({ type: "success", payload: { team } });
|
res.status(201).json({ type: "success", payload: { team } });
|
||||||
@@ -114,6 +115,7 @@ export const requestToken = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
(await mailClient).set(usid, team.pid);
|
(await mailClient).set(usid, team.pid);
|
||||||
|
|
||||||
|
// TODO: fix "eventname"
|
||||||
verificationMail(team.leaderEmail, "eventname", usid);
|
verificationMail(team.leaderEmail, "eventname", usid);
|
||||||
|
|
||||||
res.status(200).json({ type: "sucess", payload: { message: "Email sent!" } });
|
res.status(200).json({ type: "sucess", payload: { message: "Email sent!" } });
|
||||||
|
|||||||
@@ -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,10 +1,8 @@
|
|||||||
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,
|
updateDiscipline,
|
||||||
@@ -20,18 +18,6 @@ router.get("/:pid", getDiscipline);
|
|||||||
router.patch("/:pid", requireAuthentication, updateDiscipline);
|
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;
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ import Express from "express";
|
|||||||
import { string } from "zod";
|
import { string } from "zod";
|
||||||
import {
|
import {
|
||||||
addEvent,
|
addEvent,
|
||||||
addVisual,
|
|
||||||
deleteEvent,
|
deleteEvent,
|
||||||
deleteVisual,
|
|
||||||
getAllEvents,
|
getAllEvents,
|
||||||
getEvent,
|
getEvent,
|
||||||
updateEvent,
|
updateEvent,
|
||||||
@@ -22,12 +20,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;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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 +19,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;
|
||||||
|
|||||||
@@ -1,29 +1,24 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import teamRouter from "./team.routes";
|
|
||||||
import { createParticipant, deleteParticipant, updateParticipant } from "../Controllers/participant.controller";
|
import { createParticipant, deleteParticipant, updateParticipant } from "../Controllers/participant.controller";
|
||||||
import { requireAuthentication } from "../Middleware/auth/auth";
|
import { requireAuthentication, requireConfiguredAuthentication } from "../Middleware/auth/auth";
|
||||||
import { requireTeamleaderAuthentication } from "../Middleware/auth/teamleaderAuth";
|
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
teamRouter.post<"/:pid/participant/", { pid: string }>(
|
router.post(
|
||||||
"/:pid/participant/",
|
"/",
|
||||||
requireAuthentication,
|
requireConfiguredAuthentication({ optional: false, type: { admin: true, teamleader: true } }),
|
||||||
requireTeamleaderAuthentication,
|
|
||||||
createParticipant
|
createParticipant
|
||||||
);
|
);
|
||||||
|
|
||||||
teamRouter.patch<"/:pid/participant/", { pid: string }>(
|
router.patch<"/:pid/", { pid: string }>(
|
||||||
"/:pid/participant/",
|
"/:pid/",
|
||||||
requireAuthentication,
|
requireConfiguredAuthentication({ optional: false, type: { admin: true, teamleader: true } }),
|
||||||
requireTeamleaderAuthentication,
|
|
||||||
updateParticipant
|
updateParticipant
|
||||||
);
|
);
|
||||||
|
|
||||||
teamRouter.delete<"/:pid/participant/", { pid: string }>(
|
router.delete<"/:pid/", { pid: string }>(
|
||||||
"/:pid/participant/",
|
"/:pid/",
|
||||||
requireAuthentication,
|
requireConfiguredAuthentication({ optional: false, type: { admin: true, teamleader: true } }),
|
||||||
requireTeamleaderAuthentication,
|
|
||||||
deleteParticipant
|
deleteParticipant
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,20 @@
|
|||||||
import Express from "express";
|
import Express from "express";
|
||||||
import { assignParticipantToRole, getRolesForTeam } from "../Controllers/role.controller";
|
import { assignParticipantToRole, getRolesForTeam } from "../Controllers/role.controller";
|
||||||
|
import { requireAuthentication, requireConfiguredAuthentication } from "../Middleware/auth/auth";
|
||||||
|
|
||||||
const router = Express.Router();
|
const router = Express.Router();
|
||||||
|
|
||||||
//TO DO: maybe transfer getRolesForTeam to team router -> Seconded
|
//TO DO: maybe transfer getRolesForTeam to team router -> Seconded
|
||||||
router.get<"team/:teamPid/", { teamPid: string }>("team/:teamPid/", getRolesForTeam);
|
router.get<"team/:teamPid/", { teamPid: string }>(
|
||||||
|
"team/:teamPid/",
|
||||||
|
requireConfiguredAuthentication({ optional: false, type: { admin: true, teamleader: true } }),
|
||||||
|
getRolesForTeam
|
||||||
|
);
|
||||||
|
|
||||||
router.put<"/:pid/participant", { pid: string }>("/:pid/participant", assignParticipantToRole);
|
router.put<"/:pid/participant", { pid: string }>(
|
||||||
|
"/:pid/participant",
|
||||||
|
requireConfiguredAuthentication({ optional: false, type: { admin: true, teamleader: true } }),
|
||||||
|
assignParticipantToRole
|
||||||
|
);
|
||||||
|
|
||||||
|
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;
|
||||||
|
|||||||
@@ -6,9 +6,17 @@ import { deleteTeam, getTeam, getTeams, updateTeam } from "../Controllers/team.c
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.get("/", requireConfiguredAuthentication({ type: "admin", optional: false }), getTeams);
|
router.get("/", requireConfiguredAuthentication({ type: "admin", optional: false }), getTeams);
|
||||||
router.get("/:id", getTeam);
|
router.get(
|
||||||
|
"/:id",
|
||||||
|
requireConfiguredAuthentication({ optional: false, type: { admin: true, teamleader: true } }),
|
||||||
|
getTeam
|
||||||
|
);
|
||||||
|
|
||||||
router.put("/", requireTeamleaderAuthentication, updateTeam);
|
router.put("/", requireTeamleaderAuthentication, updateTeam);
|
||||||
router.delete<"/:pid/", { pid: string }>("/:pid/", requireAuthentication, requireTeamleaderAuthentication, deleteTeam);
|
router.delete<"/:pid/", { pid: string }>(
|
||||||
|
"/:pid/",
|
||||||
|
requireConfiguredAuthentication({ optional: false, type: { admin: true, teamleader: true } }),
|
||||||
|
deleteTeam
|
||||||
|
);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
+8
-5
@@ -14,7 +14,9 @@ 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 userRouter from "./Routes/user_auth.routes";
|
||||||
import TeamRouter from "./Routes/team.routes";
|
import teamRouter from "./Routes/team.routes";
|
||||||
|
import roleRouter from "./Routes/role.routes";
|
||||||
|
import participantRouter from "./Routes/participant.routes";
|
||||||
import { notFoundHandler, rootHandler } from "./Middleware/error/defaultRoutes";
|
import { notFoundHandler, rootHandler } from "./Middleware/error/defaultRoutes";
|
||||||
|
|
||||||
// Set up async error handling
|
// Set up async error handling
|
||||||
@@ -87,7 +89,11 @@ async function main() {
|
|||||||
|
|
||||||
app.use("/api/users", userRouter);
|
app.use("/api/users", userRouter);
|
||||||
|
|
||||||
app.use("/api/teams", TeamRouter);
|
app.use("/api/teams", teamRouter);
|
||||||
|
|
||||||
|
app.use("/api/roles", roleRouter);
|
||||||
|
|
||||||
|
app.use("/api/participants", participantRouter);
|
||||||
|
|
||||||
app.get("/", rootHandler);
|
app.get("/", rootHandler);
|
||||||
app.get("/api", rootHandler);
|
app.get("/api", rootHandler);
|
||||||
@@ -95,9 +101,6 @@ async function main() {
|
|||||||
// 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, () => {
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ const sendMail = async (from: string, to: string, subject: string, text?: string
|
|||||||
export const verificationMail = async (to: string, eventName: string, verificationLink: string) => {
|
export const verificationMail = async (to: string, eventName: string, verificationLink: string) => {
|
||||||
const raw = mjml.getTemplate("emailVerification");
|
const raw = mjml.getTemplate("emailVerification");
|
||||||
|
|
||||||
|
// TODO: the process.env.DOMAIN is undefined in Development mode !!
|
||||||
verificationLink =
|
verificationLink =
|
||||||
"https://" + ("api." + process.env.DOMAIN ?? "localhost:3000/api") + "/users/verify/" + verificationLink;
|
"https://" + ("api." + process.env.DOMAIN ?? "localhost:3000/api") + "/users/verify/" + verificationLink;
|
||||||
const message = Handlebars.compile(raw);
|
const message = Handlebars.compile(raw);
|
||||||
|
|||||||
Reference in New Issue
Block a user