mirror of
https://github.com/detleph/server.git
synced 2026-09-04 08:36:06 +02:00
@@ -0,0 +1,2 @@
|
||||
*.env
|
||||
dist/
|
||||
@@ -4,9 +4,9 @@ name: Check prettier formatting
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [ main, dev ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
branches: [ main, dev ]
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
@@ -105,3 +105,9 @@ dist
|
||||
|
||||
# Prisma
|
||||
prisma/migrations
|
||||
|
||||
# Ignore config files for docker
|
||||
host/
|
||||
|
||||
# Ignore flags
|
||||
INITIALIZED
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "resources/email/templates"]
|
||||
path = resources/email/templates
|
||||
url = ../email-templates.git
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
# Dockerfile for the event management server
|
||||
|
||||
FROM node:17-alpine3.12
|
||||
|
||||
# Create directory for the app
|
||||
RUN mkdir /app
|
||||
|
||||
# Copy dependency files
|
||||
COPY package.json package-lock.json /app/
|
||||
|
||||
# Change directory into the container
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
RUN npm i -g typescript
|
||||
RUN npm i
|
||||
|
||||
# Selectively copy the required files and directories
|
||||
ADD prisma/ /app/prisma/
|
||||
ADD scripts/ /app/scripts/
|
||||
ADD src/ /app/src/
|
||||
ADD resources/ /app/resources
|
||||
COPY tsconfig.json /app/
|
||||
|
||||
# Generate the prisma client
|
||||
RUN npx prisma generate
|
||||
|
||||
# Compile TypeScript to JavaScript
|
||||
RUN tsc
|
||||
|
||||
CMD ["sh", "scripts/docker-entrypoint.sh"]
|
||||
@@ -9,5 +9,10 @@ Before Runningthis on you local machine some things have to be setup
|
||||
|
||||
```
|
||||
DATABASE_URL: ADDRESS TO YOUR DATABASE
|
||||
DATABASE_PASSWORD: PASSOWRD OF THE DATABASE
|
||||
PORT: PORT WHERE THE API SHOULD RUN
|
||||
DOMAIN: THE DOMAIN NAME OF THE SERVER
|
||||
MAILPASSWORD: THE PASSWORD FOR THE MAIL ACCOUNT
|
||||
DEV: SWITCH FOR DEV MODE AFFECTS EMAIL SERVER
|
||||
ALLOW_ORIGIN: ORIGIN OF THE PRODUCTION CLIENT (FOR CORS)
|
||||
```
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
#!/bin/bash
|
||||
D_SERVICES="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"
|
||||
|
||||
docker run -it \
|
||||
--name detleph_server_dev \
|
||||
--mount type=bind,source="$(pwd)",target=/app \
|
||||
--network detleph_server_default \
|
||||
-e DATABASE_PASSWORD=server \
|
||||
-e DATABASE_URL="postgresql://server:server@postgres:5432/management?schema=public" \
|
||||
-e NODE_ENV="development" \
|
||||
--entrypoint "/app/scripts/docker-entrypoint.dev.sh" \
|
||||
node
|
||||
fi
|
||||
COMPOSE_PROJECT_NAME=detleph_server docker-compose stop
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
version: "3.4"
|
||||
services:
|
||||
mail:
|
||||
image: boky/postfix
|
||||
restart: always
|
||||
environment:
|
||||
- ALLOWED_SENDER_DOMAINS=mail.${DOMAIN}
|
||||
ports:
|
||||
- "587:587"
|
||||
container_name: "postfix"
|
||||
volumes:
|
||||
- ./host/keys/:/etc/opendkim/keys
|
||||
postgres:
|
||||
image: postgres
|
||||
restart: always
|
||||
environment:
|
||||
- POSTGRES_USER=server
|
||||
- POSTGRES_PASSWORD=${DATABASE_PASSWORD}
|
||||
container_name: "postgres"
|
||||
redis:
|
||||
container_name: redis
|
||||
image: redis
|
||||
restart: always
|
||||
server:
|
||||
# REVIEW: This is setting the network context to host (during build time) which is generally considered a bad idea
|
||||
build:
|
||||
context: .
|
||||
network: host
|
||||
restart: always
|
||||
container_name: "server"
|
||||
ports:
|
||||
- ${PORT:-3000}:${PORT:-3000}
|
||||
environment:
|
||||
- PORT=${PORT:-3000}
|
||||
- DATABASE_URL=postgresql://server:${DATABASE_PASSWORD}@postgres:5432/management?schema=public
|
||||
- DATABASE_USER=server
|
||||
- DATABASE_PASSWORD=${DATABASE_PASSWORD}
|
||||
Generated
+3163
-64
File diff suppressed because it is too large
Load Diff
+25
-6
@@ -19,20 +19,39 @@
|
||||
},
|
||||
"homepage": "https://github.com/detleph/server#readme",
|
||||
"dependencies": {
|
||||
"@prisma/client": "^3.2.1",
|
||||
"@types/chai-http": "^4.2.0",
|
||||
"chai-http": "^4.3.0",
|
||||
"@prisma/client": "^3.3.0",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/handlebars": "^4.1.0",
|
||||
"@types/jsonwebtoken": "^8.5.5",
|
||||
"@types/mjml": "^4.7.0",
|
||||
"@types/node": "^16.10.3",
|
||||
"@types/nodemailer": "^6.4.4",
|
||||
"@types/redis": "^2.8.32",
|
||||
"argon2": "^0.28.2",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^10.0.0",
|
||||
"express": "^4.17.1"
|
||||
"express": "^4.17.1",
|
||||
"express-async-errors": "^3.1.1",
|
||||
"handlebars": "^4.7.7",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"mjml": "^4.11.0",
|
||||
"nanoid": "^3.3.3",
|
||||
"nodemailer": "^6.7.0",
|
||||
"redis": "^3.1.2",
|
||||
"winston": "^3.7.2",
|
||||
"zod": "^3.14.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.2.22",
|
||||
"@types/chai-as-promised": "^7.1.4",
|
||||
"@types/chai-http": "^4.2.0",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/node": "^16.10.3",
|
||||
"chai": "^4.3.4",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"chai-http": "^4.3.0",
|
||||
"mocha": "^9.1.3",
|
||||
"prisma": "^3.2.1",
|
||||
"prisma": "^3.3.0",
|
||||
"ts-node": "^10.2.1",
|
||||
"typescript": "^4.4.3"
|
||||
}
|
||||
|
||||
+139
-100
@@ -1,100 +1,139 @@
|
||||
// Schema for the main database
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
disciplines Discipline[]
|
||||
admins Admin[]
|
||||
organisations Oragnisation[]
|
||||
campaigns Campaign[]
|
||||
}
|
||||
|
||||
model Admin {
|
||||
id Int @id @default(autoincrement())
|
||||
pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
name String
|
||||
password String // TODO: Probably specify hash size (VarChar or some other type)
|
||||
permission_level Int // TODO: Maybe extract into an enum
|
||||
|
||||
event Event @relation(fields: [eventId], references: [id])
|
||||
eventId Int
|
||||
}
|
||||
|
||||
model Campaign {
|
||||
id Int @id @default(autoincrement())
|
||||
pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
expiresAt DateTime
|
||||
user_limit Int @default(40)
|
||||
user_number Int @default(0)
|
||||
links String[]
|
||||
|
||||
group Group @relation(fields: [groupId], references: [id])
|
||||
groupId Int
|
||||
event Event @relation(fields: [eventId], references: [id])
|
||||
eventId Int
|
||||
}
|
||||
|
||||
model Discipline {
|
||||
id Int @id @default(autoincrement())
|
||||
pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
name String
|
||||
minTeamSize Int @default(1)
|
||||
maxTeamSize Int @default(1)
|
||||
|
||||
teams Team[]
|
||||
event Event @relation(fields: [eventId], references: [id])
|
||||
eventId Int
|
||||
}
|
||||
|
||||
model Team {
|
||||
id Int @id @default(autoincrement())
|
||||
pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
name String
|
||||
|
||||
participants Participant[]
|
||||
discipline Discipline @relation(fields: [disciplineId], references: [id])
|
||||
disciplineId Int
|
||||
}
|
||||
|
||||
model Participant {
|
||||
id Int @id @default(autoincrement())
|
||||
pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
firstName String
|
||||
lastName String
|
||||
email String
|
||||
|
||||
team Team @relation(fields: [teamId], references: [id])
|
||||
teamId Int
|
||||
}
|
||||
|
||||
model Oragnisation {
|
||||
id Int @id @default(autoincrement())
|
||||
pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
name String
|
||||
|
||||
groups Group[]
|
||||
event Event @relation(fields: [eventId], references: [id])
|
||||
eventId Int
|
||||
}
|
||||
|
||||
model Group {
|
||||
id Int @id @default(autoincrement())
|
||||
pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
name String
|
||||
|
||||
oragnisation Oragnisation @relation(fields: [oragnisationId], references: [id])
|
||||
oragnisationId Int
|
||||
campaign Campaign[]
|
||||
}
|
||||
// Schema for the main database
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
disciplines Discipline[]
|
||||
organisations Organisation[]
|
||||
visual Media[]
|
||||
}
|
||||
|
||||
model Admin {
|
||||
id Int @id @default(autoincrement())
|
||||
pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
name String @unique
|
||||
password String // TODO: Probably specify hash size (VarChar or some other type)
|
||||
|
||||
permission_level AdminLevel @default(STANDARD)
|
||||
revision DateTime @default(now())
|
||||
groups Group[]
|
||||
}
|
||||
|
||||
model Discipline {
|
||||
id Int @id @default(autoincrement())
|
||||
pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
name String
|
||||
minTeamSize Int
|
||||
maxTeamSize Int
|
||||
|
||||
roles RoleSchema[]
|
||||
teams Team[]
|
||||
event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
|
||||
eventId Int
|
||||
visual Media[]
|
||||
}
|
||||
|
||||
model RoleSchema {
|
||||
id Int @id @default(autoincrement())
|
||||
pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
name String
|
||||
schema Json
|
||||
|
||||
roles Role[]
|
||||
discipline Discipline @relation(fields: [disciplineId], references: [id], onDelete: Cascade)
|
||||
disciplineId Int
|
||||
visual Media[]
|
||||
}
|
||||
|
||||
model Team {
|
||||
id Int @id @default(autoincrement())
|
||||
pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
name String
|
||||
leaderEmail String
|
||||
|
||||
roles Role[] @relation(name: "participants")
|
||||
discipline Discipline @relation(fields: [disciplineId], references: [id], onDelete: Cascade)
|
||||
disciplineId Int
|
||||
}
|
||||
|
||||
model Participant {
|
||||
id Int @id @default(autoincrement())
|
||||
pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
firstName String
|
||||
lastName String
|
||||
relevance Job
|
||||
|
||||
group Group @relation(fields: [groupId], references: [id], onDelete: Cascade)
|
||||
groupId Int
|
||||
roles Role[]
|
||||
}
|
||||
|
||||
model Role {
|
||||
id Int @id @default(autoincrement())
|
||||
pid String @unique @default(dbgenerated("gen_random_uuid()"))
|
||||
score String
|
||||
|
||||
participant Participant? @relation(fields: [participantId], references: [id], onDelete: SetNull)
|
||||
participantId Int?
|
||||
team Team @relation(name: "participants", fields: [teamId], references: [id], onDelete: Cascade)
|
||||
teamId Int
|
||||
schema RoleSchema @relation(fields: [schemaId], references: [id], onDelete: Cascade)
|
||||
schemaId Int
|
||||
}
|
||||
|
||||
model Organisation {
|
||||
id Int @id @default(autoincrement())
|
||||
pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
name String
|
||||
|
||||
groups Group[]
|
||||
event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
|
||||
eventId Int
|
||||
}
|
||||
|
||||
model Group {
|
||||
id Int @id @default(autoincrement())
|
||||
pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
name String
|
||||
user_limit Int @default(40)
|
||||
level Int
|
||||
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
organisationId Int
|
||||
participants Participant[]
|
||||
admins Admin[]
|
||||
}
|
||||
|
||||
model Media {
|
||||
id Int @id @default(autoincrement())
|
||||
pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
description String
|
||||
location String @unique
|
||||
|
||||
event Event[]
|
||||
discipline Discipline[]
|
||||
role RoleSchema[]
|
||||
}
|
||||
|
||||
enum AdminLevel {
|
||||
STANDARD
|
||||
ELEVATED
|
||||
}
|
||||
|
||||
enum Job {
|
||||
TEAMLEADER
|
||||
MEMBER
|
||||
}
|
||||
|
||||
Submodule
+1
Submodule resources/email/templates added at 387e49326d
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#! /bin/bash
|
||||
|
||||
|
||||
#load .env file
|
||||
|
||||
[[ -f .env ]] || { echo "`tput setaf 1`✗`tput sgr0` Could not find .env file are you in the correct directory (you should be in the server directory)"; exit 1;}
|
||||
|
||||
set -o allexport
|
||||
[[ -f .env ]] && source .env
|
||||
set +o allexport
|
||||
|
||||
#static variables
|
||||
NC='tput sgr0' #No Color
|
||||
GREEN='tput setaf 2'
|
||||
RED='tput setaf 1'
|
||||
|
||||
|
||||
#check dependencies
|
||||
command -v opendkim-genkey >/dev/null 2>&1 || { echo >&2 "`$RED`✗`$NC` Opendkim not installed install with your favorite package manager (opendkim-tools | opendkim-utils)"; exit 1;}
|
||||
|
||||
#generate dkim keys
|
||||
mkdir -p ./host/keys
|
||||
echo "`$GREEN`✓`$NC` Created folder"
|
||||
|
||||
cd ./host/keys
|
||||
opendkim-genkey -b 2048 -h rsa-sha256 -r -v --subdomains -s mail -d mail.$DOMAIN
|
||||
sed -i 's/h=rsa-sha256/sha256/' mail.txt
|
||||
mv mail.private mail.$DOMAIN.private
|
||||
mv mail.txt mail.$DOMAIN.txt
|
||||
|
||||
echo "`$GREEN`✓`$NC` Generated dkim keys"
|
||||
|
||||
exit 0
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
|
||||
if ! test -f INITIALIZED; then
|
||||
touch INITIALIZED
|
||||
|
||||
cd /app
|
||||
|
||||
npm i -g ts-node nodemon
|
||||
npm i
|
||||
npx prisma db push
|
||||
fi
|
||||
|
||||
nodemon --watch /app /app/src/app.ts
|
||||
@@ -0,0 +1,11 @@
|
||||
# Entry point for docker
|
||||
|
||||
if ! test -f INITIALIZED; then
|
||||
# Migrate the database
|
||||
# TODO: Update to use prisma migrate
|
||||
npx prisma db push
|
||||
touch INITIALIZED
|
||||
fi
|
||||
|
||||
# Start the server
|
||||
node ./dist/app.js
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#! /bin/bash
|
||||
|
||||
docker-compose up -d
|
||||
@@ -0,0 +1,212 @@
|
||||
import prisma from "../lib/prisma";
|
||||
import { Request, Response } from "express";
|
||||
import { AdminLevel } from "@prisma/client";
|
||||
import argon2 from "argon2";
|
||||
import { AUTH_ERROR, createInsufficientPermissionsError, DataType, generateInvalidBodyError } from "./common";
|
||||
import { authClient } from "../lib/redis";
|
||||
|
||||
export const regenerateRevision = async (pid: string) => {
|
||||
// TOOO: Add error handling
|
||||
const { revision } = await prisma.admin.update({
|
||||
where: { pid },
|
||||
data: { revision: new Date() },
|
||||
select: { revision: true },
|
||||
});
|
||||
|
||||
await authClient.set(pid, revision.toISOString());
|
||||
};
|
||||
|
||||
// requires: auth(elevated)
|
||||
export const getAllAdmins = 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());
|
||||
}
|
||||
|
||||
// TODO: Add exception handling
|
||||
const users = await prisma.admin.findMany({ select: { pid: true, name: true, permission_level: true } });
|
||||
|
||||
res.status(200).json({
|
||||
type: "success",
|
||||
payload: {
|
||||
users,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
interface CreateAdminBody {
|
||||
name?: string;
|
||||
password: string;
|
||||
permission_level: string;
|
||||
groups?: string[];
|
||||
}
|
||||
|
||||
const PERMISSION_LEVELS: readonly AdminLevel[] = ["ELEVATED", "STANDARD"]; // TODO: Enforce completeness
|
||||
|
||||
const isPermissionLevel = (level: string): level is AdminLevel => PERMISSION_LEVELS.includes(level as any);
|
||||
|
||||
// requires: auth(elevated)
|
||||
export const createAdmin = async (req: Request<{}, {}, CreateAdminBody>, 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, password, permission_level, groups } = req.body || {};
|
||||
|
||||
const groupsIsValid = groups ? groups.filter((e) => typeof e !== "string").length === 0 : true;
|
||||
|
||||
if (
|
||||
!(typeof name === "string" && typeof password == "string" && isPermissionLevel(permission_level) && groupsIsValid)
|
||||
) {
|
||||
return res.status(400).json(
|
||||
generateInvalidBodyError({
|
||||
name: DataType.STRING,
|
||||
password: DataType.STRING,
|
||||
permission_level: DataType.PERMISSION_LEVEL,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const password_hash = await argon2.hash(password, { type: argon2.argon2id });
|
||||
|
||||
// Check if all gropus exist
|
||||
for (const groupId of groups || []) {
|
||||
if (!(await prisma.group.findUnique({ where: { pid: groupId } }))) {
|
||||
return res.status(404).json({
|
||||
type: "error",
|
||||
payload: {
|
||||
message: `The group with ID '${groupId}' could not be found!`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Check for uniqueness of the name
|
||||
const user = await prisma.admin.create({
|
||||
data: {
|
||||
name,
|
||||
password: password_hash,
|
||||
permission_level,
|
||||
groups: { connect: groups?.map((group) => ({ pid: group })) },
|
||||
},
|
||||
select: { pid: true, name: true, permission_level: true },
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
type: "success",
|
||||
payload: {
|
||||
user,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Expects a valid username (Should be tested beforehand)
|
||||
const updatePasswordField = async (pid: string, new_password: string) => {
|
||||
const new_password_hash = await argon2.hash(new_password, { type: argon2.argon2id });
|
||||
|
||||
await prisma.admin.update({ where: { pid }, data: { password: new_password_hash } });
|
||||
};
|
||||
|
||||
interface UpdateForeignPasswordBody {
|
||||
new_password?: string;
|
||||
}
|
||||
|
||||
interface UpdateForeignPasswordQueryParams {
|
||||
pid: string;
|
||||
}
|
||||
|
||||
export const updateForeignPassword = async (
|
||||
req: Request<UpdateForeignPasswordQueryParams, {}, UpdateForeignPasswordBody>,
|
||||
res: Response
|
||||
) => {
|
||||
if (!req.auth?.isAuthenticated) {
|
||||
return res.status(500).json(AUTH_ERROR);
|
||||
}
|
||||
|
||||
if (typeof req.body.new_password !== "string") {
|
||||
return res.status(400).json(generateInvalidBodyError({ new_password: DataType.STRING }));
|
||||
}
|
||||
|
||||
const user_to_upate = await prisma.admin.findUnique({ where: { pid: req.params.pid } });
|
||||
|
||||
if (!user_to_upate) {
|
||||
// REVIEW: This allows potential attackers (which are authorized with some account)
|
||||
// to test account names
|
||||
return res.status(404).json({
|
||||
type: "error",
|
||||
payload: {
|
||||
message: "The requested user was not found",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (req.auth.permission_level == "ELEVATED" && user_to_upate.permission_level == "STANDARD") {
|
||||
await updatePasswordField(req.params.pid, req.body.new_password); // REVIEW: Should this be awaited?
|
||||
await regenerateRevision(req.params.pid);
|
||||
|
||||
res.status(200).json({
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
res.status(403).json({
|
||||
type: "error",
|
||||
payload: {
|
||||
message: "Operation not permitted; Try logging in as another user",
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
interface UpdatePasswordBody {
|
||||
password?: string;
|
||||
new_password?: string;
|
||||
}
|
||||
|
||||
// requires: auth
|
||||
export const updateOwnPassword = async (req: Request<{}, {}, UpdatePasswordBody>, res: Response) => {
|
||||
if (!req.auth?.isAuthenticated) {
|
||||
return res.status(500).json(AUTH_ERROR);
|
||||
}
|
||||
|
||||
const pid = req.auth.pid;
|
||||
|
||||
if (typeof req.body.password !== "string" || typeof req.body.new_password !== "string") {
|
||||
return res.status(400).json(generateInvalidBodyError({ password: DataType.STRING, new_password: DataType.STRING }));
|
||||
}
|
||||
|
||||
const user_to_upate = await prisma.admin.findUnique({ where: { pid } });
|
||||
|
||||
if (!user_to_upate) {
|
||||
// REVIEW: This allows potential attackers (which are authorized with some account)
|
||||
// to test account names
|
||||
return res.status(404).json({
|
||||
type: "error",
|
||||
payload: {
|
||||
message: "The requested user was not found",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (await argon2.verify(user_to_upate.password, req.body.password, { type: argon2.argon2id })) {
|
||||
await updatePasswordField(pid, req.body.new_password);
|
||||
await regenerateRevision(pid);
|
||||
|
||||
return res.status(200).json({
|
||||
type: "success",
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(401).json({
|
||||
type: "error",
|
||||
payload: {
|
||||
message: "The provided password is not valid",
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Admin, AdminLevel, Group } from "@prisma/client";
|
||||
import { authClient } from "../lib/redis";
|
||||
import prisma from "../lib/prisma";
|
||||
import argon2 from "argon2";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { DataType, generateInvalidBodyError } from "./common";
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || "secret";
|
||||
const TOKEN_EXPIRY = "4 days";
|
||||
|
||||
export interface AuthJWTPayload {
|
||||
pid: string;
|
||||
name: string;
|
||||
permission_level: AdminLevel;
|
||||
revision: string;
|
||||
groups: string[];
|
||||
}
|
||||
|
||||
function createAdminJWT(admin: Admin & { groups: Group[] }) {
|
||||
const payload: AuthJWTPayload = {
|
||||
pid: admin.pid,
|
||||
name: admin.name,
|
||||
permission_level: admin.permission_level,
|
||||
revision: admin.revision.toISOString(),
|
||||
groups: admin.groups.map((group) => group.pid),
|
||||
};
|
||||
|
||||
return jwt.sign(payload, JWT_SECRET, { expiresIn: TOKEN_EXPIRY });
|
||||
}
|
||||
|
||||
interface AuthenticateUserBody {
|
||||
name?: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export const authenticateUser = async (req: Request<{}, {}, AuthenticateUserBody>, res: Response) => {
|
||||
// TODO: Provide more useful error messages (Maybe express-validator?)
|
||||
|
||||
const { name, password } = req.body || {};
|
||||
|
||||
if (name == null || password == null) {
|
||||
return res.status(400).json(generateInvalidBodyError({ name: DataType.STRING, password: DataType.STRING }));
|
||||
}
|
||||
|
||||
const user = await prisma.admin.findFirst({ where: { name }, include: { groups: true } });
|
||||
|
||||
// REVIEW: It is possible to initiate a timing attack here (To see which users exist)
|
||||
// This should, however, not be of too much concern, as one can basically do nothing
|
||||
// with only the username
|
||||
|
||||
if (!user) {
|
||||
return res.status(403).json({
|
||||
type: "error",
|
||||
payload: {
|
||||
message: `The provided credentials are not valid`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (await argon2.verify(user.password, password, { type: argon2.argon2id })) {
|
||||
// Set password revision ID in redis
|
||||
await authClient.set(user.pid, user.revision.toISOString());
|
||||
|
||||
return res.status(200).json({
|
||||
type: "success",
|
||||
payload: {
|
||||
token: createAdminJWT(user),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(403).json({
|
||||
type: "error",
|
||||
payload: {
|
||||
message: "The provided credentials are not valid",
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,153 @@
|
||||
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",
|
||||
INTEGER = "integer",
|
||||
PERMISSION_LEVEL = "'ELEVATED' | 'STANDARD'",
|
||||
DATETIME = "ISOstring",
|
||||
UUID = "string",
|
||||
RESULT_SCHEMA = "result_schema",
|
||||
}
|
||||
|
||||
interface Body {
|
||||
[k: string]: DataType;
|
||||
}
|
||||
|
||||
export function generateInvalidBodyError(body: Body) {
|
||||
return {
|
||||
type: "error",
|
||||
payload: {
|
||||
message: "The body of your request did not conform to the requirements",
|
||||
schema: { 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 const NAME_ERROR = {
|
||||
type: "error",
|
||||
payload: {
|
||||
message: "The name has to be at least 1 character long",
|
||||
},
|
||||
};
|
||||
|
||||
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(NAME_ERROR);
|
||||
}
|
||||
|
||||
// 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 } });
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import { Prisma, Organisation, Admin, AdminLevel, Team } from "@prisma/client";
|
||||
import { PrismaClientKnownRequestError, PrismaClientUnknownRequestError } from "@prisma/client/runtime";
|
||||
import { Request, Response } from "express";
|
||||
import prisma from "../lib/prisma";
|
||||
import ForwardableError from "../Middleware/error/ForwardableError";
|
||||
import NotFoundError from "../Middleware/error/NotFoundError";
|
||||
import {
|
||||
createInsufficientPermissionsError,
|
||||
DataType,
|
||||
generateError,
|
||||
generateInvalidBodyError,
|
||||
NAME_ERROR,
|
||||
validateName,
|
||||
} from "./common";
|
||||
|
||||
require("express-async-errors");
|
||||
|
||||
const basicDiscipline = {
|
||||
pid: true,
|
||||
name: true,
|
||||
visual: { select: { pid: true, location: true } },
|
||||
maxTeamSize: true,
|
||||
minTeamSize: true,
|
||||
event: { select: { pid: true, name: true } },
|
||||
roles: { select: { pid: true, name: true } },
|
||||
} as const;
|
||||
|
||||
const authenticatedDiscipline = {
|
||||
...basicDiscipline,
|
||||
teams: { select: { pid: true, name: true } },
|
||||
} as const;
|
||||
|
||||
const elevatedDiscipline: Prisma.DisciplineFindManyArgs["select"] = {
|
||||
...authenticatedDiscipline,
|
||||
teams: { select: { pid: true, name: true, leaderEmail: true } },
|
||||
};
|
||||
|
||||
export const _getAllDisciplines = async (
|
||||
res: Response,
|
||||
authenticated: boolean | undefined,
|
||||
eventId: string | undefined
|
||||
) => {
|
||||
const disciplines = await prisma.discipline.findMany({
|
||||
where: { event: { pid: eventId } },
|
||||
select: !authenticated ? basicDiscipline : authenticatedDiscipline,
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
type: "success",
|
||||
payload: {
|
||||
disciplines,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
interface GetAllDisciplinesSearchParams {
|
||||
eventPid?: string;
|
||||
}
|
||||
|
||||
export const getAllDisciplines = async (req: Request<{}, {}, {}, GetAllDisciplinesSearchParams>, res: Response) => {
|
||||
return _getAllDisciplines(res, req.auth?.isAuthenticated, req.query.eventPid);
|
||||
};
|
||||
|
||||
interface GetAllDisciplinesQueryParams {
|
||||
eventPid: string;
|
||||
}
|
||||
|
||||
export const GetAllDisciplinesWithParam = async (req: Request<GetAllDisciplinesQueryParams>, res: Response) => {
|
||||
return _getAllDisciplines(res, req.auth?.isAuthenticated, req.params.eventPid);
|
||||
};
|
||||
|
||||
interface GetDisciplineQueryParams {
|
||||
pid: string;
|
||||
}
|
||||
|
||||
export const getDiscipline = async (req: Request<GetDisciplineQueryParams>, res: Response) => {
|
||||
const { pid } = req.params;
|
||||
|
||||
const discipline = await prisma.discipline.findUnique({
|
||||
where: { pid },
|
||||
select: !req.auth?.isAuthenticated
|
||||
? basicDiscipline
|
||||
: req.auth.permission_level !== "ELEVATED"
|
||||
? authenticatedDiscipline
|
||||
: elevatedDiscipline,
|
||||
});
|
||||
|
||||
if (!discipline) {
|
||||
throw new NotFoundError("discipline", pid);
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
type: "success",
|
||||
payload: {
|
||||
discipline: {
|
||||
...discipline,
|
||||
event: {
|
||||
...discipline.event,
|
||||
_links: [{ rel: "self", type: "GET", href: `/api/events/${discipline.event.pid}` }],
|
||||
},
|
||||
roles: discipline.roles.map((role) => ({
|
||||
...role,
|
||||
_links: [{ rel: "self", type: "GET", href: `/api/role/${role.pid}` }],
|
||||
})),
|
||||
...((discipline as any).teams
|
||||
? (discipline as any).teams.map((team: Team) => ({
|
||||
...team,
|
||||
_links: [{ rel: "self", type: "GET", href: `/api/teams/${team.pid}` }],
|
||||
}))
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
interface CreateDisciplineBody {
|
||||
name?: string;
|
||||
minTeamSize?: number;
|
||||
maxTeamSize?: number;
|
||||
}
|
||||
|
||||
// require: auth(ELEVATED)
|
||||
// at: POST /event/:eventPid/discipliens
|
||||
export const createDiscipline = async (req: Request<{ eventPid: string }, {}, CreateDisciplineBody>, res: Response) => {
|
||||
if (req.auth?.permission_level !== "ELEVATED") {
|
||||
return res.status(403).json(createInsufficientPermissionsError());
|
||||
}
|
||||
|
||||
const { name, minTeamSize, maxTeamSize } = req.body;
|
||||
|
||||
if (typeof name !== "string" || typeof minTeamSize !== "number" || typeof maxTeamSize !== "number") {
|
||||
return res.status(400).json(
|
||||
generateInvalidBodyError({
|
||||
name: DataType.STRING,
|
||||
minTeamSize: DataType.NUMBER,
|
||||
maxTeamSize: DataType.NUMBER,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (!validateName(name)) {
|
||||
return res.status(400).json(NAME_ERROR);
|
||||
}
|
||||
|
||||
try {
|
||||
const discipline = await prisma.discipline.create({
|
||||
data: { name, minTeamSize, maxTeamSize, event: { connect: { pid: req.params.eventPid } } },
|
||||
select: {
|
||||
pid: true,
|
||||
name: true,
|
||||
minTeamSize: true,
|
||||
maxTeamSize: true,
|
||||
event: { select: { pid: true, name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return res.status(201).json({ type: "success", payload: { discipline } });
|
||||
} catch (e) {
|
||||
if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") {
|
||||
return res.status(404).json(generateError(`Could not link to event with ID '${req.params.eventPid}'`));
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
interface DeleteDisciplineQueryParams {
|
||||
pid: string;
|
||||
}
|
||||
|
||||
// requires: auth(ELEVATED)
|
||||
export const deleteDiscipline = async (req: Request<DeleteDisciplineQueryParams>, res: Response) => {
|
||||
if (req.auth?.permission_level !== "ELEVATED") {
|
||||
res.status(403).json(createInsufficientPermissionsError());
|
||||
}
|
||||
|
||||
const { pid } = req.params;
|
||||
|
||||
try {
|
||||
await prisma.discipline.delete({ where: { pid } });
|
||||
|
||||
return res.status(204).end();
|
||||
} catch (e) {
|
||||
if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") {
|
||||
throw new NotFoundError("discipline", pid);
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
@@ -1,40 +1,147 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
|
||||
import { Request, Response } from "express";
|
||||
import prisma from "../lib/prisma";
|
||||
import { createInsufficientPermissionsError, DataType, generateError, generateInvalidBodyError } from "./common";
|
||||
|
||||
const getAllEvents = async (req: Request, res: Response) => {
|
||||
export const getAllEvents = async (req: Request, res: Response) => {
|
||||
const events = await prisma.event.findMany({
|
||||
select: {
|
||||
name: true,
|
||||
description: true,
|
||||
date: true,
|
||||
pid: true,
|
||||
id: false,
|
||||
},
|
||||
});
|
||||
if (events.length > 0) res.status(200).send(events);
|
||||
else res.status(204).send();
|
||||
|
||||
res.status(200).json({
|
||||
type: "success",
|
||||
payload: {
|
||||
events,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const addEvent = async (req: Request, res: Response) => {
|
||||
// TODO: Add checks if user has admin permissions
|
||||
export const getEvent = async (req: Request, res: Response) => {
|
||||
const eventId = req.params.eventId;
|
||||
|
||||
if (req.body.name == null || undefined || req.body.date == null || undefined) {
|
||||
res.status(400).send("Malformed request");
|
||||
if (typeof eventId !== "string") {
|
||||
res.status(400).json(
|
||||
generateInvalidBodyError({
|
||||
eventId: DataType.UUID,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const event = await prisma.event.findUnique({
|
||||
where: {
|
||||
pid: eventId,
|
||||
},
|
||||
select: {
|
||||
name: true,
|
||||
description: true,
|
||||
date: true,
|
||||
pid: true,
|
||||
id: false,
|
||||
},
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
type: "success",
|
||||
payload: {
|
||||
event,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
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({
|
||||
type: "error",
|
||||
payload: {
|
||||
message: "Unknown error occurred with your request. Check if your parameters are correct",
|
||||
schema: {
|
||||
eventId: DataType.UUID,
|
||||
},
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 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"
|
||||
) {
|
||||
generateInvalidBodyError({
|
||||
name: DataType.STRING,
|
||||
date: DataType.DATETIME,
|
||||
description: DataType.STRING,
|
||||
});
|
||||
}
|
||||
|
||||
//TODO: Check if date is valid
|
||||
|
||||
const event = await prisma.event.create({
|
||||
data: {
|
||||
name: req.body.name,
|
||||
date: req.body.date,
|
||||
description: req.body.description,
|
||||
},
|
||||
select: {
|
||||
name: true,
|
||||
date: true,
|
||||
pid: true,
|
||||
id: false,
|
||||
description: true,
|
||||
},
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
type: "success",
|
||||
payload: {
|
||||
event,
|
||||
},
|
||||
});
|
||||
res.status(201).send(event);
|
||||
};
|
||||
|
||||
export { getAllEvents, addEvent };
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { Admin } from "@prisma/client";
|
||||
import { PrismaClientKnownRequestError, PrismaClientUnknownRequestError } from "@prisma/client/runtime";
|
||||
import { Request, Response } from "express";
|
||||
import prisma from "../lib/prisma";
|
||||
import { createInsufficientPermissionsError, 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
|
||||
);
|
||||
};
|
||||
|
||||
interface DeleteGroupQueryParams {
|
||||
pid: string;
|
||||
}
|
||||
|
||||
export const deleteGroup = async (req: Request<DeleteGroupQueryParams>, res: Response) => {
|
||||
if (req.auth?.permission_level !== "ELEVATED") {
|
||||
return res.status(403).json(createInsufficientPermissionsError());
|
||||
}
|
||||
|
||||
const { pid } = req.params;
|
||||
|
||||
try {
|
||||
prisma.group.delete({ where: { pid } });
|
||||
|
||||
return res.status(204).end();
|
||||
} catch (e) {
|
||||
if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") {
|
||||
return res.status(404).json(generateError(`The group with the ID ${pid} could not be found`));
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -0,0 +1,125 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
|
||||
import { Request, Response } from "express";
|
||||
import prisma from "../lib/prisma";
|
||||
import { DurationSchemaT, parseSchema, PointSchemaT } from "../lib/result_schema";
|
||||
import NotFoundError from "../Middleware/error/NotFoundError";
|
||||
import SchemaError from "../Middleware/error/SchemaError";
|
||||
import {
|
||||
createInsufficientPermissionsError,
|
||||
DataType,
|
||||
generateInvalidBodyError,
|
||||
NAME_ERROR,
|
||||
validateName,
|
||||
} from "./common";
|
||||
|
||||
const roleSchema = {
|
||||
name: true,
|
||||
schema: true,
|
||||
discipline: { select: { pid: true, name: true } },
|
||||
visual: { select: { pid: true, location: true } },
|
||||
} as const;
|
||||
|
||||
export const _getAllRoleSchemas = async (res: Response, disciplinePid: string | undefined) => {
|
||||
const schemas = await prisma.roleSchema.findMany({
|
||||
where: { discipline: { pid: disciplinePid } },
|
||||
select: roleSchema,
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
type: "success",
|
||||
payload: {
|
||||
roleSchemas: schemas,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
interface GetAllRoleSchemasSearchParams {
|
||||
disciplinePid?: string;
|
||||
}
|
||||
|
||||
export const getAllRoleSchemas = async (req: Request<{}, {}, {}, GetAllRoleSchemasSearchParams>, res: Response) => {
|
||||
return _getAllRoleSchemas(res, req.query.disciplinePid);
|
||||
};
|
||||
|
||||
interface GetAllRoleSchemasWithParamQueryParams {
|
||||
organisationPid: string;
|
||||
}
|
||||
|
||||
export const getAllRoleSchemasWithParam = async (
|
||||
req: Request<GetAllRoleSchemasWithParamQueryParams>,
|
||||
res: Response
|
||||
) => {
|
||||
return _getAllRoleSchemas(res, req.params.organisationPid);
|
||||
};
|
||||
|
||||
interface GetRoleSchemaQueryParams {
|
||||
pid: string;
|
||||
}
|
||||
|
||||
export const getRoleSchema = async (req: Request<GetRoleSchemaQueryParams>, res: Response) => {
|
||||
const { pid } = req.params;
|
||||
|
||||
const schema = await prisma.roleSchema.findUnique({ where: { pid }, select: roleSchema });
|
||||
|
||||
if (!schema) {
|
||||
throw new NotFoundError("roleSchema", pid);
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
type: "success",
|
||||
payload: {
|
||||
roleSchema: {
|
||||
...schema,
|
||||
discipline: {
|
||||
...schema.discipline,
|
||||
_links: [{ rel: "self", type: "GET", href: `/api/disciplines/${schema.discipline.pid}` }],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
interface CreateRoleSchemaBody {
|
||||
name?: string;
|
||||
schema?: any;
|
||||
}
|
||||
|
||||
// at: POST /discipline/:disciplinePid/role-schemas
|
||||
// requires: auth(ELEVATED)
|
||||
export const createRoleSchema = async (
|
||||
req: Request<{ disciplinePid: string }, {}, CreateRoleSchemaBody>,
|
||||
res: Response
|
||||
) => {
|
||||
if (req.auth?.permission_level !== "ELEVATED") {
|
||||
return res.status(403).json(createInsufficientPermissionsError());
|
||||
}
|
||||
|
||||
const { name, schema: resultSchema } = req.body;
|
||||
|
||||
if (typeof name !== "string") {
|
||||
return res.status(400).json(generateInvalidBodyError({ name: DataType.STRING, schema: DataType.RESULT_SCHEMA }));
|
||||
}
|
||||
|
||||
if (!validateName(name)) {
|
||||
return res.status(400).json(NAME_ERROR);
|
||||
}
|
||||
|
||||
// Validate the result schema (Errors should be handled by the default error handler)
|
||||
const validatedSchema = parseSchema(resultSchema);
|
||||
|
||||
try {
|
||||
const schema = await prisma.roleSchema.create({
|
||||
data: { name, schema: validatedSchema, discipline: { connect: { pid: req.params.disciplinePid } } },
|
||||
select: roleSchema,
|
||||
});
|
||||
|
||||
return res.status(201).json({ type: "success", payload: { roleSchema: schema } });
|
||||
} catch (e) {
|
||||
if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") {
|
||||
throw new NotFoundError("discipline", req.params.disciplinePid);
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Request, Response } from "express";
|
||||
import prisma from "../lib/prisma";
|
||||
import { mailClient } from "../lib/redis";
|
||||
import { nanoid } from "nanoid";
|
||||
import { verificationMail } from "../lib/mail";
|
||||
|
||||
export const register = async (req: Request, res: Response) => {
|
||||
//TODO: Implemnt user endpoint and use following code to send verification mail
|
||||
|
||||
const user = {
|
||||
//Supposed to come from database
|
||||
id: "10",
|
||||
email: "[email protected]",
|
||||
};
|
||||
|
||||
const usid = nanoid();
|
||||
|
||||
(await mailClient).set(usid, user.id);
|
||||
|
||||
verificationMail(user.email, "eventname", usid);
|
||||
|
||||
//Send status code
|
||||
};
|
||||
|
||||
export const verifyEmail = async (req: Request, res: Response) => {
|
||||
const { code } = req.body || {};
|
||||
|
||||
if (!(typeof code === "string")) {
|
||||
return res.status(400).json({
|
||||
type: "error",
|
||||
payload: {
|
||||
message: "Invalid Request parameter",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const acc = await mailClient.get(code);
|
||||
|
||||
if (!acc) {
|
||||
return res.status(404).json({
|
||||
type: "error",
|
||||
payload: {
|
||||
message: "Invalid token. It might be expired",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
prisma.participant.update({
|
||||
where: {
|
||||
id: parseInt(acc),
|
||||
},
|
||||
data: {
|
||||
verified: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
/// <reference path="../../custom.d.ts" />
|
||||
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { AuthJWTPayload } from "../../Controllers/admin_auth.controller";
|
||||
import { authClient } from "../../lib/redis";
|
||||
import jwt, { JsonWebTokenError, JwtPayload } from "jsonwebtoken";
|
||||
import prisma from "../../lib/prisma";
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || "secret";
|
||||
|
||||
const verifyAuthorizationFormat = (authorization: string) => /^Bearer .+$/.test(authorization);
|
||||
|
||||
const getBearerToken = (authorization: string) => authorization.slice(7);
|
||||
|
||||
export const requireAuthentication = async (req: Request, res: Response, next: NextFunction) => {
|
||||
const { authorization } = req.headers;
|
||||
|
||||
if (!authorization) {
|
||||
return res.status(403).send({
|
||||
type: "error",
|
||||
payload: {
|
||||
message: "The requeset did not include the Authorization header",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!verifyAuthorizationFormat(authorization)) {
|
||||
return res.status(400).send({
|
||||
type: "error",
|
||||
payload: {
|
||||
message: "Malformed Authorization header",
|
||||
format: "Bearer <token>",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let token_payload_: string | JwtPayload;
|
||||
|
||||
try {
|
||||
token_payload_ = jwt.verify(getBearerToken(authorization), JWT_SECRET);
|
||||
} catch (e) {
|
||||
if (e instanceof JsonWebTokenError) {
|
||||
return res.status(403).json({
|
||||
type: "error",
|
||||
payload: {
|
||||
message: "Token could not be verified; It might be expired",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
const token_payload = token_payload_ as AuthJWTPayload;
|
||||
|
||||
const { pid, revision } = token_payload;
|
||||
|
||||
let db_revision = await authClient.get(pid);
|
||||
|
||||
if (db_revision === null) {
|
||||
// Load the revision ID from the main DB and cache it in redis
|
||||
const user = await prisma.admin.findUnique({ where: { pid }, select: { revision: true } });
|
||||
|
||||
if (user) {
|
||||
db_revision = user.revision.toISOString();
|
||||
|
||||
await authClient.set(pid, db_revision);
|
||||
}
|
||||
}
|
||||
|
||||
if (revision !== db_revision || !revision || !db_revision) {
|
||||
return res.status(403).json({
|
||||
type: "error",
|
||||
payload: {
|
||||
message: "Token could not be verified; It might be expired",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
req.auth = {
|
||||
isAuthenticated: true,
|
||||
pid: token_payload.pid,
|
||||
name: token_payload.name,
|
||||
permission_level: token_payload.permission_level,
|
||||
groups: token_payload.groups,
|
||||
revision: token_payload.revision,
|
||||
};
|
||||
|
||||
next();
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import logger from "../error/logger";
|
||||
|
||||
export default function debugLogger(req: Request, res: Response, next: NextFunction) {
|
||||
// Only log when in development mode
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
logger.debug(`Request to: ${req.url}`);
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export default class ForwardableError extends Error {
|
||||
// If in different context
|
||||
private readonly __id = "CUSTOM_ERROR";
|
||||
protected readonly __oid?: string;
|
||||
|
||||
public readonly status: number;
|
||||
|
||||
constructor(status: number, message: string) {
|
||||
super(message);
|
||||
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
static isForwardableError(error: any): error is ForwardableError {
|
||||
return error.__id === "CUSTOM_ERROR";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import ForwardableError from "./ForwardableError";
|
||||
|
||||
export default class NotFoundError extends ForwardableError {
|
||||
protected __oid = "NOT_FOUND_ERROR";
|
||||
|
||||
constructor(resource?: string, pid?: string) {
|
||||
super(404, `The requested ${resource ?? "resource"}${pid ? ` with PID '${pid}'` : ""} could not be found!`);
|
||||
}
|
||||
|
||||
static isNotFoundError(err: any): err is NotFoundError {
|
||||
return err.__oid === "NOT_FOUND_ERROR";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import ForwardableError from "./ForwardableError";
|
||||
|
||||
export default class SchemaError extends ForwardableError {
|
||||
protected __oid = "SCHEMA_ERROR";
|
||||
|
||||
constructor(message: string) {
|
||||
super(400, message);
|
||||
}
|
||||
|
||||
public static isSchemaError(err: any): err is SchemaError {
|
||||
return err.__oid === "SCHEMA_ERROR";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Request, Response } from "express";
|
||||
|
||||
// Only called when no other route matches
|
||||
export function notFoundHandler(req: Request, res: Response) {
|
||||
return res.status(404).json({
|
||||
type: "error",
|
||||
payload: {
|
||||
message: `The ${req.method} HTTP method is implemented for '${req.path}'`,
|
||||
_links: [
|
||||
{
|
||||
rel: "root",
|
||||
href: "/api",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function rootHandler(req: Request, res: Response) {
|
||||
return res.status(200).json({
|
||||
type: "success",
|
||||
payload: {
|
||||
message: "Detleph event API",
|
||||
detail: "This is the API for the Detleph event system",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { PrismaClientUnknownRequestError } from "@prisma/client/runtime";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import ForwardableError from "./ForwardableError";
|
||||
import logger from "./logger";
|
||||
|
||||
const env = process.env.NODE_ENV || "production";
|
||||
|
||||
type ErrorHandler = (err: any, req: Request, res: Response) => boolean;
|
||||
|
||||
const customHandlers: ErrorHandler[] = [];
|
||||
|
||||
export function addCustomHandler(handler: ErrorHandler) {
|
||||
customHandlers.push(handler);
|
||||
}
|
||||
|
||||
export function removeCustomHandler(handler: ErrorHandler): Boolean {
|
||||
const index = customHandlers.findIndex((h) => h === handler);
|
||||
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
customHandlers.splice(index);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export default function defaultErrorHandler(err: any, req: Request, res: Response, next: NextFunction) {
|
||||
for (const handler of customHandlers) {
|
||||
// Run custom handler
|
||||
if (handler(err, req, res)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (ForwardableError.isForwardableError(err)) {
|
||||
return res.status(err.status).json({
|
||||
type: "error",
|
||||
payload: {
|
||||
message: err.message,
|
||||
...(env === "development"
|
||||
? {
|
||||
stack: err.stack,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (err instanceof PrismaClientUnknownRequestError) {
|
||||
logger.warning(err);
|
||||
|
||||
return res.status(404).json({
|
||||
type: "error",
|
||||
payload: {
|
||||
message: "An unknown error occured. This could be due to malformed IDs",
|
||||
...(env === "development"
|
||||
? {
|
||||
prisma: err.message,
|
||||
stack: err.stack,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
logger.error("--- Unhandled error ---");
|
||||
logger.error(err);
|
||||
|
||||
return res.status(err.status).json({
|
||||
type: "error",
|
||||
payload: {
|
||||
message: err.message,
|
||||
...(env === "development"
|
||||
? {
|
||||
notice: "This error was not caught by any handler, please add handling!",
|
||||
stack: err.stack,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import winston, { createLogger } from "winston";
|
||||
|
||||
const {
|
||||
format: { printf, colorize, combine, timestamp, json, errors, prettyPrint },
|
||||
} = winston;
|
||||
|
||||
const defaultJsonFormat = combine(errors({ stack: true }), timestamp(), json({ space: 2 }));
|
||||
|
||||
const customCLIFormat = printf(({ level, message, label, timestamp, stack }) => {
|
||||
let output = `${level}${stack ? `(1/2:message)` : ""} at ${timestamp}${label ? ` (#${label})` : ""}: ${message}${
|
||||
stack ? "\n" : ""
|
||||
}`;
|
||||
|
||||
if (stack) {
|
||||
output += `\n${level}(2/2:stack)${label ? ` (#${label})` : ""}: ${stack}\n\n`;
|
||||
}
|
||||
|
||||
return output;
|
||||
});
|
||||
|
||||
export default createLogger({
|
||||
levels: winston.config.syslog.levels,
|
||||
format: combine(errors({ stack: true }), timestamp()),
|
||||
transports: [
|
||||
new winston.transports.Console({
|
||||
level: "debug",
|
||||
format: combine(timestamp(), colorize(), customCLIFormat),
|
||||
}),
|
||||
new winston.transports.File({
|
||||
filename: "/app/error.log",
|
||||
level: "error",
|
||||
format: defaultJsonFormat,
|
||||
}),
|
||||
new winston.transports.File({
|
||||
level: "debug",
|
||||
filename: "/app/debug.log",
|
||||
format: defaultJsonFormat,
|
||||
silent: !(process.env.NODE_ENV === "development"), // Silent when not in development
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import express from "express";
|
||||
import { getAllAdmins, createAdmin, updateOwnPassword, updateForeignPassword } from "../Controllers/admin.controller";
|
||||
import { requireAuthentication } from "../Middleware/auth/auth";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/", requireAuthentication, getAllAdmins);
|
||||
|
||||
router.post("/", requireAuthentication, createAdmin);
|
||||
|
||||
router.put("/current/password", requireAuthentication, updateOwnPassword);
|
||||
router.put<"/:pid/password", { pid: string }>("/:pid/password", requireAuthentication, updateForeignPassword);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,8 @@
|
||||
import express from "express";
|
||||
import { authenticateUser } from "../Controllers/admin_auth.controller";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post("/", authenticateUser);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,19 @@
|
||||
import express from "express";
|
||||
import eventRouter from "./event.routes";
|
||||
import {
|
||||
createDiscipline,
|
||||
deleteDiscipline,
|
||||
getAllDisciplines,
|
||||
getDiscipline,
|
||||
} from "../Controllers/discipline.controller";
|
||||
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);
|
||||
|
||||
eventRouter.post("/:eventPid/disciplines", requireAuthentication, createDiscipline);
|
||||
|
||||
export default router;
|
||||
@@ -1,9 +1,12 @@
|
||||
import Express from "express";
|
||||
import { addEvent, getAllEvents } from "../Controllers/event.controller";
|
||||
import { addEvent, getAllEvents, getEvent } from "../Controllers/event.controller";
|
||||
import { requireAuthentication } from "../Middleware/auth/auth";
|
||||
const router = Express.Router();
|
||||
|
||||
router.get("/", getAllEvents);
|
||||
|
||||
router.post("/", addEvent);
|
||||
router.get("/:eventId", getEvent);
|
||||
|
||||
router.post("/", requireAuthentication, addEvent);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import express from "express";
|
||||
import {
|
||||
createGroup,
|
||||
deleteGroup,
|
||||
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);
|
||||
router.delete<"/:pid", { pid: string }>("/:pid", requireAuthentication, deleteGroup);
|
||||
|
||||
organisationRouter.get("/:organisationPid/groups", getAllGroupsWithParam);
|
||||
organisationRouter.post("/:organisationPid/groups", requireAuthentication, createGroup);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,27 @@
|
||||
import express from "express";
|
||||
import eventRouter from "./event.routes";
|
||||
import {
|
||||
createOrganisation,
|
||||
deleteOrganisation,
|
||||
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);
|
||||
router.delete<"/:pid", { pid: string }>("/:pid", requireAuthentication, deleteOrganisation);
|
||||
|
||||
eventRouter.get("/:eventPid/organisations", getAllOrganisationsWithParam);
|
||||
eventRouter.post<"/:eventPid/organisations", { eventPid: string }>(
|
||||
"/:eventPid/organisations",
|
||||
requireAuthentication,
|
||||
createOrganisation
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,19 @@
|
||||
import express from "express";
|
||||
import disciplineRouter from "./discipline.routes";
|
||||
import {
|
||||
createRoleSchema,
|
||||
getAllRoleSchemas,
|
||||
getAllRoleSchemasWithParam,
|
||||
getRoleSchema,
|
||||
} from "../Controllers/role_schema.controller";
|
||||
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);
|
||||
|
||||
export default router;
|
||||
@@ -28,7 +28,7 @@ describe("events", () => {
|
||||
it("should return an array with all the objects", (done) => {
|
||||
prisma.event
|
||||
.create({
|
||||
data: { date: new Date(Date.now()).toISOString(), name: "yes" },
|
||||
data: { date: new Date(Date.now()).toISOString(), name: "yes", description: "Hallo hansi" },
|
||||
})
|
||||
.then((a) => {
|
||||
chai
|
||||
@@ -62,6 +62,7 @@ describe("events", () => {
|
||||
res.body.should.have.property("pid");
|
||||
res.body.should.have.property("name");
|
||||
res.body.should.have.property("date");
|
||||
res.body.should.have.property("description");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
process.env.NODE_ENV = "test";
|
||||
|
||||
import chai, { expect } from "chai";
|
||||
import chaiaspromised from "chai-as-promised";
|
||||
import mail from "../lib/mail";
|
||||
|
||||
chai.use(chaiaspromised);
|
||||
const should = chai.should();
|
||||
|
||||
describe("mail", () => {
|
||||
it("should succesfully send a email", async () => {
|
||||
//NOTE THESE TESTS ONLY WORK WITH DEV BEING TRUE
|
||||
const info = await mail.sendMail('"Chai Testing suite" <[email protected]>', "[email protected]", "Hello", "Test");
|
||||
info.rejected.length.should.eq(0);
|
||||
info.accepted.length.should.eq(1);
|
||||
});
|
||||
});
|
||||
+80
-2
@@ -1,28 +1,106 @@
|
||||
import express from "express";
|
||||
import express, { ErrorRequestHandler, NextFunction, Request, Response } from "express";
|
||||
import prisma from "./lib/prisma";
|
||||
import eventRouter from "./Routes/event.routes";
|
||||
import adminAuthRouter from "./Routes/admin_auth.routes";
|
||||
import argon2 from "argon2";
|
||||
import cors from "cors";
|
||||
import adminRouter from "./Routes/admin.routes";
|
||||
import organisationRouter from "./Routes/organisation.routes";
|
||||
import groupRouter from "./Routes/group.routes";
|
||||
import disciplineRouter from "./Routes/discipline.routes";
|
||||
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 { notFoundHandler, rootHandler } from "./Middleware/error/defaultRoutes";
|
||||
|
||||
// Set up async error handling
|
||||
require("express-async-errors");
|
||||
|
||||
require("dotenv").config(); // Load dotenv config
|
||||
|
||||
const app = express();
|
||||
|
||||
async function main() {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
logger.info("Using development mode");
|
||||
logger.warning(
|
||||
"This mode should not be used in any production-near environment as it is significantly less secure than the production mode"
|
||||
);
|
||||
|
||||
// TODO: How should you login to the prod server by default? Maybe random password?
|
||||
await prisma.admin.upsert({
|
||||
where: { id: 1 },
|
||||
create: {
|
||||
name: "admin",
|
||||
password: await argon2.hash("test", { type: argon2.argon2id }),
|
||||
permission_level: "ELEVATED",
|
||||
},
|
||||
update: {},
|
||||
});
|
||||
|
||||
// Allow all CORS requests
|
||||
app.use(cors());
|
||||
} else {
|
||||
logger.info("Using production mode");
|
||||
|
||||
// Configure cors
|
||||
app.use(
|
||||
cors({
|
||||
origin: process.env.ALLOW_ORIGIN,
|
||||
allowedHeaders: ["Content-Type", "Authorization"],
|
||||
preflightContinue: false,
|
||||
methods: ["GET", "PUT", "PATCH", "POST", "DELETE"],
|
||||
optionsSuccessStatus: 204,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Todo: Everything
|
||||
|
||||
// Bodyparser and urlencoded to parse post request bodies
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.json());
|
||||
|
||||
app.use(debugLogger);
|
||||
|
||||
// Admin authentication endpoints
|
||||
app.use("/api/authentication", adminAuthRouter);
|
||||
|
||||
// All API endpoints are behind /api/...
|
||||
app.use("/api/events", eventRouter);
|
||||
|
||||
app.use("/api/admins", adminRouter);
|
||||
|
||||
app.use("/api/organisations", organisationRouter);
|
||||
|
||||
app.use("/api/groups", groupRouter);
|
||||
|
||||
app.use("/api/disciplines", disciplineRouter);
|
||||
|
||||
app.use("/api/role-schemas", roleSchemaRouter);
|
||||
|
||||
app.get("/", rootHandler);
|
||||
|
||||
// Error handling
|
||||
app.use(defaultErrorHandler); // Not working
|
||||
|
||||
app.use(notFoundHandler);
|
||||
|
||||
app.listen(process.env.PORT, () => {
|
||||
console.log(`Listening on Port: ${process.env.PORT}`);
|
||||
logger.info(`Listening on port ${process.env.PORT}`);
|
||||
});
|
||||
|
||||
logger.info("Server started");
|
||||
|
||||
process.on("exit", () => {
|
||||
logger.info("Server stopping...");
|
||||
});
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
logger.crit(e);
|
||||
throw e;
|
||||
})
|
||||
.finally(async () => {
|
||||
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
import { AuthJWTPayload } from "./Controllers/admin_auth.controller";
|
||||
|
||||
declare module "express-serve-static-core" {
|
||||
interface Request {
|
||||
auth?: AuthJWTPayload & { isAuthenticated: boolean };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { writeFile } from "fs";
|
||||
import Handlebars, { template } from "handlebars";
|
||||
import { mailClient } from "./redis";
|
||||
import nodemailer from "nodemailer";
|
||||
import SMTPTransport from "nodemailer/lib/smtp-transport";
|
||||
|
||||
import mjml from "./mjml";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
export let mailAccount = { user: process.env.MAILUSER + "@mail." + process.env.DOMAIN, pass: process.env.MAILPASSWORD };
|
||||
|
||||
let transporter =
|
||||
process.env.DEV == "true" || process.env.DOMAIN == undefined
|
||||
? (async () => {
|
||||
mailAccount = await nodemailer.createTestAccount();
|
||||
if (process.env.NODE_ENV != "test") {
|
||||
console.log(mailAccount);
|
||||
}
|
||||
return nodemailer.createTransport({
|
||||
host: "smtp.ethereal.email",
|
||||
port: 587,
|
||||
secure: false, // true for 465, false for other ports
|
||||
auth: {
|
||||
user: mailAccount.user, // generated ethereal user
|
||||
pass: mailAccount.pass, // generated ethereal password
|
||||
},
|
||||
});
|
||||
})()
|
||||
: nodemailer.createTransport(
|
||||
new SMTPTransport({
|
||||
host: "localhost",
|
||||
port: 587,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: mailAccount.user,
|
||||
pass: mailAccount.pass,
|
||||
},
|
||||
tls: {
|
||||
rejectUnauthorized: false,
|
||||
secureProtocol: "TLSv1_method",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const sendMail = async (from: string, to: string, subject: string, text?: string, html?: string) => {
|
||||
return await (
|
||||
await transporter
|
||||
).sendMail({
|
||||
from: from,
|
||||
to: to,
|
||||
subject: subject,
|
||||
text: text,
|
||||
html: html,
|
||||
});
|
||||
};
|
||||
|
||||
export const verificationMail = async (to: string, eventName: string, verificationLink: string) => {
|
||||
const raw = mjml.getTemplate("emailVerification");
|
||||
|
||||
//TODO: Replace other handlebars with final values
|
||||
const message = Handlebars.compile(raw);
|
||||
|
||||
const data = { eventName, verificationLink };
|
||||
const compiled = message(data);
|
||||
|
||||
sendMail(mailAccount.user, to, "Verify Email", undefined, compiled);
|
||||
};
|
||||
|
||||
export default { sendMail };
|
||||
@@ -0,0 +1,33 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import mjml from "mjml";
|
||||
|
||||
const template_folder = "/app/resources/email/templates/";
|
||||
|
||||
const compileTemplates = () => {
|
||||
console.log("Compiling templates: ");
|
||||
|
||||
fs.readdir(template_folder, (err, files) => {
|
||||
if (err) console.log(err);
|
||||
|
||||
files.forEach((file) => {
|
||||
if (path.extname(file) !== ".mjml") return;
|
||||
|
||||
let content = fs.readFileSync(template_folder + file);
|
||||
let mjmlres = mjml(content.toString());
|
||||
|
||||
let hbs = template_folder + file.replace(".mjml", ".hbs");
|
||||
fs.writeFileSync(hbs, mjmlres.html);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
compileTemplates(); // This will compile all templates when this file is first included
|
||||
|
||||
const getTemplate = (name: string): string => {
|
||||
let file = "";
|
||||
file = fs.readFileSync(path.join(template_folder, name + ".hbs")).toString();
|
||||
return file;
|
||||
};
|
||||
|
||||
export default { getTemplate };
|
||||
@@ -0,0 +1,37 @@
|
||||
import redis from "redis";
|
||||
import { promisify } from "util";
|
||||
|
||||
const REDIS_INDICES = {
|
||||
auth: 0,
|
||||
mail: 1,
|
||||
};
|
||||
|
||||
const REDIS_HOST = process.env.REDIS_HOST || "redis";
|
||||
const REDIS_PORT = process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : 6379;
|
||||
|
||||
// TODO: Extend
|
||||
function createAsyncClient(client: redis.RedisClient) {
|
||||
return {
|
||||
get: promisify(client.get).bind(client),
|
||||
set: promisify(client.set).bind(client),
|
||||
};
|
||||
}
|
||||
|
||||
// ----------------- //
|
||||
// Redis auth client //
|
||||
// ----------------- //
|
||||
|
||||
const auth = redis.createClient({ host: REDIS_HOST, port: REDIS_PORT });
|
||||
|
||||
auth.select(REDIS_INDICES.auth);
|
||||
|
||||
// ----------------- //
|
||||
// Redis mail client //
|
||||
// ----------------- //
|
||||
|
||||
const mail = redis.createClient({ host: REDIS_HOST, port: REDIS_PORT });
|
||||
|
||||
mail.select(REDIS_INDICES.mail);
|
||||
|
||||
export const authClient = createAsyncClient(auth);
|
||||
export const mailClient = createAsyncClient(mail);
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Schema, z, ZodError } from "zod";
|
||||
import SchemaError from "../Middleware/error/SchemaError";
|
||||
|
||||
// -- Schema definitions --
|
||||
|
||||
const SchemaVersion = z.enum(["1.0"]);
|
||||
|
||||
const TimeUnit = z.enum(["days", "hours", "minutes", "seconds", "milliseconds"]);
|
||||
|
||||
const DurationSchema = z
|
||||
.object({
|
||||
type: z.literal("duration"),
|
||||
min: z.number().int({ message: "min must be an integer (relative to smallestUnit)" }),
|
||||
max: z.number().int({ message: "max must be an integer (relative to smallestUnit)" }),
|
||||
smallestUnit: TimeUnit,
|
||||
higherIsBetter: z.boolean(),
|
||||
})
|
||||
.refine(({ min, max }) => min < max, { message: "min must be smaller than max" });
|
||||
|
||||
const PointSchema = z
|
||||
.object({
|
||||
type: z.literal("points"),
|
||||
min: z.number(),
|
||||
max: z.number(),
|
||||
step: z.number(),
|
||||
start: z.number(),
|
||||
|
||||
unit: z.string(),
|
||||
unitSign: z.string(),
|
||||
|
||||
higherIsBetter: z.boolean(),
|
||||
})
|
||||
.refine(({ min, max }) => min < max, { message: "min must be smaller than max" })
|
||||
.refine(({ min, max, start }) => min <= start && max >= start, {
|
||||
message: "start must be larger than or equal to min and smaller than or equal to max",
|
||||
});
|
||||
|
||||
export type DurationSchemaT = z.infer<typeof DurationSchema>;
|
||||
export type PointSchemaT = z.infer<typeof PointSchema>;
|
||||
|
||||
// -- Parser --
|
||||
|
||||
export function parseSchema(schema: any): DurationSchemaT | PointSchemaT {
|
||||
try {
|
||||
if (schema.type === "duration") {
|
||||
return DurationSchema.parse(schema);
|
||||
} else if (schema.type === "points") {
|
||||
return PointSchema.parse(schema);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof ZodError) {
|
||||
const issue = e.issues.at(0);
|
||||
|
||||
if (issue) {
|
||||
throw new SchemaError(`Error with the schema: ${issue.path ? `${issue.path}:` : ""} ${issue.message}`);
|
||||
}
|
||||
|
||||
throw new SchemaError("Unknown error occured while validating the schema");
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
throw new SchemaError("Error with the schema: type must be either 'duration' or 'points'");
|
||||
}
|
||||
Reference in New Issue
Block a user