diff --git a/.gitea/workflows/cd-deploy.yaml b/.gitea/workflows/cd-deploy.yaml new file mode 100644 index 0000000..8ee90f4 --- /dev/null +++ b/.gitea/workflows/cd-deploy.yaml @@ -0,0 +1,95 @@ +name: Continuous Deployment +on: + push: + tags: + - '*' + +jobs: + Deploy: + name: Update Deployment + runs-on: ubuntu-latest + container: + volumes: + - /var/www/html/milsim-site-v4:/var/www/html/milsim-site-v4:z + steps: + - name: Setup Local Environment + run: | + groupadd -g 989 nginx || true + useradd nginx -u 990 -g nginx -m || true + + - name: Update Node Environment + uses: actions/setup-node@v6 + with: + node-version: 20.19 + + - name: Verify Local Environment + run: | + which npm + npm -v + which node + node -v + which sed + sed --version + + - name: Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 + ref: 'main' + + - name: Token Copy + run: | + cd /var/www/html/milsim-site-v4 + cp /workspace/17th-Ranger-Battalion-ORG/milsim-site-v4/.git/config .git/config + chown nginx:nginx .git/config + + - name: Update Application Code + run: | + cd /var/www/html/milsim-site-v4 + version=`git log -1 --format=%H` + echo "Current Revision: $version" + echo "Updating to: ${{ github.sha }}" + sudo -u nginx git reset --hard + sudo -u nginx git fetch --tags + sudo -u nginx git pull origin main + new_version=`git log -1 --format=%H` + echo "Successfully updated to: $new_version" + + - name: Update Shared Dependencies and Fix Permissions + run: | + cd /var/www/html/milsim-site-v4/shared + npm install + chown -R nginx:nginx . + + - name: Update UI Dependencies and Fix Permissions + run: | + cd /var/www/html/milsim-site-v4/ui + npm install + chown -R nginx:nginx . + + - name: Update API Dependencies and Fix Permissions + run: | + cd /var/www/html/milsim-site-v4/api + npm install + chown -R nginx:nginx . + + - name: Build UI / Update Version / Fix Permissions + run: | + cd /var/www/html/milsim-site-v4/ui + npm run build + version=`git describe --abbrev=0 --tags` + sed -i "s/VITE_APPLICATION_VERSION=.*/VITE_APPLICATION_VERSION=$version/" .env + chown -R nginx:nginx . + + - name: Build API / Update Version / Fix Permissions + run: | + cd /var/www/html/milsim-site-v4/api + npm run build + version=`git describe --abbrev=0 --tags` + sed -i "s/APPLICATION_VERSION=.*/APPLICATION_VERSION=$version/" .env + chown -R nginx:nginx . + + - name: Reset File Permissions + run: | + sudo chown -R nginx:nginx /var/www/html/milsim-site-v4 + sudo chmod -R u+w /var/www/html/milsim-site-v4 \ No newline at end of file diff --git a/.gitea/workflows/ci-deploy.yaml b/.gitea/workflows/ci-deploy.yaml new file mode 100644 index 0000000..95ebb3d --- /dev/null +++ b/.gitea/workflows/ci-deploy.yaml @@ -0,0 +1,89 @@ +name: Continuous Integration +on: + push: + branches: + - main + +jobs: + Deploy: + name: Update Development + runs-on: ubuntu-latest + container: + volumes: + - /var/www/html/milsim-site-v4-dev:/var/www/html/milsim-site-v4:z + steps: + - name: Setup Local Environment + run: | + groupadd -g 989 nginx || true + useradd nginx -u 990 -g nginx -m || true + + - name: Update Node Environment + uses: actions/setup-node@v6 + with: + node-version: 20.19 + + - name: Verify Local Environment + run: | + which npm + npm -v + which node + node -v + which sed + sed --version + + - name: Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 + ref: 'main' + + - name: Token Copy + run: | + cd /var/www/html/milsim-site-v4 + cp /workspace/17th-Ranger-Battalion-ORG/milsim-site-v4/.git/config .git/config + chown nginx:nginx .git/config + + - name: Update Application Code + run: | + cd /var/www/html/milsim-site-v4 + sudo -u nginx git reset --hard + sudo -u nginx git pull origin main + + - name: Update Shared Dependencies and Fix Permissions + run: | + cd /var/www/html/milsim-site-v4/shared + npm install + chown -R nginx:nginx . + + - name: Update UI Dependencies and Fix Permissions + run: | + cd /var/www/html/milsim-site-v4/ui + npm install + chown -R nginx:nginx . + + - name: Update API Dependencies and Fix Permissions + run: | + cd /var/www/html/milsim-site-v4/api + npm install + chown -R nginx:nginx . + + - name: Build UI / Update Version / Fix Permissions + run: | + cd /var/www/html/milsim-site-v4/ui + npm run build + version=`git rev-parse --short=10 HEAD` + sed -i "s/VITE_APPLICATION_VERSION=.*/VITE_APPLICATION_VERSION=$version/" .env + chown -R nginx:nginx . + + - name: Build API / Update Version / Fix Permissions + run: | + cd /var/www/html/milsim-site-v4/api + npm run build + version=`git rev-parse --short=10 HEAD` + sed -i "s/APPLICATION_VERSION=.*/APPLICATION_VERSION=$version/" .env + chown -R nginx:nginx . + + - name: Reset File Permissions + run: | + sudo chown -R nginx:nginx /var/www/html/milsim-site-v4 + sudo chmod -R u+w /var/www/html/milsim-site-v4 \ No newline at end of file diff --git a/api/.env.example b/api/.env.example new file mode 100644 index 0000000..9a1e8da --- /dev/null +++ b/api/.env.example @@ -0,0 +1,33 @@ +# DATABASE SETTINGS +DB_HOST= +DB_PORT= +DB_DATABASE= +DB_USERNAME= +DB_PASSWORD= + +# AUTH SETTINGS +AUTH_DOMAIN= +AUTH_ISSUER= +AUTH_CLIENT_ID= +AUTH_CLIENT_SECRET= +AUTH_REDIRECT_URI= +AUTH_REVOCATION_URI= +AUTH_END_SESSION_URI= +# AUTH_MODE=mock #uncomment this to bypass authentik + +# SERVER SETTINGS +SERVER_PORT=3000 +CLIENT_URL= # This is whatever URL the client web app is served on +CLIENT_DOMAIN= #whatever.com +APPLICATION_VERSION= # Should match release tag +APPLICATION_ENVIRONMENT= # dev / prod +CONFIG_ID= # configures + +# Glitchtip +GLITCHTIP_DSN= +DISABLE_GLITCHTIP= # true/false + +# Bookstack +DOC_HOST= # https://bookstack.whatever.com/ +DOC_TOKEN_SECRET= +DOC_TOKEN_ID= \ No newline at end of file diff --git a/api/package-lock.json b/api/package-lock.json index 24aec14..25088d0 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -9,6 +9,9 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@sentry/node": "^10.27.0", + "@types/express-session": "^1.18.2", + "chalk": "^5.6.2", "connect-sqlite3": "^0.9.16", "cors": "^2.8.5", "dotenv": "16.6.1", @@ -24,6 +27,7 @@ "@types/express": "^5.0.3", "@types/morgan": "^1.9.10", "@types/node": "^24.8.1", + "tsc-alias": "^1.8.16", "cross-env": "^10.1.0", "typescript": "^5.9.3" } @@ -35,6 +39,23 @@ "dev": true, "license": "MIT" }, + "node_modules/@apm-js-collab/code-transformer": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@apm-js-collab/code-transformer/-/code-transformer-0.8.2.tgz", + "integrity": "sha512-YRjJjNq5KFSjDUoqu5pFUWrrsvGOxl6c3bu+uMFc9HNNptZ2rNU/TI2nLw4jnhQNtka972Ee2m3uqbvDQtPeCA==", + "license": "Apache-2.0" + }, + "node_modules/@apm-js-collab/tracing-hooks": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@apm-js-collab/tracing-hooks/-/tracing-hooks-0.3.1.tgz", + "integrity": "sha512-Vu1CbmPURlN5fTboVuKMoJjbO5qcq9fA5YXpskx3dXe/zTBvjODFoerw+69rVBlRLrJpwPqSDqEuJDEKIrTldw==", + "license": "Apache-2.0", + "dependencies": { + "@apm-js-collab/code-transformer": "^0.8.0", + "debug": "^4.4.1", + "module-details-from-path": "^1.0.4" + } + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -42,6 +63,44 @@ "license": "MIT", "optional": true }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@npmcli/fs": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", @@ -68,6 +127,632 @@ "node": ">=10" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.208.0.tgz", + "integrity": "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.2.0.tgz", + "integrity": "sha512-qRkLWiUEZNAmYapZ7KGS5C4OmBLcP/H2foXeOEaowYCR0wi89fHejrfYfbuLVCMLp/dWZXKvQusdbUEZjERfwQ==", + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", + "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.208.0.tgz", + "integrity": "sha512-Eju0L4qWcQS+oXxi6pgh7zvE2byogAkcsVv0OjHF/97iOz1N/aKE6etSGowYkie+YA1uo6DNwdSxaaNnLvcRlA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.208.0", + "import-in-the-middle": "^2.0.0", + "require-in-the-middle": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-amqplib": { + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.55.0.tgz", + "integrity": "sha512-5ULoU8p+tWcQw5PDYZn8rySptGSLZHNX/7srqo2TioPnAAcvTy6sQFQXsNPrAnyRRtYGMetXVyZUy5OaX1+IfA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.208.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-connect": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.52.0.tgz", + "integrity": "sha512-GXPxfNB5szMbV3I9b7kNWSmQBoBzw7MT0ui6iU/p+NIzVx3a06Ri2cdQO7tG9EKb4aKSLmfX9Cw5cKxXqX6Ohg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.208.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/connect": "3.4.38" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-dataloader": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.26.0.tgz", + "integrity": "sha512-P2BgnFfTOarZ5OKPmYfbXfDFjQ4P9WkQ1Jji7yH5/WwB6Wm/knynAoA1rxbjWcDlYupFkyT0M1j6XLzDzy0aCA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.208.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-express": { + "version": "0.57.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.57.0.tgz", + "integrity": "sha512-HAdx/o58+8tSR5iW+ru4PHnEejyKrAy9fYFhlEI81o10nYxrGahnMAHWiSjhDC7UQSY3I4gjcPgSKQz4rm/asg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.208.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-fs": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.28.0.tgz", + "integrity": "sha512-FFvg8fq53RRXVBRHZViP+EMxMR03tqzEGpuq55lHNbVPyFklSVfQBN50syPhK5UYYwaStx0eyCtHtbRreusc5g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.208.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-generic-pool": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.52.0.tgz", + "integrity": "sha512-ISkNcv5CM2IwvsMVL31Tl61/p2Zm2I2NAsYq5SSBgOsOndT0TjnptjufYVScCnD5ZLD1tpl4T3GEYULLYOdIdQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.208.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-graphql": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.56.0.tgz", + "integrity": "sha512-IPvNk8AFoVzTAM0Z399t34VDmGDgwT6rIqCUug8P9oAGerl2/PEIYMPOl/rerPGu+q8gSWdmbFSjgg7PDVRd3Q==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.208.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-hapi": { + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.55.0.tgz", + "integrity": "sha512-prqAkRf9e4eEpy4G3UcR32prKE8NLNlA90TdEU1UsghOTg0jUvs40Jz8LQWFEs5NbLbXHYGzB4CYVkCI8eWEVQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.208.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-http": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.208.0.tgz", + "integrity": "sha512-rhmK46DRWEbQQB77RxmVXGyjs6783crXCnFjYQj+4tDH/Kpv9Rbg3h2kaNyp5Vz2emF1f9HOQQvZoHzwMWOFZQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/instrumentation": "0.208.0", + "@opentelemetry/semantic-conventions": "^1.29.0", + "forwarded-parse": "2.1.2" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-ioredis": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.56.0.tgz", + "integrity": "sha512-XSWeqsd3rKSsT3WBz/JKJDcZD4QYElZEa0xVdX8f9dh4h4QgXhKRLorVsVkK3uXFbC2sZKAS2Ds+YolGwD83Dg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.208.0", + "@opentelemetry/redis-common": "^0.38.2" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-kafkajs": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.18.0.tgz", + "integrity": "sha512-KCL/1HnZN5zkUMgPyOxfGjLjbXjpd4odDToy+7c+UsthIzVLFf99LnfIBE8YSSrYE4+uS7OwJMhvhg3tWjqMBg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.208.0", + "@opentelemetry/semantic-conventions": "^1.30.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-knex": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.53.0.tgz", + "integrity": "sha512-xngn5cH2mVXFmiT1XfQ1aHqq1m4xb5wvU6j9lSgLlihJ1bXzsO543cpDwjrZm2nMrlpddBf55w8+bfS4qDh60g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.208.0", + "@opentelemetry/semantic-conventions": "^1.33.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-koa": { + "version": "0.57.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.57.0.tgz", + "integrity": "sha512-3JS8PU/D5E3q295mwloU2v7c7/m+DyCqdu62BIzWt+3u9utjxC9QS7v6WmUNuoDN3RM+Q+D1Gpj13ERo+m7CGg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.208.0", + "@opentelemetry/semantic-conventions": "^1.36.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + } + }, + "node_modules/@opentelemetry/instrumentation-lru-memoizer": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.53.0.tgz", + "integrity": "sha512-LDwWz5cPkWWr0HBIuZUjslyvijljTwmwiItpMTHujaULZCxcYE9eU44Qf/pbVC8TulT0IhZi+RoGvHKXvNhysw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.208.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mongodb": { + "version": "0.61.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.61.0.tgz", + "integrity": "sha512-OV3i2DSoY5M/pmLk+68xr5RvkHU8DRB3DKMzYJdwDdcxeLs62tLbkmRyqJZsYf3Ht7j11rq35pHOWLuLzXL7pQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.208.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mongoose": { + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.55.0.tgz", + "integrity": "sha512-5afj0HfF6aM6Nlqgu6/PPHFk8QBfIe3+zF9FGpX76jWPS0/dujoEYn82/XcLSaW5LPUDW8sni+YeK0vTBNri+w==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.208.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mysql": { + "version": "0.54.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.54.0.tgz", + "integrity": "sha512-bqC1YhnwAeWmRzy1/Xf9cDqxNG2d/JDkaxnqF5N6iJKN1eVWI+vg7NfDkf52/Nggp3tl1jcC++ptC61BD6738A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.208.0", + "@types/mysql": "2.15.27" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mysql2": { + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.55.0.tgz", + "integrity": "sha512-0cs8whQG55aIi20gnK8B7cco6OK6N+enNhW0p5284MvqJ5EPi+I1YlWsWXgzv/V2HFirEejkvKiI4Iw21OqDWg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.208.0", + "@opentelemetry/semantic-conventions": "^1.33.0", + "@opentelemetry/sql-common": "^0.41.2" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-pg": { + "version": "0.61.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.61.0.tgz", + "integrity": "sha512-UeV7KeTnRSM7ECHa3YscoklhUtTQPs6V6qYpG283AB7xpnPGCUCUfECFT9jFg6/iZOQTt3FHkB1wGTJCNZEvPw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.208.0", + "@opentelemetry/semantic-conventions": "^1.34.0", + "@opentelemetry/sql-common": "^0.41.2", + "@types/pg": "8.15.6", + "@types/pg-pool": "2.0.6" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-redis": { + "version": "0.57.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.57.0.tgz", + "integrity": "sha512-bCxTHQFXzrU3eU1LZnOZQ3s5LURxQPDlU3/upBzlWY77qOI1GZuGofazj3jtzjctMJeBEJhNwIFEgRPBX1kp/Q==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.208.0", + "@opentelemetry/redis-common": "^0.38.2", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-tedious": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.27.0.tgz", + "integrity": "sha512-jRtyUJNZppPBjPae4ZjIQ2eqJbcRaRfJkr0lQLHFmOU/no5A6e9s1OHLd5XZyZoBJ/ymngZitanyRRA5cniseA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.208.0", + "@types/tedious": "^4.0.14" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-undici": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.19.0.tgz", + "integrity": "sha512-Pst/RhR61A2OoZQZkn6OLpdVpXp6qn3Y92wXa6umfJe9rV640r4bc6SWvw4pPN6DiQqPu2c8gnSSZPDtC6JlpQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.208.0", + "@opentelemetry/semantic-conventions": "^1.24.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.7.0" + } + }, + "node_modules/@opentelemetry/redis-common": { + "version": "0.38.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.38.2.tgz", + "integrity": "sha512-1BCcU93iwSRZvDAgwUxC/DV4T/406SkMfxGqu5ojc3AvNI+I9GhV7v0J1HljsczuuhcnFLYqD5VmwVXfCGHzxA==", + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", + "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.38.0.tgz", + "integrity": "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/sql-common": { + "version": "0.41.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/sql-common/-/sql-common-0.41.2.tgz", + "integrity": "sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0" + } + }, + "node_modules/@prisma/instrumentation": { + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-6.19.0.tgz", + "integrity": "sha512-QcuYy25pkXM8BJ37wVFBO7Zh34nyRV1GOb2n3lPkkbRYfl4hWl3PTcImP41P0KrzVXfa/45p6eVCos27x3exIg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": ">=0.52.0 <1" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.8" + } + }, + "node_modules/@sentry/core": { + "version": "10.27.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.27.0.tgz", + "integrity": "sha512-Zc68kdH7tWTDtDbV1zWIbo3Jv0fHAU2NsF5aD2qamypKgfSIMSbWVxd22qZyDBkaX8gWIPm/0Sgx6aRXRBXrYQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node": { + "version": "10.27.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.27.0.tgz", + "integrity": "sha512-1cQZ4+QqV9juW64Jku1SMSz+PoZV+J59lotz4oYFvCNYzex8hRAnDKvNiKW1IVg5mEEkz98mg1fvcUtiw7GTiQ==", + "license": "MIT", + "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^2.2.0", + "@opentelemetry/core": "^2.2.0", + "@opentelemetry/instrumentation": "^0.208.0", + "@opentelemetry/instrumentation-amqplib": "0.55.0", + "@opentelemetry/instrumentation-connect": "0.52.0", + "@opentelemetry/instrumentation-dataloader": "0.26.0", + "@opentelemetry/instrumentation-express": "0.57.0", + "@opentelemetry/instrumentation-fs": "0.28.0", + "@opentelemetry/instrumentation-generic-pool": "0.52.0", + "@opentelemetry/instrumentation-graphql": "0.56.0", + "@opentelemetry/instrumentation-hapi": "0.55.0", + "@opentelemetry/instrumentation-http": "0.208.0", + "@opentelemetry/instrumentation-ioredis": "0.56.0", + "@opentelemetry/instrumentation-kafkajs": "0.18.0", + "@opentelemetry/instrumentation-knex": "0.53.0", + "@opentelemetry/instrumentation-koa": "0.57.0", + "@opentelemetry/instrumentation-lru-memoizer": "0.53.0", + "@opentelemetry/instrumentation-mongodb": "0.61.0", + "@opentelemetry/instrumentation-mongoose": "0.55.0", + "@opentelemetry/instrumentation-mysql": "0.54.0", + "@opentelemetry/instrumentation-mysql2": "0.55.0", + "@opentelemetry/instrumentation-pg": "0.61.0", + "@opentelemetry/instrumentation-redis": "0.57.0", + "@opentelemetry/instrumentation-tedious": "0.27.0", + "@opentelemetry/instrumentation-undici": "0.19.0", + "@opentelemetry/resources": "^2.2.0", + "@opentelemetry/sdk-trace-base": "^2.2.0", + "@opentelemetry/semantic-conventions": "^1.37.0", + "@prisma/instrumentation": "6.19.0", + "@sentry/core": "10.27.0", + "@sentry/node-core": "10.27.0", + "@sentry/opentelemetry": "10.27.0", + "import-in-the-middle": "^2", + "minimatch": "^9.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node-core": { + "version": "10.27.0", + "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-10.27.0.tgz", + "integrity": "sha512-Dzo1I64Psb7AkpyKVUlR9KYbl4wcN84W4Wet3xjLmVKMgrCo2uAT70V4xIacmoMH5QLZAx0nGfRy9yRCd4nzBg==", + "license": "MIT", + "dependencies": { + "@apm-js-collab/tracing-hooks": "^0.3.1", + "@sentry/core": "10.27.0", + "@sentry/opentelemetry": "10.27.0", + "import-in-the-middle": "^2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0 || ^2.2.0", + "@opentelemetry/core": "^1.30.1 || ^2.1.0 || ^2.2.0", + "@opentelemetry/instrumentation": ">=0.57.1 <1", + "@opentelemetry/resources": "^1.30.1 || ^2.1.0 || ^2.2.0", + "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0 || ^2.2.0", + "@opentelemetry/semantic-conventions": "^1.37.0" + } + }, + "node_modules/@sentry/node/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@sentry/node/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@sentry/opentelemetry": { + "version": "10.27.0", + "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.27.0.tgz", + "integrity": "sha512-z2vXoicuGiqlRlgL9HaYJgkin89ncMpNQy0Kje6RWyhpzLe8BRgUXlgjux7WrSrcbopDdC1OttSpZsJ/Wjk7fg==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.27.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0 || ^2.2.0", + "@opentelemetry/core": "^1.30.1 || ^2.1.0 || ^2.2.0", + "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0 || ^2.2.0", + "@opentelemetry/semantic-conventions": "^1.37.0" + } + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -82,7 +767,6 @@ "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, "license": "MIT", "dependencies": { "@types/connect": "*", @@ -93,7 +777,6 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -103,7 +786,6 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", - "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", @@ -115,7 +797,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -124,6 +805,15 @@ "@types/send": "*" } }, + "node_modules/@types/express-session": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.18.2.tgz", + "integrity": "sha512-k+I0BxwVXsnEU2hV77cCobC08kIsn4y44C3gC0b46uxZVMaXA04lSPgRLR/bSL2w0t0ShJiG8o4jPzRG/nscFg==", + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/geojson": { "version": "7946.0.16", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", @@ -134,14 +824,12 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true, "license": "MIT" }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, "license": "MIT" }, "node_modules/@types/morgan": { @@ -154,6 +842,15 @@ "@types/node": "*" } }, + "node_modules/@types/mysql": { + "version": "2.15.27", + "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.27.tgz", + "integrity": "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "24.8.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.8.1.tgz", @@ -163,25 +860,42 @@ "undici-types": "~7.14.0" } }, + "node_modules/@types/pg": { + "version": "8.15.6", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.6.tgz", + "integrity": "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/pg-pool": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/pg-pool/-/pg-pool-2.0.6.tgz", + "integrity": "sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==", + "license": "MIT", + "dependencies": { + "@types/pg": "*" + } + }, "node_modules/@types/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", - "dev": true, "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, "license": "MIT" }, "node_modules/@types/send": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.0.tgz", "integrity": "sha512-zBF6vZJn1IaMpg3xUF25VK3gd3l8zwE0ZLRX7dsQyQi+jp4E8mMDJNGDYnYse+bQhYwWERTxVwHpi3dMOq7RKQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -191,7 +905,6 @@ "version": "1.15.9", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.9.tgz", "integrity": "sha512-dOTIuqpWLyl3BBXU3maNQsS4A3zuuoYRNIvYSxxhebPfXg2mzWQEPne/nlJ37yOse6uGgR386uTpdsx4D0QZWA==", - "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", @@ -203,13 +916,21 @@ "version": "0.17.5", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", - "dev": true, "license": "MIT", "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, + "node_modules/@types/tedious": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz", + "integrity": "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -230,6 +951,27 @@ "node": ">= 0.6" } }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -280,6 +1022,20 @@ "node": ">=8" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/aproba": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", @@ -302,6 +1058,16 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/aws-ssl-profiles": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", @@ -315,8 +1081,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/base64-js": { "version": "1.5.1", @@ -356,6 +1121,19 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bindings": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", @@ -377,23 +1155,43 @@ } }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/brace-expansion": { @@ -407,6 +1205,19 @@ "concat-map": "0.0.1" } }, + "node_modules/braces": { + "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.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -512,6 +1323,43 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", @@ -521,6 +1369,12 @@ "node": ">=10" } }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "license": "MIT" + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -541,6 +1395,16 @@ "color-support": "bin.js" } }, + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -652,9 +1516,9 @@ } }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -726,6 +1590,19 @@ "node": ">=8" } }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", @@ -946,12 +1823,52 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", "license": "MIT" }, + "node_modules/fill-range": { + "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" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/finalhandler": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", @@ -978,6 +1895,12 @@ "node": ">= 0.6" } }, + "node_modules/forwarded-parse": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", + "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==", + "license": "MIT" + }, "node_modules/fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", @@ -1012,6 +1935,21 @@ "license": "ISC", "optional": true }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -1088,6 +2026,19 @@ "node": ">= 0.4" } }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -1116,6 +2067,40 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -1174,28 +2159,23 @@ "optional": true }, "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==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" - } - }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/http-proxy-agent": { @@ -1269,6 +2249,28 @@ ], "license": "BSD-3-Clause" }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-in-the-middle": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-2.0.0.tgz", + "integrity": "sha512-yNZhyQYqXpkT0AKq3F3KLasUSK4fHvebNH5hOsKQw2dhGSALvQ4U0BqUc5suziKvydO5u5hgN2hy1RJaho8U5A==", + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.14.0", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -1339,6 +2341,29 @@ "node": ">= 0.10" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -1349,6 +2374,19 @@ "node": ">=8" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-lambda": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", @@ -1356,6 +2394,16 @@ "license": "MIT", "optional": true }, + "node_modules/is-number": { + "version": "7.0.0", + "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" + } + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -1499,6 +2547,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "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.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -1667,6 +2739,12 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", + "license": "MIT" + }, "node_modules/morgan": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", @@ -1716,6 +2794,20 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mylas": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/mylas/-/mylas-2.1.14.tgz", + "integrity": "sha512-BzQguy9W9NJgoVn2mRWzbFrFWWztGCcng2QI9+41frfk+Athwgx3qhqhvStz7ExeUUu7Kzw427sNzHpEZNINog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/raouldeheer" + } + }, "node_modules/mysql2": { "version": "3.14.3", "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.14.3.tgz", @@ -1831,6 +2923,16 @@ "node": ">=6" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/npmlog": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", @@ -2002,11 +3104,117 @@ "node": ">=16" } }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/pause": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", + "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/plimit-lit": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/plimit-lit/-/plimit-lit-1.6.1.tgz", + "integrity": "sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "queue-lit": "^1.5.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -2092,6 +3300,37 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/queue-lit": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/queue-lit/-/queue-lit-1.5.2.tgz", + "integrity": "sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/random-bytes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", @@ -2111,18 +3350,34 @@ } }, "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/rc": { @@ -2154,6 +3409,42 @@ "node": ">= 6" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-in-the-middle": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", + "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3" + }, + "engines": { + "node": ">=9.3.0 || >=8.10.0 <9.0.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -2164,6 +3455,17 @@ "node": ">= 4" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -2197,6 +3499,30 @@ "node": ">= 18" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2437,6 +3763,16 @@ "simple-concat": "^1.0.0" } }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -2639,6 +3975,19 @@ "node": ">=8" } }, + "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" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -2648,6 +3997,28 @@ "node": ">=0.6" } }, + "node_modules/tsc-alias": { + "version": "1.8.16", + "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.16.tgz", + "integrity": "sha512-QjCyu55NFyRSBAl6+MTFwplpFcnm2Pq01rR/uxfqJoLMm6X3O14KEGtaSDZpJYaE1bJBGDjD0eSuiIWPe2T58g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.3", + "commander": "^9.0.0", + "get-tsconfig": "^4.10.0", + "globby": "^11.0.4", + "mylas": "^2.1.9", + "normalize-path": "^3.0.0", + "plimit-lit": "^1.2.6" + }, + "bin": { + "tsc-alias": "dist/bin/index.js" + }, + "engines": { + "node": ">=16.20.2" + } + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -2791,6 +4162,15 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/api/package.json b/api/package.json index c6c4585..64392f2 100644 --- a/api/package.json +++ b/api/package.json @@ -8,8 +8,9 @@ "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "dev": "tsc && cross-env NODE_ENV=development node ./built/api/src/index.js", - "prod": "tsc && node ./built/api/src/index.js", + "dev": "tsc && && tsc-alias && cross-env NODE_ENV=development node ./built/api/src/index.js", + "prod": "tsc && tsc-alias && node ./built/api/src/index.js", + "build": "tsc && tsc-alias", "migrate": "node ./scripts/migrate.js", "migrate:create": "npm run migrate -- create -ext sql -dir /migrations", @@ -19,6 +20,9 @@ }, "dependencies": { + "@sentry/node": "^10.27.0", + "@types/express-session": "^1.18.2", + "chalk": "^5.6.2", "connect-sqlite3": "^0.9.16", "cors": "^2.8.5", "dotenv": "16.6.1", @@ -35,6 +39,7 @@ "@types/morgan": "^1.9.10", "@types/node": "^24.8.1", "cross-env": "^10.1.0", + "tsc-alias": "^1.8.16", "typescript": "^5.9.3" } } \ No newline at end of file diff --git a/api/src/db.ts b/api/src/db.ts index 8b858bf..1539dbd 100644 --- a/api/src/db.ts +++ b/api/src/db.ts @@ -18,7 +18,7 @@ const pool = mariadb.createPool({ connectionLimit: 5, connectTimeout: 10000, // give it more breathing room acquireTimeout: 15000, - database: 'ranger_unit_tracker', + database: process.env.DB_DATABASE, ssl: false, }); diff --git a/api/src/index.js b/api/src/index.js deleted file mode 100644 index 8e59aff..0000000 --- a/api/src/index.js +++ /dev/null @@ -1,70 +0,0 @@ -const dotenv = require('dotenv') -const path = require('path') -const envFile = process.env.NODE_ENV === 'development' ? '.env.development' : '.env'; -dotenv.config({ path: path.resolve(process.cwd(), envFile) }); -console.log(`Loaded environment from ${envFile}`); - -const express = require('express') -const cors = require('cors') -const morgan = require('morgan') -const app = express() -app.use(morgan('dev')) - -app.use(cors({ - origin: ['https://aj17thdev.nexuszone.net', 'http://localhost:5173'], // your SPA origins - credentials: true -})); - -app.use(express.json()) - -app.set('trust proxy', 1); - -const port = process.env.SERVER_PORT; - -//session setup -const path = require('path') -const session = require('express-session') -const passport = require('passport') -const SQLiteStore = require('connect-sqlite3')(session); - -app.use(session({ - secret: 'whatever', - resave: false, - saveUninitialized: false, - store: new SQLiteStore({ db: 'sessions.db', dir: './' }), - cookie: { - httpOnly: true, - sameSite: 'lax', - domain: 'nexuszone.net' - } -})); -app.use(passport.authenticate('session')); - -// Mount route modules -const applicationsRouter = require('./routes/applications'); -const { memberRanks, ranks } = require('./routes/ranks'); -const members = require('./routes/members'); -const loaHandler = require('./routes/loa') -const { status, memberStatus } = require('./routes/statuses') -const authRouter = require('./routes/auth') -const { roles, memberRoles } = require('./routes/roles'); -const morgan = require('morgan'); - -app.use('/application', applicationsRouter); -app.use('/ranks', ranks); -app.use('/memberRanks', memberRanks); -app.use('/members', members); -app.use('/loa', loaHandler); -app.use('/status', status) -app.use('/memberStatus', memberStatus) -app.use('/roles', roles) -app.use('/memberRoles', memberRoles) -app.use('/', authRouter) - -app.get('/ping', (req, res) => { - res.status(200).json({ message: 'pong' }); -}); - -app.listen(port, () => { - console.log(`Example app listening on port ${port} `) -}) diff --git a/api/src/index.ts b/api/src/index.ts new file mode 100644 index 0000000..118162e --- /dev/null +++ b/api/src/index.ts @@ -0,0 +1,114 @@ +import dotenv = require('dotenv'); +dotenv.config(); + +import express = require('express'); +import cors = require('cors'); +import morgan = require('morgan'); +const app = express() +import chalk from 'chalk'; +app.use(morgan((tokens: morgan.TokenIndexer, req: express.Request, res: express.Response) => { + const status = Number(tokens.status(req, res)); + + // Colorize status code + const statusColor = status >= 500 ? chalk.red + : status >= 400 ? chalk.yellow + : status >= 300 ? chalk.cyan + : chalk.green; + + return [ + chalk.gray(`[${new Date().toISOString()}]`), + chalk.blue.bold(tokens.method(req, res)), + tokens.url(req, res), + statusColor(status), + chalk.magenta(tokens['response-time'](req, res) + ' ms'), + chalk.yellow(`- User: ${req.user?.name ? `${req.user.name} (${req.user.id})` : 'Unauthenticated'}`), + ].join(' '); +}, { + skip: (req: express.Request) => { + return req.originalUrl === '/members/me'; + } +})) + +app.use(cors({ + origin: [process.env.CLIENT_URL], // your SPA origins + credentials: true +})); + +app.use(express.json()) + +app.set('trust proxy', 1); + +const port = process.env.SERVER_PORT; + +//glitchtip setup +import sentry = require('@sentry/node'); +if (process.env.DISABLE_GLITCHTIP === "true") { + console.log("Glitchtip disabled") +} else { + let dsn = process.env.GLITCHTIP_DSN; + let release = process.env.APPLICATION_VERSION; + let environment = process.env.APPLICATION_ENVIRONMENT; + console.log(release, environment) + sentry.init({ dsn: dsn, release: release, environment: environment, integrations: [sentry.captureConsoleIntegration({ levels: ['error'] })] }); + console.log("Glitchtip initialized"); +} + +//session setup +import path = require('path'); +// import session = require('express-session'); +import session = require('express-session'); +import passport = require('passport'); +const SQLiteStore = require('connect-sqlite3')(session); + +const cookieOptions: session.CookieOptions = { + httpOnly: true, + sameSite: 'lax', + domain: process.env.CLIENT_DOMAIN, + maxAge: 1000 * 60 * 60 * 24 * 30, //30 days +} +const sessionOptions: session.SessionOptions = { + secret: 'whatever', + resave: false, + saveUninitialized: false, + store: new SQLiteStore({ db: 'sessions.db', dir: './' }), + rolling: true, + cookie: cookieOptions +} + +app.use(session(sessionOptions)); +app.use(passport.authenticate('session')); + +// Mount route modules +import { applicationRouter } from './routes/applications'; +import { memberRanks, ranks } from './routes/ranks'; +import { memberRouter } from './routes/members'; +import { loaRouter } from './routes/loa'; +import { status, memberStatus } from './routes/statuses'; +import { authRouter } from './routes/auth'; +import { roles, memberRoles } from './routes/roles'; +import { courseRouter, eventRouter } from './routes/course'; +import { calendarRouter } from './routes/calendar'; +import { docsRouter } from './routes/docs'; + +app.use('/application', applicationRouter); +app.use('/ranks', ranks); +app.use('/memberRanks', memberRanks); +app.use('/members', memberRouter); +app.use('/loa', loaRouter); +app.use('/status', status) +app.use('/memberStatus', memberStatus) +app.use('/roles', roles) +app.use('/memberRoles', memberRoles) +app.use('/course', courseRouter) +app.use('/courseEvent', eventRouter) +app.use('/calendar', calendarRouter) +app.use('/docs', docsRouter) +app.use('/', authRouter) + +app.get('/ping', (req, res) => { + res.status(200).json({ message: 'pong' }); +}); + +app.listen(port, () => { + console.log(`Example app listening on port ${port} `) +}) diff --git a/api/src/middleware/auth.ts b/api/src/middleware/auth.ts new file mode 100644 index 0000000..e908b3e --- /dev/null +++ b/api/src/middleware/auth.ts @@ -0,0 +1,49 @@ +import { MemberState } from "@app/shared/types/member"; +import { NextFunction, Request, Response } from "express"; +import { stat } from "fs"; + +export const requireLogin = function (req: Request, res: Response, next: NextFunction) { + if (req.user?.id) + next(); + else + res.sendStatus(401) +} + +export function requireMemberState(state: MemberState) { + return function (req: Request, res: Response, next: NextFunction) { + if (req.user?.state === state) + next(); + else + res.status(403).send(`You must be a ${state} of the 17th RBN to access this resource`); + } +} + +export function requireRole(requiredRoles: string | string[]) { + // Normalize the input to always be an array of lowercase required roles + const normalizedRequiredRoles: string[] = Array.isArray(requiredRoles) + ? requiredRoles.map(role => role.toLowerCase()) + : [requiredRoles.toLowerCase()]; + + const DEV_ROLE = 'dev'; + + return function (req: Request, res: Response, next: NextFunction) { + if (!req.user || !req.user.roles) { + // User is not authenticated or has no roles array + return res.sendStatus(401); + } + + const userRolesLowercase = req.user.roles.map(role => role.name.toLowerCase()); + + // Check if the user has *any* of the required roles OR the 'dev' role + const hasAccess = userRolesLowercase.some(userRole => + userRole === DEV_ROLE || normalizedRequiredRoles.includes(userRole) + ); + + if (hasAccess) { + return next(); + } else { + // User is authenticated but does not have the necessary permissions + return res.sendStatus(403); + } + }; +} \ No newline at end of file diff --git a/api/src/routes/applications.ts b/api/src/routes/applications.ts index 8b2f61de..5a98f07 100644 --- a/api/src/routes/applications.ts +++ b/api/src/routes/applications.ts @@ -2,32 +2,54 @@ const express = require('express'); const router = express.Router(); import pool from '../db'; -import { approveApplication, createApplication, getApplicationByID, getApplicationComments, getApplicationList, getMemberApplication } from '../services/applicationService'; -import { MemberState, setUserState } from '../services/memberService'; +import { approveApplication, createApplication, denyApplication, getAllMemberApplications, getApplicationByID, getApplicationComments, getApplicationList, getMemberApplication } from '../services/applicationService'; +import { setUserState } from '../services/memberService'; +import { MemberState } from '@app/shared/types/member'; import { getRankByName, insertMemberRank } from '../services/rankService'; import { ApplicationFull, CommentRow } from "@app/shared/types/application" import { assignUserToStatus } from '../services/statusService'; +import { Request, response, Response } from 'express'; +import { getUserRoles } from '../services/rolesService'; +import { requireLogin, requireRole } from '../middleware/auth'; + +//get CoC +router.get('/coc', async (req: Request, res: Response) => { + const output = await fetch(`${process.env.DOC_HOST}/api/pages/714`, { + headers: { + Authorization: `Token ${process.env.DOC_TOKEN_ID}:${process.env.DOC_TOKEN_SECRET}`, + } + }) + + if (output.ok) { + const out = await output.json(); + res.status(200).json(out.html); + } else { + console.error("Failed to fetch LOA policy from bookstack"); + res.sendStatus(500); + } +}) + // POST /application -router.post('/', async (req, res) => { +router.post('/', [requireLogin], async (req, res) => { try { const App = req.body?.App || {}; const memberID = req.user.id; const appVersion = 1; - createApplication(memberID, appVersion, JSON.stringify(App)) - setUserState(memberID, MemberState.Applicant); + await createApplication(memberID, appVersion, JSON.stringify(App)) + await setUserState(memberID, MemberState.Applicant); res.sendStatus(201); } catch (err) { - console.error('Insert failed:', err); - res.status(500).json({ error: 'Failed to save application' }); + console.error('Failed to create application: \n', err); + res.status(500).json({ error: 'Failed to create application' }); } }); // GET /application/all -router.get('/all', async (req, res) => { +router.get('/all', [requireLogin, requireRole("Recruiter")], async (req, res) => { try { const rows = await getApplicationList(); res.status(200).json(rows); @@ -37,24 +59,56 @@ router.get('/all', async (req, res) => { } }); -router.get('/me', async (req, res) => { +router.get('/meList', async (req, res) => { + let userID = req.user.id; - console.log("application/me") + try { + let application = await getAllMemberApplications(userID); - let app = getMemberApplication(userID); - console.log(app); + return res.status(200).json(application); + } catch (error) { + console.error('Failed to load applications: \n', error); + return res.status(500).json(error); + } +}) + +router.get('/me', [requireLogin], async (req, res) => { + + let userID = req.user.id; + + try { + let application = await getMemberApplication(userID); + + if (application === undefined) + res.sendStatus(204); + + const comments: CommentRow[] = await getApplicationComments(application.id); + + const output: ApplicationFull = { + application, + comments, + } + + return res.status(200).json(output); + } catch (error) { + console.error('Failed to load application:', error); + return res.status(500).json(error); + } }) // GET /application/:id -router.get('/:id', async (req, res) => { - let appID = req.params.id; - console.log("HELLO") +router.get('/me/:id', [requireLogin], async (req: Request, res: Response) => { + let appID = Number(req.params.id); + let member = req.user.id; try { const application = await getApplicationByID(appID); if (application === undefined) return res.sendStatus(204); - + if (application.member_id != member) { + return res.sendStatus(403); + } + const comments: CommentRow[] = await getApplicationComments(appID); const output: ApplicationFull = { @@ -69,30 +123,44 @@ router.get('/:id', async (req, res) => { } }); +// GET /application/:id +router.get('/:id', [requireLogin, requireRole("Recruiter")], async (req: Request, res: Response) => { + let appID = Number(req.params.id); + let asAdmin = !!req.query.admin || false; + + try { + const application = await getApplicationByID(appID); + if (application === undefined) + return res.sendStatus(204); + + const comments: CommentRow[] = await getApplicationComments(appID, asAdmin); + + const output: ApplicationFull = { + application, + comments, + } + return res.status(200).json(output); + } + catch (err) { + console.error('Query failed:', err); + return res.status(500).json({ error: 'Failed to load application' }); + } +}); + // POST /application/approve/:id -router.post('/approve/:id', async (req, res) => { - const appID = req.params.id; +router.post('/approve/:id', [requireLogin, requireRole("Recruiter")], async (req: Request, res: Response) => { + const appID = Number(req.params.id); + const approved_by = req.user.id; try { const app = await getApplicationByID(appID); - const result = await approveApplication(appID); + await approveApplication(appID, approved_by); - console.log("START"); - console.log(app, result); - - //guard against failures - if (result.affectedRows != 1) { - throw new Error("Something went wrong approving the application"); - } - - console.log(app.member_id); //update user profile await setUserState(app.member_id, MemberState.Member); - let nextRank = await getRankByName('Recruit') - await insertMemberRank(app.member_id, nextRank.id); - //assign user to "pending basic" - await assignUserToStatus(app.member_id, 1); + await pool.query('CALL sp_accept_new_recruit_validation(?, ?, ?, ?)', [Number(process.env.CONFIG_ID), app.member_id, approved_by, approved_by]) + res.sendStatus(200); } catch (err) { console.error('Approve failed:', err); @@ -101,29 +169,15 @@ router.post('/approve/:id', async (req, res) => { }); // POST /application/deny/:id -router.post('/deny/:id', async (req, res) => { - const appID = req.params.id; +router.post('/deny/:id', [requireLogin, requireRole("Recruiter")], async (req: Request, res: Response) => { + const appID = Number(req.params.id); + const approver = Number(req.user.id); - const sql = ` - UPDATE applications - SET denied_at = NOW() - WHERE id = ? - AND approved_at IS NULL - AND denied_at IS NULL - `; try { - const result = await pool.execute(sql, appID); - - console.log(result); - - if (result.affectedRows === 0) { - res.status(400).json('Something went wrong denying the application'); - } - - if (result.affectedRows == 1) { - res.sendStatus(200); - } - + const app = await getApplicationByID(appID); + await denyApplication(appID, approver); + await setUserState(app.member_id, MemberState.Denied); + res.sendStatus(200); } catch (err) { console.error('Approve failed:', err); res.status(500).json({ error: 'Failed to deny application' }); @@ -131,10 +185,10 @@ router.post('/deny/:id', async (req, res) => { }); // POST /application/:id/comment -router.post('/:id/comment', async (req, res) => { +router.post('/:id/comment', [requireLogin], async (req: Request, res: Response) => { const appID = req.params.id; const data = req.body.message; - const user = 1; + const user = req.user; const sql = `INSERT INTO application_comments( application_id, @@ -143,11 +197,11 @@ router.post('/:id/comment', async (req, res) => { ) VALUES(?, ?, ?);` - try { - const conn = await pool.getConnection(); - const result = await conn.query(sql, [appID, user, data]) - console.log(result) + try { + var conn = await pool.getConnection(); + + const result = await conn.query(sql, [appID, user.id, data]) if (result.affectedRows !== 1) { conn.release(); throw new Error("Insert Failure") @@ -168,7 +222,64 @@ VALUES(?, ?, ?);` } catch (err) { console.error('Comment failed:', err); res.status(500).json({ error: 'Could not post comment' }); + } finally { + conn.release(); } }); -module.exports = router; +// POST /application/:id/comment +router.post('/:id/adminComment', [requireLogin, requireRole("Recruiter")], async (req: Request, res: Response) => { + const appID = req.params.id; + const data = req.body.message; + const user = req.user; + + const sql = `INSERT INTO application_comments( + application_id, + poster_id, + post_content, + admin_only + ) +VALUES(?, ?, ?, 1);` + + try { + var conn = await pool.getConnection(); + + const result = await conn.query(sql, [appID, user.id, data]) + if (result.affectedRows !== 1) { + conn.release(); + throw new Error("Insert Failure") + } + + const getSQL = `SELECT app.id AS comment_id, + app.post_content, + app.poster_id, + app.post_time, + app.last_modified, + app.admin_only, + member.name AS poster_name + FROM application_comments AS app + INNER JOIN members AS member ON member.id = app.poster_id + WHERE app.id = ?; `; + const comment = await conn.query(getSQL, [result.insertId]) + res.status(201).json(comment[0]); + + } catch (err) { + console.error('Comment failed:', err); + res.status(500).json({ error: 'Could not post comment' }); + } finally { + conn.release(); + } +}); + +router.post('/restart', async (req: Request, res: Response) => { + const user = req.user.id; + try { + await setUserState(user, MemberState.Guest); + res.sendStatus(200); + } catch (error) { + console.error('Comment failed:', error); + res.status(500).json({ error: 'Could not rester application' }); + } +}) + +export const applicationRouter = router; diff --git a/api/src/routes/auth.js b/api/src/routes/auth.js deleted file mode 100644 index a0ec0b8..0000000 --- a/api/src/routes/auth.js +++ /dev/null @@ -1,131 +0,0 @@ -const passport = require('passport'); -const OpenIDConnectStrategy = require('passport-openidconnect'); -const dotenv = require('dotenv'); -dotenv.config(); - -const express = require('express'); -const { param } = require('./applications'); -const router = express.Router(); -import pool from '../db'; -const querystring = require('querystring'); - - -passport.use(new OpenIDConnectStrategy({ - issuer: process.env.AUTH_ISSUER, - authorizationURL: 'https://sso.iceberg-gaming.com/application/o/authorize/', - tokenURL: 'https://sso.iceberg-gaming.com/application/o/token/', - userInfoURL: 'https://sso.iceberg-gaming.com/application/o/userinfo/', - clientID: process.env.AUTH_CLIENT_ID, - clientSecret: process.env.AUTH_CLIENT_SECRET, - callbackURL: process.env.AUTH_REDIRECT_URI, - scope: ['openid', 'profile'] -}, async function verify(issuer, sub, profile, jwtClaims, accessToken, refreshToken, params, cb) { - - // console.log('--- OIDC verify() called ---'); - // console.log('issuer:', issuer); - // console.log('sub:', sub); - // console.log('profile:', JSON.stringify(profile, null, 2)); - // console.log('id_token claims:', JSON.stringify(jwtClaims, null, 2)); - // console.log('preferred_username:', jwtClaims?.preferred_username); - - const con = await pool.getConnection(); - try { - await con.beginTransaction(); - - //lookup existing user - const existing = await con.query(`SELECT id FROM members WHERE authentik_issuer = ? AND authentik_sub = ? LIMIT 1;`, [issuer, sub]); - let memberId; - //if member exists - if (existing.length > 0) { - memberId = existing[0].id; - } else { - //otherwise: create account - const username = sub.username; - - const result = await con.query( - `INSERT INTO members (name, authentik_sub, authentik_issuer) VALUES (?, ?, ?)`, - [username, sub, issuer] - ) - memberId = result.insertId; - } - - await con.commit(); - return cb(null, { memberId }); - } catch (error) { - await con.rollback(); - return cb(error); - } finally { - con.release(); - } -})); - -router.get('/login', (req, res, next) => { - // Store redirect target in session if provided - req.session.redirectTo = req.query.redirect || '/'; - - next(); -}, passport.authenticate('openidconnect')); - -// router.get('/callback', (req, res, next) => { -// passport.authenticate('openidconnect', { -// successRedirect: req.session.redirectTo, -// failureRedirect: 'https://aj17thdev.nexuszone.net/' -// }) -// }); - -router.get('/callback', (req, res, next) => { - const redirectURI = req.session.redirectTo; - passport.authenticate('openidconnect', (err, user) => { - if (err) return next(err); - if (!user) return res.redirect('https://aj17thdev.nexuszone.net/'); - - req.logIn(user, err => { - if (err) return next(err); - - // Use redirect saved from session - const redirectTo = redirectURI || 'https://aj17thdev.nexuszone.net/'; - delete req.session.redirectTo; - return res.redirect(redirectTo); - }); - })(req, res, next); -}); - -router.post('/logout', function (req, res, next) { - req.logout(function (err) { - if (err) { return next(err); } - var params = { - client_id: process.env.AUTH_CLIENT_ID, - returnTo: 'https://aj17thdev.nexuszone.net/' - }; - res.redirect(process.env.AUTH_DOMAIN + '/v2/logout?' + querystring.stringify(params)); - }); -}); - -passport.serializeUser(function (user, cb) { - process.nextTick(function () { - cb(null, user); - }); -}); - -passport.deserializeUser(function (user, cb) { - process.nextTick(async function () { - const memberID = user.memberId; - - const con = await pool.getConnection(); - - var userData; - try { - let userResults = await con.query(`SELECT id, name FROM members WHERE id = ?;`, [memberID]) - userData = userResults[0]; - - } catch (error) { - console.error(error) - } finally { - con.release(); - } - return cb(null, userData); - }); -}); - - -module.exports = router; \ No newline at end of file diff --git a/api/src/routes/auth.ts b/api/src/routes/auth.ts new file mode 100644 index 0000000..4925da1 --- /dev/null +++ b/api/src/routes/auth.ts @@ -0,0 +1,185 @@ +const passport = require('passport'); +const OpenIDConnectStrategy = require('passport-openidconnect'); +const dotenv = require('dotenv'); +dotenv.config(); + +const express = require('express'); +const { param } = require('./applications'); +const router = express.Router(); +import { Role } from '@app/shared/types/roles'; +import pool from '../db'; +import { requireLogin } from '../middleware/auth'; +import { getUserRoles } from '../services/rolesService'; +import { getUserState, mapDiscordtoID } from '../services/memberService'; +import { MemberState } from '@app/shared/types/member'; +import { toDateTime } from '@app/shared/utils/time'; +const querystring = require('querystring'); + +function parseJwt(token) { + return JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()); +} + +passport.use(new OpenIDConnectStrategy({ + issuer: process.env.AUTH_ISSUER, + authorizationURL: process.env.AUTH_DOMAIN + '/authorize/', + tokenURL: process.env.AUTH_DOMAIN + '/token/', + userInfoURL: process.env.AUTH_DOMAIN + '/userinfo/', + clientID: process.env.AUTH_CLIENT_ID, + clientSecret: process.env.AUTH_CLIENT_SECRET, + callbackURL: process.env.AUTH_REDIRECT_URI, + scope: ['openid', 'profile', 'discord'] +}, async function verify(issuer, sub, profile, jwtClaims, accessToken, refreshToken, params, cb) { + + // console.log('--- OIDC verify() called ---'); + // console.log('issuer:', issuer); + // console.log('sub:', sub); + // // console.log('discord:', discord); + // console.log('profile:', profile); + // console.log('jwt: ', parseJwt(jwtClaims)); + // console.log('params:', params); + + + try { + var con = await pool.getConnection(); + + await con.beginTransaction(); + + //lookup existing user + const existing = await con.query(`SELECT id FROM members WHERE authentik_issuer = ? AND authentik_sub = ? LIMIT 1;`, [issuer, sub]); + let memberId: number | null = null; + //if member exists + if (existing.length > 0) { + memberId = existing[0].id; + } else { + //otherwise: create account mode + const jwt = parseJwt(jwtClaims); + const discordID = jwt.discord?.id as number; + + //check if account is available to claim + if (discordID) + memberId = await mapDiscordtoID(discordID); + + if (discordID && memberId) { + // claim account + console.log("Claiming account"); + const result = await con.query( + `UPDATE members SET authentik_sub = ?, authentik_issuer = ? WHERE id = ?;`, + [sub, issuer, memberId] + ) + } else { + console.log("New Account"); + // new account + const username = sub.username; + const result = await con.query( + `INSERT INTO members (name, authentik_sub, authentik_issuer) VALUES (?, ?, ?)`, + [username, sub, issuer] + ) + memberId = Number(result.insertId); + } + } + + await con.query(`UPDATE members SET last_login = ? WHERE id = ?`, [toDateTime(new Date()), memberId]) + + await con.commit(); + return cb(null, { memberId }); + } catch (error) { + await con.rollback(); + return cb(error); + } finally { + con.release(); + } +})); + +router.get('/login', (req, res, next) => { + // Store redirect target in session if provided + req.session.redirectTo = req.query.redirect; + + next(); +}, passport.authenticate('openidconnect')); + + +router.get('/callback', (req, res, next) => { + const redirectURI = req.session.redirectTo; + passport.authenticate('openidconnect', (err, user) => { + if (err) return next(err); + if (!user) return res.redirect(process.env.CLIENT_URL); + + req.logIn(user, err => { + if (err) return next(err); + + // Use redirect saved from session + const redirectTo = redirectURI || process.env.CLIENT_URL; + delete req.session.redirectTo; + return res.redirect(redirectTo); + }); + })(req, res, next); +}); + +router.get('/logout', [requireLogin], function (req, res, next) { + req.logout(function (err) { + if (err) { return next(err); } + + req.session.destroy((err) => { + if (err) { return next(err); } + + res.clearCookie('connect.sid', { + path: '/', + domain: process.env.CLIENT_DOMAIN, + httpOnly: true, + sameSite: 'lax' + }); + + var params = { + client_id: process.env.AUTH_CLIENT_ID, + returnTo: process.env.CLIENT_URL + }; + res.redirect(process.env.AUTH_END_SESSION_URI + '?' + querystring.stringify(params)); + + }) + }); +}); + +passport.serializeUser(function (user, cb) { + process.nextTick(function () { + cb(null, user); + }); +}); + +passport.deserializeUser(function (user, cb) { + process.nextTick(async function () { + + const memberID = user.memberId as number; + + + var userData: { id: number, name: string, roles: Role[], state: MemberState }; + try { + var con = await pool.getConnection(); + let userResults = await con.query(`SELECT id, name FROM members WHERE id = ?;`, [memberID]) + userData = userResults[0]; + let userRoles = await getUserRoles(memberID); + userData.roles = userRoles || []; + userData.state = await getUserState(memberID); + } catch (error) { + console.error(error) + } finally { + con.release(); + } + return cb(null, userData); + }); +}); + +declare global { + namespace Express { + interface Request { + user: { + id: number; + name: string; + roles: Role[]; + state: MemberState; + }; + } + } +} + + +export const authRouter = router; \ No newline at end of file diff --git a/api/src/routes/calendar.ts b/api/src/routes/calendar.ts index 2154eec..a5c96be 100644 --- a/api/src/routes/calendar.ts +++ b/api/src/routes/calendar.ts @@ -1,4 +1,8 @@ -import { getEventAttendance, getEventDetails, getShortEventsInRange } from "../services/calendarService"; +import { Request, Response } from "express"; +import { createEvent, getEventAttendance, getEventDetails, getShortEventsInRange, setAttendanceStatus, setEventCancelled, updateEvent } from "../services/calendarService"; +import { CalendarAttendance, CalendarEvent } from "@app/shared/types/calendar"; +import { requireLogin, requireMemberState, requireRole } from "../middleware/auth"; +import { MemberState } from "@app/shared/types/member"; const express = require('express'); const r = express.Router(); @@ -9,42 +13,106 @@ function addMonths(date: Date, months: number): Date { return d } -//get calendar events paged +//get calendar events paged, requires a query string with from= and to= as mariadb ISO strings r.get('/', async (req, res) => { - const viewDate: Date = req.body.date; - //generate date range - const backDate: Date = addMonths(viewDate, -1); - const frontDate: Date = addMonths(viewDate, 2); + try { + const fromDate: string = req.query.from; + const toDate: string = req.query.to; - const events = getShortEventsInRange(backDate, frontDate); + if (fromDate === undefined || toDate === undefined) { + res.status(400).send("Missing required query parameters 'from' and 'to'"); + return; + } - res.status(200).json(events); + const events = await getShortEventsInRange(fromDate, toDate); + + res.status(200).json(events); + } catch (error) { + console.error('Error fetching calendar events:', error); + res.status(500).send('Error fetching calendar events'); + } }); r.get('/upcoming', async (req, res) => { res.sendStatus(501); }) -//get event details -r.get('/:id', async (req, res) => { +r.post('/:id/cancel', [requireLogin, requireMemberState(MemberState.Member)], async (req: Request, res: Response) => { try { - const eventID: number = req.params.id; + const eventID = Number(req.params.id); + setEventCancelled(eventID, true); + res.sendStatus(200); + } catch (error) { + console.error('Error setting cancel status:', error); + res.status(500).send('Error setting cancel status'); + } +}) +r.post('/:id/uncancel', [requireLogin, requireMemberState(MemberState.Member)], async (req: Request, res: Response) => { + try { + const eventID = Number(req.params.id); + setEventCancelled(eventID, false); + res.sendStatus(200); + } catch (error) { + console.error('Error setting cancel status:', error); + res.status(500).send('Error setting cancel status'); + } +}) - let details = getEventDetails(eventID); - let attendance = await getEventAttendance(eventID); - let out = { ...details, attendance } - console.log(out); - res.status(200).json(out); +r.post('/:id/attendance', [requireLogin, requireMemberState(MemberState.Member)], async (req: Request, res: Response) => { + try { + let member = req.user.id; + let event = Number(req.params.id); + let state = req.query.state as CalendarAttendance; + setAttendanceStatus(member, event, state); + res.sendStatus(200); + } catch (error) { + console.error('Failed to set attendance:', error); + res.status(500).json(error); + } +}) +//get event details +r.get('/:id', async (req: Request, res: Response) => { + try { + const eventID: number = Number(req.params.id); + + let details: CalendarEvent = await getEventDetails(eventID); + details.eventSignups = await getEventAttendance(eventID); + res.status(200).json(details); } catch (err) { console.error('Insert failed:', err); res.status(500).json(err); } }) -//post a new calendar event -r.post('/', async (req, res) => { +//post a new calendar event +r.post('/', [requireLogin, requireMemberState(MemberState.Member)], async (req: Request, res: Response) => { + try { + const member = req.user.id; + let event: CalendarEvent = req.body; + event.creator_id = member; + event.start = new Date(event.start); + event.end = new Date(event.end); + createEvent(event); + res.sendStatus(200); + } catch (error) { + console.error('Failed to create event:', error); + res.status(500).json(error); + } }) -module.exports.calendar = r; \ No newline at end of file +r.put('/', [requireLogin, requireMemberState(MemberState.Member)], async (req: Request, res: Response) => { + try { + let event: CalendarEvent = req.body; + event.start = new Date(event.start); + event.end = new Date(event.end); + updateEvent(event); + res.sendStatus(200); + } catch (error) { + console.error('Failed to update event:', error); + res.status(500).json(error); + } +}) + +export const calendarRouter = r; diff --git a/api/src/routes/course.ts b/api/src/routes/course.ts new file mode 100644 index 0000000..91a2057 --- /dev/null +++ b/api/src/routes/course.ts @@ -0,0 +1,98 @@ +import { CourseAttendee, CourseEventDetails } from "@app/shared/types/course"; +import { getAllCourses, getCourseEventAttendees, getCourseEventDetails, getCourseEventRoles, getCourseEvents, insertCourseEvent } from "../services/CourseSerivce"; +import { Request, Response, Router } from "express"; +import { requireLogin, requireMemberState } from "../middleware/auth"; +import { MemberState } from "@app/shared/types/member"; + +const cr = Router(); +const er = Router(); + +cr.use(requireLogin) +er.use(requireLogin) +cr.use(requireMemberState(MemberState.Member)) +er.use(requireMemberState(MemberState.Member)) + +cr.get('/', async (req, res) => { + try { + const courses = await getAllCourses(); + res.status(200).json(courses); + } catch (err) { + console.error('failed to fetch courses', err); + res.status(500).json('failed to fetch courses\n' + err); + } +}) + +cr.get('/roles', async (req, res) => { + try { + const roles = await getCourseEventRoles(); + res.status(200).json(roles); + } catch (err) { + console.error('failed to fetch course roles', err); + res.status(500).json('failed to fetch course roles\n' + err); + } +}) + +er.get('/', async (req: Request, res: Response) => { + try { + const allowedSorts = new Map([ + ["ascending", "ASC"], + ["descending", "DESC"] + ]); + + const page = Number(req.query.page) || undefined; + const pageSize = Number(req.query.pageSize) || undefined; + + const sort = String(req.query.sort || "").toLowerCase(); + const search = String(req.query.search || "").toLowerCase(); + if (!allowedSorts.has(sort)) { + return res.status(400).json({ + message: `Invalid sort direction '${req.query.sort}'. Allowed values are 'ascending' or 'descending'.` + }); + } + + const sortDir = allowedSorts.get(sort); + + let events = await getCourseEvents(sortDir, search, page, pageSize); + res.status(200).json(events); + } catch (error) { + console.error('failed to fetch reports', error); + res.status(500).json(error); + } +}); + +er.get('/:id', async (req: Request, res: Response) => { + try { + let out = await getCourseEventDetails(Number(req.params.id)); + res.status(200).json(out); + } catch (error) { + console.error('failed to fetch report', error); + res.status(500).json(error); + } +}); + +er.get('/attendees/:id', async (req: Request, res: Response) => { + try { + const attendees: CourseAttendee[] = await getCourseEventAttendees(Number(req.params.id)); + res.status(200).json(attendees); + } catch (err) { + console.error('failed to fetch attendees', err); + res.status(500).json("failed to fetch attendees\n" + err); + } +}) + +er.post('/', async (req: Request, res: Response) => { + const posterID: number = req.user.id; + try { + let data: CourseEventDetails = req.body; + data.created_by = posterID; + data.event_date = new Date(data.event_date); + const id = await insertCourseEvent(data); + res.status(201).json(id); + } catch (error) { + console.error('failed to post training', error); + res.status(500).json("failed to post training\n" + error) + } +}) + +export const courseRouter = cr; +export const eventRouter = er; diff --git a/api/src/routes/docs.ts b/api/src/routes/docs.ts new file mode 100644 index 0000000..13ada87 --- /dev/null +++ b/api/src/routes/docs.ts @@ -0,0 +1,24 @@ +const express = require('express'); +const router = express.Router(); + +import { Request, Response } from 'express'; +import { requireLogin } from '../middleware/auth'; + +router.get('/welcome', [requireLogin], async (req: Request, res: Response) => { + const output = await fetch(`${process.env.DOC_HOST}/api/pages/717`, { + headers: { + Authorization: `Token ${process.env.DOC_TOKEN_ID}:${process.env.DOC_TOKEN_SECRET}`, + } + }) + + if (output.ok) { + const out = await output.json(); + res.status(200).json(out.html); + } else { + console.error("Failed to fetch LOA policy from bookstack"); + res.sendStatus(500); + } +}) + + +export const docsRouter = router; \ No newline at end of file diff --git a/api/src/routes/loa.js b/api/src/routes/loa.js deleted file mode 100644 index c14bf24..0000000 --- a/api/src/routes/loa.js +++ /dev/null @@ -1,56 +0,0 @@ -const express = require('express'); -const router = express.Router(); - -import pool from '../db'; - -//post a new LOA -router.post("/", async (req, res) => { - const { member_id, filed_date, start_date, end_date, reason } = req.body; - - if (!member_id || !filed_date || !start_date || !end_date) { - return res.status(400).json({ error: "Missing required fields" }); - } - - try { - const result = await pool.query( - `INSERT INTO leave_of_absences - (member_id, filed_date, start_date, end_date, reason) - VALUES (?, ?, ?, ?, ?)`, - [member_id, filed_date, start_date, end_date, reason] - ); - res.sendStatus(201); - } catch (error) { - console.error(error); - res.status(500).send('Something went wrong', error); - } -}); - -//get my current LOA -router.get("/me", async (req, res) => { - //TODO: implement current user getter - const user = 89; - - try { - const result = await pool.query("SELECT * FROM leave_of_absences WHERE member_id = ?", [user]) - res.status(200).json(result) - } catch (error) { - console.error(error); - res.status(500).send(error); - } -}) - -router.get('/all', async (req, res) => { - try { - const result = await pool.query( - `SELECT loa.*, members.name - FROM leave_of_absences AS loa - INNER JOIN members ON loa.member_id = members.id; - `); - res.status(200).json(result) - } catch (error) { - console.error(error); - res.status(500).send(error); - } -}) - -module.exports = router; diff --git a/api/src/routes/loa.ts b/api/src/routes/loa.ts new file mode 100644 index 0000000..65aa115 --- /dev/null +++ b/api/src/routes/loa.ts @@ -0,0 +1,154 @@ +const express = require('express'); +const router = express.Router(); + +import { Request, Response } from 'express'; +import pool from '../db'; +import { closeLOA, createNewLOA, getAllLOA, getLOAbyID, getLoaTypes, getUserLOA, setLOAExtension } from '../services/loaService'; +import { LOARequest } from '@app/shared/types/loa'; +import { requireLogin, requireRole } from '../middleware/auth'; + +router.use(requireLogin); + +//member posts LOA +router.post("/", async (req: Request, res: Response) => { + let LOARequest = req.body as LOARequest; + LOARequest.member_id = req.user.id; + LOARequest.created_by = req.user.id; + LOARequest.filed_date = new Date(); + + try { + await createNewLOA(LOARequest); + res.sendStatus(201); + } catch (error) { + console.error(error); + res.status(500).send(error); + } +}); + +//admin posts LOA +router.post("/admin", [requireRole(['17th Administrator', '17th HQ', '17th Command'])], async (req: Request, res: Response) => { + let LOARequest = req.body as LOARequest; + LOARequest.created_by = req.user.id; + LOARequest.filed_date = new Date(); + try { + await createNewLOA(LOARequest); + res.sendStatus(201); + } catch (error) { + console.error(error); + res.status(500).send(error); + } +}); + +//get my current LOA +router.get("/me", async (req: Request, res: Response) => { + const user = req.user.id; + try { + const result = await getUserLOA(user); + res.status(200).json(result) + } catch (error) { + console.error(error); + res.status(500).send(error); + } +}) + +//get my LOA history +router.get("/history", async (req: Request, res: Response) => { + try { + const user = req.user.id; + + const page = Number(req.query.page) || undefined; + const pageSize = Number(req.query.pageSize) || undefined; + + const result = await getUserLOA(user, page, pageSize); + res.status(200).json(result) + } catch (error) { + console.error(error); + res.status(500).send(error); + } +}) + +router.get('/all', [requireRole(['17th Administrator', '17th HQ', '17th Command'])], async (req: Request, res: Response) => { + try { + const page = Number(req.query.page) || undefined; + const pageSize = Number(req.query.pageSize) || undefined; + const result = await getAllLOA(page, pageSize); + res.status(200).json(result) + } catch (error) { + console.error(error); + res.status(500).send(error); + } +}) + +router.get('/types', async (req: Request, res: Response) => { + try { + let out = await getLoaTypes(); + res.status(200).json(out); + } catch (error) { + res.status(500).json(error); + console.error(error); + } +}) + +router.post('/cancel/:id', async (req: Request, res: Response) => { + let closer = req.user.id; + let id = Number(req.params.id); + try { + let loa = await getLOAbyID(id); + if (loa.member_id != closer) { + return res.sendStatus(403); + } + + await closeLOA(Number(req.params.id), closer); + res.sendStatus(200); + } catch (error) { + console.error(error); + res.status(500).json(error); + } +}) + +//TODO: enforce admin only +router.post('/adminCancel/:id', [requireRole(['17th Administrator', '17th HQ', '17th Command'])], async (req: Request, res: Response) => { + let closer = req.user.id; + try { + await closeLOA(Number(req.params.id), closer); + res.sendStatus(200); + } catch (error) { + console.error(error); + res.status(500).json(error); + } +}) + +// TODO: Enforce admin only +router.post('/extend/:id', [requireRole(['17th Administrator', '17th HQ', '17th Command'])], async (req: Request, res: Response) => { + const to: Date = req.body.to; + + if (!to) { + res.status(400).send("Extension length is required"); + } + + try { + await setLOAExtension(Number(req.params.id), to); + res.sendStatus(200); + } catch (error) { + console.error(error) + res.status(500).json(error); + } +}) + +router.get('/policy', async (req: Request, res: Response) => { + const output = await fetch(`${process.env.DOC_HOST}/api/pages/42`, { + headers: { + Authorization: `Token ${process.env.DOC_TOKEN_ID}:${process.env.DOC_TOKEN_SECRET}`, + } + }) + + if (output.ok) { + const out = await output.json(); + res.status(200).json(out.html); + } else { + console.error("Failed to fetch LOA policy from bookstack"); + res.sendStatus(500); + } +}) + +export const loaRouter = router; \ No newline at end of file diff --git a/api/src/routes/members.js b/api/src/routes/members.js deleted file mode 100644 index c93f249..0000000 --- a/api/src/routes/members.js +++ /dev/null @@ -1,84 +0,0 @@ -const express = require('express'); -const router = express.Router(); - -import pool from '../db'; -import { getUserData } from '../services/memberService'; -import { getUserRoles } from '../services/rolesService'; - -router.use((req, res, next) => { - console.log(req.user); - console.log('Time:', Date.now()) - next() -}) - -//get all users -router.get('/', async (req, res) => { - try { - const result = await pool.query( - `SELECT - v.*, - CASE - WHEN EXISTS ( - SELECT 1 - FROM leave_of_absences l - WHERE l.member_id = v.member_id - AND l.deleted = 0 - AND UTC_TIMESTAMP() BETWEEN l.start_date AND l.end_date - ) THEN 1 ELSE 0 - END AS on_loa - FROM view_member_rank_status_all v;`); - return res.status(200).json(result); - } catch (err) { - console.error('Error fetching users:', err); - return res.status(500).json({ error: 'Failed to fetch users' }); - } -}); - -router.get('/me', async (req, res) => { - if (req.user === undefined) - return res.sendStatus(401) - - try { - const { id, name, state } = await getUserData(req.user.id); - const LOAData = await pool.query( - `SELECT * - FROM leave_of_absences - WHERE member_id = ? - AND deleted = 0 - AND UTC_TIMESTAMP() BETWEEN start_date AND end_date;`, req.user.id); - - const roleData = await getUserRoles(req.user.id); - - const userDataFull = { id, name, state, LOAData, roleData }; - console.log(userDataFull) - res.status(200).json(userDataFull); - } catch (error) { - console.error('Error fetching user data:', error); - return res.status(500).json({ error: 'Failed to fetch user data' }); - } -}) - -router.get('/:id', async (req, res) => { - try { - const userId = req.params.id; - const result = await pool.query('SELECT * FROM view_member_rank_status_all WHERE id = $1;', [userId]); - if (result.rows.length === 0) { - return res.status(404).json({ error: 'User not found' }); - } - return res.status(200).json(result.rows[0]); - } catch (err) { - console.error('Error fetching user:', err); - return res.status(500).json({ error: 'Failed to fetch user' }); - } -}); - -//update a user's display name (stub) -router.put('/:id/displayname', async (req, res) => { - // Stub: not implemented yet - return res.status(501).json({ error: 'Update display name not implemented' }); -}); - - - - -module.exports = router; diff --git a/api/src/routes/members.ts b/api/src/routes/members.ts new file mode 100644 index 0000000..c73b8f2 --- /dev/null +++ b/api/src/routes/members.ts @@ -0,0 +1,130 @@ +const express = require('express'); +const router = express.Router(); + +import { Request, Response } from 'express'; +import pool from '../db'; +import { requireLogin, requireMemberState, requireRole } from '../middleware/auth'; +import { getUserActiveLOA } from '../services/loaService'; +import { getAllMembersLite, getMemberSettings, getMembersFull, getMembersLite, getUserData, getUserState, setUserSettings } from '../services/memberService'; +import { getUserRoles } from '../services/rolesService'; +import { memberSettings, MemberState, myData } from '@app/shared/types/member'; + +//get all users +router.get('/', [requireLogin, requireMemberState(MemberState.Member)], async (req, res) => { + try { + const result = await pool.query( + `SELECT + v.*, + CASE + WHEN EXISTS ( + SELECT 1 + FROM leave_of_absences l + WHERE l.member_id = v.member_id + AND l.deleted = 0 + AND UTC_TIMESTAMP() BETWEEN l.start_date AND l.end_date + ) THEN 1 ELSE 0 + END AS on_loa + FROM view_member_rank_unit_status_latest v;`); + return res.status(200).json(result); + } catch (err) { + console.error('Error fetching users:', err); + return res.status(500).json({ error: 'Failed to fetch users' }); + } +}); + +router.get('/me', [requireLogin], async (req, res) => { + if (req.user === undefined) + return res.sendStatus(401) + + try { + const memData = await getUserData(req.user.id); + const LOAData = await getUserActiveLOA(req.user.id); + const memState = await getUserState(req.user.id); + const roleData = await getUserRoles(req.user.id); + + const userDataFull: myData = { member: memData, LOAs: LOAData, roles: roleData, state: memState }; + res.status(200).json(userDataFull); + } catch (error) { + console.error('Error fetching user data:', error); + return res.status(500).json({ error: 'Failed to fetch user data' }); + } +}) + +router.get('/settings', [requireLogin], async (req: Request, res: Response) => { + try { + let user = req.user.id; + let output = await getMemberSettings(user); + res.status(200).json(output); + } catch (error) { + console.error(error); + res.status(500).json(error); + } +}) + +router.put('/settings', [requireLogin], async (req: Request, res: Response) => { + try { + let user = req.user.id; + let settings: memberSettings = req.body; + await setUserSettings(user, settings); + res.sendStatus(200); + } catch (error) { + console.error(error); + res.status(500).json(error); + } +}) + +router.get('/lite', [requireLogin], async (req: Request, res: Response) => { + try { + let out = await getAllMembersLite(); + res.status(200).json(out); + } catch (error) { + console.error(error); + res.status(500).json(error); + } +}) + +router.post('/lite/bulk', async (req: Request, res: Response) => { + try { + let ids = req.body.ids; + let out = await getMembersLite(ids); + res.status(200).json(out); + } catch (error) { + console.error(error); + res.status(500).json(error); + } +}) + + +router.post('/full/bulk', async (req: Request, res: Response) => { + try { + let ids = req.body.ids; + let out = await getMembersFull(ids); + res.status(200).json(out); + } catch (error) { + console.error(error); + res.status(500).json(error); + } +}) + +router.get('/:id', [requireLogin], async (req, res) => { + try { + const userId = req.params.id; + const result = await pool.query('SELECT * FROM view_member_rank_unit_status_latest WHERE id = $1;', [userId]); + if (result.rows.length === 0) { + return res.status(404).json({ error: 'User not found' }); + } + return res.status(200).json(result.rows[0]); + } catch (err) { + console.error('Error fetching user:', err); + return res.status(500).json({ error: 'Failed to fetch user' }); + } +}); + +//update a user's display name (stub) +router.put('/:id/displayname', async (req, res) => { + // Stub: not implemented yet + return res.status(501); +}); + + +export const memberRouter = router; diff --git a/api/src/routes/ranks.js b/api/src/routes/ranks.ts similarity index 56% rename from api/src/routes/ranks.js rename to api/src/routes/ranks.ts index cb8c4b1..5eff733 100644 --- a/api/src/routes/ranks.js +++ b/api/src/routes/ranks.ts @@ -1,10 +1,18 @@ -const express = require('express'); +import { MemberState } from "@app/shared/types/member"; +import { requireLogin, requireMemberState, requireRole } from "../middleware/auth"; +import { getAllRanks, insertMemberRank } from "../services/rankService"; + +import express = require('express'); const r = express.Router(); const ur = express.Router(); -const { getAllRanks, insertMemberRank } = require('../services/rankService') + + +r.use(requireLogin) +ur.use(requireLogin) //insert a new latest rank for a user -ur.post('/', async (req, res) => {3 +ur.post('/', [requireRole(["17th Command", "17th Administrator", "17th HQ"]), requireMemberState(MemberState.Member)], async (req, res) => { + 3 try { const change = req.body?.change; await insertMemberRank(change.member_id, change.rank_id, change.date); @@ -27,5 +35,5 @@ r.get('/', async (req, res) => { } }); -module.exports.ranks = r; -module.exports.memberRanks = ur; \ No newline at end of file +export const ranks = r; +export const memberRanks = ur; \ No newline at end of file diff --git a/api/src/routes/roles.js b/api/src/routes/roles.ts similarity index 74% rename from api/src/routes/roles.js rename to api/src/routes/roles.ts index f1857f6..d0f0e68 100644 --- a/api/src/routes/roles.js +++ b/api/src/routes/roles.ts @@ -2,11 +2,16 @@ const express = require('express'); const r = express.Router(); const ur = express.Router(); +import { MemberState } from '@app/shared/types/member'; import pool from '../db'; +import { requireLogin, requireMemberState, requireRole } from '../middleware/auth'; import { assignUserGroup, createGroup } from '../services/rolesService'; +r.use(requireLogin) +ur.use(requireLogin) + //manually assign a member to a group -ur.post('/', async (req, res) => { +ur.post('/', [requireMemberState(MemberState.Member), requireRole("17th Administrator")], async (req, res) => { try { const body = req.body; @@ -20,10 +25,9 @@ ur.post('/', async (req, res) => { }); //manually remove member from group -ur.delete('/', async (req, res) => { +ur.delete('/', [requireMemberState(MemberState.Member), requireRole("17th Administrator")], async (req, res) => { try { const body = req.body; - console.log(body); const sql = 'DELETE FROM members_roles WHERE member_id = ? AND role_id = ?' await pool.query(sql, [body.member_id, body.role_id]) @@ -38,9 +42,9 @@ ur.delete('/', async (req, res) => { }) //get all roles -r.get('/', async (req, res) => { +r.get('/', [requireMemberState(MemberState.Member)], async (req, res) => { try { - const con = await pool.getConnection(); + var con = await pool.getConnection(); // Get all roles const roles = await con.query('SELECT * FROM roles;'); @@ -49,7 +53,7 @@ r.get('/', async (req, res) => { const membersRoles = await con.query(` SELECT mr.role_id, v.* FROM members_roles mr - JOIN view_member_rank_status_all v ON mr.member_id = v.member_id + JOIN view_member_rank_unit_status_latest v ON mr.member_id = v.member_id `); @@ -68,19 +72,19 @@ r.get('/', async (req, res) => { members: roleIdToMembers[role.id] || [] })); - con.release(); res.json(result); } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); + } finally { + con.release(); } }); //create a new role -r.post('/', async (req, res) => { +r.post('/', [requireMemberState(MemberState.Member), requireRole("17th Administrator")], async (req, res) => { try { const { name, color, description } = req.body; - console.log('Creating role:', { name, color, description }); if (!name || !color) { return res.status(400).json({ error: 'Name and color are required' }); } @@ -99,7 +103,7 @@ r.post('/', async (req, res) => { } }) -r.delete('/:id', async (req, res) => { +r.delete('/:id', [requireMemberState(MemberState.Member), requireRole("17th Administrator")], async (req, res) => { try { const id = req.params.id; @@ -107,10 +111,10 @@ r.delete('/:id', async (req, res) => { const res = await pool.query(sql, [id]); res.sendStatus(200); } catch (error) { - console.log(error); + console.error(error); res.sendStatus(500); } }) -module.exports.roles = r; -module.exports.memberRoles = ur; \ No newline at end of file +export const roles = r; +export const memberRoles = ur; diff --git a/api/src/routes/statuses.js b/api/src/routes/statuses.ts similarity index 70% rename from api/src/routes/statuses.js rename to api/src/routes/statuses.ts index 8e9d48e..8457800 100644 --- a/api/src/routes/statuses.js +++ b/api/src/routes/statuses.ts @@ -1,11 +1,15 @@ -const express = require('express'); -const status = express.Router(); -const memberStatus = express.Router(); +import express = require('express'); +const statusR = express.Router(); +const memberStatusR = express.Router(); import pool from '../db'; +import { requireLogin } from '../middleware/auth'; + +statusR.use(requireLogin); +memberStatusR.use(requireLogin); //insert a new latest rank for a user -memberStatus.post('/', async (req, res) => { +memberStatusR.post('/', async (req, res) => { // try { // const App = req.body?.App || {}; @@ -30,7 +34,7 @@ memberStatus.post('/', async (req, res) => { }); //get all statuses -status.get('/', async (req, res) => { +statusR.get('/', async (req, res) => { try { const result = await pool.query('SELECT * FROM statuses;'); res.json(result); @@ -40,7 +44,8 @@ status.get('/', async (req, res) => { } }); -module.exports.status = status; -module.exports.memberStatus = memberStatus; +export const status = statusR; +export const memberStatus = memberStatusR; + // TODO, implement get all ranks route with SQL stirng SELECT id, name, short_name, category, sort_id FROM ranks; \ No newline at end of file diff --git a/api/src/services/CourseSerivce.ts b/api/src/services/CourseSerivce.ts new file mode 100644 index 0000000..d85ffd8 --- /dev/null +++ b/api/src/services/CourseSerivce.ts @@ -0,0 +1,163 @@ +import pool from "../db" +import { Course, CourseAttendee, CourseAttendeeRole, CourseEventDetails, CourseEventSummary, RawAttendeeRow } from "@app/shared/types/course" +import { PagedData } from "@app/shared/types/pagination"; +import { toDateTime } from "@app/shared/utils/time"; +export async function getAllCourses(): Promise { + const sql = "SELECT * FROM courses WHERE deleted = false ORDER BY name ASC;" + + const res: Course[] = await pool.query(sql); + + return res; +} + +export async function getCourseByID(id: number): Promise { + const sql = "SELECT * FROM courses WHERE id = ?;" + const res: Course[] = await pool.query(sql, [id]); + return res[0]; +} + +function buildAttendee(row: RawAttendeeRow): CourseAttendee { + return { + passed_bookwork: !!row.passed_bookwork, + passed_qual: !!row.passed_qual, + attendee_id: row.attendee_id, + course_event_id: row.course_event_id, + created_at: new Date(row.created_at), + updated_at: new Date(row.updated_at), + remarks: row.remarks, + attendee_role_id: row.attendee_role_id, + attendee_name: row.attendee_name, + role: row.role_id + ? { + id: row.role_id, + name: row.role_name, + description: row.role_description, + deleted: !!row.role_deleted, + created_at: new Date(row.role_created_at), + updated_at: new Date(row.role_updated_at), + } + : null + }; +} + +export async function getCourseEventAttendees(id: number): Promise { + const sql = `SELECT + ca.*, + mem.name AS attendee_name, + ar.id AS role_id, + ar.name AS role_name, + ar.description AS role_description, + ar.deleted AS role_deleted, + ar.created_at AS role_created_at, + ar.updated_at AS role_updated_at + FROM course_attendees ca + LEFT JOIN course_attendee_roles ar ON ar.id = ca.attendee_role_id + LEFT JOIN members mem ON ca.attendee_id = mem.id + WHERE ca.course_event_id = ?;`; + + const res: RawAttendeeRow[] = await pool.query(sql, [id]); + + return res.map((row) => buildAttendee(row)) +} + +export async function getCourseEventDetails(id: number): Promise { + const sql = `SELECT + E.*, + M.name AS created_by_name, + C.name AS course_name + FROM course_events AS E + LEFT JOIN courses AS C + ON E.course_id = C.id + LEFT JOIN members AS M + ON E.created_by = M.id + WHERE E.id = ?; + `; + let rows: CourseEventDetails[] = await pool.query(sql, [id]); + let event = rows[0]; + event.attendees = await getCourseEventAttendees(id); + event.course = await getCourseByID(event.course_id); + return event; +} + +export async function insertCourseEvent(event: CourseEventDetails): Promise { + try { + var con = await pool.getConnection(); + + let course: Course = await getCourseByID(event.course_id); + + await con.beginTransaction(); + const res = await con.query("INSERT INTO course_events (course_id, event_date, remarks, created_by, hasBookwork, hasQual) VALUES (?, ?, ?, ?, ?, ?);", [event.course_id, toDateTime(event.event_date), event.remarks, event.created_by, course.hasBookwork, course.hasQual]); + var eventID: number = res.insertId; + + for (const attendee of event.attendees) { + await con.query(`INSERT INTO course_attendees ( + attendee_id, + course_event_id, + attendee_role_id, + passed_bookwork, + passed_qual, + remarks + ) + VALUES (?, ?, ?, ?, ?, ?);`, [attendee.attendee_id, eventID, attendee.attendee_role_id, attendee.passed_bookwork, attendee.passed_qual, attendee.remarks]); + } + await con.commit(); + return Number(eventID); + } catch (error) { + if (con) await con.rollback(); + throw error; + } finally { + if (con) await con.release(); + } +} + +export async function getCourseEvents(sortDir: string, search: string = "", page = 1, pageSize = 10): Promise> { + const offset = (page - 1) * pageSize; + + let params = []; + let searchString = ""; + if (search !== "") { + searchString = `WHERE (C.name LIKE ? OR + C.short_name LIKE ? OR + M.name LIKE ?) `; + const p = `%${search}%`; + params.push(p, p, p); + } + + const sql = `SELECT + E.id AS event_id, + E.course_id, + E.event_date AS date, + E.created_by, + C.name AS course_name, + C.short_name AS course_shortname, + M.name AS created_by_name + FROM course_events AS E + LEFT JOIN courses AS C + ON E.course_id = C.id + LEFT JOIN members AS M + ON E.created_by = M.id + ${searchString} + ORDER BY E.event_date ${sortDir} + LIMIT ? OFFSET ?;`; + + let countSQL = `SELECT COUNT(*) AS count + FROM course_events AS E + LEFT JOIN courses AS C + ON E.course_id = C.id + LEFT JOIN members AS M + ON E.created_by = M.id ${searchString};` + let recordCount = Number((await pool.query(countSQL, [...params]))[0].count); + let pageCount = recordCount / pageSize; + + let events: CourseEventSummary[] = await pool.query(sql, [...params, pageSize, offset]); + + let output: PagedData = { data: events, pagination: { page: page, pageSize: pageSize, total: recordCount, totalPages: pageCount } } + + return output; +} + +export async function getCourseEventRoles(): Promise { + const sql = "SELECT * FROM course_attendee_roles;" + const roles: CourseAttendeeRole[] = await pool.query(sql); + return roles; +} \ No newline at end of file diff --git a/api/src/services/applicationService.ts b/api/src/services/applicationService.ts index 8e94a08..159dbe3 100644 --- a/api/src/services/applicationService.ts +++ b/api/src/services/applicationService.ts @@ -1,5 +1,6 @@ import { ApplicationListRow, ApplicationRow, CommentRow } from "@app/shared/types/application"; import pool from "../db"; +import { error } from "console"; export async function createApplication(memberID: number, appVersion: number, app: string) { const sql = `INSERT INTO applications (member_id, app_version, app_data) VALUES (?, ?, ?);`; @@ -12,12 +13,13 @@ export async function getMemberApplication(memberID: number): Promise { const sql = `SELECT app.*, @@ -29,7 +31,9 @@ export async function getApplicationByID(appID: number): Promise return app[0]; } -export async function getApplicationList(): Promise { +export async function getApplicationList(page: number = 1, pageSize: number = 25): Promise { + const offset = (page - 1) * pageSize; + const sql = `SELECT member.name AS member_name, app.id, @@ -38,34 +42,75 @@ export async function getApplicationList(): Promise { app.app_status FROM applications AS app LEFT JOIN members AS member - ON member.id = app.member_id;` + ON member.id = app.member_id + ORDER BY app.submitted_at DESC + LIMIT ? OFFSET ?;` - const rows: ApplicationListRow[] = await pool.query(sql); + const rows: ApplicationListRow[] = await pool.query(sql, [pageSize, offset]); return rows; } -export async function approveApplication(id) { +export async function getAllMemberApplications(memberID: number): Promise { + const sql = `SELECT + app.id, + app.member_id, + app.submitted_at, + app.app_status + FROM applications AS app WHERE app.member_id = ? ORDER BY submitted_at DESC;`; + + const rows: ApplicationListRow[] = await pool.query(sql, [memberID]) + return rows; +} + + +export async function approveApplication(id: number, approver: number) { const sql = ` UPDATE applications - SET approved_at = NOW() + SET approved_at = NOW(), approved_by = ? WHERE id = ? AND approved_at IS NULL AND denied_at IS NULL `; - const result = await pool.execute(sql, id); - return result; + const result = await pool.execute(sql, [approver, id]); + if (result.affectedRows == 1) { + return + } else { + throw new Error(`"Something went wrong approving application with ID ${id}`); + } } -export async function getApplicationComments(appID: number): Promise { +export async function denyApplication(id: number, approver: number) { + const sql = ` + UPDATE applications + SET denied_at = NOW(), approved_by = ? + WHERE id = ? + AND approved_at IS NULL + AND denied_at IS NULL + `; + + const result = await pool.execute(sql, [approver, id]); + if (result.affectedRows == 1) { + return + } else { + throw new Error(`"Something went wrong denying application with ID ${id}`); + } +} + +export async function getApplicationComments(appID: number, admin: boolean = false): Promise { + const excludeAdmin = ' AND app.admin_only = false'; + + const whereClause = `WHERE app.application_id = ?${!admin ? excludeAdmin : ''}`; + return await pool.query(`SELECT app.id AS comment_id, app.post_content, app.poster_id, app.post_time, app.last_modified, + app.admin_only, member.name AS poster_name FROM application_comments AS app INNER JOIN members AS member ON member.id = app.poster_id - WHERE app.application_id = ?;`, + ${whereClause}`, [appID]); } \ No newline at end of file diff --git a/api/src/services/calendarService.d.ts b/api/src/services/calendarService.d.ts deleted file mode 100644 index 6273805..0000000 --- a/api/src/services/calendarService.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -export declare function createEvent(eventObject: any): Promise; -export declare function updateEvent(eventObject: any): Promise; -export declare function cancelEvent(eventID: any): Promise; -export declare function getShortEventsInRange(startDate: any, endDate: any): Promise; -export declare function getEventDetailed(eventID: any): Promise; -//# sourceMappingURL=calendarService.d.ts.map \ No newline at end of file diff --git a/api/src/services/calendarService.d.ts.map b/api/src/services/calendarService.d.ts.map deleted file mode 100644 index 156c9d6..0000000 --- a/api/src/services/calendarService.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"calendarService.d.ts","sourceRoot":"","sources":["calendarService.ts"],"names":[],"mappings":"AAEA,wBAAsB,WAAW,CAAC,WAAW,KAAA,iBAE5C;AAED,wBAAsB,WAAW,CAAC,WAAW,KAAA,iBAE5C;AAED,wBAAsB,WAAW,CAAC,OAAO,KAAA,iBAExC;AAED,wBAAsB,qBAAqB,CAAC,SAAS,KAAA,EAAE,OAAO,KAAA,iBAE7D;AAED,wBAAsB,gBAAgB,CAAC,OAAO,KAAA,iBAE7C"} \ No newline at end of file diff --git a/api/src/services/calendarService.js b/api/src/services/calendarService.js deleted file mode 100644 index ee8e08e..0000000 --- a/api/src/services/calendarService.js +++ /dev/null @@ -1,19 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.createEvent = createEvent; -exports.updateEvent = updateEvent; -exports.cancelEvent = cancelEvent; -exports.getShortEventsInRange = getShortEventsInRange; -exports.getEventDetailed = getEventDetailed; -const pool = require('../db'); -async function createEvent(eventObject) { -} -async function updateEvent(eventObject) { -} -async function cancelEvent(eventID) { -} -async function getShortEventsInRange(startDate, endDate) { -} -async function getEventDetailed(eventID) { -} -//# sourceMappingURL=calendarService.js.map \ No newline at end of file diff --git a/api/src/services/calendarService.js.map b/api/src/services/calendarService.js.map deleted file mode 100644 index e8a02ee..0000000 --- a/api/src/services/calendarService.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"calendarService.js","sourceRoot":"","sources":["calendarService.ts"],"names":[],"mappings":";;AAEA,kCAEC;AAED,kCAEC;AAED,kCAEC;AAED,sDAEC;AAED,4CAEC;AApBD,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;AAEtB,KAAK,UAAU,WAAW,CAAC,WAAW;AAE7C,CAAC;AAEM,KAAK,UAAU,WAAW,CAAC,WAAW;AAE7C,CAAC;AAEM,KAAK,UAAU,WAAW,CAAC,OAAO;AAEzC,CAAC;AAEM,KAAK,UAAU,qBAAqB,CAAC,SAAS,EAAE,OAAO;AAE9D,CAAC;AAEM,KAAK,UAAU,gBAAgB,CAAC,OAAO;AAE9C,CAAC"} \ No newline at end of file diff --git a/api/src/services/calendarService.ts b/api/src/services/calendarService.ts index 890889c..4862650 100644 --- a/api/src/services/calendarService.ts +++ b/api/src/services/calendarService.ts @@ -1,26 +1,12 @@ import pool from '../db'; - -export interface CalendarEvent { - id: number; - name: string; - start: Date; // DATETIME -> Date - end: Date; // DATETIME -> Date - location: string; - color: string; // 7 character hex string - description?: string | null; - creator?: number | null; // foreign key to members.id, nullable - cancelled: boolean; // TINYINT(1) -> boolean - created_at: Date; // TIMESTAMP -> Date - updated_at: Date; // TIMESTAMP -> Date -} - -export type Attendance = 'attending' | 'maybe' | 'not_attending'; +import { CalendarEventShort, CalendarSignup, CalendarEvent, CalendarAttendance } from "@app/shared/types/calendar" +import { toDateTime } from "@app/shared/utils/time" export async function createEvent(eventObject: Omit) { const sql = ` INSERT INTO calendar_events (name, start, end, location, color, description, creator) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?) `; const params = [ eventObject.name, @@ -29,7 +15,7 @@ export async function createEvent(eventObject: Omit { const sql = ` - SELECT id, name, start, end, color + SELECT id, name, start, end, color, cancelled, full_day FROM calendar_events WHERE start BETWEEN ? AND ? ORDER BY start ASC `; - return await pool.query(sql, [startDate, endDate]); + const res: CalendarEventShort[] = await pool.query(sql, [startDate, endDate]); + return res; } -export async function getEventDetails(eventID: number) { +export async function getEventDetails(eventID: number): Promise { const sql = ` SELECT e.id, @@ -101,14 +88,14 @@ export async function getEventDetails(eventID: number) { e.cancelled, e.created_at, e.updated_at, - m.id AS creator_id, + e.creator AS creator_id, m.name AS creator_name FROM calendar_events e LEFT JOIN members m ON e.creator = m.id WHERE e.id = ? `; - - return await pool.query(sql, [eventID]) + let vals: CalendarEvent[] = await pool.query(sql, [eventID]); + return vals[0]; } export async function getUpcomingEvents(date: Date, limit: number) { @@ -124,7 +111,7 @@ export async function getUpcomingEvents(date: Date, limit: number) { } -export async function setAttendanceStatus(memberID: number, eventID: number, status: Attendance) { +export async function setAttendanceStatus(memberID: number, eventID: number, status: CalendarAttendance) { const sql = ` INSERT INTO calendar_events_signups (member_id, event_id, status) VALUES (?, ?, ?) @@ -135,16 +122,9 @@ export async function setAttendanceStatus(memberID: number, eventID: number, sta return { success: true } } -export async function getEventAttendance(eventID: number) { - const sql = ` - SELECT - s.member_id, - s.status, - m.name AS member_name - FROM calendar_events_signups s - LEFT JOIN members m ON s.member_id = m.id - WHERE s.event_id = ? - `; +export async function getEventAttendance(eventID: number): Promise { - return await pool.query(sql, [eventID]); + const sql = "CALL `sp_GetCalendarEventSignups`(?)" + const res = await pool.query(sql, [eventID]); + return res[0]; } \ No newline at end of file diff --git a/api/src/services/loaService.ts b/api/src/services/loaService.ts new file mode 100644 index 0000000..c2378b9 --- /dev/null +++ b/api/src/services/loaService.ts @@ -0,0 +1,109 @@ +import { toDateTime } from "@app/shared/utils/time"; +import pool from "../db"; +import { LOARequest, LOAType } from '@app/shared/types/loa' +import { PagedData } from '@app/shared/types/pagination' + +export async function getLoaTypes(): Promise { + return await pool.query('SELECT * FROM leave_of_absences_types;'); +} + +export async function getAllLOA(page = 1, pageSize = 10): Promise> { + const offset = (page - 1) * pageSize; + + const sql = ` + SELECT loa.*, members.name, t.name AS type_name + FROM leave_of_absences AS loa + LEFT JOIN members ON loa.member_id = members.id + LEFT JOIN leave_of_absences_types AS t ON loa.type_id = t.id + ORDER BY + CASE + WHEN loa.closed IS NULL + AND NOW() > COALESCE(loa.extended_till, loa.end_date) THEN 1 + WHEN loa.closed IS NULL + AND NOW() BETWEEN loa.start_date AND COALESCE(loa.extended_till, loa.end_date) THEN 2 + WHEN loa.closed IS NULL AND NOW() < loa.start_date THEN 3 + WHEN loa.closed IS NOT NULL THEN 4 + END, + loa.start_date DESC + LIMIT ? OFFSET ?; + `; + let loaList: LOARequest[] = await pool.query(sql, [pageSize, offset]) as LOARequest[]; + + let loaCount = Number((await pool.query(`SELECT COUNT(*) as count FROM leave_of_absences;`))[0].count); + let pageCount = loaCount / pageSize; + + let output: PagedData = { data: loaList, pagination: { page: page, pageSize: pageSize, total: loaCount, totalPages: pageCount } } + return output; +} + +export async function getUserLOA(userId: number, page = 1, pageSize = 10): Promise> { + + const offset = (page - 1) * pageSize; + + const result: LOARequest[] = await pool.query(` + SELECT loa.*, members.name, t.name AS type_name + FROM leave_of_absences AS loa + LEFT JOIN members ON loa.member_id = members.id + LEFT JOIN leave_of_absences_types AS t ON loa.type_id = t.id + WHERE member_id = ? + ORDER BY + CASE + WHEN loa.closed IS NULL + AND NOW() > COALESCE(loa.extended_till, loa.end_date) THEN 1 + WHEN loa.closed IS NULL + AND NOW() BETWEEN loa.start_date AND COALESCE(loa.extended_till, loa.end_date) THEN 2 + WHEN loa.closed IS NULL AND NOW() < loa.start_date THEN 3 + WHEN loa.closed IS NOT NULL THEN 4 + END, + loa.start_date DESC + LIMIT ? OFFSET ?;`, [userId, pageSize, offset]) + + let loaCount = Number((await pool.query(`SELECT COUNT(*) as count FROM leave_of_absences WHERE member_id = ?;`, [userId]))[0].count); + let pageCount = loaCount / pageSize; + let output: PagedData = { data: result, pagination: { page: page, pageSize: pageSize, total: loaCount, totalPages: pageCount } } + return output; +} + +export async function getUserActiveLOA(userId: number): Promise { + const sql = `SELECT * + FROM leave_of_absences + WHERE member_id = ? + AND closed IS NULL + AND UTC_TIMESTAMP() BETWEEN start_date AND end_date;` + const LOAData = await pool.query(sql, [userId]); + return LOAData; +} + +export async function createNewLOA(data: LOARequest) { + const sql = `INSERT INTO leave_of_absences + (member_id, filed_date, start_date, end_date, type_id, reason) + VALUES (?, ?, ?, ?, ?, ?)`; + await pool.query(sql, [data.member_id, toDateTime(data.filed_date), toDateTime(data.start_date), toDateTime(data.end_date), data.type_id, data.reason]) + return; +} + +export async function closeLOA(id: number, closer: number) { + const sql = `UPDATE leave_of_absences + SET closed = 1, + closed_by = ?, + ended_at = NOW() + WHERE leave_of_absences.id = ?`; + let out = await pool.query(sql, [closer, id]); + return out; +} + +export async function getLOAbyID(id: number): Promise { + let res = await pool.query(`SELECT * FROM leave_of_absences WHERE id = ?`, [id]); + if (res.length != 1) + throw new Error(`LOA with id ${id} not found`); + return res[0]; +} + +export async function setLOAExtension(id: number, extendTo: Date) { + let res = await pool.query(`UPDATE leave_of_absences + SET extended_till = ? + WHERE leave_of_absences.id = ? `, [toDateTime(extendTo), id]); + if (res.affectedRows != 1) + throw new Error(`Could not extend LOA`); + return res[0]; +} \ No newline at end of file diff --git a/api/src/services/memberService.ts b/api/src/services/memberService.ts index 98045da..844ef33 100644 --- a/api/src/services/memberService.ts +++ b/api/src/services/memberService.ts @@ -1,23 +1,73 @@ import pool from "../db"; +import { Member, MemberLight, memberSettings, MemberState } from '@app/shared/types/member' -export enum MemberState { - Guest = "guest", - Applicant = "applicant", - Member = "member", - Retired = "retired", - Banned = "banned", - Denied = "denied" -} - -export async function getUserData(userID: number) { - const sql = `SELECT * FROM members WHERE id = ?`; - const res = await pool.query(sql, [userID]); - return res[0] ?? null; +export async function getUserData(userID: number): Promise { + const sql = `SELECT * FROM view_member_rank_unit_status_latest WHERE member_id = ?`; + const res: Member = await pool.query(sql, [userID]); + return res[0] ?? null; } export async function setUserState(userID: number, state: MemberState) { - const sql = `UPDATE members + const sql = `UPDATE members SET state = ? WHERE id = ?;`; - return await pool.query(sql, [state, userID]); + return await pool.query(sql, [state, userID]); +} + +export async function getUserState(user: number): Promise { + let out = await pool.query(`SELECT state FROM members WHERE id = ?`, [user]); + return (out[0].state as MemberState); +} + +export async function getMemberSettings(id: number): Promise { + const sql = `SELECT * FROM view_member_settings WHERE id = ?`; + let out: memberSettings[] = await pool.query(sql, [id]); + + if (out.length != 1) + throw new Error("Could not get user settings"); + + return out[0]; +} + +export async function setUserSettings(id: number, settings: memberSettings) { + const sql = `UPDATE view_member_settings SET + displayName = ? + WHERE id = ?;`; + let result = await pool.query(sql, [settings.displayName, id]) +} + +export async function getMembersLite(ids: number[]): Promise { + const sql = `SELECT m.member_id AS id, + m.member_name AS username, + m.displayName, + u.color + FROM view_member_rank_unit_status_latest m + LEFT JOIN units u ON u.name = m.unit + WHERE member_id IN (?);`; + const res: MemberLight[] = await pool.query(sql, [ids]); + return res; +} + +export async function getAllMembersLite(): Promise { + const sql = `SELECT m.member_id AS id, + m.member_name AS username, + m.displayName, + u.color + FROM view_member_rank_unit_status_latest m + LEFT JOIN units u ON u.name = m.unit;`; + + const res: MemberLight[] = await pool.query(sql); + return res; +} + +export async function getMembersFull(ids: number[]): Promise { + const sql = `SELECT * FROM view_member_rank_unit_status_latest WHERE member_id IN (?);`; + const res: Member[] = await pool.query(sql, [ids]); + return res; +} + +export async function mapDiscordtoID(id: number): Promise { + const sql = `SELECT id FROM members WHERE discord_id = ?;` + let res = await pool.query(sql, [id]); + return res.length > 0 ? res[0].id : null; } \ No newline at end of file diff --git a/api/src/services/rankService.ts b/api/src/services/rankService.ts index f29a8b3..ada4ea0 100644 --- a/api/src/services/rankService.ts +++ b/api/src/services/rankService.ts @@ -21,8 +21,8 @@ export async function insertMemberRank(member_id: number, rank_id: number): Prom export async function insertMemberRank(member_id: number, rank_id: number, date?: Date): Promise { const sql = date - ? `INSERT INTO members_ranks (member_id, rank_id, event_date) VALUES (?, ?, ?);` - : `INSERT INTO members_ranks (member_id, rank_id, event_date) VALUES (?, ?, NOW());`; + ? `INSERT INTO members_ranks (member_id, rank_id, start_date) VALUES (?, ?, ?);` + : `INSERT INTO members_ranks (member_id, rank_id, start_date) VALUES (?, ?, NOW());`; const params = date ? [member_id, rank_id, date] diff --git a/api/src/services/rolesService.ts b/api/src/services/rolesService.ts index 11fc3ff..f9e724c 100644 --- a/api/src/services/rolesService.ts +++ b/api/src/services/rolesService.ts @@ -1,4 +1,5 @@ import pool from '../db'; +import { Role } from '@app/shared/types/roles' export async function assignUserGroup(userID: number, roleID: number) { @@ -16,11 +17,11 @@ export async function createGroup(name: string, color: string, description: stri return { id: result.insertId, name, color, description }; } -export async function getUserRoles(userID: number) { +export async function getUserRoles(userID: number): Promise { const sql = `SELECT r.id, r.name FROM members_roles mr INNER JOIN roles r ON mr.role_id = r.id - WHERE mr.member_id = 190;`; + WHERE mr.member_id = ?;`; return await pool.query(sql, [userID]); } \ No newline at end of file diff --git a/api/src/services/statusService.ts b/api/src/services/statusService.ts index 7a62f3a..8b8fb28 100644 --- a/api/src/services/statusService.ts +++ b/api/src/services/statusService.ts @@ -1,6 +1,6 @@ import pool from "../db" export async function assignUserToStatus(userID: number, statusID: number) { - const sql = `INSERT INTO members_statuses (member_id, status_id, event_date) VALUES (?, ?, NOW())` + const sql = `INSERT INTO members_statuses (member_id, status_id, start_date) VALUES (?, ?, NOW())` await pool.execute(sql, [userID, statusID]); } diff --git a/ecosystem.config.js b/ecosystem.config.js new file mode 100644 index 0000000..aa2c2ed --- /dev/null +++ b/ecosystem.config.js @@ -0,0 +1,15 @@ +module.exports = { + apps : [{ + name: '17th-api', + cwd: 'api', + script: 'built/api/src/index.js', + watch: ['.env', 'built'], + ignore_watch: ['.gitignore', '\.json', 'src', '\.db', 'node_modules'], + appendEnvToName: true, + watch_options: { + usePolling: true, + interval: 10000 + }, + time: true + }] +}; diff --git a/shared/package-lock.json b/shared/package-lock.json new file mode 100644 index 0000000..91148cb --- /dev/null +++ b/shared/package-lock.json @@ -0,0 +1,24 @@ +{ + "name": "@app/shared", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@app/shared", + "version": "1.0.0", + "dependencies": { + "zod": "^3.25.76" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/shared/package.json b/shared/package.json index 374484e..1f2adda 100644 --- a/shared/package.json +++ b/shared/package.json @@ -2,5 +2,8 @@ "name": "@app/shared", "version": "1.0.0", "main": "index.ts", - "type": "module" -} \ No newline at end of file + "type": "module", + "dependencies": { + "zod": "^3.25.76" + } +} diff --git a/shared/schemas/calendarEventSchema.ts b/shared/schemas/calendarEventSchema.ts new file mode 100644 index 0000000..c0b4893 --- /dev/null +++ b/shared/schemas/calendarEventSchema.ts @@ -0,0 +1,33 @@ +import z from "zod"; + +const dateRe = /^\d{4}-\d{2}-\d{2}$/ // YYYY-MM-DD +const timeRe = /^(?:[01]\d|2[0-3]):[0-5]\d$/ // HH:mm (24h)\ + +export function parseLocalDateTime(dateStr: string, timeStr: string) { + // Construct a Date in the user's local timezone + const [y, m, d] = dateStr.split("-").map(Number) + const [hh, mm] = timeStr.split(":").map(Number) + return new Date(y, m - 1, d, hh, mm, 0, 0) +} + +export const calendarEventSchema = z.object({ + name: z.string().min(2, "Please enter at least 2 characters").max(100), + startDate: z.string().regex(dateRe, "Use YYYY-MM-DD"), + startTime: z.string().regex(timeRe, "Use HH:mm (24h)"), + endDate: z.string().regex(dateRe, "Use YYYY-MM-DD"), + endTime: z.string().regex(timeRe, "Use HH:mm (24h)"), + location: z.string().max(200).default(""), + color: z.string().regex(/^#([0-9A-Fa-f]{6})$/, "Use a hex color like #AABBCC"), + description: z.string().max(2000).default(""), + id: z.number().int().nonnegative().nullable().default(null), +}).superRefine((vals, ctx) => { + const start = parseLocalDateTime(vals.startDate, vals.startTime) + const end = parseLocalDateTime(vals.endDate, vals.endTime) + if (!(end > start)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "End must be after start", + path: ["endTime"], // attach to a visible field + }) + } +}) \ No newline at end of file diff --git a/shared/schemas/loaSchema.ts b/shared/schemas/loaSchema.ts new file mode 100644 index 0000000..30c94c2 --- /dev/null +++ b/shared/schemas/loaSchema.ts @@ -0,0 +1,51 @@ +import * as z from "zod"; +import { LOAType } from "../types/loa"; + +export const loaTypeSchema = z.object({ + id: z.number(), + name: z.string(), + max_length_days: z.number(), +}); + +export const loaSchema = z.object({ + member_id: z.number(), + start_date: z.date(), + end_date: z.date(), + type: loaTypeSchema, + reason: z.string(), +}) + .superRefine((data, ctx) => { + const { start_date, end_date, type } = data; + + const today = new Date(); + today.setHours(0, 0, 0, 0); + + if (start_date < today) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["start_date"], + message: "Start date cannot be in the past.", + }); + } + + // 1. end > start + if (end_date <= start_date) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["end_date"], + message: "End date must be after start date.", + }); + } + + // 2. calculate max + const maxEnd = new Date(start_date); + maxEnd.setDate(maxEnd.getDate() + type.max_length_days); + + if (end_date > maxEnd) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["end_date"], + message: `This LOA type allows a maximum of ${type.max_length_days} days.`, + }); + } + }); diff --git a/shared/schemas/trainingReportSchema.ts b/shared/schemas/trainingReportSchema.ts new file mode 100644 index 0000000..7dd7baa --- /dev/null +++ b/shared/schemas/trainingReportSchema.ts @@ -0,0 +1,60 @@ +import { z } from "zod"; + +export const courseEventAttendeeSchema = z.object({ + attendee_id: z.number({ invalid_type_error: "Must select a member" }).int().positive(), + passed_bookwork: z.boolean(), + passed_qual: z.boolean(), + remarks: z.string(), + attendee_role_id: z.number({ invalid_type_error: "Must select a role" }).int().positive() +}) + +export const trainingReportSchema = z.object({ + id: z.number().int().positive().optional(), + course_id: z.number({ invalid_type_error: "Must select a training" }).int(), + event_date: z + .string() + .refine( + (val) => !isNaN(Date.parse(val)), + "Must be a valid date" + ), + remarks: z.string().nullable().optional(), + attendees: z.array(courseEventAttendeeSchema).default([]), +}).superRefine((data, ctx) => { + const trainerRole = 1; + const traineeRole = 2; + + const hasTrainer = data.attendees.some((a) => a.attendee_role_id === trainerRole); + const hasTrainee = data.attendees.some((a) => a.attendee_role_id === traineeRole); + + if (!hasTrainer) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["attendees"], + message: "At least one Primary Trainer is required.", + }); + } + + if (!hasTrainee) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["attendees"], + message: "At least one Trainee is required.", + }); + } + + //no duplicates + const idCounts = new Map(); + + data.attendees.forEach((a, index) => { + idCounts.set(a.attendee_id, (idCounts.get(a.attendee_id) ?? 0) + 1); + + if (idCounts.get(a.attendee_id)! > 1) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["attendees"], + message: "Cannot have duplicate attendee.", + }); + } + }) +}) + diff --git a/shared/tsconfig.json b/shared/tsconfig.json new file mode 100644 index 0000000..3f94d22 --- /dev/null +++ b/shared/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "dist", + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "esModuleInterop": true, + "isolatedModules": true, + "resolveJsonModule": true + }, + "include": ["./**/*.ts"] +} diff --git a/shared/types/application.ts b/shared/types/application.ts index f8b648d..12b0db5 100644 --- a/shared/types/application.ts +++ b/shared/types/application.ts @@ -40,6 +40,7 @@ export interface CommentRow { post_time: string; last_modified: string | null; poster_name: string; + admin_only: boolean; } export interface ApplicationFull { diff --git a/shared/types/calendar.ts b/shared/types/calendar.ts new file mode 100644 index 0000000..6f31a03 --- /dev/null +++ b/shared/types/calendar.ts @@ -0,0 +1,40 @@ +export interface CalendarEvent { + id?: number; + name: string; + start: Date; + end: Date; + location: string; + color: string; + description: string; + creator_id?: number; + cancelled?: boolean; + created_at?: Date; + updated_at?: Date; + + creator_name?: string | null; + eventSignups?: CalendarSignup[] | null; +} + +export enum CalendarAttendance { + Attending = "attending", + NotAttending = "not_attending", + Maybe = "maybe" +} + +export interface CalendarSignup { + member_id: number; + eventID: number; + status: CalendarAttendance; + member_name?: string; + unit_name?: string; +} + +export interface CalendarEventShort { + id: number; + name: string; + start: Date; + end: Date; + color: string; + cancelled: boolean; + full_day: boolean; +} \ No newline at end of file diff --git a/shared/types/course.ts b/shared/types/course.ts new file mode 100644 index 0000000..e615db8 --- /dev/null +++ b/shared/types/course.ts @@ -0,0 +1,91 @@ +export interface Course { + id: number; + name: string; + short_name: string; + category: string; + description?: string | null; + image_url?: string | null; + created_at: Date; + updated_at: Date; + deleted?: number | boolean; + prereq_id?: number | null; + hasBookwork: boolean; + hasQual: boolean; +} + +export interface CourseEventDetails { + id: number | null; // PK + course_id: number | null; // FK → courses.id + event_type: number | null; // FK → event_types.id + event_date: Date; // datetime (not nullable) + + guilded_event_id: number | null; + + created_at: Date; // datetime + updated_at: Date; // datetime + + deleted: boolean | null; // tinyint(4), nullable + report_url: string | null; // varchar(2048) + remarks: string | null; // text + + attendees: CourseAttendee[] | null; + + created_by: number | null; + created_by_name: string | null; + course_name: string | null; + course: Course | null; +} + + +export interface CourseAttendee { + passed_bookwork: boolean; // tinyint(1) + passed_qual: boolean; // tinyint(1) + attendee_id: number; // PK + course_event_id: number; // PK + attendee_role_id: number | null; + role: CourseAttendeeRole | null; + created_at: Date; // datetime → ISO string + updated_at: Date; // datetime → ISO string + remarks: string | null; + + attendee_name: string | null; +} + +export interface CourseAttendeeRole { + id: number; // PK, auto-increment + name: string | null; // varchar(50), unique, nullable + description: string | null; // text + created_at: Date | null; // datetime (nullable) + updated_at: Date | null; // datetime (nullable) + deleted: boolean; // tinyint(4) +} + +export interface RawAttendeeRow { + passed_bookwork: number; // tinyint(1) + passed_qual: number; // tinyint(1) + attendee_id: number; + course_event_id: number; + attendee_role_id: number | null; + created_at: string; + updated_at: string; + remarks: string | null; + + role_id: number | null; + role_name: string | null; + role_description: string | null; + role_deleted: number | null; + role_created_at: string | null; + role_updated_at: string | null; + + attendee_name: string | null; +} + +export interface CourseEventSummary { + event_id: number; + course_id: number; + course_name: string; + course_shortname: string; + date: string; + created_by: number; + created_by_name: string; +} \ No newline at end of file diff --git a/shared/types/loa.ts b/shared/types/loa.ts new file mode 100644 index 0000000..4eae0dc --- /dev/null +++ b/shared/types/loa.ts @@ -0,0 +1,24 @@ +export interface LOARequest { + id?: number; + member_id?: number; + filed_date?: Date; // ISO 8601 string + start_date: Date; // ISO 8601 string + end_date: Date; // ISO 8601 string + extended_till?: Date; + type_id?: number; + reason?: string; + expired?: boolean; + closed?: boolean; + closed_by?: number; + created_by?: number; + + name?: string; //member name + type_name?: string; +}; + +export interface LOAType { + id: number; + name: string; + max_length_days: number; + extendable: boolean; +} \ No newline at end of file diff --git a/shared/types/member.ts b/shared/types/member.ts new file mode 100644 index 0000000..7caa9f0 --- /dev/null +++ b/shared/types/member.ts @@ -0,0 +1,42 @@ +import { LOARequest } from "./loa"; +import { Role } from "./roles"; + +export interface memberSettings { + displayName: string; +} + +export enum MemberState { + Guest = "guest", + Applicant = "applicant", + Member = "member", + Retired = "retired", + Banned = "banned", + Denied = "denied" +} + +export type Member = { + member_id: number; + member_name: string; + displayName?: string; + rank: string | null; + rank_date: string | null; + unit: string | null; + unit_date: string | null; + status: string | null; + status_date: string | null; + loa_until?: Date; +}; + +export interface MemberLight { + id: number + displayName: string + username: string + color: string +} + +export interface myData { + member: Member; + LOAs: LOARequest[]; + roles: Role[]; + state: MemberState; +} \ No newline at end of file diff --git a/shared/types/pagination.ts b/shared/types/pagination.ts new file mode 100644 index 0000000..4e6b563 --- /dev/null +++ b/shared/types/pagination.ts @@ -0,0 +1,11 @@ +export interface PagedData { + data: T[] + pagination: pagination +} + +export interface pagination { + page: number + pageSize: number + total: number + totalPages: number +} \ No newline at end of file diff --git a/shared/types/roles.ts b/shared/types/roles.ts new file mode 100644 index 0000000..a232c52 --- /dev/null +++ b/shared/types/roles.ts @@ -0,0 +1,6 @@ +export interface Role { + id: number; + name: string; + color?: string; + description?: string; +} \ No newline at end of file diff --git a/shared/utils/time.ts b/shared/utils/time.ts new file mode 100644 index 0000000..e1b5ea8 --- /dev/null +++ b/shared/utils/time.ts @@ -0,0 +1,14 @@ +export function toDateTime(date: Date): string { + if (typeof date === 'string') { + date = new Date(date); + } + // This produces a CST-local time because server runs in CST + const year = date.getFullYear(); + const month = (date.getMonth() + 1).toString().padStart(2, "0"); + const day = date.getDate().toString().padStart(2, "0"); + const hour = date.getHours().toString().padStart(2, "0"); + const minute = date.getMinutes().toString().padStart(2, "0"); + const second = date.getSeconds().toString().padStart(2, "0"); + + return `${year}-${month}-${day} ${hour}:${minute}:${second}`; +} diff --git a/ui/.env.example b/ui/.env.example new file mode 100644 index 0000000..9a0998f --- /dev/null +++ b/ui/.env.example @@ -0,0 +1,10 @@ +# SITE SETTINGS +VITE_APIHOST= +VITE_DOCHOST= # https://bookstack.whatever.com/api +VITE_ENVIRONMENT= # dev / prod +VITE_APPLICATION_VERSION= # Should match release tag + + +# Glitchtip +VITE_GLITCHTIP_DSN= +VITE_DISABLE_GLITCHTIP= # true/false \ No newline at end of file diff --git a/ui/index.html b/ui/index.html index b19040a..b9cc2ea 100644 --- a/ui/index.html +++ b/ui/index.html @@ -4,7 +4,7 @@ - Vite App + 17th Ranger Battalion
diff --git a/ui/package-lock.json b/ui/package-lock.json index da4cbea..25a2e81 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -13,6 +13,7 @@ "@fullcalendar/interaction": "^6.1.19", "@fullcalendar/timegrid": "^6.1.19", "@fullcalendar/vue3": "^6.1.19", + "@sentry/vue": "^10.27.0", "@tailwindcss/vite": "^4.1.11", "@tanstack/vue-table": "^8.21.3", "@vee-validate/zod": "^4.15.1", @@ -21,7 +22,7 @@ "clsx": "^2.1.1", "lucide-vue-next": "^0.539.0", "pinia": "^3.0.3", - "reka-ui": "^2.5.0", + "reka-ui": "^2.6.1", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.11", "tw-animate-css": "^1.3.6", @@ -1392,6 +1393,103 @@ "dev": true, "license": "MIT" }, + "node_modules/@sentry-internal/browser-utils": { + "version": "10.27.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.27.0.tgz", + "integrity": "sha512-17tO6AXP+rmVQtLJ3ROQJF2UlFmvMWp7/8RDT5x9VM0w0tY31z8Twc0gw2KA7tcDxa5AaHDUbf9heOf+R6G6ow==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.27.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/feedback": { + "version": "10.27.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.27.0.tgz", + "integrity": "sha512-UecsIDJcv7VBwycge/MDvgSRxzevDdcItE1i0KSwlPz00rVVxLY9kV28PJ4I2E7r6/cIaP9BkbWegCEcv09NuA==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.27.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/replay": { + "version": "10.27.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.27.0.tgz", + "integrity": "sha512-tKSzHq1hNzB619Ssrqo25cqdQJ84R3xSSLsUWEnkGO/wcXJvpZy94gwdoS+KmH18BB1iRRRGtnMxZcUkiPSesw==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "10.27.0", + "@sentry/core": "10.27.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/replay-canvas": { + "version": "10.27.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.27.0.tgz", + "integrity": "sha512-inhsRYSVBpu3BI1kZphXj6uB59baJpYdyHeIPCiTfdFNBE5tngNH0HS/aedZ1g9zICw290lwvpuyrWJqp4VBng==", + "license": "MIT", + "dependencies": { + "@sentry-internal/replay": "10.27.0", + "@sentry/core": "10.27.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/browser": { + "version": "10.27.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.27.0.tgz", + "integrity": "sha512-G8q362DdKp9y1b5qkQEmhTFzyWTOVB0ps1rflok0N6bVA75IEmSDX1pqJsNuY3qy14VsVHYVwQBJQsNltQLS0g==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "10.27.0", + "@sentry-internal/feedback": "10.27.0", + "@sentry-internal/replay": "10.27.0", + "@sentry-internal/replay-canvas": "10.27.0", + "@sentry/core": "10.27.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/core": { + "version": "10.27.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.27.0.tgz", + "integrity": "sha512-Zc68kdH7tWTDtDbV1zWIbo3Jv0fHAU2NsF5aD2qamypKgfSIMSbWVxd22qZyDBkaX8gWIPm/0Sgx6aRXRBXrYQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/vue": { + "version": "10.27.0", + "resolved": "https://registry.npmjs.org/@sentry/vue/-/vue-10.27.0.tgz", + "integrity": "sha512-vQVxnw59jRe5WsdB9ad/WpMPQ93QXE6Y0JEy01xIRcDlQ1pXp5wuxLkKGuTfvjdQzVUGIBLr0CgIqRAmPRymVg==", + "license": "MIT", + "dependencies": { + "@sentry/browser": "10.27.0", + "@sentry/core": "10.27.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "pinia": "2.x || 3.x", + "vue": "2.x || 3.x" + }, + "peerDependenciesMeta": { + "pinia": { + "optional": true + } + } + }, "node_modules/@sindresorhus/merge-streams": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", @@ -3235,9 +3333,9 @@ } }, "node_modules/reka-ui": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.5.0.tgz", - "integrity": "sha512-81aMAmJeVCy2k0E6x7n1kypDY6aM1ldLis5+zcdV1/JtoAlSDck5OBsyLRJU9CfgbrQp1ImnRnBSmC4fZ2fkZQ==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.6.1.tgz", + "integrity": "sha512-XK7cJDQoNuGXfCNzBBo/81Yg/OgjPwvbabnlzXG2VsdSgNsT6iIkuPBPr+C0Shs+3bb0x0lbPvgQAhMSCKm5Ww==", "license": "MIT", "dependencies": { "@floating-ui/dom": "^1.6.13", diff --git a/ui/package.json b/ui/package.json index 9771d5d..9110023 100644 --- a/ui/package.json +++ b/ui/package.json @@ -17,6 +17,7 @@ "@fullcalendar/interaction": "^6.1.19", "@fullcalendar/timegrid": "^6.1.19", "@fullcalendar/vue3": "^6.1.19", + "@sentry/vue": "^10.27.0", "@tailwindcss/vite": "^4.1.11", "@tanstack/vue-table": "^8.21.3", "@vee-validate/zod": "^4.15.1", @@ -25,7 +26,7 @@ "clsx": "^2.1.1", "lucide-vue-next": "^0.539.0", "pinia": "^3.0.3", - "reka-ui": "^2.5.0", + "reka-ui": "^2.6.1", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.11", "tw-animate-css": "^1.3.6", diff --git a/ui/public/17RBN_Logo.png b/ui/public/17RBN_Logo.png new file mode 100644 index 0000000..dd2b920 Binary files /dev/null and b/ui/public/17RBN_Logo.png differ diff --git a/ui/public/bg.jpg b/ui/public/bg.jpg new file mode 100644 index 0000000..25c6ace Binary files /dev/null and b/ui/public/bg.jpg differ diff --git a/ui/public/favicon.ico b/ui/public/favicon.ico index df36fcf..6d2de51 100644 Binary files a/ui/public/favicon.ico and b/ui/public/favicon.ico differ diff --git a/ui/src/App.vue b/ui/src/App.vue index af9f817..d27efcd 100644 --- a/ui/src/App.vue +++ b/ui/src/App.vue @@ -1,39 +1,14 @@ diff --git a/ui/src/api/application.ts b/ui/src/api/application.ts index 7ef6086..e6d41bf 100644 --- a/ui/src/api/application.ts +++ b/ui/src/api/application.ts @@ -1,80 +1,11 @@ -export type ApplicationDto = Partial<{ - age: number | string - name: string - playtime: number | string - hobbies: string - military: boolean - communities: string - joinReason: string - milsimAttraction: string - referral: string - steamProfile: string - timezone: string - canAttendSaturday: boolean - interests: string - aknowledgeRules: boolean -}> - -export interface ApplicationData { - dob: string; - name: string; - playtime: number; - hobbies: string; - military: boolean; - communities: string; - joinReason: string; - milsimAttraction: string; - referral: string; - steamProfile: string; - timezone: string; - canAttendSaturday: boolean; - interests: string; - aknowledgeRules: boolean; -} - -//reflects how applications are stored in the database -export interface ApplicationRow { - id: number; - member_id: number; - app_version: number; - app_data: ApplicationData; - - submitted_at: string; // ISO datetime from DB (e.g., "2025-08-25T18:04:29.000Z") - updated_at: string | null; - approved_at: string | null; - denied_at: string | null; - - app_status: ApplicationStatus; // generated column - decision_at: string | null; // generated column - - // present when you join members (e.g., SELECT a.*, m.name AS member_name) - member_name: string; -} -export interface CommentRow { - comment_id: number; - post_content: string; - poster_id: number; - post_time: string; - last_modified: string | null; - poster_name: string; -} - -export interface ApplicationFull { - application: ApplicationRow; - comments: CommentRow[]; -} +import { ApplicationFull } from "@shared/types/application"; -export enum ApplicationStatus { - Pending = "Pending", - Accepted = "Accepted", - Denied = "Denied", -} // @ts-ignore const addr = import.meta.env.VITE_APIHOST; -export async function loadApplication(id: number | string): Promise { - const res = await fetch(`${addr}/application/${id}`) +export async function loadApplication(id: number | string, asAdmin: boolean = false): Promise { + const res = await fetch(`${addr}/application/${id}?admin=${asAdmin}`, { credentials: 'include' }) if (res.status === 204) return null if (!res.ok) throw new Error('Failed to load application') const json = await res.json() @@ -104,6 +35,22 @@ export async function postChatMessage(message: any, post_id: number) { const response = await fetch(`${addr}/application/${post_id}/comment`, { method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(out), + }) + + return await response.json(); +} + +export async function postAdminChatMessage(message: any, post_id: number) { + const out = { + message: message + } + + const response = await fetch(`${addr}/application/${post_id}/adminComment`, { + method: 'POST', + credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(out), }) @@ -112,7 +59,9 @@ export async function postChatMessage(message: any, post_id: number) { } export async function getAllApplications(): Promise { - const res = await fetch(`${addr}/application/all`) + const res = await fetch(`${addr}/application/all`, { + credentials: 'include', + }) if (res.ok) { return res.json() @@ -121,18 +70,67 @@ export async function getAllApplications(): Promise { } } -export async function approveApplication(id: Number) { - const res = await fetch(`${addr}/application/approve/${id}`, { method: 'POST' }) +export async function loadMyApplications(): Promise { + const res = await fetch(`${addr}/application/meList`, { credentials: 'include' }) - if (!res.ok) { + if (res.ok) { + return res.json() + } else { console.error("Something went wrong approving the application") } } -export async function denyApplication(id: Number) { - const res = await fetch(`${addr}/application/deny/${id}`, { method: 'POST' }) +export async function getMyApplication(id: number): Promise { + const res = await fetch(`${addr}/application/me/${id}`, { credentials: 'include' }) + if (res.status === 204) return null + if (res.status === 403) throw new Error("Unauthorized"); + if (!res.ok) throw new Error('Failed to load application') + const json = await res.json() + // Accept either the object at root or under `application` + return json; +} + +export async function approveApplication(id: Number) { + const res = await fetch(`${addr}/application/approve/${id}`, { method: 'POST', credentials: 'include' }) if (!res.ok) { - console.error("Something went wrong denying the application") + throw new Error("Something went wrong approving the application"); + } + return; +} + +export async function denyApplication(id: Number) { + const res = await fetch(`${addr}/application/deny/${id}`, { method: 'POST', credentials: 'include' }) + + if (!res.ok) { + throw new Error("Something went wrong denyting the application"); + } + return; +} + +export async function restartApplication() { + const res = await fetch(`${addr}/application/restart`, { + method: 'POST', + credentials: 'include' + }) + + if (!res.ok) { + console.error("Something went wrong restarting your application") + } +} + +export async function getCoC(): Promise { + const res = await fetch(`${addr}/application/coc`, { + method: "GET", + credentials: 'include', + }); + if (res.ok) { + const out = res.json(); + if (!out) { + return null; + } + return out; + } else { + return null; } } \ No newline at end of file diff --git a/ui/src/api/calendar.ts b/ui/src/api/calendar.ts index d4c01ac..58a4b1a 100644 --- a/ui/src/api/calendar.ts +++ b/ui/src/api/calendar.ts @@ -1,13 +1,13 @@ -export interface CalendarEvent { - name: string, - start: Date, - end: Date, - location: string, - color: string, - description: string, - creator: any | null, // user object - id: number | null -} +// export interface CalendarEvent { +// name: string, +// start: Date, +// end: Date, +// location: string, +// color: string, +// description: string, +// creator: any | null, // user object +// id: number | null +// } export enum CalendarAttendance { Attending = "attending", @@ -21,22 +21,106 @@ export interface CalendarSignup { state: CalendarAttendance } -export async function createCalendarEvent(eventData: CalendarEvent) { +import { CalendarEventShort, CalendarEvent } from "@shared/types/calendar"; +//@ts-ignore +const addr = import.meta.env.VITE_APIHOST; + +export async function getMonthCalendarEvents(viewedMonth: Date): Promise { + + const year = viewedMonth.getFullYear(); + const month = viewedMonth.getMonth(); + + // Base range: first and last day of the month + const firstOfMonth = new Date(year, month, 1); + const lastOfMonth = new Date(year, month + 1, 0); + + // --- Apply 10 day padding --- + const start = new Date(firstOfMonth); + start.setDate(start.getDate() - 10); + + const end = new Date(lastOfMonth); + end.setDate(end.getDate() + 10); + end.setHours(23, 59, 59, 999); + + const from = start.toISOString(); + const to = end.toISOString(); + + const url = `${addr}/calendar?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`; + + const res = await fetch(url); + + if (!res.ok) { + throw new Error(`Failed to fetch events: ${res.status} ${res.statusText}`); + } + + return res.json(); +} + +export async function getCalendarEvent(id: number): Promise { + let res = await fetch(`${addr}/calendar/${id}`); + + if (res.ok) { + return await res.json(); + } else { + throw new Error(`Failed to fetch event: ${res.status} ${res.statusText}`); + } +} + +export async function createCalendarEvent(eventData: CalendarEvent) { + let res = await fetch(`${addr}/calendar`, { + method: "POST", + credentials: "include", + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(eventData) + }); + + if (res.ok) { + return; + } else { + throw new Error(`Failed to set attendance: ${res.status} ${res.statusText}`); + } } export async function editCalendarEvent(eventData: CalendarEvent) { + let res = await fetch(`${addr}/calendar`, { + method: "PUT", + credentials: "include", + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(eventData) + }); + if (res.ok) { + return; + } else { + throw new Error(`Failed to set attendance: ${res.status} ${res.statusText}`); + } } -export async function cancelCalendarEvent(eventID: number) { +export async function setCancelCalendarEvent(eventID: number, cancel: boolean) { + let route = cancel ? "cancel" : "uncancel"; -} + let res = await fetch(`${addr}/calendar/${eventID}/${route}`, { + method: "POST", + credentials: "include" + }); -export async function adminCancelCalendarEvent(eventID: number) { - + if (res.ok) { + return; + } else { + throw new Error(`Failed to set attendance: ${res.status} ${res.statusText}`); + } } export async function setCalendarEventAttendance(eventID: number, state: CalendarAttendance) { + let res = await fetch(`${addr}/calendar/${eventID}/attendance?state=${state}`, { + method: "POST", + credentials: "include", + }); + if (res.ok) { + return; + } else { + throw new Error(`Failed to set attendance: ${res.status} ${res.statusText}`); + } } \ No newline at end of file diff --git a/ui/src/api/docs.ts b/ui/src/api/docs.ts new file mode 100644 index 0000000..24c31c8 --- /dev/null +++ b/ui/src/api/docs.ts @@ -0,0 +1,18 @@ +// @ts-ignore +const addr = import.meta.env.VITE_APIHOST; + +export async function getWelcomeMessage(): Promise { + const res = await fetch(`${addr}/docs/welcome`, { + method: "GET", + credentials: 'include', + }); + if (res.ok) { + const out = res.json(); + if (!out) { + return null; + } + return out; + } else { + return null; + } +} \ No newline at end of file diff --git a/ui/src/api/loa.ts b/ui/src/api/loa.ts index 6f9314b..ee2f82e 100644 --- a/ui/src/api/loa.ts +++ b/ui/src/api/loa.ts @@ -1,12 +1,5 @@ -export type LOARequest = { - id?: number; - name?: string; - member_id: number; - filed_date: string; // ISO 8601 string - start_date: string; // ISO 8601 string - end_date: string; // ISO 8601 string - reason?: string; -}; +import { LOARequest, LOAType } from '@shared/types/loa' +import { PagedData } from '@shared/types/pagination' // @ts-ignore const addr = import.meta.env.VITE_APIHOST; @@ -17,21 +10,41 @@ export async function submitLOA(request: LOARequest): Promise<{ id?: number; err "Content-Type": "application/json", }, body: JSON.stringify(request), + credentials: 'include', }); if (res.ok) { - return res.json(); + return; } else { - return { error: "Failed to submit LOA" }; + throw new Error("Failed to submit LOA"); } } +export async function adminSubmitLOA(request: LOARequest): Promise<{ id?: number; error?: string }> { + const res = await fetch(`${addr}/loa/admin`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(request), + credentials: 'include', + }); + + if (res.ok) { + return + } else { + throw new Error("Failed to submit LOA"); + } +} + + export async function getMyLOA(): Promise { const res = await fetch(`${addr}/loa/me`, { method: "GET", headers: { "Content-Type": "application/json", }, + credentials: 'include', }); @@ -46,12 +59,49 @@ export async function getMyLOA(): Promise { } } -export function getAllLOAs(): Promise { - return fetch(`${addr}/loa/all`, { +export async function getAllLOAs(page?: number, pageSize?: number): Promise> { + const params = new URLSearchParams(); + + if (page !== undefined) { + params.set("page", page.toString()); + } + + if (pageSize !== undefined) { + params.set("pageSize", pageSize.toString()); + } + + return fetch(`${addr}/loa/all?${params}`, { method: "GET", headers: { "Content-Type": "application/json", }, + credentials: 'include', + }).then((res) => { + if (res.ok) { + return res.json(); + } else { + return []; + } + }); +} + +export function getMyLOAs(page?: number, pageSize?: number): Promise> { + const params = new URLSearchParams(); + + if (page !== undefined) { + params.set("page", page.toString()); + } + + if (pageSize !== undefined) { + params.set("pageSize", pageSize.toString()); + } + + return fetch(`${addr}/loa/history?${params}`, { + method: "GET", + credentials: 'include', + headers: { + "Content-Type": "application/json", + }, }).then((res) => { if (res.ok) { return res.json(); @@ -59,4 +109,69 @@ export function getAllLOAs(): Promise { return []; } }); + } + +export async function getLoaTypes(): Promise { + const res = await fetch(`${addr}/loa/types`, { + method: "GET", + credentials: 'include', + }); + + if (res.ok) { + const out = res.json(); + if (!out) { + return null; + } + return out; + } else { + return null; + } +}; + +export async function getLoaPolicy(): Promise { + const res = await fetch(`${addr}/loa/policy`, { + method: "GET", + credentials: 'include', + }); + if (res.ok) { + const out = res.json(); + if (!out) { + return null; + } + return out; + } else { + return null; + } +} + +export async function cancelLOA(id: number, admin: boolean = false) { + let route = admin ? 'adminCancel' : 'cancel'; + const res = await fetch(`${addr}/loa/${route}/${id}`, { + method: "POST", + credentials: 'include', + }); + + if (res.ok) { + return + } else { + throw new Error("Could not cancel LOA"); + } +} + +export async function extendLOA(id: number, to: Date) { + const res = await fetch(`${addr}/loa/extend/${id}`, { + method: "POST", + credentials: 'include', + body: JSON.stringify({ to }), + headers: { + "Content-Type": "application/json", + } + }); + + if (res.ok) { + return + } else { + throw new Error("Could not extend LOA"); + } +} \ No newline at end of file diff --git a/ui/src/api/member.ts b/ui/src/api/member.ts index b97e7ac..327a5ce 100644 --- a/ui/src/api/member.ts +++ b/ui/src/api/member.ts @@ -1,12 +1,4 @@ -export type Member = { - member_id: number; - member_name: string; - rank: string | null; - rank_date: string | null; - status: string | null; - status_date: string | null; - on_loa: boolean | null; -}; +import { memberSettings, Member, MemberLight } from "@shared/types/member"; // @ts-ignore const addr = import.meta.env.VITE_APIHOST; @@ -19,4 +11,80 @@ export async function getMembers(): Promise { throw new Error("Failed to fetch members"); } return response.json(); +} + +export async function getMemberSettings(): Promise { + const response = await fetch(`${addr}/members/settings`, { + credentials: 'include' + }); + if (!response.ok) { + throw new Error("Failed to fetch settings"); + } + return response.json(); +} + +export async function setMemberSettings(settings: memberSettings) { + const response = await fetch(`${addr}/members/settings`, { + credentials: 'include', + method: 'PUT', + headers: { + 'Content-Type': 'Application/json', + }, + body: JSON.stringify(settings) + }); + if (!response.ok) { + throw new Error("Failed to fetch settings"); + } + return; +} + +export async function getAllLightMembers(): Promise { + const response = await fetch(`${addr}/members/lite`, { + credentials: 'include', + headers: { + "Content-Type": "application/json", + } + }); + + if (!response.ok) { + throw new Error("Failed to fetch light members"); + } + return response.json(); +} + +export async function getLightMembers(ids: number[]): Promise { + + if (ids.length === 0) return []; + + const response = await fetch(`${addr}/members/lite/bulk`, { + credentials: 'include', + method: 'POST', + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ ids }) + }); + + if (!response.ok) { + throw new Error("Failed to fetch light members"); + } + return response.json(); +} + +export async function getFullMembers(ids: number[]): Promise { + + if (ids.length === 0) return []; + + const response = await fetch(`${addr}/members/full/bulk`, { + credentials: 'include', + method: 'POST', + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ ids }) + }); + if (!response.ok) { + throw new Error("Failed to fetch settings"); + } + return response.json(); } \ No newline at end of file diff --git a/ui/src/api/roles.ts b/ui/src/api/roles.ts index c128dbf..3fb67ee 100644 --- a/ui/src/api/roles.ts +++ b/ui/src/api/roles.ts @@ -10,7 +10,9 @@ export type Role = { const addr = import.meta.env.VITE_APIHOST; export async function getRoles(): Promise { - const res = await fetch(`${addr}/roles`) + const res = await fetch(`${addr}/roles`, { + credentials: 'include', + }) if (res.ok) { return res.json() as Promise; @@ -26,11 +28,12 @@ export async function createRole(name: string, color: string, description: strin headers: { "Content-Type": "application/json" }, + credentials: 'include', body: JSON.stringify({ name, color, description - }) + }), }); if (res.ok) { @@ -47,6 +50,7 @@ export async function addMemberToRole(member_id: number, role_id: number): Promi headers: { "Content-Type": "application/json" }, + credentials: 'include', body: JSON.stringify({ member_id, role_id @@ -64,6 +68,7 @@ export async function addMemberToRole(member_id: number, role_id: number): Promi export async function removeMemberFromRole(member_id: number, role_id: number): Promise { const res = await fetch(`${addr}/memberRoles`, { method: "DELETE", + credentials: 'include', headers: { "Content-Type": "application/json" }, @@ -83,7 +88,8 @@ export async function removeMemberFromRole(member_id: number, role_id: number): export async function deleteRole(role_id: number): Promise { const res = await fetch(`${addr}/roles/${role_id}`, { - method: "DELETE" + method: "DELETE", + credentials: 'include', }); if (res.ok) { diff --git a/ui/src/api/trainingReport.ts b/ui/src/api/trainingReport.ts new file mode 100644 index 0000000..a261b7b --- /dev/null +++ b/ui/src/api/trainingReport.ts @@ -0,0 +1,93 @@ +import { Course, CourseAttendeeRole, CourseEventDetails, CourseEventSummary } from '@shared/types/course' +import { PagedData } from '@shared/types/pagination'; + +//@ts-ignore +const addr = import.meta.env.VITE_APIHOST; + +export async function getTrainingReports(sortMode?: string, search?: string, page?: number, pageSize?: number): Promise> { + const params = new URLSearchParams(); + + if (page !== undefined) { + params.set("page", page.toString()); + } + + if (pageSize !== undefined) { + params.set("pageSize", pageSize.toString()); + } + + if (sortMode !== undefined) { + params.set("sort", sortMode.toString()); + } + + if (search !== undefined || search !== "") { + params.set("search", search.toString()); + } + + const res = await fetch(`${addr}/courseEvent?${params}`, { + credentials: 'include', + }); + + if (res.ok) { + return await res.json() as Promise>; + } else { + console.error("Something went wrong"); + throw new Error("Failed to load training reports"); + } +} + +export async function getTrainingReport(id: number): Promise { + const res = await fetch(`${addr}/courseEvent/${id}`, { + credentials: 'include', + }); + + if (res.ok) { + return await res.json() as Promise; + } else { + console.error("Something went wrong"); + throw new Error("Failed to load training reports"); + } +} + +export async function getAllTrainings(): Promise { + const res = await fetch(`${addr}/course`, { + credentials: 'include', + }); + + if (res.ok) { + return await res.json() as Promise; + } else { + console.error("Something went wrong"); + throw new Error("Failed to load training list"); + } +} + +export async function getAllAttendeeRoles(): Promise { + const res = await fetch(`${addr}/course/roles`, { + credentials: 'include', + }); + + if (res.ok) { + return await res.json() as Promise; + } else { + console.error("Something went wrong"); + throw new Error("Failed to load attendee roles"); + } +} + +export async function postTrainingReport(report: CourseEventDetails) { + const res = await fetch(`${addr}/courseEvent`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(report), + credentials: 'include', + }); + + if (!res.ok) { + const errorText = await res.text(); + throw new Error(`Failed to post training report: ${res.status} ${errorText}`); + } + + return res.json(); // expected to return the inserted report or new ID +} diff --git a/ui/src/assets/base.css b/ui/src/assets/base.css index 3f67ca8..b31c2d0 100644 --- a/ui/src/assets/base.css +++ b/ui/src/assets/base.css @@ -4,7 +4,7 @@ @custom-variant dark (&:is(.dark *)); :root { - --background: oklch(0.2046 0 0); + --background: oklch(19.125% 0.00002 271.152); --foreground: oklch(0.9219 0 0); --card: oklch(23.075% 0.00003 271.152); --card-foreground: oklch(0.9219 0 0); @@ -16,8 +16,8 @@ --secondary-foreground: oklch(0.9219 0 0); --muted: oklch(0.2686 0 0); --muted-foreground: oklch(0.7155 0 0); - --accent: oklch(0.4732 0.1247 46.2007); - --accent-foreground: oklch(0.9243 0.1151 95.7459); + --accent: oklch(100% 0.00011 271.152 / 0.253); + --accent-foreground: oklch(100% 0.00011 271.152); --destructive: oklch(0.6368 0.2078 25.3313); --destructive-foreground: oklch(1.0000 0 0); --success: oklch(66.104% 0.16937 144.153); @@ -52,7 +52,7 @@ --shadow-2xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.35); } -.dark { +/* .dark { --background: oklch(0.2046 0 0); --foreground: oklch(0.9219 0 0); --card: oklch(23.075% 0.00003 271.152); @@ -99,7 +99,7 @@ --shadow-lg: 0px 4px 8px -1px hsl(0 0% 0% / 0.14), 0px 4px 6px -2px hsl(0 0% 0% / 0.14); --shadow-xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.14), 0px 8px 10px -2px hsl(0 0% 0% / 0.14); --shadow-2xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.35); -} +} */ @theme inline { @@ -165,4 +165,112 @@ body { @apply bg-background text-foreground; } +} + +/* Root container */ +.bookstack-container { + font-family: var(--font-sans, system-ui), sans-serif; + color: var(--foreground); + line-height: 1.45; + max-width: 760px; + margin: 0 auto; + font-size: 0.9rem; +} + +/* Headers */ +.bookstack-container h4 { + margin: 0.9rem 0 0.4rem 0; + font-weight: 600; + line-height: 1.35; + font-size: 1.05rem; + color: var(--foreground); +} + +.bookstack-container h5 { + margin: 0.9rem 0 0.4rem 0; + font-weight: 600; + line-height: 1.35; + font-size: 0.95rem; + color: var(--foreground); +} + +/* Lists */ +.bookstack-container ul { + list-style-type: disc; + margin-left: 1.1rem; + margin-bottom: 0.6rem; + padding-left: 0.6rem; + color: var(--muted-foreground); +} + +/* Nested lists */ +.bookstack-container ul ul { + list-style-type: circle; + margin-left: 0.9rem; +} + +/* List items */ +.bookstack-container li { + margin: 0.15rem 0; + padding-left: 0.1rem; + color: var(--muted-foreground); +} + +/* Bullet color */ +.bookstack-container li::marker { + color: var(--muted-foreground); +} + +/* Inline elements */ +.bookstack-container li p, +.bookstack-container li span, +.bookstack-container p { + display: inline; + margin: 0; + padding: 0; + color: var(--muted-foreground); +} + +/* Top-level spacing */ +.bookstack-container>ul>li { + margin-top: 0.3rem; +} + +/* links */ +.bookstack-container a { + color: var(--color-primary); + margin-top: 0.3rem; +} + +.bookstack-container a:hover { + text-decoration: underline; +} + +/* Scrollbar stuff */ +/* Firefox */ +.scrollbar-themed { + scrollbar-width: thin; + scrollbar-color: #555 #1f1f1f; + padding-right: 6px; +} + +/* Chrome, Edge, Safari */ +.scrollbar-themed::-webkit-scrollbar { + width: 10px; + /* slightly wider to allow padding look */ +} + +.scrollbar-themed::-webkit-scrollbar-track { + background: #1f1f1f; + margin-left: 6px; + /* ❗ adds space between content + scrollbar */ +} + +.scrollbar-themed::-webkit-scrollbar-thumb { + background: #555; + border-radius: 9999px; +} + +.scrollbar-themed::-webkit-scrollbar-thumb:hover { + background: #777; } \ No newline at end of file diff --git a/ui/src/components/Navigation/Navbar.vue b/ui/src/components/Navigation/Navbar.vue new file mode 100644 index 0000000..e918511 --- /dev/null +++ b/ui/src/components/Navigation/Navbar.vue @@ -0,0 +1,201 @@ + + + \ No newline at end of file diff --git a/ui/src/components/application/ApplicationChat.vue b/ui/src/components/application/ApplicationChat.vue index a4d48a1..1d6b352 100644 --- a/ui/src/components/application/ApplicationChat.vue +++ b/ui/src/components/application/ApplicationChat.vue @@ -11,13 +11,22 @@ import { import Textarea from '@/components/ui/textarea/Textarea.vue' import { toTypedSchema } from '@vee-validate/zod' import * as z from 'zod' +import { useAuth } from '@/composables/useAuth' +import { CommentRow } from '@shared/types/application' +import { Dot } from 'lucide-vue-next' +import { ref } from 'vue' +import MemberCard from '../members/MemberCard.vue' -const props = defineProps<{ - messages: Array> -}>() +const props = withDefaults(defineProps<{ + messages: CommentRow[] + adminMode?: boolean +}>(), { + adminMode: false, +}) const emit = defineEmits<{ (e: 'post', text: string): void + (e: 'postInternal', text: string): void }>() const commentSchema = toTypedSchema( @@ -26,9 +35,14 @@ const commentSchema = toTypedSchema( }) ) +const submitMode = ref("public"); + // vee-validate passes (values, actions) to @submit function onSubmit(values: { text: string }, { resetForm }: { resetForm: () => void }) { - emit('post', values.text.trim()) + if (submitMode.value === "internal") + emit('postInternal', values.text.trim()) + else + emit('post', values.text.trim()) resetForm() } @@ -38,7 +52,7 @@ function onSubmit(values: { text: string }, { resetForm }: { resetForm: () => vo
- Comment + Comment +
+ +
+ + + + +
+ +
+ +
+

+ LOA Request Submitted +

+ +

+ {{ adminMode ? 'You have successfully submitted a Leave of Absence on behalf of another member.' : + `Your Leave + of Absence request has been submitted successfully. + It will take effect on your selected start date.` }} +

+ + +
+ +
- +
+ +
+ + + +
+ + +
+
+
+ + + + + + + + + + + + + Remarks +