Merge pull request #53 from detleph/dev

Dev
This commit is contained in:
Stefan
2022-05-20 17:51:43 +02:00
committed by GitHub
18 changed files with 3193 additions and 132 deletions
+3
View File
@@ -111,3 +111,6 @@ host/
# Ignore flags
INITIALIZED
# Ignore runtime files
media/
+2
View File
@@ -11,6 +11,8 @@ COPY package.json package-lock.json /app/
# Change directory into the container
WORKDIR /app
RUN mkdir /app/media
# Install dependencies
RUN npm i -g typescript
RUN npm i
+2
View File
@@ -51,6 +51,8 @@ if [ "$RECREATE" = true ]; then
-e DATABASE_PASSWORD=server \
-e DATABASE_URL="postgresql://server:server@postgres:5432/management?schema=public" \
-e NODE_ENV="development" \
-e PORT="${D_PORT}" \
-p "${D_PORT}":"${D_PORT}" \
--entrypoint "/app/scripts/docker-entrypoint.dev.sh" \
node
fi
+2584 -101
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -22,6 +22,7 @@
"@prisma/client": "^3.3.0",
"@types/cors": "^2.8.12",
"@types/handlebars": "^4.1.0",
"@types/express-fileupload": "^1.2.1",
"@types/jsonwebtoken": "^8.5.5",
"@types/mjml": "^4.7.0",
"@types/node": "^16.10.3",
@@ -32,6 +33,9 @@
"dotenv": "^10.0.0",
"express": "^4.17.1",
"express-async-errors": "^3.1.1",
"express-fileupload": "^1.2.1",
"file-type": "^16.5.3",
"is-svg": "^4.3.2",
"handlebars": "^4.7.7",
"jsonwebtoken": "^8.5.1",
"mjml": "^4.11.0",
+10 -10
View File
@@ -10,11 +10,12 @@ generator client {
}
model Event {
id Int @id @default(autoincrement()) // Primary key
pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid // Public key
date DateTime
name String
description String
id Int @id @default(autoincrement()) // Primary key
pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid // Public key
date DateTime
name String
briefDescription String
fullDescription String?
disciplines Discipline[]
organisations Organisation[]
@@ -119,13 +120,12 @@ model Group {
model Media {
id Int @id @default(autoincrement())
pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
pid String @unique
description String
location String @unique
event Event[]
discipline Discipline[]
role RoleSchema[]
events Event[]
disciplines Discipline[]
roles RoleSchema[]
}
enum AdminLevel {
+1
View File
@@ -4,6 +4,7 @@ if ! test -f INITIALIZED; then
touch INITIALIZED
cd /app
mkdir media
npm i -g ts-node nodemon
npm i
+15
View File
@@ -3,6 +3,21 @@ import { PrismaClientKnownRequestError, PrismaClientUnknownRequestError } from "
import { Request, Response } from "express";
import prisma from "../lib/prisma";
interface StringIndexedObject {
[k: string]: any;
}
export function createError(message: string, payload: StringIndexedObject = {}, links: StringIndexedObject = {}) {
return {
type: "error",
payload: {
message: message,
...payload,
},
_links: links,
};
}
export enum DataType {
STRING = "string",
NUMBER = "number",
+59 -1
View File
@@ -18,7 +18,7 @@ require("express-async-errors");
const basicDiscipline = {
pid: true,
name: true,
visual: { select: { pid: true, location: true } },
visual: { select: { pid: true } },
maxTeamSize: true,
minTeamSize: true,
event: { select: { pid: true, name: true } },
@@ -187,3 +187,61 @@ export const deleteDiscipline = async (req: Request<DeleteDisciplineQueryParams>
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;
}
};
+182 -16
View File
@@ -1,14 +1,20 @@
import { Prisma } from "@prisma/client";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
import { Request, Response } from "express";
import { z } from "zod";
import prisma from "../lib/prisma";
import NotFoundError from "../Middleware/error/NotFoundError";
import { createInsufficientPermissionsError, DataType, generateError, generateInvalidBodyError } from "./common";
require("express-async-errors");
export const getAllEvents = async (req: Request, res: Response) => {
const events = await prisma.event.findMany({
select: {
name: true,
description: true,
briefDescription: true,
fullDescription: true,
visual: { select: { pid: true, description: true } },
date: true,
pid: true,
id: false,
@@ -41,13 +47,19 @@ export const getEvent = async (req: Request, res: Response) => {
},
select: {
name: true,
description: true,
briefDescription: true,
fullDescription: true,
date: true,
pid: true,
id: false,
visual: { select: { pid: true, description: true } },
},
});
if (!event) {
throw new NotFoundError("event", eventId);
}
res.status(200).json({
type: "success",
payload: {
@@ -56,16 +68,15 @@ export const getEvent = async (req: Request, res: Response) => {
});
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
res.status(500).json({
return res.status(500).json({
type: "error",
payload: {
message: `Internal Server error occured. Try again later`,
},
});
return;
}
if (e instanceof Prisma.PrismaClientUnknownRequestError) {
res.status(500).json({
return res.status(500).json({
type: "error",
payload: {
message: "Unknown error occurred with your request. Check if your parameters are correct",
@@ -74,8 +85,9 @@ export const getEvent = async (req: Request, res: Response) => {
},
},
});
return;
}
throw e;
}
};
@@ -87,13 +99,17 @@ export const addEvent = async (req: Request, res: Response) => {
if (
typeof req.body.name !== "string" ||
typeof req.body.date !== "string" ||
typeof req.body.description !== "string"
typeof req.body.briefDescription !== "string" ||
(req.body.fullDescription && typeof req.body.fullDescription !== "string")
) {
generateInvalidBodyError({
name: DataType.STRING,
date: DataType.DATETIME,
description: DataType.STRING,
});
return res.status(400).json(
generateInvalidBodyError({
name: DataType.STRING,
date: DataType.DATETIME,
briefDescription: DataType.STRING,
["fullDescription?"]: DataType.STRING,
})
);
}
//TODO: Check if date is valid
@@ -102,14 +118,16 @@ export const addEvent = async (req: Request, res: Response) => {
data: {
name: req.body.name,
date: req.body.date,
description: req.body.description,
briefDescription: req.body.briefDescription,
fullDescription: req.body.fullDescription,
},
select: {
name: true,
date: true,
pid: true,
id: false,
description: true,
briefDescription: true,
fullDescription: true,
},
});
@@ -121,12 +139,96 @@ export const addEvent = async (req: Request, res: Response) => {
});
};
const EventBody = z.object({
name: z.string(),
date: z.string(),
briefDescription: z.string(),
fullDescription: z.string(),
});
const UpdateBody = EventBody.partial();
export const updateEvent = async (req: Request<{ pid: string }>, res: Response) => {
if (req.auth?.permission_level !== "ELEVATED") {
res.status(403).json(createInsufficientPermissionsError());
}
const { pid } = req.params;
const result = UpdateBody.safeParse(req.body);
if (result.success === false) {
return res.status(400).json(
generateInvalidBodyError({
name: DataType.STRING,
date: DataType.DATETIME,
briefDescription: DataType.STRING,
["fullDescription?"]: DataType.STRING,
})
);
}
const body = result.data;
try {
const event = await prisma.event.update({
where: { pid },
data: {
name: body.name,
date: body.date,
briefDescription: body.briefDescription,
fullDescription: body.fullDescription,
},
select: {
pid: true,
name: true,
date: true,
briefDescription: true,
fullDescription: true,
},
});
if (!event) {
throw new NotFoundError("event", pid);
}
res.status(200).json({
type: "success",
payload: {
event,
},
});
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
return res.status(500).json({
type: "error",
payload: {
message: `Internal Server error occured. Try again later`,
},
});
}
if (e instanceof Prisma.PrismaClientUnknownRequestError) {
return res.status(500).json({
type: "error",
payload: {
message: "Unknown error occurred with your request. Check if your parameters are correct",
schema: {
eventId: DataType.UUID,
},
},
});
}
throw e;
}
};
interface DeleteEventQueryParams {
pid: string;
}
// requires: auth(ELEVATED)
export const deleteEvent = (req: Request<DeleteEventQueryParams>, res: Response) => {
export const deleteEvent = async (req: Request<DeleteEventQueryParams>, res: Response) => {
if (req.auth?.permission_level !== "ELEVATED") {
res.status(403).json(createInsufficientPermissionsError());
}
@@ -134,7 +236,7 @@ export const deleteEvent = (req: Request<DeleteEventQueryParams>, res: Response)
const { pid } = req.params;
try {
prisma.event.delete({ where: { pid } });
await prisma.event.delete({ where: { pid } });
return res.status(204).end();
} catch (e) {
@@ -145,3 +247,67 @@ export const deleteEvent = (req: Request<DeleteEventQueryParams>, res: Response)
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;
}
};
+191
View File
@@ -0,0 +1,191 @@
import { Request, response, Response } from "express";
import fs from "fs";
import isSvg from "is-svg";
import { fromBuffer as fileTypeFromBuffer } from "file-type";
import { AUTH_ERROR, createError, createInsufficientPermissionsError } from "./common";
import { Prisma } from "@prisma/client";
import prisma from "../lib/prisma";
import NotFoundError from "../Middleware/error/NotFoundError";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
import { generateInvalidBodyError, DataType } from "./common";
import { type } from "os";
import { unlink } from "fs/promises";
import ForwardableError from "../Middleware/error/ForwardableError";
import SchemaError from "../Middleware/error/SchemaError";
require("express-async-errors");
function createMediaLinks(fileName: string) {
return [{ rel: "self", type: "GET", href: `/api/media/${fileName}` }];
}
export const uploadImage = async (req: Request, res: Response) => {
if (!req.auth?.isAuthenticated) {
return res.status(500).json(AUTH_ERROR);
}
if (!(req.auth.permission_level === "ELEVATED")) {
return res.status(403).json(createInsufficientPermissionsError());
}
let description = "Sample images";
const meta = req.body.meta;
if (meta) {
try {
const { description: desc } = JSON.parse(meta);
if (typeof desc === "string") description = desc;
} catch (e) {
if (e instanceof SyntaxError) {
return res.status(400).json({
type: "error",
payload: {
message: "'meta' could not be parsed",
schema: { mutlipart: { file: "binary file", meta: { ["description?"]: "string" } } },
},
});
}
throw e;
}
}
if (!req.files || !("file" in req.files)) {
return res.json({
type: "error",
payload: {
message: "The format of the data was not valid",
schema: {
multipart: {
file: "binary file",
},
},
},
});
}
const file = req.files.file;
if (Array.isArray(file)) {
return res.status(500).json({
type: "failure",
payload: {
message: "Invalid application state",
},
});
}
if (file.size > 1024 * 1024) {
// Maybe resize (express-fileupload seems to support this)
return res.status(413).json(createError("The uploaded image is too large", { max_size: "1 MB" }));
}
const fileIsSvg = isSvg(file.data);
const fileType = await fileTypeFromBuffer(file.data); // Could be optimized
if (!(fileType && ["image/jpeg", "image/png"].includes(fileType.mime))) {
if (!fileIsSvg)
return res
.status(415)
.json(
createError(
"The uploaded image does not satisfy the MIME type constraints (Only image/jpeg, image/png and SVG files are accepted)"
)
);
}
const fileName = file.md5 + (fileIsSvg ? ".svg" : "." + fileType?.ext);
try {
//generate record
const media = await prisma.media.create({
data: {
pid: fileName,
description: description,
},
select: {
pid: true,
description: true,
},
});
if (!fs.existsSync("media/" + fileName)) {
file.mv("media/" + fileName, console.error);
}
return res.status(201).json({
type: "success",
payload: { media },
});
} catch (e) {
if (e instanceof PrismaClientKnownRequestError && e.code === "P2002") {
throw new ForwardableError(409, `The image with the the hash and extenstion ${fileName} already exists!`);
}
throw e;
}
};
export const getAllMedia = async (req: Request, res: Response) => {
const medias = await prisma.media.findMany({
select: {
description: true,
events: { select: { pid: true } },
disciplines: { select: { pid: true } },
roles: { select: { pid: true } },
pid: true,
id: false,
},
});
res.status(200).json({
type: "success",
payload: {
media: medias,
},
});
};
export const getMediaMeta = async (req: Request, res: Response) => {
const pid = req.params.pid;
const meta = await prisma.media.findUnique({ where: { pid }, select: { pid: true, description: true } });
if (!meta) {
throw new NotFoundError("media", pid);
}
return res.status(200).json({
type: "success",
payload: {
meta,
},
});
};
export const deleteMedia = async (req: Request, res: Response) => {
if (req.auth?.permission_level != "ELEVATED") {
res.status(403).json(createInsufficientPermissionsError());
}
const { pid } = req.params;
const location = "media/" + pid;
if (fs.existsSync(location)) {
await unlink(location);
}
try {
await prisma.media.delete({ where: { pid } });
return res.status(204).end();
} catch (e) {
if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") {
throw new NotFoundError("media", pid);
}
throw e;
}
};
+59 -1
View File
@@ -17,7 +17,7 @@ const roleSchema = {
name: true,
schema: true,
discipline: { select: { pid: true, name: true } },
visual: { select: { pid: true, location: true } },
visual: { select: { pid: true } },
} as const;
export const _getAllRoleSchemas = async (res: Response, disciplinePid: string | undefined) => {
@@ -123,3 +123,61 @@ export const createRoleSchema = async (
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;
}
};
+1 -1
View File
@@ -67,7 +67,7 @@ export default function defaultErrorHandler(err: any, req: Request, res: Respons
logger.error("--- Unhandled error ---");
logger.error(err);
return res.status(err.status).json({
return res.status(err.status || 500).json({
type: "error",
payload: {
message: err.message,
+16
View File
@@ -1,8 +1,10 @@
import express from "express";
import eventRouter from "./event.routes";
import {
addVisual,
createDiscipline,
deleteDiscipline,
deleteVisual,
getAllDisciplines,
getDiscipline,
} from "../Controllers/discipline.controller";
@@ -11,9 +13,23 @@ import { requireAuthentication } from "../Middleware/auth/auth";
const router = express.Router();
router.get("/", getAllDisciplines); // TODO: Optional auth
router.get("/:pid", getDiscipline);
router.delete<"/:pid", { pid: string }>("/:pid", requireAuthentication, deleteDiscipline);
router.post<"/:disciplinePid/images", { disciplinePid: string }>(
"/:disciplinePid/images",
requireAuthentication,
addVisual
);
router.delete<"/:disciplinePid/images/:pid", { disciplinePid: string; pid: string }>(
"/:disciplinePid/images/:pid",
requireAuthentication,
deleteVisual
);
eventRouter.post("/:eventPid/disciplines", requireAuthentication, createDiscipline);
export default router;
+22 -1
View File
@@ -1,5 +1,14 @@
import Express from "express";
import { addEvent, getAllEvents, getEvent } from "../Controllers/event.controller";
import { string } from "zod";
import {
addEvent,
addVisual,
deleteEvent,
deleteVisual,
getAllEvents,
getEvent,
updateEvent,
} from "../Controllers/event.controller";
import { requireAuthentication } from "../Middleware/auth/auth";
const router = Express.Router();
@@ -9,4 +18,16 @@ router.get("/:eventId", getEvent);
router.post("/", requireAuthentication, addEvent);
router.patch<"/:pid/", { pid: string }>("/:pid/", requireAuthentication, updateEvent);
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;
+22
View File
@@ -0,0 +1,22 @@
import express from "express";
import fileUpload from "express-fileupload";
import { requireAuthentication } from "../Middleware/auth/auth";
import { deleteMedia, getAllMedia, getMediaMeta, uploadImage } from "../Controllers/media.controller";
import eventRouter from "./event.routes";
import disciplineRouter from "./discipline.routes";
import roleSchemaRouter from "./role_schema.routes";
const router = express.Router();
// Media storage with express static (Should only handle GET and HEAD request methods)
router.use("/", express.static("/app/media", { redirect: false }));
router.post("/", requireAuthentication, fileUpload(), uploadImage);
router.get("/", requireAuthentication, getAllMedia); // Reading all files should be a senstive operation
router.get("/:pid/meta", getMediaMeta);
router.delete("/:pid", requireAuthentication, deleteMedia);
export default router;
+12
View File
@@ -1,7 +1,9 @@
import express from "express";
import disciplineRouter from "./discipline.routes";
import {
addVisual,
createRoleSchema,
deleteVisual,
getAllRoleSchemas,
getAllRoleSchemasWithParam,
getRoleSchema,
@@ -11,9 +13,19 @@ import { requireAuthentication } from "../Middleware/auth/auth";
const router = express.Router();
router.get("/", getAllRoleSchemas);
router.get("/:pid", getRoleSchema);
disciplineRouter.get("/:disciplinePid/role-schemas", getAllRoleSchemasWithParam);
disciplineRouter.post("/:disciplinePid/role-schemas", requireAuthentication, createRoleSchema);
router.post<"/:schemaPid/images", { schemaPid: string }>("/:schemaPid/images", requireAuthentication, addVisual);
router.delete<"/:schemaPid/images/:pid", { schemaPid: string; pid: string }>(
"/:schemaPid/images/:pid",
requireAuthentication,
deleteVisual
);
export default router;
+8 -1
View File
@@ -12,6 +12,7 @@ import roleSchemaRouter from "./Routes/role_schema.routes";
import defaultErrorHandler from "./Middleware/error/handler";
import logger from "./Middleware/error/logger";
import debugLogger from "./Middleware/debug/logger";
import mediaRouter from "./Routes/media.routes";
import { notFoundHandler, rootHandler } from "./Middleware/error/defaultRoutes";
// Set up async error handling
@@ -80,10 +81,16 @@ async function main() {
app.use("/api/role-schemas", roleSchemaRouter);
app.use("/api/media", mediaRouter);
app.get("/", rootHandler);
app.get("/api", rootHandler);
// Error handling
app.use(defaultErrorHandler); // Not working
app.use(defaultErrorHandler); // This has to be the LAST ROUTE
// Disable the media router for now
// app.use("/api/media", mediaRouter);
app.use(notFoundHandler);