remove server code for demo instance
This commit is contained in:
@ -1,17 +0,0 @@
|
||||
# For production, the database credentials should be changed
|
||||
# here and in the docker-compose file for the postgres container,
|
||||
# but they will work for testing or development
|
||||
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres?schema=public
|
||||
|
||||
# These credentials are required for fetching substitution plan
|
||||
# files from BOLLE. More information on how to get them can be found
|
||||
# in the BOLLE parser (./server/parser/bolle.js)
|
||||
# (If you are using a different file provider than bolle, you can
|
||||
# remove these and add the ones required by your file provider)
|
||||
BOLLE_URL=
|
||||
BOLLE_USER=
|
||||
BOLLE_KEY=
|
||||
|
||||
# This password is required for logging into your timetable v2
|
||||
# instance. You can leave it empty to disable authentication.
|
||||
AUTH_PASSWORD=
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"node": true
|
||||
}
|
||||
}
|
2
server/.gitignore
vendored
2
server/.gitignore
vendored
@ -1,2 +0,0 @@
|
||||
node_modules
|
||||
.env
|
@ -1,152 +0,0 @@
|
||||
import Prisma from "@prisma/client";
|
||||
const prisma = new Prisma.PrismaClient();
|
||||
|
||||
export function registerAdmin(app) {
|
||||
app.get("/api/admin/timetable", listTimetables);
|
||||
app.post("/api/admin/timetable", createTimetable);
|
||||
app.put("/api/admin/timetable", editTimetable);
|
||||
app.delete("/api/admin/timetable", deleteTimetable);
|
||||
|
||||
app.get("/api/admin/key", listKeys);
|
||||
app.post("/api/admin/key", createKey);
|
||||
app.put("/api/admin/key", editKey);
|
||||
app.delete("/api/admin/key", deleteKey);
|
||||
}
|
||||
|
||||
function sendMissingArguments(res) {
|
||||
res.status(400).send({
|
||||
success: false,
|
||||
error: "missing_arguments",
|
||||
});
|
||||
}
|
||||
|
||||
async function listTimetables(_, res) {
|
||||
res.send(
|
||||
await prisma.timetable.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
class: true,
|
||||
source: true,
|
||||
trusted: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async function createTimetable(req, res) {
|
||||
let data = req.body;
|
||||
if (!data.title || !data.data || !data.class) {
|
||||
sendMissingArguments(res);
|
||||
return;
|
||||
}
|
||||
const timetable = await prisma.timetable.create({
|
||||
data: req.body,
|
||||
});
|
||||
res.status(201).send(timetable);
|
||||
}
|
||||
|
||||
async function editTimetable(req, res) {
|
||||
let id = parseInt(req.query.id);
|
||||
if (!id) {
|
||||
sendMissingArguments(res);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const timetable = await prisma.timetable.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: req.body,
|
||||
});
|
||||
res.status(201).send(timetable);
|
||||
} catch (e) {
|
||||
res.status(500).send(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteTimetable(req, res) {
|
||||
if (!req.query.id) {
|
||||
sendMissingArguments(res);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await prisma.timetable.delete({
|
||||
where: {
|
||||
id: parseInt(req.query.id),
|
||||
},
|
||||
});
|
||||
res.status(200).send();
|
||||
} catch (e) {
|
||||
res.status(500).send(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function listKeys(_, res) {
|
||||
res.send(await prisma.key.findMany());
|
||||
}
|
||||
|
||||
async function createKey(req, res) {
|
||||
let data = req.body;
|
||||
if (!data.key) {
|
||||
sendMissingArguments(res);
|
||||
return;
|
||||
}
|
||||
const existingKey = await prisma.key.findUnique({
|
||||
where: {
|
||||
key: data.key,
|
||||
},
|
||||
});
|
||||
if (existingKey) {
|
||||
res.status(400).send({
|
||||
success: false,
|
||||
error: "key_already_exists",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const key = await prisma.key.create({
|
||||
data: {
|
||||
key: data.key,
|
||||
permissions: data.permissions || [],
|
||||
validUntil: data.validUntil,
|
||||
notes: data.notes,
|
||||
},
|
||||
});
|
||||
res.status(201).send(key);
|
||||
}
|
||||
|
||||
async function editKey(req, res) {
|
||||
if (!req.query.id) {
|
||||
sendMissingArguments(res);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const timetable = await prisma.key.update({
|
||||
where: {
|
||||
key: req.query.id,
|
||||
},
|
||||
data: req.body,
|
||||
});
|
||||
res.status(201).send(timetable);
|
||||
} catch (e) {
|
||||
res.status(500).send(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteKey(req, res) {
|
||||
if (!req.query.id) {
|
||||
sendMissingArguments(res);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await prisma.key.delete({
|
||||
where: {
|
||||
key: req.query.id,
|
||||
},
|
||||
});
|
||||
res.status(200).send();
|
||||
} catch (e) {
|
||||
res.status(500).send(e);
|
||||
}
|
||||
}
|
@ -1,158 +0,0 @@
|
||||
import Prisma from "@prisma/client";
|
||||
const prisma = new Prisma.PrismaClient();
|
||||
|
||||
import { log } from "../logs.js";
|
||||
|
||||
async function isLoggedIn(req) {
|
||||
// If AUTH_PASSWORD env variable is not present don't require any login
|
||||
if (!process.env.AUTH_PASSWORD) {
|
||||
return true;
|
||||
}
|
||||
// If no session cookie is set and no token query
|
||||
// parameter is provided the user can't be logged in
|
||||
const token = req.query.token || req.cookies.session;
|
||||
if (!token) {
|
||||
return false;
|
||||
}
|
||||
// If there is a session cookie check it
|
||||
const session = await prisma.session.findUnique({
|
||||
where: {
|
||||
token,
|
||||
},
|
||||
});
|
||||
// If no session is found (the session probably
|
||||
// exired) the user is not logged in
|
||||
if (!session) {
|
||||
return false;
|
||||
}
|
||||
// If no checks failed the user is logged in
|
||||
return session;
|
||||
}
|
||||
|
||||
async function renewSession(session) {
|
||||
// Don't try to renew sessions if auth is disabled
|
||||
if (session === true) return;
|
||||
|
||||
await prisma.session.update({
|
||||
where: {
|
||||
token: session.token,
|
||||
},
|
||||
data: {
|
||||
// 14 Days from now on
|
||||
validUntil: new Date(Date.now() + 1000 * 60 * 60 * 24 * 14),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function login(req, res) {
|
||||
// Check if the user is already logged in
|
||||
let session = await isLoggedIn(req);
|
||||
if (session) {
|
||||
renewSession(session);
|
||||
res.redirect("/");
|
||||
return;
|
||||
}
|
||||
// Check password
|
||||
if (!req.body.password || req.body.password != process.env.AUTH_PASSWORD) {
|
||||
res.redirect("/login");
|
||||
return;
|
||||
}
|
||||
// Create a new auth session
|
||||
session = await prisma.session.create({
|
||||
data: {
|
||||
// Expires after 14 days of inactivity
|
||||
validUntil: new Date(Date.now() + 1000 * 60 * 60 * 24 * 14),
|
||||
},
|
||||
});
|
||||
res.cookie("session", session.token, {
|
||||
httpOnly: true,
|
||||
// Expire "never"
|
||||
expires: new Date(253402300000000),
|
||||
});
|
||||
log("API / Auth", `New session: ${session.token}`);
|
||||
res.redirect("/");
|
||||
}
|
||||
|
||||
async function logout(req, res) {
|
||||
const session = await isLoggedIn(req);
|
||||
if (!session) {
|
||||
res.sendStatus(401);
|
||||
return;
|
||||
}
|
||||
await prisma.session.deleteMany({
|
||||
where: {
|
||||
token: session.token,
|
||||
},
|
||||
});
|
||||
log("API / Auth", `Removed session: ${session.token}`);
|
||||
res.redirect("/");
|
||||
}
|
||||
|
||||
async function checkLogin(req, res, next) {
|
||||
// Allow requests to `/api/token`
|
||||
if (req.path == "/token") {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
const session = await isLoggedIn(req);
|
||||
if (!session) {
|
||||
// send 401 Unauthorized so the
|
||||
// app redirects to the login page
|
||||
res.sendStatus(401);
|
||||
return;
|
||||
}
|
||||
req.locals = {
|
||||
session: session.token,
|
||||
};
|
||||
renewSession(session);
|
||||
next();
|
||||
}
|
||||
|
||||
async function token(req, res) {
|
||||
if (!req.query.password || req.query.password != process.env.AUTH_PASSWORD) {
|
||||
res.status(401).send({
|
||||
success: false,
|
||||
error: "wrong_auth",
|
||||
message: "Wrong password",
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Create a new auth session
|
||||
const session = await prisma.session.create({
|
||||
data: {
|
||||
// API token expires after 1 hour
|
||||
validUntil: new Date(Date.now() + 1000 * 60 * 60),
|
||||
},
|
||||
});
|
||||
log("API / Auth", `New token: ${session.token}`);
|
||||
|
||||
res.send({
|
||||
success: true,
|
||||
token: session.token,
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
login,
|
||||
logout,
|
||||
checkLogin,
|
||||
token,
|
||||
};
|
||||
|
||||
// Clean up expired sessions every hour
|
||||
setInterval(
|
||||
async () => {
|
||||
const sessions = await prisma.session.findMany();
|
||||
for (const session of sessions) {
|
||||
if (session.validUntil < new Date()) {
|
||||
log("API / Auth", `Removed expired session: ${session.token}`);
|
||||
await prisma.session.delete({
|
||||
where: {
|
||||
token: session.token,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
1000 * 60 * 60,
|
||||
);
|
@ -1,275 +0,0 @@
|
||||
import Prisma from "@prisma/client";
|
||||
const prisma = new Prisma.PrismaClient();
|
||||
|
||||
import {
|
||||
applyKey,
|
||||
hasPermission,
|
||||
listPermissions,
|
||||
revokeKey,
|
||||
} from "./permission.js";
|
||||
|
||||
// Get info API endpoint (/api/info)
|
||||
// Returns information about the requesting session
|
||||
export async function getInfo(req, res) {
|
||||
// If server has auth disabled
|
||||
if (!req.locals.session) {
|
||||
res.send({
|
||||
authenticated: true,
|
||||
appliedKeys: [],
|
||||
permissions: [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const session = await prisma.session.findUnique({
|
||||
where: {
|
||||
token: req.locals.session,
|
||||
},
|
||||
include: {
|
||||
appliedKeys: {
|
||||
select: {
|
||||
key: true,
|
||||
permissions: true,
|
||||
validUntil: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
res.send({
|
||||
authenticated: true,
|
||||
appliedKeys: session.appliedKeys,
|
||||
permissions: await listPermissions(session.token),
|
||||
});
|
||||
}
|
||||
|
||||
// Put and Delete key API endpoints (/api/key)
|
||||
// Applies or revokes a key from the requesting user's session
|
||||
export async function putKey(req, res) {
|
||||
if (await applyKey(req.locals.session, req.query.key)) {
|
||||
res.status(200).send();
|
||||
} else {
|
||||
res.status(400).send({
|
||||
success: false,
|
||||
error: "invalid_key",
|
||||
message: "This key does not exist",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteKey(req, res) {
|
||||
if (await revokeKey(req.locals.session, req.query.key)) {
|
||||
res.status(200).send();
|
||||
} else {
|
||||
res.status(400).send();
|
||||
}
|
||||
}
|
||||
|
||||
// Get timetable API endpoint (/api/timetable)
|
||||
// Returns timetable data for requested class if available
|
||||
export async function getTimetable(req, res) {
|
||||
if (!req.query.class) {
|
||||
res.status(400).send({
|
||||
success: false,
|
||||
error: "missing_parameter",
|
||||
message: "No class parameter provided",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const requestedClass = req.query.class.toLowerCase();
|
||||
const timetables = await prisma.timetable.findMany({
|
||||
where: {
|
||||
class: requestedClass,
|
||||
},
|
||||
orderBy: {
|
||||
updatedAt: "desc",
|
||||
},
|
||||
});
|
||||
const times = await prisma.time.findMany();
|
||||
res.send({
|
||||
timetables,
|
||||
times,
|
||||
});
|
||||
}
|
||||
|
||||
// Edit timetable API endpoint (/api/timetable)
|
||||
// Updates a remote timetable with the requested data
|
||||
export async function putTimetable(req, res) {
|
||||
const timetableId = parseInt(req.query.id);
|
||||
const data = req.body.data;
|
||||
if (
|
||||
!(await hasPermission(req.locals.session, "timetable.update", timetableId))
|
||||
) {
|
||||
res.status(401).send({
|
||||
success: false,
|
||||
error: "missing_permission",
|
||||
message: "You don't have permission to update this timetable!",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await prisma.timetable.update({
|
||||
where: {
|
||||
id: timetableId,
|
||||
},
|
||||
data: {
|
||||
data,
|
||||
title: req.body.title,
|
||||
},
|
||||
});
|
||||
res.status(201).send();
|
||||
}
|
||||
|
||||
// Helper function for converting a date string
|
||||
// (eg. "2022-06-02" or "1654128000000") to a
|
||||
// unix timestamp
|
||||
function convertToDate(dateQuery) {
|
||||
var date;
|
||||
if (dateQuery.match(/^[0-9]+$/) != null) date = parseInt(dateQuery);
|
||||
else date = dateQuery;
|
||||
date = new Date(date).setUTCHours(0, 0, 0, 0);
|
||||
return new Date(date);
|
||||
}
|
||||
|
||||
// Get substitutions API endpoint (/api/substitutions)
|
||||
// Returns all known substitutions for requested date / class
|
||||
// If no class is supplied, all substitutions are returned
|
||||
export async function getSubstitutions(req, res) {
|
||||
const requestedClass = (req.query.class || "").toLowerCase();
|
||||
var from, to, date;
|
||||
// Check if from or to date is set in request
|
||||
if (req.query.from && req.query.to) {
|
||||
from = convertToDate(req.query.from);
|
||||
to = convertToDate(req.query.to);
|
||||
} else if (req.query.date) {
|
||||
date = convertToDate(req.query.date);
|
||||
}
|
||||
|
||||
const prismaOptions = {
|
||||
where: {
|
||||
removed: false,
|
||||
},
|
||||
orderBy: {
|
||||
lesson: "asc",
|
||||
},
|
||||
};
|
||||
if (requestedClass) {
|
||||
prismaOptions.where.class = { has: requestedClass };
|
||||
}
|
||||
// Choose which date to use in database query
|
||||
if (from && to) {
|
||||
prismaOptions.where.date = {
|
||||
gte: from,
|
||||
lte: to,
|
||||
};
|
||||
} else if (date) {
|
||||
prismaOptions.where.date = date;
|
||||
} else {
|
||||
// Default to all substitutions for today and in the future
|
||||
prismaOptions.where.date = {
|
||||
gte: new Date(new Date().setUTCHours(0, 0, 0, 0)),
|
||||
};
|
||||
}
|
||||
|
||||
const rawSubstitutions = await prisma.substitution.findMany(prismaOptions);
|
||||
const substitutions = rawSubstitutions.map((element) => {
|
||||
const substitution = {
|
||||
id: element.id,
|
||||
class: element.class,
|
||||
type: element.type,
|
||||
rawType: element.rawType,
|
||||
lesson: element.lesson,
|
||||
date: new Date(element.date).getTime(),
|
||||
notes: element.notes,
|
||||
teacher: element.teacher,
|
||||
change: {},
|
||||
};
|
||||
if (element.changedRoom) substitution.change.room = element.changedRoom;
|
||||
if (element.changedTeacher)
|
||||
substitution.change.teacher = element.changedTeacher;
|
||||
if (element.changedSubject)
|
||||
substitution.change.subject = element.changedSubject;
|
||||
return substitution;
|
||||
});
|
||||
res.send(substitutions);
|
||||
}
|
||||
|
||||
// Get history API endpoint (/api/history)
|
||||
// Returns history of changes for all substituions in the date range
|
||||
// for the requested class if supplied
|
||||
export async function getHistory(req, res) {
|
||||
const requestedClass = (req.query.class || "").toLowerCase();
|
||||
var from, to, date;
|
||||
// Check if from or to date is set in request
|
||||
if (req.query.from && req.query.to) {
|
||||
from = convertToDate(req.query.from);
|
||||
to = convertToDate(req.query.to);
|
||||
} else if (req.query.date) {
|
||||
date = convertToDate(req.query.date);
|
||||
}
|
||||
|
||||
const prismaOptions = {
|
||||
where: {
|
||||
substitution: {},
|
||||
},
|
||||
include: {
|
||||
substitution: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
};
|
||||
if (requestedClass) {
|
||||
prismaOptions.where.substitution.class = { has: requestedClass };
|
||||
}
|
||||
// Choose which date to use in database query
|
||||
if (from && to) {
|
||||
prismaOptions.where.substitution.date = {
|
||||
gte: from,
|
||||
lte: to,
|
||||
};
|
||||
} else if (date) {
|
||||
prismaOptions.where.substitution.date = date;
|
||||
} else {
|
||||
// Default to history of all substitutions for today and in the future
|
||||
prismaOptions.where.substitution.date = {
|
||||
gte: new Date(new Date().setUTCHours(0, 0, 0, 0)),
|
||||
};
|
||||
}
|
||||
|
||||
const rawChanges = await prisma.substitutionChange.findMany(prismaOptions);
|
||||
const changes = rawChanges.map((element) => {
|
||||
return {
|
||||
id: element.id,
|
||||
type: element.type,
|
||||
class: element.substitution.class,
|
||||
substitutionId: element.substitutionId,
|
||||
lesson: element.substitution.lesson,
|
||||
updatedAt: new Date(element.createdAt).getTime(),
|
||||
date: new Date(element.substitution.date).getTime(),
|
||||
teacher: element.teacher,
|
||||
change: element.changes,
|
||||
notes: element.notes,
|
||||
parseEventId: element.parseEventId,
|
||||
};
|
||||
});
|
||||
res.send(changes);
|
||||
}
|
||||
|
||||
// Get classes API endpoints (/api/classes)
|
||||
// Get all available classes where timetable and
|
||||
// substitutions can be requested for
|
||||
export async function getClasses(_req, res) {
|
||||
const classes = await prisma.class.findMany({
|
||||
select: {
|
||||
name: true,
|
||||
regex: false,
|
||||
},
|
||||
orderBy: {
|
||||
name: "asc",
|
||||
},
|
||||
});
|
||||
// Only return the name of the class
|
||||
const classList = classes.map((element) => element.name);
|
||||
res.send(classList);
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
import Prisma from "@prisma/client";
|
||||
import { log } from "../logs.js";
|
||||
const prisma = new Prisma.PrismaClient();
|
||||
|
||||
export async function listPermissions(sessionToken) {
|
||||
const session = await prisma.session.findUnique({
|
||||
where: {
|
||||
token: sessionToken,
|
||||
},
|
||||
include: {
|
||||
appliedKeys: true,
|
||||
},
|
||||
});
|
||||
if (!session) return [];
|
||||
|
||||
const perms = [];
|
||||
for (const key of session.appliedKeys) {
|
||||
if (key.validUntil && new Date() > key.validUntil) continue;
|
||||
for (const perm of key.permissions) {
|
||||
perms.push(perm);
|
||||
}
|
||||
}
|
||||
return perms;
|
||||
}
|
||||
|
||||
export async function hasPermission(sessionToken, permission, forValue) {
|
||||
let hasPermission = false;
|
||||
for (const perm of await listPermissions(sessionToken)) {
|
||||
if (perm == permission) hasPermission = true;
|
||||
else if (perm == permission + ":" + forValue) hasPermission = true;
|
||||
}
|
||||
return hasPermission;
|
||||
}
|
||||
|
||||
export async function checkAdmin(req, res, next) {
|
||||
if (!(await hasPermission(req.locals.session, "admin"))) {
|
||||
res.status(401).send({
|
||||
success: false,
|
||||
error: "admin_only",
|
||||
message: "You need to be admin to do this!",
|
||||
});
|
||||
return;
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
export async function applyKey(sessionToken, key) {
|
||||
if (!key) return false;
|
||||
const foundKey = await prisma.key.findUnique({
|
||||
where: {
|
||||
key,
|
||||
},
|
||||
});
|
||||
if (!foundKey) return false;
|
||||
|
||||
await prisma.session.update({
|
||||
where: {
|
||||
token: sessionToken,
|
||||
},
|
||||
data: {
|
||||
appliedKeys: {
|
||||
connect: {
|
||||
key: foundKey.key,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function revokeKey(sessionToken, key) {
|
||||
if (!key) return false;
|
||||
|
||||
await prisma.session.update({
|
||||
where: {
|
||||
token: sessionToken,
|
||||
},
|
||||
data: {
|
||||
appliedKeys: {
|
||||
disconnect: {
|
||||
key: key,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// Clean up expired keys every hour
|
||||
setInterval(
|
||||
async () => {
|
||||
const keys = await prisma.key.findMany();
|
||||
for (const key of keys) {
|
||||
if (key.validUntil && key.validUntil < new Date()) {
|
||||
log(
|
||||
"API / Permissions",
|
||||
`Removed expired key: ${key.key}; Permissions: ${key.permissions.join(
|
||||
", ",
|
||||
)}`,
|
||||
);
|
||||
await prisma.key.delete({
|
||||
where: {
|
||||
key: key.key,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
1000 * 60 * 60,
|
||||
);
|
@ -1,13 +0,0 @@
|
||||
import Prisma from "@prisma/client";
|
||||
|
||||
const prisma = new Prisma.PrismaClient();
|
||||
(async () => {
|
||||
const key = await prisma.key.create({
|
||||
data: {
|
||||
permissions: ["admin"],
|
||||
notes: `Created at ${new Date().toLocaleString()} using the "createAdminKeys.js" script`,
|
||||
},
|
||||
});
|
||||
console.log("Created admin key:");
|
||||
console.log(key.key);
|
||||
})();
|
@ -1,91 +0,0 @@
|
||||
import express from "express";
|
||||
import cors from "cors";
|
||||
import cookieParser from "cookie-parser";
|
||||
|
||||
// Import API endpoints
|
||||
import {
|
||||
getTimetable,
|
||||
getSubstitutions,
|
||||
getHistory,
|
||||
getClasses,
|
||||
putTimetable,
|
||||
getInfo,
|
||||
putKey,
|
||||
deleteKey,
|
||||
} from "./api/index.js";
|
||||
import auth from "./api/auth.js";
|
||||
import { Parser } from "./parser/index.js";
|
||||
import { BolleClient } from "./parser/bolle.js";
|
||||
import { parseSubstitutionPlan } from "./parser/untis.js";
|
||||
import { registerAdmin } from "./api/admin.js";
|
||||
import { checkAdmin } from "./api/permission.js";
|
||||
|
||||
// Check that credentials are supplied
|
||||
if (
|
||||
!process.env.BOLLE_URL ||
|
||||
!process.env.BOLLE_USER ||
|
||||
!process.env.BOLLE_KEY
|
||||
) {
|
||||
console.error("Error: Bolle Auth environment variables missing!");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Create the Webserver
|
||||
const app = express();
|
||||
const port = process.env.PORT || 3000;
|
||||
app.use(cors());
|
||||
app.use(cookieParser());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.json());
|
||||
|
||||
// Initialize the Parser and set it to update the
|
||||
// substitution plan at the specified update interval
|
||||
new Parser(
|
||||
new BolleClient(
|
||||
process.env.BOLLE_URL,
|
||||
process.env.BOLLE_USER,
|
||||
process.env.BOLLE_KEY,
|
||||
),
|
||||
parseSubstitutionPlan,
|
||||
process.env.UPDATE_INTERVAL || 1 * 60 * 1000, // Default to 1 minute
|
||||
);
|
||||
|
||||
// Create new Auth class to store sessions
|
||||
app.post("/auth/login", auth.login);
|
||||
app.get("/auth/logout", auth.logout);
|
||||
// Check login for every API request
|
||||
app.use("/api", auth.checkLogin);
|
||||
// Provide check endpoint so the frontend
|
||||
// can check if the user is logged in
|
||||
app.get("/api/check", (_req, res) => {
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
// Register API endpoints
|
||||
app.get("/api/info", getInfo);
|
||||
app.put("/api/key", putKey);
|
||||
app.delete("/api/key", deleteKey);
|
||||
app.get("/api/timetable", getTimetable);
|
||||
app.put("/api/timetable", putTimetable);
|
||||
app.get("/api/substitutions", getSubstitutions);
|
||||
app.get("/api/history", getHistory);
|
||||
app.get("/api/classes", getClasses);
|
||||
app.post("/api/token", auth.token);
|
||||
|
||||
// Register Admin endpoints
|
||||
app.use("/api/admin", checkAdmin);
|
||||
registerAdmin(app);
|
||||
|
||||
// Respond with 400 for non-existent endpoints
|
||||
app.get("/api/*", (_req, res) => {
|
||||
res.sendStatus(400);
|
||||
});
|
||||
|
||||
// Supply frontend files if any other url
|
||||
// is requested to make vue router work
|
||||
app.use("/", express.static("../dist"));
|
||||
app.use("/*", express.static("../dist"));
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Server listening on http://localhost:${port}`);
|
||||
});
|
@ -1,24 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"checkJs": false,
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "node",
|
||||
"target": "es2020",
|
||||
"module": "es2015"
|
||||
},
|
||||
"exclude": [
|
||||
"dist",
|
||||
"node_modules",
|
||||
"build",
|
||||
".vscode",
|
||||
"coverage",
|
||||
".npm",
|
||||
".yarn"
|
||||
],
|
||||
"typeAcquisition": {
|
||||
"enable": true,
|
||||
"include": [
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
import fs from "fs";
|
||||
|
||||
export function log(type, text) {
|
||||
if (!fs.existsSync("logs")) fs.mkdirSync("logs");
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const logName = now.split("T")[0] + ".log";
|
||||
const timestamp = now.replace("T", " ").split(".")[0];
|
||||
const logLine = `<${timestamp}> [${type}]: ${text}`;
|
||||
fs.appendFileSync("logs/" + logName, logLine + "\n");
|
||||
console.log(logLine);
|
||||
}
|
||||
|
||||
export function getLogPath() {
|
||||
const logName = new Date().toISOString().split("T")[0] + ".log";
|
||||
return "logs/" + logName;
|
||||
}
|
952
server/package-lock.json
generated
952
server/package-lock.json
generated
@ -1,952 +0,0 @@
|
||||
{
|
||||
"name": "timetable-server",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "timetable-server",
|
||||
"version": "1.0.0",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.2.0",
|
||||
"axios": "^1.4.0",
|
||||
"cheerio": "^1.0.0-rc.11",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.18.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prisma": "^5.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/client": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.2.0.tgz",
|
||||
"integrity": "sha512-AiTjJwR4J5Rh6Z/9ZKrBBLel3/5DzUNntMohOy7yObVnVoTNVFi2kvpLZlFuKO50d7yDspOtW6XBpiAd0BVXbQ==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@prisma/engines-version": "5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.13"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prisma": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"prisma": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/engines": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.2.0.tgz",
|
||||
"integrity": "sha512-dT7FOLUCdZmq+AunLqB1Iz+ZH/IIS1Fz2THmKZQ6aFONrQD/BQ5ecJ7g2wGS2OgyUFf4OaLam6/bxmgdOBDqig==",
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true
|
||||
},
|
||||
"node_modules/@prisma/engines-version": {
|
||||
"version": "5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f.tgz",
|
||||
"integrity": "sha512-jsnKT5JIDIE01lAeCj2ghY9IwxkedhKNvxQeoyLs6dr4ZXynetD0vTy7u6wMJt8vVPv8I5DPy/I4CFaoXAgbtg=="
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
||||
"dependencies": {
|
||||
"mime-types": "~2.1.34",
|
||||
"negotiator": "0.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
|
||||
"integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.1",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
|
||||
"integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"content-type": "~1.0.4",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "2.4.1",
|
||||
"qs": "6.11.0",
|
||||
"raw-body": "2.5.1",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/boolbase": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1",
|
||||
"get-intrinsic": "^1.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==",
|
||||
"dependencies": {
|
||||
"cheerio-select": "^2.1.0",
|
||||
"dom-serializer": "^2.0.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.0.1",
|
||||
"htmlparser2": "^8.0.1",
|
||||
"parse5": "^7.0.0",
|
||||
"parse5-htmlparser2-tree-adapter": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/cheeriojs/cheerio?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio-select": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz",
|
||||
"integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0",
|
||||
"css-select": "^5.1.0",
|
||||
"css-what": "^6.1.0",
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
|
||||
"dependencies": {
|
||||
"safe-buffer": "5.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/content-type": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
|
||||
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-parser": {
|
||||
"version": "1.4.6",
|
||||
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz",
|
||||
"integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==",
|
||||
"dependencies": {
|
||||
"cookie": "0.4.1",
|
||||
"cookie-signature": "1.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
|
||||
},
|
||||
"node_modules/cors": {
|
||||
"version": "2.8.5",
|
||||
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
|
||||
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
|
||||
"dependencies": {
|
||||
"object-assign": "^4",
|
||||
"vary": "^1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/css-select": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
|
||||
"integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0",
|
||||
"css-what": "^6.1.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"domutils": "^3.0.1",
|
||||
"nth-check": "^2.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/css-what": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
|
||||
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/destroy": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
|
||||
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"entities": "^4.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domelementtype": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/domhandler": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
|
||||
"integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
|
||||
"dependencies": {
|
||||
"dom-serializer": "^2.0.0",
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
|
||||
},
|
||||
"node_modules/etag": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.18.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
|
||||
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.20.1",
|
||||
"content-disposition": "0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.5.0",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"encodeurl": "~1.0.2",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"finalhandler": "1.2.0",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"merge-descriptors": "1.0.1",
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "0.1.7",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "6.11.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "0.18.0",
|
||||
"serve-static": "1.15.0",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"type-is": "~1.6.18",
|
||||
"utils-merge": "1.0.1",
|
||||
"vary": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/cookie": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
|
||||
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "~1.0.2",
|
||||
"escape-html": "~1.0.3",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"statuses": "2.0.1",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
|
||||
"integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
"has-proto": "^1.0.1",
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/has-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
||||
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/htmlparser2": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
|
||||
"funding": [
|
||||
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.0.1",
|
||||
"entities": "^4.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
|
||||
"dependencies": {
|
||||
"depd": "2.0.0",
|
||||
"inherits": "2.0.4",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"toidentifier": "1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
||||
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
|
||||
},
|
||||
"node_modules/methods": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/nth-check": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.12.3",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
|
||||
"integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||
"dependencies": {
|
||||
"ee-first": "1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
|
||||
"integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==",
|
||||
"dependencies": {
|
||||
"entities": "^4.4.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5-htmlparser2-tree-adapter": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz",
|
||||
"integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==",
|
||||
"dependencies": {
|
||||
"domhandler": "^5.0.2",
|
||||
"parse5": "^7.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
|
||||
},
|
||||
"node_modules/prisma": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.2.0.tgz",
|
||||
"integrity": "sha512-FfFlpjVCkZwrqxDnP4smlNYSH1so+CbfjgdpioFzGGqlQAEm6VHAYSzV7jJgC3ebtY9dNOhDMS2+4/1DDSM7bQ==",
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@prisma/engines": "5.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"prisma": "build/index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.13"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
||||
"dependencies": {
|
||||
"forwarded": "0.2.0",
|
||||
"ipaddr.js": "1.9.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.11.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
|
||||
"integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"unpipe": "1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
|
||||
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
"encodeurl": "~1.0.2",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"mime": "1.6.0",
|
||||
"ms": "2.1.3",
|
||||
"on-finished": "2.4.1",
|
||||
"range-parser": "~1.2.1",
|
||||
"statuses": "2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/send/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
|
||||
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
|
||||
"dependencies": {
|
||||
"encodeurl": "~1.0.2",
|
||||
"escape-html": "~1.0.3",
|
||||
"parseurl": "~1.3.3",
|
||||
"send": "0.18.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.0",
|
||||
"get-intrinsic": "^1.0.2",
|
||||
"object-inspect": "^1.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
||||
"dependencies": {
|
||||
"media-typer": "0.3.0",
|
||||
"mime-types": "~2.1.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
{
|
||||
"name": "timetable-server",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "minie4",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.2.0",
|
||||
"axios": "^1.4.0",
|
||||
"cheerio": "^1.0.0-rc.11",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.18.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prisma": "^5.2.0"
|
||||
}
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
import axios from "axios";
|
||||
import crypto from "node:crypto";
|
||||
import { log } from "../logs.js";
|
||||
|
||||
/*
|
||||
This BOLLE (https://bolle-software.de/) parser tries to download
|
||||
the latest substitution plan HTML files from the "Pinnwand".
|
||||
|
||||
It uses the same API that the mobile app (v0.3.6) uses.
|
||||
To get your login credentials you need to log into your BOLLE account
|
||||
and register a new device in the settings (/einstellungen/geraete_personal).
|
||||
Click on "Manuelle Logindaten Einblenden" and use "ID" as apiUser
|
||||
and "Token" as apiKey. You need to make at least one request with
|
||||
this API token before closing the registration window or else the
|
||||
token will be invalidated immediately.
|
||||
*/
|
||||
|
||||
// Files to download from the "Pinnwand"
|
||||
const filenames = ["vp_heute", "vp_morgen"];
|
||||
|
||||
export class BolleClient {
|
||||
constructor(bolleInstance, apiUser, apiKey) {
|
||||
this.bolleInstance = bolleInstance;
|
||||
this.apiUser = apiUser;
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
async getFiles() {
|
||||
try {
|
||||
let contents = [];
|
||||
for (let file of filenames) {
|
||||
contents.push(await this.getFile(file));
|
||||
}
|
||||
return contents;
|
||||
} catch (error) {
|
||||
log("Parser / Bolle", "Error getting data: " + error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async getFile(filename) {
|
||||
// Generate the BOLLE api payload
|
||||
let payload = {
|
||||
api_payload: JSON.stringify({
|
||||
method: "vertretungsplan_html",
|
||||
payload: { content: filename },
|
||||
}),
|
||||
};
|
||||
// Generate request headers
|
||||
let headers = this.buildRequestHeaders(payload.api_payload);
|
||||
// Send the POST request
|
||||
let response = await axios.post(this.getRequestUrl(), payload, {
|
||||
headers,
|
||||
});
|
||||
// The server responds with a json object
|
||||
// containing the base64 encoded html data
|
||||
let base64 = response.data["html_base64"];
|
||||
// Decode the base64 data using the latin1 (ISO 8859-1) character set
|
||||
return Buffer.from(base64, "base64").toString("latin1");
|
||||
}
|
||||
getRequestUrl() {
|
||||
// The API that the bolle mobile app uses is available at /app/basic
|
||||
return `https://${this.bolleInstance}/app/basic`;
|
||||
}
|
||||
buildRequestHeaders(payload) {
|
||||
// Bolle needs the sha1 hash of the payload
|
||||
// to be set as the "b-hash" header
|
||||
let hash = crypto.createHash("sha1");
|
||||
hash.update(payload);
|
||||
|
||||
return {
|
||||
Accept: "application/json",
|
||||
// Set the auth headers
|
||||
"X-Auth-User": this.apiUser,
|
||||
"X-Auth-Token": this.apiKey,
|
||||
"App-Version": "5",
|
||||
// Set the hash
|
||||
"B-Hash": hash.digest("hex"),
|
||||
"Content-Type": "application/json",
|
||||
Connection: "Keep-Alive",
|
||||
"Accept-Encoding": "gzip",
|
||||
"User-Agent": "okhttp/4.9.2",
|
||||
};
|
||||
}
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
import axios from "axios";
|
||||
import { log } from "../logs.js";
|
||||
|
||||
const baseUrl = "https://mobileapi.dsbcontrol.de";
|
||||
|
||||
export async function getAuthtoken(username, password) {
|
||||
const response = await axios.get(
|
||||
`${baseUrl}/authid?user=${username}&password=${password}&bundleid&appversion&osversion&pushid`,
|
||||
);
|
||||
if (response.data == "") throw "Wrong DSB username or password";
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getTimetables(authtoken) {
|
||||
const response = await axios.get(
|
||||
`${baseUrl}/dsbtimetables?authid=${authtoken}`,
|
||||
);
|
||||
const timetables = response.data;
|
||||
|
||||
const urls = [];
|
||||
timetables.forEach((timetable) => {
|
||||
const rawTimestamp = timetable.Date;
|
||||
// Convert the timestamp to the correct
|
||||
// format so new Date() accepts it
|
||||
const date = rawTimestamp.split(" ")[0].split(".").reverse().join("-");
|
||||
const time = rawTimestamp.split(" ")[1];
|
||||
const timestamp = date + " " + time;
|
||||
urls.push({
|
||||
title: timetable.Title,
|
||||
url: timetable.Childs[0].Detail,
|
||||
updatedAt: new Date(timestamp),
|
||||
});
|
||||
});
|
||||
|
||||
return urls;
|
||||
}
|
||||
|
||||
// List of files that include timetable data
|
||||
const dsbFiles = ["Schüler_Monitor - subst_001", "Schüler Morgen - subst_001"];
|
||||
|
||||
export class DSBClient {
|
||||
constructor(dsbUser, dsbPassword) {
|
||||
this.dsbUser = dsbUser;
|
||||
this.dsbPassword = dsbPassword;
|
||||
}
|
||||
async getFiles() {
|
||||
try {
|
||||
// Get authtoken
|
||||
const token = await getAuthtoken(this.dsbUser, this.dsbPassword);
|
||||
// Fetch available files
|
||||
const response = await getTimetables(token);
|
||||
// Filter files that should be parsed
|
||||
const timetables = response.filter((e) => dsbFiles.includes(e.title));
|
||||
// Fetch the contents
|
||||
const files = [];
|
||||
for (let timetable of timetables) {
|
||||
const result = await axios.request({
|
||||
method: "GET",
|
||||
url: timetable.url,
|
||||
responseEncoding: "binary",
|
||||
});
|
||||
files.push(result.data);
|
||||
}
|
||||
|
||||
return files;
|
||||
} catch (error) {
|
||||
log("Parser / DSB Mobile", "Error getting data: " + error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
import fs from "fs";
|
||||
|
||||
/*
|
||||
This file provider allows the application to use a local folder containing
|
||||
parsable (.html) files, instead of downloading them from the internet.
|
||||
|
||||
This can be especially useful for development.
|
||||
*/
|
||||
|
||||
export class FileSystemClient {
|
||||
constructor(path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
getFiles() {
|
||||
const contents = [];
|
||||
const files = fs.readdirSync(this.path);
|
||||
for (const file of files) {
|
||||
const data = fs.readFileSync(this.path + "/" + file).toString();
|
||||
contents.push(data);
|
||||
}
|
||||
return contents;
|
||||
}
|
||||
}
|
@ -1,286 +0,0 @@
|
||||
import Prisma from "@prisma/client";
|
||||
import { log, getLogPath } from "../logs.js";
|
||||
|
||||
const prisma = new Prisma.PrismaClient();
|
||||
|
||||
export class Parser {
|
||||
constructor(fileProvider, documentParser, interval) {
|
||||
this.fileProvider = fileProvider;
|
||||
this.documentParser = documentParser;
|
||||
|
||||
// Schedule plan updates
|
||||
setInterval(() => this.updatePlan(), interval);
|
||||
// Do the first update instantly
|
||||
this.updatePlan();
|
||||
}
|
||||
async updatePlan() {
|
||||
const startedAt = new Date();
|
||||
try {
|
||||
// Request substitution plan files using the fileProvider
|
||||
const files = await this.fileProvider.getFiles();
|
||||
const plans = [];
|
||||
// Parse them using the provided parser
|
||||
for (const file of files) {
|
||||
// Parse the substitution plan
|
||||
const parsed = this.documentParser(file);
|
||||
plans.push(parsed);
|
||||
}
|
||||
// Create a new parse event
|
||||
const parseEvent = await prisma.parseEvent.create({
|
||||
data: {
|
||||
logFile: getLogPath(),
|
||||
originalData: "",
|
||||
duration: new Date() - startedAt,
|
||||
succeeded: true,
|
||||
},
|
||||
});
|
||||
// Group plans by date to prevent having
|
||||
// multiple plan files with the same date
|
||||
const dayPlans = [];
|
||||
for (const plan of plans) {
|
||||
const foundPlan = dayPlans.find((e) => e.date == plan.date);
|
||||
if (!foundPlan) {
|
||||
// Make sure to not insert duplicate substitutions within a file
|
||||
const changes = structuredClone(plan.changes);
|
||||
const cleanedChanges = [];
|
||||
for (const change of changes) {
|
||||
const changeExists = cleanedChanges.find(
|
||||
(e) => JSON.stringify(e) == JSON.stringify(change),
|
||||
);
|
||||
if (!changeExists) {
|
||||
cleanedChanges.push(change);
|
||||
}
|
||||
// Use the new array of changes
|
||||
plan.changes = cleanedChanges;
|
||||
}
|
||||
dayPlans.push(plan);
|
||||
} else {
|
||||
for (const change of plan.changes) {
|
||||
// Make sure to not insert a substitution that already exists in the changes
|
||||
const changeExists = foundPlan.changes.find(
|
||||
(e) => JSON.stringify(e) == JSON.stringify(change),
|
||||
);
|
||||
if (!changeExists) {
|
||||
foundPlan.changes.push(change);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Insert substitutions of all substitution plans
|
||||
for (const plan of dayPlans) {
|
||||
await this.insertSubstitutions(plan, parseEvent);
|
||||
}
|
||||
} catch (error) {
|
||||
// If something went wrong, create a failed
|
||||
// parse event with the error message
|
||||
await prisma.parseEvent.create({
|
||||
data: {
|
||||
logFile: getLogPath(),
|
||||
originalData: error.toString(),
|
||||
duration: new Date() - startedAt,
|
||||
succeeded: false,
|
||||
},
|
||||
});
|
||||
// Log the error
|
||||
log("Parser / Main", "Parse event failed: " + error);
|
||||
}
|
||||
}
|
||||
async insertSubstitutions(parsedData, parseEvent) {
|
||||
const { date, changes } = parsedData;
|
||||
const classList = await prisma.class.findMany();
|
||||
|
||||
const knownSubstitutions = await prisma.substitution.findMany({
|
||||
where: {
|
||||
date: new Date(new Date(date).setUTCHours(0, 0, 0, 0)),
|
||||
removed: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Loop through every change of the substitution plan
|
||||
for (const change of changes) {
|
||||
// Find all classes the substitution belongs to
|
||||
const classes = this.getSubstitutionClasses(classList, change.class);
|
||||
// If the substitution does not belong to any classes known
|
||||
// by the server, use the provied class string instead
|
||||
if (classes.length == 0) classes.push(change.class || "unknown");
|
||||
|
||||
// Workaround: no correct match possible for subsitutions of this
|
||||
// type beacuse they do not have a class and a subject attribute
|
||||
if (change.type == "Sondereins." && !change.subject) {
|
||||
change.subject = change.notes;
|
||||
}
|
||||
if (change.type == "Sondereins." && !change.teacher) {
|
||||
change.teacher = change.changedTeacher;
|
||||
change.changedTeacher = "";
|
||||
}
|
||||
|
||||
// Check if a substitution exists in the database that
|
||||
// it similar enough to the entry in the substitution
|
||||
// plan to be considered the same substitution
|
||||
// (Date, Type, Lesson, Classes and Subject need to be the same)
|
||||
const matchingSubstitutionId = knownSubstitutions.findIndex(
|
||||
(substitution) => {
|
||||
return (
|
||||
substitution.date.getTime() ==
|
||||
new Date(date).setUTCHours(0, 0, 0, 0) &&
|
||||
substitution.rawType == change.type &&
|
||||
substitution.lesson == change.lesson &&
|
||||
classes.sort().join(",") == substitution.class.sort().join(",") &&
|
||||
substitution.changedSubject == change.subject &&
|
||||
substitution.teacher == (change.teacher || "")
|
||||
);
|
||||
},
|
||||
);
|
||||
const matchingSubstitution = knownSubstitutions[matchingSubstitutionId];
|
||||
|
||||
if (!matchingSubstitution) {
|
||||
// If the substitution is new, create it in the database
|
||||
const newSubstitution = await prisma.substitution.create({
|
||||
data: {
|
||||
class: classes,
|
||||
date: new Date(date),
|
||||
type:
|
||||
change.type == "Entfall" ||
|
||||
change.type == "eigenverantwortliches Arbeiten"
|
||||
? "cancellation"
|
||||
: "change",
|
||||
rawType: change.type,
|
||||
lesson: parseInt(change.lesson),
|
||||
teacher: change.teacher || "",
|
||||
changedTeacher: change.changedTeacher,
|
||||
changedRoom: change.room || undefined,
|
||||
changedSubject: change.subject,
|
||||
notes: change.notes,
|
||||
removed: false,
|
||||
},
|
||||
});
|
||||
// Also create a change entry for it
|
||||
const substitutionChange = await prisma.substitutionChange.create({
|
||||
data: {
|
||||
substitutionId: newSubstitution.id,
|
||||
type: "addition",
|
||||
changes: {
|
||||
class: classes,
|
||||
type: change.type == "Entfall" ? "cancellation" : "change",
|
||||
rawType: change.type,
|
||||
lesson: parseInt(change.lesson),
|
||||
date: new Date(date),
|
||||
notes: change.notes,
|
||||
teacher: change.teacher || "",
|
||||
change: {
|
||||
teacher: change.changedTeacher,
|
||||
room: change.room || undefined,
|
||||
subject: change.subject,
|
||||
},
|
||||
},
|
||||
parseEventId: parseEvent.id,
|
||||
},
|
||||
});
|
||||
log(
|
||||
"Insert / DB",
|
||||
`Created new substitution: S:${newSubstitution.id} C:${substitutionChange.id}`,
|
||||
);
|
||||
} else {
|
||||
// If the entry was updated, find the differences
|
||||
const differences = this.findDifferences(matchingSubstitution, change);
|
||||
if (Object.keys(differences).length > 0) {
|
||||
// If differences were found, update the entry in the database
|
||||
const prismaOptions = {
|
||||
where: {
|
||||
id: matchingSubstitution.id,
|
||||
},
|
||||
data: {},
|
||||
};
|
||||
if (differences.teacher)
|
||||
prismaOptions.data.changedTeacher = change.changedTeacher;
|
||||
if (differences.room) prismaOptions.data.changedRoom = change.room;
|
||||
if (differences.notes) prismaOptions.data.notes = change.notes;
|
||||
|
||||
await prisma.substitution.update(prismaOptions);
|
||||
// And create a change event for it
|
||||
const substitutionChange = await prisma.substitutionChange.create({
|
||||
data: {
|
||||
substitutionId: matchingSubstitution.id,
|
||||
type: "change",
|
||||
changes: differences,
|
||||
parseEventId: parseEvent.id,
|
||||
},
|
||||
});
|
||||
log(
|
||||
"Insert / DB",
|
||||
`Found changed substitution: S:${matchingSubstitution.id} C:${substitutionChange.id}`,
|
||||
);
|
||||
}
|
||||
// Remove the substitution from the array to later know the
|
||||
// entries that are not present in the substitution plan
|
||||
knownSubstitutions.splice(matchingSubstitutionId, 1);
|
||||
}
|
||||
}
|
||||
// Mark all entries as removed that were
|
||||
// not found in the substitution plan
|
||||
for (const remainingSubstitution of knownSubstitutions) {
|
||||
await prisma.substitution.update({
|
||||
where: {
|
||||
id: remainingSubstitution.id,
|
||||
},
|
||||
data: {
|
||||
removed: true,
|
||||
},
|
||||
});
|
||||
const substitutionChange = await prisma.substitutionChange.create({
|
||||
data: {
|
||||
substitutionId: remainingSubstitution.id,
|
||||
type: "deletion",
|
||||
changes: {
|
||||
class: remainingSubstitution.class,
|
||||
type: remainingSubstitution.type,
|
||||
rawType: remainingSubstitution.rawType,
|
||||
lesson: remainingSubstitution.lesson,
|
||||
date: remainingSubstitution.date.getTime(),
|
||||
notes: remainingSubstitution.notes,
|
||||
teacher: remainingSubstitution.teacher,
|
||||
change: {
|
||||
teacher: remainingSubstitution.changedTeacher,
|
||||
room: remainingSubstitution.changedRoom,
|
||||
subject: remainingSubstitution.changedSubject,
|
||||
},
|
||||
},
|
||||
parseEventId: parseEvent.id,
|
||||
},
|
||||
});
|
||||
log(
|
||||
"Insert / DB",
|
||||
`Deleted removed substitution: S:${remainingSubstitution.id} C:${substitutionChange.id}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
getSubstitutionClasses(classList, classString) {
|
||||
const matchingClasses = classList.filter((element) => {
|
||||
const regex = new RegExp(element.regex);
|
||||
return (classString || "").toLowerCase().match(regex);
|
||||
});
|
||||
return matchingClasses.map((e) => e.name);
|
||||
}
|
||||
findDifferences(currentSubstitution, newChange) {
|
||||
const differences = {};
|
||||
if (newChange.changedTeacher != currentSubstitution.changedTeacher) {
|
||||
differences.teacher = {
|
||||
before: currentSubstitution.changedTeacher,
|
||||
after: newChange.changedTeacher,
|
||||
};
|
||||
}
|
||||
if (newChange.room != currentSubstitution.changedRoom) {
|
||||
differences.room = {
|
||||
before: currentSubstitution.changedRoom,
|
||||
after: newChange.room,
|
||||
};
|
||||
}
|
||||
if (newChange.notes != currentSubstitution.notes) {
|
||||
differences.notes = {
|
||||
before: currentSubstitution.notes,
|
||||
after: newChange.notes,
|
||||
};
|
||||
}
|
||||
return differences;
|
||||
}
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
import * as cheerio from "cheerio";
|
||||
const titleTranslations = {
|
||||
"Klasse(n)": "class",
|
||||
Datum: "date",
|
||||
Stunde: "lesson",
|
||||
"(Lehrer)": "teacher",
|
||||
Vertreter: "changedTeacher",
|
||||
Fach: "subject",
|
||||
Raum: "room",
|
||||
Art: "type",
|
||||
Text: "notes",
|
||||
};
|
||||
|
||||
export function parseSubstitutionPlan(html) {
|
||||
const infos = {};
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const data = [];
|
||||
const tables = $("table.mon_list");
|
||||
tables.each((tableIndex, tableElement) => {
|
||||
// Extract the date, weekday and a/b week from the title
|
||||
const title = $(tableElement)
|
||||
.parent()
|
||||
.siblings(".mon_title")
|
||||
.text()
|
||||
.split(" ");
|
||||
const rawDate = title[0];
|
||||
const date = rawDate
|
||||
.split(".")
|
||||
.reverse()
|
||||
.map((e) => e.padStart(2, 0))
|
||||
.join("-");
|
||||
|
||||
if (tableIndex == 0) {
|
||||
infos.date = new Date(date).setUTCHours(0, 0, 0, 0);
|
||||
infos.week = title[3];
|
||||
|
||||
// Get the export timestamp
|
||||
const rawTimestamp = $(".mon_head")
|
||||
.first()
|
||||
.find("td")
|
||||
.text()
|
||||
.split("Stand: ")[1]
|
||||
.replace(/[\s\n]*$/g, "");
|
||||
const exportDate = rawTimestamp
|
||||
.split(" ")[0]
|
||||
.split(".")
|
||||
.reverse()
|
||||
.join("-");
|
||||
const timestamp = exportDate + " " + rawTimestamp.split(" ")[1];
|
||||
infos.updatedAt = new Date(timestamp).getTime();
|
||||
} else {
|
||||
// If there are multiple days in one file,
|
||||
// ignore all except the first one
|
||||
if (new Date(date).setUTCHours(0, 0, 0, 0) != infos.date) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const titles = [];
|
||||
const titleElements = $(tableElement).find("tr.list th");
|
||||
titleElements.each((index, titleElement) => {
|
||||
const title = $(titleElement).text();
|
||||
titles[index] = titleTranslations[title];
|
||||
});
|
||||
|
||||
const subsitutionTable = $(tableElement).find("tr.list");
|
||||
// Loop through each table row
|
||||
subsitutionTable.each((_rowcnt, row) => {
|
||||
const rowData = {};
|
||||
|
||||
// Find the columns and ignore empty ones
|
||||
const columns = $(row).find("td");
|
||||
if (columns.text() == "") return;
|
||||
// Ignore columns that include "Keine Vertretungen"
|
||||
// to have an empty array if there are no substitutions
|
||||
if (columns.text().includes("Keine Vertretungen")) return;
|
||||
|
||||
columns.each((columncnt, column) => {
|
||||
const text = $(column).text();
|
||||
// Clean the text by removing new lines, tabs, ...
|
||||
var cleantext = text.replace(/^\n\s*/g, "").replace(/\s*$/, "");
|
||||
if (cleantext == "" || cleantext == "---") cleantext = null;
|
||||
|
||||
const columntitle = titles[columncnt];
|
||||
rowData[columntitle] = cleantext;
|
||||
});
|
||||
|
||||
// Split change if it spans over multiple lessons
|
||||
const rawLesson = rowData.lesson || "0";
|
||||
const fromToLessons = rawLesson.match(/\d+/g).map(Number);
|
||||
const from = fromToLessons[0];
|
||||
const to = fromToLessons[1] || fromToLessons[0];
|
||||
|
||||
// Generate numbers from `from` to `to`
|
||||
const lessons = Array(to - from + 1)
|
||||
.fill()
|
||||
.map((_e, i) => i + from);
|
||||
|
||||
// Create new change for each lesson the change spans over
|
||||
for (const lesson of lessons) {
|
||||
rowData.lesson = lesson;
|
||||
data.push({ ...rowData });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
infos.changes = data;
|
||||
return infos;
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model Timetable {
|
||||
id Int @id @unique @default(autoincrement())
|
||||
title String @default("Default")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
class String
|
||||
validFrom DateTime @default(now())
|
||||
validUntil DateTime?
|
||||
data Json
|
||||
source String?
|
||||
trusted Boolean @default(true)
|
||||
}
|
||||
|
||||
model Substitution {
|
||||
id Int @id @unique @default(autoincrement())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
class String[]
|
||||
date DateTime
|
||||
type String
|
||||
rawType String @default("unknown")
|
||||
lesson Int
|
||||
teacher String
|
||||
changedTeacher String?
|
||||
changedRoom String?
|
||||
changedSubject String?
|
||||
notes String?
|
||||
removed Boolean @default(false)
|
||||
|
||||
SubstitutionChange SubstitutionChange[]
|
||||
}
|
||||
|
||||
model SubstitutionChange {
|
||||
id Int @id @unique @default(autoincrement())
|
||||
createdAt DateTime @default(now())
|
||||
substitution Substitution @relation(fields: [substitutionId], references: [id])
|
||||
substitutionId Int
|
||||
type String
|
||||
teacher String?
|
||||
changes Json?
|
||||
parseEvent ParseEvent @relation(fields: [parseEventId], references: [id])
|
||||
parseEventId Int
|
||||
}
|
||||
|
||||
model ParseEvent {
|
||||
id Int @id @unique @default(autoincrement())
|
||||
createdAt DateTime @default(now())
|
||||
logFile String
|
||||
originalData String
|
||||
duration Int
|
||||
succeeded Boolean
|
||||
|
||||
SubstitutionChange SubstitutionChange[]
|
||||
}
|
||||
|
||||
model Class {
|
||||
name String @id @unique
|
||||
regex String
|
||||
}
|
||||
|
||||
model Time {
|
||||
lesson Int @unique
|
||||
start DateTime
|
||||
end DateTime
|
||||
}
|
||||
|
||||
model Session {
|
||||
token String @id @unique @default(uuid())
|
||||
createdAt DateTime @default(now())
|
||||
validUntil DateTime
|
||||
appliedKeys Key[]
|
||||
}
|
||||
|
||||
model Key {
|
||||
key String @id @unique @default(uuid())
|
||||
createdAt DateTime? @default(now())
|
||||
validUntil DateTime?
|
||||
permissions String[]
|
||||
notes String?
|
||||
sessions Session[]
|
||||
}
|
Reference in New Issue
Block a user