Compare commits

...

6 Commits

30 changed files with 613 additions and 2959 deletions

View File

@ -1,3 +1,4 @@
node_modules
server/node_modules
Dockerfile
.env

View File

@ -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
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

View File

@ -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

View File

@ -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" />

561
package-lock.json generated
View File

@ -51,89 +51,20 @@
}
},
"node_modules/@babel/code-frame": {
"version": "7.22.10",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.10.tgz",
"integrity": "sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==",
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/highlight": "^7.22.10",
"chalk": "^2.4.2"
"@babel/helper-validator-identifier": "^7.27.1",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/code-frame/node_modules/ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"dependencies": {
"color-convert": "^1.9.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/code-frame/node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/code-frame/node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/@babel/code-frame/node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"dev": true
},
"node_modules/@babel/code-frame/node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"dev": true,
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@babel/code-frame/node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/code-frame/node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"dependencies": {
"has-flag": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/compat-data": {
"version": "7.22.9",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz",
@ -183,15 +114,17 @@
}
},
"node_modules/@babel/generator": {
"version": "7.22.10",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.10.tgz",
"integrity": "sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A==",
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz",
"integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.22.10",
"@jridgewell/gen-mapping": "^0.3.2",
"@jridgewell/trace-mapping": "^0.3.17",
"jsesc": "^2.5.1"
"@babel/parser": "^7.28.0",
"@babel/types": "^7.28.0",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
},
"engines": {
"node": ">=6.9.0"
@ -357,6 +290,16 @@
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-globals": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-hoist-variables": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz",
@ -504,19 +447,19 @@
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
"integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==",
"dev": true,
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz",
"integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==",
"dev": true,
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
@ -545,108 +488,27 @@
}
},
"node_modules/@babel/helpers": {
"version": "7.22.11",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.11.tgz",
"integrity": "sha512-vyOXC8PBWaGc5h7GMsNx68OH33cypkEDJCHvYVVgVbbxJDROYVtexSk0gK5iCF1xNjRIN2s8ai7hwkWDq5szWg==",
"version": "7.27.6",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz",
"integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.22.5",
"@babel/traverse": "^7.22.11",
"@babel/types": "^7.22.11"
"@babel/template": "^7.27.2",
"@babel/types": "^7.27.6"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/highlight": {
"version": "7.22.10",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.10.tgz",
"integrity": "sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==",
"dev": true,
"dependencies": {
"@babel/helper-validator-identifier": "^7.22.5",
"chalk": "^2.4.2",
"js-tokens": "^4.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/highlight/node_modules/ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"dependencies": {
"color-convert": "^1.9.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/highlight/node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/highlight/node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/@babel/highlight/node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"dev": true
},
"node_modules/@babel/highlight/node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"dev": true,
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@babel/highlight/node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/highlight/node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"dependencies": {
"has-flag": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/parser": {
"version": "7.22.11",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.11.tgz",
"integrity": "sha512-R5zb8eJIBPJriQtbH/htEQy4k7E2dHWlD2Y2VT07JCzwYZHBxV5ZYtM0UhXSNMT74LyxuM+b1jdL7pSesXbC/g==",
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz",
"integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.28.0"
},
"bin": {
"parser": "bin/babel-parser.js"
},
@ -1838,70 +1700,57 @@
"dev": true
},
"node_modules/@babel/runtime": {
"version": "7.22.11",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.11.tgz",
"integrity": "sha512-ee7jVNlWN09+KftVOu9n7S8gQzD/Z6hN/I8VBRXW4P1+Xe7kJGXMwu8vds4aGIMHZnNbdpSWCfZZtinytpcAvA==",
"version": "7.27.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
"integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==",
"dev": true,
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz",
"integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==",
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.22.5",
"@babel/parser": "^7.22.5",
"@babel/types": "^7.22.5"
"@babel/code-frame": "^7.27.1",
"@babel/parser": "^7.27.2",
"@babel/types": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
"version": "7.22.11",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.11.tgz",
"integrity": "sha512-mzAenteTfomcB7mfPtyi+4oe5BZ6MXxWcn4CX+h4IRJ+OOGXBrWU6jDQavkQI9Vuc5P+donFabBfFCcmWka9lQ==",
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz",
"integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.22.10",
"@babel/generator": "^7.22.10",
"@babel/helper-environment-visitor": "^7.22.5",
"@babel/helper-function-name": "^7.22.5",
"@babel/helper-hoist-variables": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6",
"@babel/parser": "^7.22.11",
"@babel/types": "^7.22.11",
"debug": "^4.1.0",
"globals": "^11.1.0"
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.0",
"@babel/helper-globals": "^7.28.0",
"@babel/parser": "^7.28.0",
"@babel/template": "^7.27.2",
"@babel/types": "^7.28.0",
"debug": "^4.3.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse/node_modules/globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/types": {
"version": "7.22.11",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.11.tgz",
"integrity": "sha512-siazHiGuZRz9aB9NpHy9GOs9xiQPKnMzgdr493iI1M67vRXpnEq8ZOOKzezC5q7zwuQ6sDhdSp4SD9ixKSqKZg==",
"dev": true,
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.0.tgz",
"integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.22.5",
"@babel/helper-validator-identifier": "^7.22.5",
"to-fast-properties": "^2.0.0"
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@ -2349,74 +2198,58 @@
"dev": true
},
"node_modules/@intlify/core-base": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.2.2.tgz",
"integrity": "sha512-JjUpQtNfn+joMbrXvpR4hTF8iJQ2sEFzzK3KIESOx+f+uwIjgw20igOyaIdhfsVVBCds8ZM64MoeNSx+PHQMkA==",
"version": "9.14.4",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.14.4.tgz",
"integrity": "sha512-vtZCt7NqWhKEtHa3SD/322DlgP5uR9MqWxnE0y8Q0tjDs9H5Lxhss+b5wv8rmuXRoHKLESNgw9d+EN9ybBbj9g==",
"license": "MIT",
"dependencies": {
"@intlify/devtools-if": "9.2.2",
"@intlify/message-compiler": "9.2.2",
"@intlify/shared": "9.2.2",
"@intlify/vue-devtools": "9.2.2"
"@intlify/message-compiler": "9.14.4",
"@intlify/shared": "9.14.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@intlify/devtools-if": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/@intlify/devtools-if/-/devtools-if-9.2.2.tgz",
"integrity": "sha512-4ttr/FNO29w+kBbU7HZ/U0Lzuh2cRDhP8UlWOtV9ERcjHzuyXVZmjyleESK6eVP60tGC9QtQW9yZE+JeRhDHkg==",
"dependencies": {
"@intlify/shared": "9.2.2"
"node": ">= 16"
},
"engines": {
"node": ">= 14"
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/message-compiler": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.2.2.tgz",
"integrity": "sha512-IUrQW7byAKN2fMBe8z6sK6riG1pue95e5jfokn8hA5Q3Bqy4MBJ5lJAofUsawQJYHeoPJ7svMDyBaVJ4d0GTtA==",
"version": "9.14.4",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.14.4.tgz",
"integrity": "sha512-vcyCLiVRN628U38c3PbahrhbbXrckrM9zpy0KZVlDk2Z0OnGwv8uQNNXP3twwGtfLsCf4gu3ci6FMIZnPaqZsw==",
"license": "MIT",
"dependencies": {
"@intlify/shared": "9.2.2",
"source-map": "0.6.1"
"@intlify/shared": "9.14.4",
"source-map-js": "^1.0.2"
},
"engines": {
"node": ">= 14"
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/shared": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.2.2.tgz",
"integrity": "sha512-wRwTpsslgZS5HNyM7uDQYZtxnbI12aGiBZURX3BTR9RFIKKRWpllTsgzHWvj3HKm3Y2Sh5LPC1r0PDCKEhVn9Q==",
"version": "9.14.4",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.14.4.tgz",
"integrity": "sha512-P9zv6i1WvMc9qDBWvIgKkymjY2ptIiQ065PjDv7z7fDqH3J/HBRBN5IoiR46r/ujRcU7hCuSIZWvCAFCyuOYZA==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/@intlify/vue-devtools": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/@intlify/vue-devtools/-/vue-devtools-9.2.2.tgz",
"integrity": "sha512-+dUyqyCHWHb/UcvY1MlIpO87munedm3Gn6E9WWYdWrMuYLcoIoOEVDWSS8xSwtlPU+kA+MEQTP6Q1iI/ocusJg==",
"dependencies": {
"@intlify/core-base": "9.2.2",
"@intlify/shared": "9.2.2"
"node": ">= 16"
},
"engines": {
"node": ">= 14"
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
"integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
"version": "0.3.12",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz",
"integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/set-array": "^1.0.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.9"
},
"engines": {
"node": ">=6.0.0"
"@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/resolve-uri": {
@ -2428,15 +2261,6 @@
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/set-array": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
"dev": true,
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/source-map": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz",
@ -2448,15 +2272,17 @@
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.15",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz",
"integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==",
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.19",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz",
"integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==",
"version": "0.3.29",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz",
"integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
@ -2914,22 +2740,24 @@
}
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.0.1"
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
@ -3117,10 +2945,11 @@
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@ -3273,10 +3102,11 @@
"integrity": "sha512-LN1gO7+u9xjU5oEScGFKvXhYf7Y/empUIIEAGBs1LzUq/rg5duiDrkuH5A2lQGd5jfMOb9X9usDa2oVXwJ0U/Q=="
},
"node_modules/ejs": {
"version": "3.1.9",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz",
"integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==",
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
"integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"jake": "^10.8.5"
},
@ -3761,10 +3591,11 @@
}
},
"node_modules/filelist/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
@ -3782,10 +3613,11 @@
}
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
},
@ -4367,6 +4199,7 @@
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
}
@ -4585,7 +4418,8 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
"dev": true,
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.0",
@ -4600,15 +4434,16 @@
}
},
"node_modules/jsesc": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
"dev": true,
"license": "MIT",
"bin": {
"jsesc": "bin/jsesc"
},
"engines": {
"node": ">=4"
"node": ">=6"
}
},
"node_modules/json-schema": {
@ -4770,12 +4605,13 @@
}
},
"node_modules/micromatch": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.2",
"braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
@ -4813,15 +4649,16 @@
"dev": true
},
"node_modules/nanoid": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@ -5051,9 +4888,10 @@
"dev": true
},
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.1",
@ -5068,9 +4906,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.28",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.28.tgz",
"integrity": "sha512-Z7V5j0cq8oEKyejIKfpD8b4eBy9cwW2JWPk0+fB1HOAMsfHbnAXLLS+PfVWlzMSLQaWttKDt607I0XHmpE67Vw==",
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"funding": [
{
"type": "opencollective",
@ -5085,10 +4923,11 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.6",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
@ -5211,12 +5050,6 @@
"node": ">=4"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
"integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==",
"dev": true
},
"node_modules/regenerator-transform": {
"version": "0.15.2",
"resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz",
@ -5342,10 +5175,11 @@
}
},
"node_modules/rollup": {
"version": "3.28.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.28.1.tgz",
"integrity": "sha512-R9OMQmIHJm9znrU3m3cpE8uhN0fGdXiawME7aZIpQqvpS/85+Vt1Hq1/yVIcYfOmaQiHjvXkQAoJukvLpau6Yw==",
"version": "3.29.5",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz",
"integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==",
"dev": true,
"license": "MIT",
"bin": {
"rollup": "dist/bin/rollup"
},
@ -5593,14 +5427,16 @@
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
@ -5878,20 +5714,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
},
@ -6144,10 +5972,11 @@
"dev": true
},
"node_modules/vite": {
"version": "4.4.9",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz",
"integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==",
"version": "4.5.14",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz",
"integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.18.10",
"postcss": "^8.4.27",
@ -6259,17 +6088,20 @@
}
},
"node_modules/vue-i18n": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.2.2.tgz",
"integrity": "sha512-yswpwtj89rTBhegUAv9Mu37LNznyu3NpyLQmozF3i1hYOhwpG8RjcjIFIIfnu+2MDZJGSZPXaKWvnQA71Yv9TQ==",
"version": "9.14.4",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.14.4.tgz",
"integrity": "sha512-B934C8yUyWLT0EMud3DySrwSUJI7ZNiWYsEEz2gknTthqKiG4dzWE/WSa8AzCuSQzwBEv4HtG1jZDhgzPfWSKQ==",
"license": "MIT",
"dependencies": {
"@intlify/core-base": "9.2.2",
"@intlify/shared": "9.2.2",
"@intlify/vue-devtools": "9.2.2",
"@vue/devtools-api": "^6.2.1"
"@intlify/core-base": "9.14.4",
"@intlify/shared": "9.14.4",
"@vue/devtools-api": "^6.5.0"
},
"engines": {
"node": ">= 14"
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
},
"peerDependencies": {
"vue": "^3.0.0"
@ -6563,10 +6395,11 @@
}
},
"node_modules/workbox-build/node_modules/rollup": {
"version": "2.79.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz",
"integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==",
"version": "2.79.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz",
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
"dev": true,
"license": "MIT",
"bin": {
"rollup": "dist/bin/rollup"
},

View File

@ -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=

View File

@ -1,5 +0,0 @@
{
"env": {
"node": true
}
}

2
server/.gitignore vendored
View File

@ -1,2 +0,0 @@
node_modules
.env

View File

@ -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);
}
}

View File

@ -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,
);

View File

@ -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);
}

View File

@ -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,
);

View File

@ -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);
})();

View File

@ -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}`);
});

View File

@ -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"
]
}
}

View File

@ -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
View File

@ -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"
}
}
}
}

View File

@ -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"
}
}

View File

@ -1,85 +0,0 @@
import axios from "axios";
import crypto from "node:crypto";
import { log } from "../logs.js";
/*
This BOLLE (https://bolle-software.de/) parser tries to download
the latest substitution plan HTML files from the "Pinnwand".
It uses the same API that the mobile app (v0.3.6) uses.
To get your login credentials you need to log into your BOLLE account
and register a new device in the settings (/einstellungen/geraete_personal).
Click on "Manuelle Logindaten Einblenden" and use "ID" as apiUser
and "Token" as apiKey. You need to make at least one request with
this API token before closing the registration window or else the
token will be invalidated immediately.
*/
// Files to download from the "Pinnwand"
const filenames = ["vp_heute", "vp_morgen"];
export class BolleClient {
constructor(bolleInstance, apiUser, apiKey) {
this.bolleInstance = bolleInstance;
this.apiUser = apiUser;
this.apiKey = apiKey;
}
async getFiles() {
try {
let contents = [];
for (let file of filenames) {
contents.push(await this.getFile(file));
}
return contents;
} catch (error) {
log("Parser / Bolle", "Error getting data: " + error);
return [];
}
}
async getFile(filename) {
// Generate the BOLLE api payload
let payload = {
api_payload: JSON.stringify({
method: "vertretungsplan_html",
payload: { content: filename },
}),
};
// Generate request headers
let headers = this.buildRequestHeaders(payload.api_payload);
// Send the POST request
let response = await axios.post(this.getRequestUrl(), payload, {
headers,
});
// The server responds with a json object
// containing the base64 encoded html data
let base64 = response.data["html_base64"];
// Decode the base64 data using the latin1 (ISO 8859-1) character set
return Buffer.from(base64, "base64").toString("latin1");
}
getRequestUrl() {
// The API that the bolle mobile app uses is available at /app/basic
return `https://${this.bolleInstance}/app/basic`;
}
buildRequestHeaders(payload) {
// Bolle needs the sha1 hash of the payload
// to be set as the "b-hash" header
let hash = crypto.createHash("sha1");
hash.update(payload);
return {
Accept: "application/json",
// Set the auth headers
"X-Auth-User": this.apiUser,
"X-Auth-Token": this.apiKey,
"App-Version": "5",
// Set the hash
"B-Hash": hash.digest("hex"),
"Content-Type": "application/json",
Connection: "Keep-Alive",
"Accept-Encoding": "gzip",
"User-Agent": "okhttp/4.9.2",
};
}
}

View File

@ -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 [];
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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[]
}

View File

@ -13,6 +13,7 @@ import {
selectedDay,
changeDay,
changeDate,
activeProfile,
} from "@/store";
import { computed, ref } from "vue";
@ -50,6 +51,19 @@ const isDataView = computed(() => route.meta.dataView || false);
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 />
</div>
@ -95,7 +109,7 @@ main {
height: 100%;
overflow: hidden;
display: grid;
grid-template-rows: 1fr;
grid-template-rows: auto 1fr;
}
</style>
@ -110,4 +124,9 @@ main {
.wrapper {
overflow: hidden;
}
.demoNotice {
padding: 15px 15px;
font-style: italic;
}
</style>

334
src/demoData.js Normal file
View 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,
};
}

View File

@ -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: [],
},
],
@ -162,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;
@ -180,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 */

View File

@ -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",

View File

@ -81,7 +81,7 @@ import {
<style scoped>
.settings {
padding: 20px 10px 0px 10px;
padding: 0px 10px 0px 10px;
}
.wrapper {

View File

@ -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,
},
},
},
});