Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
f10abd86b2 | |||
4b47cf9c96 | |||
0a7e92be06 | |||
df241f9a30 | |||
cfa1674421 | |||
dc8921c679 | |||
a19d7f8dfe | |||
43331ae226 |
@ -1,3 +1,4 @@
|
||||
node_modules
|
||||
server/node_modules
|
||||
Dockerfile
|
||||
.env
|
39
Dockerfile
39
Dockerfile
@ -1,20 +1,37 @@
|
||||
FROM node:lts-alpine
|
||||
FROM node:lts-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache --update git
|
||||
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
WORKDIR /app/server
|
||||
COPY server/package*.json ./
|
||||
RUN npm ci
|
||||
COPY server/prisma/schema.prisma ./prisma/schema.prisma
|
||||
RUN npx prisma generate
|
||||
|
||||
WORKDIR /app
|
||||
COPY ./ ./
|
||||
RUN npm run build
|
||||
|
||||
WORKDIR /app/server
|
||||
CMD ["node", "index.js"]
|
||||
FROM nginx:alpine-slim
|
||||
COPY <<EOF /etc/nginx/conf.d/default.conf
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index non-existent-to-prevent-caching.html;
|
||||
try_files \$uri @index;
|
||||
}
|
||||
|
||||
location @index {
|
||||
root /usr/share/nginx/html;
|
||||
add_header Cache-Control no-cache;
|
||||
expires 0;
|
||||
try_files /index.html =404;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
@ -3,20 +3,4 @@ services:
|
||||
app:
|
||||
build: .
|
||||
ports:
|
||||
- 3000:3000
|
||||
environment:
|
||||
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
|
||||
depends_on:
|
||||
- db
|
||||
env_file:
|
||||
- ./server/.env
|
||||
command: /bin/sh -c "npx prisma db push && node index.js"
|
||||
db:
|
||||
image: postgres
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
- 3000:80
|
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Timetable V2</title>
|
||||
<title>Timetable V2 Demo</title>
|
||||
<meta name="description" content="Timetable and Substitution plan viewer" />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="mask-icon" href="/favicon.svg" color="#212121" />
|
||||
|
1249
package-lock.json
generated
1249
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -14,12 +14,9 @@
|
||||
"downloadjs": "^1.4.7",
|
||||
"lucide-vue-next": "^0.268.0",
|
||||
"swiper": "^10.2.0",
|
||||
"tsparticles": "^3.0.2",
|
||||
"tsparticles-preset-snow": "^2.12.0",
|
||||
"vue": "^3.2.37",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-router": "^4.2.4",
|
||||
"vue3-particles": "^2.12.0"
|
||||
"vue-router": "^4.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.3.3",
|
||||
|
@ -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": "4",
|
||||
// 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[]
|
||||
}
|
48
src/App.vue
48
src/App.vue
@ -13,7 +13,7 @@ import {
|
||||
selectedDay,
|
||||
changeDay,
|
||||
changeDate,
|
||||
eventSettings,
|
||||
activeProfile,
|
||||
} from "@/store";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
@ -28,30 +28,12 @@ colorSchemeMedia.addEventListener(
|
||||
|
||||
const route = useRoute();
|
||||
const isDataView = computed(() => route.meta.dataView || false);
|
||||
|
||||
const particlesOptions = {
|
||||
preset: "snow",
|
||||
fullScreen: {
|
||||
zIndex: -1,
|
||||
},
|
||||
background: {
|
||||
color: "#0f0f0f",
|
||||
},
|
||||
};
|
||||
|
||||
import { loadSnowPreset } from "tsparticles-preset-snow";
|
||||
async function particlesInit(engine) {
|
||||
await loadSnowPreset(engine);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="app"
|
||||
:class="
|
||||
(eventSettings.disabled.includes('snow') ? '' : 'nobg ') +
|
||||
(theme == 'auto' ? `theme-${autoTheme}` : `theme-${theme}`)
|
||||
"
|
||||
:class="theme == 'auto' ? `theme-${autoTheme}` : `theme-${theme}`"
|
||||
>
|
||||
<TitleBar />
|
||||
<LoadingElement
|
||||
@ -69,14 +51,21 @@ async function particlesInit(engine) {
|
||||
v-show="isDataView"
|
||||
/>
|
||||
<main>
|
||||
<div>
|
||||
<div
|
||||
v-if="
|
||||
activeProfile.classFilter == 'Demo' ||
|
||||
$route.fullPath.startsWith('/settings')
|
||||
"
|
||||
class="demoNotice"
|
||||
>
|
||||
<span v-if="activeProfile.classFilter == 'Demo'">
|
||||
{{ $t("demoNotice") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wrapper">
|
||||
<RouterView />
|
||||
<Particles
|
||||
v-if="!eventSettings.disabled.includes('snow')"
|
||||
id="tsparticles"
|
||||
:particlesInit="particlesInit"
|
||||
:options="particlesOptions"
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
@ -120,7 +109,7 @@ main {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: grid;
|
||||
grid-template-rows: 1fr;
|
||||
grid-template-rows: auto 1fr;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -136,7 +125,8 @@ main {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nobg {
|
||||
background-color: unset !important;
|
||||
.demoNotice {
|
||||
padding: 15px 15px;
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,130 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
id="svg2"
|
||||
sodipodi:docname="TheresaKnott-Santa-Hat.svg"
|
||||
viewBox="0 0 410.44 285.17"
|
||||
sodipodi:version="0.32"
|
||||
version="1.0"
|
||||
inkscape:output_extension="org.inkscape.output.svg.inkscape"
|
||||
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
bordercolor="#666666"
|
||||
inkscape:pageshadow="2"
|
||||
guidetolerance="10.0"
|
||||
pagecolor="#ffffff"
|
||||
gridtolerance="10.0"
|
||||
inkscape:zoom="0.74879164"
|
||||
objecttolerance="10.0"
|
||||
borderopacity="1.0"
|
||||
inkscape:current-layer="svg2"
|
||||
inkscape:cx="295.14218"
|
||||
inkscape:cy="200.99049"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-height="1043"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:window-maximized="1" />
|
||||
<g
|
||||
id="layer1"
|
||||
transform="matrix(-1,0,0,1,419.33911,-24.384997)">
|
||||
<path
|
||||
id="path1309"
|
||||
style="fill:#d00000;fill-rule:evenodd;stroke:#000000;stroke-width:0.83349px"
|
||||
d="M 234.1,28.783 C 206,36.517 177.2,51.898 156.94,66.79 136.67,81.682 83.746,119.21 71.662,131.1 c -11.716,11.53 -25.555,15.55 -31.611,29.75 -2.335,5.48 15.837,22.01 19.398,23.72 9.241,4.46 24.304,-3.44 31.178,-3.91 22.303,-1.53 45.163,-4.5 67.763,10.79 -4.18,4.22 2.45,-0.32 -6.09,36.7 -3.01,13.05 -9.6,46.92 -1.51,53.38 8.04,6.42 12.95,-18.61 20.94,-14.78 -0.01,0.01 0,0.02 -0.01,0.03 v 0.02 c 0,0.01 -0.01,0.03 0,0.03 0,0 0.02,0 0.02,0.01 0.02,0 0.08,0 0.13,-0.01 0.01,0.01 0.03,0.01 0.05,0.01 0.06,0.03 0.11,0.06 0.16,0.09 l 0.05,-0.13 c 3.78,-0.97 33.26,-13.96 60.18,-8.74 15.17,2.94 39.37,-5.31 53.29,-9.62 22.07,-6.82 53.07,-12.99 72.73,-13.35 14.48,-0.26 27.58,-8.46 41.52,-9.41 8.22,-0.56 0.59,-27.07 0.42,-29.87 -0.81,-13.45 -17.56,-34.13 -26.19,-52.5 -8.62,-18.37 -23.08,-41.7 -39,-59.986 C 319.16,65.034 297.68,41.352 282.55,34.065 267.43,26.778 264.6,20.39 234.1,28.783 Z" />
|
||||
<path
|
||||
id="path5882"
|
||||
style="opacity:0.10112;fill:#0e0000;fill-rule:evenodd"
|
||||
d="m 166.96,93.344 c -18.24,4.416 -33.85,62.696 -56.32,72.826 -14.701,6.62 -54.752,16.69 -51.191,18.4 9.241,4.46 24.304,-3.44 31.178,-3.91 22.303,-1.53 45.163,-4.5 67.763,10.79 -4.18,4.22 2.45,-0.32 -6.09,36.7 -3.01,13.05 -9.6,46.92 -1.51,53.38 8.04,6.42 12.95,-18.61 20.94,-14.78 -0.01,0.01 0,0.02 -0.01,0.03 v 0.02 c 0,0.01 -0.01,0.03 0,0.03 0,0 0.02,0 0.02,0.01 0.02,0 0.08,0 0.13,-0.01 0.01,0.01 0.03,0.01 0.05,0.01 0.06,0.03 0.11,0.06 0.16,0.09 l 0.05,-0.13 c 3.78,-0.97 33.26,-13.96 60.18,-8.74 15.17,2.94 39.37,-5.31 53.29,-9.62 22.07,-6.82 53.07,-12.99 72.73,-13.35 14.48,-0.26 27.58,-8.46 41.52,-9.41 8.22,-0.56 -51.09,4.52 -27.45,-12.58 10.48,-7.58 -104.99,15.81 -149.32,-34.77 -43.89,-50.09 -37.45,-89.509 -56.12,-84.986 z" />
|
||||
<path
|
||||
id="path5878"
|
||||
style="opacity:0.26966;fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-width:0.83016"
|
||||
d="m 41.923,154.63 c 3.551,2.36 27.398,32.52 30.215,30.41 0.341,-3.33 -2.44,-4.64 -4.397,-5.02 2.62,-3.33 3.114,-6.82 -1.432,-7.11 -5.04,-0.89 -10.543,-1.52 -5.133,-6.94 2.498,-3.04 -0.168,-8.44 -1.701,-11.18 -4.03,-2.18 -7.066,-6.11 -8.718,-8.79" />
|
||||
<path
|
||||
id="path5003"
|
||||
style="opacity:0.29404;fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-width:0.60143px"
|
||||
d="m 365.6,219.59 c -7.45,-2.65 -16.8,-5.7 -23.39,-5.29 -4.53,-3.82 -8,0.89 -11.92,2.14 -0.66,-7.36 -11.51,3.11 -16.29,3.17 -3.4,1.95 -1.06,-5.8 -7.14,-2.85 -7.55,0.71 -12.23,1.23 -17.83,3.66 -1.35,8.01 -13.45,12.53 -15.27,3.74 -6.59,2.88 -18.86,8.45 -18.94,-0.64 -10.57,8.2 -10.9,15.6 -21.22,19.56 0.92,-7.1 -22.37,-10.91 -25.54,-3.47 -6.39,2.57 1.33,-9.32 -7.91,-6.05 -10.4,-3.02 -4.34,17.93 -25.55,9.37 -11.81,-0.38 -5.27,16.71 -13.06,23.88 -2.08,7.53 -11.08,-2.63 -14.73,-2.06 0.85,11.92 3.41,7.88 0.72,15.99 -1.61,10.17 -3.75,15.26 7.43,17.38 3.62,-4.91 -0.18,-2.83 0.63,6.21 6.46,-1.88 14.94,-12.64 14.07,-1.58 4.39,-6.67 18.26,-10.15 18.08,-17.34 7.03,0.74 14.91,8.58 20.83,-0.95 5.95,-7.04 14.51,-10.31 14.6,-4.87 7.23,6.76 7.95,5.39 15.48,3.67 -1.94,-8.71 17.59,-0.6 7.28,-9.27 4.1,-4.11 15.19,-6.5 21.59,-5.08 5.67,8.26 22.95,8.94 25.64,-1.74 0.56,-8.41 13.68,-11.28 18.58,-5.47 8.53,-7.48 21.99,3.39 29.33,-2.35 -6.6,-8.56 9.32,-14.16 12.3,-7.59 5.84,-3.14 11.27,-14.42 17.66,-6.34 7.35,-2.4 16.33,-5.73 22.55,-8.22 4.66,7.69 16.74,-0.23 11.51,-9.54 -2.84,-6.65 9.31,3.28 6,-5.16 -3.76,-9.08 -9.04,-21.69 -15.51,-21.28 -8.82,-2.2 -14.04,3.75 -14.41,12.16 -0.11,-6.14 -10.52,-0.15 -15.98,2.08 l -2.99,0.71 -3.58,-2.52" />
|
||||
<path
|
||||
id="path1307"
|
||||
style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:0.60143px"
|
||||
d="m 370.52,221.83 c -7.45,-2.64 -14.35,-3.01 -20.94,-2.61 -4.53,-3.82 -9.9,-2.56 -13.82,-1.31 -0.65,-7.36 -9.61,6.57 -14.39,6.63 -3.4,1.95 -1.06,-5.8 -7.13,-2.86 -7.56,0.72 -16.32,-3.97 -21.93,-1.54 -1.35,8.01 -13.45,12.53 -15.26,3.74 -6.6,2.88 -14.78,13.65 -14.86,4.57 -12.39,-0.54 -17.69,11.63 -28.01,15.59 0.92,-7.1 -15.58,-6.95 -18.75,0.5 -6.39,2.57 -1.7,-13.44 -10.93,-10.17 -10.4,-3.03 -10.12,17.69 -22.53,13.48 -11.81,-0.38 -10.16,11.36 -17.95,18.52 -2.08,7.53 -6.19,2.74 -9.84,3.3 6,8.52 -18.39,6.74 -11.15,19.87 -1.6,10.17 0.85,17.47 12.03,19.59 3.62,-4.9 7.09,-8.92 7.9,0.13 6.46,-1.88 14.95,-12.64 14.08,-1.58 4.38,-6.68 18.25,-10.15 18.08,-17.34 7.02,0.74 14.9,8.58 20.82,-0.95 5.96,-7.05 6.94,-1.3 7.03,4.14 7.23,6.75 15.53,-3.63 23.05,-5.35 -1.94,-8.71 17.6,-0.59 7.28,-9.27 4.1,-4.1 15.19,-6.5 21.6,-5.07 5.62,5.33 22.94,8.93 25.63,-1.75 0.56,-8.4 13.68,-11.28 18.59,-5.47 8.52,-7.48 21.98,3.39 29.32,-2.34 -6.6,-8.57 9.32,-14.16 12.3,-7.6 5.84,-3.13 11.28,-14.42 17.67,-6.34 7.35,-2.39 16.32,-5.72 22.54,-8.22 4.66,7.69 16.75,-0.22 11.51,-9.53 -2.84,-6.65 9.31,3.27 6,-5.17 0.08,-9.65 -3.56,-24.6 -15.51,-21.27 -8.81,-2.2 -14.04,3.74 -14.41,12.16 -0.11,-6.14 -14.02,-3.92 -19.48,-1.7 l -1.79,0.95 -1.28,1.01" />
|
||||
<path
|
||||
id="path3126"
|
||||
style="fill:none;stroke:#000000;stroke-width:0.83349px"
|
||||
d="m 155.68,210.89 c 3.14,-40.85 23.79,-59.45 20.15,-92.12 -2.15,-19.313 -19.54,-4.68 -13.87,13.44" />
|
||||
<path
|
||||
id="path3249"
|
||||
style="opacity:0.11236;fill:#000000;fill-rule:evenodd"
|
||||
d="m 163.98,266.62 c -2.08,7.53 -6.2,2.74 -9.84,3.3 6,8.51 -18.39,6.74 -11.15,19.87 -1.61,10.17 0.85,17.47 12.03,19.59 3.62,-4.9 7.09,-8.92 7.9,0.13 6.46,-1.88 14.94,-12.64 14.07,-1.58 4.38,-6.68 18.26,-10.16 18.08,-17.34 7.02,0.73 14.57,7.35 20.5,-2.19 5.95,-7.04 7.27,-0.06 7.36,5.38 7.23,6.75 15.52,-3.63 23.04,-5.35 -1.93,-8.71 8.87,-1.54 7.29,-9.27 -1.04,-5.06 33.64,-3.56 28.65,-8.51 -14.9,-14.79 -38.19,7.49 -41.56,6.07 -3.36,-1.42 -7.09,-0.56 -2.38,-2.72 4.71,-2.16 -6.74,1.39 -16.28,-1.99 -9.53,-3.39 -16.51,0.89 -17.15,9.34 -7.18,-4.47 -7.7,0.61 -18.88,3.72 7.12,-10.43 -12.6,-2.77 -12.6,-2.77 0,0 3.43,-11.43 -2.74,-9.92 3.32,-5.97 -0.37,-7.8 -0.37,-7.8 0,0 1.82,-5.12 -5.97,2.04 z" />
|
||||
<path
|
||||
id="path2251"
|
||||
style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:0.8136"
|
||||
d="m 46.945,146.24 c -1.302,-2.31 -5.358,-2.95 -5.88,-4.92 0.907,-2.28 -2.659,-0.62 -3.361,-1.88 -4.195,3.43 -9.985,4.92 -13.093,9.64 1.653,-0.36 1.649,-2.78 -0.157,-0.99 -3.401,1.69 -6.688,4.65 -5.409,8.89 0.309,2.17 -2.426,4.04 0.913,4.56 -0.435,3.48 -5.655,3.33 -5.658,6.4 1.155,3.35 0.647,6.84 -2.101,9.19 -2.7176,2.54 -1.598,6.28 -1.499,9.37 -1.3672,2.07 -2.1366,3.99 -0.335,5.95 0.382,3.55 3.327,5.9 3.349,9.59 1.24,1.94 3.727,1.82 4.997,4.01 3.71,0.31 3.983,5.49 7.517,6.21 3.036,0.99 6.629,0.3 9.365,1.03 2.243,1.82 4.819,3.09 7.643,3.6 3.551,2.26 4.758,-3.37 6.907,-5.18 0.112,-2.83 2.146,-3.05 3.509,-0.67 3.101,2.11 6.314,-1.51 8.537,-3.43 2.173,-2.37 4.612,-4.42 7.032,-6.5 3.436,-3.88 4.656,-9.43 4.464,-14.53 0.34,-3.2 -2.635,-3.38 -4.591,-3.74 0.885,-2.69 3.113,-6.54 -1.433,-6.83 -2.607,-0.68 -8.517,-3.6 -5.132,-6.66 2.498,-2.92 -0.94,-6.85 -2.474,-9.49 -2.656,-2.11 -4.199,-5.06 -5.851,-7.63 -3.197,-0.51 -6.194,-2.45 -7.009,-5.84 -0.854,-2.7 -0.237,-0.7 0.033,0.61" />
|
||||
<path
|
||||
id="path4128"
|
||||
style="opacity:0.095506;fill:#000000;fill-rule:evenodd"
|
||||
d="m 20.405,181.85 c -1.601,-1.97 4.721,-1.05 4.199,-3.01 0.907,-2.28 -0.178,-2.38 -0.881,-3.63 -4.194,3.43 -3.206,-4.71 -6.314,0.02 1.652,-0.37 1.381,-6.34 -0.425,-4.55 -3.401,1.69 -2.681,-5.81 -2.684,-2.74 1.155,3.35 0.647,6.84 -2.101,9.19 -2.7176,2.54 -1.598,6.28 -1.499,9.37 -1.3672,2.07 -2.1366,3.99 -0.335,5.95 0.382,3.55 3.327,5.9 3.349,9.59 1.24,1.94 3.727,1.82 4.997,4.01 3.71,0.31 3.983,5.49 7.517,6.21 3.036,0.99 6.629,0.3 9.365,1.03 2.243,1.82 4.819,3.09 7.643,3.6 3.551,2.26 4.758,-3.37 6.907,-5.18 0.112,-2.83 2.146,-3.05 3.509,-0.67 3.101,2.11 6.314,-1.51 8.537,-3.43 2.173,-2.37 -8.346,0.36 -5.926,-1.72 3.436,-3.88 -7.752,5.42 -7.945,0.31 0.341,-3.2 -12.37,4.07 -9.678,-1.55 0.884,-2.69 1.536,-0.75 -3.01,-1.04 -2.607,-0.68 -9.804,-5.49 -6.419,-8.56 2.498,-2.92 -1.342,0.46 -2.876,-2.18 -2.656,-2.11 -0.959,-0.95 -2.611,-3.52 -3.198,-0.51 -1.792,-1.82 -2.607,-5.21 -1.383,3.98 -0.982,-3.61 -0.712,-2.29 z" />
|
||||
<path
|
||||
id="path5880"
|
||||
style="fill:#000000;fill-opacity:0.22346;fill-rule:evenodd"
|
||||
d="m 157.67,192.36 c 0,0 13.14,-33.51 16.3,-46.88 2.96,-12.51 2.58,-31.6 -1.85,-35.14 -4.43,-3.53 -10.25,2.58 -10.86,9.82 -0.61,7.24 -1.13,9.06 1.84,22.87 1.35,6.25 0.68,21.09 -1.15,28.09 -1.83,7 -5.83,22.95 -4.28,21.24 z" />
|
||||
</g>
|
||||
<metadata
|
||||
id="metadata1">
|
||||
<rdf:RDF>
|
||||
<cc:Work>
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<cc:license
|
||||
rdf:resource="http://creativecommons.org/licenses/publicdomain/" />
|
||||
<dc:publisher>
|
||||
<cc:Agent
|
||||
rdf:about="http://openclipart.org/">
|
||||
<dc:title>Openclipart</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:publisher>
|
||||
<dc:title>Santa Hat</dc:title>
|
||||
<dc:date>2006-10-25T09:09:50</dc:date>
|
||||
<dc:description>A santa hat for christmas sutable for putting on other drawings.</dc:description>
|
||||
<dc:source>https://openclipart.org/detail/889/santa-hat-by-theresaknott</dc:source>
|
||||
<dc:creator>
|
||||
<cc:Agent>
|
||||
<dc:title>TheresaKnott</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:creator>
|
||||
<dc:subject>
|
||||
<rdf:Bag>
|
||||
<rdf:li>christmas</rdf:li>
|
||||
<rdf:li>hat</rdf:li>
|
||||
<rdf:li>santa</rdf:li>
|
||||
</rdf:Bag>
|
||||
</dc:subject>
|
||||
</cc:Work>
|
||||
<cc:License
|
||||
rdf:about="http://creativecommons.org/licenses/publicdomain/">
|
||||
<cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#Reproduction" />
|
||||
<cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#Distribution" />
|
||||
<cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
|
||||
</cc:License>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
</svg>
|
Before Width: | Height: | Size: 11 KiB |
@ -16,10 +16,7 @@ function goBack() {
|
||||
|
||||
<template>
|
||||
<div class="titlebar">
|
||||
<span class="title"
|
||||
>{{ $t(routeName || "")
|
||||
}}<img src="../assets/santa-hat.svg" class="hat" width="30"
|
||||
/></span>
|
||||
<span class="title">{{ $t(routeName || "") }}</span>
|
||||
<div class="settings">
|
||||
<RouterLink
|
||||
to="/settings"
|
||||
@ -68,12 +65,4 @@ a {
|
||||
a.router-link-active {
|
||||
background-color: var(--titlebar-element-active-color);
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.hat {
|
||||
transform: translate(-10px, -4px) rotate(20deg);
|
||||
}
|
||||
</style>
|
||||
|
334
src/demoData.js
Normal file
334
src/demoData.js
Normal file
@ -0,0 +1,334 @@
|
||||
export const DEMO_SESSION_INFO = {
|
||||
authenticated: true,
|
||||
appliedKeys: [],
|
||||
permissions: [],
|
||||
};
|
||||
|
||||
export const DEMO_CLASS_LIST = ["Demo", "Empty"];
|
||||
|
||||
export const DEMO_TIMETABLE = {
|
||||
timetables: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Demo",
|
||||
createdAt: "2023-08-28T08:00:53.233Z",
|
||||
updatedAt: "2025-01-07T11:37:42.590Z",
|
||||
class: "Demo",
|
||||
validFrom: "2023-08-28T08:00:53.233Z",
|
||||
validUntil: null,
|
||||
data: [
|
||||
[
|
||||
[{}],
|
||||
[{ room: "R 206", length: 1, subject: "MA1", teacher: "Wen" }],
|
||||
[{ room: "R 307", length: 2, subject: "en1", teacher: "Fre" }],
|
||||
[{ room: "R 005", length: 2, subject: "de2", teacher: "Str" }],
|
||||
],
|
||||
[
|
||||
[{ room: "R 107", length: 2, subject: "IN2", teacher: "Kom" }],
|
||||
[{ room: "R 206", length: 2, subject: "MA1", teacher: "Wen" }],
|
||||
[{ room: "R 403", subject: "geo2", teacher: "Spl" }],
|
||||
[{ room: "", length: 1, subject: "", teacher: "" }],
|
||||
[{ room: "R 007", subject: "Sp-Th", teacher: "Bun" }],
|
||||
[{ room: "R 313", subject: "pw1", teacher: "Göl" }],
|
||||
],
|
||||
[
|
||||
[{ room: "R 313", length: 2, subject: "pw1", teacher: "Göl" }],
|
||||
[{ room: "R 314", length: 2, subject: "ge1", teacher: "Mog" }],
|
||||
[{ room: "R 307", subject: "en1", teacher: "Fre" }],
|
||||
[{ room: "R 403", length: 2, subject: "geo2", teacher: "Spl" }],
|
||||
[{}],
|
||||
[{ room: "R 002", length: 2, subject: "Sp-Fit", teacher: "Gan" }],
|
||||
],
|
||||
[
|
||||
[{ room: "R 206", length: 2, subject: "MA1", teacher: "Wen" }],
|
||||
[{ room: "R 107", length: 2, subject: "IN2", teacher: "Kom" }],
|
||||
[{ room: "R 209", subject: "ph1", teacher: "And" }],
|
||||
[{ room: "R 005", length: 2, subject: "Sp-Th", teacher: "Bun" }],
|
||||
],
|
||||
[
|
||||
[{ room: "R 107", length: 2, subject: "IN2", teacher: "Kom" }],
|
||||
[{ room: "R 314", length: 1, subject: "ge1", teacher: "Mog" }],
|
||||
[{ room: "R 005", subject: "de2", teacher: "Str" }],
|
||||
[{ room: "R 209", length: 2, subject: "ph1", teacher: "And" }],
|
||||
],
|
||||
],
|
||||
source: "Demo Provider",
|
||||
trusted: true,
|
||||
},
|
||||
],
|
||||
times: [
|
||||
{
|
||||
lesson: 1,
|
||||
start: "1970-01-01T07:00:00.000Z",
|
||||
end: "1970-01-01T07:45:00.000Z",
|
||||
},
|
||||
{
|
||||
lesson: 3,
|
||||
start: "1970-01-01T08:50:00.000Z",
|
||||
end: "1970-01-01T09:35:00.000Z",
|
||||
},
|
||||
{
|
||||
lesson: 5,
|
||||
start: "1970-01-01T11:10:00.000Z",
|
||||
end: "1970-01-01T11:55:00.000Z",
|
||||
},
|
||||
{
|
||||
lesson: 6,
|
||||
start: "1970-01-01T12:00:00.000Z",
|
||||
end: "1970-01-01T12:45:00.000Z",
|
||||
},
|
||||
{
|
||||
lesson: 7,
|
||||
start: "1970-01-01T12:50:00.000Z",
|
||||
end: "1970-01-01T13:35:00.000Z",
|
||||
},
|
||||
{
|
||||
lesson: 8,
|
||||
start: "1970-01-01T13:40:00.000Z",
|
||||
end: "1970-01-01T14:25:00.000Z",
|
||||
},
|
||||
{
|
||||
lesson: 9,
|
||||
start: "1970-01-01T14:30:00.000Z",
|
||||
end: "1970-01-01T15:15:00.000Z",
|
||||
},
|
||||
{
|
||||
lesson: 10,
|
||||
start: "1970-01-01T15:20:00.000Z",
|
||||
end: "1970-01-01T16:05:00.000Z",
|
||||
},
|
||||
{
|
||||
lesson: 11,
|
||||
start: "1970-01-01T16:10:00.000Z",
|
||||
end: "1970-01-01T16:55:00.000Z",
|
||||
},
|
||||
{
|
||||
lesson: 2,
|
||||
start: "1970-01-01T07:45:00.000Z",
|
||||
end: "1970-01-01T08:30:00.000Z",
|
||||
},
|
||||
{
|
||||
lesson: 4,
|
||||
start: "1970-01-01T09:40:00.000Z",
|
||||
end: "1970-01-01T10:25:00.000Z",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export function getDemoSubstitutions(date) {
|
||||
let weekday = new Date(date).getDay();
|
||||
|
||||
switch (weekday) {
|
||||
case 1: {
|
||||
return [
|
||||
{
|
||||
id: 0,
|
||||
class: ["Demo"],
|
||||
type: "cancellation",
|
||||
rawType: "Entfall",
|
||||
lesson: 5,
|
||||
date: date,
|
||||
notes: null,
|
||||
teacher: "Str",
|
||||
change: {},
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
class: ["Demo"],
|
||||
type: "cancellation",
|
||||
rawType: "Entfall",
|
||||
lesson: 6,
|
||||
date: date,
|
||||
notes: null,
|
||||
teacher: "Str",
|
||||
change: {},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
class: ["Demo"],
|
||||
type: "change",
|
||||
rawType: "Raum-Vtr.",
|
||||
lesson: 3,
|
||||
date: date,
|
||||
notes: null,
|
||||
teacher: "Fre",
|
||||
change: {
|
||||
room: "308",
|
||||
teacher: "Fre",
|
||||
subject: "en1",
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
case 2: {
|
||||
return [
|
||||
{
|
||||
id: 3,
|
||||
class: ["Demo"],
|
||||
type: "cancellation",
|
||||
rawType: "Entfall",
|
||||
lesson: 7,
|
||||
date: date,
|
||||
notes: null,
|
||||
teacher: "Bun",
|
||||
change: {},
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
class: ["Demo"],
|
||||
type: "change",
|
||||
rawType: "Vertretung",
|
||||
lesson: 8,
|
||||
date: date,
|
||||
notes: null,
|
||||
teacher: "Bun",
|
||||
change: {
|
||||
room: "007",
|
||||
teacher: "Aci",
|
||||
subject: "Sp-Th",
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
case 3: {
|
||||
return [
|
||||
{
|
||||
id: 5,
|
||||
class: ["Demo"],
|
||||
type: "cancellation",
|
||||
rawType: "Entfall",
|
||||
lesson: 9,
|
||||
date: date,
|
||||
notes: "Aufgaben im Sekretariat",
|
||||
teacher: "Gan",
|
||||
change: {},
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
class: ["Demo"],
|
||||
type: "cancellation",
|
||||
rawType: "Entfall",
|
||||
lesson: 10,
|
||||
date: date,
|
||||
notes: null,
|
||||
teacher: "Gan",
|
||||
change: {},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
export function getDemoHistory(date) {
|
||||
let weekday = new Date(date).getDay();
|
||||
|
||||
switch (weekday) {
|
||||
case 1: {
|
||||
return [
|
||||
...getDemoSubstitutions(date).map((e) => substitutionToChange(e)),
|
||||
];
|
||||
}
|
||||
case 2: {
|
||||
return [
|
||||
...getDemoSubstitutions(date).map((e) => substitutionToChange(e)),
|
||||
{
|
||||
id: 101,
|
||||
type: "deletion",
|
||||
class: ["Demo"],
|
||||
substitutionId: 100,
|
||||
lesson: 5,
|
||||
updatedAt: date - 47800000,
|
||||
date: date,
|
||||
teacher: null,
|
||||
change: {
|
||||
date: date,
|
||||
type: "cancellation",
|
||||
class: ["Demo"],
|
||||
notes: null,
|
||||
change: {},
|
||||
lesson: 5,
|
||||
rawType: "Entfall",
|
||||
teacher: "Spl",
|
||||
},
|
||||
parseEventId: 0,
|
||||
},
|
||||
{
|
||||
id: 100,
|
||||
type: "addition",
|
||||
class: ["Demo"],
|
||||
substitutionId: 100,
|
||||
lesson: 5,
|
||||
updatedAt: date - 57800000,
|
||||
date: date,
|
||||
teacher: null,
|
||||
change: {
|
||||
date: date,
|
||||
type: "cancellation",
|
||||
class: ["Demo"],
|
||||
notes: null,
|
||||
change: {},
|
||||
lesson: 5,
|
||||
rawType: "Entfall",
|
||||
teacher: "Spl",
|
||||
},
|
||||
parseEventId: 0,
|
||||
},
|
||||
];
|
||||
}
|
||||
case 3: {
|
||||
return [
|
||||
{
|
||||
id: 102,
|
||||
type: "change",
|
||||
class: ["Demo"],
|
||||
substitutionId: 5,
|
||||
lesson: 9,
|
||||
updatedAt: date - 57800000,
|
||||
date: date,
|
||||
teacher: null,
|
||||
change: {
|
||||
notes: {
|
||||
before: null,
|
||||
after: "Aufgaben im Sekretariat",
|
||||
},
|
||||
},
|
||||
parseEventId: 0,
|
||||
},
|
||||
...getDemoSubstitutions(date)
|
||||
.map((e) => substitutionToChange(e))
|
||||
.map((e) => {
|
||||
e.change.notes = null;
|
||||
return e;
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function substitutionToChange(substitution) {
|
||||
return {
|
||||
id: substitution.id,
|
||||
type: "addition",
|
||||
class: substitution.class,
|
||||
substitutionId: substitution.id,
|
||||
lesson: substitution.lesson,
|
||||
updatedAt: substitution.date - 46800000,
|
||||
date: substitution.date,
|
||||
teacher: null,
|
||||
change: {
|
||||
date: substitution.date,
|
||||
type: substitution.type,
|
||||
class: substitution.class,
|
||||
notes: substitution.notes,
|
||||
change: substitution.change,
|
||||
lesson: substitution.lesson,
|
||||
rawType: substitution.rawType,
|
||||
teacher: substitution.teacher,
|
||||
},
|
||||
parseEventId: 0,
|
||||
};
|
||||
}
|
@ -3,13 +3,11 @@ import App from "@/App.vue";
|
||||
import router from "@/router";
|
||||
import { registerSW } from "virtual:pwa-register";
|
||||
import i18n from "@/i18n";
|
||||
import Particles from "vue3-particles";
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(router);
|
||||
app.use(i18n);
|
||||
app.use(Particles);
|
||||
|
||||
app.mount("#app");
|
||||
|
||||
|
@ -16,7 +16,6 @@ import AppearanceSettings from "@/views/settings/AppearanceSettings.vue";
|
||||
import ProfileSettings from "@/views/settings/ProfileSettings.vue";
|
||||
import KeySettings from "@/views/settings/KeySettings.vue";
|
||||
import AdminSettings from "@/views/settings/AdminSettings.vue";
|
||||
import EventSettings from "@/views/settings/EventSettings.vue";
|
||||
import AboutPage from "@/views/settings/AboutPage.vue";
|
||||
|
||||
const router = createRouter({
|
||||
@ -90,11 +89,6 @@ const router = createRouter({
|
||||
name: "title.settings.admin",
|
||||
component: AdminSettings,
|
||||
},
|
||||
{
|
||||
path: "event",
|
||||
name: "Event settings",
|
||||
component: EventSettings,
|
||||
},
|
||||
{
|
||||
path: "about",
|
||||
name: "title.settings.about",
|
||||
|
81
src/store.js
81
src/store.js
@ -1,6 +1,13 @@
|
||||
import { ref, watch, computed } from "vue";
|
||||
import { getNextAndPrevDay, setUTCMidnight } from "@/util";
|
||||
import i18n from "@/i18n";
|
||||
import {
|
||||
DEMO_CLASS_LIST,
|
||||
DEMO_SESSION_INFO,
|
||||
DEMO_TIMETABLE,
|
||||
getDemoHistory,
|
||||
getDemoSubstitutions,
|
||||
} from "./demoData";
|
||||
|
||||
/* Router */
|
||||
export const shouldLogin = ref(false);
|
||||
@ -15,7 +22,7 @@ export const profiles = ref(
|
||||
id: 0,
|
||||
name: "Default Profile",
|
||||
classFilter: "none",
|
||||
timetableId: "none",
|
||||
timetableId: 1,
|
||||
timetableGroups: [],
|
||||
},
|
||||
],
|
||||
@ -29,9 +36,6 @@ export const activeProfile = computed(() => {
|
||||
export const activeProfileId = ref(
|
||||
localStorage.getItem("activeProfile") || profiles.value[0].id,
|
||||
);
|
||||
export const eventSettings = ref(
|
||||
JSON.parse(localStorage.getItem("eventSettings")) || { disabled: [] },
|
||||
);
|
||||
|
||||
watch(
|
||||
profiles,
|
||||
@ -49,13 +53,6 @@ watch(
|
||||
fetchData(getNextAndPrevDay(selectedDate.value), false);
|
||||
},
|
||||
);
|
||||
watch(
|
||||
eventSettings,
|
||||
(newValue) => {
|
||||
localStorage.setItem("eventSettings", JSON.stringify(newValue));
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
export const cachedTimetables = ref(
|
||||
JSON.parse(localStorage.getItem("cachedTimetables")) || {},
|
||||
@ -172,16 +169,7 @@ watch(selectedDate, () =>
|
||||
|
||||
export async function fetchSessionInfo() {
|
||||
try {
|
||||
const checkResponse = await fetch(`${baseUrl}/info`);
|
||||
if (checkResponse.status == 401) {
|
||||
shouldLogin.value = true;
|
||||
return false;
|
||||
} else if (checkResponse.status != 200) {
|
||||
console.log("Other error while fetching data: " + checkResponse.status);
|
||||
return false;
|
||||
} else {
|
||||
sessionInfo.value = await checkResponse.json();
|
||||
}
|
||||
sessionInfo.value = DEMO_SESSION_INFO;
|
||||
} catch {
|
||||
console.log("Error while fetching data: No internet connection!");
|
||||
return false;
|
||||
@ -190,53 +178,32 @@ export async function fetchSessionInfo() {
|
||||
}
|
||||
|
||||
export async function fetchClassList() {
|
||||
const classListResponse = await fetch(`${baseUrl}/classes`);
|
||||
const classListData = await classListResponse.json();
|
||||
classList.value = classListData;
|
||||
classList.value = DEMO_CLASS_LIST;
|
||||
}
|
||||
|
||||
export async function fetchTimetables() {
|
||||
const timetableResponse = await fetch(
|
||||
`${baseUrl}/timetable?class=${activeProfile.value.classFilter}`,
|
||||
);
|
||||
const timetableData = await timetableResponse.json();
|
||||
if (timetableData.error) {
|
||||
console.warn("API Error: " + timetableData.error);
|
||||
timetables.value = [];
|
||||
if (activeProfile.value.classFilter == "Demo") {
|
||||
timetables.value = DEMO_TIMETABLE.timetables;
|
||||
} else {
|
||||
timetables.value = timetableData.timetables;
|
||||
times.value = timetableData.times;
|
||||
|
||||
cachedTimetables.value[activeProfileId.value] =
|
||||
structuredClone(timetableData);
|
||||
for (const timetable of cachedTimetables.value[activeProfileId.value]
|
||||
.timetables) {
|
||||
timetable.fromCache = true;
|
||||
}
|
||||
timetables.value = [];
|
||||
}
|
||||
times.value = DEMO_TIMETABLE.times;
|
||||
}
|
||||
|
||||
export async function fetchSubstitutions(day) {
|
||||
const requestDate = `?date=${day}`;
|
||||
const substitutionResponse = await fetch(
|
||||
activeProfile.value.classFilter == "none"
|
||||
? `${baseUrl}/substitutions${requestDate}`
|
||||
: `${baseUrl}/substitutions${requestDate}&class=${activeProfile.value.classFilter}`,
|
||||
);
|
||||
const substitutionData = await substitutionResponse.json();
|
||||
substitutions.value[day] = substitutionData;
|
||||
if (activeProfile.value.classFilter == "Demo") {
|
||||
substitutions.value[day] = getDemoSubstitutions(day);
|
||||
} else {
|
||||
substitutions.value[day] = [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchHistory(day) {
|
||||
const requestDate = `?date=${day}`;
|
||||
const historyResponse = await fetch(
|
||||
activeProfile.value.classFilter == "none"
|
||||
? `${baseUrl}/history${requestDate}`
|
||||
: `${baseUrl}/history${requestDate}&class=${activeProfile.value.classFilter}`,
|
||||
);
|
||||
const historyData = await historyResponse.json();
|
||||
if (historyData.error) console.warn("API Error: " + historyData.error);
|
||||
else history.value[day] = historyData;
|
||||
if (activeProfile.value.classFilter == "Demo") {
|
||||
history.value[day] = getDemoHistory(day);
|
||||
} else {
|
||||
history.value[day] = [];
|
||||
}
|
||||
}
|
||||
|
||||
/* Preprocess the timetable data */
|
||||
|
@ -1,5 +1,7 @@
|
||||
export const strings = {
|
||||
en: {
|
||||
demoNotice:
|
||||
"This is a demo instance of Timetable V2. The displayed data is entirely fictional.",
|
||||
title: {
|
||||
timetable: "Timetable",
|
||||
substitutions: "Substitutions",
|
||||
@ -133,6 +135,8 @@ export const strings = {
|
||||
},
|
||||
},
|
||||
de: {
|
||||
demoNotice:
|
||||
"Dies ist eine Demo-Instanz von Timetable V2. Die angezeigten Daten sind nicht echt.",
|
||||
title: {
|
||||
timetable: "Stundenplan",
|
||||
substitutions: "Vertretungsplan",
|
||||
|
@ -2,7 +2,6 @@
|
||||
import ScrollableContainer from "@/components/scrollable-container.vue";
|
||||
import PageCard from "@/components/settings/page-card.vue";
|
||||
import { hasPermission } from "@/permission";
|
||||
import { PartyPopperIcon } from "lucide-vue-next";
|
||||
import { BookmarkIcon } from "lucide-vue-next";
|
||||
import {
|
||||
FilterIcon,
|
||||
@ -63,12 +62,6 @@ import {
|
||||
"
|
||||
route="settings/admin"
|
||||
/>
|
||||
<PageCard
|
||||
name="Event Settings"
|
||||
:icon="PartyPopperIcon"
|
||||
route="settings/event"
|
||||
>
|
||||
</PageCard>
|
||||
<PageCard
|
||||
:name="$t('title.settings.about')"
|
||||
:icon="InfoIcon"
|
||||
@ -88,7 +81,7 @@ import {
|
||||
|
||||
<style scoped>
|
||||
.settings {
|
||||
padding: 20px 10px 0px 10px;
|
||||
padding: 0px 10px 0px 10px;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
|
@ -1,31 +0,0 @@
|
||||
<script setup>
|
||||
import { eventSettings } from "@/store";
|
||||
import MultiselectButtons from "@/components/settings/multiselect-buttons.vue";
|
||||
|
||||
if (!eventSettings.value.disabled) eventSettings.value.disabled = [];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2>Event Settings</h2>
|
||||
<p>
|
||||
Last Christmas I gave you my heart But the very next day you gave it away
|
||||
This year, to save me from tears I'll give it to someone special Last
|
||||
Christmas I gave you my heart But the very next day you gave it away This
|
||||
year, to save me from tears I'll give it to someone special [...]
|
||||
</p>
|
||||
<MultiselectButtons
|
||||
:options="['Disable Snow']"
|
||||
:values="['snow']"
|
||||
v-model="eventSettings.disabled"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
h2 {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 5px 0px;
|
||||
}
|
||||
</style>
|
@ -27,8 +27,8 @@ export default defineConfig({
|
||||
],
|
||||
registerType: "autoUpdate",
|
||||
manifest: {
|
||||
name: "Timetable V2",
|
||||
short_name: "Timetable",
|
||||
name: "Timetable V2 Demo",
|
||||
short_name: "Timetable Demo",
|
||||
description: "Timetable and Substitution plan viewer",
|
||||
theme_color: "#212121",
|
||||
background_color: "#353535",
|
||||
@ -61,15 +61,5 @@ export default defineConfig({
|
||||
},
|
||||
server: {
|
||||
port: 3001,
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://localhost:3000",
|
||||
changeOrigin: true,
|
||||
},
|
||||
"/auth": {
|
||||
target: "http://localhost:3000",
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
Reference in New Issue
Block a user