diff --git a/.gitignore b/.gitignore index 416921d..e3e1c2a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ +# Stored error-log audio (admin playback) +service/error_audio/ + +# macOS +.DS_Store + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/app/.env.example b/app/.env.example index e574ee6..8e18205 100644 --- a/app/.env.example +++ b/app/.env.example @@ -1,5 +1,9 @@ DATABASE_URL=XXXXXXXXXXX NEXT_PUBLIC_MICROSERVICE_URL=XXXXXXXXXXX + +# Basic auth for /admin routes (format: user:password or user:password|user2:password2) +# Optional: when set, visiting /admin/* will prompt for these credentials. +BASIC_AUTH_CREDENTIALS=admin:your-secure-password AWS_REGION=XXXXXXXXXXX AWS_ACCESS_KEY_ID=XXXXXXXXXXX AWS_SECRET_ACCESS_KEY=XXXXXXXXXXX diff --git a/app/middleware.ts b/app/middleware.ts new file mode 100644 index 0000000..61ef41e --- /dev/null +++ b/app/middleware.ts @@ -0,0 +1,38 @@ +import { createNextAuthMiddleware } from "nextjs-basic-auth-middleware"; +import { NextRequest, NextResponse } from "next/server"; + +/** + * Parse BASIC_AUTH_CREDENTIALS env (format: user:password or user:password|user2:password2) + * into users array. Package expects { name, password } per user. + */ +function getUsersFromEnv(): { name: string; password: string }[] { + const raw = process.env.BASIC_AUTH_CREDENTIALS; + if (!raw?.trim()) return []; + return raw + .split("|") + .map((pair) => { + const [name, ...rest] = pair.trim().split(":"); + const password = rest.join(":").trim(); + return name && password ? { name, password } : null; + }) + .filter((u): u is { name: string; password: string } => u !== null); +} + +export const config = { + // Protect only /admin routes. Use ['/(.*)'] to protect the whole app. + matcher: ["/admin/:path*"], +}; + +export default function middleware(req: NextRequest) { + const users = getUsersFromEnv(); + // Only run basic auth when credentials are configured + if (users.length === 0) { + return NextResponse.next(); + } + const authMiddleware = createNextAuthMiddleware({ + users, + realm: "Admin", + message: "Authentication failed", + }); + return authMiddleware(req); +} diff --git a/app/package-lock.json b/app/package-lock.json index 3f88bca..3567aa5 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -37,6 +37,7 @@ "next": "15.1.6", "next-client-cookies": "^2.0.1", "next-themes": "^0.3.0", + "nextjs-basic-auth-middleware": "^3.1.0", "nodemailer": "^7.0.5", "pg": "^8.13.3", "postgres": "^3.4.4", @@ -70,6 +71,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, "engines": { "node": ">=10" }, @@ -1918,6 +1920,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -1934,6 +1937,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, "engines": { "node": ">=12" }, @@ -1945,6 +1949,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -1959,6 +1964,7 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -1972,6 +1978,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, "engines": { "node": ">=6.0.0" } @@ -1980,6 +1987,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, "engines": { "node": ">=6.0.0" } @@ -1987,12 +1995,14 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -2699,6 +2709,7 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -2711,6 +2722,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, "engines": { "node": ">= 8" } @@ -2719,6 +2731,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -2740,6 +2753,7 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, "optional": true, "engines": { "node": ">=14" @@ -4788,7 +4802,7 @@ "version": "20.16.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.2.tgz", "integrity": "sha512-91s/n4qUPV/wg8eE9KHYW1kouTfDk2FPGjXbBMfRWP/2vg1rCXNQL1OCabwGs0XSdukuK+MwCDXE30QpSeMUhQ==", - "devOptional": true, + "dev": true, "dependencies": { "undici-types": "~6.19.2" } @@ -4807,7 +4821,7 @@ "version": "8.11.11", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.11.tgz", "integrity": "sha512-kGT1qKM8wJQ5qlawUrEkXgvMSXoV213KfMGXcwfDwUIfUHXqXYXOfS1nE1LINRJVVVx5wCm70XnFlMHaIcQAfw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -4819,6 +4833,7 @@ "version": "19.0.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.8.tgz", "integrity": "sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw==", + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -4828,7 +4843,7 @@ "version": "19.0.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.3.tgz", "integrity": "sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" @@ -5113,6 +5128,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "engines": { "node": ">=8" } @@ -5121,6 +5137,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -5134,12 +5151,14 @@ "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -5151,7 +5170,8 @@ "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true }, "node_modules/argparse": { "version": "2.0.1", @@ -5423,7 +5443,8 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "node_modules/base64-js": { "version": "1.5.1", @@ -5458,6 +5479,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, "engines": { "node": ">=8" }, @@ -5486,6 +5508,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, "dependencies": { "fill-range": "^7.1.1" }, @@ -5577,6 +5600,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, "engines": { "node": ">= 6" } @@ -5665,6 +5689,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -5688,6 +5713,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -5746,6 +5772,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "devOptional": true, "dependencies": { "color-name": "~1.1.4" }, @@ -5756,7 +5783,8 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true }, "node_modules/color-string": { "version": "1.9.1", @@ -5793,6 +5821,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, "engines": { "node": ">= 6" } @@ -5807,6 +5836,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -5821,6 +5851,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, "bin": { "cssesc": "bin/cssesc" }, @@ -5831,7 +5862,8 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -6018,12 +6050,14 @@ "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true }, "node_modules/doctrine": { "version": "3.0.0", @@ -6615,7 +6649,8 @@ "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", @@ -6629,7 +6664,8 @@ "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true }, "node_modules/enhanced-resolve": { "version": "5.17.1", @@ -7328,6 +7364,7 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -7343,6 +7380,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -7388,6 +7426,7 @@ "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, "dependencies": { "reusify": "^1.0.4" } @@ -7419,6 +7458,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -7501,6 +7541,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" @@ -7541,6 +7582,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -7711,6 +7753,7 @@ "version": "10.3.10", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^2.3.5", @@ -7732,6 +7775,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -7743,6 +7787,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -7752,6 +7797,7 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -8205,6 +8251,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, "dependencies": { "binary-extensions": "^2.0.0" }, @@ -8255,6 +8302,7 @@ "version": "2.15.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, "dependencies": { "hasown": "^2.0.2" }, @@ -8313,6 +8361,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -8337,6 +8386,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "engines": { "node": ">=8" } @@ -8364,6 +8414,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -8397,6 +8448,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, "engines": { "node": ">=0.12.0" } @@ -8605,7 +8657,8 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true }, "node_modules/iterator.prototype": { "version": "1.1.5", @@ -8629,6 +8682,7 @@ "version": "2.3.6", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -8646,6 +8700,7 @@ "version": "1.21.6", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", + "dev": true, "bin": { "jiti": "bin/jiti.js" } @@ -8798,6 +8853,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, "engines": { "node": ">=10" } @@ -8805,7 +8861,8 @@ "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true }, "node_modules/locate-path": { "version": "6.0.0", @@ -8851,7 +8908,8 @@ "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true }, "node_modules/lucia": { "version": "3.2.0", @@ -9047,6 +9105,7 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, "engines": { "node": ">= 8" } @@ -9476,6 +9535,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -9528,6 +9588,7 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -9541,6 +9602,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", @@ -9674,6 +9736,15 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/nextjs-basic-auth-middleware": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nextjs-basic-auth-middleware/-/nextjs-basic-auth-middleware-3.1.0.tgz", + "integrity": "sha512-k1GSYiLMYj4tK6swAmArTUBheSkv7roUUF2KI8Cv8HYHmSYep5Y0AP0RB/1sez7xTRi2Fw2SxYbQA/jAmBJCug==", + "license": "MIT", + "peerDependencies": { + "next": ">=13.1" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -9707,6 +9778,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -9723,6 +9795,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, "engines": { "node": ">= 6" } @@ -9839,7 +9912,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "devOptional": true + "dev": true }, "node_modules/once": { "version": "1.4.0", @@ -10219,6 +10292,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "engines": { "node": ">=8" } @@ -10226,12 +10300,14 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -10295,7 +10371,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", - "devOptional": true, + "dev": true, "engines": { "node": ">=4" } @@ -10319,7 +10395,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz", "integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==", - "devOptional": true, + "dev": true, "dependencies": { "pg-int8": "1.0.1", "pg-numeric": "1.0.2", @@ -10406,6 +10482,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "engines": { "node": ">=8.6" }, @@ -10417,6 +10494,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -10425,6 +10503,7 @@ "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, "engines": { "node": ">= 6" } @@ -10443,6 +10522,7 @@ "version": "8.4.41", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", + "dev": true, "funding": [ { "type": "opencollective", @@ -10470,6 +10550,7 @@ "version": "15.1.0", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", @@ -10486,6 +10567,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, "dependencies": { "camelcase-css": "^2.0.1" }, @@ -10504,6 +10586,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, "funding": [ { "type": "opencollective", @@ -10538,6 +10621,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", + "dev": true, "engines": { "node": ">=14" }, @@ -10549,6 +10633,7 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, "funding": [ { "type": "opencollective", @@ -10573,6 +10658,7 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -10584,7 +10670,8 @@ "node_modules/postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true }, "node_modules/postgres": { "version": "3.4.4", @@ -10602,7 +10689,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==", - "devOptional": true, + "dev": true, "engines": { "node": ">=12" } @@ -10611,7 +10698,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", - "devOptional": true, + "dev": true, "dependencies": { "obuf": "~1.1.2" }, @@ -10623,7 +10710,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz", "integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==", - "devOptional": true, + "dev": true, "engines": { "node": ">=12" } @@ -10632,7 +10719,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", - "devOptional": true, + "dev": true, "engines": { "node": ">=12" } @@ -10641,7 +10728,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz", "integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==", - "devOptional": true + "dev": true }, "node_modules/prelude-ls": { "version": "1.2.1", @@ -10704,6 +10791,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, "funding": [ { "type": "github", @@ -10849,6 +10937,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, "dependencies": { "pify": "^2.3.0" } @@ -10857,6 +10946,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, "dependencies": { "picomatch": "^2.2.1" }, @@ -10943,6 +11033,7 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -10977,6 +11068,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -11023,6 +11115,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, "funding": [ { "type": "github", @@ -11226,6 +11319,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -11237,6 +11331,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "engines": { "node": ">=8" } @@ -11317,6 +11412,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, "engines": { "node": ">=14" }, @@ -11400,6 +11496,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -11417,6 +11514,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -11429,12 +11527,14 @@ "node_modules/string-width-cjs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true }, "node_modules/string-width/node_modules/ansi-regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, "engines": { "node": ">=12" }, @@ -11446,6 +11546,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -11585,6 +11686,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -11597,6 +11699,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -11672,6 +11775,7 @@ "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", @@ -11705,6 +11809,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -11725,6 +11830,7 @@ "version": "3.4.10", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz", "integrity": "sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==", + "dev": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -11784,6 +11890,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, "dependencies": { "any-promise": "^1.0.0" } @@ -11792,6 +11899,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, "dependencies": { "thenify": ">= 3.1.0 < 4" }, @@ -11803,6 +11911,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -11850,7 +11959,8 @@ "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true }, "node_modules/tsconfig-paths": { "version": "3.15.0", @@ -12008,7 +12118,7 @@ "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "devOptional": true + "dev": true }, "node_modules/unified": { "version": "11.0.5", @@ -12152,7 +12262,8 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true }, "node_modules/uuid": { "version": "11.0.3", @@ -12213,6 +12324,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -12324,6 +12436,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -12341,6 +12454,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -12356,12 +12470,14 @@ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -12375,6 +12491,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, "engines": { "node": ">=12" }, @@ -12386,6 +12503,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, "engines": { "node": ">=12" }, @@ -12397,6 +12515,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -12426,6 +12545,7 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==", + "dev": true, "bin": { "yaml": "bin.mjs" }, @@ -12467,7 +12587,8 @@ "@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==" + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true }, "@aws-crypto/crc32": { "version": "5.2.0", @@ -13458,8 +13579,7 @@ "@hookform/resolvers": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.0.tgz", - "integrity": "sha512-bU0Gr4EepJ/EQsH/IwEzYLsT/PEj5C0ynLQ4m+GSHS+xKH4TfSelhluTgOaoc4kA5s7eCsQbM4wvZLzELmWzUg==", - "requires": {} + "integrity": "sha512-bU0Gr4EepJ/EQsH/IwEzYLsT/PEj5C0ynLQ4m+GSHS+xKH4TfSelhluTgOaoc4kA5s7eCsQbM4wvZLzELmWzUg==" }, "@humanwhocodes/config-array": { "version": "0.11.14", @@ -13640,6 +13760,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, "requires": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -13652,12 +13773,14 @@ "ansi-regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==" + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true }, "strip-ansi": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, "requires": { "ansi-regex": "^6.0.1" } @@ -13668,6 +13791,7 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, "requires": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -13677,22 +13801,26 @@ "@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==" + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true }, "@jridgewell/set-array": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==" + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true }, "@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true }, "@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, "requires": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -13701,8 +13829,7 @@ "@lucia-auth/adapter-drizzle": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@lucia-auth/adapter-drizzle/-/adapter-drizzle-1.1.0.tgz", - "integrity": "sha512-iCTnZWvfI5lLZOdUHZYiXA1jaspIFEeo2extLxQ3DjP3uOVys7IPwBi7zezLIRu9dhro4H4Kji+7gSYyjcef2A==", - "requires": {} + "integrity": "sha512-iCTnZWvfI5lLZOdUHZYiXA1jaspIFEeo2extLxQ3DjP3uOVys7IPwBi7zezLIRu9dhro4H4Kji+7gSYyjcef2A==" }, "@napi-rs/wasm-runtime": { "version": "0.2.4", @@ -14054,6 +14181,7 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, "requires": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -14062,12 +14190,14 @@ "@nodelib/fs.stat": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==" + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true }, "@nodelib/fs.walk": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, "requires": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -14083,6 +14213,7 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, "optional": true }, "@radix-ui/number": { @@ -14117,14 +14248,12 @@ "@radix-ui/react-compose-refs": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "requires": {} + "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==" }, "@radix-ui/react-context": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", - "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", - "requires": {} + "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==" }, "@radix-ui/react-dialog": { "version": "1.1.1", @@ -14164,8 +14293,7 @@ "@radix-ui/react-direction": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", - "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", - "requires": {} + "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==" }, "@radix-ui/react-dismissable-layer": { "version": "1.1.0", @@ -14196,8 +14324,7 @@ "@radix-ui/react-focus-guards": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz", - "integrity": "sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==", - "requires": {} + "integrity": "sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==" }, "@radix-ui/react-focus-scope": { "version": "1.1.0", @@ -14212,8 +14339,7 @@ "@radix-ui/react-icons": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.0.tgz", - "integrity": "sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==", - "requires": {} + "integrity": "sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==" }, "@radix-ui/react-id": { "version": "1.1.0", @@ -14369,8 +14495,7 @@ "@radix-ui/react-context": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", - "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", - "requires": {} + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==" }, "@radix-ui/react-dismissable-layer": { "version": "1.1.1", @@ -14387,8 +14512,7 @@ "@radix-ui/react-focus-guards": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", - "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", - "requires": {} + "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==" }, "@radix-ui/react-portal": { "version": "1.1.2", @@ -14455,20 +14579,17 @@ "@radix-ui/react-compose-refs": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "requires": {} + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==" }, "@radix-ui/react-context": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "requires": {} + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==" }, "@radix-ui/react-direction": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "requires": {} + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==" }, "@radix-ui/react-id": { "version": "1.1.1", @@ -14522,8 +14643,7 @@ "@radix-ui/react-use-callback-ref": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", - "requires": {} + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==" }, "@radix-ui/react-use-controllable-state": { "version": "1.2.2", @@ -14537,8 +14657,7 @@ "@radix-ui/react-use-layout-effect": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", - "requires": {} + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==" } } }, @@ -14564,8 +14683,7 @@ "@radix-ui/react-use-callback-ref": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", - "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", - "requires": {} + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==" }, "@radix-ui/react-use-controllable-state": { "version": "1.1.0", @@ -14586,8 +14704,7 @@ "@radix-ui/react-use-layout-effect": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", - "requires": {} + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==" } } }, @@ -14602,14 +14719,12 @@ "@radix-ui/react-use-layout-effect": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", - "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", - "requires": {} + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==" }, "@radix-ui/react-use-previous": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz", - "integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==", - "requires": {} + "integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==" }, "@radix-ui/react-use-rect": { "version": "1.1.0", @@ -15282,7 +15397,7 @@ "version": "20.16.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.2.tgz", "integrity": "sha512-91s/n4qUPV/wg8eE9KHYW1kouTfDk2FPGjXbBMfRWP/2vg1rCXNQL1OCabwGs0XSdukuK+MwCDXE30QpSeMUhQ==", - "devOptional": true, + "dev": true, "requires": { "undici-types": "~6.19.2" } @@ -15300,7 +15415,7 @@ "version": "8.11.11", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.11.tgz", "integrity": "sha512-kGT1qKM8wJQ5qlawUrEkXgvMSXoV213KfMGXcwfDwUIfUHXqXYXOfS1nE1LINRJVVVx5wCm70XnFlMHaIcQAfw==", - "devOptional": true, + "dev": true, "requires": { "@types/node": "*", "pg-protocol": "*", @@ -15311,6 +15426,7 @@ "version": "19.0.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.8.tgz", "integrity": "sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw==", + "dev": true, "requires": { "csstype": "^3.0.2" } @@ -15319,8 +15435,7 @@ "version": "19.0.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.3.tgz", "integrity": "sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==", - "devOptional": true, - "requires": {} + "dev": true }, "@types/unist": { "version": "3.0.3", @@ -15466,8 +15581,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "requires": {} + "dev": true }, "agent-base": { "version": "7.1.3", @@ -15489,12 +15603,14 @@ "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -15502,12 +15618,14 @@ "any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true }, "anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, "requires": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -15516,7 +15634,8 @@ "arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true }, "argparse": { "version": "2.0.1", @@ -15703,7 +15822,8 @@ "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "base64-js": { "version": "1.5.1", @@ -15718,7 +15838,8 @@ "binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==" + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true }, "bowser": { "version": "2.11.0", @@ -15739,6 +15860,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, "requires": { "fill-range": "^7.1.1" } @@ -15801,7 +15923,8 @@ "camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==" + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true }, "caniuse-lite": { "version": "1.0.30001653", @@ -15847,6 +15970,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, "requires": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -15862,6 +15986,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "requires": { "is-glob": "^4.0.1" } @@ -15907,6 +16032,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "devOptional": true, "requires": { "color-name": "~1.1.4" } @@ -15914,7 +16040,8 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true }, "color-string": { "version": "1.9.1", @@ -15942,7 +16069,8 @@ "commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==" + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true }, "concat-map": { "version": "0.0.1", @@ -15954,6 +16082,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, "requires": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -15963,12 +16092,14 @@ "cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==" + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true }, "csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true }, "damerau-levenshtein": { "version": "1.0.8", @@ -16090,12 +16221,14 @@ "didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true }, "dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true }, "doctrine": { "version": "3.0.0", @@ -16313,8 +16446,7 @@ "drizzle-orm": { "version": "0.33.0", "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.33.0.tgz", - "integrity": "sha512-SHy72R2Rdkz0LEq0PSG/IdvnT3nGiWuRk+2tXZQ90GVq/XQhpCzu/EFT3V2rox+w8MlkBQxifF8pCStNYnERfA==", - "requires": {} + "integrity": "sha512-SHy72R2Rdkz0LEq0PSG/IdvnT3nGiWuRk+2tXZQ90GVq/XQhpCzu/EFT3V2rox+w8MlkBQxifF8pCStNYnERfA==" }, "dunder-proto": { "version": "1.0.1", @@ -16329,7 +16461,8 @@ "eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true }, "ecdsa-sig-formatter": { "version": "1.0.11", @@ -16342,7 +16475,8 @@ "emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true }, "enhanced-resolve": { "version": "5.17.1", @@ -16788,8 +16922,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.1.0.tgz", "integrity": "sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw==", - "dev": true, - "requires": {} + "dev": true }, "eslint-scope": { "version": "7.2.2", @@ -16868,6 +17001,7 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -16880,6 +17014,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "requires": { "is-glob": "^4.0.1" } @@ -16910,6 +17045,7 @@ "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, "requires": { "reusify": "^1.0.4" } @@ -16935,6 +17071,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, "requires": { "to-regex-range": "^5.0.1" } @@ -16984,6 +17121,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, "requires": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" @@ -17015,6 +17153,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "optional": true }, "function-bind": { @@ -17126,6 +17265,7 @@ "version": "10.3.10", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, "requires": { "foreground-child": "^3.1.0", "jackspeak": "^2.3.5", @@ -17138,6 +17278,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, "requires": { "balanced-match": "^1.0.0" } @@ -17146,6 +17287,7 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, "requires": { "brace-expansion": "^2.0.1" } @@ -17156,6 +17298,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, "requires": { "is-glob": "^4.0.3" } @@ -17459,6 +17602,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, "requires": { "binary-extensions": "^2.0.0" } @@ -17492,6 +17636,7 @@ "version": "2.15.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, "requires": { "hasown": "^2.0.2" } @@ -17525,7 +17670,8 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true }, "is-finalizationregistry": { "version": "1.1.1", @@ -17539,7 +17685,8 @@ "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true }, "is-generator-function": { "version": "1.1.0", @@ -17557,6 +17704,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "requires": { "is-extglob": "^2.1.1" } @@ -17575,7 +17723,8 @@ "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true }, "is-number-object": { "version": "1.1.1", @@ -17694,7 +17843,8 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true }, "iterator.prototype": { "version": "1.1.5", @@ -17714,6 +17864,7 @@ "version": "2.3.6", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, "requires": { "@isaacs/cliui": "^8.0.2", "@pkgjs/parseargs": "^0.11.0" @@ -17722,7 +17873,8 @@ "jiti": { "version": "1.21.6", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", - "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==" + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", + "dev": true }, "js-cookie": { "version": "3.0.5", @@ -17846,12 +17998,14 @@ "lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==" + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true }, "lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true }, "locate-path": { "version": "6.0.0", @@ -17884,7 +18038,8 @@ "lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true }, "lucia": { "version": "3.2.0", @@ -17897,8 +18052,7 @@ "lucide-react": { "version": "0.437.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.437.0.tgz", - "integrity": "sha512-RXQq6tnm1FlXDUtOwLaoXET2TOEGpQULrQlPOjGHgIVsPhicHNat9sWF33OAe2UCLMFiWF1oL+FtAg43BqVY4Q==", - "requires": {} + "integrity": "sha512-RXQq6tnm1FlXDUtOwLaoXET2TOEGpQULrQlPOjGHgIVsPhicHNat9sWF33OAe2UCLMFiWF1oL+FtAg43BqVY4Q==" }, "math-intrinsics": { "version": "1.1.0", @@ -18038,7 +18192,8 @@ "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true }, "micromark": { "version": "4.0.0", @@ -18255,6 +18410,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, "requires": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -18291,7 +18447,8 @@ "minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==" + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true }, "ms": { "version": "2.1.2", @@ -18302,6 +18459,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, "requires": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", @@ -18365,8 +18523,12 @@ "next-themes": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.3.0.tgz", - "integrity": "sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==", - "requires": {} + "integrity": "sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==" + }, + "nextjs-basic-auth-middleware": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nextjs-basic-auth-middleware/-/nextjs-basic-auth-middleware-3.1.0.tgz", + "integrity": "sha512-k1GSYiLMYj4tK6swAmArTUBheSkv7roUUF2KI8Cv8HYHmSYep5Y0AP0RB/1sez7xTRi2Fw2SxYbQA/jAmBJCug==" }, "node-fetch": { "version": "2.7.0", @@ -18384,7 +18546,8 @@ "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true }, "object-assign": { "version": "4.1.1", @@ -18394,7 +18557,8 @@ "object-hash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==" + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true }, "object-inspect": { "version": "1.13.3", @@ -18471,7 +18635,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "devOptional": true + "dev": true }, "once": { "version": "1.4.0", @@ -18693,17 +18857,20 @@ "path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true }, "path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true }, "path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, "requires": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -18779,13 +18946,12 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", - "devOptional": true + "dev": true }, "pg-pool": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.1.tgz", - "integrity": "sha512-xIOsFoh7Vdhojas6q3596mXFsR8nwBQBXX5JiV7p9buEVAGqYL4yFzclON5P9vFrpu1u7Zwl2oriyDa89n0wbw==", - "requires": {} + "integrity": "sha512-xIOsFoh7Vdhojas6q3596mXFsR8nwBQBXX5JiV7p9buEVAGqYL4yFzclON5P9vFrpu1u7Zwl2oriyDa89n0wbw==" }, "pg-protocol": { "version": "1.7.1", @@ -18796,7 +18962,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz", "integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==", - "devOptional": true, + "dev": true, "requires": { "pg-int8": "1.0.1", "pg-numeric": "1.0.2", @@ -18823,17 +18989,20 @@ "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true }, "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==" + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true }, "pirates": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==" + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true }, "possible-typed-array-names": { "version": "1.0.0", @@ -18845,6 +19014,7 @@ "version": "8.4.41", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", + "dev": true, "requires": { "nanoid": "^3.3.7", "picocolors": "^1.0.1", @@ -18855,6 +19025,7 @@ "version": "15.1.0", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, "requires": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", @@ -18865,6 +19036,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, "requires": { "camelcase-css": "^2.0.1" } @@ -18873,6 +19045,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, "requires": { "lilconfig": "^3.0.0", "yaml": "^2.3.4" @@ -18881,7 +19054,8 @@ "lilconfig": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", - "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==" + "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", + "dev": true } } }, @@ -18889,6 +19063,7 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, "requires": { "postcss-selector-parser": "^6.1.1" } @@ -18897,6 +19072,7 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, "requires": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -18905,7 +19081,8 @@ "postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true }, "postgres": { "version": "3.4.4", @@ -18916,13 +19093,13 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==", - "devOptional": true + "dev": true }, "postgres-bytea": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", - "devOptional": true, + "dev": true, "requires": { "obuf": "~1.1.2" } @@ -18931,19 +19108,19 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz", "integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==", - "devOptional": true + "dev": true }, "postgres-interval": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", - "devOptional": true + "dev": true }, "postgres-range": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz", "integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==", - "devOptional": true + "dev": true }, "prelude-ls": { "version": "1.2.1", @@ -18988,7 +19165,8 @@ "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true }, "react": { "version": "19.0.0", @@ -19016,8 +19194,7 @@ "react-hook-form": { "version": "7.53.0", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.53.0.tgz", - "integrity": "sha512-M1n3HhqCww6S2hxLxciEXy2oISPnAzxY7gvwVPrtlczTM/1dDadXgUxDpHMrMTblDOcm/AXtXxHwZ3jpg1mqKQ==", - "requires": {} + "integrity": "sha512-M1n3HhqCww6S2hxLxciEXy2oISPnAzxY7gvwVPrtlczTM/1dDadXgUxDpHMrMTblDOcm/AXtXxHwZ3jpg1mqKQ==" }, "react-is": { "version": "16.13.1", @@ -19063,6 +19240,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, "requires": { "pify": "^2.3.0" } @@ -19071,6 +19249,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, "requires": { "picomatch": "^2.2.1" } @@ -19132,6 +19311,7 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, "requires": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -19153,7 +19333,8 @@ "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true }, "rimraf": { "version": "3.0.2", @@ -19184,6 +19365,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, "requires": { "queue-microtask": "^1.2.2" } @@ -19309,6 +19491,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "requires": { "shebang-regex": "^3.0.0" } @@ -19316,7 +19499,8 @@ "shebang-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true }, "side-channel": { "version": "1.1.0", @@ -19365,7 +19549,8 @@ "signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==" + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true }, "simple-swizzle": { "version": "0.2.2", @@ -19379,8 +19564,7 @@ "sonner": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.5.0.tgz", - "integrity": "sha512-FBjhG/gnnbN6FY0jaNnqZOMmB73R+5IiyYAw8yBj7L54ER7HB3fOSE5OFiQiE2iXWxeXKvg6fIP4LtVppHEdJA==", - "requires": {} + "integrity": "sha512-FBjhG/gnnbN6FY0jaNnqZOMmB73R+5IiyYAw8yBj7L54ER7HB3fOSE5OFiQiE2iXWxeXKvg6fIP4LtVppHEdJA==" }, "source-map": { "version": "0.6.1", @@ -19422,6 +19606,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, "requires": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -19431,12 +19616,14 @@ "ansi-regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==" + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true }, "strip-ansi": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, "requires": { "ansi-regex": "^6.0.1" } @@ -19447,6 +19634,7 @@ "version": "npm:string-width@4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -19456,7 +19644,8 @@ "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true } } }, @@ -19553,6 +19742,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "requires": { "ansi-regex": "^5.0.1" } @@ -19561,6 +19751,7 @@ "version": "npm:strip-ansi@6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "requires": { "ansi-regex": "^5.0.1" } @@ -19602,6 +19793,7 @@ "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, "requires": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", @@ -19624,7 +19816,8 @@ "supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true }, "tailwind-merge": { "version": "2.5.2", @@ -19635,6 +19828,7 @@ "version": "3.4.10", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz", "integrity": "sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==", + "dev": true, "requires": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -19663,8 +19857,7 @@ "tailwindcss-animate": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", - "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", - "requires": {} + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==" }, "tapable": { "version": "2.2.1", @@ -19682,6 +19875,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, "requires": { "any-promise": "^1.0.0" } @@ -19690,6 +19884,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, "requires": { "thenify": ">= 3.1.0 < 4" } @@ -19698,6 +19893,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "requires": { "is-number": "^7.0.0" } @@ -19721,13 +19917,13 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", - "dev": true, - "requires": {} + "dev": true }, "ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true }, "tsconfig-paths": { "version": "3.15.0", @@ -19836,7 +20032,7 @@ "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "devOptional": true + "dev": true }, "unified": { "version": "11.0.5", @@ -19929,7 +20125,8 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true }, "uuid": { "version": "11.0.3", @@ -19972,6 +20169,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "requires": { "isexe": "^2.0.0" } @@ -20046,6 +20244,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, "requires": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -20055,17 +20254,20 @@ "ansi-regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==" + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true }, "ansi-styles": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==" + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true }, "strip-ansi": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, "requires": { "ansi-regex": "^6.0.1" } @@ -20076,6 +20278,7 @@ "version": "npm:wrap-ansi@7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "requires": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -20085,12 +20288,14 @@ "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true }, "string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -20113,7 +20318,8 @@ "yaml": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", - "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==" + "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==", + "dev": true }, "yocto-queue": { "version": "0.1.0", diff --git a/app/package.json b/app/package.json index f0b942b..7edf9bc 100644 --- a/app/package.json +++ b/app/package.json @@ -40,6 +40,7 @@ "next": "15.1.6", "next-client-cookies": "^2.0.1", "next-themes": "^0.3.0", + "nextjs-basic-auth-middleware": "^3.1.0", "nodemailer": "^7.0.5", "pg": "^8.13.3", "postgres": "^3.4.4", diff --git a/app/src/Validators/admin.ts b/app/src/Validators/admin.ts new file mode 100644 index 0000000..0606eb2 --- /dev/null +++ b/app/src/Validators/admin.ts @@ -0,0 +1,8 @@ +import { z } from "zod"; + +export const adminCredentialsSchema = z.object({ + username: z.string().min(1, { message: "Username is required" }).trim(), + password: z.string().min(1, { message: "Password is required" }), +}); + +export type AdminCredentialsRequest = z.infer; diff --git a/app/src/app/admin/error-logs/page.tsx b/app/src/app/admin/error-logs/page.tsx new file mode 100644 index 0000000..4e31634 --- /dev/null +++ b/app/src/app/admin/error-logs/page.tsx @@ -0,0 +1,526 @@ +"use client"; + +import { useState, useEffect, useCallback, useRef } from "react"; +import { useQuery } from "@tanstack/react-query"; +import axios from "axios"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Modal } from "@/components/ui/modal"; +import { Download, RefreshCw, AlertCircle, Eye, LogOut } from "lucide-react"; +import { toast } from "sonner"; +import { format } from "date-fns"; +import type { ErrorLog, ErrorStats } from "@/types/error-logs"; +import { ADMIN_CREDENTIALS_STORAGE_KEY, ADMIN_ERROR_LOGS_PAGE_SIZE } from "@/constants/admin"; +import { getBasicAuthHeaders } from "@/lib/admin-error-logs-auth"; +import AdminErrorLogsLoginForm from "@/components/AdminErrorLogsLoginForm"; +import type { AdminCredentialsRequest } from "@/Validators/admin"; + +export default function ErrorLogsPage() { + // Always start as null so server and first client render match (avoids hydration error). + // Restore from sessionStorage in useEffect after mount. + const [credentials, setCredentials] = useState<{ username: string; password: string } | null>(null); + const [loginError, setLoginError] = useState(null); + + const [filters, setFilters] = useState({ + endpoint: "", + failure_stage: "", + error_type: "", + }); + const [page, setPage] = useState(0); + const [selectedLog, setSelectedLog] = useState(null); + const [isDetailOpen, setIsDetailOpen] = useState(false); + const [audioBlobUrl, setAudioBlobUrl] = useState(null); + const audioBlobUrlRef = useRef(null); + + const authHeaders = getBasicAuthHeaders(credentials); + + // Restore credentials from sessionStorage after mount (client-only) to avoid hydration mismatch. + useEffect(() => { + try { + const stored = sessionStorage.getItem(ADMIN_CREDENTIALS_STORAGE_KEY); + if (!stored) return; + const parsed = JSON.parse(stored) as { username: string; password: string }; + if (parsed?.username && parsed?.password) { + setCredentials(parsed); + } + } catch { + // ignore + } + }, []); + + const saveCredentials = useCallback((user: string, pass: string) => { + const c = { username: user, password: pass }; + setCredentials(c); + try { + sessionStorage.setItem(ADMIN_CREDENTIALS_STORAGE_KEY, JSON.stringify(c)); + } catch {} + }, []); + + const clearCredentials = useCallback(() => { + setCredentials(null); + setLoginError(null); + try { + sessionStorage.removeItem(ADMIN_CREDENTIALS_STORAGE_KEY); + } catch {} + }, []); + + // Fetch error logs (only when credentials are set) + const { data, isLoading, error, refetch } = useQuery({ + queryKey: ["error-logs", page, ADMIN_ERROR_LOGS_PAGE_SIZE, filters, credentials], + queryFn: async () => { + const params = new URLSearchParams({ + skip: String(page * ADMIN_ERROR_LOGS_PAGE_SIZE), + limit: String(ADMIN_ERROR_LOGS_PAGE_SIZE), + }); + if (filters.endpoint) params.append("endpoint", filters.endpoint); + if (filters.failure_stage) params.append("failure_stage", filters.failure_stage); + if (filters.error_type) params.append("error_type", filters.error_type); + const response = await axios.get(`/api/admin/error-logs?${params.toString()}`, { + headers: authHeaders, + }); + return response.data; + }, + enabled: !!credentials?.username && !!credentials?.password, + }); + + // Fetch stats + const { data: stats } = useQuery({ + queryKey: ["error-stats", credentials], + queryFn: async () => { + const response = await axios.get("/api/admin/error-logs/stats", { + headers: authHeaders, + }); + return response.data as ErrorStats; + }, + enabled: !!credentials?.username && !!credentials?.password, + }); + + // Clear credentials on 401 + useEffect(() => { + if (error && axios.isAxiosError(error) && error.response?.status === 401) { + clearCredentials(); + toast.error("Session expired. Please sign in again."); + } + }, [error, clearCredentials]); + + // Load audio as blob when modal opens with audio (so we can send auth header) + useEffect(() => { + if (!selectedLog?.audio_storage_path || !credentials) { + if (audioBlobUrlRef.current) { + URL.revokeObjectURL(audioBlobUrlRef.current); + audioBlobUrlRef.current = null; + setAudioBlobUrl(null); + } + return; + } + const path = selectedLog.audio_storage_path; + const headers = getBasicAuthHeaders(credentials); + let cancelled = false; + fetch(`/api/admin/error-logs/audio/${encodeURIComponent(path)}`, { headers }) + .then((res) => (res.ok ? res.blob() : Promise.reject(new Error("Failed to load audio")))) + .then((blob) => { + if (cancelled) return; + if (audioBlobUrlRef.current) URL.revokeObjectURL(audioBlobUrlRef.current); + const url = URL.createObjectURL(blob); + audioBlobUrlRef.current = url; + setAudioBlobUrl(url); + }) + .catch(() => { + if (!cancelled) setAudioBlobUrl(null); + }); + return () => { + cancelled = true; + if (audioBlobUrlRef.current) { + URL.revokeObjectURL(audioBlobUrlRef.current); + audioBlobUrlRef.current = null; + setAudioBlobUrl(null); + } + }; + }, [selectedLog?.audio_storage_path, credentials?.username, credentials?.password]); + + const handleLogin = async (data: AdminCredentialsRequest) => { + setLoginError(null); + try { + const testRes = await axios.get("/api/admin/error-logs?skip=0&limit=1", { + headers: getBasicAuthHeaders({ username: data.username, password: data.password }), + }); + if (testRes.status === 200) { + saveCredentials(data.username, data.password); + toast.success("Signed in successfully."); + } + } catch (err: unknown) { + if (axios.isAxiosError(err)) { + const status = err.response?.status; + const data = err.response?.data as { error?: string; detail?: string } | undefined; + const detail = data?.error ?? data?.detail ?? "Invalid credentials."; + setLoginError(status === 401 ? "Invalid username or password." : detail); + } else { + setLoginError("Invalid credentials."); + } + } + }; + + const handleExport = async () => { + try { + const params = new URLSearchParams(); + if (filters.endpoint) params.append("endpoint", filters.endpoint); + if (filters.failure_stage) params.append("failure_stage", filters.failure_stage); + if (filters.error_type) params.append("error_type", filters.error_type); + + const response = await axios.get(`/api/admin/error-logs/export?${params.toString()}`, { + responseType: "blob", + headers: authHeaders, + }); + + // Create download link + const url = window.URL.createObjectURL(new Blob([response.data])); + const link = document.createElement("a"); + link.href = url; + link.setAttribute("download", `error_logs_${new Date().toISOString().split("T")[0]}.csv`); + document.body.appendChild(link); + link.click(); + link.remove(); + window.URL.revokeObjectURL(url); + + toast.success("Error logs exported successfully"); + } catch (err: unknown) { + toast.error("Failed to export error logs"); + console.error(err); + } + }; + + // Show login form when no credentials (or after 401 clear) + if (!credentials?.username || !credentials?.password) { + return ( + + ); + } + + if (error) { + return ( +
+ +
+ +

Failed to load error logs. Please check your permissions.

+
+
+
+ ); + } + + return ( +
+
+

API Error Logs

+
+ + + +
+
+ + {/* Stats Cards */} + {stats && ( +
+ +
Total Errors
+
{stats.total_errors}
+
+ +
Failure Stages
+
{Object.keys(stats.by_failure_stage).length}
+
+ +
Error Types
+
{Object.keys(stats.top_error_types).length}
+
+ +
Endpoints
+
{Object.keys(stats.by_endpoint).length}
+
+
+ )} + + {/* Filters */} + +
+
+ setFilters({ ...filters, endpoint: e.target.value })} + /> +
+
+ +
+
+ +
+
+
+ + {/* Error Logs Table */} + +
+ + + + ID + Timestamp + Endpoint + Failure Stage + Error Type + Error Message + Audio File + Translated Text + Actions + + + + {isLoading ? ( + + + Loading... + + + ) : !data?.data?.length ? ( + + + No error logs found + + + ) : ( + data?.data?.map((log: ErrorLog) => ( + + {log.id} + + {log.created_at + ? format(new Date(log.created_at), "MMM dd, yyyy HH:mm:ss") + : "-"} + + {log.endpoint} + + + {log.failure_stage} + + + {log.error_type} + + {log.error_message} + + + {log.audio_file_name || "-"} + + + {log.translated_text || "-"} + + + + + + )) + )} + +
+
+ + {/* Pagination */} + {data && data.total > ADMIN_ERROR_LOGS_PAGE_SIZE && ( +
+
+ Showing {page * ADMIN_ERROR_LOGS_PAGE_SIZE + 1} to {Math.min((page + 1) * ADMIN_ERROR_LOGS_PAGE_SIZE, data.total)} of{" "} + {data.total} errors +
+
+ + +
+
+ )} +
+ + {/* Error Detail Modal */} + setIsDetailOpen(false)} + title="Error Log Details" + className="max-w-4xl max-h-[90vh] overflow-y-auto" + > + {selectedLog && ( +
+
+
+ +

{selectedLog.id}

+
+
+ +

+ {selectedLog.created_at + ? format(new Date(selectedLog.created_at), "MMM dd, yyyy HH:mm:ss") + : "-"} +

+
+
+ +

{selectedLog.endpoint}

+
+
+ +

+ + {selectedLog.failure_stage || "-"} + +

+
+
+ +

{selectedLog.error_type}

+
+
+ +

{selectedLog.audio_file_name || "-"}

+
+
+ +

{selectedLog.detected_language || "-"}

+
+
+ +
+ +
+                {selectedLog.error_message}
+              
+
+ + {selectedLog.audio_storage_path && ( +
+ + + {!audioBlobUrl &&

Loading audio…

} +
+ )} + + {selectedLog.translated_text && ( +
+ +

{selectedLog.translated_text}

+
+ )} + + {selectedLog.intent_data && ( +
+ +
+                  {JSON.stringify(selectedLog.intent_data, null, 2)}
+                
+
+ )} + + {selectedLog.preprocessing_logs_text && ( +
+ +
+                  {selectedLog.preprocessing_logs_text}
+                
+
+ )} + + {selectedLog.error_traceback && ( +
+ +
+                  {selectedLog.error_traceback}
+                
+
+ )} +
+ )} +
+
+ ); +} diff --git a/app/src/app/admin/layout.tsx b/app/src/app/admin/layout.tsx new file mode 100644 index 0000000..615c85f --- /dev/null +++ b/app/src/app/admin/layout.tsx @@ -0,0 +1,11 @@ +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Admin | Lingo.ai", +}; + +export default function AdminLayout({ + children, +}: Readonly<{ children: React.ReactNode }>) { + return <>{children}; +} diff --git a/app/src/app/api/admin/error-logs/audio/[filename]/route.ts b/app/src/app/api/admin/error-logs/audio/[filename]/route.ts new file mode 100644 index 0000000..e67e74a --- /dev/null +++ b/app/src/app/api/admin/error-logs/audio/[filename]/route.ts @@ -0,0 +1,53 @@ +import { NextRequest, NextResponse } from "next/server"; +import { validateRequest } from "@/auth"; +import { getAdminBasicAuthHeaders } from "@/lib/admin-auth"; + +export async function GET( + req: NextRequest, + { params }: { params: Promise<{ filename: string }> } +) { + try { + const { user } = await validateRequest(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { filename } = await params; + if (!filename) { + return NextResponse.json({ error: "Missing filename" }, { status: 400 }); + } + + const microserviceUrl = process.env.NEXT_PUBLIC_MICROSERVICE_URL || "http://localhost:8000"; + const url = `${microserviceUrl}/admin/error-logs/audio/${encodeURIComponent(filename)}`; + + const authHeader = req.headers.get("authorization"); + const headers = authHeader + ? { Authorization: authHeader } + : getAdminBasicAuthHeaders(); + const response = await fetch(url, { + cache: "no-store", + headers, + }); + if (!response.ok) { + return NextResponse.json( + { error: "Audio not found" }, + { status: response.status } + ); + } + + const contentType = response.headers.get("content-type") || "audio/wav"; + const arrayBuffer = await response.arrayBuffer(); + + return new NextResponse(arrayBuffer, { + headers: { + "Content-Type": contentType, + }, + }); + } catch (error: any) { + console.error("Error fetching error log audio:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} diff --git a/app/src/app/api/admin/error-logs/export/route.ts b/app/src/app/api/admin/error-logs/export/route.ts new file mode 100644 index 0000000..7542e82 --- /dev/null +++ b/app/src/app/api/admin/error-logs/export/route.ts @@ -0,0 +1,69 @@ +import { NextRequest, NextResponse } from "next/server"; +import { validateRequest } from "@/auth"; +import { getAdminBasicAuthHeaders } from "@/lib/admin-auth"; +import axios from "axios"; + +export async function GET(req: NextRequest) { + try { + // Check authentication + const { user } = await validateRequest(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + // TODO: Add admin role check later when role system is implemented + // For now, any authenticated user can export error logs + + // Get query parameters + const { searchParams } = new URL(req.url); + const params = new URLSearchParams(); + + // Forward all query parameters + searchParams.forEach((value, key) => { + params.append(key, value); + }); + + // Proxy request to FastAPI backend + const microserviceUrl = process.env.NEXT_PUBLIC_MICROSERVICE_URL || "http://localhost:8000"; + const url = `${microserviceUrl}/admin/error-logs/export?${params.toString()}`; + + const authHeader = req.headers.get("authorization"); + const headers = authHeader + ? { Authorization: authHeader } + : getAdminBasicAuthHeaders(); + const response = await axios.get(url, { + responseType: "arraybuffer", + headers, + }); + + // Get filename from Content-Disposition header or generate one + const contentDisposition = response.headers["content-disposition"]; + let filename = "error_logs.csv"; + if (contentDisposition) { + const filenameMatch = contentDisposition.match(/filename="?(.+)"?/); + if (filenameMatch) { + filename = filenameMatch[1]; + } + } + + // Return CSV file + return new NextResponse(response.data, { + headers: { + "Content-Type": "text/csv", + "Content-Disposition": `attachment; filename="${filename}"`, + }, + }); + } catch (error: any) { + console.error("Error exporting error logs:", error); + if (error.response) { + return NextResponse.json( + { error: error.response.data?.detail || "Failed to export error logs" }, + { status: error.response.status } + ); + } + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} diff --git a/app/src/app/api/admin/error-logs/route.ts b/app/src/app/api/admin/error-logs/route.ts new file mode 100644 index 0000000..b7b8dde --- /dev/null +++ b/app/src/app/api/admin/error-logs/route.ts @@ -0,0 +1,49 @@ +import { NextRequest, NextResponse } from "next/server"; +import { validateRequest } from "@/auth"; +import { getAdminBasicAuthHeaders } from "@/lib/admin-auth"; +import axios from "axios"; + +export async function GET(req: NextRequest) { + try { + // Check authentication + const { user } = await validateRequest(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + // TODO: Add admin role check later when role system is implemented + // For now, any authenticated user can access error logs + + // Get query parameters + const { searchParams } = new URL(req.url); + const params = new URLSearchParams(); + + // Forward all query parameters + searchParams.forEach((value, key) => { + params.append(key, value); + }); + + // Proxy request to FastAPI backend + const microserviceUrl = process.env.NEXT_PUBLIC_MICROSERVICE_URL || "http://localhost:8000"; + const url = `${microserviceUrl}/admin/error-logs?${params.toString()}`; + + const authHeader = req.headers.get("authorization"); + const headers = authHeader + ? { Authorization: authHeader } + : getAdminBasicAuthHeaders(); + const response = await axios.get(url, { headers }); + return NextResponse.json(response.data); + } catch (error: any) { + console.error("Error fetching error logs:", error); + if (error.response) { + return NextResponse.json( + { error: error.response.data?.detail || "Failed to fetch error logs" }, + { status: error.response.status } + ); + } + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} diff --git a/app/src/app/api/admin/error-logs/stats/route.ts b/app/src/app/api/admin/error-logs/stats/route.ts new file mode 100644 index 0000000..dba93de --- /dev/null +++ b/app/src/app/api/admin/error-logs/stats/route.ts @@ -0,0 +1,40 @@ +import { NextRequest, NextResponse } from "next/server"; +import { validateRequest } from "@/auth"; +import { getAdminBasicAuthHeaders } from "@/lib/admin-auth"; +import axios from "axios"; + +export async function GET(req: NextRequest) { + try { + // Check authentication + const { user } = await validateRequest(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + // TODO: Add admin role check later when role system is implemented + // For now, any authenticated user can access error stats + + // Proxy request to FastAPI backend + const microserviceUrl = process.env.NEXT_PUBLIC_MICROSERVICE_URL || "http://localhost:8000"; + const url = `${microserviceUrl}/admin/error-logs/stats`; + + const authHeader = req.headers.get("authorization"); + const headers = authHeader + ? { Authorization: authHeader } + : getAdminBasicAuthHeaders(); + const response = await axios.get(url, { headers }); + return NextResponse.json(response.data); + } catch (error: any) { + console.error("Error fetching error stats:", error); + if (error.response) { + return NextResponse.json( + { error: error.response.data?.detail || "Failed to fetch error stats" }, + { status: error.response.status } + ); + } + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} diff --git a/app/src/app/api/forgot-password/route.ts b/app/src/app/api/forgot-password/route.ts index eebc99f..91d7b5a 100644 --- a/app/src/app/api/forgot-password/route.ts +++ b/app/src/app/api/forgot-password/route.ts @@ -34,7 +34,7 @@ export async function POST(req: NextRequest) { set: { token, expiresAt }, }); - // Send email + // Email sending code commented out for now const transporter = nodemailer.createTransport({ host: process.env.SMTP_HOST, port: Number(process.env.SMTP_PORT), diff --git a/app/src/components/AdminErrorLogsLoginForm.tsx b/app/src/components/AdminErrorLogsLoginForm.tsx new file mode 100644 index 0000000..619b164 --- /dev/null +++ b/app/src/components/AdminErrorLogsLoginForm.tsx @@ -0,0 +1,98 @@ +"use client"; + +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { AlertCircle } from "lucide-react"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { adminCredentialsSchema, type AdminCredentialsRequest } from "@/Validators/admin"; + +interface AdminErrorLogsLoginFormProps { + onLogin: (data: AdminCredentialsRequest) => Promise; + loginError: string | null; +} + +export default function AdminErrorLogsLoginForm({ + onLogin, + loginError, +}: AdminErrorLogsLoginFormProps) { + const form = useForm({ + resolver: zodResolver(adminCredentialsSchema), + defaultValues: { username: "", password: "" }, + mode: "all", + }); + + return ( +
+ +

Admin Error Logs

+

+ Sign in with the admin credentials for the error-logs API. +

+
+ onLogin(data))} + className="space-y-4" + > + ( + + Username + + + + + + )} + /> + ( + + Password + + + + + + )} + /> + {loginError && ( +

+ + {loginError} +

+ )} + + + +
+
+ ); +} diff --git a/app/src/constants/admin.ts b/app/src/constants/admin.ts new file mode 100644 index 0000000..260af42 --- /dev/null +++ b/app/src/constants/admin.ts @@ -0,0 +1,5 @@ +/** SessionStorage key for admin error-logs credentials (client-only). */ +export const ADMIN_CREDENTIALS_STORAGE_KEY = "admin-error-logs-credentials"; + +/** Default page size for admin error-logs table. */ +export const ADMIN_ERROR_LOGS_PAGE_SIZE = 50; diff --git a/app/src/lib/admin-auth.ts b/app/src/lib/admin-auth.ts new file mode 100644 index 0000000..5606c7a --- /dev/null +++ b/app/src/lib/admin-auth.ts @@ -0,0 +1,11 @@ +/** + * Server-only: build Basic auth headers for proxying to the admin microservice. + * Set ADMIN_BASIC_AUTH_USER and ADMIN_BASIC_AUTH_PASSWORD in env (no NEXT_PUBLIC_). + */ +export function getAdminBasicAuthHeaders(): Record { + const user = process.env.ADMIN_BASIC_AUTH_USER; + const password = process.env.ADMIN_BASIC_AUTH_PASSWORD; + if (!user || !password) return {}; + const encoded = Buffer.from(`${user}:${password}`, "utf-8").toString("base64"); + return { Authorization: `Basic ${encoded}` }; +} diff --git a/app/src/lib/admin-error-logs-auth.ts b/app/src/lib/admin-error-logs-auth.ts new file mode 100644 index 0000000..b8bf3ad --- /dev/null +++ b/app/src/lib/admin-error-logs-auth.ts @@ -0,0 +1,13 @@ +/** + * Client-only: build Basic auth headers for admin error-logs API calls. + * Used by the error-logs page when sending credentials from the login form. + */ + +export function getBasicAuthHeaders( + credentials: { username: string; password: string } | null +): Record { + if (!credentials?.username || !credentials?.password) return {}; + return { + Authorization: `Basic ${btoa(`${credentials.username}:${credentials.password}`)}`, + }; +} diff --git a/app/src/types/error-logs.ts b/app/src/types/error-logs.ts new file mode 100644 index 0000000..2fe30b9 --- /dev/null +++ b/app/src/types/error-logs.ts @@ -0,0 +1,22 @@ +export interface ErrorLog { + id: number; + endpoint: string; + audio_file_name: string | null; + audio_storage_path: string | null; + translated_text: string | null; + detected_language: string | null; + error_type: string; + error_message: string; + error_traceback: string | null; + failure_stage: string | null; + preprocessing_logs_text: string | null; + intent_data: unknown; + created_at: string; +} + +export interface ErrorStats { + total_errors: number; + by_failure_stage: Record; + top_error_types: Record; + by_endpoint: Record; +} diff --git a/service/admin_routes.py b/service/admin_routes.py new file mode 100644 index 0000000..6f72966 --- /dev/null +++ b/service/admin_routes.py @@ -0,0 +1,240 @@ +import os +import re +from fastapi import APIRouter, Query, Depends, HTTPException +from fastapi.responses import JSONResponse, Response, FileResponse +from banking.database import SessionLocal +from banking.error_logs import APIErrorLog +from error_logger import ERROR_AUDIO_DIR +from sqlalchemy import desc, func +from typing import Optional +from datetime import datetime +import csv +import io +from logger import logger + +router = APIRouter(prefix="/admin", tags=["admin"]) + + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + + +@router.get("/error-logs") +async def get_error_logs( + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=1000), + endpoint: Optional[str] = None, + failure_stage: Optional[str] = None, + error_type: Optional[str] = None, + session_id: Optional[str] = None, + start_date: Optional[str] = None, + end_date: Optional[str] = None, + db = Depends(get_db), +): + """ + Get paginated list of error logs with optional filters. + Access controlled by Next.js middleware and API route session auth. + """ + try: + query = db.query(APIErrorLog) + + # Apply filters + if endpoint: + query = query.filter(APIErrorLog.endpoint.contains(endpoint)) + if failure_stage: + query = query.filter(APIErrorLog.failure_stage == failure_stage) + if error_type: + query = query.filter(APIErrorLog.error_type == error_type) + if session_id: + query = query.filter(APIErrorLog.session_id == session_id) + if start_date: + try: + start_dt = datetime.fromisoformat(start_date.replace('Z', '+00:00')) + query = query.filter(APIErrorLog.created_at >= start_dt) + except ValueError: + pass + if end_date: + try: + end_dt = datetime.fromisoformat(end_date.replace('Z', '+00:00')) + query = query.filter(APIErrorLog.created_at <= end_dt) + except ValueError: + pass + + # Get total count + total = query.count() + + # Apply pagination and ordering + error_logs = query.order_by(desc(APIErrorLog.created_at)).offset(skip).limit(limit).all() + + # Convert to dict + logs_data = [log.to_dict() for log in error_logs] + + return JSONResponse(content={ + "total": total, + "skip": skip, + "limit": limit, + "data": logs_data + }) + except Exception as e: + logger.error(f"Error fetching error logs: {e}") + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/error-logs/stats") +async def get_error_stats( + db = Depends(get_db), +): + """ + Get statistics about error logs. + """ + try: + total_errors = db.query(func.count(APIErrorLog.id)).scalar() + + # Errors by failure stage + stage_stats = db.query( + APIErrorLog.failure_stage, + func.count(APIErrorLog.id).label('count') + ).group_by(APIErrorLog.failure_stage).all() + + # Errors by error type + type_stats = db.query( + APIErrorLog.error_type, + func.count(APIErrorLog.id).label('count') + ).group_by(APIErrorLog.error_type).order_by(desc('count')).limit(10).all() + + # Errors by endpoint + endpoint_stats = db.query( + APIErrorLog.endpoint, + func.count(APIErrorLog.id).label('count') + ).group_by(APIErrorLog.endpoint).order_by(desc('count')).all() + + return JSONResponse(content={ + "total_errors": total_errors, + "by_failure_stage": {stage: count for stage, count in stage_stats}, + "top_error_types": {error_type: count for error_type, count in type_stats}, + "by_endpoint": {endpoint: count for endpoint, count in endpoint_stats}, + }) + except Exception as e: + logger.error(f"Error fetching error stats: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/error-logs/export") +async def export_error_logs_csv( + endpoint: Optional[str] = None, + failure_stage: Optional[str] = None, + error_type: Optional[str] = None, + start_date: Optional[str] = None, + end_date: Optional[str] = None, + db = Depends(get_db), +): + """ + Export error logs as CSV file. + """ + try: + query = db.query(APIErrorLog) + + # Apply same filters as get_error_logs + if endpoint: + query = query.filter(APIErrorLog.endpoint.contains(endpoint)) + if failure_stage: + query = query.filter(APIErrorLog.failure_stage == failure_stage) + if error_type: + query = query.filter(APIErrorLog.error_type == error_type) + if start_date: + try: + start_dt = datetime.fromisoformat(start_date.replace('Z', '+00:00')) + query = query.filter(APIErrorLog.created_at >= start_dt) + except ValueError: + pass + if end_date: + try: + end_dt = datetime.fromisoformat(end_date.replace('Z', '+00:00')) + query = query.filter(APIErrorLog.created_at <= end_dt) + except ValueError: + pass + + error_logs = query.order_by(desc(APIErrorLog.created_at)).all() + + # Create CSV in memory + output = io.StringIO() + writer = csv.writer(output) + + # Write header + writer.writerow([ + "ID", "Endpoint", "Error Type", "Error Message", "Failure Stage", + "Audio File Name", "Translated Text", "Detected Language", + "Error Traceback", "Preprocessing Logs", "Intent Data", "Created At" + ]) + + # Write data rows + for log in error_logs: + writer.writerow([ + log.id, + log.endpoint, + log.error_type, + log.error_message[:500] if log.error_message else "", # Truncate long messages + log.failure_stage or "", + log.audio_file_name or "", + log.translated_text or "", + log.detected_language or "", + log.error_traceback[:5000] if log.error_traceback else "", # Full traceback + log.preprocessing_logs_text[:5000] if log.preprocessing_logs_text else "", # Full logs + str(log.intent_data) if log.intent_data else "", + log.created_at.isoformat() if log.created_at else "", + ]) + + # Generate filename with timestamp + filename = f"error_logs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv" + + return Response( + content=output.getvalue(), + media_type="text/csv", + headers={ + "Content-Disposition": f"attachment; filename={filename}" + } + ) + except Exception as e: + logger.error(f"Error exporting error logs: {e}") + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/error-logs/audio/{filename}") +async def get_error_log_audio( + filename: str, +): + """ + Serve stored audio file for an error log (for playback in admin UI). + """ + # Prevent path traversal: only allow safe filenames (hex + extension) + if not re.match(r"^[a-f0-9]{32}\.(wav|mp3|m4a|ogg|webm|flac)$", filename, re.IGNORECASE): + raise HTTPException(status_code=400, detail="Invalid filename") + path = os.path.join(ERROR_AUDIO_DIR, filename) + if not os.path.isfile(path): + raise HTTPException(status_code=404, detail="Audio file not found") + ext = filename.rsplit(".", 1)[-1].lower() + media_types = {"wav": "audio/wav", "mp3": "audio/mpeg", "m4a": "audio/mp4", "ogg": "audio/ogg", "webm": "audio/webm", "flac": "audio/flac"} + media_type = media_types.get(ext, "audio/wav") + return FileResponse(path, media_type=media_type) + +@router.get("/error-logs/{log_id}") +async def get_error_log_detail( + log_id: int, + db = Depends(get_db), +): + """ + Get detailed information about a specific error log. + """ + try: + error_log = db.query(APIErrorLog).filter(APIErrorLog.id == log_id).first() + if not error_log: + raise HTTPException(status_code=404, detail="Error log not found") + + return JSONResponse(content=error_log.to_dict()) + except HTTPException: + raise + except Exception as e: + logger.error(f"Error fetching error log detail: {e}") + raise HTTPException(status_code=500, detail=str(e)) diff --git a/service/audio_service.py b/service/audio_service.py index d8fa066..19ac4f0 100644 --- a/service/audio_service.py +++ b/service/audio_service.py @@ -259,10 +259,27 @@ def translate_with_whisper_from_upload(upload_file: UploadFile): ) except Exception as e: logger.error(f"Translation from upload failed: {str(e)}") - raise HTTPException( - status_code=500, - detail=f"Translation from upload failed: {str(e)}" - ) + # Propagate status code and message from upstream API (e.g. 403 Subscription key not provided) + status_code = 500 + detail = str(e) + if hasattr(e, "status_code") and e.status_code is not None: + status_code = e.status_code + if hasattr(e, "body") and isinstance(e.body, dict): + err = e.body.get("error") or e.body + if isinstance(err, dict) and err.get("message"): + detail = err["message"] + # Parse from message if SDK embeds status_code/body in str(e) + if "status_code:" in detail: + import re + m = re.search(r"status_code:\s*(\d+)", detail) + if m: + status_code = int(m.group(1)) + m = re.search(r"body:\s*\{[^}]*'message':\s*'([^']+)'", detail) + if not m: + m = re.search(r'"message":\s*"([^"]+)"', detail) + if m: + detail = m.group(1) + raise HTTPException(status_code=status_code, detail=detail) finally: # Clean up temporary file if temp_file_path and os.path.exists(temp_file_path): diff --git a/service/banking/__init__.py b/service/banking/__init__.py index 7e908ea..8b8c364 100644 --- a/service/banking/__init__.py +++ b/service/banking/__init__.py @@ -1,5 +1,36 @@ # Initialize the banking package from .database import Base, engine +# Import models to ensure they're registered +from . import models +from . import error_logs + # Create tables when the package is imported -Base.metadata.create_all(bind=engine) \ No newline at end of file +Base.metadata.create_all(bind=engine) + +# Run migration to add new columns to existing table +# This runs after table creation to add new columns +def run_migration(): + """Run migration in a separate function to avoid circular imports""" + try: + import sys + import os + # Add parent directory to path to import migrate_error_logs + parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + if parent_dir not in sys.path: + sys.path.insert(0, parent_dir) + from migrate_error_logs import migrate_error_logs_table + migrate_error_logs_table() + except Exception as e: + # Migration errors are logged but don't stop the app + import logging + logging.getLogger(__name__).warning(f"Error running migration (non-critical): {e}") + +# Run migration after a short delay to ensure all imports are complete +import threading +def delayed_migration(): + import time + time.sleep(1) # Wait 1 second for all imports to complete + run_migration() + +threading.Thread(target=delayed_migration, daemon=True).start() \ No newline at end of file diff --git a/service/banking/error_logs.py b/service/banking/error_logs.py new file mode 100644 index 0000000..4cb0d4b --- /dev/null +++ b/service/banking/error_logs.py @@ -0,0 +1,53 @@ +from sqlalchemy import Column, Integer, String, Text, DateTime, JSON +from banking.database import Base +import datetime + +class APIErrorLog(Base): + __tablename__ = "api_error_logs" + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + + # API Endpoint Information + endpoint = Column(String(255), nullable=False, index=True) # e.g., "/voice/transcribe-intent" + + # Audio-related data (TTS/STT) + audio_file_name = Column(String(255), nullable=True) # Name of the audio file uploaded + audio_storage_path = Column(String(500), nullable=True) # Path/filename to play back stored audio + translated_text = Column(Text, nullable=True) # Transcribed/translated text from audio + detected_language = Column(String(50), nullable=True) # Language detected from audio + + # Error Details + error_type = Column(String(255), nullable=False) # Exception class name + error_message = Column(Text, nullable=False) + error_traceback = Column(Text, nullable=True) # Full traceback of the error + failure_stage = Column(String(100), nullable=True) # e.g., "transcription", "intent_detection", "session_processing" + + # Logging and preprocessing + preprocessing_logs_text = Column(Text, nullable=True) # Logs from preprocessing steps (TTS/STT functions) + + # Intent data (if available before error) + intent_data = Column(JSON, nullable=True) + + # Timestamps + created_at = Column(DateTime, default=datetime.datetime.utcnow, index=True) + + def __repr__(self): + return f"" + + def to_dict(self): + """Convert to dictionary for API responses""" + return { + "id": self.id, + "endpoint": self.endpoint, + "audio_file_name": self.audio_file_name, + "audio_storage_path": getattr(self, "audio_storage_path", None), + "translated_text": self.translated_text, + "detected_language": self.detected_language, + "error_type": self.error_type, + "error_message": self.error_message, + "error_traceback": self.error_traceback, + "failure_stage": self.failure_stage, + "preprocessing_logs_text": self.preprocessing_logs_text, + "intent_data": self.intent_data, + "created_at": self.created_at.isoformat() if self.created_at else None, + } diff --git a/service/config.py b/service/config.py index c7d4c01..4ec9a60 100644 --- a/service/config.py +++ b/service/config.py @@ -27,3 +27,10 @@ redis_port = int(os.getenv("REDIS_PORT", 6379)) redis_db = int(os.getenv("REDIS_DB", 0)) redis_password = os.getenv("REDIS_PASSWORD", None) + +# Error-log audio storage (failed /voice/transcribe-intent requests; for admin playback) +# Default: next to this config file (service/error_audio). Override with ERROR_AUDIO_DIR for absolute path. +error_audio_dir = os.getenv( + "ERROR_AUDIO_DIR", + os.path.join(os.path.dirname(os.path.abspath(__file__)), "error_audio"), +) diff --git a/service/error_handler.py b/service/error_handler.py new file mode 100644 index 0000000..c1ef2be --- /dev/null +++ b/service/error_handler.py @@ -0,0 +1,162 @@ +""" +Global exception handler for FastAPI +Catches all unhandled exceptions and logs them +""" +from fastapi import Request, status +from fastapi.responses import JSONResponse +from fastapi.exceptions import RequestValidationError, HTTPException +from starlette.exceptions import HTTPException as StarletteHTTPException +import traceback +from logger import logger +from error_logger import log_api_error +from typing import Dict, Any +import json + +async def extract_request_context(request: Request) -> Dict[str, Any]: + """Extract request context for error logging""" + try: + # Get request body if available + request_body = None + try: + if request.method in ["POST", "PUT", "PATCH"]: + body = await request.body() + if body: + try: + request_body = json.loads(body.decode("utf-8")) + except: + request_body = {"raw": body.decode("utf-8")[:1000]} + except: + pass + + # Get query parameters + query_params = dict(request.query_params) if request.query_params else None + + # Get headers (sanitized) + headers = {} + sensitive_headers = {"authorization", "cookie", "x-api-key"} + for key, value in request.headers.items(): + if key.lower() in sensitive_headers: + headers[key] = "[REDACTED]" + else: + headers[key] = value + + return { + "request_body": request_body, + "query_params": query_params, + "headers": headers, + } + except Exception as e: + logger.warning(f"Failed to extract request context: {e}") + return {} + +async def global_exception_handler(request: Request, exc: Exception) -> JSONResponse: + """Global exception handler for all unhandled exceptions""" + try: + # Extract request context + context = await extract_request_context(request) + + # Get correlation ID if set by middleware + correlation_id = getattr(request.state, "correlation_id", None) + + # Log error using the error logger + log_api_error( + endpoint=request.url.path, + error=exc, + failure_stage="exception_handler", + http_method=request.method, + request_headers=context.get("headers"), + request_body=context.get("request_body"), + request_query_params=context.get("query_params"), + response_status_code=500, + correlation_id=correlation_id, + ip_address=request.client.host if request.client else None, + ) + + logger.error(f"Unhandled exception: {traceback.format_exc()}") + + return JSONResponse( + status_code=500, + content={ + "message": "Internal server error", + "error_type": type(exc).__name__, + "correlation_id": correlation_id, + } + ) + except Exception as e: + logger.error(f"Error in global exception handler: {e}") + return JSONResponse( + status_code=500, + content={"message": "Internal server error"} + ) + +async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse: + """Handler for HTTP exceptions (4xx, etc.)""" + try: + context = await extract_request_context(request) + correlation_id = getattr(request.state, "correlation_id", None) + + # Log HTTP errors (4xx and 5xx) + log_api_error( + endpoint=request.url.path, + error=exc, + failure_stage="http_exception", + http_method=request.method, + request_headers=context.get("headers"), + request_body=context.get("request_body"), + request_query_params=context.get("query_params"), + response_status_code=exc.status_code, + response_body=str(exc.detail), + correlation_id=correlation_id, + ip_address=request.client.host if request.client else None, + ) + + return JSONResponse( + status_code=exc.status_code, + content={ + "message": exc.detail, + "correlation_id": correlation_id, + } + ) + except Exception as e: + logger.error(f"Error in HTTP exception handler: {e}") + return JSONResponse( + status_code=exc.status_code, + content={"message": exc.detail} + ) + +async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse: + """Handler for request validation errors""" + try: + context = await extract_request_context(request) + correlation_id = getattr(request.state, "correlation_id", None) + + # Log validation errors + log_api_error( + endpoint=request.url.path, + error=exc, + failure_stage="validation", + http_method=request.method, + request_headers=context.get("headers"), + request_body=context.get("request_body"), + request_query_params=context.get("query_params"), + response_status_code=422, + response_body=json.dumps({"errors": exc.errors()}), + preprocessing_logs_text=f"Validation errors: {json.dumps(exc.errors(), indent=2)}", + correlation_id=correlation_id, + ip_address=request.client.host if request.client else None, + ) + + return JSONResponse( + status_code=422, + content={ + "message": "Validation error", + "errors": exc.errors(), + "correlation_id": correlation_id, + } + ) + except Exception as e: + logger.error(f"Error in validation exception handler: {e}") + return JSONResponse( + status_code=422, + content={"message": "Validation error", "errors": exc.errors()} + ) diff --git a/service/error_logger.py b/service/error_logger.py new file mode 100644 index 0000000..a1cfb78 --- /dev/null +++ b/service/error_logger.py @@ -0,0 +1,95 @@ +""" +Utility module for logging API errors to the database +""" +import os +import uuid +from banking.database import SessionLocal +from banking.error_logs import APIErrorLog +from logger import logger +import traceback +from typing import Optional, Dict, Any + +from config import error_audio_dir as ERROR_AUDIO_DIR + +def _ensure_error_audio_dir(): + os.makedirs(ERROR_AUDIO_DIR, exist_ok=True) + +def _save_audio_for_error(audio_bytes: bytes, original_filename: Optional[str] = None) -> Optional[str]: + """Save audio bytes to disk; return storage filename (e.g. uuid.wav) or None on failure.""" + if not audio_bytes: + return None + try: + _ensure_error_audio_dir() + ext = ".wav" + if original_filename and "." in original_filename: + ext = "." + original_filename.rsplit(".", 1)[-1].lower() + if ext not in (".wav", ".mp3", ".m4a", ".ogg", ".webm", ".flac"): + ext = ".wav" + filename = f"{uuid.uuid4().hex}{ext}" + path = os.path.join(ERROR_AUDIO_DIR, filename) + with open(path, "wb") as f: + f.write(audio_bytes) + return filename + except Exception as e: + logger.warning(f"Could not save error audio: {e}") + return None + +def log_api_error( + endpoint: str, + error: Exception, + failure_stage: Optional[str] = None, + # Audio-related data (TTS/STT) + audio_file_name: Optional[str] = None, + audio_bytes: Optional[bytes] = None, + translated_text: Optional[str] = None, + detected_language: Optional[str] = None, + # Logging and preprocessing + preprocessing_logs_text: Optional[str] = None, + # Intent data (if available before error) + intent_data: Optional[Dict[str, Any]] = None, +): + """ + Log an API error to the database for banking voice API. + Focuses on audio-related data (TTS/STT) and error traceback. + Only stores failure-related data, not user/session context. + + Args: + endpoint: The API endpoint that failed (e.g., "/voice/transcribe-intent") + error: The exception that occurred + failure_stage: Stage where failure occurred (e.g., "transcription", "intent_detection", "session_processing") + audio_file_name: Name of the audio file uploaded + translated_text: Transcribed/translated text from audio (if available) + detected_language: Language detected from audio (if available) + preprocessing_logs_text: Logs from preprocessing steps (TTS/STT functions) + intent_data: Intent data if available before error + """ + try: + audio_storage_path = _save_audio_for_error(audio_bytes, audio_file_name) if audio_bytes else None + + db = SessionLocal() + try: + error_log = APIErrorLog( + endpoint=endpoint, + audio_file_name=audio_file_name, + audio_storage_path=audio_storage_path, + translated_text=translated_text, + detected_language=detected_language, + error_type=type(error).__name__, + error_message=str(error), + error_traceback=traceback.format_exc(), + failure_stage=failure_stage, + preprocessing_logs_text=preprocessing_logs_text, + intent_data=intent_data if intent_data else None, + ) + db.add(error_log) + db.commit() + logger.info(f"Error logged to database: {error_log.id}") + except Exception as db_error: + logger.error(f"Failed to log error to database: {db_error}") + logger.error(traceback.format_exc()) + db.rollback() + finally: + db.close() + except Exception as e: + logger.error(f"Critical error in error logging system: {e}") + logger.error(traceback.format_exc()) diff --git a/service/error_middleware.py b/service/error_middleware.py new file mode 100644 index 0000000..c32c1e5 --- /dev/null +++ b/service/error_middleware.py @@ -0,0 +1,298 @@ +""" +Global error logging middleware for FastAPI +Captures all API errors automatically +""" +from fastapi import Request, Response, status +from fastapi.responses import JSONResponse +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.types import Message +import time +import json +import traceback +from typing import Dict, Any, Optional +from banking.database import SessionLocal +from banking.error_logs import APIErrorLog +from logger import logger +import uuid + +# Sensitive fields to sanitize from request/response +SENSITIVE_FIELDS = { + "password", "password_hash", "token", "access_token", "refresh_token", + "authorization", "api_key", "apikey", "secret", "secret_key", "private_key", + "credit_card", "card_number", "cvv", "ssn", "social_security_number" +} + +def sanitize_data(data: Any, max_depth: int = 10) -> Any: + """ + Recursively sanitize sensitive data from dictionaries/lists. + Replaces sensitive field values with '[REDACTED]' + """ + if max_depth <= 0: + return data + + if isinstance(data, dict): + sanitized = {} + for key, value in data.items(): + key_lower = str(key).lower() + # Check if key contains sensitive field name + is_sensitive = any(sensitive in key_lower for sensitive in SENSITIVE_FIELDS) + + if is_sensitive: + sanitized[key] = "[REDACTED]" + elif isinstance(value, (dict, list)): + sanitized[key] = sanitize_data(value, max_depth - 1) + else: + sanitized[key] = value + return sanitized + + elif isinstance(data, list): + return [sanitize_data(item, max_depth - 1) for item in data] + + return data + +def sanitize_headers(headers: Dict[str, str]) -> Dict[str, str]: + """Sanitize sensitive headers""" + sanitized = {} + sensitive_headers = {"authorization", "cookie", "x-api-key", "x-auth-token"} + + for key, value in headers.items(): + key_lower = key.lower() + if any(sensitive in key_lower for sensitive in sensitive_headers): + sanitized[key] = "[REDACTED]" + else: + sanitized[key] = value + + return sanitized + +async def extract_request_body(request: Request) -> Optional[Dict[str, Any]]: + """Extract and parse request body""" + try: + # Check content type + content_type = request.headers.get("content-type", "").lower() + + if "application/json" in content_type: + body = await request.json() + return sanitize_data(body) + elif "application/x-www-form-urlencoded" in content_type: + form = await request.form() + form_dict = {key: value for key, value in form.items()} + return sanitize_data(form_dict) + elif "multipart/form-data" in content_type: + form = await request.form() + form_dict = {} + for key, value in form.items(): + # Handle file uploads + if hasattr(value, "filename"): + form_dict[key] = { + "filename": value.filename, + "content_type": value.content_type, + "size": getattr(value, "size", None), + "type": "file" + } + else: + form_dict[key] = value + return sanitize_data(form_dict) + else: + # Try to read as text + body = await request.body() + if body: + try: + return {"raw": body.decode("utf-8")[:1000]} # Limit size + except: + return {"raw": "[BINARY_DATA]"} + except Exception as e: + logger.warning(f"Failed to extract request body: {e}") + return None + +def extract_file_metadata(request: Request) -> Optional[Dict[str, Any]]: + """Extract file metadata from request""" + try: + content_type = request.headers.get("content-type", "").lower() + files_metadata = [] + + if "multipart/form-data" in content_type: + # Files would be extracted in extract_request_body + # This is a placeholder - actual file extraction happens in the endpoint + pass + + return files_metadata if files_metadata else None + except Exception as e: + logger.warning(f"Failed to extract file metadata: {e}") + return None + +class ErrorLoggingMiddleware(BaseHTTPMiddleware): + """Middleware to log all API errors""" + + async def dispatch(self, request: Request, call_next): + start_time = time.time() + correlation_id = str(uuid.uuid4()) + request.state.correlation_id = correlation_id + + # Store request details (extract before processing to avoid consuming body) + request_headers = sanitize_headers(dict(request.headers)) + request_query_params = dict(request.query_params) if request.query_params else None + + # For request body, we'll try to peek but not consume + # Store in request.state so endpoints can still read it + request_body = None + try: + # Only extract body for non-streaming requests + if request.method in ["POST", "PUT", "PATCH"]: + # Peek at body without consuming (this is tricky - we'll handle it in exception handlers) + # For now, we'll extract it but endpoints should still be able to read it + # FastAPI handles this internally, so we'll skip body extraction here + # and let exception handlers extract it if needed + pass + except: + pass + + try: + # Process request + response = await call_next(request) + + execution_time = (time.time() - start_time) * 1000 # Convert to milliseconds + + # Log errors (4xx and 5xx status codes) + if response.status_code >= 400: + # Extract request body from request.state if stored by endpoint + # Otherwise, try to extract it now (may fail if already consumed) + extracted_request_body = getattr(request.state, "request_body", None) + if extracted_request_body is None: + try: + extracted_request_body = await extract_request_body(request) + except: + extracted_request_body = None + + # Capture response body for error logging + response_body_text = None + try: + # Try to get response body if it's already rendered + if hasattr(response, "body") and response.body: + response_body_text = response.body.decode("utf-8")[:5000] if isinstance(response.body, bytes) else str(response.body)[:5000] + except Exception as e: + logger.warning(f"Failed to capture response body: {e}") + + # Log error asynchronously without blocking + await self._log_error( + request=request, + response_body=response_body_text, + error=None, + status_code=response.status_code, + execution_time=execution_time, + request_body=extracted_request_body, + request_headers=request_headers, + request_query_params=request_query_params, + correlation_id=correlation_id, + ) + + return response + + except Exception as e: + execution_time = (time.time() - start_time) * 1000 + + # Try to extract request body if not already extracted + extracted_request_body = getattr(request.state, "request_body", None) + if extracted_request_body is None: + try: + extracted_request_body = await extract_request_body(request) + except: + extracted_request_body = None + + # Log the exception + await self._log_error( + request=request, + response_body=None, + error=e, + status_code=500, + execution_time=execution_time, + request_body=extracted_request_body, + request_headers=request_headers, + request_query_params=request_query_params, + correlation_id=correlation_id, + ) + + # Return error response + return JSONResponse( + status_code=500, + content={ + "message": str(e), + "correlation_id": correlation_id, + "error_type": type(e).__name__ + } + ) + + async def _log_error( + self, + request: Request, + response_body: Optional[str] = None, + error: Optional[Exception] = None, + status_code: int = 500, + execution_time: float = 0.0, + request_body: Optional[Dict[str, Any]] = None, + request_headers: Optional[Dict[str, str]] = None, + request_query_params: Optional[Dict[str, Any]] = None, + correlation_id: Optional[str] = None, + ): + """Log error to database""" + try: + db = SessionLocal() + try: + # Extract user info if available (from request state or headers) + user_id = getattr(request.state, "user_id", None) + ip_address = request.client.host if request.client else None + + # Extract file metadata from request body + attachment_metadata = None + attachment_storage_url = None + if request_body: + for key, value in request_body.items(): + if isinstance(value, dict) and value.get("type") == "file": + attachment_metadata = { + "field_name": key, + "filename": value.get("filename"), + "content_type": value.get("content_type"), + "size": value.get("size"), + } + # If there's a storage URL in metadata, extract it + if "storage_url" in value: + attachment_storage_url = value.get("storage_url") + break + + # Create error object if not provided + if error is None: + class HTTPError(Exception): + pass + error = HTTPError(f"HTTP {status_code}") + + error_log = APIErrorLog( + endpoint=request.url.path, + http_method=request.method, + request_headers=request_headers, + request_body=request_body, + request_query_params=request_query_params, + response_status_code=status_code, + response_body=response_body, + execution_time_ms=execution_time, + attachment_metadata=attachment_metadata, + attachment_storage_url=attachment_storage_url, + error_type=type(error).__name__, + error_message=str(error), + error_traceback=traceback.format_exc() if error else None, + failure_stage=None, # Will be set by specific endpoints if needed + user_id=user_id, + ip_address=ip_address, + correlation_id=correlation_id, + ) + + db.add(error_log) + db.commit() + logger.info(f"Error logged to database: {error_log.id} (correlation_id: {correlation_id})") + except Exception as db_error: + logger.error(f"Failed to log error to database: {db_error}") + logger.error(traceback.format_exc()) + db.rollback() + finally: + db.close() + except Exception as e: + logger.error(f"Critical error in error logging middleware: {e}") + logger.error(traceback.format_exc()) diff --git a/service/main.py b/service/main.py index 2a9617d..5c17526 100644 --- a/service/main.py +++ b/service/main.py @@ -19,6 +19,7 @@ from redis_client import session_manager from datetime import datetime from session_service import SessionService, SessionFlowProcessor +from error_logger import log_api_error app = FastAPI() @@ -105,6 +106,10 @@ async def upload_audio(body: Body): app.include_router(banking_router) +# Import and include admin routes +from admin_routes import router as admin_router +app.include_router(admin_router) + @app.post("/voice/transcribe-intent") async def transcribe_intent( @@ -124,110 +129,252 @@ async def transcribe_intent( 1. Standard flow: Audio is provided, transcribed, and intent is detected 2. OTP + session_id flow: No audio, fetch transcribe_text and language from session data """ + translation_text = None + detected_language = None + audio_file_name = None + audio_bytes = None + intent_data = None + try: # Check for (OTP or beneficiary_name) + session_id flow (no audio required) if not audio and session_id and ((otp and otp.isdigit() and (int(otp) == 123456)) or beneficiary_name) : logger.info(f"Using OTP + session_id flow for session: {session_id}") - # Initialize session flow processor - session_processor = SessionFlowProcessor() - - # Get session data to fetch stored transcribe_text and language - session_data = SessionService.get_session_data(session_id) - if not session_data: - return JSONResponse(status_code=400, content={"message": f"Session {session_id} not found"}) - - # Extract transcribe_text and language from session data - translations = session_data.get("translations", []) - if not translations: - return JSONResponse(status_code=400, content={"message": "No translation data found in session"}) - - # Use the first (original) translation as transcribe_text - translation_text = translations[0] - language = session_data.get("language") - - if not language: - return JSONResponse(status_code=400, content={"message": "No language data found in session"}) - - # Use existing intent data from the session - formatted_intent_data = session_data.get("intent_data", {}) - - logger.info(f"Retrieved from session - translation: {translation_text}, language: {language}") - # Process existing session with OTP - success, response_data = await session_processor.process_existing_session( - session_id, translation_text, language, formatted_intent_data, otp, beneficiary_name - ) - if not success: - return JSONResponse(status_code=400, content=response_data) + try: + # Initialize session flow processor + session_processor = SessionFlowProcessor() - return JSONResponse(content=response_data, status_code=200) + # Get session data to fetch stored transcribe_text and language + session_data = SessionService.get_session_data(session_id) + if not session_data: + error_msg = f"Session {session_id} not found" + log_api_error( + endpoint="/voice/transcribe-intent", + error=Exception(error_msg), + failure_stage="session_retrieval", + ) + return JSONResponse(status_code=400, content={"message": error_msg}) + + # Extract transcribe_text and language from session data + translations = session_data.get("translations", []) + if not translations: + error_msg = "No translation data found in session" + log_api_error( + endpoint="/voice/transcribe-intent", + error=Exception(error_msg), + failure_stage="session_data_extraction", + ) + return JSONResponse(status_code=400, content={"message": error_msg}) + + # Use the first (original) translation as transcribe_text + translation_text = translations[0] + detected_language = session_data.get("language") + + if not detected_language: + error_msg = "No language data found in session" + log_api_error( + endpoint="/voice/transcribe-intent", + error=Exception(error_msg), + failure_stage="session_data_extraction", + translated_text=translation_text, + ) + return JSONResponse(status_code=400, content={"message": error_msg}) + + # Use existing intent data from the session + formatted_intent_data = session_data.get("intent_data", {}) + + logger.info(f"Retrieved from session - translation: {translation_text}, language: {detected_language}") + + # Process existing session with OTP + success, response_data = await session_processor.process_existing_session( + session_id, translation_text, detected_language, formatted_intent_data, otp, beneficiary_name + ) + if not success: + log_api_error( + endpoint="/voice/transcribe-intent", + error=Exception(str(response_data)), + failure_stage="session_processing", + translated_text=translation_text, + detected_language=detected_language, + intent_data=formatted_intent_data, + ) + return JSONResponse(status_code=400, content=response_data) + + return JSONResponse(content=response_data, status_code=200) + except Exception as e: + log_api_error( + endpoint="/voice/transcribe-intent", + error=e, + failure_stage="otp_session_flow", + translated_text=translation_text, + detected_language=detected_language, + intent_data=intent_data, + ) + raise + # Standard flow - audio is required if not audio: - return JSONResponse(status_code=400, content={"message":"No audio file provided"}) + error_msg = "No audio file provided" + log_api_error( + endpoint="/voice/transcribe-intent", + error=Exception(error_msg), + failure_stage="input_validation", + ) + return JSONResponse(status_code=400, content={"message": error_msg}) + + # Read audio once so we can store it on error and still pass a copy to whisper + from io import BytesIO + from starlette.datastructures import UploadFile as StarletteUploadFile + audio_bytes = await audio.read() + audio_file_name = audio.filename if audio else None + audio_for_whisper = StarletteUploadFile( + filename=audio_file_name or "audio.wav", + file=BytesIO(audio_bytes), + ) # Step 1: Common audio processing (transcription and intent detection) - id,response,lang,dia = translate_with_whisper_from_upload(audio) - translation_text = response[1] - language = lang[1] - logger.info("Translation done") - logger.info(translation_text) - logger.info(language) + try: + id, response, lang, dia = translate_with_whisper_from_upload(audio_for_whisper) + translation_text = response[1] + detected_language = lang[1] + logger.info("Translation done") + logger.info(translation_text) + logger.info(detected_language) + except Exception as e: + # Capture error traceback and logs + error_traceback = traceback.format_exc() + logger.error(f"Transcription error: {error_traceback}") + + log_api_error( + endpoint="/voice/transcribe-intent", + error=e, + failure_stage="transcription", + audio_file_name=audio_file_name, + audio_bytes=audio_bytes, + preprocessing_logs_text=error_traceback, # Store traceback as preprocessing logs + ) + raise #translation_text = "How much I spend on Flipkart last week" #translation_text = "list all my beneficiaries" #translation_text = "Pay 10 to Shailesh" #language = "hi-IN" # Detect intent - intent = detect_intent_with_llama(translation_text, language) - logger.info("Intent identified") try: - if isinstance(intent, dict): - intent_dict = intent - else: - intent_dict = json.loads(intent) - except json.JSONDecodeError: - logger.warning(f"Intent detection returned non-JSON response: {intent}") - result = {"error": intent, "session_id": session_id, "translation": translation_text} - return JSONResponse(content=result, status_code=200) - - # Format intent response - formatted_intent_data = format_intent_response(intent_dict) - logger.info(f"Formatted intent data: {formatted_intent_data}") - + intent = detect_intent_with_llama(translation_text, detected_language) + logger.info("Intent identified") + + try: + if isinstance(intent, dict): + intent_dict = intent + else: + intent_dict = json.loads(intent) + except json.JSONDecodeError as e: + logger.warning(f"Intent detection returned non-JSON response: {intent}") + error_traceback = traceback.format_exc() + log_api_error( + endpoint="/voice/transcribe-intent", + error=e, + failure_stage="intent_parsing", + audio_file_name=audio_file_name, + audio_bytes=audio_bytes, + translated_text=translation_text, + detected_language=detected_language, + preprocessing_logs_text=error_traceback, + intent_data={"raw_intent_response": str(intent)}, + ) + result = {"error": intent, "session_id": session_id, "translation": translation_text} + return JSONResponse(content=result, status_code=200) - # Step 3: Initialize session flow processor - session_processor = SessionFlowProcessor() - # Step 4: Decision logic - use session service to determine flow - if SessionService.should_use_session_flow(session_id): - # SESSION-BASED FLOW - logger.info(f"Using session flow for session: {session_id}") + # Format intent response + formatted_intent_data = format_intent_response(intent_dict) + intent_data = formatted_intent_data + logger.info(f"Formatted intent data: {formatted_intent_data}") + except Exception as e: + # Capture error traceback + error_traceback = traceback.format_exc() + logger.error(f"Intent detection error: {error_traceback}") - success, response_data = await session_processor.process_existing_session( - session_id, translation_text, language, formatted_intent_data, otp, beneficiary_name + log_api_error( + endpoint="/voice/transcribe-intent", + error=e, + failure_stage="intent_detection", + audio_file_name=audio_file_name, + audio_bytes=audio_bytes, + translated_text=translation_text, + detected_language=detected_language, + preprocessing_logs_text=error_traceback, ) - - if not success: - return JSONResponse(status_code=400, content=response_data) + raise + + # Step 3: Initialize session flow processor + try: + session_processor = SessionFlowProcessor() + # Step 4: Decision logic - use session service to determine flow + if SessionService.should_use_session_flow(session_id): + # SESSION-BASED FLOW + logger.info(f"Using session flow for session: {session_id}") - return JSONResponse(content=response_data, status_code=200) - - else: - # NEW SESSION FLOW - logger.info("Creating new session flow") + success, response_data = await session_processor.process_existing_session( + session_id, translation_text, detected_language, formatted_intent_data, otp, beneficiary_name + ) + + if not success: + log_api_error( + endpoint="/voice/transcribe-intent", + error=Exception(str(response_data)), + failure_stage="session_processing", + audio_file_name=audio_file_name, + audio_bytes=audio_bytes, + translated_text=translation_text, + detected_language=detected_language, + intent_data=formatted_intent_data, + ) + return JSONResponse(status_code=400, content=response_data) + + return JSONResponse(content=response_data, status_code=200) - response_data = await session_processor.process_new_session( - customer_id, - phone, - transaction_type, - payment_method, - language, - translation_text, - formatted_intent_data + else: + # NEW SESSION FLOW + logger.info("Creating new session flow") + + response_data = await session_processor.process_new_session( + customer_id, + phone, + transaction_type, + payment_method, + detected_language, + translation_text, + formatted_intent_data + ) + + return JSONResponse(content=response_data, status_code=200) + except Exception as e: + log_api_error( + endpoint="/voice/transcribe-intent", + error=e, + failure_stage="session_processing", + audio_file_name=audio_file_name, + audio_bytes=audio_bytes, + translated_text=translation_text, + detected_language=detected_language, + intent_data=intent_data, ) - - return JSONResponse(content=response_data, status_code=200) + raise except Exception as e: logger.error(f"Error in transcribe-intent: {traceback.format_exc()}") current_session_id = session_id if session_id else "unknown" + # Log the top-level error if not already logged + log_api_error( + endpoint="/voice/transcribe-intent", + error=e, + failure_stage="unknown", + audio_file_name=audio_file_name, + audio_bytes=audio_bytes, + translated_text=translation_text, + detected_language=detected_language, + intent_data=intent_data, + ) + return JSONResponse(content={"message": str(e), "session_id": current_session_id}, status_code=500) diff --git a/service/migrate_error_logs.py b/service/migrate_error_logs.py new file mode 100644 index 0000000..b3c02c9 --- /dev/null +++ b/service/migrate_error_logs.py @@ -0,0 +1,132 @@ +""" +Migration script to add new columns to api_error_logs table +Run this once to update the existing table schema +""" +from banking.database import engine +from sqlalchemy import text +from logger import logger + +def migrate_error_logs_table(): + """Add new columns to api_error_logs table if they don't exist""" + try: + # Use begin() to ensure transactions are committed + with engine.begin() as conn: + # Check if table exists + result = conn.execute(text(""" + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'api_error_logs' + ); + """)) + table_exists = result.scalar() + + if not table_exists: + logger.info("Table api_error_logs does not exist. It will be created by SQLAlchemy.") + return + + # List of new columns to add (column_name, column_type, default_clause) + new_columns = [ + ("audio_storage_path", "VARCHAR(500)", ""), + ("http_method", "VARCHAR(10)", "NOT NULL DEFAULT 'UNKNOWN'"), + ("request_headers", "JSONB", ""), + ("request_body", "JSONB", ""), + ("request_query_params", "JSONB", ""), + ("request_form_data", "JSONB", ""), + ("response_status_code", "INTEGER", ""), + ("response_body", "TEXT", ""), + ("preprocessing_steps", "JSONB", ""), + ("preprocessing_logs_text", "TEXT", ""), + ("execution_time_ms", "DOUBLE PRECISION", ""), + ("attachment_metadata", "JSONB", ""), + ("attachment_storage_url", "VARCHAR(500)", ""), + ("user_id", "VARCHAR(255)", ""), + ("ip_address", "VARCHAR(45)", ""), + ("correlation_id", "VARCHAR(255)", ""), + ] + + # Add indexes for new columns + indexes = [ + ("ix_api_error_logs_http_method", "http_method"), + ("ix_api_error_logs_response_status_code", "response_status_code"), + ("ix_api_error_logs_user_id", "user_id"), + ("ix_api_error_logs_ip_address", "ip_address"), + ("ix_api_error_logs_correlation_id", "correlation_id"), + ] + + # Add columns that don't exist + for column_name, column_type, default in new_columns: + try: + # Check if column exists + check_result = conn.execute(text(f""" + SELECT EXISTS ( + SELECT FROM information_schema.columns + WHERE table_name = 'api_error_logs' + AND column_name = '{column_name}' + ); + """)) + column_exists = check_result.scalar() + + if not column_exists: + logger.info(f"Adding column: {column_name}") + if default and default.strip(): + conn.execute(text(f""" + ALTER TABLE api_error_logs + ADD COLUMN {column_name} {column_type} {default}; + """)) + else: + conn.execute(text(f""" + ALTER TABLE api_error_logs + ADD COLUMN {column_name} {column_type}; + """)) + logger.info(f"Successfully added column: {column_name}") + else: + logger.info(f"Column {column_name} already exists") + except Exception as e: + logger.error(f"Error adding column {column_name}: {e}") + # Don't rollback - continue with other columns + + # Add indexes that don't exist + for index_name, column_name in indexes: + try: + # Check if index exists + check_result = conn.execute(text(f""" + SELECT EXISTS ( + SELECT FROM pg_indexes + WHERE indexname = '{index_name}' + ); + """)) + index_exists = check_result.scalar() + + if not index_exists: + logger.info(f"Adding index: {index_name}") + conn.execute(text(f""" + CREATE INDEX {index_name} + ON api_error_logs ({column_name}); + """)) + logger.info(f"Successfully added index: {index_name}") + else: + logger.info(f"Index {index_name} already exists") + except Exception as e: + logger.error(f"Error adding index {index_name}: {e}") + conn.rollback() + + # Make failure_stage nullable if it's not already + try: + conn.execute(text(""" + ALTER TABLE api_error_logs + ALTER COLUMN failure_stage DROP NOT NULL; + """)) + logger.info("Made failure_stage nullable") + except Exception as e: + logger.warning(f"Could not make failure_stage nullable (might already be nullable): {e}") + + logger.info("Migration completed successfully") + + except Exception as e: + logger.error(f"Migration failed: {e}") + import traceback + logger.error(traceback.format_exc()) + raise + +if __name__ == "__main__": + migrate_error_logs_table() diff --git a/service/preprocessing_tracker.py b/service/preprocessing_tracker.py new file mode 100644 index 0000000..dbe933a --- /dev/null +++ b/service/preprocessing_tracker.py @@ -0,0 +1,61 @@ +""" +Helper module for tracking preprocessing steps +""" +from typing import List, Dict, Any +import time +from datetime import datetime + +class PreprocessingTracker: + """Tracks preprocessing steps for error logging""" + + def __init__(self): + self.steps: List[Dict[str, Any]] = [] + self.start_time = time.time() + + def add_step(self, step_name: str, status: str = "completed", details: Any = None, error: Exception = None): + """ + Add a preprocessing step. + + Args: + step_name: Name of the preprocessing step (e.g., "audio_upload", "transcription", "intent_detection") + status: Status of the step ("started", "completed", "failed") + details: Additional details about the step + error: Exception if the step failed + """ + elapsed_time = (time.time() - self.start_time) * 1000 # milliseconds + + step_data = { + "step": step_name, + "status": status, + "timestamp": datetime.utcnow().isoformat(), + "elapsed_time_ms": round(elapsed_time, 2), + } + + if details: + step_data["details"] = details + + if error: + step_data["error"] = { + "type": type(error).__name__, + "message": str(error), + } + + self.steps.append(step_data) + + def get_steps(self) -> List[Dict[str, Any]]: + """Get all tracked preprocessing steps""" + return self.steps + + def get_logs_text(self) -> str: + """Get free-form text representation of preprocessing logs""" + if not self.steps: + return "" + + log_lines = [] + for step in self.steps: + line = f"[{step['timestamp']}] {step['step']}: {step['status']}" + if 'error' in step: + line += f" - Error: {step['error']['message']}" + log_lines.append(line) + + return "\n".join(log_lines)