Merge branch 'dev' into feature-email-verification

This commit is contained in:
Stefan
2022-04-28 14:07:53 +02:00
committed by GitHub
10 changed files with 616 additions and 47 deletions
+44 -14
View File
@@ -1,26 +1,56 @@
#!/bin/bash
D_SERVICES="mail postgres redis"
DATABASE_PASSWORD=server COMPOSE_PROJECT_NAME=detleph_server docker-compose up -d mail postgres redis
DATABASE_PASSWORD=server COMPOSE_PROJECT_NAME=detleph_server docker-compose up -d $D_SERVICES
# Flag: Should the container be deleted and created again?
RECREATE=true
D_PORT="${PORT:-3000}"
if [ "$(docker ps -a | grep detleph_server_dev)" ]; then
echo "The dev server is already on the system!"
RECREATE=false
# Use -s flag to skip any prompts
if [ "$1" != "-s" ]; then
echo "Do you want to recreate it? [y/n]"
read input
if [ "$input" = "y" ]; then
RECREATE=true
fi
fi
if [ "$RECREATE" = true ]; then
echo "Removing container"
docker rm detleph_server_dev
rm -f INITIALIZED # Flag has to be reset
echo "Do you also want to reset the other services? [y/n]"
read input
if [ "$input" = "y" ]; then
COMPOSE_PROJECT_NAME=detleph_server docker-compose down
DATABASE_PASSWORD=server COMPOSE_PROJECT_NAME=detleph_server docker-compose up -d $D_SERVICES
fi
else
echo "Starting existing container"
docker start -ia detleph_server_dev
fi
fi
if [ "$RECREATE" = true ]; then
echo "Creating the dev container"
if [ ! "$(docker ps -a | grep detleph_server_dev)" ]; then
echo "Creating the server container"
docker run -it \
--name detleph_server_dev \
--mount type=bind,source="$(pwd)",target=/app \
--network detleph_server_default \
-p 3000:3000 -e PORT=3000 \
-e DATABASE_URL="postgresql://server:server@postgres:5432/management?schema=public" \
-e DATABASE_USER=server \
-e DATABASE_PASSWORD=server \
-e DATABASE_URL="postgresql://server:server@postgres:5432/management?schema=public" \
--entrypoint "/app/scripts/docker-entrypoint.dev.sh" \
node
else
echo "The server container already exists; Starting..."
docker start -ia detleph_server_dev
fi
# After container termination
COMPOSE_PROJECT_NAME=detleph_server docker-compose stop
+2 -2
View File
@@ -134,9 +134,9 @@ model Group {
name String
user_limit Int @default(40)
level Int
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
organisationId Int
oragnisation Organisation @relation(fields: [oragnisationId], references: [id], onDelete: Cascade)
oragnisationId Int
participants Participant[]
link Link?
admins Admin[]
+1 -23
View File
@@ -2,7 +2,7 @@ import prisma from "../lib/prisma";
import { Request, Response } from "express";
import { AdminLevel } from "@prisma/client";
import argon2 from "argon2";
import { DataType, generateInvalidBodyError } from "./common";
import { AUTH_ERROR, createInsufficientPermissionsError, DataType, generateInvalidBodyError } from "./common";
import { authClient } from "../lib/redis";
export const regenerateRevision = async (pid: string) => {
@@ -16,28 +16,6 @@ export const regenerateRevision = async (pid: string) => {
await authClient.set(pid, revision.toISOString());
};
const AUTH_ERROR = {
type: "failure",
payload: {
message: "The server was not able to validate your credentials; Please try again later",
},
};
const createInsufficientPermissionsError = (required: AdminLevel = "ELEVATED") => ({
type: "error",
payload: {
message: "You do not have sufficient permissions to use this feature",
required_level: required,
},
_links: [
{
rel: "authentication",
href: "/api/authentication",
type: "POST",
},
],
});
// requires: auth(elevated)
export const getAllAdmins = async (req: Request, res: Response) => {
if (!req.auth?.isAuthenticated) {
+128
View File
@@ -1,3 +1,8 @@
import { AdminLevel, Prisma } from "@prisma/client";
import { PrismaClientKnownRequestError, PrismaClientUnknownRequestError } from "@prisma/client/runtime";
import { Request, Response } from "express";
import prisma from "../lib/prisma";
export enum DataType {
STRING = "string",
NUMBER = "number",
@@ -20,3 +25,126 @@ export function generateInvalidBodyError(body: Body) {
},
};
}
export const AUTH_ERROR = {
type: "failure",
payload: {
message: "The server was not able to validate your credentials; Please try again later",
},
};
export const createInsufficientPermissionsError = (required: AdminLevel = "ELEVATED") => ({
type: "error",
payload: {
message: "You do not have sufficient permissions to use this feature",
required_level: required,
},
_links: [
{
rel: "authentication",
href: "/api/authentication",
type: "POST",
},
],
});
export const generateError = (message: string) => {
return {
type: "error",
payload: {
message,
},
};
};
export const genericError = {
type: "error",
payload: {
message: "There was an error processing your request, please try again later",
},
};
export function validateName(name: string) {
return name.length > 0;
}
type Organisation = Partial<Prisma.OrganisationCreateArgs["data"]>;
type Group = Partial<Prisma.GroupCreateArgs["data"]>;
export async function handleCreateByName(
create: { type: "organisation"; data: Organisation },
link: { type: "event"; id: string },
req: Request<any>,
res: Response
): Promise<unknown>;
export async function handleCreateByName(
create: { type: "group"; data: Group },
link: { type: "organisation"; id: string },
req: Request<any>,
res: Response
): Promise<unknown>;
export async function handleCreateByName(
create: { type: "organisation" | "group"; data: Organisation | Group },
link: { type: "event" | "organisation"; id: string },
req: Request<any>,
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());
}
const { name } = create.data;
if (typeof name !== "string") {
return res
.status(400)
.json(generateInvalidBodyError({ name: DataType.STRING, [link.type + "Pid"]: DataType.STRING }));
}
if (!validateName(name)) {
return res.status(400).json({
type: "error",
payload: {
message: "The name has to be at least 1 character long",
},
});
}
// Check if link object exsits
try {
// @ts-ignore
const linked = await prisma[link.type].findUnique({ where: { pid: link.id }, select: { id: true } });
if (!linked) {
return res.status(404).json({
type: "error",
payload: {
message: `Could not link to ${link.type} with ID '${link.id}'`,
},
});
}
} catch (e) {
// REVIEW: Check for valid UUID
if (e instanceof PrismaClientUnknownRequestError) {
return res.status(400).json({
type: "error",
payload: {
message: "Unknown error occured. This could be due to malformed IDs",
},
});
}
}
// @ts-ignore
const object = await prisma[create.type].create({
data: { ...create.data, [link.type]: { connect: { pid: link.id } } },
select: { pid: true, name: true },
});
res.status(201).json({ type: "success", payload: { [create.type]: object } });
}
+47 -8
View File
@@ -1,7 +1,8 @@
import { Prisma } from "@prisma/client";
import { Prisma } from "@prisma/client"
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
import { Request, Response } from "express";
import prisma from "../lib/prisma";
import { DataType, generateInvalidBodyError } from "./common";
import { createInsufficientPermissionsError, DataType, generateError, generateInvalidBodyError } from "./common";
export const getAllEvents = async (req: Request, res: Response) => {
const events = await prisma.event.findMany({
@@ -13,8 +14,13 @@ export const getAllEvents = async (req: Request, res: Response) => {
id: false,
},
});
if (events.length > 0) res.status(200).json(events);
else res.status(200).json([]);
if (events.length > 0)
res.status(200).json({
type: "success",
payload: {
events,
},
});
};
export const getEvent = async (req: Request, res: Response) => {
@@ -42,7 +48,12 @@ export const getEvent = async (req: Request, res: Response) => {
},
});
res.status(200).json(event);
res.status(200).json({
type: "success",
payload: {
event,
},
});
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
res.status(500).json({
@@ -68,20 +79,23 @@ export const getEvent = async (req: Request, res: Response) => {
}
};
// requires: auth(ELEVATED)
export const addEvent = async (req: Request, res: Response) => {
if (req.auth?.permission_level !== "ELEVATED") {
res.status(403).json(createInsufficientPermissionsError());
}
if (
typeof req.body.name !== "string" ||
typeof req.body.date !== "string" ||
typeof req.body.description !== "string"
) {
res.status(400).json(
generateInvalidBodyError({
name: DataType.STRING,
date: DataType.DATETIME,
description: DataType.STRING,
})
);
return;
}
//TODO: Check if date is valid
@@ -102,9 +116,34 @@ export const addEvent = async (req: Request, res: Response) => {
});
res.status(201).json({
type: "succes",
type: "success",
payload: {
event,
},
});
};
interface DeleteEventQueryParams {
pid: string;
}
// requires: auth(ELEVATED)
export const deleteEvent = (req: Request<DeleteEventQueryParams>, res: Response) => {
if (req.auth?.permission_level !== "ELEVATED") {
res.status(403).json(createInsufficientPermissionsError());
}
const { pid } = req.params;
try {
prisma.event.delete({ where: { pid } });
return res.status(204).end();
} catch (e) {
if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") {
return res.status(404).json(generateError(`The organisation with the ID ${pid} could not be found`));
}
throw e;
}
};
+116
View File
@@ -0,0 +1,116 @@
import { Admin } from "@prisma/client";
import { PrismaClientUnknownRequestError } from "@prisma/client/runtime";
import { Request, Response } from "express";
import prisma from "../lib/prisma";
import { generateError, genericError, handleCreateByName } from "./common";
const basicGroup = {
pid: true,
name: true,
organisation: { select: { pid: true, name: true } },
} as const;
export const _getAllGroups = async (res: Response, organisationId: string | undefined) => {
const groups = await prisma.group.findMany({
where: { organisation: { pid: organisationId } },
select: basicGroup,
});
return res.status(200).json({
type: "success",
payload: {
groups,
},
});
};
interface GetAllGroupsSearchParams {
organisationPid?: string;
}
export const getAllGroups = async (req: Request<{}, {}, {}, GetAllGroupsSearchParams>, res: Response) => {
return _getAllGroups(res, req.query.organisationPid);
};
interface getAllGroupsWithParamQueryParams {
organisationPid: string;
}
export const getAllGroupsWithParam = async (req: Request<getAllGroupsWithParamQueryParams>, res: Response) => {
return _getAllGroups(res, req.params.organisationPid);
};
interface GetGroupQueryParams {
pid: string;
}
export const getGroup = async (req: Request<GetGroupQueryParams>, res: Response) => {
const { pid } = req.params;
try {
const group: {
pid: string;
name: string;
organisation: { pid: string; name: string };
admins?: { pid: string; name: string }[];
participants?: { pid: string }[];
} | null = await prisma.group.findUnique({
where: { pid },
select: req.auth?.isAuthenticated
? {
pid: true,
name: true,
organisation: { select: { pid: true, name: true } },
admins: { select: { pid: true, name: true } },
participants: { select: { pid: true } },
}
: basicGroup,
});
if (!group) {
return res.status(404).json(generateError(`The group with ID '${pid}' could not be found`));
}
return res.status(200).json({
type: "success",
payload: {
group: {
...group,
organisation: {
...group.organisation,
_links: [{ rel: "self", type: "GET", href: `/api/organisation/${group.organisation.pid}` }],
},
...(req.auth?.isAuthenticated
? {
admins: group.admins?.map((admin) => ({
...admin,
_links: [{ rel: "self", type: "GET", href: `/api/admins/${admin.pid}` }],
})),
participants: group.participants?.map((participant) => ({
...participant,
_links: [{ rel: "self", type: "GET", href: `/api/participant/${participant.pid}` }],
})),
}
: {}),
},
},
});
} catch (e) {
if (e instanceof PrismaClientUnknownRequestError) {
return res.status(400).json(generateError("Unknown error occured. This could be due to malformed IDs"));
}
}
return res.status(500).json(genericError);
};
// at: POST /api/organisations/:eventPid/groups
// requires: auth(ELEVATED)
export const createGroup = async (req: Request<{ organisationPid: string }, {}, { name?: string }>, res: Response) => {
return handleCreateByName(
{ type: "group", data: { name: req.body.name, level: 1 } },
{ type: "organisation", id: req.params.organisationPid },
req,
res
);
};
+233
View File
@@ -0,0 +1,233 @@
import { PrismaClientUnknownRequestError } from "@prisma/client/runtime";
import { Request, Response } from "express";
import prisma from "../lib/prisma";
import {
AUTH_ERROR,
createInsufficientPermissionsError,
DataType,
generateError,
generateInvalidBodyError,
genericError,
handleCreateByName,
} from "./common";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
import { Prisma } from "@prisma/client";
function validateOranisationName(name: string) {
return name.length > 0;
}
const detailedOrganisation = {
pid: true,
name: true,
event: {
select: {
pid: true,
name: true,
date: true,
description: true,
},
},
} as const;
const basicOrganisation = {
pid: true,
name: true,
event: {
select: {
pid: true,
name: true,
},
},
} as const;
export const _getAllOrganisations = async (res: Response, eventId: string | undefined = undefined) => {
const organisations = await prisma.organisation.findMany({
where: { event: { pid: eventId } },
select: basicOrganisation,
});
return res.status(200).json({
type: "success",
payload: {
organisations,
},
});
};
interface GetAllOrganisationsSearchParams {
eventId?: string;
}
export const getAllOrganisations = async (req: Request<{}, {}, {}, GetAllOrganisationsSearchParams>, res: Response) => {
// TODO: Maybe rename to eventPid
return _getAllOrganisations(res, req.query.eventId);
};
interface GetAllOrganisationsWithParamQueryParams {
eventPid: string;
}
export const getAllOrganisationsWithParam = async (
req: Request<GetAllOrganisationsWithParamQueryParams>,
res: Response
) => {
return _getAllOrganisations(res, req.params.eventPid);
};
interface GetOrganisationQueryParams {
pid: string;
}
export const getOrganisation = async (req: Request<GetOrganisationQueryParams>, res: Response) => {
const { pid } = req.params;
const organisation = await prisma.organisation.findUnique({
where: { pid },
select: detailedOrganisation,
});
if (!organisation) {
return res.status(404).json({
type: "error",
payload: {
message: `The organisation with ID '${pid} could not be found'`,
},
});
}
return res.status(200).json({
type: "success",
payload: {
organisation: {
...organisation,
event: {
...organisation.event,
_links: [{ rel: "self", type: "GET", href: `/api/event/${organisation.event.pid}` }],
},
},
},
});
};
interface CreateOrganisationQueryParams {
eventPid: string;
}
interface CreateOrganisationBody {
name?: string;
}
// at: POST /api/events/:eventPid/organisations
// requires: auth(ELEVATED)
export const createOrganisation = async (
req: Request<CreateOrganisationQueryParams, {}, CreateOrganisationBody>,
res: Response
) => {
return handleCreateByName(
{ type: "organisation", data: { name: req.body.name } },
{ type: "event", id: req.params.eventPid },
req,
res
);
};
interface UpdateOrganisationQueryParams {
pid: string;
}
interface UpdateOrganisationBody {
name?: string;
}
// requires: auth(ELEVATED)
export const updateOrganisation = async (
req: Request<UpdateOrganisationQueryParams, {}, UpdateOrganisationBody>,
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());
}
const { pid } = req.params;
const { name } = req.body;
if (name !== undefined && typeof name !== "string") {
return res.status(400).json(
generateInvalidBodyError({
name: DataType.STRING,
})
);
}
if (name !== undefined && !validateOranisationName(name)) {
return res.status(400).json({
type: "error",
payload: {
message: "The name has to be at least 1 character long",
},
});
}
try {
const organisation = await prisma.organisation.update({
where: { pid },
data: { name },
select: detailedOrganisation,
});
return res.status(200).json({
type: "success",
payload: {
organisation,
},
});
} catch (e) {
if (e instanceof PrismaClientKnownRequestError) {
if (e.code === "P2025") {
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);
};
interface DeleteOrganisationQueryParams {
pid: string;
}
// requires: auth(ELEVATED)
export const deleteOrganisation = async (req: Request<DeleteOrganisationQueryParams>, 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());
}
const { pid } = req.params;
try {
prisma.organisation.delete({ where: { pid } });
res.status(204).end();
} catch (e) {
if (e instanceof PrismaClientKnownRequestError) {
if ((e.code = "P2025")) {
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);
};
+14
View File
@@ -0,0 +1,14 @@
import express from "express";
import { createGroup, getAllGroups, getAllGroupsWithParam, getGroup } from "../Controllers/group.controllers";
import { requireAuthentication } from "../Middleware/auth/auth";
import organisationRouter from "./organisation.routes";
const router = express.Router();
router.get("/", getAllGroups);
router.get("/:pid", getGroup);
organisationRouter.get("/:organisationPid/groups", getAllGroupsWithParam);
organisationRouter.post("/:organisationPid/groups", requireAuthentication, createGroup);
export default router;
+25
View File
@@ -0,0 +1,25 @@
import express from "express";
import eventRouter from "./event.routes";
import {
createOrganisation,
getAllOrganisations,
getAllOrganisationsWithParam,
getOrganisation,
updateOrganisation,
} from "../Controllers/organisation.controller";
import { requireAuthentication } from "../Middleware/auth/auth";
const router = express.Router();
router.get("/", getAllOrganisations);
router.get("/:pid", getOrganisation);
router.put<"/:pid", { pid: string }>("/:pid", requireAuthentication, updateOrganisation);
eventRouter.get("/:eventPid/organisations", getAllOrganisationsWithParam);
eventRouter.post<"/:eventPid/organisations", { eventPid: string }>(
"/:eventPid/organisations",
requireAuthentication,
createOrganisation
);
export default router;
+6
View File
@@ -4,6 +4,9 @@ import eventRouter from "./Routes/event.routes";
import adminAuthRouter from "./Routes/admin_auth.routes";
import argon2 from "argon2";
import adminRouter from "./Routes/admin.routes";
import organisationRouter from "./Routes/organisation.routes";
import groupRouter from "./Routes/group.routes";
require("dotenv").config(); // Load dotenv config
@@ -46,6 +49,9 @@ async function main() {
app.use("/api/admins", adminRouter);
app.use("/api/organisations", organisationRouter);
app.use("/api/groups", groupRouter);
app.listen(process.env.PORT, () => {
console.log(`Listening on Port: ${process.env.PORT}`);
});