diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..87fa3a3 --- /dev/null +++ b/.env.example @@ -0,0 +1,45 @@ +NODE_ENV=production +APP_PORT=3000 +PORT=3000 +TZ=Asia/Ho_Chi_Minh +APP_TIME_ZONE=Asia/Ho_Chi_Minh + +# Database: never commit real credentials. +DB_SERVER=sql-server.internal +DB_USER=accmanager_app +DB_PASSWORD=replace-with-a-strong-database-password +DB_NAME=AccManager +DB_ENCRYPT=true +DB_TRUST_CERTIFICATE=false +DB_CONNECT_TIMEOUT=30000 + +# Generate a separate random value of at least 32 bytes and keep it stable. +# Changing this value without a key-rotation migration will make stored +# application credentials unreadable. +DATA_ENCRYPTION_SECRET=replace-with-at-least-32-random-bytes +BCRYPT_ROUNDS=12 + +# Public HTTPS origin used by email links, CORS and secure cookies. +APP_BASE_URL=https://accmanager.example.internal +CORS_ALLOWED_ORIGINS=https://accmanager.example.internal +COOKIE_SECURE=true +TRUST_PROXY_HOPS=1 +SESSION_TTL_HOURS=12 +REMEMBER_SESSION_TTL_DAYS=14 +ALLOW_SELF_REGISTRATION=false + +# Only needed once when bootstrapping an empty database. Remove the password +# from the environment after the first administrator has been created. +INITIAL_ADMIN_USERNAME=admin +INITIAL_ADMIN_PASSWORD= +INITIAL_ADMIN_EMAIL=admin@example.internal + +SMTP_HOST= +SMTP_PORT=587 +SMTP_SECURE=false +SMTP_REQUIRE_TLS=true +SMTP_USER= +SMTP_PASS= +SMTP_FROM= +EMAIL_VERIFY_TOKEN_TTL_MINUTES=30 +PASSWORD_RESET_TOKEN_TTL_MINUTES=30 diff --git a/.gitignore b/.gitignore index bfecc27..3db1449 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ node_modules .env +.env.* +!.env.example +DEPLOYMENT_GUIDE.md docs-output scripts .codex-server.err.log diff --git a/DEPLOYMENT_GUIDE.md b/DEPLOYMENT_GUIDE.md deleted file mode 100644 index 6f57b8b..0000000 --- a/DEPLOYMENT_GUIDE.md +++ /dev/null @@ -1,296 +0,0 @@ -# 📘 Hướng dẫn Triển khai AccManager - -Tài liệu này hướng dẫn chi tiết cách build Docker image, push lên registry, và triển khai trên server. - ---- - -## 📋 Mục lục - -1. [Chuẩn bị](#chuẩn-bị) -2. [Máy DEV: Build & Push Image](#máy-dev-build--push-image) -3. [Máy Server: Pull & Deploy](#máy-server-pull--deploy) -4. [Kiểm tra & Troubleshoot](#kiểm-tra--troubleshoot) -5. [Cập nhật bản mới](#cập-nhật-bản-mới) -6. [Public Domain qua Nginx Proxy Manager](#public-domain-qua-nginx-proxy-manager) - ---- - -## 🔧 Chuẩn bị - -### Tài khoản & Biến môi trường - -1. **Docker Hub Account**: Tạo account tại https://hub.docker.com - - Username: `toiiiiday` (dùng username của bạn) - - Repository: `accmanager` - -2. **File .env trên máy dev** - Kiểm tra nội dung: - ```env - NODE_ENV=production - APP_PORT=3000 - DOCKER_IMAGE=toiiiiday/accmanager:1.0.1 - PORT=3000 - DB_SERVER=172.20.235.176 - DB_USER=sa - DB_PASSWORD=robotics@2022 - DB_NAME=AccManager - DB_ENCRYPT=false - DB_TRUST_CERTIFICATE=true - DB_CONNECT_TIMEOUT=30000 - BCRYPT_ROUNDS=12 - ``` - -3. **Thư mục trên server** - SSH vào server tạo: - ```bash - mkdir -p ~/accmanager - cd ~/accmanager - ``` - ---- - -## 🖥️ Máy DEV: Build & Push Image - -### Bước 1: Chọn version mới - -Mỗi lần sửa code và muốn deploy, chọn một tag mới theo thứ tự tăng dần. - -Ví dụ: -- Bản cũ đang chạy: `1.0.2` -- Bản mới sau khi sửa code: `1.0.3` - -### Bước 2: Build image mới (trên máy DEV) - -```powershell -cd D:\RoboticsSource\AccManager -docker build -t toiiiiday/accmanager:"version" . -``` - -### Bước 3: Push image mới lên Docker Hub - -```powershell -docker push toiiiiday/accmanager:"version" -``` - -### Bước 4: Kiểm tra image đã có trên registry - -```powershell -docker image ls | findstr toiiiiday/accmanager -``` - -Hoặc kiểm tra trên Docker Hub: -https://hub.docker.com/r/toiiiiday/accmanager/tags - -### Bước 5: Cập nhật .env - -Sửa dòng `DOCKER_IMAGE`: -``` -DOCKER_IMAGE=toiiiiday/accmanager:"version" -``` - -Mẹo PowerShell (cập nhật nhanh): -```powershell -(Get-Content .env) -replace '^DOCKER_IMAGE=.*', 'DOCKER_IMAGE=toiiiiday/accmanager:1.0.3' | Set-Content .env -``` - -### Bước 6: Copy confi lên server - -Từ máy dev: -```powershell -scp .env robotics@172.20.235.176:~/accmanager/.env -scp docker-compose.yml robotics@172.20.235.176:~/accmanager/docker-compose.yml -scp docker-compose.image.yml robotics@172.20.235.176:~/accmanager/docker-compose.image.yml -``` - ---- - -## 🐧 Máy Server: Pull & Deploy - -### Bước 1: SSH vào server - -```bash -ssh robotics@172.20.235.176 -``` - -### Bước 2: Vào thư mục deploy - -```bash -cd ~/accmanager -``` - -### Bước 3: Pull image mới và chạy lại container - -```bash -docker compose --env-file .env -f docker-compose.image.yml pull accmanager -docker compose --env-file .env -f docker-compose.image.yml up -d accmanager -``` - -Kiểm tra trạng thái: -```bash -docker compose -f docker-compose.image.yml ps -``` - -Xem log: -```bash -docker compose -f docker-compose.image.yml logs -f accmanager -``` - ---- - -## ✅ Kiểm tra & Troubleshoot - -### Kiểm tra app chạy OK - -```bash -# Kiểm tra container đang running -docker compose -f docker-compose.image.yml ps - -# Xem log (tìm "Server running") -docker compose -f docker-compose.image.yml logs --tail=50 accmanager - - -### Nếu gặp lỗi - -**Lỗi: "image not found"** -- Kiểm tra: `cat .env | grep DOCKER_IMAGE` -- Đảm bảo image đã push lên Docker Hub, kiểm tra: https://hub.docker.com/r/toiiiiday/accmanager - -**Lỗi: "connection refused"** -- Kiểm tra DB Server có chạy: `ssh robotics@172.20.235.176` -- Kiểm tra DB credentials trong .env - -**Lỗi: Container restart liên tục** -- Xem log: `docker compose -f docker-compose.image.yml logs --tail=100 accmanager` - ---- - -## 🔄 Cập nhật bản mới - -Áp dụng đúng 8 bước sau cho mỗi lần sửa code: - -### Trên máy DEV - -1. Chọn version mới (ví dụ `1.0.3`) -2. Build: -```powershell -docker build -t toiiiiday/accmanager:"version" . -``` -3. Push: -```powershell -docker push toiiiiday/accmanager:"version" -``` -4. Cập nhật `.env`: -```env -DOCKER_IMAGE=toiiiiday/accmanager:"version" -``` -5. Copy `.env` lên server: -```powershell -scp .env robotics@172.20.235.176:~/accmanager/.env -``` - -### Trên máy SERVER - -6. SSH và vào thư mục deploy: -```bash -ssh robotics@172.20.235.176 -cd ~/accmanager -``` -7. Pull + Up: -```bash -docker compose --env-file .env -f docker-compose.image.yml pull accmanager -docker compose --env-file .env -f docker-compose.image.yml up -d accmanager -``` -8. Kiểm tra bản mới đã chạy: -```bash -docker compose -f docker-compose.image.yml ps -docker compose -f docker-compose.image.yml logs --tail=50 accmanager -``` - -### Có cần gắn tag/version cho mỗi phiên bản mới không? - -**Có, nên làm bắt buộc cho production.** - -Lý do: -1. Tránh đè image cũ và tránh nhầm lẫn khi deploy. -2. Rollback nhanh về bản ổn định trước đó. -3. Truy vết được bản code nào đang chạy trên server. -4. Tránh rủi ro do dùng `latest` (khó kiểm soát). - -Quy ước khuyến nghị: -- `1.0.2` -> fix nhỏ -- `1.1.0` -> thêm tính năng -- `2.0.0` -> thay đổi lớn/breaking - -Ví dụ rollback về bản cũ `1.0.2`: -```env -DOCKER_IMAGE=toiiiiday/accmanager:1.0.2 -``` -```bash -docker compose --env-file .env -f docker-compose.image.yml pull accmanager -docker compose --env-file .env -f docker-compose.image.yml up -d accmanager -``` - ---- - -## 🌐 Public Domain qua Nginx Proxy Manager - -### Chuẩn bị - -1. **Domain** - - Trỏ DNS A record về IP public nơi đặt Nginx Proxy Manager - -2. **Firewall/Router** - - Mở inbound port 80, 443 từ Internet - - Port 3000 chỉ nội bộ (không public) - - - ---- - -## 📝 Các file liên quan - -- `docker-compose.yml` - Build local -- `docker-compose.image.yml` - Pull & run từ registry -- `.env` - Biến môi trường -- `.dockerignore` - Ignore file khi build -- `Dockerfile` - Config image -- `deploy-dev.ps1` - Script build & push (Windows) -- `deploy-server.sh` - Script pull & deploy (Linux) - ---- - -## 🎯 Tóm tắt quy trình - -``` -Máy DEV -├─ Chỉnh sửa code -├─ docker build -t toiiiiday/accmanager:X . -├─ docker push toiiiiday/accmanager:X -├─ sửa DOCKER_IMAGE trong .env -└─ scp .env server (copy env) - -Máy Server -├─ ssh vào server -├─ cd ~/accmanager -├─ docker compose pull accmanager -└─ docker compose up -d accmanager - -Nginx Proxy Manager -└─ Forward từ domain → http://172.20.235.176:3000 -``` - ---- - -## 💡 Mẹo - -1. **Luôn tăng version tag**: 1.0.1 → 1.0.2 → 1.0.3 -2. **Rollback nhanh**: Chỉ cần đổi DOCKER_IMAGE trong .env sang tag cũ rồi deploy lại -3. **Giữ log**: `docker compose logs --tail=1000 > backup.log` -4. **Restart container**: `docker compose -f docker-compose.image.yml restart accmanager` -5. **Xóa container cũ**: `docker compose -f docker-compose.image.yml down` -6. **Không dùng `latest` cho production**: luôn deploy bằng tag cụ thể - ---- - -**Cần giúp? Xem log chi tiết:** -```bash -docker compose -f docker-compose.image.yml logs --tail=200 accmanager -``` diff --git a/Dockerfile b/Dockerfile index 9ce880d..a4cf8c5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-bookworm-slim +FROM node:22-bookworm-slim WORKDIR /app diff --git a/backend/server.js b/backend/server.js index b26929c..a51848b 100644 --- a/backend/server.js +++ b/backend/server.js @@ -10,6 +10,8 @@ const nodemailer = require('nodemailer'); const multer = require('multer'); const XLSX = require('xlsx'); const dotenv = require('dotenv'); +const helmet = require('helmet'); +const { rateLimit } = require('express-rate-limit'); dotenv.config(); @@ -17,6 +19,7 @@ const APP_TIME_ZONE = process.env.APP_TIME_ZONE || process.env.TZ || 'Asia/Ho_Ch process.env.TZ = APP_TIME_ZONE; const app = express(); +const IS_PRODUCTION = process.env.NODE_ENV === 'production'; function envBool(name, defaultValue) { const value = process.env[name]; @@ -27,23 +30,36 @@ function envBool(name, defaultValue) { return ['1', 'true', 'yes', 'on'].includes(String(value).toLowerCase()); } -const DB_SERVER = process.env.DB_SERVER || '172.20.235.176'; +const DB_SERVER = process.env.DB_SERVER || 'localhost'; const DB_USER = process.env.DB_USER || 'sa'; -const DB_PASSWORD = process.env.DB_PASSWORD || 'robotics@2022'; +const DB_PASSWORD = process.env.DB_PASSWORD || ''; const DB_NAME = process.env.DB_NAME || 'AccManager'; -const DB_ENCRYPT = envBool('DB_ENCRYPT', false); -const DB_TRUST_CERTIFICATE = envBool('DB_TRUST_CERTIFICATE', true); +const DB_ENCRYPT = envBool('DB_ENCRYPT', IS_PRODUCTION); +const DB_TRUST_CERTIFICATE = envBool('DB_TRUST_CERTIFICATE', !IS_PRODUCTION); const DB_CONNECT_TIMEOUT = Number(process.env.DB_CONNECT_TIMEOUT || 30000); -const BCRYPT_ROUNDS = Number(process.env.BCRYPT_ROUNDS || 12); -const PASSWORD_VIEW_SECRET = process.env.PASSWORD_VIEW_SECRET || 'change-this-password-view-secret'; -const PASSWORD_VIEW_KEY = crypto.createHash('sha256').update(String(PASSWORD_VIEW_SECRET)).digest(); -const PASSWORD_VIEW_PREFIX = 'enc:v1'; +const configuredBcryptRounds = Number(process.env.BCRYPT_ROUNDS || 12); +const BCRYPT_ROUNDS = Number.isInteger(configuredBcryptRounds) + ? Math.min(15, Math.max(10, configuredBcryptRounds)) + : 12; +const DATA_ENCRYPTION_SECRET = String( + process.env.DATA_ENCRYPTION_SECRET + || process.env.PASSWORD_VIEW_SECRET + || '' +); +const DATA_ENCRYPTION_KEY = crypto.createHash('sha256').update(DATA_ENCRYPTION_SECRET).digest(); +const DATA_ENCRYPTION_PREFIX = 'enc:v2'; const PORT = process.env.PORT || 3000; const APP_BASE_URL = String(process.env.APP_BASE_URL || `http://localhost:${PORT}`).replace(/\/+$/, ''); +const SESSION_COOKIE_NAME = 'accmanager_session'; +const SESSION_TTL_HOURS = Math.min(24, Math.max(1, Number(process.env.SESSION_TTL_HOURS || 12))); +const REMEMBER_SESSION_TTL_DAYS = Math.min(30, Math.max(1, Number(process.env.REMEMBER_SESSION_TTL_DAYS || 14))); +const COOKIE_SECURE = envBool('COOKIE_SECURE', IS_PRODUCTION); +const ALLOW_SELF_REGISTRATION = envBool('ALLOW_SELF_REGISTRATION', false); const SMTP_HOST = process.env.SMTP_HOST || ''; const SMTP_PORT = Number(process.env.SMTP_PORT || 587); const SMTP_SECURE = envBool('SMTP_SECURE', false); +const SMTP_REQUIRE_TLS = envBool('SMTP_REQUIRE_TLS', IS_PRODUCTION && !SMTP_SECURE); const SMTP_USER = process.env.SMTP_USER || ''; const SMTP_PASS = process.env.SMTP_PASS || ''; const SMTP_FROM = process.env.SMTP_FROM || SMTP_USER || 'no-reply@accmanager.local'; @@ -52,6 +68,26 @@ const PASSWORD_RESET_TOKEN_TTL_MINUTES = Number(process.env.PASSWORD_RESET_TOKEN let mailTransporter; +if (IS_PRODUCTION && (!process.env.DB_SERVER || !process.env.DB_USER || !DB_PASSWORD)) { + throw new Error('DB_SERVER, DB_USER and DB_PASSWORD are required in production'); +} + +if (DATA_ENCRYPTION_SECRET.length < 32) { + throw new Error('DATA_ENCRYPTION_SECRET must be configured with at least 32 characters before startup'); +} + +if (IS_PRODUCTION && !APP_BASE_URL.startsWith('https://')) { + throw new Error('APP_BASE_URL must use HTTPS in production'); +} + +if (IS_PRODUCTION && !COOKIE_SECURE) { + throw new Error('COOKIE_SECURE must be true in production'); +} + +if (IS_PRODUCTION && (!DB_ENCRYPT || DB_TRUST_CERTIFICATE)) { + console.warn('[SECURITY] SQL TLS certificate validation is not fully enabled. Set DB_ENCRYPT=true and DB_TRUST_CERTIFICATE=false when the SQL Server has a trusted certificate.'); +} + const appTimePartsFormatter = new Intl.DateTimeFormat('en-CA', { timeZone: APP_TIME_ZONE, year: 'numeric', @@ -115,21 +151,25 @@ async function verifyPassword(plainPassword, storedPassword) { return String(plainPassword) === String(storedPassword || ''); } -function encryptPasswordForView(plainPassword) { +function encryptSensitiveValue(plainValue) { + if (plainValue === null || plainValue === undefined || plainValue === '') { + return ''; + } + const iv = crypto.randomBytes(12); - const cipher = crypto.createCipheriv('aes-256-gcm', PASSWORD_VIEW_KEY, iv); + const cipher = crypto.createCipheriv('aes-256-gcm', DATA_ENCRYPTION_KEY, iv); const encrypted = Buffer.concat([ - cipher.update(String(plainPassword), 'utf8'), + cipher.update(String(plainValue), 'utf8'), cipher.final() ]); const tag = cipher.getAuthTag(); - return `${PASSWORD_VIEW_PREFIX}:${iv.toString('base64')}:${tag.toString('base64')}:${encrypted.toString('base64')}`; + return `${DATA_ENCRYPTION_PREFIX}:${iv.toString('base64')}:${tag.toString('base64')}:${encrypted.toString('base64')}`; } -function decryptPasswordForView(payload) { +function decryptSensitiveValue(payload) { try { - if (typeof payload !== 'string' || !payload.startsWith(`${PASSWORD_VIEW_PREFIX}:`)) { + if (typeof payload !== 'string' || !payload.startsWith(`${DATA_ENCRYPTION_PREFIX}:`)) { return null; } @@ -141,7 +181,17 @@ function decryptPasswordForView(payload) { const iv = Buffer.from(parts[2], 'base64'); const tag = Buffer.from(parts[3], 'base64'); const encrypted = Buffer.from(parts[4], 'base64'); - const decipher = crypto.createDecipheriv('aes-256-gcm', PASSWORD_VIEW_KEY, iv); + if ( + iv.length !== 12 + || tag.length !== 16 + || encrypted.length === 0 + || iv.toString('base64') !== parts[2] + || tag.toString('base64') !== parts[3] + || encrypted.toString('base64') !== parts[4] + ) { + return null; + } + const decipher = crypto.createDecipheriv('aes-256-gcm', DATA_ENCRYPTION_KEY, iv); decipher.setAuthTag(tag); const plain = Buffer.concat([decipher.update(encrypted), decipher.final()]); return plain.toString('utf8'); @@ -180,6 +230,9 @@ function getMailTransporter() { host: SMTP_HOST, port: SMTP_PORT, secure: SMTP_SECURE, + requireTLS: SMTP_REQUIRE_TLS, + disableFileAccess: true, + disableUrlAccess: true, auth: { user: SMTP_USER, pass: SMTP_PASS @@ -192,12 +245,14 @@ function getMailTransporter() { async function sendVerificationEmail({ email, username, token }) { const verifyUrl = getEmailVerificationUrl(token); + const safeUsername = escapeHtml(username || 'there'); + const safeVerifyUrl = escapeHtml(verifyUrl); if (!canSendEmails()) { - console.warn(`SMTP is not configured. Verification URL for ${email}: ${verifyUrl}`); + console.warn(`SMTP is not configured. Cannot send verification email to ${email}.`); return { sent: false, - previewUrl: verifyUrl, + ...(!IS_PRODUCTION ? { previewUrl: verifyUrl } : {}), reason: 'SMTP is not configured' }; } @@ -212,13 +267,13 @@ async function sendVerificationEmail({ email, username, token }) { html: `

Confirm your email

-

Hello ${username || 'there'},

+

Hello ${safeUsername},

Thank you for registering. Please confirm your email by clicking the button below:

- Confirm Email + Confirm Email

Or copy this URL into your browser:

-

${verifyUrl}

+

${safeVerifyUrl}

This link will expire in ${EMAIL_VERIFY_TOKEN_TTL_MINUTES} minutes.

If you did not register this account, you can ignore this message.

@@ -237,12 +292,14 @@ async function sendVerificationEmail({ email, username, token }) { async function sendPasswordResetEmail({ email, username, token }) { const resetUrl = getPasswordResetUrl(token); + const safeUsername = escapeHtml(username || 'there'); + const safeResetUrl = escapeHtml(resetUrl); if (!canSendEmails()) { - console.warn(`SMTP is not configured. Password reset URL for ${email}: ${resetUrl}`); + console.warn(`SMTP is not configured. Cannot send password reset email to ${email}.`); return { sent: false, - previewUrl: resetUrl, + ...(!IS_PRODUCTION ? { previewUrl: resetUrl } : {}), reason: 'SMTP is not configured' }; } @@ -257,13 +314,13 @@ async function sendPasswordResetEmail({ email, username, token }) { html: `

Reset your password

-

Hello ${username || 'there'},

+

Hello ${safeUsername},

We received a password reset request for your account.

- Reset Password + Reset Password

Or copy this URL into your browser:

-

${resetUrl}

+

${safeResetUrl}

This link will expire in ${PASSWORD_RESET_TOKEN_TTL_MINUTES} minutes.

If you did not request this reset, you can ignore this message.

@@ -281,11 +338,33 @@ async function sendPasswordResetEmail({ email, username, token }) { } function getUserIdFromRequest(req) { - const rawUserId = req.headers['x-user-id'] || req.query.userId; - const userId = Number(rawUserId); + const userId = Number(req.user?.UserId); return Number.isInteger(userId) && userId > 0 ? userId : null; } +function getRequesterRole(req) { + return normalizeRole(req.user?.Role || req.user?.RoleName); +} + +function sendInternalError(res, err, publicMessage = 'Internal server error') { + console.error('Request failed:', err?.message || err); + return res.status(500).json({ success: false, message: publicMessage }); +} + +function normalizeOptionalHttpUrl(value) { + const text = String(value || '').trim(); + if (!text) { + return ''; + } + + try { + const parsed = new URL(text); + return ['http:', 'https:'].includes(parsed.protocol) ? parsed.toString() : null; + } catch (err) { + return null; + } +} + async function getUserDisplayNameById(userId) { if (!userId) { return null; @@ -742,6 +821,19 @@ function normalizeImportToken(value) { .replace(/[^a-z0-9]/g, ''); } +function isEncryptedSensitiveValue(value) { + return typeof value === 'string' && value.startsWith(`${DATA_ENCRYPTION_PREFIX}:`); +} + +function escapeHtml(value) { + return String(value || '') + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); +} + function normalizeAssetDamageType(value) { const normalized = normalizeImportToken(value); if (['disposed', 'dispose', 'disposal', 'thanhly', 'liquidated', 'liquidation'].includes(normalized)) { @@ -2019,12 +2111,114 @@ function parseConsumableImportRowsFromWorkbook(workbook) { }; } -// Middleware -app.use(cors()); -app.use(express.json()); +// Security and request middleware +app.disable('x-powered-by'); +app.set('trust proxy', Number(process.env.TRUST_PROXY_HOPS || (IS_PRODUCTION ? 1 : 0))); +app.use(helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + baseUri: ["'self'"], + connectSrc: ["'self'"], + fontSrc: ["'self'", 'data:', 'https://fonts.gstatic.com'], + formAction: ["'self'"], + frameAncestors: ["'none'"], + imgSrc: ["'self'", 'data:', 'blob:'], + objectSrc: ["'none'"], + scriptSrc: ["'self'", "'unsafe-inline'", 'https://cdn.jsdelivr.net', 'https://cdn.sheetjs.com'], + styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com', 'https://cdn.jsdelivr.net'], + workerSrc: ["'self'", 'blob:'], + ...(IS_PRODUCTION ? { upgradeInsecureRequests: [] } : {}) + } + }, + crossOriginEmbedderPolicy: false, + strictTransportSecurity: IS_PRODUCTION + ? { maxAge: 31536000, includeSubDomains: true } + : false +})); + +const configuredOrigins = String(process.env.CORS_ALLOWED_ORIGINS || '') + .split(',') + .map(origin => origin.trim().replace(/\/+$/, '')) + .filter(Boolean); +const allowedOrigins = new Set(configuredOrigins); +try { + allowedOrigins.add(new URL(APP_BASE_URL).origin); +} catch (err) { + console.warn('[SECURITY] APP_BASE_URL is not a valid absolute URL; configure CORS_ALLOWED_ORIGINS explicitly.'); +} + +function requestOriginIsAllowed(req) { + const origin = String(req.headers.origin || '').replace(/\/+$/, ''); + if (!origin) { + return true; + } + + const requestOrigin = `${req.protocol}://${req.get('host')}`.replace(/\/+$/, ''); + return origin === requestOrigin || allowedOrigins.has(origin); +} + +app.use(cors({ + origin(origin, callback) { + if (!origin || allowedOrigins.has(String(origin).replace(/\/+$/, ''))) { + return callback(null, true); + } + return callback(null, false); + }, + credentials: true, + methods: ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'X-Requested-With'] +})); +app.use((req, res, next) => { + if (!['GET', 'HEAD', 'OPTIONS'].includes(req.method) && !requestOriginIsAllowed(req)) { + return res.status(403).json({ success: false, message: 'Request origin is not allowed' }); + } + next(); +}); +app.use(express.json({ limit: '1mb', strict: true })); +app.use('/api', (req, res, next) => { + res.setHeader('Cache-Control', 'no-store'); + next(); +}); + +const loginRateLimit = rateLimit({ + windowMs: 15 * 60 * 1000, + limit: 10, + standardHeaders: 'draft-8', + legacyHeaders: false, + message: { success: false, message: 'Too many sign-in attempts. Please try again later.' } +}); +const accountRecoveryRateLimit = rateLimit({ + windowMs: 60 * 60 * 1000, + limit: 5, + standardHeaders: 'draft-8', + legacyHeaders: false, + message: { success: false, message: 'Too many requests. Please try again later.' } +}); +const credentialRevealRateLimit = rateLimit({ + windowMs: 15 * 60 * 1000, + limit: 60, + standardHeaders: 'draft-8', + legacyHeaders: false, + message: { success: false, message: 'Too many credential reveal requests. Please try again later.' } +}); + +const allowedSpreadsheetExtensions = new Set(['.xls', '.xlsx']); const upload = multer({ storage: multer.memoryStorage(), - limits: { fileSize: 15 * 1024 * 1024 } + limits: { + fileSize: 10 * 1024 * 1024, + files: 1, + fields: 10, + parts: 12 + }, + fileFilter(req, file, callback) { + const extension = require('path').extname(String(file.originalname || '')).toLowerCase(); + if (!allowedSpreadsheetExtensions.has(extension)) { + return callback(new Error('Only .xls and .xlsx files are allowed')); + } + callback(null, true); + } }); // Serve static files from /public @@ -2060,6 +2254,164 @@ const sqlConfig = { // Initialize Database Pool let pool; +function parseRequestCookies(req) { + const cookies = {}; + const header = String(req.headers.cookie || ''); + for (const part of header.split(';')) { + const separatorIndex = part.indexOf('='); + if (separatorIndex <= 0) { + continue; + } + + const name = part.slice(0, separatorIndex).trim(); + const rawValue = part.slice(separatorIndex + 1).trim(); + try { + cookies[name] = decodeURIComponent(rawValue); + } catch (err) { + cookies[name] = rawValue; + } + } + return cookies; +} + +function hashSessionToken(token) { + return crypto.createHash('sha256').update(String(token)).digest('hex'); +} + +function getSessionLifetimeMs(remember) { + return remember + ? REMEMBER_SESSION_TTL_DAYS * 24 * 60 * 60 * 1000 + : SESSION_TTL_HOURS * 60 * 60 * 1000; +} + +function setSessionCookie(res, token, expiresAt) { + res.cookie(SESSION_COOKIE_NAME, token, { + httpOnly: true, + secure: COOKIE_SECURE, + sameSite: 'strict', + path: '/', + expires: expiresAt + }); +} + +function clearSessionCookie(res) { + res.clearCookie(SESSION_COOKIE_NAME, { + httpOnly: true, + secure: COOKIE_SECURE, + sameSite: 'strict', + path: '/' + }); +} + +function toSafeUser(user) { + if (!user) { + return null; + } + + const role = user.Role || user.RoleName || 'guest'; + return { + UserId: user.UserId, + Username: user.Username, + Email: user.Email, + FullName: user.FullName, + Role: role, + role, + RoleId: user.RoleId, + Status: user.Status, + EmailVerified: Boolean(user.EmailVerified) + }; +} + +async function createAuthSession(userId, req, res, remember = false) { + const token = crypto.randomBytes(32).toString('base64url'); + const tokenHash = hashSessionToken(token); + const expiresAt = new Date(Date.now() + getSessionLifetimeMs(remember)); + const userAgent = String(req.get('user-agent') || '').slice(0, 500); + const ipAddress = String(req.ip || req.socket?.remoteAddress || '').slice(0, 64); + + await pool.request() + .input('tokenHash', sql.Char(64), tokenHash) + .input('userId', sql.Int, userId) + .input('expiresAt', sql.DateTime2, expiresAt) + .input('userAgent', sql.NVarChar(500), userAgent || null) + .input('ipAddress', sql.NVarChar(64), ipAddress || null) + .query(` + DELETE FROM AuthSessions WHERE ExpiresAt <= SYSUTCDATETIME(); + INSERT INTO AuthSessions (TokenHash, UserId, ExpiresAt, UserAgent, IpAddress) + VALUES (@tokenHash, @userId, @expiresAt, @userAgent, @ipAddress); + `); + + setSessionCookie(res, token, expiresAt); +} + +async function destroyAuthSession(req, res) { + const token = parseRequestCookies(req)[SESSION_COOKIE_NAME]; + if (token) { + await pool.request() + .input('tokenHash', sql.Char(64), hashSessionToken(token)) + .query('DELETE FROM AuthSessions WHERE TokenHash = @tokenHash'); + } + clearSessionCookie(res); +} + +async function invalidateUserSessions(userId, exceptSessionId = null) { + if (!userId) { + return; + } + + const request = pool.request().input('userId', sql.Int, userId); + if (exceptSessionId) { + request.input('sessionId', sql.BigInt, exceptSessionId); + await request.query('DELETE FROM AuthSessions WHERE UserId = @userId AND SessionId <> @sessionId'); + return; + } + await request.query('DELETE FROM AuthSessions WHERE UserId = @userId'); +} + +async function requireAuth(req, res, next) { + try { + const token = parseRequestCookies(req)[SESSION_COOKIE_NAME]; + if (!token) { + return res.status(401).json({ success: false, message: 'Authentication required' }); + } + + const result = await pool.request() + .input('tokenHash', sql.Char(64), hashSessionToken(token)) + .query(` + SELECT TOP 1 + s.SessionId, + u.UserId, + u.Username, + u.Email, + u.FullName, + COALESCE(r.RoleName, u.Role) AS Role, + u.RoleId, + u.Status, + u.EmailVerified + FROM AuthSessions s + INNER JOIN Users u ON u.UserId = s.UserId + LEFT JOIN Roles r ON r.RoleId = u.RoleId + WHERE s.TokenHash = @tokenHash + AND s.ExpiresAt > SYSUTCDATETIME() + AND u.IsActive = 1 + AND u.EmailVerified = 1; + `); + + if (result.recordset.length === 0) { + clearSessionCookie(res); + return res.status(401).json({ success: false, message: 'Session is invalid or expired' }); + } + + const authenticated = result.recordset[0]; + req.authSessionId = authenticated.SessionId; + req.user = toSafeUser(authenticated); + next(); + } catch (err) { + console.error('Authentication middleware error:', err.message); + res.status(500).json({ success: false, message: 'Unable to validate session' }); + } +} + async function initializeDatabase() { try { pool = new sql.ConnectionPool(sqlConfig); @@ -2090,6 +2442,7 @@ async function initializeDatabase() { // Now create tables in AccManager await createTables(); await migrateLegacyPasswords(); + await migrateStoredAccountPasswords(); console.log('[OK] Database and tables created'); } catch (err) { @@ -2101,36 +2454,24 @@ async function initializeDatabase() { async function migrateLegacyPasswords() { try { const usersResult = await pool.request() - .query('SELECT UserId, Password, ViewPassword FROM Users WHERE Password IS NOT NULL'); + .query('SELECT UserId, Password FROM Users WHERE Password IS NOT NULL'); let migratedCount = 0; for (const row of usersResult.recordset) { - const request = pool.request() - .input('userId', sql.Int, row.UserId); - - let hasUpdates = false; const rawPassword = String(row.Password || ''); - - if (!row.ViewPassword && !isBcryptHash(rawPassword)) { - request.input('viewPassword', sql.NVarChar, encryptPasswordForView(rawPassword)); - hasUpdates = true; - } - - if (!isBcryptHash(row.Password)) { - const hashedPassword = await hashPassword(row.Password); - request.input('password', sql.NVarChar, hashedPassword); - hasUpdates = true; + if (!isBcryptHash(rawPassword)) { + await pool.request() + .input('userId', sql.Int, row.UserId) + .input('password', sql.NVarChar, await hashPassword(rawPassword)) + .query('UPDATE Users SET Password = @password WHERE UserId = @userId'); migratedCount += 1; } - - if (hasUpdates) { - await request.query(`UPDATE Users - SET ${!isBcryptHash(rawPassword) ? 'Password = @password' : 'Password = Password'} - ${(!row.ViewPassword && !isBcryptHash(rawPassword)) ? ', ViewPassword = @viewPassword' : ''} - WHERE UserId = @userId`); - } } + // Login passwords must never be recoverable. Retain the legacy column only + // for backward-compatible schema upgrades, but permanently erase its data. + await pool.request().query('UPDATE Users SET ViewPassword = NULL WHERE ViewPassword IS NOT NULL'); + if (migratedCount > 0) { console.log(`[OK] Migrated ${migratedCount} legacy plain-text password(s) to bcrypt`); } @@ -2139,6 +2480,34 @@ async function migrateLegacyPasswords() { } } +async function migrateStoredAccountPasswords() { + try { + const accountsResult = await pool.request() + .query('SELECT AccountId, AccountPassword FROM Accounts WHERE AccountPassword IS NOT NULL AND AccountPassword <> N\'\''); + + let migratedCount = 0; + for (const row of accountsResult.recordset) { + const currentValue = String(row.AccountPassword || ''); + if (isEncryptedSensitiveValue(currentValue)) { + continue; + } + + await pool.request() + .input('accountId', sql.Int, row.AccountId) + .input('accountPassword', sql.NVarChar(2048), encryptSensitiveValue(currentValue)) + .query('UPDATE Accounts SET AccountPassword = @accountPassword WHERE AccountId = @accountId'); + migratedCount += 1; + } + + if (migratedCount > 0) { + console.log(`[OK] Encrypted ${migratedCount} stored application password(s)`); + } + } catch (err) { + console.error('Stored account password migration error:', err.message); + throw err; + } +} + async function ensureAppTimeDefaultConstraints() { await pool.request().query(` DECLARE @schemaName SYSNAME = N'dbo'; @@ -2253,6 +2622,23 @@ async function createTables() { IsActive BIT DEFAULT 1 ) END`, + + // Server-side authentication sessions + `IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AuthSessions') + BEGIN + CREATE TABLE AuthSessions ( + SessionId BIGINT PRIMARY KEY IDENTITY(1,1), + TokenHash CHAR(64) UNIQUE NOT NULL, + UserId INT NOT NULL, + ExpiresAt DATETIME2 NOT NULL, + CreatedAt DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(), + UserAgent NVARCHAR(500), + IpAddress NVARCHAR(64), + FOREIGN KEY (UserId) REFERENCES Users(UserId) ON DELETE CASCADE + ); + CREATE INDEX IX_AuthSessions_UserId ON AuthSessions(UserId); + CREATE INDEX IX_AuthSessions_ExpiresAt ON AuthSessions(ExpiresAt); + END`, // Applications Table `IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Applications') @@ -2278,7 +2664,7 @@ async function createTables() { UserId INT NOT NULL, AppId INT NOT NULL, AccountUsername NVARCHAR(100), - AccountPassword NVARCHAR(255), + AccountPassword NVARCHAR(2048), Email NVARCHAR(100), AccessLevel NVARCHAR(50), Status NVARCHAR(20) DEFAULT 'Active', @@ -3003,6 +3389,7 @@ async function createTables() { await pool.request().query(`IF COL_LENGTH('dbo.Applications','Url') IS NULL ALTER TABLE Applications ADD Url NVARCHAR(255);`); await pool.request().query(`IF COL_LENGTH('dbo.Applications','Description') IS NULL ALTER TABLE Applications ADD Description NVARCHAR(500);`); + await pool.request().query(`IF COL_LENGTH('dbo.Accounts','AccountPassword') IS NOT NULL ALTER TABLE Accounts ALTER COLUMN AccountPassword NVARCHAR(2048) NULL;`); await pool.request().query(`IF COL_LENGTH('dbo.Users','ViewPassword') IS NULL ALTER TABLE Users ADD ViewPassword NVARCHAR(1024);`); await pool.request().query(`IF COL_LENGTH('dbo.Users','EmailVerified') IS NULL ALTER TABLE Users ADD EmailVerified BIT NOT NULL CONSTRAINT DF_Users_EmailVerified DEFAULT(0);`); await pool.request().query(`IF COL_LENGTH('dbo.Users','EmailVerifiedAt') IS NULL ALTER TABLE Users ADD EmailVerifiedAt DATETIME NULL;`); @@ -3039,26 +3426,41 @@ async function createTables() { // Insert initial admin user try { - const adminPasswordHash = await hashPassword('admin'); - const adminViewPassword = encryptPasswordForView('admin'); - await pool.request() - .input('username', sql.NVarChar, 'admin') - .input('password', sql.NVarChar, adminPasswordHash) - .input('viewPassword', sql.NVarChar, adminViewPassword) - .input('email', sql.NVarChar, 'admin@accmanager.local') - .input('fullname', sql.NVarChar, 'Administrator') - .input('role', sql.NVarChar, 'admin') - .query(`IF NOT EXISTS (SELECT * FROM Users WHERE Username = @username) - INSERT INTO Users (Username, Password, ViewPassword, Email, FullName, Role, IsActive, EmailVerified, EmailVerifiedAt) - VALUES (@username, @password, @viewPassword, @email, @fullname, @role, 1, 1, DATEADD(HOUR, 7, SYSUTCDATETIME())) - ELSE - UPDATE Users - SET EmailVerified = 1, - EmailVerifiedAt = ISNULL(EmailVerifiedAt, DATEADD(HOUR, 7, SYSUTCDATETIME())) - WHERE Username = @username`); - console.log('[OK] Admin user created: admin / admin'); + const initialAdminPassword = String(process.env.INITIAL_ADMIN_PASSWORD || ''); + if (initialAdminPassword) { + if (initialAdminPassword.length < 12) { + throw new Error('INITIAL_ADMIN_PASSWORD must contain at least 12 characters'); + } + + const result = await pool.request() + .input('username', sql.NVarChar, String(process.env.INITIAL_ADMIN_USERNAME || 'admin').trim()) + .input('password', sql.NVarChar, await hashPassword(initialAdminPassword)) + .input('email', sql.NVarChar, String(process.env.INITIAL_ADMIN_EMAIL || 'admin@accmanager.local').trim()) + .input('fullname', sql.NVarChar, String(process.env.INITIAL_ADMIN_FULLNAME || 'Administrator').trim()) + .input('role', sql.NVarChar, 'admin') + .query(`IF NOT EXISTS (SELECT 1 FROM Users WHERE Username = @username) + BEGIN + INSERT INTO Users (Username, Password, Email, FullName, Role, IsActive, EmailVerified, EmailVerifiedAt) + VALUES (@username, @password, @email, @fullname, @role, 1, 1, DATEADD(HOUR, 7, SYSUTCDATETIME())); + SELECT CAST(1 AS BIT) AS Created; + END + ELSE SELECT CAST(0 AS BIT) AS Created;`); + + if (result.recordset?.[0]?.Created) { + console.log('[OK] Initial admin user created from environment configuration'); + } + } else { + const adminCount = await pool.request() + .query("SELECT COUNT(*) AS AdminCount FROM Users WHERE LOWER(ISNULL(Role, '')) = 'admin' AND IsActive = 1"); + if (Number(adminCount.recordset?.[0]?.AdminCount || 0) === 0) { + console.warn('[SECURITY] No active admin exists. Set a strong INITIAL_ADMIN_PASSWORD once to bootstrap the first administrator.'); + } + } } catch (err) { console.error('Admin user error:', err.message); + if (IS_PRODUCTION) { + throw err; + } } // Insert sample applications @@ -3083,10 +3485,16 @@ async function createTables() { // API ROUTES - Authentication // ========================================== +app.get('/api/auth/config', (req, res) => { + res.json({ success: true, allowSelfRegistration: ALLOW_SELF_REGISTRATION }); +}); + // Login endpoint -app.post('/api/auth/login', async (req, res) => { +app.post('/api/auth/login', loginRateLimit, async (req, res) => { try { - const { username, password } = req.body; + const username = String(req.body?.username || '').trim(); + const password = String(req.body?.password || ''); + const remember = req.body?.remember === true; if (!username || !password) { return res.status(400).json({ @@ -3094,6 +3502,10 @@ app.post('/api/auth/login', async (req, res) => { message: 'Username and password are required' }); } + + if (username.length > 254 || password.length > 256) { + return res.status(400).json({ success: false, message: 'Invalid username or password' }); + } const result = await pool.request() .input('username', sql.NVarChar, username) @@ -3129,8 +3541,7 @@ app.post('/api/auth/login', async (req, res) => { await pool.request() .input('userId', sql.Int, dbUser.UserId) .input('password', sql.NVarChar, upgradedHash) - .input('viewPassword', sql.NVarChar, encryptPasswordForView(password)) - .query('UPDATE Users SET Password = @password, ViewPassword = ISNULL(ViewPassword, @viewPassword) WHERE UserId = @userId'); + .query('UPDATE Users SET Password = @password, ViewPassword = NULL WHERE UserId = @userId'); } const { Password: _, ...safeUser } = dbUser; @@ -3140,6 +3551,9 @@ app.post('/api/auth/login', async (req, res) => { await pool.request() .input('userId', sql.Int, user.UserId) .query('UPDATE Users SET LastLogin = DATEADD(HOUR, 7, SYSUTCDATETIME()) WHERE UserId = @userId'); + + await destroyAuthSession(req, res); + await createAuthSession(user.UserId, req, res, remember); res.json({ success: true, @@ -3147,20 +3561,26 @@ app.post('/api/auth/login', async (req, res) => { user: user }); } else { + // Equalize the expensive bcrypt work for unknown and known accounts. + await hashPassword(password); res.status(401).json({ success: false, message: 'Invalid username or password' }); } } catch (err) { - console.error('Login error:', err); - res.status(500).json({ success: false, message: err.message }); + console.error('Login error:', err.message); + res.status(500).json({ success: false, message: 'Unable to sign in right now' }); } }); // Public registration endpoint -app.post('/api/auth/register', async (req, res) => { +app.post('/api/auth/register', accountRecoveryRateLimit, async (req, res) => { try { + if (!ALLOW_SELF_REGISTRATION) { + return res.status(403).json({ success: false, message: 'Self-registration is disabled. Please contact an administrator.' }); + } + const { username, password, email, fullname } = req.body; if (!username || !password || !email) { @@ -3169,6 +3589,13 @@ app.post('/api/auth/register', async (req, res) => { const normalizedEmail = String(email).trim().toLowerCase(); const safeUsername = String(username).trim(); + const safePassword = String(password || ''); + if (!/^[A-Za-z0-9._-]{3,50}$/.test(safeUsername)) { + return res.status(400).json({ success: false, message: 'Username must be 3-50 characters and contain only letters, numbers, dot, underscore or hyphen' }); + } + if (safePassword.length < 12 || safePassword.length > 256) { + return res.status(400).json({ success: false, message: 'Password must be between 12 and 256 characters' }); + } const isEmailFormatValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail); if (!isEmailFormatValid) { return res.status(400).json({ success: false, message: 'Email format is invalid' }); @@ -3189,8 +3616,7 @@ app.post('/api/auth/register', async (req, res) => { }); } - const hashedPassword = await hashPassword(password); - const viewPassword = encryptPasswordForView(password); + const hashedPassword = await hashPassword(safePassword); const { token, tokenHash } = generateEmailVerificationToken(); const safeFullname = fullname && fullname.trim() ? fullname.trim() : safeUsername; @@ -3234,12 +3660,11 @@ app.post('/api/auth/register', async (req, res) => { .input('fullname', sql.NVarChar, safeFullname) .input('roleId', sql.Int, guestRoleId) .input('role', sql.NVarChar, guestRoleName) - .input('viewPassword', sql.NVarChar, viewPassword) .input('emailVerifyToken', sql.NVarChar, tokenHash) .input('tokenTtlMinutes', sql.Int, EMAIL_VERIFY_TOKEN_TTL_MINUTES) - .query(`INSERT INTO Users (Username, Password, ViewPassword, Email, FullName, RoleId, Role, Status, IsActive, EmailVerified, EmailVerifyToken, EmailVerifyTokenExpires) + .query(`INSERT INTO Users (Username, Password, Email, FullName, RoleId, Role, Status, IsActive, EmailVerified, EmailVerifyToken, EmailVerifyTokenExpires) OUTPUT INSERTED.UserId, INSERTED.Username, INSERTED.Email, INSERTED.FullName, INSERTED.Role, INSERTED.RoleId - VALUES (@username, @password, @viewPassword, @email, @fullname, @roleId, @role, 'Active', 1, 0, @emailVerifyToken, DATEADD(MINUTE, @tokenTtlMinutes, DATEADD(HOUR, 7, SYSUTCDATETIME())))`); + VALUES (@username, @password, @email, @fullname, @roleId, @role, 'Active', 1, 0, @emailVerifyToken, DATEADD(MINUTE, @tokenTtlMinutes, DATEADD(HOUR, 7, SYSUTCDATETIME())))`); } else { result = await pool.request() .input('username', sql.NVarChar, safeUsername) @@ -3247,12 +3672,11 @@ app.post('/api/auth/register', async (req, res) => { .input('email', sql.NVarChar, normalizedEmail) .input('fullname', sql.NVarChar, safeFullname) .input('role', sql.NVarChar, guestRoleName) - .input('viewPassword', sql.NVarChar, viewPassword) .input('emailVerifyToken', sql.NVarChar, tokenHash) .input('tokenTtlMinutes', sql.Int, EMAIL_VERIFY_TOKEN_TTL_MINUTES) - .query(`INSERT INTO Users (Username, Password, ViewPassword, Email, FullName, Role, Status, IsActive, EmailVerified, EmailVerifyToken, EmailVerifyTokenExpires) + .query(`INSERT INTO Users (Username, Password, Email, FullName, Role, Status, IsActive, EmailVerified, EmailVerifyToken, EmailVerifyTokenExpires) OUTPUT INSERTED.UserId, INSERTED.Username, INSERTED.Email, INSERTED.FullName, INSERTED.Role - VALUES (@username, @password, @viewPassword, @email, @fullname, @role, 'Active', 1, 0, @emailVerifyToken, DATEADD(MINUTE, @tokenTtlMinutes, DATEADD(HOUR, 7, SYSUTCDATETIME())))`); + VALUES (@username, @password, @email, @fullname, @role, 'Active', 1, 0, @emailVerifyToken, DATEADD(MINUTE, @tokenTtlMinutes, DATEADD(HOUR, 7, SYSUTCDATETIME())))`); } const inserted = result.recordset[0]; @@ -3284,11 +3708,11 @@ app.post('/api/auth/register', async (req, res) => { userId: inserted?.UserId }; - if (emailResult.previewUrl) { + if (!IS_PRODUCTION && emailResult.previewUrl) { responsePayload.verificationPreviewUrl = emailResult.previewUrl; } - if (emailResult.reason && !emailResult.sent) { + if (!IS_PRODUCTION && emailResult.reason && !emailResult.sent) { responsePayload.emailError = emailResult.reason; } @@ -3347,6 +3771,9 @@ app.get('/api/auth/verify-email', async (req, res) => { .input('userId', sql.Int, verifiedUser.UserId) .query('UPDATE Users SET LastLogin = DATEADD(HOUR, 7, SYSUTCDATETIME()) WHERE UserId = @userId'); + await destroyAuthSession(req, res); + await createAuthSession(verifiedUser.UserId, req, res, false); + res.json({ success: true, message: 'Email confirmed successfully. Logging you in...', @@ -3359,7 +3786,7 @@ app.get('/api/auth/verify-email', async (req, res) => { } }); -app.post('/api/auth/resend-verification', async (req, res) => { +app.post('/api/auth/resend-verification', accountRecoveryRateLimit, async (req, res) => { try { const identifier = String(req.body?.identifier || req.body?.email || '').trim(); if (!identifier) { @@ -3409,11 +3836,11 @@ app.post('/api/auth/resend-verification', async (req, res) => { emailSent: emailResult.sent }; - if (emailResult.previewUrl) { + if (!IS_PRODUCTION && emailResult.previewUrl) { payload.verificationPreviewUrl = emailResult.previewUrl; } - if (emailResult.reason && !emailResult.sent) { + if (!IS_PRODUCTION && emailResult.reason && !emailResult.sent) { payload.emailError = emailResult.reason; } @@ -3424,7 +3851,7 @@ app.post('/api/auth/resend-verification', async (req, res) => { } }); -app.post('/api/auth/forgot-password', async (req, res) => { +app.post('/api/auth/forgot-password', accountRecoveryRateLimit, async (req, res) => { try { await ensurePasswordResetColumns(); @@ -3476,11 +3903,11 @@ app.post('/api/auth/forgot-password', async (req, res) => { emailSent: emailResult.sent }; - if (emailResult.previewUrl) { + if (!IS_PRODUCTION && emailResult.previewUrl) { payload.resetPreviewUrl = emailResult.previewUrl; } - if (emailResult.reason && !emailResult.sent) { + if (!IS_PRODUCTION && emailResult.reason && !emailResult.sent) { payload.emailError = emailResult.reason; } @@ -3491,7 +3918,7 @@ app.post('/api/auth/forgot-password', async (req, res) => { } }); -app.post('/api/auth/reset-password', async (req, res) => { +app.post('/api/auth/reset-password', accountRecoveryRateLimit, async (req, res) => { try { await ensurePasswordResetColumns(); @@ -3502,8 +3929,8 @@ app.post('/api/auth/reset-password', async (req, res) => { return res.status(400).json({ success: false, message: 'Reset token and new password are required' }); } - if (newPassword.length < 6) { - return res.status(400).json({ success: false, message: 'New password must be at least 6 characters' }); + if (newPassword.length < 12 || newPassword.length > 256) { + return res.status(400).json({ success: false, message: 'New password must be between 12 and 256 characters' }); } const tokenHash = hashVerificationToken(token); @@ -3528,19 +3955,18 @@ app.post('/api/auth/reset-password', async (req, res) => { } const hashedPassword = await hashPassword(newPassword); - const viewPassword = encryptPasswordForView(newPassword); - await pool.request() .input('userId', sql.Int, user.UserId) .input('password', sql.NVarChar, hashedPassword) - .input('viewPassword', sql.NVarChar, viewPassword) .query(`UPDATE Users SET Password = @password, - ViewPassword = @viewPassword, + ViewPassword = NULL, PasswordResetToken = NULL, PasswordResetTokenExpires = NULL WHERE UserId = @userId`); + await invalidateUserSessions(user.UserId); + res.json({ success: true, message: 'Password reset successful. You can sign in now.' }); } catch (err) { console.error('Reset password error:', err.message); @@ -3548,6 +3974,29 @@ app.post('/api/auth/reset-password', async (req, res) => { } }); +// Public, non-sensitive health check for container monitoring. +app.get('/api/health', (req, res) => { + res.json({ status: 'ok', timestamp: new Date().toISOString() }); +}); + +app.get('/api/auth/session', requireAuth, (req, res) => { + res.json({ success: true, user: req.user }); +}); + +app.post('/api/auth/logout', requireAuth, async (req, res) => { + try { + await destroyAuthSession(req, res); + res.json({ success: true, message: 'Signed out' }); + } catch (err) { + console.error('Logout error:', err.message); + clearSessionCookie(res); + res.status(500).json({ success: false, message: 'Unable to sign out cleanly' }); + } +}); + +// Every API declared below this point requires a valid server-side session. +app.use('/api', requireAuth); + // Middleware for role-based access control function normalizeRole(value) { return String(value || '').trim().toLowerCase(); @@ -3557,7 +4006,7 @@ function requireRoles(roles = [], message = 'Access denied') { const allowedRoles = new Set(roles.map(normalizeRole)); return (req, res, next) => { - const userRole = normalizeRole(req.headers['x-user-role'] || req.query.userRole); + const userRole = getRequesterRole(req); if (!allowedRoles.has(userRole)) { return res.status(403).json({ success: false, message }); } @@ -3579,7 +4028,7 @@ app.get('/api/roles', async (req, res) => { .query('SELECT RoleId, RoleName, Description, CreatedDate FROM Roles ORDER BY RoleName'); res.json({ success: true, data: result.recordset }); } catch (err) { - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -3596,7 +4045,7 @@ app.get('/api/roles/:id', async (req, res) => { res.status(404).json({ success: false, message: 'Role not found' }); } } catch (err) { - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -3621,7 +4070,7 @@ app.post('/api/roles', requireAdmin, async (req, res) => { res.json({ success: true, message: 'Role created', roleId: result.recordset[0].RoleId }); } catch (err) { - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -3641,7 +4090,7 @@ app.put('/api/roles/:id', requireAdmin, async (req, res) => { res.json({ success: true, message: 'Role updated' }); } catch (err) { - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -3666,7 +4115,7 @@ app.delete('/api/roles/:id', requireAdmin, async (req, res) => { res.json({ success: true, message: 'Role deleted' }); } catch (err) { - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -3685,7 +4134,7 @@ app.get('/api/users', async (req, res) => { ORDER BY u.CreatedDate DESC`); res.json({ success: true, data: result.recordset }); } catch (err) { - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -3709,7 +4158,7 @@ app.get('/api/users/me', async (req, res) => { res.json({ success: true, data: result.recordset[0] }); } catch (err) { - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -3752,9 +4201,13 @@ app.put('/api/users/me', async (req, res) => { const emailChanged = String(existingUser.Email || '').toLowerCase() !== incomingEmail; const shouldChangePassword = newPassword.length > 0; - if (shouldChangePassword) { + if (shouldChangePassword && (newPassword.length < 12 || newPassword.length > 256)) { + return res.status(400).json({ success: false, message: 'New password must be between 12 and 256 characters' }); + } + + if (shouldChangePassword || emailChanged) { if (!currentPassword) { - return res.status(400).json({ success: false, message: 'Current password is required to set a new password' }); + return res.status(400).json({ success: false, message: 'Current password is required for security-sensitive profile changes' }); } const isCurrentPasswordValid = await verifyPassword(currentPassword, existingUser.Password); @@ -3789,15 +4242,13 @@ app.put('/api/users/me', async (req, res) => { if (shouldChangePassword) { const hashedPassword = await hashPassword(newPassword); - const viewPassword = encryptPasswordForView(newPassword); request.input('password', sql.NVarChar, hashedPassword); - request.input('viewPassword', sql.NVarChar, viewPassword); } await request.query(`UPDATE Users SET FullName = @fullname, Email = @email - ${shouldChangePassword ? ', Password = @password, ViewPassword = @viewPassword' : ''} + ${shouldChangePassword ? ', Password = @password, ViewPassword = NULL' : ''} ${emailChanged ? ', EmailVerified = 0, EmailVerifiedAt = NULL, EmailVerifyToken = @emailVerifyToken, EmailVerifyTokenExpires = DATEADD(MINUTE, @tokenTtlMinutes, DATEADD(HOUR, 7, SYSUTCDATETIME()))' : ''} WHERE UserId = @userId`); @@ -3827,11 +4278,19 @@ app.put('/api/users/me', async (req, res) => { emailSent: emailChanged ? emailResult.sent : undefined }; - if (emailChanged && emailResult.previewUrl) { + if (emailChanged) { + await invalidateUserSessions(userId); + clearSessionCookie(res); + payload.sessionEnded = true; + } else if (shouldChangePassword) { + await invalidateUserSessions(userId, req.authSessionId); + } + + if (!IS_PRODUCTION && emailChanged && emailResult.previewUrl) { payload.verificationPreviewUrl = emailResult.previewUrl; } - if (emailChanged && emailResult.reason && !emailResult.sent) { + if (!IS_PRODUCTION && emailChanged && emailResult.reason && !emailResult.sent) { payload.emailError = emailResult.reason; } @@ -3847,28 +4306,23 @@ app.get('/api/users/:id', requireAdmin, async (req, res) => { try { const result = await pool.request() .input('userId', sql.Int, req.params.id) - .query(`SELECT u.*, r.RoleName FROM Users u + .query(`SELECT u.UserId, u.Username, u.Email, u.FullName, u.Role, u.RoleId, + u.Status, u.CreatedDate, u.LastLogin, u.IsActive, + u.EmailVerified, u.EmailVerifiedAt, r.RoleName + FROM Users u LEFT JOIN Roles r ON u.RoleId = r.RoleId WHERE u.UserId = @userId`); if (result.recordset.length > 0) { - const record = result.recordset[0]; - const viewPassword = decryptPasswordForView(record.ViewPassword || ''); - const fallbackPlainPassword = !isBcryptHash(record.Password) ? String(record.Password || '') : ''; - const plainPassword = viewPassword || fallbackPlainPassword; - - const userDetails = { - ...record, - Password: plainPassword, - PasswordAvailable: Boolean(plainPassword) - }; - - res.json({ success: true, data: userDetails }); + res.json({ + success: true, + data: { ...result.recordset[0], PasswordAvailable: false } + }); } else { res.status(404).json({ success: false, message: 'User not found' }); } } catch (err) { - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -3881,8 +4335,14 @@ app.post('/api/users', requireAdmin, async (req, res) => { return res.status(400).json({ success: false, message: 'Username and password are required' }); } + if (!/^[A-Za-z0-9._-]{3,50}$/.test(String(username).trim())) { + return res.status(400).json({ success: false, message: 'Username format is invalid' }); + } + if (String(password).length < 12 || String(password).length > 256) { + return res.status(400).json({ success: false, message: 'Password must be between 12 and 256 characters' }); + } + const hashedPassword = await hashPassword(password); - const viewPassword = encryptPasswordForView(password); const finalRoleId = roleId || 2; // Default to Guest role // Get role name from Roles table @@ -3899,11 +4359,10 @@ app.post('/api/users', requireAdmin, async (req, res) => { .input('fullname', sql.NVarChar, fullname) .input('roleId', sql.Int, finalRoleId) .input('role', sql.NVarChar, roleName) - .input('viewPassword', sql.NVarChar, viewPassword) .query(`IF NOT EXISTS (SELECT * FROM Users WHERE Username = @username) BEGIN - INSERT INTO Users (Username, Password, ViewPassword, Email, FullName, RoleId, Role, IsActive) - VALUES (@username, @password, @viewPassword, @email, @fullname, @roleId, @role, 1); + INSERT INTO Users (Username, Password, Email, FullName, RoleId, Role, IsActive, EmailVerified, EmailVerifiedAt) + VALUES (@username, @password, @email, @fullname, @roleId, @role, 1, 1, DATEADD(HOUR, 7, SYSUTCDATETIME())); SELECT SCOPE_IDENTITY() as UserId END ELSE @@ -3917,7 +4376,7 @@ app.post('/api/users', requireAdmin, async (req, res) => { res.status(400).json({ success: false, message: 'Username already exists' }); } } catch (err) { - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -3927,6 +4386,9 @@ app.put('/api/users/:id', requireAdmin, async (req, res) => { const { email, fullname, roleId, status, isActive, password } = req.body; const nextPassword = typeof password === 'string' ? password.trim() : ''; const shouldUpdatePassword = nextPassword.length > 0; + if (shouldUpdatePassword && (nextPassword.length < 12 || nextPassword.length > 256)) { + return res.status(400).json({ success: false, message: 'Password must be between 12 and 256 characters' }); + } // Get role name from Roles table let roleName = ''; @@ -3948,9 +4410,7 @@ app.put('/api/users/:id', requireAdmin, async (req, res) => { if (shouldUpdatePassword) { const hashedPassword = await hashPassword(nextPassword); - const viewPassword = encryptPasswordForView(nextPassword); request.input('password', sql.NVarChar, hashedPassword); - request.input('viewPassword', sql.NVarChar, viewPassword); } await request.query(`UPDATE Users @@ -3960,33 +4420,51 @@ app.put('/api/users/:id', requireAdmin, async (req, res) => { Role = @role, Status = @status, IsActive = @isActive - ${shouldUpdatePassword ? ', Password = @password, ViewPassword = @viewPassword' : ''} + ${shouldUpdatePassword ? ', Password = @password, ViewPassword = NULL' : ''} WHERE UserId = @userId`); + if (shouldUpdatePassword || !isActive) { + await invalidateUserSessions(Number(req.params.id)); + } + res.json({ success: true, message: shouldUpdatePassword ? 'User updated and password changed' : 'User updated' }); } catch (err) { - res.status(500).json({ success: false, message: err.message }); + console.error('Update user error:', err.message); + res.status(500).json({ success: false, message: 'Unable to update user' }); } }); // Delete user (Admin only) app.delete('/api/users/:id', requireAdmin, async (req, res) => { try { - // Prevent deleting the current admin user (ID = 1) - if (req.params.id === '1') { + const targetUserId = Number(req.params.id); + if (!Number.isInteger(targetUserId) || targetUserId <= 0) { + return res.status(400).json({ success: false, message: 'Invalid user id' }); + } + if (targetUserId === getUserIdFromRequest(req)) { return res.status(400).json({ success: false, - message: 'Cannot delete the primary admin user' + message: 'You cannot delete your own signed-in account' }); } + const adminCheck = await pool.request() + .input('userId', sql.Int, targetUserId) + .query(`SELECT TOP 1 LOWER(ISNULL(Role, '')) AS Role FROM Users WHERE UserId = @userId; + SELECT COUNT(*) AS ActiveAdminCount FROM Users WHERE LOWER(ISNULL(Role, '')) = 'admin' AND IsActive = 1;`); + const targetRole = adminCheck.recordsets?.[0]?.[0]?.Role; + const activeAdminCount = Number(adminCheck.recordsets?.[1]?.[0]?.ActiveAdminCount || 0); + if (targetRole === 'admin' && activeAdminCount <= 1) { + return res.status(400).json({ success: false, message: 'The last active administrator cannot be deleted' }); + } + // Delete associated accounts first await pool.request() - .input('userId', sql.Int, req.params.id) + .input('userId', sql.Int, targetUserId) .query('DELETE FROM Accounts WHERE UserId = @userId'); await pool.request() - .input('userId', sql.Int, req.params.id) + .input('userId', sql.Int, targetUserId) .query(` IF COL_LENGTH('dbo.ConsumableExportHistory', 'RecipientUserId') IS NOT NULL UPDATE ConsumableExportHistory SET RecipientUserId = NULL WHERE RecipientUserId = @userId @@ -4000,12 +4478,13 @@ app.delete('/api/users/:id', requireAdmin, async (req, res) => { // Then delete the user await pool.request() - .input('userId', sql.Int, req.params.id) + .input('userId', sql.Int, targetUserId) .query('DELETE FROM Users WHERE UserId = @userId'); res.json({ success: true, message: 'User deleted' }); } catch (err) { - res.status(500).json({ success: false, message: err.message }); + console.error('Delete user error:', err.message); + res.status(500).json({ success: false, message: 'Unable to delete user' }); } }); @@ -4020,14 +4499,18 @@ app.get('/api/applications', async (req, res) => { .query('SELECT AppId, Name, Type, Status, Icon, Description, Url, CreatedDate, UpdatedDate FROM Applications ORDER BY Name'); res.json({ success: true, data: result.recordset }); } catch (err) { - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); // Create application -app.post('/api/applications', async (req, res) => { +app.post('/api/applications', requireAdmin, async (req, res) => { try { const { name, type, status, icon, description, url } = req.body; + const safeUrl = normalizeOptionalHttpUrl(url); + if (!String(name || '').trim() || safeUrl === null) { + return res.status(400).json({ success: false, message: 'Application name and a valid HTTP(S) URL are required' }); + } const result = await pool.request() .input('name', sql.NVarChar, name) @@ -4035,21 +4518,25 @@ app.post('/api/applications', async (req, res) => { .input('status', sql.NVarChar, status) .input('icon', sql.NVarChar, icon) .input('description', sql.NVarChar, description) - .input('url', sql.NVarChar, url) + .input('url', sql.NVarChar, safeUrl) .query(`INSERT INTO Applications (Name, Type, Status, Icon, Description, Url) VALUES (@name, @type, @status, @icon, @description, @url); SELECT SCOPE_IDENTITY() as AppId`); res.json({ success: true, message: 'Application created', appId: result.recordset[0].AppId }); } catch (err) { - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); // Update application -app.put('/api/applications/:id', async (req, res) => { +app.put('/api/applications/:id', requireAdmin, async (req, res) => { try { const { name, type, status, icon, description, url } = req.body; + const safeUrl = normalizeOptionalHttpUrl(url); + if (!String(name || '').trim() || safeUrl === null) { + return res.status(400).json({ success: false, message: 'Application name and a valid HTTP(S) URL are required' }); + } await pool.request() .input('appId', sql.Int, req.params.id) @@ -4058,7 +4545,7 @@ app.put('/api/applications/:id', async (req, res) => { .input('status', sql.NVarChar, status) .input('icon', sql.NVarChar, icon) .input('description', sql.NVarChar, description) - .input('url', sql.NVarChar, url) + .input('url', sql.NVarChar, safeUrl) .query(`UPDATE Applications SET Name = @name, Type = @type, @@ -4071,12 +4558,12 @@ app.put('/api/applications/:id', async (req, res) => { res.json({ success: true, message: 'Application updated' }); } catch (err) { - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); // Delete application -app.delete('/api/applications/:id', async (req, res) => { +app.delete('/api/applications/:id', requireAdmin, async (req, res) => { try { await pool.request() .input('appId', sql.Int, req.params.id) @@ -4084,7 +4571,7 @@ app.delete('/api/applications/:id', async (req, res) => { res.json({ success: true, message: 'Application deleted' }); } catch (err) { - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -4092,11 +4579,66 @@ app.delete('/api/applications/:id', async (req, res) => { // API ROUTES - Accounts // ========================================== +function accountRecordForResponse(record) { + const storedPassword = String(record?.AccountPassword || ''); + + return { + ...record, + AccountPassword: '', + PasswordAvailable: Boolean(storedPassword) + }; +} + +function currentUserCanAccessAccount(req, ownerUserId) { + return getRequesterRole(req) === 'admin' || Number(ownerUserId) === getUserIdFromRequest(req); +} + +app.get('/api/accounts/:id/secret', credentialRevealRateLimit, async (req, res) => { + try { + const accountId = Number(req.params.id); + if (!Number.isInteger(accountId) || accountId <= 0) { + return res.status(400).json({ success: false, message: 'Invalid account id' }); + } + + const result = await pool.request() + .input('accountId', sql.Int, accountId) + .query('SELECT TOP 1 UserId, AccountPassword FROM Accounts WHERE AccountId = @accountId'); + const account = result.recordset[0]; + if (!account) { + return res.status(404).json({ success: false, message: 'Account not found' }); + } + if (!currentUserCanAccessAccount(req, account.UserId)) { + return res.status(403).json({ success: false, message: 'You cannot reveal credentials assigned to another user' }); + } + + const storedPassword = String(account.AccountPassword || ''); + const password = storedPassword + ? (isEncryptedSensitiveValue(storedPassword) ? decryptSensitiveValue(storedPassword) : storedPassword) + : ''; + if (storedPassword && password === null) { + return res.status(500).json({ success: false, message: 'Stored credential cannot be decrypted' }); + } + + res.json({ success: true, password: password || '' }); + } catch (err) { + console.error('Reveal account password error:', err.message); + res.status(500).json({ success: false, message: 'Unable to reveal stored credential' }); + } +}); + // Get accounts for a user app.get('/api/accounts/user/:userId', async (req, res) => { try { + const requestedUserId = Number(req.params.userId); + if (!Number.isInteger(requestedUserId) || requestedUserId <= 0) { + return res.status(400).json({ success: false, message: 'Invalid user id' }); + } + if (!currentUserCanAccessAccount(req, requestedUserId)) { + return res.status(403).json({ success: false, message: 'You cannot access credentials assigned to another user' }); + } + const result = await pool.request() - .input('userId', sql.Int, req.params.userId) + .input('userId', sql.Int, requestedUserId) .query(`SELECT a.*, app.Name as AppName, app.Type as AppType, u.Username FROM Accounts a JOIN Applications app ON a.AppId = app.AppId @@ -4105,22 +4647,30 @@ app.get('/api/accounts/user/:userId', async (req, res) => { ORDER BY a.CreatedDate DESC`); res.json({ success: true, data: result.recordset }); } catch (err) { - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); // Get all accounts (from all users) app.get('/api/accounts/all', async (req, res) => { try { - const result = await pool.request() + const requesterId = getUserIdFromRequest(req); + const canViewAll = getRequesterRole(req) === 'admin'; + const request = pool.request(); + if (!canViewAll) { + request.input('requesterId', sql.Int, requesterId); + } + const result = await request .query(`SELECT a.*, app.Name as AppName, app.Type as AppType, u.Username, u.FullName FROM Accounts a JOIN Applications app ON a.AppId = app.AppId JOIN Users u ON a.UserId = u.UserId + ${canViewAll ? '' : 'WHERE a.UserId = @requesterId'} ORDER BY a.CreatedDate DESC`); - res.json({ success: true, data: result.recordset }); + res.json({ success: true, data: result.recordset.map(accountRecordForResponse) }); } catch (err) { - res.status(500).json({ success: false, message: err.message }); + console.error('Get all accounts error:', err.message); + res.status(500).json({ success: false, message: 'Unable to load accounts' }); } }); @@ -4128,12 +4678,20 @@ app.get('/api/accounts/all', async (req, res) => { app.post('/api/accounts', async (req, res) => { try { const { userId, appId, accountUsername, accountPassword, email, accessLevel, notes } = req.body; - + const requesterId = getUserIdFromRequest(req); + const targetUserId = getRequesterRole(req) === 'admin' ? Number(userId) : requesterId; + if (!Number.isInteger(targetUserId) || targetUserId <= 0 || !Number.isInteger(Number(appId)) || Number(appId) <= 0) { + return res.status(400).json({ success: false, message: 'User and application are required' }); + } + if (!String(accountUsername || '').trim() || !String(accountPassword || '')) { + return res.status(400).json({ success: false, message: 'Account username and password are required' }); + } + const result = await pool.request() - .input('userId', sql.Int, userId) - .input('appId', sql.Int, appId) - .input('accountUsername', sql.NVarChar, accountUsername) - .input('accountPassword', sql.NVarChar, accountPassword) + .input('userId', sql.Int, targetUserId) + .input('appId', sql.Int, Number(appId)) + .input('accountUsername', sql.NVarChar, String(accountUsername).trim().slice(0, 100)) + .input('accountPassword', sql.NVarChar(2048), encryptSensitiveValue(String(accountPassword))) .input('email', sql.NVarChar, email) .input('accessLevel', sql.NVarChar, accessLevel) .input('notes', sql.NVarChar, notes) @@ -4143,7 +4701,7 @@ app.post('/api/accounts', async (req, res) => { res.json({ success: true, message: 'Account created', accountId: result.recordset[0].AccountId }); } catch (err) { - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -4151,13 +4709,32 @@ app.post('/api/accounts', async (req, res) => { app.put('/api/accounts/:id', async (req, res) => { try { const { userId, appId, accountUsername, accountPassword, email, accessLevel, notes } = req.body; + const accountId = Number(req.params.id); + if (!Number.isInteger(accountId) || accountId <= 0) { + return res.status(400).json({ success: false, message: 'Invalid account id' }); + } + + const existingResult = await pool.request() + .input('accountId', sql.Int, accountId) + .query('SELECT TOP 1 UserId, AccountPassword FROM Accounts WHERE AccountId = @accountId'); + const existing = existingResult.recordset[0]; + if (!existing) { + return res.status(404).json({ success: false, message: 'Account not found' }); + } + if (!currentUserCanAccessAccount(req, existing.UserId)) { + return res.status(403).json({ success: false, message: 'You cannot update credentials assigned to another user' }); + } + + const targetUserId = getRequesterRole(req) === 'admin' ? Number(userId) : getUserIdFromRequest(req); + const nextPassword = String(accountPassword || ''); + const storedPassword = nextPassword ? encryptSensitiveValue(nextPassword) : existing.AccountPassword; await pool.request() - .input('accountId', sql.Int, req.params.id) - .input('userId', sql.Int, userId) - .input('appId', sql.Int, appId) + .input('accountId', sql.Int, accountId) + .input('userId', sql.Int, targetUserId) + .input('appId', sql.Int, Number(appId)) .input('accountUsername', sql.NVarChar, accountUsername) - .input('accountPassword', sql.NVarChar, accountPassword) + .input('accountPassword', sql.NVarChar(2048), storedPassword) .input('email', sql.NVarChar, email) .input('accessLevel', sql.NVarChar, accessLevel) .input('notes', sql.NVarChar, notes) @@ -4174,20 +4751,35 @@ app.put('/api/accounts/:id', async (req, res) => { res.json({ success: true, message: 'Account updated' }); } catch (err) { - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); // Delete account app.delete('/api/accounts/:id', async (req, res) => { try { + const accountId = Number(req.params.id); + if (!Number.isInteger(accountId) || accountId <= 0) { + return res.status(400).json({ success: false, message: 'Invalid account id' }); + } + const existingResult = await pool.request() + .input('accountId', sql.Int, accountId) + .query('SELECT TOP 1 UserId FROM Accounts WHERE AccountId = @accountId'); + const existing = existingResult.recordset[0]; + if (!existing) { + return res.status(404).json({ success: false, message: 'Account not found' }); + } + if (!currentUserCanAccessAccount(req, existing.UserId)) { + return res.status(403).json({ success: false, message: 'You cannot delete credentials assigned to another user' }); + } + await pool.request() - .input('accountId', sql.Int, req.params.id) + .input('accountId', sql.Int, accountId) .query('DELETE FROM Accounts WHERE AccountId = @accountId'); res.json({ success: true, message: 'Account deleted' }); } catch (err) { - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -4215,7 +4807,7 @@ app.get('/api/asset-departments', async (req, res) => { res.json({ success: true, data: result.recordset }); } catch (err) { - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -4258,7 +4850,7 @@ app.post('/api/asset-departments', requireAssetOrAdmin, async (req, res) => { return res.status(409).json({ success: false, message: 'Phong ban da ton tai' }); } - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -4348,7 +4940,7 @@ app.put('/api/asset-departments/:id', requireAssetOrAdmin, async (req, res) => { return res.status(409).json({ success: false, message: 'Phong ban da ton tai' }); } - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -4405,7 +4997,7 @@ app.delete('/api/asset-departments/:id', requireAssetOrAdmin, async (req, res) = throw transactionErr; } } catch (err) { - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -4433,7 +5025,7 @@ app.get('/api/asset-projects', async (req, res) => { res.json({ success: true, data: result.recordset }); } catch (err) { - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -4476,7 +5068,7 @@ app.post('/api/asset-projects', requireAssetOrAdmin, async (req, res) => { return res.status(409).json({ success: false, message: 'Du an da ton tai' }); } - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -4566,7 +5158,7 @@ app.put('/api/asset-projects/:id', requireAssetOrAdmin, async (req, res) => { return res.status(409).json({ success: false, message: 'Du an da ton tai' }); } - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -4623,7 +5215,7 @@ app.delete('/api/asset-projects/:id', requireAssetOrAdmin, async (req, res) => { throw transactionErr; } } catch (err) { - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -4633,7 +5225,7 @@ app.delete('/api/asset-projects/:id', requireAssetOrAdmin, async (req, res) => { app.get('/api/asset-borrows', async (req, res) => { try { - const requesterRole = normalizeRole(req.headers['x-user-role'] || req.query.userRole); + const requesterRole = getRequesterRole(req); const requesterId = getUserIdFromRequest(req); const canManageRequests = requesterRole === 'admin' || requesterRole === 'asset'; @@ -4710,13 +5302,13 @@ app.get('/api/asset-borrows', async (req, res) => { res.json({ success: true, data: result.recordset }); } catch (err) { - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); app.get('/api/asset-borrows/:id/history', async (req, res) => { try { - const requesterRole = normalizeRole(req.headers['x-user-role'] || req.query.userRole); + const requesterRole = getRequesterRole(req); const requesterId = getUserIdFromRequest(req); const canManageRequests = requesterRole === 'admin' || requesterRole === 'asset'; const borrowId = Number(req.params.id); @@ -4883,7 +5475,7 @@ app.get('/api/asset-borrows/:id/history', async (req, res) => { } }); } catch (err) { - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -5118,7 +5710,7 @@ app.post('/api/asset-borrows', async (req, res) => { // Ignore rollback errors when transaction already finished. } } - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -5126,7 +5718,7 @@ app.post('/api/asset-borrows/:id/return', async (req, res) => { const transaction = new sql.Transaction(pool); try { - const requesterRole = normalizeRole(req.headers['x-user-role'] || req.query.userRole); + const requesterRole = getRequesterRole(req); const requesterId = getUserIdFromRequest(req); const canManageRequests = requesterRole === 'admin' || requesterRole === 'asset'; const borrowId = Number(req.params.id); @@ -5279,7 +5871,7 @@ app.post('/api/asset-borrows/:id/return', async (req, res) => { } catch (rollbackErr) { // Ignore rollback errors when transaction already finished. } - return res.status(500).json({ success: false, message: err.message }); + return sendInternalError(res, err); } }); @@ -5293,7 +5885,8 @@ app.post('/api/asset-borrows/:id/process', requireAssetOrAdmin, async (req, res) const processedBy = getUserIdFromRequest(req); const processorName = String( await getUserDisplayNameById(processedBy) - || req.headers['x-user-role'] + || req.user?.FullName + || req.user?.Username || 'Asset/Admin' ).trim(); @@ -5648,7 +6241,7 @@ app.post('/api/asset-borrows/:id/process', requireAssetOrAdmin, async (req, res) } catch (rollbackErr) { // Ignore rollback errors when transaction already finished. } - return res.status(500).json({ success: false, message: err.message }); + return sendInternalError(res, err); } }); @@ -5656,7 +6249,7 @@ app.delete('/api/asset-borrows/:id', async (req, res) => { const transaction = new sql.Transaction(pool); try { - const requesterRole = normalizeRole(req.headers['x-user-role'] || req.query.userRole); + const requesterRole = getRequesterRole(req); const requesterId = getUserIdFromRequest(req); const canManageRequests = requesterRole === 'admin' || requesterRole === 'asset'; const borrowId = Number(req.params.id); @@ -5728,7 +6321,7 @@ app.delete('/api/asset-borrows/:id', async (req, res) => { } catch (rollbackErr) { // Ignore rollback errors when transaction already finished. } - return res.status(500).json({ success: false, message: err.message }); + return sendInternalError(res, err); } }); @@ -5878,7 +6471,7 @@ async function applyConsumableReturn(transaction, { app.get('/api/consumable-borrows', async (req, res) => { try { - const requesterRole = normalizeRole(req.headers['x-user-role'] || req.query.userRole); + const requesterRole = getRequesterRole(req); const requesterId = getUserIdFromRequest(req); const canManageRequests = requesterRole === 'admin' || requesterRole === 'asset'; @@ -5920,7 +6513,8 @@ app.get('/api/consumable-borrows', async (req, res) => { res.json({ success: true, data: Array.isArray(result.recordset) ? result.recordset : [] }); } catch (err) { - res.status(500).json({ success: false, message: err.message }); + console.error('Create account error:', err.message); + res.status(500).json({ success: false, message: 'Unable to create account' }); } }); @@ -6011,7 +6605,7 @@ app.post('/api/consumable-borrows', async (req, res) => { }); } catch (err) { console.error('Create consumable borrow request error:', err.message); - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -6144,7 +6738,7 @@ app.post('/api/consumable-exports/:id/return-request', async (req, res) => { }); } catch (err) { console.error('Create consumable return request error:', err.message); - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -6157,7 +6751,8 @@ app.post('/api/consumable-borrows/:id/process', requireAssetOrAdmin, async (req, const rejectReason = String(req.body?.rejectReason || '').trim() || null; const processedBy = getUserIdFromRequest(req); const processedByName = await getUserDisplayNameById(processedBy) - || String(req.headers['x-user-role'] || '').trim() + || req.user?.FullName + || req.user?.Username || 'Unknown'; if (!Number.isInteger(borrowRequestId) || borrowRequestId <= 0) { @@ -6360,14 +6955,18 @@ app.post('/api/consumable-borrows/:id/process', requireAssetOrAdmin, async (req, // Ignore rollback error, respond original error below. } } - res.status(Number(err.statusCode) || 500).json({ success: false, message: err.message }); + const statusCode = Number(err.statusCode) || 500; + if (statusCode >= 400 && statusCode < 500) { + return res.status(statusCode).json({ success: false, message: err.message }); + } + sendInternalError(res, err); } }); app.delete('/api/consumable-borrows/:id', async (req, res) => { try { const borrowRequestId = Number(req.params.id); - const requesterRole = normalizeRole(req.headers['x-user-role'] || req.query.userRole); + const requesterRole = getRequesterRole(req); const requesterId = getUserIdFromRequest(req); const canManageRequests = requesterRole === 'admin' || requesterRole === 'asset'; @@ -6394,7 +6993,8 @@ app.delete('/api/consumable-borrows/:id', async (req, res) => { const deletedType = String(result.recordset?.[0]?.RequestType || 'borrow').trim().toLowerCase(); res.json({ success: true, message: deletedType === 'return' ? 'Đã hủy đơn trả' : 'Đã hủy đơn mượn' }); } catch (err) { - res.status(500).json({ success: false, message: err.message }); + console.error('Update account error:', err.message); + res.status(500).json({ success: false, message: 'Unable to update account' }); } }); @@ -6524,9 +7124,10 @@ app.get('/api/consumables', async (req, res) => { ORDER BY UpdatedDate DESC, ConsumableName ASC `); - res.json({ success: true, data: result.recordset }); + res.json({ success: true, data: result.recordset.map(accountRecordForResponse) }); } catch (err) { - res.status(500).json({ success: false, message: err.message }); + console.error('Get user accounts error:', err.message); + res.status(500).json({ success: false, message: 'Unable to load accounts' }); } }); @@ -6578,7 +7179,7 @@ app.post('/api/consumables', requireAssetOrAdmin, async (req, res) => { return res.status(409).json({ success: false, message: 'Consumable code already exists' }); } - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -6638,7 +7239,7 @@ app.put('/api/consumables/:id', requireAssetOrAdmin, async (req, res) => { return res.status(409).json({ success: false, message: 'Consumable code already exists' }); } - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -6659,14 +7260,15 @@ app.delete('/api/consumables/:id', requireAssetOrAdmin, async (req, res) => { res.json({ success: true, message: 'Consumable deleted' }); } catch (err) { - res.status(500).json({ success: false, message: err.message }); + console.error('Delete account error:', err.message); + res.status(500).json({ success: false, message: 'Unable to delete account' }); } }); app.get('/api/consumable-export-history', async (req, res) => { try { const limit = Math.min(parsePositiveInteger(req.query.limit, 300), 2000); - const requesterRole = normalizeRole(req.headers['x-user-role'] || req.query.userRole); + const requesterRole = getRequesterRole(req); const requesterId = getUserIdFromRequest(req); const canManageAssets = requesterRole === 'admin' || requesterRole === 'asset'; if (!canManageAssets && (!Number.isInteger(requesterId) || requesterId <= 0)) { @@ -6778,7 +7380,8 @@ app.get('/api/consumable-export-history', async (req, res) => { data: Array.isArray(result.recordset) ? result.recordset : [] }); } catch (err) { - res.status(500).json({ success: false, message: err.message }); + console.error('Get user details error:', err.message); + res.status(500).json({ success: false, message: 'Unable to load user details' }); } }); @@ -6796,7 +7399,7 @@ app.post('/api/consumables/:id/export', requireAssetOrAdmin, async (req, res) => const projectName = String(req.body?.projectName || req.body?.project || '').trim(); const exportNote = String(req.body?.note || '').trim() || null; const createdBy = getUserIdFromRequest(req); - const exportedByName = await getUserDisplayNameById(createdBy) || String(req.headers['x-user-role'] || '').trim() || 'Unknown'; + const exportedByName = await getUserDisplayNameById(createdBy) || req.user?.FullName || req.user?.Username || 'Unknown'; const exportedDate = new Date(); if (!Number.isInteger(consumableId) || consumableId <= 0) { @@ -6999,7 +7602,7 @@ app.post('/api/consumables/:id/export', requireAssetOrAdmin, async (req, res) => // Ignore rollback error, respond original error below. } } - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -7012,7 +7615,8 @@ app.post('/api/consumable-exports/:id/return', requireAssetOrAdmin, async (req, const returnNote = String(req.body?.note || '').trim() || null; const createdBy = getUserIdFromRequest(req); const returnedByName = await getUserDisplayNameById(createdBy) - || String(req.headers['x-user-role'] || '').trim() + || req.user?.FullName + || req.user?.Username || 'Unknown'; if (!Number.isInteger(exportHistoryId) || exportHistoryId <= 0) { @@ -7048,7 +7652,11 @@ app.post('/api/consumable-exports/:id/return', requireAssetOrAdmin, async (req, // Ignore rollback error, respond original error below. } } - res.status(Number(err.statusCode) || 500).json({ success: false, message: err.message }); + const statusCode = Number(err.statusCode) || 500; + if (statusCode >= 400 && statusCode < 500) { + return res.status(statusCode).json({ success: false, message: err.message }); + } + sendInternalError(res, err); } }); @@ -7176,7 +7784,7 @@ app.post('/api/consumables/import', requireAssetOrAdmin, upload.single('file'), } catch (rollbackErr) { // Ignore rollback errors if transaction is already completed. } - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -7200,7 +7808,7 @@ app.get('/api/assets', async (req, res) => { res.json({ success: true, data: result.recordset }); } catch (err) { - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -7282,7 +7890,7 @@ app.get('/api/assets/search', async (req, res) => { res.json({ success: true, data: rows, hasMore, total: totalCount }); } catch (err) { - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -7312,7 +7920,7 @@ app.get('/api/assets/:id', async (req, res) => { res.json({ success: true, data: result.recordset[0] }); } catch (err) { - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -7345,7 +7953,7 @@ app.get('/api/asset-export-history', requireAssetOrAdmin, async (req, res) => { data: Array.isArray(result.recordset) ? result.recordset : [] }); } catch (err) { - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -7391,7 +7999,8 @@ app.get('/api/asset-damage-disposal-history', requireAssetOrAdmin, async (req, r data: Array.isArray(result.recordset) ? result.recordset : [] }); } catch (err) { - res.status(500).json({ success: false, message: err.message }); + console.error('Create user error:', err.message); + res.status(500).json({ success: false, message: 'Unable to create user' }); } }); @@ -7630,7 +8239,7 @@ app.post('/api/assets/:id/damage-disposal', requireAssetOrAdmin, async (req, res // Ignore rollback error, respond original error below. } } - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -7643,7 +8252,7 @@ app.post('/api/assets/:id/export', requireAssetOrAdmin, async (req, res) => { const projectName = String(req.body?.projectName || '').trim() || null; const exportNote = String(req.body?.note || '').trim() || null; const createdBy = getUserIdFromRequest(req); - const exportedByName = await getUserDisplayNameById(createdBy) || String(req.headers['x-user-role'] || '').trim() || 'Unknown'; + const exportedByName = await getUserDisplayNameById(createdBy) || req.user?.FullName || req.user?.Username || 'Unknown'; const exportedDate = new Date(); if (!Number.isInteger(assetId) || assetId <= 0) { @@ -7834,7 +8443,7 @@ app.post('/api/assets/:id/export', requireAssetOrAdmin, async (req, res) => { // Ignore rollback error, respond original error below. } } - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -7898,7 +8507,7 @@ app.post('/api/assets', requireAssetOrAdmin, async (req, res) => { return res.status(409).json({ success: false, message: 'Asset code already exists' }); } - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -7974,7 +8583,7 @@ app.put('/api/assets/:id', requireAssetOrAdmin, async (req, res) => { return res.status(409).json({ success: false, message: 'Asset code already exists' }); } - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -8018,7 +8627,7 @@ app.delete('/api/assets/:id', requireAssetOrAdmin, async (req, res) => { } catch (rollbackErr) { // Ignore rollback errors when transaction already finished. } - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -8224,7 +8833,7 @@ app.post('/api/assets/import', requireAssetOrAdmin, upload.single('file'), async } catch (rollbackErr) { // Ignore rollback errors if transaction is already completed. } - res.status(500).json({ success: false, message: err.message }); + sendInternalError(res, err); } }); @@ -8233,7 +8842,7 @@ app.post('/api/assets/import', requireAssetOrAdmin, upload.single('file'), async // ========================================== // Get database information -app.get('/api/database/info', async (req, res) => { +app.get('/api/database/info', requireAdmin, async (req, res) => { try { const tables = await pool.request().query(` SELECT TABLE_NAME as TableName, @@ -8251,8 +8860,7 @@ app.get('/api/database/info', async (req, res) => { res.json({ success: true, - database: 'AccManager', - server: '172.20.235.176', + database: DB_NAME, tables: tables.recordset, statistics: { users: users.recordset[0].Count, @@ -8263,22 +8871,21 @@ app.get('/api/database/info', async (req, res) => { } }); } catch (err) { - res.status(500).json({ success: false, message: err.message }); + console.error('Database info error:', err.message); + res.status(500).json({ success: false, message: 'Unable to load database information' }); } }); -// Health check -app.get('/api/health', (req, res) => { - res.json({ status: 'OK', database: 'Connected' }); -}); - // ========================================== // Error Handling // ========================================== app.use((err, req, res, next) => { - console.error(err); - res.status(500).json({ success: false, message: err.message }); + console.error('Unhandled request error:', err.message); + if (err instanceof multer.MulterError || err.message === 'Only .xls and .xlsx files are allowed') { + return res.status(400).json({ success: false, message: err.message }); + } + res.status(500).json({ success: false, message: 'Internal server error' }); }); // ========================================== @@ -8294,8 +8901,7 @@ async function startServer() { console.log(`AccManager Backend Server`); console.log(`========================================`); console.log(`[OK] Server running on http://localhost:${PORT}`); - console.log('[OK] Database: AccManager'); - console.log('[OK] Default admin: admin / admin'); + console.log(`[OK] Database: ${DB_NAME}`); console.log(`\nAPI Endpoints:`); console.log(` POST /api/auth/login`); console.log(` GET /api/database/info`); @@ -8320,4 +8926,15 @@ process.on('SIGINT', async () => { process.exit(0); }); -startServer(); +if (require.main === module) { + startServer(); +} + +module.exports = { + app, + startServer, + encryptSensitiveValue, + decryptSensitiveValue, + hashSessionToken, + normalizeOptionalHttpUrl +}; diff --git a/database/setup.sql b/database/setup.sql index e8cc5d3..032ccd3 100644 --- a/database/setup.sql +++ b/database/setup.sql @@ -1,7 +1,6 @@ -- =========================================== -- SQL Server Setup Script for AccManager -- Database: AccManager --- Server: 172.20.235.176 -- =========================================== -- Create Database @@ -39,6 +38,24 @@ BEGIN PRINT 'Table Users created successfully.'; END +-- Server-side authentication sessions. Only SHA-256 token hashes are stored. +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AuthSessions') +BEGIN + CREATE TABLE AuthSessions ( + SessionId BIGINT PRIMARY KEY IDENTITY(1,1), + TokenHash CHAR(64) UNIQUE NOT NULL, + UserId INT NOT NULL, + ExpiresAt DATETIME2 NOT NULL, + CreatedAt DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(), + UserAgent NVARCHAR(500), + IpAddress NVARCHAR(64), + FOREIGN KEY (UserId) REFERENCES Users(UserId) ON DELETE CASCADE + ); + CREATE INDEX IX_AuthSessions_UserId ON AuthSessions(UserId); + CREATE INDEX IX_AuthSessions_ExpiresAt ON AuthSessions(ExpiresAt); + PRINT 'Table AuthSessions created successfully.'; +END + -- =========================================== -- 2. CREATE APPLICATIONS TABLE -- =========================================== @@ -67,7 +84,7 @@ BEGIN UserId INT NOT NULL, AppId INT NOT NULL, AccountUsername NVARCHAR(100), - AccountPassword NVARCHAR(255), + AccountPassword NVARCHAR(2048), Email NVARCHAR(100), AccessLevel NVARCHAR(50), Status NVARCHAR(20) DEFAULT 'Active', @@ -80,6 +97,11 @@ BEGIN PRINT 'Table Accounts created successfully.'; END +IF COL_LENGTH('dbo.Accounts', 'AccountPassword') IS NOT NULL +BEGIN + ALTER TABLE Accounts ALTER COLUMN AccountPassword NVARCHAR(2048) NULL; +END + -- =========================================== -- 4. CREATE ASSET INVENTORY TABLE -- =========================================== diff --git a/docker-compose.image.yml b/docker-compose.image.yml index a8b0e23..54ea2ee 100644 --- a/docker-compose.image.yml +++ b/docker-compose.image.yml @@ -10,19 +10,31 @@ services: PORT: 3000 TZ: ${TZ:-Asia/Ho_Chi_Minh} APP_TIME_ZONE: ${APP_TIME_ZONE:-Asia/Ho_Chi_Minh} - DB_SERVER: ${DB_SERVER:-172.20.235.176} - DB_USER: ${DB_USER:-sa} - DB_PASSWORD: ${DB_PASSWORD:-changeme} + DB_SERVER: ${DB_SERVER:?DB_SERVER is required} + DB_USER: ${DB_USER:?DB_USER is required} + DB_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required} DB_NAME: ${DB_NAME:-AccManager} DB_ENCRYPT: ${DB_ENCRYPT:-false} DB_TRUST_CERTIFICATE: ${DB_TRUST_CERTIFICATE:-true} DB_CONNECT_TIMEOUT: ${DB_CONNECT_TIMEOUT:-30000} BCRYPT_ROUNDS: ${BCRYPT_ROUNDS:-12} - APP_BASE_URL: ${APP_BASE_URL:-http://localhost:3000} + DATA_ENCRYPTION_SECRET: ${DATA_ENCRYPTION_SECRET:?DATA_ENCRYPTION_SECRET is required} + APP_BASE_URL: ${APP_BASE_URL:?APP_BASE_URL is required} + CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-} + COOKIE_SECURE: ${COOKIE_SECURE:-true} + SESSION_TTL_HOURS: ${SESSION_TTL_HOURS:-12} + REMEMBER_SESSION_TTL_DAYS: ${REMEMBER_SESSION_TTL_DAYS:-14} + ALLOW_SELF_REGISTRATION: ${ALLOW_SELF_REGISTRATION:-false} + TRUST_PROXY_HOPS: ${TRUST_PROXY_HOPS:-1} + INITIAL_ADMIN_USERNAME: ${INITIAL_ADMIN_USERNAME:-admin} + INITIAL_ADMIN_PASSWORD: ${INITIAL_ADMIN_PASSWORD:-} + INITIAL_ADMIN_EMAIL: ${INITIAL_ADMIN_EMAIL:-admin@accmanager.local} SMTP_HOST: ${SMTP_HOST:-} SMTP_PORT: ${SMTP_PORT:-587} SMTP_SECURE: ${SMTP_SECURE:-false} + SMTP_REQUIRE_TLS: ${SMTP_REQUIRE_TLS:-true} SMTP_USER: ${SMTP_USER:-} SMTP_PASS: ${SMTP_PASS:-} SMTP_FROM: ${SMTP_FROM:-} EMAIL_VERIFY_TOKEN_TTL_MINUTES: ${EMAIL_VERIFY_TOKEN_TTL_MINUTES:-30} + PASSWORD_RESET_TOKEN_TTL_MINUTES: ${PASSWORD_RESET_TOKEN_TTL_MINUTES:-30} diff --git a/docker-compose.yml b/docker-compose.yml index 7285f39..e066a3b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,19 +12,31 @@ services: PORT: 3000 TZ: ${TZ:-Asia/Ho_Chi_Minh} APP_TIME_ZONE: ${APP_TIME_ZONE:-Asia/Ho_Chi_Minh} - DB_SERVER: ${DB_SERVER:-172.20.235.176} - DB_USER: ${DB_USER:-sa} - DB_PASSWORD: ${DB_PASSWORD:-changeme} + DB_SERVER: ${DB_SERVER:?DB_SERVER is required} + DB_USER: ${DB_USER:?DB_USER is required} + DB_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required} DB_NAME: ${DB_NAME:-AccManager} DB_ENCRYPT: ${DB_ENCRYPT:-false} DB_TRUST_CERTIFICATE: ${DB_TRUST_CERTIFICATE:-true} DB_CONNECT_TIMEOUT: ${DB_CONNECT_TIMEOUT:-30000} BCRYPT_ROUNDS: ${BCRYPT_ROUNDS:-12} - APP_BASE_URL: ${APP_BASE_URL:-http://localhost:3000} + DATA_ENCRYPTION_SECRET: ${DATA_ENCRYPTION_SECRET:?DATA_ENCRYPTION_SECRET is required} + APP_BASE_URL: ${APP_BASE_URL:?APP_BASE_URL is required} + CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-} + COOKIE_SECURE: ${COOKIE_SECURE:-true} + SESSION_TTL_HOURS: ${SESSION_TTL_HOURS:-12} + REMEMBER_SESSION_TTL_DAYS: ${REMEMBER_SESSION_TTL_DAYS:-14} + ALLOW_SELF_REGISTRATION: ${ALLOW_SELF_REGISTRATION:-false} + TRUST_PROXY_HOPS: ${TRUST_PROXY_HOPS:-1} + INITIAL_ADMIN_USERNAME: ${INITIAL_ADMIN_USERNAME:-admin} + INITIAL_ADMIN_PASSWORD: ${INITIAL_ADMIN_PASSWORD:-} + INITIAL_ADMIN_EMAIL: ${INITIAL_ADMIN_EMAIL:-admin@accmanager.local} SMTP_HOST: ${SMTP_HOST:-} SMTP_PORT: ${SMTP_PORT:-587} SMTP_SECURE: ${SMTP_SECURE:-false} + SMTP_REQUIRE_TLS: ${SMTP_REQUIRE_TLS:-true} SMTP_USER: ${SMTP_USER:-} SMTP_PASS: ${SMTP_PASS:-} SMTP_FROM: ${SMTP_FROM:-} EMAIL_VERIFY_TOKEN_TTL_MINUTES: ${EMAIL_VERIFY_TOKEN_TTL_MINUTES:-30} + PASSWORD_RESET_TOKEN_TTL_MINUTES: ${PASSWORD_RESET_TOKEN_TTL_MINUTES:-30} diff --git a/package-lock.json b/package-lock.json index f1ed615..14f3500 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,11 +12,13 @@ "bcrypt": "^6.0.0", "cors": "^2.8.5", "dotenv": "^16.3.1", - "express": "^4.18.2", - "mssql": "^9.1.1", - "multer": "^2.1.1", - "nodemailer": "^8.0.4", - "xlsx": "^0.18.5" + "express": "^5.2.1", + "express-rate-limit": "^8.6.0", + "helmet": "^8.3.0", + "mssql": "^12.7.0", + "multer": "^2.2.0", + "nodemailer": "^9.0.3", + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz" }, "devDependencies": { "@tailwindcss/container-queries": "^0.1.1", @@ -25,6 +27,9 @@ "nodemon": "^3.0.1", "postcss": "^8.4.38", "tailwindcss": "^3.4.13" + }, + "engines": { + "node": ">=22.0.0" } }, "node_modules/@alloc/quick-lru": { @@ -41,50 +46,38 @@ } }, "node_modules/@azure-rest/core-client": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@azure-rest/core-client/-/core-client-2.5.1.tgz", - "integrity": "sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@azure-rest/core-client/-/core-client-2.8.0.tgz", + "integrity": "sha512-F1ybHeN+++QhyFCF/ehLUEvrOB6fehPdFBFtGdj0C3B2lpQ9zkPiO5JDgsqc6IfjuUe6b3dAbXK0a7+VgSGfhw==", "license": "MIT", "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", - "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-rest-pipeline": "^1.24.0", "@azure/core-tracing": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.0.0" } }, - "node_modules/@azure-rest/core-client/node_modules/@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "node_modules/@azure/abort-controller": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.2.0.tgz", + "integrity": "sha512-fNAjWnA/nZ2jz31kxR/AqRaUT8ewHBw/WuBIosK0moMy1C9e5ValbDfFdIxJzVOOYaYkV/b2F1S4H/aHiqfVQg==", "license": "MIT", "dependencies": { "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@azure/abort-controller": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", - "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", - "license": "MIT", - "dependencies": { - "tslib": "^2.2.0" - }, - "engines": { - "node": ">=12.0.0" + "node": ">=22.0.0" } }, "node_modules/@azure/core-auth": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", - "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.11.0.tgz", + "integrity": "sha512-IUZydyTUkDnYdstOW9pFOOUQlBjAepK5teihDE3x6yxsPJs/hsAaaYpeGxdxrgtOiJbBKSjKW7MDk7AEhb4LRg==", "license": "MIT", "dependencies": { "@azure/abort-controller": "^2.1.2", @@ -92,25 +85,13 @@ "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@azure/core-auth/node_modules/@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "node": ">=22.0.0" } }, "node_modules/@azure/core-client": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", - "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.11.0.tgz", + "integrity": "sha512-JjQWO6akOck45PH/XBrxzsQGAiKrfFl4m5iggJ0ItMIz5omRufOXWpqCPpdjKN3vKDzlSUvFjaMb7Zwf0gvAdA==", "license": "MIT", "dependencies": { "@azure/abort-controller": "^2.1.2", @@ -122,47 +103,7 @@ "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@azure/core-client/node_modules/@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@azure/core-http-compat": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.3.2.tgz", - "integrity": "sha512-Tf6ltdKzOJEgxZeWLCjMxrxbodB/ZeCbzzA1A2qHbhzAjzjHoBVSUeSl/baT/oHAxhc4qdqVaDKnc2+iE932gw==", - "license": "MIT", - "dependencies": { - "@azure/abort-controller": "^2.1.2" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@azure/core-client": "^1.10.0", - "@azure/core-rest-pipeline": "^1.22.0" - } - }, - "node_modules/@azure/core-http-compat/node_modules/@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "node": ">=22.0.0" } }, "node_modules/@azure/core-lro": { @@ -180,34 +121,22 @@ "node": ">=18.0.0" } }, - "node_modules/@azure/core-lro/node_modules/@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@azure/core-paging": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz", - "integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.7.0.tgz", + "integrity": "sha512-7GEAoIsaoBr6KELNRb8nypowCqvk8dnCHFCYg4XD4lOQGY2GqjQg5IhkRjyBFRO18CGSMq05PaNqSOE9GQro3g==", "license": "MIT", "dependencies": { "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=22.0.0" } }, "node_modules/@azure/core-rest-pipeline": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.23.0.tgz", - "integrity": "sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ==", + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.25.0.tgz", + "integrity": "sha512-bMs8ekJLjX8wPV+9IPBges1SLPyuDtE9g5gLDWOpxzKcoOFQnpLGkbcT1tdw3FaAmDS1gnPmMmJ6y/T5B96kIA==", "license": "MIT", "dependencies": { "@azure/abort-controller": "^2.1.2", @@ -219,37 +148,25 @@ "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@azure/core-rest-pipeline/node_modules/@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "node": ">=22.0.0" } }, "node_modules/@azure/core-tracing": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", - "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.4.0.tgz", + "integrity": "sha512-eGwxD0AtncrxeBM4tG8R55Pc3rdX1hNW2WibJAgYpCVA6E93mvvVH+LcssoVjOBrSKWS55yEIHsk0X8ctHmfOQ==", "license": "MIT", "dependencies": { "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.0.0" } }, "node_modules/@azure/core-util": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", - "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.14.0.tgz", + "integrity": "sha512-9n2pWK61veAuN0V20t9lOuoV4CFMdyAZ1ygZzvBGk/pBBJRib/PjL9PLXa/aI2CcPpyHfqVsxxqLCYl6uZlfDw==", "license": "MIT", "dependencies": { "@azure/abort-controller": "^2.1.2", @@ -257,57 +174,40 @@ "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@azure/core-util/node_modules/@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "node": ">=22.0.0" } }, "node_modules/@azure/identity": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-2.1.0.tgz", - "integrity": "sha512-BPDz1sK7Ul9t0l9YKLEa8PHqWU4iCfhGJ+ELJl6c8CP3TpJt2urNCbm0ZHsthmxRsYoMPbz2Dvzj30zXZVmAFw==", - "license": "MIT", - "dependencies": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-auth": "^1.3.0", - "@azure/core-client": "^1.4.0", - "@azure/core-rest-pipeline": "^1.1.0", - "@azure/core-tracing": "^1.0.0", - "@azure/core-util": "^1.0.0", - "@azure/logger": "^1.0.0", - "@azure/msal-browser": "^2.26.0", - "@azure/msal-common": "^7.0.0", - "@azure/msal-node": "^1.10.0", - "events": "^3.0.0", - "jws": "^4.0.0", - "open": "^8.0.0", - "stoppable": "^1.1.0", - "tslib": "^2.2.0", - "uuid": "^8.3.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@azure/keyvault-common": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@azure/keyvault-common/-/keyvault-common-2.0.0.tgz", - "integrity": "sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w==", + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.1.tgz", + "integrity": "sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw==", "license": "MIT", "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^5.5.0", + "@azure/msal-node": "^5.1.0", + "open": "^10.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/keyvault-common": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-common/-/keyvault-common-2.1.0.tgz", + "integrity": "sha512-aCDidWuKY06LWQ4x7/8TIXK6iRqTaRWRL3t7T+LC+j1b07HtoIsOxP/tU90G4jCSBn5TAyUTCtA4MS/y5Hudaw==", + "license": "MIT", + "dependencies": { + "@azure-rest/core-client": "^2.3.3", "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.3.0", - "@azure/core-client": "^1.5.0", "@azure/core-rest-pipeline": "^1.8.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.10.0", @@ -315,121 +215,76 @@ "tslib": "^2.2.0" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@azure/keyvault-common/node_modules/@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@azure/keyvault-keys": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.10.0.tgz", - "integrity": "sha512-eDT7iXoBTRZ2n3fLiftuGJFD+yjkiB1GNqzU2KbY1TLYeXeSPVTVgn2eJ5vmRTZ11978jy2Kg2wI7xa9Tyr8ag==", + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.10.2.tgz", + "integrity": "sha512-VmUSLbXRAbSzDD8grXHGPaknYs0SKr3yuf6U+d4XMpX4XuVYskNqbTTwXce0zR1LyxfTZm9rWEBcvs3vdYwCmQ==", "license": "MIT", "dependencies": { "@azure-rest/core-client": "^2.3.3", "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", - "@azure/core-http-compat": "^2.2.0", "@azure/core-lro": "^2.7.2", "@azure/core-paging": "^1.6.2", "@azure/core-rest-pipeline": "^1.19.0", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", - "@azure/keyvault-common": "^2.0.0", + "@azure/keyvault-common": "^2.1.0", "@azure/logger": "^1.1.4", "tslib": "^2.8.1" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@azure/keyvault-keys/node_modules/@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@azure/logger": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", - "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.4.0.tgz", + "integrity": "sha512-rbAE25KUfjU/s3XHUdJgceoCP5dEOpMx85J04kF+QMdta73XkuG9JGHHinch+XIoKpBdqljin+KqURpJriSzLA==", "license": "MIT", "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.0.0" } }, "node_modules/@azure/msal-browser": { - "version": "2.39.0", - "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-2.39.0.tgz", - "integrity": "sha512-kks/n2AJzKUk+DBqZhiD+7zeQGBl+WpSOQYzWy6hff3bU0ZrYFqr4keFLlzB5VKuKZog0X59/FGHb1RPBDZLVg==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.17.1.tgz", + "integrity": "sha512-zBhRGzABKSI7hfWh5EaZmril5ybZ7imBN1qEZl5sDTaelr+l8SnPjZO50Q4dnKnm347YPIlBMSnXKZyh3Yu5DQ==", "license": "MIT", "dependencies": { - "@azure/msal-common": "13.3.3" + "@azure/msal-common": "16.11.2" }, "engines": { "node": ">=0.8.0" } }, - "node_modules/@azure/msal-browser/node_modules/@azure/msal-common": { - "version": "13.3.3", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-13.3.3.tgz", - "integrity": "sha512-n278DdCXKeiWhLwhEL7/u9HRMyzhUXLefeajiknf6AmEedoiOiv2r5aRJ7LXdT3NGPyubkdIbthaJlVtmuEqvA==", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/@azure/msal-common": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-7.6.0.tgz", - "integrity": "sha512-XqfbglUTVLdkHQ8F9UQJtKseRr3sSnr9ysboxtoswvaMVaEfvyLtMoHv9XdKUfOc0qKGzNgRFd9yRjIWVepl6Q==", + "version": "16.11.2", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.11.2.tgz", + "integrity": "sha512-yDhtBOGDCdK9ipQ9g3+wmlMEPnZx2pXaDicDd9jYyR1L+7lEbvEohTDmF5qejZDutZY3m9pWPxeYxzNC701A2w==", "license": "MIT", "engines": { "node": ">=0.8.0" } }, "node_modules/@azure/msal-node": { - "version": "1.18.4", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.18.4.tgz", - "integrity": "sha512-Kc/dRvhZ9Q4+1FSfsTFDME/v6+R2Y1fuMty/TfwqE5p9GTPw08BPbKgeWinE8JRHRp+LemjQbUZsn4Q4l6Lszg==", - "deprecated": "A newer major version of this library is available. Please upgrade to the latest available version.", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.4.1.tgz", + "integrity": "sha512-yqgoyOIMCH7TNaSLMBTP+4LUlbMMf1zgC8nzOFG95lmW82CmsAEtUT0J93e4BdqDcnX5qle/9X+yb7A8Mw9M0g==", "license": "MIT", "dependencies": { - "@azure/msal-common": "13.3.1", - "jsonwebtoken": "^9.0.0", - "uuid": "^8.3.0" + "@azure/msal-common": "16.11.2", + "jsonwebtoken": "^9.0.0" }, "engines": { - "node": "10 || 12 || 14 || 16 || 18" - } - }, - "node_modules/@azure/msal-node/node_modules/@azure/msal-common": { - "version": "13.3.1", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-13.3.1.tgz", - "integrity": "sha512-Lrk1ozoAtaP/cp53May3v6HtcFSVxdFrg2Pa/1xu5oIvsIwhxW6zSPibKefCOVgd5osgykMi5jjcZHv8XkzZEQ==", - "license": "MIT", - "engines": { - "node": ">=0.8.0" + "node": ">=20" } }, "node_modules/@jridgewell/gen-mapping": { @@ -472,9 +327,9 @@ } }, "node_modules/@js-joda/core": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@js-joda/core/-/core-5.7.0.tgz", - "integrity": "sha512-WBu4ULVVxySLLzK1Ppq+OdfP+adRS4ntmDQT915rzDJ++i95gc2jZkM5B6LWEAwN3lGXpfie3yPABozdD3K3Vg==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@js-joda/core/-/core-6.1.0.tgz", + "integrity": "sha512-H8NTMRDJqad/leyv/D/A3kSOsf5/58Ydj4DJGDyaCWk9OU/zuZOLhndVffJgQjsgrn5GC0znHMHie7TfvPPG4w==", "license": "BSD-3-Clause" }, "node_modules/@nodelib/fs.scandir": { @@ -539,15 +394,33 @@ } }, "node_modules/@tediousjs/connection-string": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@tediousjs/connection-string/-/connection-string-0.5.0.tgz", - "integrity": "sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@tediousjs/connection-string/-/connection-string-1.1.0.tgz", + "integrity": "sha512-z9ZBWEG+8pIB5V1zYzlRPXx0oRJ5H7coPnMQK8EZOw03UTPI9Umn6viL36f5w+CuqkKsnCM50RVStpjZmR0Bng==", "license": "MIT" }, + "node_modules/@types/node": { + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/readable-stream": { + "version": "4.0.24", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.24.tgz", + "integrity": "sha512-NRvUNC/JFGPJvqdAfEve8oginbM6V08u5NzLWpG8MwA2kTPOLnqk+wpwuPT+mp3aUsxyuT6m2gnrPuHYCruzEg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typespec/ts-http-runtime": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.4.tgz", - "integrity": "sha512-CI0NhTrz4EBaa0U+HaaUZrJhPoso8sG7ZFya8uQoBA57fjzrjRSv87ekCjLZOFExN+gXE/z0xuN2QfH4H2HrLQ==", + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.7.tgz", + "integrity": "sha512-JVUD8X2tfDMWjcjLs4yVxxVrS8yR5vnh386GAXT9Qj79nBxxXSaHFQZg5FweLmT8HlPQ3kii6noUB+Z9RN7DvQ==", "license": "MIT", "dependencies": { "http-proxy-agent": "^7.0.0", @@ -555,29 +428,57 @@ "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.0.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" } }, "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "license": "MIT", "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" }, "engines": { "node": ">= 0.6" } }, - "node_modules/adler-32": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", - "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", - "license": "Apache-2.0", + "node_modules/accepts/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", "engines": { - "node": ">=0.8" + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/agent-base": { @@ -623,58 +524,6 @@ "dev": true, "license": "MIT" }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/autoprefixer": { "version": "10.4.19", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", @@ -713,21 +562,6 @@ "postcss": "^8.1.0" } }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -799,44 +633,126 @@ } }, "node_modules/bl": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", - "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz", + "integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==", "license": "MIT", "dependencies": { + "@types/readable-stream": "^4.0.0", "buffer": "^6.0.3", "inherits": "^2.0.4", - "readable-stream": "^3.4.0" + "readable-stream": "^4.2.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", "license": "MIT", "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.14.0", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" }, "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/body-parser/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "dev": true, "license": "MIT", "dependencies": { @@ -929,6 +845,21 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -949,24 +880,6 @@ "node": ">= 0.8" } }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -1027,19 +940,6 @@ ], "license": "CC-BY-4.0" }, - "node_modules/cfb": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", - "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", - "license": "Apache-2.0", - "dependencies": { - "adler-32": "~1.3.0", - "crc-32": "~1.2.0" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -1065,15 +965,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/codepage": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", - "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, "node_modules/commander": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", @@ -1099,15 +990,16 @@ } }, "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/content-type": { @@ -1129,10 +1021,13 @@ } }, "node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "license": "MIT" + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } }, "node_modules/cors": { "version": "2.8.6", @@ -1151,18 +1046,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/crc-32": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", - "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", - "license": "Apache-2.0", - "bin": { - "crc32": "bin/crc32.njs" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -1176,107 +1059,61 @@ "node": ">=4" } }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" + "ms": "^2.1.3" }, "engines": { - "node": ">= 0.4" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", "license": "MIT", "engines": { - "node": ">=8" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/depd": { @@ -1288,16 +1125,6 @@ "node": ">= 0.8" } }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -1369,96 +1196,6 @@ "node": ">= 0.8" } }, - "node_modules/es-abstract": { - "version": "1.24.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", - "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.3.0", - "get-proto": "^1.0.1", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.2.1", - "is-set": "^2.0.3", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.1", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.4", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.4", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "stop-iteration-iterator": "^1.1.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.19" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-aggregate-error": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/es-aggregate-error/-/es-aggregate-error-1.0.14.tgz", - "integrity": "sha512-3YxX6rVb07B5TV11AV5wsL7nQCHXNwoHPsQC8S4AmBiqYhyNCJ5BRKXkXyDJvs8QzXN20NgRtxe3dEEQD9NLHA==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "globalthis": "^1.0.4", - "has-property-descriptors": "^1.0.2", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1478,9 +1215,9 @@ } }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -1489,38 +1226,6 @@ "node": ">= 0.4" } }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -1546,6 +1251,15 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -1556,45 +1270,126 @@ } }, "node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.3", - "content-disposition": "~0.5.4", - "content-type": "~1.0.4", - "cookie": "~0.7.1", - "cookie-signature": "~1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.3.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", - "proxy-addr": "~2.0.7", - "qs": "~6.14.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "~0.19.0", - "serve-static": "~1.16.2", - "setprototypeof": "1.2.0", - "statuses": "~2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" }, "engines": { - "node": ">= 0.10.0" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.0.tgz", + "integrity": "sha512-XKJXDsASUOo0LLtFwW5hCcQGH0N4WQc/Rn8/Pvoia+TJFOkkFPvrtW9lZOeeNcxQJspvOIERMwiRLsVFlhHEkA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" }, "funding": { "type": "opencollective", @@ -1642,36 +1437,24 @@ } }, "node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "license": "MIT", "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "statuses": "~2.0.2", - "unpipe": "~1.0.0" + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" }, "engines": { - "node": ">= 0.8" - } - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" + "node": ">= 18.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/forwarded": { @@ -1683,15 +1466,6 @@ "node": ">= 0.6" } }, - "node_modules/frac": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", - "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -1707,12 +1481,12 @@ } }, "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/fsevents": { @@ -1739,44 +1513,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/generator-function": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", - "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -1814,23 +1550,6 @@ "node": ">= 0.4" } }, - "node_modules/get-symbol-description": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -1844,22 +1563,6 @@ "node": ">= 6" } }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -1872,18 +1575,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -1894,33 +1585,6 @@ "node": ">=4" } }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -1933,21 +1597,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -1960,6 +1609,18 @@ "node": ">= 0.4" } }, + "node_modules/helmet": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.3.0.tgz", + "integrity": "sha512-Qgpiaws3Sm30Av8Eah6sjMCZZwjlBu+E68rhpCWBshY1lb09HtLwj5GviX0OyQIn+ulUS0iX0AxN5n3tLZzz1w==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/EvanHahn" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -1993,29 +1654,6 @@ "node": ">= 14" } }, - "node_modules/http-proxy-agent/node_modules/debug": { - "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" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/http-proxy-agent/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -2029,39 +1667,20 @@ "node": ">= 14" } }, - "node_modules/https-proxy-agent/node_modules/debug": { - "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" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/https-proxy-agent/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/ieee754": { @@ -2097,18 +1716,13 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, "engines": { - "node": ">= 0.4" + "node": ">= 12" } }, "node_modules/ipaddr.js": { @@ -2120,57 +1734,6 @@ "node": ">= 0.10" } }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-async-function": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", - "license": "MIT", - "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -2184,34 +1747,6 @@ "node": ">=8" } }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -2228,49 +1763,16 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", "license": "MIT", "bin": { "is-docker": "cli.js" }, "engines": { - "node": ">=8" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -2286,40 +1788,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", - "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.4", - "generator-function": "^2.0.0", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2333,28 +1801,22 @@ "node": ">=0.10.0" } }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, "engines": { - "node": ">= 0.4" + "node": ">=14.16" }, "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/is-number": { @@ -2367,176 +1829,27 @@ "node": ">=0.12.0" } }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" }, "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", "license": "MIT", "dependencies": { - "is-docker": "^2.0.0" + "is-inside-container": "^1.0.0" }, "engines": { - "node": ">=8" + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "license": "MIT" - }, "node_modules/jiti": { "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", @@ -2553,12 +1866,6 @@ "integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==", "license": "MIT" }, - "node_modules/jsbi": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-4.3.2.tgz", - "integrity": "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew==", - "license": "Apache-2.0" - }, "node_modules/jsonwebtoken": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", @@ -2581,12 +1888,6 @@ "npm": ">=6" } }, - "node_modules/jsonwebtoken/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, "node_modules/jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -2686,10 +1987,13 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "license": "MIT", + "engines": { + "node": ">=18" + }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } @@ -2704,15 +2008,6 @@ "node": ">= 8" } }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -2727,18 +2022,6 @@ "node": ">=8.6" } }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -2787,58 +2070,34 @@ } }, "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/mssql": { - "version": "9.3.2", - "resolved": "https://registry.npmjs.org/mssql/-/mssql-9.3.2.tgz", - "integrity": "sha512-XI5GOGCCSSNL8K2SSXg9HMyugJoCjLmrhiZfcZrJrJ2r3NfTcnz3Cegeg4m+xPkNVd0o3owsSL/NsDCFYfjOlw==", - "license": "MIT", - "dependencies": { - "@tediousjs/connection-string": "^0.5.0", - "commander": "^11.0.0", - "debug": "^4.3.3", - "rfdc": "^1.3.0", - "tarn": "^3.0.2", - "tedious": "^15.0.1" - }, - "bin": { - "mssql": "bin/mssql" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mssql/node_modules/debug": { - "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" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/mssql/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mssql": { + "version": "12.7.0", + "resolved": "https://registry.npmjs.org/mssql/-/mssql-12.7.0.tgz", + "integrity": "sha512-J6SJKXi1jYbhHjjooLNtPnX7+s3cq5IJ701Wgy/UW1SXRpgFlJJsYi3IPve9RVgCUkq0Cqv2aaaSJ4IXtIF3mg==", + "license": "MIT", + "dependencies": { + "@tediousjs/connection-string": "^1.0.0", + "commander": "^11.0.0", + "debug": "^4.3.3", + "tarn": "^3.0.2", + "tedious": "^19.2.2 || ^20.0.0" + }, + "bin": { + "mssql": "bin/mssql" + }, + "engines": { + "node": ">=18.19.0" + } + }, "node_modules/multer": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz", - "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.2.0.tgz", + "integrity": "sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==", "license": "MIT", "dependencies": { "append-field": "^1.0.0", @@ -2867,9 +2126,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "dev": true, "funding": [ { @@ -2892,20 +2151,14 @@ "license": "MIT" }, "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "license": "MIT", "engines": { "node": ">= 0.6" } }, - "node_modules/node-abort-controller": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", - "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", - "license": "MIT" - }, "node_modules/node-addon-api": { "version": "8.7.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.7.0.tgz", @@ -2934,9 +2187,9 @@ "license": "MIT" }, "node_modules/nodemailer": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.4.tgz", - "integrity": "sha512-k+jf6N8PfQJ0Fe8ZhJlgqU5qJU44Lpvp2yvidH3vp1lPnVQMgi4yEEMPXg5eJS1gFIJTVq1NHBk7Ia9ARdSBdQ==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.3.tgz", + "integrity": "sha512-n+YP+NKwR5zRWa60k3GiQ6Q3B4KXCoAw40dAKeCtYn020iNN74aWK2liXIC3ZEATeGql7we3tE3t8QwhY0eskw==", "license": "MIT-0", "engines": { "node": ">=6.0.0" @@ -2971,31 +2224,6 @@ "url": "https://opencollective.com/nodemon" } }, - "node_modules/nodemon/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/nodemon/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -3047,35 +2275,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -3088,40 +2287,33 @@ "node": ">= 0.8" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", "license": "MIT", "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -3139,10 +2331,14 @@ "license": "MIT" }, "node_modules/path-to-regexp": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", - "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", - "license": "MIT" + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } }, "node_modules/picocolors": { "version": "1.1.1", @@ -3184,19 +2380,10 @@ "node": ">= 6" } }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", "dev": true, "funding": [ { @@ -3214,9 +2401,9 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -3362,6 +2549,15 @@ "dev": true, "license": "MIT" }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -3382,22 +2578,14 @@ "dev": true, "license": "MIT" }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.1.0" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" }, "engines": { "node": ">=0.6" @@ -3428,27 +2616,31 @@ "license": "MIT" }, "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", "license": "MIT", "engines": { "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "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.1", - "iconv-lite": "~0.4.24", + "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.10" } }, "node_modules/read-cache": { @@ -3488,48 +2680,6 @@ "node": ">=8.10.0" } }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.1", - "which-builtin-type": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -3562,11 +2712,33 @@ "node": ">=0.10.0" } }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "license": "MIT" + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/run-parallel": { "version": "1.2.0", @@ -3592,25 +2764,6 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "has-symbols": "^1.1.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -3631,39 +2784,6 @@ ], "license": "MIT" }, - "node_modules/safe-push-apply": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -3683,94 +2803,73 @@ } }, "node_modules/send": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", - "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "license": "MIT", "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.1", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "~2.4.1", - "range-parser": "~1.2.1", - "statuses": "~2.0.2" + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" + "node_modules/send/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } }, "node_modules/serve-static": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", - "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "license": "MIT", "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "~0.19.1" + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" }, "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" + "node": ">= 18" }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/setprototypeof": { @@ -3780,14 +2879,14 @@ "license": "ISC" }, "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -3799,13 +2898,13 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -3880,18 +2979,6 @@ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "license": "BSD-3-Clause" }, - "node_modules/ssf": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", - "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", - "license": "Apache-2.0", - "dependencies": { - "frac": "~1.1.2" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -3901,29 +2988,6 @@ "node": ">= 0.8" } }, - "node_modules/stop-iteration-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/stoppable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", - "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", - "license": "MIT", - "engines": { - "node": ">=4", - "npm": ">=6" - } - }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -3941,62 +3005,6 @@ "safe-buffer": "~5.2.0" } }, - "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/sucrase": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", @@ -4117,38 +3125,24 @@ } }, "node_modules/tedious": { - "version": "15.1.3", - "resolved": "https://registry.npmjs.org/tedious/-/tedious-15.1.3.tgz", - "integrity": "sha512-166EpRm5qknwhEisjZqz/mF7k14fXKJYHRg6XiAXVovd/YkyHJ3SG4Ppy89caPaNFfRr7PVYe+s4dAvKaCMFvw==", + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/tedious/-/tedious-20.0.0.tgz", + "integrity": "sha512-bTR0aou0Ghucf0ytvZUJjnKHGKDV8tT57jPYtEkSpfTWFe++4uR1wxJLQ4mh5wlSvAYXzmgBxbI0vaE56qigXw==", "license": "MIT", "dependencies": { - "@azure/identity": "^2.0.4", - "@azure/keyvault-keys": "^4.4.0", - "@js-joda/core": "^5.2.0", - "bl": "^5.0.0", - "es-aggregate-error": "^1.0.8", - "iconv-lite": "^0.6.3", + "@azure/core-auth": "^1.10.1", + "@azure/identity": "^4.13.1", + "@azure/keyvault-keys": "^4.10.2", + "@js-joda/core": "^6.0.1", + "@types/node": ">=22", + "bl": "^6.1.4", + "iconv-lite": "^0.7.0", "js-md4": "^0.3.2", - "jsbi": "^4.3.0", "native-duplexpair": "^1.0.0", - "node-abort-controller": "^3.0.1", - "punycode": "^2.1.0", - "sprintf-js": "^1.1.2" + "sprintf-js": "^1.1.3" }, "engines": { - "node": ">=14" - } - }, - "node_modules/tedious/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" + "node": ">=22" } }, "node_modules/thenify": { @@ -4280,104 +3274,12 @@ "node": ">= 0.6" } }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "license": "MIT" }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -4385,6 +3287,12 @@ "dev": true, "license": "MIT" }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -4431,24 +3339,6 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -4458,123 +3348,32 @@ "node": ">= 0.8" } }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "license": "MIT", - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.1.0", - "is-finalizationregistry": "^1.1.0", - "is-generator-function": "^1.0.10", - "is-regex": "^1.2.1", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.1.0", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { + "node_modules/wrappy": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", "license": "MIT", "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" + "is-wsl": "^3.1.0" }, "engines": { - "node": ">= 0.4" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.20", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", - "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/wmf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", - "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/word": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", - "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/xlsx": { - "version": "0.18.5", - "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", - "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "version": "0.20.3", + "resolved": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", + "integrity": "sha512-oLDq3jw7AcLqKWH2AhCpVTZl8mf6X2YReP+Neh0SJUzV/BdZYjth94tG5toiMB1PPrYtxOCfaoUCkvtuH+3AJA==", "license": "Apache-2.0", - "dependencies": { - "adler-32": "~1.3.0", - "cfb": "~1.2.1", - "codepage": "~1.15.0", - "crc-32": "~1.2.1", - "ssf": "~0.11.2", - "wmf": "~1.0.1", - "word": "~0.3.0" - }, "bin": { "xlsx": "bin/xlsx.njs" }, diff --git a/package.json b/package.json index 1705ec0..e9048ff 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,8 @@ "scripts": { "start": "node backend/server.js", "dev": "nodemon backend/server.js", + "test": "node --test", + "check": "node --check backend/server.js && node --check public/js/app.js && npm audit", "build:css": "tailwindcss -c tailwind.config.js -i ./public/css/tailwind.css -o ./public/css/main.css --minify", "watch:css": "tailwindcss -c tailwind.config.js -i ./public/css/tailwind.css -o ./public/css/main.css --watch" }, @@ -17,15 +19,20 @@ ], "author": "", "license": "MIT", + "engines": { + "node": ">=22.0.0" + }, "dependencies": { "bcrypt": "^6.0.0", "cors": "^2.8.5", "dotenv": "^16.3.1", - "express": "^4.18.2", - "mssql": "^9.1.1", - "multer": "^2.1.1", - "nodemailer": "^8.0.4", - "xlsx": "^0.18.5" + "express": "^5.2.1", + "express-rate-limit": "^8.6.0", + "helmet": "^8.3.0", + "mssql": "^12.7.0", + "multer": "^2.2.0", + "nodemailer": "^9.0.3", + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz" }, "devDependencies": { "@tailwindcss/container-queries": "^0.1.1", diff --git a/public/css/responsive.css b/public/css/responsive.css index a60b21b..57e062b 100644 --- a/public/css/responsive.css +++ b/public/css/responsive.css @@ -150,6 +150,89 @@ textarea { font-size: 1rem; } + /* Keep the asset and consumable toolbars compact so the data remains + visible on tablets and phones. */ + .asset-borrows-page .compact-page-actions, + .consumable-exports-page .compact-page-actions, + .assets-page .asset-header-actions, + .consumables-page .consumable-header-actions { + display: flex; + width: 100%; + max-width: 100%; + flex-flow: row nowrap; + gap: 0.5rem; + overflow-x: auto; + overscroll-behavior-x: contain; + padding: 0.125rem 0.125rem 0.375rem; + scrollbar-width: thin; + -webkit-overflow-scrolling: touch; + } + + .asset-borrows-page .compact-page-actions > button, + .consumable-exports-page .compact-page-actions > button, + .assets-page .asset-header-actions > button, + .consumables-page .consumable-header-actions > button { + width: auto; + min-width: max-content; + min-height: 2.5rem; + flex: 0 0 auto; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + white-space: nowrap; + } + + .compact-page-filters { + display: grid !important; + align-items: end !important; + gap: 0.625rem !important; + padding: 0.625rem; + border: 1px solid #e2e8f0; + border-radius: 0.75rem; + background: rgb(248 250 252 / 0.82); + } + + .compact-page-filters.asset-borrow-filter-bar { + grid-template-columns: minmax(10rem, 0.35fr) minmax(0, 1fr); + } + + .compact-page-filters.asset-filter-bar { + grid-template-columns: minmax(9rem, 0.3fr) minmax(14rem, 1fr) max-content; + } + + .compact-page-filters.consumable-filter-bar { + grid-template-columns: minmax(8rem, 0.25fr) minmax(8rem, 0.25fr) minmax(14rem, 1fr); + } + + .compact-page-filters.consumable-export-filter-bar { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .compact-page-filters > div { + width: auto; + min-width: 0; + flex-direction: column; + align-items: stretch; + gap: 0.25rem; + } + + .compact-page-filters input, + .compact-page-filters select { + width: 100%; + min-width: 0; + min-height: 2.5rem; + font-size: 0.875rem; + line-height: 1.25rem; + } + + .compact-page-filters .compact-filter-action { + width: auto; + min-width: max-content; + min-height: 2.5rem; + align-self: end; + justify-content: center; + white-space: nowrap; + } + .dashboard-stats, .apps-stats { gap: 0.75rem; @@ -374,6 +457,29 @@ textarea { grid-template-columns: minmax(0, 1fr) !important; } + .compact-page-filters.asset-borrow-filter-bar { + grid-template-columns: minmax(0, 1fr); + } + + .compact-page-filters.asset-filter-bar { + grid-template-columns: minmax(0, 1fr) max-content; + } + + .compact-page-filters.asset-filter-bar .compact-filter-search { + grid-column: 1 / -1; + grid-row: 2; + } + + .compact-page-filters.consumable-filter-bar, + .compact-page-filters.consumable-export-filter-bar { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .compact-page-filters.consumable-filter-bar .compact-filter-search, + .compact-page-filters.consumable-export-filter-bar .compact-filter-search { + grid-column: 1 / -1; + } + #mainContent table.mobile-card-table:not(.keep-table-mobile) > tbody { padding: 0.625rem; } diff --git a/public/js/app.js b/public/js/app.js index 37de47a..91f4d38 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -1,6 +1,27 @@ // VaultSentinel - Account Management Application // Main JavaScript functionality +const authenticatedFetch = window.fetch.bind(window); +let authRedirectInProgress = false; +window.fetch = async (input, init = {}) => { + const response = await authenticatedFetch(input, { + ...init, + credentials: init.credentials || 'same-origin' + }); + + const requestUrl = typeof input === 'string' ? input : String(input?.url || ''); + const isAuthRequest = requestUrl.includes('/api/auth/login') + || requestUrl.includes('/api/auth/session') + || requestUrl.includes('/api/auth/logout'); + if (response.status === 401 && !isAuthRequest && !authRedirectInProgress) { + authRedirectInProgress = true; + localStorage.removeItem('currentUser'); + window.location.replace('../pages/login.html?reason=session-expired'); + } + + return response; +}; + const APP_TIME_ZONE = 'Asia/Ho_Chi_Minh'; const APP_DATE_FORMATTER = new Intl.DateTimeFormat('vi-VN', { timeZone: APP_TIME_ZONE, @@ -220,10 +241,7 @@ class AccountManager { } getAuthHeaders(includeJson = false) { - const headers = { - 'x-user-id': String(this.getUserId()), - 'x-user-role': this.getCurrentUserRole() - }; + const headers = {}; if (includeJson) { headers['Content-Type'] = 'application/json'; @@ -463,6 +481,15 @@ class AccountManager { } } + async fetchAccountSecret(accountId) { + const response = await fetch(`${this.apiBase}/accounts/${accountId}/secret`, { cache: 'no-store' }); + const data = await response.json(); + if (!response.ok || !data.success) { + throw new Error(data.message || 'Unable to reveal stored credential'); + } + return String(data.password || ''); + } + async fetchUsers() { try { const res = await fetch(`${this.apiBase}/users`); @@ -2502,11 +2529,17 @@ class AccountManager { return `${value.slice(0, 3)}*****`; } - handleLogout() { + async handleLogout() { if (confirm('Are you sure you want to logout?')) { - this.saveToStorage('currentUser', null); - localStorage.clear(); - window.location.href = '../pages/login.html'; + try { + await fetch(`${this.apiBase}/auth/logout`, { method: 'POST' }); + } catch (error) { + console.error('Logout request failed:', error); + } finally { + this.saveToStorage('currentUser', null); + localStorage.removeItem('currentUser'); + window.location.replace('../pages/login.html'); + } } } @@ -2643,13 +2676,14 @@ class AccountManager { ${pageInfo.data.map(acc => { const isOwnAccount = acc.UserId == currentUserId; + const canAccessAccount = isOwnAccount || this.getCurrentUserRole() === 'admin'; const accountUsername = acc.AccountUsername || '-'; - const displayAccountUsername = isOwnAccount + const displayAccountUsername = canAccessAccount ? accountUsername : this.maskForeignAccountUsername(accountUsername); const createdDate = this.formatDateTime(acc.CreatedDate); const updatedDate = this.formatDateTime(acc.UpdatedDate); - const actionContent = isOwnAccount + const actionContent = canAccessAccount ? ` @@ -4244,7 +4278,7 @@ class AccountManager {

Mượn/Trả tài sản

Theo dõi trạng thái đơn mượn và đơn trả tài sản.

-
+
-
+
Danh mục
-
+