From 71bdd08da9833834120a27d88976702438900f22 Mon Sep 17 00:00:00 2001 From: MarcUs7i <96580944+MarcUs7i@users.noreply.github.com> Date: Fri, 11 Jul 2025 00:59:08 +0200 Subject: [PATCH] =?UTF-8?q?finished=20=C2=B5LinkShortener=20v2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 19 +- .env.example | 6 +- .github/dependabot.yml | 28 +- .github/workflows/build.yml | 7 +- .gitignore | 46 +- Dockerfile | 27 +- LICENSE | 7 - README.md | 17 +- bun.lock | 998 ++++++++++++++++++ cmd/api/main.go | 92 -- docker-compose-build.yml | 13 +- docker-compose.yml | 15 +- eslint.config.mjs | 16 + go.mod | 23 - go.sum | 56 - internal/api/handlers/analytics.go | 126 --- internal/api/handlers/auth.go | 95 -- internal/api/handlers/handlers.go | 38 - internal/api/handlers/url.go | 132 --- internal/api/router.go | 39 - internal/config/config.go | 26 - internal/database/mongodb.go | 73 -- internal/models/analytics.go | 46 - internal/models/link.go | 12 - internal/models/user.go | 10 - internal/utils/clientinfo.go | 83 -- internal/utils/shortid.go | 32 - internal/utils/url.go | 19 - next.config.ts | 7 + package.json | 34 + postcss.config.mjs | 5 + src/app/Home.module.css | 168 +++ src/app/admin/AdminDashboard.module.css | 228 ++++ src/app/admin/page.tsx | 278 +++++ .../user/[accountId]/UserDetail.module.css | 174 +++ .../[shortId]/AdminLinkDetail.module.css | 344 ++++++ .../user/[accountId]/links/[shortId]/page.tsx | 574 ++++++++++ src/app/admin/user/[accountId]/page.tsx | 241 +++++ src/app/api/admin/statistics/rebuild/route.ts | 31 + .../admin/users/[accountId]/admin/route.ts | 67 ++ .../links/[shortId]/analytics/route.ts | 116 ++ .../[accountId]/links/[shortId]/route.ts | 137 +++ .../admin/users/[accountId]/links/route.ts | 45 + src/app/api/admin/users/[accountId]/route.ts | 53 + .../[accountId]/sessions/revoke/route.ts | 39 + .../admin/users/[accountId]/sessions/route.ts | 47 + src/app/api/admin/users/route.ts | 91 ++ src/app/api/analytics/route.ts | 185 ++++ src/app/api/auth/[...nextauth]/route.ts | 102 ++ src/app/api/auth/check-session/route.ts | 43 + src/app/api/auth/register/route.ts | 61 ++ src/app/api/auth/remove/route.ts | 47 + src/app/api/auth/sessions/revoke/route.ts | 46 + src/app/api/auth/sessions/route.ts | 38 + src/app/api/link/route.ts | 252 +++++ src/app/api/links/route.ts | 44 + src/app/api/statistics/route.ts | 33 + src/app/dashboard/Dashboard.module.css | 163 +++ .../link/[shortId]/LinkDetail.module.css | 280 +++++ src/app/dashboard/link/[shortId]/page.tsx | 545 ++++++++++ src/app/dashboard/page.tsx | 164 +++ .../dashboard/security/Security.module.css | 120 +++ src/app/dashboard/security/page.tsx | 96 ++ src/app/favicon.ico | Bin 0 -> 41662 bytes src/app/globals.css | 67 ++ src/app/l/[shortId]/route.ts | 36 + src/app/layout.tsx | 52 + src/app/not-found/NotFound.module.css | 57 + src/app/not-found/page.tsx | 18 + src/app/page.tsx | 212 ++++ src/app/privacy/Privacy.module.css | 117 ++ src/app/privacy/page.tsx | 77 ++ src/app/tos/ToS.module.css | 116 ++ src/app/tos/page.tsx | 77 ++ src/components/Footer.module.css | 53 + src/components/Header.module.css | 222 ++++ src/components/Providers.tsx | 11 + src/components/ResponsiveLayout.tsx | 43 + src/components/SessionMonitor.tsx | 45 + src/components/footer.tsx | 19 + src/components/header.tsx | 197 ++++ src/components/ui/ConfirmModal.module.css | 84 ++ src/components/ui/ConfirmModal.tsx | 90 ++ src/components/ui/Graph.module.css | 46 + src/components/ui/Graph.tsx | 296 ++++++ src/components/ui/LoadingIcon.tsx | 46 + src/components/ui/Toast.module.css | 57 + src/components/ui/Toast.tsx | 24 + .../ui/admin/AdminLinkTable.module.css | 219 ++++ src/components/ui/admin/AdminLinkTable.tsx | 203 ++++ .../ui/dashboard/LinkTable.module.css | 219 ++++ src/components/ui/dashboard/LinkTable.tsx | 196 ++++ .../ui/dashboard/SessionManager.module.css | 157 +++ .../ui/dashboard/SessionManager.tsx | 189 ++++ .../dashboard/link/AnalyticsTable.module.css | 267 +++++ .../ui/dashboard/link/AnalyticsTable.tsx | 262 +++++ src/contexts/ToastContext.tsx | 51 + src/lib/analyticsdb.ts | 146 +++ src/lib/linkdb.ts | 131 +++ src/lib/logger.ts | 22 + src/lib/mongodb.ts | 43 + src/lib/sessiondb.ts | 141 +++ src/lib/statisticsdb.ts | 80 ++ src/lib/userdb.ts | 158 +++ src/lib/utils.ts | 127 +++ src/middleware.ts | 55 + src/types/analytics.d.ts | 30 + src/types/auth.d.ts | 22 + src/types/global.d.ts | 4 + src/types/link.d.ts | 7 + src/types/session.d.ts | 10 + src/types/statistics.d.ts | 18 + src/types/user.d.ts | 5 + tsconfig.json | 27 + web/static/css/style.css | 299 ------ web/static/js/script.js | 344 ------ web/templates/index.html | 84 -- 117 files changed, 10892 insertions(+), 1685 deletions(-) delete mode 100644 LICENSE create mode 100644 bun.lock delete mode 100644 cmd/api/main.go create mode 100644 eslint.config.mjs delete mode 100644 go.mod delete mode 100644 go.sum delete mode 100644 internal/api/handlers/analytics.go delete mode 100644 internal/api/handlers/auth.go delete mode 100644 internal/api/handlers/handlers.go delete mode 100644 internal/api/handlers/url.go delete mode 100644 internal/api/router.go delete mode 100644 internal/config/config.go delete mode 100644 internal/database/mongodb.go delete mode 100644 internal/models/analytics.go delete mode 100644 internal/models/link.go delete mode 100644 internal/models/user.go delete mode 100644 internal/utils/clientinfo.go delete mode 100644 internal/utils/shortid.go delete mode 100644 internal/utils/url.go create mode 100644 next.config.ts create mode 100644 package.json create mode 100644 postcss.config.mjs create mode 100644 src/app/Home.module.css create mode 100644 src/app/admin/AdminDashboard.module.css create mode 100644 src/app/admin/page.tsx create mode 100644 src/app/admin/user/[accountId]/UserDetail.module.css create mode 100644 src/app/admin/user/[accountId]/links/[shortId]/AdminLinkDetail.module.css create mode 100644 src/app/admin/user/[accountId]/links/[shortId]/page.tsx create mode 100644 src/app/admin/user/[accountId]/page.tsx create mode 100644 src/app/api/admin/statistics/rebuild/route.ts create mode 100644 src/app/api/admin/users/[accountId]/admin/route.ts create mode 100644 src/app/api/admin/users/[accountId]/links/[shortId]/analytics/route.ts create mode 100644 src/app/api/admin/users/[accountId]/links/[shortId]/route.ts create mode 100644 src/app/api/admin/users/[accountId]/links/route.ts create mode 100644 src/app/api/admin/users/[accountId]/route.ts create mode 100644 src/app/api/admin/users/[accountId]/sessions/revoke/route.ts create mode 100644 src/app/api/admin/users/[accountId]/sessions/route.ts create mode 100644 src/app/api/admin/users/route.ts create mode 100644 src/app/api/analytics/route.ts create mode 100644 src/app/api/auth/[...nextauth]/route.ts create mode 100644 src/app/api/auth/check-session/route.ts create mode 100644 src/app/api/auth/register/route.ts create mode 100644 src/app/api/auth/remove/route.ts create mode 100644 src/app/api/auth/sessions/revoke/route.ts create mode 100644 src/app/api/auth/sessions/route.ts create mode 100644 src/app/api/link/route.ts create mode 100644 src/app/api/links/route.ts create mode 100644 src/app/api/statistics/route.ts create mode 100644 src/app/dashboard/Dashboard.module.css create mode 100644 src/app/dashboard/link/[shortId]/LinkDetail.module.css create mode 100644 src/app/dashboard/link/[shortId]/page.tsx create mode 100644 src/app/dashboard/page.tsx create mode 100644 src/app/dashboard/security/Security.module.css create mode 100644 src/app/dashboard/security/page.tsx create mode 100644 src/app/favicon.ico create mode 100644 src/app/globals.css create mode 100644 src/app/l/[shortId]/route.ts create mode 100644 src/app/layout.tsx create mode 100644 src/app/not-found/NotFound.module.css create mode 100644 src/app/not-found/page.tsx create mode 100644 src/app/page.tsx create mode 100644 src/app/privacy/Privacy.module.css create mode 100644 src/app/privacy/page.tsx create mode 100644 src/app/tos/ToS.module.css create mode 100644 src/app/tos/page.tsx create mode 100644 src/components/Footer.module.css create mode 100644 src/components/Header.module.css create mode 100644 src/components/Providers.tsx create mode 100644 src/components/ResponsiveLayout.tsx create mode 100644 src/components/SessionMonitor.tsx create mode 100644 src/components/footer.tsx create mode 100644 src/components/header.tsx create mode 100644 src/components/ui/ConfirmModal.module.css create mode 100644 src/components/ui/ConfirmModal.tsx create mode 100644 src/components/ui/Graph.module.css create mode 100644 src/components/ui/Graph.tsx create mode 100644 src/components/ui/LoadingIcon.tsx create mode 100644 src/components/ui/Toast.module.css create mode 100644 src/components/ui/Toast.tsx create mode 100644 src/components/ui/admin/AdminLinkTable.module.css create mode 100644 src/components/ui/admin/AdminLinkTable.tsx create mode 100644 src/components/ui/dashboard/LinkTable.module.css create mode 100644 src/components/ui/dashboard/LinkTable.tsx create mode 100644 src/components/ui/dashboard/SessionManager.module.css create mode 100644 src/components/ui/dashboard/SessionManager.tsx create mode 100644 src/components/ui/dashboard/link/AnalyticsTable.module.css create mode 100644 src/components/ui/dashboard/link/AnalyticsTable.tsx create mode 100644 src/contexts/ToastContext.tsx create mode 100644 src/lib/analyticsdb.ts create mode 100644 src/lib/linkdb.ts create mode 100644 src/lib/logger.ts create mode 100644 src/lib/mongodb.ts create mode 100644 src/lib/sessiondb.ts create mode 100644 src/lib/statisticsdb.ts create mode 100644 src/lib/userdb.ts create mode 100644 src/lib/utils.ts create mode 100644 src/middleware.ts create mode 100644 src/types/analytics.d.ts create mode 100644 src/types/auth.d.ts create mode 100644 src/types/global.d.ts create mode 100644 src/types/link.d.ts create mode 100644 src/types/session.d.ts create mode 100644 src/types/statistics.d.ts create mode 100644 src/types/user.d.ts create mode 100644 tsconfig.json delete mode 100644 web/static/css/style.css delete mode 100644 web/static/js/script.js delete mode 100644 web/templates/index.html diff --git a/.dockerignore b/.dockerignore index 37c14e6..ab64301 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,13 +1,10 @@ -__pycache__ -*.pyc -*.pyo -*.pyd -.Python -env/ -venv/ -.env +node_modules +.next .git -.gitignore -.vscode -*.md +.github *.log +.env +.env.local +.env.development.local +.env.test.local +.env.production.local \ No newline at end of file diff --git a/.env.example b/.env.example index da6f996..3d9d7fe 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,4 @@ -MONGO_URI=mongodb://localhost:27017/uLinkShortener -PORT=5000 \ No newline at end of file +MONGO_URI=mongodb://:@:/ +MONGO_DB_NAME= +NEXTAUTH_SECRET=VERY_SECURE_SECRET +NEXTAUTH_URL=http://localhost:3000 \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a4dc91f..89184f4 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,14 +1,32 @@ version: 2 updates: - - package-ecosystem: "gomod" + # Monitor npm dependencies + - package-ecosystem: "npm" directory: "/" schedule: interval: "weekly" open-pull-requests-limit: 10 - - # Enable version updates for GitHub Actions - - package-ecosystem: "github-actions" + versioning-strategy: auto + labels: + - "dependencies" + - "npm" + + # Monitor Docker dependencies + - package-ecosystem: "docker" directory: "/" schedule: interval: "weekly" - open-pull-requests-limit: 5 \ No newline at end of file + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "docker" + + # Monitor GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "github-actions" \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eaf2e1a..0449ae4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,9 +36,10 @@ jobs: id: meta uses: docker/metadata-action@v5 with: - images: ghcr.io/marcus7i/ulinkshortener + images: ghcr.io/${{ github.repository }} tags: | - type=raw,value=latest + type=ref,event=tag + type=raw,value=latest,enable={{is_default_branch}} type=sha,format=short type=ref,event=branch type=ref,event=pr @@ -50,5 +51,5 @@ jobs: file: ./Dockerfile push: ${{ github.event_name != 'pull_request' }} platforms: linux/amd64, linux/arm64/v8 - tags: ghcr.io/marcus7i/ulinkshortener:latest + tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index d558a03..3431edd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,44 @@ -venv/ -.env \ No newline at end of file +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# logs +*.log \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index aed9d38..370565d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,23 @@ -FROM golang:1.21-alpine AS builder +FROM oven/bun:1 AS builder WORKDIR /app -COPY go.mod go.sum ./ -RUN go mod download +COPY package.json bun.lock ./ +RUN bun install --frozen-lockfile COPY . . -RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o ulinkshortener ./cmd/api -FROM alpine:latest + +RUN bun run build + +FROM oven/bun:1-slim AS runner WORKDIR /app -RUN apk --no-cache add ca-certificates -COPY --from=builder /app/ulinkshortener . -COPY --from=builder /app/web /app/web +ENV NODE_ENV=production -EXPOSE ${PORT} -CMD ["./ulinkshortener"] +COPY --from=builder /app/.next ./.next +COPY --from=builder /app/public ./public +COPY --from=builder /app/package.json ./package.json +COPY --from=builder /app/next.config.ts ./next.config.ts + +RUN bun install --production +EXPOSE 3000 + +CMD ["bun", "run", "start"] \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index b369d7f..0000000 --- a/LICENSE +++ /dev/null @@ -1,7 +0,0 @@ -Copyright 2025 MarcUs7i - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 34002ee..e6b075b 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -# uLinkShortener +# µLinkShortener v2 -This project is the code behind [u.marcus7i.net](https://u.marcus7i.net), a custom URL shortener. It uses Go, MongoDB, and Docker for quick deployment. +This project is the code behind [u.marcus7i.net](https://u.marcus7i.net), a custom URL shortener. It uses Next.JS, MongoDB, and Docker for quick deployment. ## Prerequisites -- Go +- Next.js - MongoDB database (local or remote) - Docker & Docker Compose (optional, for containerized deployments) @@ -12,7 +12,9 @@ This project is the code behind [u.marcus7i.net](https://u.marcus7i.net), a cust 4. Define environment variables in the `.env` file: ``` MONGO_URI=mongodb://:@:/ - PORT= + MONGO_DB_NAME= + NEXTAUTH_SECRET=VERY_SECURE_SECRET + NEXTAUTH_URL=http://localhost:3000 ``` ## Running Locally @@ -21,11 +23,12 @@ This project is the code behind [u.marcus7i.net](https://u.marcus7i.net), a cust 1. Install dependencies: ``` - go mod download + bun install ``` 2. Build and run: ``` - go run cmd/api/main.go + bun run build + bun run start ``` ### With Docker @@ -44,7 +47,7 @@ This project is the code behind [u.marcus7i.net](https://u.marcus7i.net), a cust ``` docker-compose up --build ``` -2. The application will be available at http://localhost:5000 +2. The application will be available at http://localhost:3000 ## License diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..a50a921 --- /dev/null +++ b/bun.lock @@ -0,0 +1,998 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "ulinkshortener", + "dependencies": { + "chart.js": "^4.5.0", + "mongodb": "^6.17.0", + "next": "15.3.3", + "next-auth": "^4.24.11", + "react": "^19.0.0", + "react-chartjs-2": "^5.3.0", + "react-dom": "^19.0.0", + "ua-parser-js": "^2.0.3", + "winston": "^3.17.0", + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "@types/uuid": "^10.0.0", + "eslint": "^9", + "eslint-config-next": "15.3.3", + "tailwindcss": "^4", + "typescript": "^5", + }, + }, + }, + "packages": { + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + + "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], + + "@babel/runtime": ["@babel/runtime@7.27.6", "", {}, "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q=="], + + "@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="], + + "@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="], + + "@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" } }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], + + "@eslint/config-array": ["@eslint/config-array@0.20.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.2.3", "", {}, "sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg=="], + + "@eslint/core": ["@eslint/core@0.14.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], + + "@eslint/js": ["@eslint/js@9.28.0", "", {}, "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg=="], + + "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.2", "", { "dependencies": { "@eslint/core": "^0.15.0", "levn": "^0.4.1" } }, "sha512-4SaFZCNfJqvk/kenHpI8xvN42DMaoycy4PzKc5otHxRswww1kAt82OlBuwRVLofCACCTZEcla2Ydxv8scMXaTg=="], + + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.1.0" }, "os": "darwin", "cpu": "arm64" }, "sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.1.0" }, "os": "darwin", "cpu": "x64" }, "sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.1.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.1.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.1.0", "", { "os": "linux", "cpu": "arm" }, "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew=="], + + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.1.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.1.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.1.0" }, "os": "linux", "cpu": "arm" }, "sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.1.0" }, "os": "linux", "cpu": "arm64" }, "sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.1.0" }, "os": "linux", "cpu": "s390x" }, "sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.1.0" }, "os": "linux", "cpu": "x64" }, "sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.1.0" }, "os": "linux", "cpu": "arm64" }, "sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.1.0" }, "os": "linux", "cpu": "x64" }, "sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.2", "", { "dependencies": { "@emnapi/runtime": "^1.4.3" }, "cpu": "none" }, "sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ=="], + + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-cfP/r9FdS63VA5k0xiqaNaEoGxBg9k7uE+RQGzuK9fHt7jib4zAVVseR9LsE4gJcNWgT6APKMNnCcnyOtmSEUQ=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.2", "", { "os": "win32", "cpu": "x64" }, "sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw=="], + + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], + + "@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="], + + "@mongodb-js/saslprep": ["@mongodb-js/saslprep@1.3.0", "", { "dependencies": { "sparse-bitfield": "^3.0.3" } }, "sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.11", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.9.0" } }, "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA=="], + + "@next/env": ["@next/env@15.3.3", "", {}, "sha512-OdiMrzCl2Xi0VTjiQQUK0Xh7bJHnOuET2s+3V+Y40WJBAXrJeGA3f+I8MZJ/YQ3mVGi5XGR1L66oFlgqXhQ4Vw=="], + + "@next/eslint-plugin-next": ["@next/eslint-plugin-next@15.3.3", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-VKZJEiEdpKkfBmcokGjHu0vGDG+8CehGs90tBEy/IDoDDKGngeyIStt2MmE5FYNyU9BhgR7tybNWTAJY/30u+Q=="], + + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-WRJERLuH+O3oYB4yZNVahSVFmtxRNjNF1I1c34tYMoJb0Pve+7/RaLAJJizyYiFhjYNGHRAE1Ri2Fd23zgDqhg=="], + + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-XHdzH/yBc55lu78k/XwtuFR/ZXUTcflpRXcsu0nKmF45U96jt1tsOZhVrn5YH+paw66zOANpOnFQ9i6/j+UYvw=="], + + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-VZ3sYL2LXB8znNGcjhocikEkag/8xiLgnvQts41tq6i+wql63SMS1Q6N8RVXHw5pEUjiof+II3HkDd7GFcgkzw=="], + + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-h6Y1fLU4RWAp1HPNJWDYBQ+e3G7sLckyBXhmH9ajn8l/RSMnhbuPBV/fXmy3muMcVwoJdHL+UtzRzs0nXOf9SA=="], + + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-jJ8HRiF3N8Zw6hGlytCj5BiHyG/K+fnTKVDEKvUCyiQ/0r5tgwO7OgaRiOjjRoIx2vwLR+Rz8hQoPrnmFbJdfw=="], + + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-HrUcTr4N+RgiiGn3jjeT6Oo208UT/7BuTr7K0mdKRBtTbT4v9zJqCDKO97DUqqoBK1qyzP1RwvrWTvU6EPh/Cw=="], + + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.3.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-SxorONgi6K7ZUysMtRF3mIeHC5aA3IQLmKFQzU0OuhuUYwpOBc1ypaLJLP5Bf3M9k53KUUUj4vTPwzGvl/NwlQ=="], + + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4QZG6F8enl9/S2+yIiOiju0iCTFd93d8VC1q9LZS4p/Xuk81W2QDjCFeoogmrWWkAD59z8ZxepBQap2dKS5ruw=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="], + + "@panva/hkdf": ["@panva/hkdf@1.2.1", "", {}, "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw=="], + + "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], + + "@rushstack/eslint-patch": ["@rushstack/eslint-patch@1.11.0", "", {}, "sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ=="], + + "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], + + "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], + + "@tailwindcss/node": ["@tailwindcss/node@4.1.10", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.10" } }, "sha512-2ACf1znY5fpRBwRhMgj9ZXvb2XZW8qs+oTfotJ2C5xR0/WNL7UHZ7zXl6s+rUqedL1mNi+0O+WQr5awGowS3PQ=="], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.10", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.10", "@tailwindcss/oxide-darwin-arm64": "4.1.10", "@tailwindcss/oxide-darwin-x64": "4.1.10", "@tailwindcss/oxide-freebsd-x64": "4.1.10", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.10", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.10", "@tailwindcss/oxide-linux-arm64-musl": "4.1.10", "@tailwindcss/oxide-linux-x64-gnu": "4.1.10", "@tailwindcss/oxide-linux-x64-musl": "4.1.10", "@tailwindcss/oxide-wasm32-wasi": "4.1.10", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.10", "@tailwindcss/oxide-win32-x64-msvc": "4.1.10" } }, "sha512-v0C43s7Pjw+B9w21htrQwuFObSkio2aV/qPx/mhrRldbqxbWJK6KizM+q7BF1/1CmuLqZqX3CeYF7s7P9fbA8Q=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.10", "", { "os": "android", "cpu": "arm64" }, "sha512-VGLazCoRQ7rtsCzThaI1UyDu/XRYVyH4/EWiaSX6tFglE+xZB5cvtC5Omt0OQ+FfiIVP98su16jDVHDEIuH4iQ=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZIFqvR1irX2yNjWJzKCqTCcHZbgkSkSkZKbRM3BPzhDL/18idA8uWCoopYA2CSDdSGFlDAxYdU2yBHwAwx8euQ=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-eCA4zbIhWUFDXoamNztmS0MjXHSEJYlvATzWnRiTqJkcUteSjO94PoRHJy1Xbwp9bptjeIxxBHh+zBWFhttbrQ=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.10", "", { "os": "freebsd", "cpu": "x64" }, "sha512-8/392Xu12R0cc93DpiJvNpJ4wYVSiciUlkiOHOSOQNH3adq9Gi/dtySK7dVQjXIOzlpSHjeCL89RUUI8/GTI6g=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.10", "", { "os": "linux", "cpu": "arm" }, "sha512-t9rhmLT6EqeuPT+MXhWhlRYIMSfh5LZ6kBrC4FS6/+M1yXwfCtp24UumgCWOAJVyjQwG+lYva6wWZxrfvB+NhQ=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-3oWrlNlxLRxXejQ8zImzrVLuZ/9Z2SeKoLhtCu0hpo38hTO2iL86eFOu4sVR8cZc6n3z7eRXXqtHJECa6mFOvA=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-saScU0cmWvg/Ez4gUmQWr9pvY9Kssxt+Xenfx1LG7LmqjcrvBnw4r9VjkFcqmbBb7GCBwYNcZi9X3/oMda9sqQ=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.10", "", { "os": "linux", "cpu": "x64" }, "sha512-/G3ao/ybV9YEEgAXeEg28dyH6gs1QG8tvdN9c2MNZdUXYBaIY/Gx0N6RlJzfLy/7Nkdok4kaxKPHKJUlAaoTdA=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.10", "", { "os": "linux", "cpu": "x64" }, "sha512-LNr7X8fTiKGRtQGOerSayc2pWJp/9ptRYAa4G+U+cjw9kJZvkopav1AQc5HHD+U364f71tZv6XamaHKgrIoVzA=="], + + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.10", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@emnapi/wasi-threads": "^1.0.2", "@napi-rs/wasm-runtime": "^0.2.10", "@tybys/wasm-util": "^0.9.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-d6ekQpopFQJAcIK2i7ZzWOYGZ+A6NzzvQ3ozBvWFdeyqfOZdYHU66g5yr+/HC4ipP1ZgWsqa80+ISNILk+ae/Q=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-i1Iwg9gRbwNVOCYmnigWCCgow8nDWSFmeTUU5nbNx3rqbe4p0kRbEqLwLJbYZKmSSp23g4N6rCDmm7OuPBXhDA=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.10", "", { "os": "win32", "cpu": "x64" }, "sha512-sGiJTjcBSfGq2DVRtaSljq5ZgZS2SDHSIfhOylkBvHVjwOsodBhnb3HdmiKkVuUGKD0I7G63abMOVaskj1KpOA=="], + + "@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.10", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.10", "@tailwindcss/oxide": "4.1.10", "postcss": "^8.4.41", "tailwindcss": "4.1.10" } }, "sha512-B+7r7ABZbkXJwpvt2VMnS6ujcDoR2OOcFaqrLIo1xbcdxje4Vf+VgJdBzNNbrAjBj/rLZ66/tlQ1knIGNLKOBQ=="], + + "@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], + + "@types/node": ["@types/node@20.19.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-hfrc+1tud1xcdVTABC2JiomZJEklMcXYNTVtZLAeqTVWD+qL5jkHKT+1lOtqDdGxt+mB53DTtiz673vfjU8D1Q=="], + + "@types/node-fetch": ["@types/node-fetch@2.6.12", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.0" } }, "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA=="], + + "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="], + + "@types/react-dom": ["@types/react-dom@19.1.6", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw=="], + + "@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="], + + "@types/uuid": ["@types/uuid@10.0.0", "", {}, "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ=="], + + "@types/webidl-conversions": ["@types/webidl-conversions@7.0.3", "", {}, "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="], + + "@types/whatwg-url": ["@types/whatwg-url@11.0.5", "", { "dependencies": { "@types/webidl-conversions": "*" } }, "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ=="], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.34.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.34.0", "@typescript-eslint/type-utils": "8.34.0", "@typescript-eslint/utils": "8.34.0", "@typescript-eslint/visitor-keys": "8.34.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.34.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-QXwAlHlbcAwNlEEMKQS2RCgJsgXrTJdjXT08xEgbPFa2yYQgVjBymxP5DrfrE7X7iodSzd9qBUHUycdyVJTW1w=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.34.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.34.0", "@typescript-eslint/types": "8.34.0", "@typescript-eslint/typescript-estree": "8.34.0", "@typescript-eslint/visitor-keys": "8.34.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-vxXJV1hVFx3IXz/oy2sICsJukaBrtDEQSBiV48/YIV5KWjX1dO+bcIr/kCPrW6weKXvsaGKFNlwH0v2eYdRRbA=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.34.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.34.0", "@typescript-eslint/types": "^8.34.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.34.0", "", { "dependencies": { "@typescript-eslint/types": "8.34.0", "@typescript-eslint/visitor-keys": "8.34.0" } }, "sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.34.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.34.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.34.0", "@typescript-eslint/utils": "8.34.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-n7zSmOcUVhcRYC75W2pnPpbO1iwhJY3NLoHEtbJwJSNlVAZuwqu05zY3f3s2SDWWDSo9FdN5szqc73DCtDObAg=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.34.0", "", {}, "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.34.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.34.0", "@typescript-eslint/tsconfig-utils": "8.34.0", "@typescript-eslint/types": "8.34.0", "@typescript-eslint/visitor-keys": "8.34.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.34.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.34.0", "@typescript-eslint/types": "8.34.0", "@typescript-eslint/typescript-estree": "8.34.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.34.0", "", { "dependencies": { "@typescript-eslint/types": "8.34.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-qHV7pW7E85A0x6qyrFn+O+q1k1p3tQCsqIZ1KZ5ESLXY57aTvUd3/a4rdPTeXisvhXn2VQG0VSKUqs8KHF2zcA=="], + + "@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.9.0", "", { "os": "android", "cpu": "arm" }, "sha512-h1T2c2Di49ekF2TE8ZCoJkb+jwETKUIPDJ/nO3tJBKlLFPu+fyd93f0rGP/BvArKx2k2HlRM4kqkNarj3dvZlg=="], + + "@unrs/resolver-binding-android-arm64": ["@unrs/resolver-binding-android-arm64@1.9.0", "", { "os": "android", "cpu": "arm64" }, "sha512-sG1NHtgXtX8owEkJ11yn34vt0Xqzi3k9TJ8zppDmyG8GZV4kVWw44FHwKwHeEFl07uKPeC4ZoyuQaGh5ruJYPA=="], + + "@unrs/resolver-binding-darwin-arm64": ["@unrs/resolver-binding-darwin-arm64@1.9.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-nJ9z47kfFnCxN1z/oYZS7HSNsFh43y2asePzTEZpEvK7kGyuShSl3RRXnm/1QaqFL+iP+BjMwuB+DYUymOkA5A=="], + + "@unrs/resolver-binding-darwin-x64": ["@unrs/resolver-binding-darwin-x64@1.9.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-TK+UA1TTa0qS53rjWn7cVlEKVGz2B6JYe0C++TdQjvWYIyx83ruwh0wd4LRxYBM5HeuAzXcylA9BH2trARXJTw=="], + + "@unrs/resolver-binding-freebsd-x64": ["@unrs/resolver-binding-freebsd-x64@1.9.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6uZwzMRFcD7CcCd0vz3Hp+9qIL2jseE/bx3ZjaLwn8t714nYGwiE84WpaMCYjU+IQET8Vu/+BNAGtYD7BG/0yA=="], + + "@unrs/resolver-binding-linux-arm-gnueabihf": ["@unrs/resolver-binding-linux-arm-gnueabihf@1.9.0", "", { "os": "linux", "cpu": "arm" }, "sha512-bPUBksQfrgcfv2+mm+AZinaKq8LCFvt5PThYqRotqSuuZK1TVKkhbVMS/jvSRfYl7jr3AoZLYbDkItxgqMKRkg=="], + + "@unrs/resolver-binding-linux-arm-musleabihf": ["@unrs/resolver-binding-linux-arm-musleabihf@1.9.0", "", { "os": "linux", "cpu": "arm" }, "sha512-uT6E7UBIrTdCsFQ+y0tQd3g5oudmrS/hds5pbU3h4s2t/1vsGWbbSKhBSCD9mcqaqkBwoqlECpUrRJCmldl8PA=="], + + "@unrs/resolver-binding-linux-arm64-gnu": ["@unrs/resolver-binding-linux-arm64-gnu@1.9.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-vdqBh911wc5awE2bX2zx3eflbyv8U9xbE/jVKAm425eRoOVv/VseGZsqi3A3SykckSpF4wSROkbQPvbQFn8EsA=="], + + "@unrs/resolver-binding-linux-arm64-musl": ["@unrs/resolver-binding-linux-arm64-musl@1.9.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-/8JFZ/SnuDr1lLEVsxsuVwrsGquTvT51RZGvyDB/dOK3oYK2UqeXzgeyq6Otp8FZXQcEYqJwxb9v+gtdXn03eQ=="], + + "@unrs/resolver-binding-linux-ppc64-gnu": ["@unrs/resolver-binding-linux-ppc64-gnu@1.9.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FkJjybtrl+rajTw4loI3L6YqSOpeZfDls4SstL/5lsP2bka9TiHUjgMBjygeZEis1oC8LfJTS8FSgpKPaQx2tQ=="], + + "@unrs/resolver-binding-linux-riscv64-gnu": ["@unrs/resolver-binding-linux-riscv64-gnu@1.9.0", "", { "os": "linux", "cpu": "none" }, "sha512-w/NZfHNeDusbqSZ8r/hp8iL4S39h4+vQMc9/vvzuIKMWKppyUGKm3IST0Qv0aOZ1rzIbl9SrDeIqK86ZpUK37w=="], + + "@unrs/resolver-binding-linux-riscv64-musl": ["@unrs/resolver-binding-linux-riscv64-musl@1.9.0", "", { "os": "linux", "cpu": "none" }, "sha512-bEPBosut8/8KQbUixPry8zg/fOzVOWyvwzOfz0C0Rw6dp+wIBseyiHKjkcSyZKv/98edrbMknBaMNJfA/UEdqw=="], + + "@unrs/resolver-binding-linux-s390x-gnu": ["@unrs/resolver-binding-linux-s390x-gnu@1.9.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-LDtMT7moE3gK753gG4pc31AAqGUC86j3AplaFusc717EUGF9ZFJ356sdQzzZzkBk1XzMdxFyZ4f/i35NKM/lFA=="], + + "@unrs/resolver-binding-linux-x64-gnu": ["@unrs/resolver-binding-linux-x64-gnu@1.9.0", "", { "os": "linux", "cpu": "x64" }, "sha512-WmFd5KINHIXj8o1mPaT8QRjA9HgSXhN1gl9Da4IZihARihEnOylu4co7i/yeaIpcfsI6sYs33cNZKyHYDh0lrA=="], + + "@unrs/resolver-binding-linux-x64-musl": ["@unrs/resolver-binding-linux-x64-musl@1.9.0", "", { "os": "linux", "cpu": "x64" }, "sha512-CYuXbANW+WgzVRIl8/QvZmDaZxrqvOldOwlbUjIM4pQ46FJ0W5cinJ/Ghwa/Ng1ZPMJMk1VFdsD/XwmCGIXBWg=="], + + "@unrs/resolver-binding-wasm32-wasi": ["@unrs/resolver-binding-wasm32-wasi@1.9.0", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.11" }, "cpu": "none" }, "sha512-6Rp2WH0OoitMYR57Z6VE8Y6corX8C6QEMWLgOV6qXiJIeZ1F9WGXY/yQ8yDC4iTraotyLOeJ2Asea0urWj2fKQ=="], + + "@unrs/resolver-binding-win32-arm64-msvc": ["@unrs/resolver-binding-win32-arm64-msvc@1.9.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-rknkrTRuvujprrbPmGeHi8wYWxmNVlBoNW8+4XF2hXUnASOjmuC9FNF1tGbDiRQWn264q9U/oGtixyO3BT8adQ=="], + + "@unrs/resolver-binding-win32-ia32-msvc": ["@unrs/resolver-binding-win32-ia32-msvc@1.9.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-Ceymm+iBl+bgAICtgiHyMLz6hjxmLJKqBim8tDzpX61wpZOx2bPK6Gjuor7I2RiUynVjvvkoRIkrPyMwzBzF3A=="], + + "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.9.0", "", { "os": "win32", "cpu": "x64" }, "sha512-k59o9ZyeyS0hAlcaKFezYSH2agQeRFEB7KoQLXl3Nb3rgkqT1NY9Vwy+SqODiLmYnEjxWJVRE/yq2jFVqdIxZw=="], + + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + + "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], + + "array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="], + + "array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="], + + "array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="], + + "array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="], + + "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="], + + "array.prototype.tosorted": ["array.prototype.tosorted@1.1.4", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3", "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA=="], + + "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], + + "ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="], + + "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + + "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], + + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], + + "axe-core": ["axe-core@4.10.3", "", {}, "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg=="], + + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "bson": ["bson@6.10.4", "", {}, "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng=="], + + "busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="], + + "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001723", "", {}, "sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "chart.js": ["chart.js@4.5.0", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ=="], + + "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + + "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], + + "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + + "colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="], + + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="], + + "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], + + "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], + + "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], + + "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + + "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + + "detect-europe-js": ["detect-europe-js@0.1.2", "", {}, "sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow=="], + + "detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="], + + "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="], + + "enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="], + + "es-abstract": ["es-abstract@1.24.0", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-iterator-helpers": ["es-iterator-helpers@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.0.3", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.6", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.4", "safe-array-concat": "^1.1.3" } }, "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="], + + "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@9.28.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.1", "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.28.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ=="], + + "eslint-config-next": ["eslint-config-next@15.3.3", "", { "dependencies": { "@next/eslint-plugin-next": "15.3.3", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^5.0.0" }, "peerDependencies": { "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-QJLv/Ouk2vZnxL4b67njJwTLjTf7uZRltI0LL4GERYR4qMF5z08+gxkfODAeaK7TiC6o+cER91bDaEnwrTWV6Q=="], + + "eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.9", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", "resolve": "^1.22.4" } }, "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g=="], + + "eslint-import-resolver-typescript": ["eslint-import-resolver-typescript@3.10.1", "", { "dependencies": { "@nolyfill/is-core-module": "1.0.39", "debug": "^4.4.0", "get-tsconfig": "^4.10.0", "is-bun-module": "^2.0.0", "stable-hash": "^0.0.5", "tinyglobby": "^0.2.13", "unrs-resolver": "^1.6.2" }, "peerDependencies": { "eslint": "*", "eslint-plugin-import": "*", "eslint-plugin-import-x": "*" }, "optionalPeers": ["eslint-plugin-import", "eslint-plugin-import-x"] }, "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ=="], + + "eslint-module-utils": ["eslint-module-utils@2.12.0", "", { "dependencies": { "debug": "^3.2.7" } }, "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg=="], + + "eslint-plugin-import": ["eslint-plugin-import@2.31.0", "", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.8", "array.prototype.findlastindex": "^1.2.5", "array.prototype.flat": "^1.3.2", "array.prototype.flatmap": "^1.3.2", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.0", "hasown": "^2.0.2", "is-core-module": "^2.15.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.0", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.8", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A=="], + + "eslint-plugin-jsx-a11y": ["eslint-plugin-jsx-a11y@6.10.2", "", { "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", "axe-core": "^4.10.0", "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "safe-regex-test": "^1.0.3", "string.prototype.includes": "^2.0.1" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q=="], + + "eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="], + + "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="], + + "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + + "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], + + "fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="], + + "fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + + "fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="], + + "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + + "form-data": ["form-data@4.0.3", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="], + + "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], + + "get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + + "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], + + "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + + "has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], + + "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], + + "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], + + "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], + + "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="], + + "is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="], + + "is-bun-module": ["is-bun-module@2.0.0", "", { "dependencies": { "semver": "^7.7.1" } }, "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ=="], + + "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], + + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + + "is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="], + + "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], + + "is-generator-function": ["is-generator-function@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "get-proto": "^1.0.0", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], + + "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], + + "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], + + "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="], + + "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="], + + "is-standalone-pwa": ["is-standalone-pwa@0.1.1", "", {}, "sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g=="], + + "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="], + + "is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="], + + "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], + + "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], + + "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="], + + "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="], + + "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="], + + "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], + + "jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], + + "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], + + "language-subtag-registry": ["language-subtag-registry@0.3.23", "", {}, "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="], + + "language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="], + + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + + "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + + "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "memory-pager": ["memory-pager@1.5.0", "", {}, "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "minizlib": ["minizlib@3.0.2", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA=="], + + "mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="], + + "mongodb": ["mongodb@6.17.0", "", { "dependencies": { "@mongodb-js/saslprep": "^1.1.9", "bson": "^6.10.4", "mongodb-connection-string-url": "^3.0.0" }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.188.0", "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", "mongodb-client-encryption": ">=6.0.0 <7", "snappy": "^7.2.2", "socks": "^2.7.1" }, "optionalPeers": ["@aws-sdk/credential-providers", "@mongodb-js/zstd", "gcp-metadata", "kerberos", "mongodb-client-encryption", "snappy", "socks"] }, "sha512-neerUzg/8U26cgruLysKEjJvoNSXhyID3RvzvdcpsIi2COYM3FS3o9nlH7fxFtefTb942dX3W9i37oPfCVj4wA=="], + + "mongodb-connection-string-url": ["mongodb-connection-string-url@3.0.2", "", { "dependencies": { "@types/whatwg-url": "^11.0.2", "whatwg-url": "^14.1.0 || ^13.0.0" } }, "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "napi-postinstall": ["napi-postinstall@0.2.4", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-ZEzHJwBhZ8qQSbknHqYcdtQVr8zUgGyM/q6h6qAyhtyVMNrSgDhrC4disf03dYW0e+czXyLnZINnCTEkWy0eJg=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "next": ["next@15.3.3", "", { "dependencies": { "@next/env": "15.3.3", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.3.3", "@next/swc-darwin-x64": "15.3.3", "@next/swc-linux-arm64-gnu": "15.3.3", "@next/swc-linux-arm64-musl": "15.3.3", "@next/swc-linux-x64-gnu": "15.3.3", "@next/swc-linux-x64-musl": "15.3.3", "@next/swc-win32-arm64-msvc": "15.3.3", "@next/swc-win32-x64-msvc": "15.3.3", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-JqNj29hHNmCLtNvd090SyRbXJiivQ+58XjCcrC50Crb5g5u2zi7Y2YivbsEfzk6AtVI80akdOQbaMZwWB1Hthw=="], + + "next-auth": ["next-auth@4.24.11", "", { "dependencies": { "@babel/runtime": "^7.20.13", "@panva/hkdf": "^1.0.2", "cookie": "^0.7.0", "jose": "^4.15.5", "oauth": "^0.9.15", "openid-client": "^5.4.0", "preact": "^10.6.3", "preact-render-to-string": "^5.1.19", "uuid": "^8.3.2" }, "peerDependencies": { "@auth/core": "0.34.2", "next": "^12.2.5 || ^13 || ^14 || ^15", "nodemailer": "^6.6.5", "react": "^17.0.2 || ^18 || ^19", "react-dom": "^17.0.2 || ^18 || ^19" }, "optionalPeers": ["@auth/core", "nodemailer"] }, "sha512-pCFXzIDQX7xmHFs4KVH4luCjaCbuPRtZ9oBUjUhOk84mZ9WVPf94n87TxYI4rSRf9HmfHEF8Yep3JrYDVOo3Cw=="], + + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + + "oauth": ["oauth@0.9.15", "", {}, "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-hash": ["object-hash@2.2.0", "", {}, "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], + + "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], + + "object.entries": ["object.entries@1.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.1" } }, "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw=="], + + "object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="], + + "object.groupby": ["object.groupby@1.0.3", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2" } }, "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ=="], + + "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], + + "oidc-token-hash": ["oidc-token-hash@5.1.0", "", {}, "sha512-y0W+X7Ppo7oZX6eovsRkuzcSM40Bicg2JEJkDJ4irIt1wsYAP5MLSNv+QAogO8xivMffw/9OvV3um1pxXgt1uA=="], + + "one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="], + + "openid-client": ["openid-client@5.7.1", "", { "dependencies": { "jose": "^4.15.9", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" } }, "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], + + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], + + "postcss": ["postcss@8.5.5", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg=="], + + "preact": ["preact@10.26.9", "", {}, "sha512-SSjF9vcnF27mJK1XyFMNJzFd5u3pQiATFqoaDy03XuN00u4ziveVVEGt5RKJrDR8MHE/wJo9Nnad56RLzS2RMA=="], + + "preact-render-to-string": ["preact-render-to-string@5.2.6", "", { "dependencies": { "pretty-format": "^3.8.0" }, "peerDependencies": { "preact": ">=10" } }, "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "pretty-format": ["pretty-format@3.8.0", "", {}, "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew=="], + + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], + + "react-chartjs-2": ["react-chartjs-2@5.3.0", "", { "peerDependencies": { "chart.js": "^4.1.1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw=="], + + "react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="], + + "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], + + "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], + + "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], + + "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], + + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], + + "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], + + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], + + "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], + + "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], + + "sharp": ["sharp@0.34.2", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.4", "semver": "^7.7.2" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.2", "@img/sharp-darwin-x64": "0.34.2", "@img/sharp-libvips-darwin-arm64": "1.1.0", "@img/sharp-libvips-darwin-x64": "1.1.0", "@img/sharp-libvips-linux-arm": "1.1.0", "@img/sharp-libvips-linux-arm64": "1.1.0", "@img/sharp-libvips-linux-ppc64": "1.1.0", "@img/sharp-libvips-linux-s390x": "1.1.0", "@img/sharp-libvips-linux-x64": "1.1.0", "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", "@img/sharp-libvips-linuxmusl-x64": "1.1.0", "@img/sharp-linux-arm": "0.34.2", "@img/sharp-linux-arm64": "0.34.2", "@img/sharp-linux-s390x": "0.34.2", "@img/sharp-linux-x64": "0.34.2", "@img/sharp-linuxmusl-arm64": "0.34.2", "@img/sharp-linuxmusl-x64": "0.34.2", "@img/sharp-wasm32": "0.34.2", "@img/sharp-win32-arm64": "0.34.2", "@img/sharp-win32-ia32": "0.34.2", "@img/sharp-win32-x64": "0.34.2" } }, "sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "sparse-bitfield": ["sparse-bitfield@3.0.3", "", { "dependencies": { "memory-pager": "^1.0.2" } }, "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ=="], + + "stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="], + + "stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="], + + "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], + + "streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="], + + "string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="], + + "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="], + + "string.prototype.repeat": ["string.prototype.repeat@1.0.0", "", { "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" } }, "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w=="], + + "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], + + "string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="], + + "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "tailwindcss": ["tailwindcss@4.1.10", "", {}, "sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA=="], + + "tapable": ["tapable@2.2.2", "", {}, "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg=="], + + "tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="], + + "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], + + "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], + + "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], + + "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], + + "tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], + + "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="], + + "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="], + + "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], + + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + + "ua-is-frozen": ["ua-is-frozen@0.1.2", "", {}, "sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw=="], + + "ua-parser-js": ["ua-parser-js@2.0.3", "", { "dependencies": { "@types/node-fetch": "^2.6.12", "detect-europe-js": "^0.1.2", "is-standalone-pwa": "^0.1.1", "node-fetch": "^2.7.0", "ua-is-frozen": "^0.1.2" }, "bin": { "ua-parser-js": "script/cli.js" } }, "sha512-LZyXZdNttONW8LjzEH3Z8+6TE7RfrEiJqDKyh0R11p/kxvrV2o9DrT2FGZO+KVNs3k+drcIQ6C3En6wLnzJGpw=="], + + "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "unrs-resolver": ["unrs-resolver@1.9.0", "", { "dependencies": { "napi-postinstall": "^0.2.2" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.9.0", "@unrs/resolver-binding-android-arm64": "1.9.0", "@unrs/resolver-binding-darwin-arm64": "1.9.0", "@unrs/resolver-binding-darwin-x64": "1.9.0", "@unrs/resolver-binding-freebsd-x64": "1.9.0", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.9.0", "@unrs/resolver-binding-linux-arm-musleabihf": "1.9.0", "@unrs/resolver-binding-linux-arm64-gnu": "1.9.0", "@unrs/resolver-binding-linux-arm64-musl": "1.9.0", "@unrs/resolver-binding-linux-ppc64-gnu": "1.9.0", "@unrs/resolver-binding-linux-riscv64-gnu": "1.9.0", "@unrs/resolver-binding-linux-riscv64-musl": "1.9.0", "@unrs/resolver-binding-linux-s390x-gnu": "1.9.0", "@unrs/resolver-binding-linux-x64-gnu": "1.9.0", "@unrs/resolver-binding-linux-x64-musl": "1.9.0", "@unrs/resolver-binding-wasm32-wasi": "1.9.0", "@unrs/resolver-binding-win32-arm64-msvc": "1.9.0", "@unrs/resolver-binding-win32-ia32-msvc": "1.9.0", "@unrs/resolver-binding-win32-x64-msvc": "1.9.0" } }, "sha512-wqaRu4UnzBD2ABTC1kLfBjAqIDZ5YUTr/MLGa7By47JV1bJDSW7jq/ZSLigB7enLe7ubNaJhtnBXgrc/50cEhg=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + + "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], + + "whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], + + "which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="], + + "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], + + "which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="], + + "winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="], + + "winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@eslint/plugin-kit/@eslint/core": ["@eslint/core@0.15.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-b7ePw78tEWWkpgZCDYkbqDOP8dmM6qe+AOC6iuJqlq1R/0ahMAeH3qynpnqKFGkMltrp44ohV4ubGyvLX28tzw=="], + + "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="], + + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.11", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.9.0" }, "bundled": true }, "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA=="], + + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="], + + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "@typescript-eslint/typescript-estree/fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "@typescript-eslint/typescript-estree/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "colorspace/color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="], + + "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-plugin-react/resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="], + + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "is-bun-module/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], + + "node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "sharp/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "@typescript-eslint/typescript-estree/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "colorspace/color/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "colorspace/color/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + } +} diff --git a/cmd/api/main.go b/cmd/api/main.go deleted file mode 100644 index 06bc8f9..0000000 --- a/cmd/api/main.go +++ /dev/null @@ -1,92 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "html/template" - "log" - "net/http" - "os" - "os/signal" - "syscall" - "time" - - "github.com/joho/godotenv" - "github.com/marcus7i/ulinkshortener/internal/api" - "github.com/marcus7i/ulinkshortener/internal/config" - "github.com/marcus7i/ulinkshortener/internal/database" - "github.com/marcus7i/ulinkshortener/internal/api/handlers" -) - -func main() { - if err := godotenv.Load(); err != nil { - log.Println("No .env file found, using environment variables") - } - - cfg := config.New() - - db, err := database.New(cfg.MongoURI) - if err != nil { - log.Fatalf("Failed to connect to MongoDB: %v", err) - } - defer db.Close() - - r := api.SetupRouter(db) - - funcMap := template.FuncMap{ - "marshal": func(v interface{}) template.JS { - a, _ := json.Marshal(v) - return template.JS(a) - }, - } - - templates := template.Must(template.New("").Funcs(funcMap).ParseGlob("web/templates/*.html")) - - r.PathPrefix("/").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/" { - h := handlers.NewHandler(db) - stats, err := h.GetStatsData(r.Context()) - if err != nil { - http.Error(w, "Error generating stats", http.StatusInternalServerError) - return - } - - cookie, _ := r.Cookie("account_id") - if cookie != nil { - stats.LoggedIn = true - } - - templates.ExecuteTemplate(w, "index.html", map[string]interface{}{ - "stats": stats, - }) - return - } - - http.NotFound(w, r) - }) - - srv := &http.Server{ - Addr: ":" + cfg.Port, - Handler: r, - } - - go func() { - log.Printf("Server running on port %s", cfg.Port) - if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Fatalf("Listen error: %v", err) - } - }() - - quit := make(chan os.Signal, 1) - signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - <-quit - log.Println("Shutting down server...") - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - if err := srv.Shutdown(ctx); err != nil { - log.Fatal("Server forced to shutdown:", err) - } - - log.Println("Server exited with code 0") -} diff --git a/docker-compose-build.yml b/docker-compose-build.yml index 9754b28..c379cf5 100644 --- a/docker-compose-build.yml +++ b/docker-compose-build.yml @@ -3,8 +3,13 @@ services: build: context: . dockerfile: Dockerfile - container_name: ulinkshortener - restart: unless-stopped - network_mode: "host" + ports: + - "3000:3000" + environment: + - MONGO_URI=${MONGO_URI} + - MONGO_DB_NAME=${MONGO_DB_NAME} + - NEXTAUTH_SECRET=${NEXTAUTH_SECRET} + - NEXTAUTH_URL=${NEXTAUTH_URL} volumes: - - ./.env:/app/.env \ No newline at end of file + - ./.env:/app/.env + restart: unless-stopped \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index b4c0489..8a1fc90 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,13 @@ services: ulinkshortener: - image: ghcr.io/marcus7i/ulinkshortener:latest - container_name: ulinkshortener - restart: unless-stopped - network_mode: "host" + image: ghcr.io/kizuren/ulinkshortener + ports: + - "3000:3000" + environment: + - MONGO_URI=${MONGO_URI} + - MONGO_DB_NAME=${MONGO_DB_NAME} + - NEXTAUTH_SECRET=${NEXTAUTH_SECRET} + - NEXTAUTH_URL=${NEXTAUTH_URL} volumes: - - ./.env:/app/.env \ No newline at end of file + - ./.env:/app/.env + restart: unless-stopped \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..c85fb67 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,16 @@ +import { dirname } from "path"; +import { fileURLToPath } from "url"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const compat = new FlatCompat({ + baseDirectory: __dirname, +}); + +const eslintConfig = [ + ...compat.extends("next/core-web-vitals", "next/typescript"), +]; + +export default eslintConfig; diff --git a/go.mod b/go.mod deleted file mode 100644 index 80d3a84..0000000 --- a/go.mod +++ /dev/null @@ -1,23 +0,0 @@ -module github.com/marcus7i/ulinkshortener - -go 1.21 - -require ( - github.com/gorilla/mux v1.8.1 - github.com/joho/godotenv v1.5.1 - github.com/mssola/user_agent v0.6.0 - go.mongodb.org/mongo-driver v1.17.3 -) - -require ( - github.com/golang/snappy v0.0.4 // indirect - github.com/klauspost/compress v1.17.5 // indirect - github.com/montanaflynn/stats v0.7.1 // indirect - github.com/xdg-go/pbkdf2 v1.0.0 // indirect - github.com/xdg-go/scram v1.1.2 // indirect - github.com/xdg-go/stringprep v1.0.4 // indirect - github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect - golang.org/x/crypto v0.31.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/text v0.21.0 // indirect -) diff --git a/go.sum b/go.sum deleted file mode 100644 index 5f2dc6a..0000000 --- a/go.sum +++ /dev/null @@ -1,56 +0,0 @@ -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= -github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/klauspost/compress v1.17.5 h1:d4vBd+7CHydUqpFBgUEKkSdtSugf9YFmSkvUYPquI5E= -github.com/klauspost/compress v1.17.5/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= -github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= -github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= -github.com/mssola/user_agent v0.6.0 h1:uwPR4rtWlCHRFyyP9u2KOV0u8iQXmS7Z7feTrstQwk4= -github.com/mssola/user_agent v0.6.0/go.mod h1:TTPno8LPY3wAIEKRpAtkdMT0f8SE24pLRGPahjCH4uw= -github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= -github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= -github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= -github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= -github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= -github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= -github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ= -go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/api/handlers/analytics.go b/internal/api/handlers/analytics.go deleted file mode 100644 index 22a94bb..0000000 --- a/internal/api/handlers/analytics.go +++ /dev/null @@ -1,126 +0,0 @@ -package handlers - -import ( - "context" - "net/http" - - "github.com/gorilla/mux" - "github.com/marcus7i/ulinkshortener/internal/database" - "github.com/marcus7i/ulinkshortener/internal/models" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" -) - -func (h *Handler) GetAnalytics(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - accountID := vars["accountID"] - - ctx := context.Background() - var user models.User - err := h.DB.Collection(database.UsersCollection).FindOne(ctx, bson.M{"account_id": accountID}).Decode(&user) - if err != nil { - respondWithError(w, http.StatusUnauthorized, "Invalid account") - return - } - - cursor, err := h.DB.Collection(database.LinksCollection).Find(ctx, bson.M{"account_id": accountID}) - if err != nil { - respondWithError(w, http.StatusInternalServerError, "Failed to retrieve links") - return - } - defer cursor.Close(ctx) - - var links []models.Link - if err = cursor.All(ctx, &links); err != nil { - respondWithError(w, http.StatusInternalServerError, "Failed to process links") - return - } - - cursor, err = h.DB.Collection(database.AnalyticsCollection).Find(ctx, bson.M{"account_id": accountID}) - if err != nil { - respondWithError(w, http.StatusInternalServerError, "Failed to retrieve analytics") - return - } - defer cursor.Close(ctx) - - var analytics []map[string]interface{} - if err = cursor.All(ctx, &analytics); err != nil { - respondWithError(w, http.StatusInternalServerError, "Failed to process analytics") - return - } - - respondWithJSON(w, http.StatusOK, map[string]interface{}{ - "links": links, - "analytics": analytics, - }) -} - -func (h *Handler) GetStatsData(ctx context.Context) (models.Stats, error) { - - totalLinks, err := h.DB.Collection(database.LinksCollection).CountDocuments(ctx, bson.M{}) - if err != nil { - totalLinks = 0 - } - - totalClicks, err := h.DB.Collection(database.AnalyticsCollection).CountDocuments(ctx, bson.M{}) - if err != nil { - totalClicks = 0 - } - - ipVersionsPipeline := mongo.Pipeline{ - {{Key: "$group", Value: bson.D{{Key: "_id", Value: "$ip_version"}, {Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}}}}}, - {{Key: "$sort", Value: bson.D{{Key: "count", Value: -1}}}}, - } - ipVersionsCursor, err := h.DB.Collection(database.AnalyticsCollection).Aggregate(ctx, ipVersionsPipeline) - var ipVersions []models.StatItem - if err == nil { - ipVersionsCursor.All(ctx, &ipVersions) - } - - osPipeline := mongo.Pipeline{ - {{Key: "$group", Value: bson.D{{Key: "_id", Value: "$platform"}, {Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}}}}}, - {{Key: "$sort", Value: bson.D{{Key: "count", Value: -1}}}}, - {{Key: "$limit", Value: 10}}, - } - osCursor, err := h.DB.Collection(database.AnalyticsCollection).Aggregate(ctx, osPipeline) - var osStats []models.StatItem - if err == nil { - osCursor.All(ctx, &osStats) - } - - countryPipeline := mongo.Pipeline{ - {{Key: "$group", Value: bson.D{{Key: "_id", Value: "$country"}, {Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}}}}}, - {{Key: "$sort", Value: bson.D{{Key: "count", Value: -1}}}}, - {{Key: "$limit", Value: 10}}, - } - countryCursor, err := h.DB.Collection(database.AnalyticsCollection).Aggregate(ctx, countryPipeline) - var countryStats []models.StatItem - if err == nil { - countryCursor.All(ctx, &countryStats) - } - - ispPipeline := mongo.Pipeline{ - {{Key: "$group", Value: bson.D{{Key: "_id", Value: "$isp"}, {Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}}}}}, - {{Key: "$sort", Value: bson.D{{Key: "count", Value: -1}}}}, - {{Key: "$limit", Value: 10}}, - } - ispCursor, err := h.DB.Collection(database.AnalyticsCollection).Aggregate(ctx, ispPipeline) - var ispStats []models.StatItem - if err == nil { - ispCursor.All(ctx, &ispStats) - } - - stats := models.Stats{ - TotalLinks: totalLinks, - TotalClicks: totalClicks, - ChartData: models.ChartData{ - IPVersions: ipVersions, - OSStats: osStats, - CountryStats: countryStats, - ISPStats: ispStats, - }, - LoggedIn: false, - } - - return stats, nil -} diff --git a/internal/api/handlers/auth.go b/internal/api/handlers/auth.go deleted file mode 100644 index e6b0135..0000000 --- a/internal/api/handlers/auth.go +++ /dev/null @@ -1,95 +0,0 @@ -package handlers - -import ( - "context" - "encoding/json" - "net/http" - "time" - - "github.com/marcus7i/ulinkshortener/internal/database" - "github.com/marcus7i/ulinkshortener/internal/models" - "github.com/marcus7i/ulinkshortener/internal/utils" - "go.mongodb.org/mongo-driver/bson" -) - -type LoginRequest struct { - AccountID string `json:"account_id"` -} - -func (h *Handler) Register(w http.ResponseWriter, r *http.Request) { - ctx := context.Background() - accountID := utils.GenerateAccountID() - - for { - var user models.User - err := h.DB.Collection(database.UsersCollection).FindOne(ctx, bson.M{"account_id": accountID}).Decode(&user) - if err != nil { - break - } - accountID = utils.GenerateAccountID() - } - - _, err := h.DB.Collection(database.UsersCollection).InsertOne(ctx, models.User{ - AccountID: accountID, - CreatedAt: time.Now(), - }) - - if err != nil { - respondWithError(w, http.StatusInternalServerError, "Failed to create account") - return - } - - http.SetCookie(w, &http.Cookie{ - Name: "account_id", - Value: accountID, - Path: "/", - MaxAge: 31536000, // 1 year - HttpOnly: true, - Secure: r.TLS != nil, - SameSite: http.SameSiteLaxMode, - }) - - respondWithJSON(w, http.StatusOK, map[string]string{"account_id": accountID}) -} - -func (h *Handler) Login(w http.ResponseWriter, r *http.Request) { - var req LoginRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - respondWithError(w, http.StatusBadRequest, "Invalid request") - return - } - - ctx := context.Background() - var user models.User - err := h.DB.Collection(database.UsersCollection).FindOne(ctx, bson.M{"account_id": req.AccountID}).Decode(&user) - if err != nil { - respondWithError(w, http.StatusUnauthorized, "Invalid account ID") - return - } - - http.SetCookie(w, &http.Cookie{ - Name: "account_id", - Value: req.AccountID, - Path: "/", - MaxAge: 31536000, // 1 year - HttpOnly: true, - Secure: r.TLS != nil, - SameSite: http.SameSiteLaxMode, - }) - - respondWithJSON(w, http.StatusOK, map[string]bool{"success": true}) -} - -func (h *Handler) Logout(w http.ResponseWriter, r *http.Request) { - http.SetCookie(w, &http.Cookie{ - Name: "account_id", - Value: "", - Path: "/", - MaxAge: -1, - HttpOnly: true, - Secure: r.TLS != nil, - SameSite: http.SameSiteLaxMode, - }) - - respondWithJSON(w, http.StatusOK, map[string]bool{"success": true}) -} \ No newline at end of file diff --git a/internal/api/handlers/handlers.go b/internal/api/handlers/handlers.go deleted file mode 100644 index caebab4..0000000 --- a/internal/api/handlers/handlers.go +++ /dev/null @@ -1,38 +0,0 @@ -package handlers - -import ( - "encoding/json" - "net/http" - - "github.com/marcus7i/ulinkshortener/internal/database" -) - -type Handler struct { - DB *database.MongoDB -} - -func NewHandler(db *database.MongoDB) *Handler { - return &Handler{ - DB: db, - } -} - -func respondWithError(w http.ResponseWriter, code int, message string) { - respondWithJSON(w, code, map[string]string{"error": message}) -} - -func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) { - response, _ := json.Marshal(payload) - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(code) - w.Write(response) -} - -func getAccountIDFromCookie(r *http.Request) string { - cookie, err := r.Cookie("account_id") - if err != nil { - return "" - } - return cookie.Value -} \ No newline at end of file diff --git a/internal/api/handlers/url.go b/internal/api/handlers/url.go deleted file mode 100644 index b5ffb6e..0000000 --- a/internal/api/handlers/url.go +++ /dev/null @@ -1,132 +0,0 @@ -package handlers - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "time" - - "github.com/gorilla/mux" - "github.com/marcus7i/ulinkshortener/internal/database" - "github.com/marcus7i/ulinkshortener/internal/models" - "github.com/marcus7i/ulinkshortener/internal/utils" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" -) - -type CreateLinkRequest struct { - AccountID string `json:"account_id"` - URL string `json:"url"` -} - -type CreateLinkResponse struct { - ShortURL string `json:"short_url"` -} - -func (h *Handler) CreateLink(w http.ResponseWriter, r *http.Request) { - var req CreateLinkRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - respondWithError(w, http.StatusBadRequest, "Invalid request") - return - } - - ctx := context.Background() - user := h.DB.Collection(database.UsersCollection).FindOne(ctx, bson.M{"account_id": req.AccountID}) - if user.Err() != nil { - respondWithError(w, http.StatusUnauthorized, "Invalid account") - return - } - - if !utils.IsValidURL(req.URL) { - respondWithError(w, http.StatusBadRequest, "Invalid URL. Please provide a valid URL with scheme (e.g., http:// or https://)") - return - } - - shortID := utils.GenerateShortID() - - link := models.Link{ - ShortID: shortID, - TargetURL: req.URL, - AccountID: req.AccountID, - CreatedAt: time.Now(), - } - - _, err := h.DB.Collection(database.LinksCollection).InsertOne(ctx, link) - if err != nil { - respondWithError(w, http.StatusInternalServerError, "Failed to create link") - return - } - - respondWithJSON(w, http.StatusOK, CreateLinkResponse{ - ShortURL: fmt.Sprintf("/l/%s", shortID), - }) -} - -func (h *Handler) RedirectLink(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - shortID := vars["shortID"] - - ctx := context.Background() - var link models.Link - err := h.DB.Collection(database.LinksCollection).FindOne(ctx, bson.M{"short_id": shortID}).Decode(&link) - if err != nil { - if err == mongo.ErrNoDocuments { - http.Error(w, "Link not found", http.StatusNotFound) - } else { - http.Error(w, "Server error", http.StatusInternalServerError) - } - return - } - - clientInfo := utils.GetClientInfo(r) - clientInfo["link_id"] = shortID - clientInfo["account_id"] = link.AccountID - - _, err = h.DB.Collection(database.AnalyticsCollection).InsertOne(ctx, clientInfo) - if err != nil { - fmt.Printf("Failed to log analytics: %v\n", err) - } - - http.Redirect(w, r, link.TargetURL, http.StatusFound) -} - -func (h *Handler) DeleteLink(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - shortID := vars["shortID"] - accountID := getAccountIDFromCookie(r) - - if accountID == "" { - respondWithError(w, http.StatusUnauthorized, "Not logged in") - return - } - - ctx := context.Background() - var link models.Link - err := h.DB.Collection(database.LinksCollection).FindOne(ctx, bson.M{ - "short_id": shortID, - "account_id": accountID, - }).Decode(&link) - - if err != nil { - if err == mongo.ErrNoDocuments { - respondWithError(w, http.StatusNotFound, "Link not found or unauthorized") - } else { - respondWithError(w, http.StatusInternalServerError, "Server error") - } - return - } - - _, err = h.DB.Collection(database.LinksCollection).DeleteOne(ctx, bson.M{"short_id": shortID}) - if err != nil { - respondWithError(w, http.StatusInternalServerError, "Failed to delete link") - return - } - - _, err = h.DB.Collection(database.AnalyticsCollection).DeleteMany(ctx, bson.M{"link_id": shortID}) - if err != nil { - fmt.Printf("Failed to delete analytics: %v\n", err) - } - - respondWithJSON(w, http.StatusOK, map[string]bool{"success": true}) -} \ No newline at end of file diff --git a/internal/api/router.go b/internal/api/router.go deleted file mode 100644 index 542b6fd..0000000 --- a/internal/api/router.go +++ /dev/null @@ -1,39 +0,0 @@ -package api - -import ( - "net/http" - - "github.com/gorilla/mux" - "github.com/marcus7i/ulinkshortener/internal/api/handlers" - "github.com/marcus7i/ulinkshortener/internal/database" -) - -func SetupRouter(db *database.MongoDB) *mux.Router { - r := mux.NewRouter() - h := handlers.NewHandler(db) - - fs := http.FileServer(http.Dir("./web/static")) - r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", fs)) - - r.Use(func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/static/css/style.css" { - w.Header().Set("Content-Type", "text/css") - } else if r.URL.Path == "/static/js/script.js" { - w.Header().Set("Content-Type", "application/javascript") - } - next.ServeHTTP(w, r) - }) - }) - - r.HandleFunc("/register", h.Register).Methods("POST") - r.HandleFunc("/login", h.Login).Methods("POST") - r.HandleFunc("/logout", h.Logout).Methods("POST") - - r.HandleFunc("/create", h.CreateLink).Methods("POST") - r.HandleFunc("/l/{shortID}", h.RedirectLink).Methods("GET") - r.HandleFunc("/analytics/{accountID}", h.GetAnalytics).Methods("GET") - r.HandleFunc("/delete/{shortID}", h.DeleteLink).Methods("DELETE") - - return r -} \ No newline at end of file diff --git a/internal/config/config.go b/internal/config/config.go deleted file mode 100644 index d356cc4..0000000 --- a/internal/config/config.go +++ /dev/null @@ -1,26 +0,0 @@ -package config - -import ( - "os" - - "github.com/joho/godotenv" -) - -type Config struct { - MongoURI string - Port string -} - -func New() *Config { - godotenv.Load() - - port := os.Getenv("PORT") - if port == "" { - port = "5000" - } - - return &Config{ - MongoURI: os.Getenv("MONGO_URI"), - Port: port, - } -} \ No newline at end of file diff --git a/internal/database/mongodb.go b/internal/database/mongodb.go deleted file mode 100644 index 711b85f..0000000 --- a/internal/database/mongodb.go +++ /dev/null @@ -1,73 +0,0 @@ -package database - -import ( - "context" - "time" - "strings" - - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" -) - -type MongoDB struct { - Client *mongo.Client - DB *mongo.Database -} - -const ( - LinksCollection = "links" - AnalyticsCollection = "analytics" - UsersCollection = "users" - DefaultDBName = "uLinkShortener" -) - -func New(uri string) (*MongoDB, error) { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - client, err := mongo.Connect(ctx, options.Client().ApplyURI(uri)) - if err != nil { - return nil, err - } - - err = client.Ping(ctx, nil) - if err != nil { - return nil, err - } - - dbName := getDatabaseName(uri) - if dbName == "" { - dbName = DefaultDBName - } - db := client.Database(dbName) - - return &MongoDB{ - Client: client, - DB: db, - }, nil -} - -func getDatabaseName(uri string) string { - lastSlashIndex := strings.LastIndex(uri, "/") - if lastSlashIndex == -1 || lastSlashIndex == len(uri)-1 { - return "" - } - - dbName := uri[lastSlashIndex+1:] - - if queryIndex := strings.Index(dbName, "?"); queryIndex != -1 { - dbName = dbName[:queryIndex] - } - - return dbName -} - -func (m *MongoDB) Close() error { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - return m.Client.Disconnect(ctx) -} - -func (m *MongoDB) Collection(name string) *mongo.Collection { - return m.DB.Collection(name) -} \ No newline at end of file diff --git a/internal/models/analytics.go b/internal/models/analytics.go deleted file mode 100644 index bb5b54c..0000000 --- a/internal/models/analytics.go +++ /dev/null @@ -1,46 +0,0 @@ -package models - -import ( - "time" -) - -type Analytics struct { - LinkID string `json:"link_id" bson:"link_id"` - AccountID string `json:"account_id" bson:"account_id"` - IP string `json:"ip" bson:"ip"` - UserAgent string `json:"user_agent" bson:"user_agent"` - Platform string `json:"platform" bson:"platform"` - Browser string `json:"browser" bson:"browser"` - Version string `json:"version" bson:"version"` - Language string `json:"language" bson:"language"` - Referrer string `json:"referrer" bson:"referrer"` - Timestamp time.Time `json:"timestamp" bson:"timestamp"` - RemotePort string `json:"remote_port" bson:"remote_port"` - Accept string `json:"accept" bson:"accept"` - AcceptLanguage string `json:"accept_language" bson:"accept_language"` - AcceptEncoding string `json:"accept_encoding" bson:"accept_encoding"` - ScreenSize string `json:"screen_size" bson:"screen_size"` - WindowSize string `json:"window_size" bson:"window_size"` - Country string `json:"country" bson:"country"` - ISP string `json:"isp" bson:"isp"` - IPVersion string `json:"ip_version" bson:"ip_version"` -} - -type Stats struct { - TotalLinks int64 `json:"total_links"` - TotalClicks int64 `json:"total_clicks"` - ChartData ChartData `json:"chart_data"` - LoggedIn bool `json:"logged_in"` -} - -type ChartData struct { - IPVersions []StatItem `json:"ip_versions"` - OSStats []StatItem `json:"os_stats"` - CountryStats []StatItem `json:"country_stats"` - ISPStats []StatItem `json:"isp_stats"` -} - -type StatItem struct { - ID string `json:"_id" bson:"_id"` - Count int `json:"count" bson:"count"` -} diff --git a/internal/models/link.go b/internal/models/link.go deleted file mode 100644 index 84c6de4..0000000 --- a/internal/models/link.go +++ /dev/null @@ -1,12 +0,0 @@ -package models - -import ( - "time" -) - -type Link struct { - ShortID string `json:"short_id" bson:"short_id"` - TargetURL string `json:"target_url" bson:"target_url"` - AccountID string `json:"account_id" bson:"account_id"` - CreatedAt time.Time `json:"created_at" bson:"created_at"` -} diff --git a/internal/models/user.go b/internal/models/user.go deleted file mode 100644 index 6efb3f2..0000000 --- a/internal/models/user.go +++ /dev/null @@ -1,10 +0,0 @@ -package models - -import ( - "time" -) - -type User struct { - AccountID string `json:"account_id" bson:"account_id"` - CreatedAt time.Time `json:"created_at" bson:"created_at"` -} diff --git a/internal/utils/clientinfo.go b/internal/utils/clientinfo.go deleted file mode 100644 index 0151745..0000000 --- a/internal/utils/clientinfo.go +++ /dev/null @@ -1,83 +0,0 @@ -package utils - -import ( - "net/http" - "strings" - "time" - - "github.com/mssola/user_agent" -) - -func GetClientInfo(r *http.Request) map[string]interface{} { - ua := user_agent.New(r.UserAgent()) - - ipAddress := r.Header.Get("CF-Connecting-IP") - if ipAddress == "" { - ipAddress = r.Header.Get("X-Real-IP") - } - if ipAddress == "" { - forwardedFor := r.Header.Get("X-Forwarded-For") - if forwardedFor != "" { - ips := strings.Split(forwardedFor, ",") - ipAddress = strings.TrimSpace(ips[0]) - } - } - if ipAddress == "" { - ipAddress = r.RemoteAddr - if colonIndex := strings.LastIndex(ipAddress, ":"); colonIndex != -1 { - ipAddress = ipAddress[:colonIndex] - } - } - if ipAddress == "" { - ipAddress = "Unknown" - } - - browser, version := ua.Browser() - - platform := r.Header.Get("sec-ch-ua-platform") - if platform == "" { - platform = ua.OS() - if platform == "" { - platform = "Unknown" - } - } - - language := r.Header.Get("Accept-Language") - if language != "" && strings.Contains(language, ",") { - language = strings.Split(language, ",")[0] - } else if language == "" { - language = "Unknown" - } - - ipVersion := "IPv4" - if strings.Contains(ipAddress, ":") { - ipVersion = "IPv6" - } - - return map[string]interface{}{ - "ip": ipAddress, - "user_agent": r.UserAgent(), - "platform": platform, - "browser": browser, - "version": version, - "language": language, - "referrer": valueOrDefault(r.Referer(), "Direct"), - "timestamp": time.Now(), - "remote_port": valueOrDefault(r.Header.Get("X-Forwarded-Port"), "Unknown"), - "accept": valueOrDefault(r.Header.Get("Accept"), "Unknown"), - "accept_language": valueOrDefault(r.Header.Get("Accept-Language"), "Unknown"), - "accept_encoding": valueOrDefault(r.Header.Get("Accept-Encoding"), "Unknown"), - "screen_size": valueOrDefault(r.Header.Get("Sec-CH-UA-Platform-Screen"), "Unknown"), - "window_size": valueOrDefault(r.Header.Get("Viewport-Width"), "Unknown"), - "country": valueOrDefault(r.Header.Get("CF-IPCountry"), "Unknown"), - "isp": valueOrDefault(r.Header.Get("X-ISP"), "Unknown"), - "ip_version": ipVersion, - } -} - -func valueOrDefault(value, defaultValue string) string { - if value == "" { - return defaultValue - } - return value -} \ No newline at end of file diff --git a/internal/utils/shortid.go b/internal/utils/shortid.go deleted file mode 100644 index 9d85620..0000000 --- a/internal/utils/shortid.go +++ /dev/null @@ -1,32 +0,0 @@ -package utils - -import ( - "math/rand" - "strings" - "time" -) - -const ( - letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - idLength = 8 - accountLength = 8 -) - -var seededRand = rand.New(rand.NewSource(time.Now().UnixNano())) - -func GenerateShortID() string { - return randomString(idLength, letterBytes) -} - -func GenerateAccountID() string { - return randomString(accountLength, "0123456789") -} - -func randomString(length int, charset string) string { - b := strings.Builder{} - b.Grow(length) - for i := 0; i < length; i++ { - b.WriteByte(charset[seededRand.Intn(len(charset))]) - } - return b.String() -} diff --git a/internal/utils/url.go b/internal/utils/url.go deleted file mode 100644 index 4dfe01b..0000000 --- a/internal/utils/url.go +++ /dev/null @@ -1,19 +0,0 @@ -package utils - -import ( - "net/url" - "strings" -) - -func IsValidURL(urlStr string) bool { - if strings.TrimSpace(urlStr) == "" { - return false - } - - parsedURL, err := url.Parse(urlStr) - if err != nil { - return false - } - - return parsedURL.Scheme != "" && parsedURL.Host != "" -} \ No newline at end of file diff --git a/next.config.ts b/next.config.ts new file mode 100644 index 0000000..fb5d802 --- /dev/null +++ b/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + allowedDevOrigins: ['localhost', '*.marcus7i.net'], +}; + +export default nextConfig; diff --git a/package.json b/package.json new file mode 100644 index 0000000..9b0c9b6 --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "ulinkshortener", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "chart.js": "^4.5.0", + "mongodb": "^6.17.0", + "next": "15.3.3", + "next-auth": "^4.24.11", + "react": "^19.0.0", + "react-chartjs-2": "^5.3.0", + "react-dom": "^19.0.0", + "ua-parser-js": "^2.0.3", + "winston": "^3.17.0" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "@types/uuid": "^10.0.0", + "eslint": "^9", + "eslint-config-next": "15.3.3", + "tailwindcss": "^4", + "typescript": "^5" + } +} diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..c7bcb4b --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,5 @@ +const config = { + plugins: ["@tailwindcss/postcss"], +}; + +export default config; diff --git a/src/app/Home.module.css b/src/app/Home.module.css new file mode 100644 index 0000000..0fa9437 --- /dev/null +++ b/src/app/Home.module.css @@ -0,0 +1,168 @@ +/* Homepage specific styles */ +.default-container { + width: 100%; + max-width: 90%; + margin: 0 auto; + padding: 0 1.5rem; +} +@media (min-width: 768px) { + .default-container { + max-width: 80%; + } +} +@media (min-width: 1200px) { + .default-container { + max-width: 60%; + } +} + +/* Hero section */ +.hero-section { + min-height: 25vh; + display: flex; + flex-direction: column; + justify-content: center; + padding: 3rem 0 2rem; +} + +.hero-title { + font-size: 3.5rem; + font-weight: 700; + margin-bottom: 1.5rem; + background: linear-gradient(90deg, var(--accent), #9089fc); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.hero-description { + font-size: 1.25rem; + line-height: 1.7; + color: var(--text-secondary); + margin-bottom: 2rem; +} + +.hero-cta { + display: inline-block; + background-color: var(--accent); + color: white; + padding: 0.75rem 1.5rem; + border-radius: 0.375rem; + font-weight: 500; + text-decoration: none; + transition: background-color 0.2s; +} + +.hero-cta:hover { + background-color: var(--accent-hover); +} + +/* Stats section */ +.stats-summary { + margin-bottom: 2rem; + padding: 0 1rem; +} + +.stats-container { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 2rem; + margin: 0 auto; +} + +.stats-card { + background-color: var(--card-bg); + border-radius: 0.5rem; + padding: 1.5rem; + box-shadow: var(--card-shadow); + border: 1px solid var(--border-color); + transition: transform 0.2s, box-shadow 0.2s; + text-align: center; +} + +.stats-card:hover { + transform: translateY(-5px); + box-shadow: 0 10px 15px rgba(0, 0, 0, 0.2); +} + +.stats-card h3 { + font-size: 1rem; + margin-top: 0; + margin-bottom: 0.5rem; + color: var(--text-secondary); +} + +.stats-number { + font-size: 2.5rem; + font-weight: 700; + margin: 0; + color: var(--primary-color); +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .stats-number { + font-size: 2rem; + } +} + +/* Graphs section */ +.graphs-section { + padding: 2rem 0 4rem; +} + +.graphs-title { + font-size: 2rem; + margin-bottom: 2rem; + text-align: center; +} + +.graphs-container { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 2rem; +} + +.graph-card { + background-color: var(--card-bg); + border-radius: 0.5rem; + padding: 1.5rem; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + border: 1px solid var(--border-color); + transition: transform 0.2s, box-shadow 0.2s; + height: 20rem; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.graph-card:hover { + transform: translateY(-5px); + box-shadow: 0 10px 15px rgba(0, 0, 0, 0.2); +} + +.graph-title { + font-size: 1.25rem; + margin-bottom: 1rem; + color: var(--text-primary); +} + +.graph-content { + flex: 1; + position: relative; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .graphs-container { + grid-template-columns: 1fr; + } + + .hero-title { + font-size: 2.5rem; + } + + .default-container { + max-width: max-content; + } +} \ No newline at end of file diff --git a/src/app/admin/AdminDashboard.module.css b/src/app/admin/AdminDashboard.module.css new file mode 100644 index 0000000..2c57fbd --- /dev/null +++ b/src/app/admin/AdminDashboard.module.css @@ -0,0 +1,228 @@ +.adminContainer { + max-width: 75rem; + width: 100%; + margin: 0 auto; + padding: 2rem 1rem; +} + +.adminHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; +} + +.adminTitle { + font-size: 2rem; + color: var(--text-primary); + margin: 0; +} + +.actionButtons { + display: flex; + gap: 1rem; +} + +.dashboardButton, .statsButton { + padding: 0.5rem 1rem; + border-radius: 4px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + border: none; +} + +.dashboardButton { + background-color: var(--accent); + color: white; +} + +.statsButton { + background-color: var(--success); + color: white; +} + +.dashboardButton:hover, .statsButton:hover { + opacity: 0.9; + transform: translateY(-2px); +} + +.statsButton:disabled { + background-color: var(--disabled-bg); + color: var(--disabled-text); + cursor: not-allowed; + transform: none; +} + +.usersSection { + background-color: var(--card-bg); + border-radius: 8px; + padding: 1.5rem; + margin-bottom: 2rem; + border: 1px solid var(--border-color); +} + +.sectionHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; +} + +.searchContainer { + position: relative; + display: flex; + align-items: center; +} + +.searchInput { + padding: 0.5rem 0.75rem; + padding-right: 2rem; + border-radius: 4px; + border: 1px solid var(--border-color); + background-color: var(--input-bg); + color: var(--text-primary); + font-size: 0.9rem; + width: 250px; +} + +.clearSearchButton { + position: absolute; + right: 0.5rem; + background: none; + border: none; + color: var(--text-secondary); + cursor: pointer; + font-size: 0.8rem; +} + +.tableContainer { + overflow-x: auto; +} + +.usersTable { + width: 100%; + border-collapse: collapse; + margin-top: 0.5rem; +} + +.usersTable th, +.usersTable td { + padding: 0.75rem; + text-align: left; + border-bottom: 1px solid var(--border-color); +} + +.usersTable th { + background-color: var(--table-header-bg); + color: var(--text-primary); + font-weight: 500; +} + +.usersTable tr:last-child td { + border-bottom: none; +} + +.usersTable tr:hover td { + background-color: var(--hover-bg); +} + +.actions { + display: flex; + gap: 0.5rem; +} + +.viewButton, .deleteButton { + padding: 0.4rem 0.6rem; + border-radius: 4px; + font-size: 0.8rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + border: none; +} + +.viewButton { + background-color: var(--accent); + color: white; +} + +.deleteButton { + background-color: var(--error); + color: white; +} + +.viewButton:hover, .deleteButton:hover { + opacity: 0.9; +} + +.noResults { + text-align: center; + padding: 2rem; + color: var(--text-secondary); + font-style: italic; +} + +.loading { + display: flex; + justify-content: center; + align-items: center; + padding: 2rem; + color: var(--text-secondary); + font-size: 1rem; +} + +.makeAdminButton, .removeAdminButton { + padding: 0.4rem 0.6rem; + border-radius: 4px; + font-size: 0.8rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + border: none; + margin-right: 0.5rem; +} + +.makeAdminButton { + background-color: var(--success); + color: white; +} + +.removeAdminButton { + background-color: var(--warning); + color: white; +} + +.makeAdminButton:hover, .removeAdminButton:hover { + opacity: 0.9; +} + +@media (max-width: 768px) { + .adminHeader { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + } + + .actionButtons { + width: 100%; + } + + .sectionHeader { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + } + + .searchContainer { + width: 100%; + } + + .searchInput { + width: 100%; + } + + .actions { + flex-direction: column; + } +} \ No newline at end of file diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx new file mode 100644 index 0000000..8b48db9 --- /dev/null +++ b/src/app/admin/page.tsx @@ -0,0 +1,278 @@ +"use client"; + +import { useState, useEffect } from 'react'; +import { useSession } from 'next-auth/react'; +import { useRouter } from 'next/navigation'; +import Link from 'next/link'; +import ConfirmModal from '@/components/ui/ConfirmModal'; +import { useToast } from '@/contexts/ToastContext'; +import styles from './AdminDashboard.module.css'; +import type { User } from '@/types/user'; + +export default function AdminDashboard() { + const { data: session, status } = useSession(); + const router = useRouter(); + const { showToast } = useToast(); + + const [users, setUsers] = useState([]); + const [filteredUsers, setFilteredUsers] = useState([]); + const [searchTerm, setSearchTerm] = useState(''); + const [loading, setLoading] = useState(true); + const [userToDelete, setUserToDelete] = useState(null); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [isRecreatingStats, setIsRecreatingStats] = useState(false); + + useEffect(() => { + if (status === "unauthenticated" || (status === "authenticated" && !session?.user?.isAdmin)) { + router.push('/dashboard'); + } + }, [status, session, router]); + + useEffect(() => { + if (session?.user?.isAdmin) { + fetchUsers(); + } + }, [session]); + + useEffect(() => { + if (!searchTerm.trim()) { + setFilteredUsers(users); + return; + } + + const term = searchTerm.toLowerCase(); + const filtered = users.filter(user => + user.account_id.toLowerCase().includes(term) || + user.created_at.toLocaleString().toLowerCase().includes(term) || + (user.is_admin ? "admin" : "user").includes(term) + ); + + setFilteredUsers(filtered); + }, [searchTerm, users]); + + const fetchUsers = async () => { + try { + setLoading(true); + const response = await fetch('/api/admin/users'); + + if (!response.ok) { + throw new Error('Failed to fetch users'); + } + + const data = await response.json(); + + if (data.success) { + const processedUsers = data.users.map((user: User) => ({ + account_id: user.account_id, + is_admin: user.is_admin, + created_at: new Date(user.created_at) + })); + + setUsers(processedUsers); + setFilteredUsers(processedUsers); + } else { + showToast(data.message || 'Failed to load users', 'error'); + } + } catch (error) { + console.error('Error fetching users:', error); + showToast('Failed to load users', 'error'); + } finally { + setLoading(false); + } + }; + + const handleDeleteUser = async () => { + if (!userToDelete) return; + + try { + const response = await fetch('/api/admin/users', { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ account_id: userToDelete }), + }); + + const data = await response.json(); + + if (response.ok && data.success) { + showToast('User deleted successfully', 'success'); + setUsers(prevUsers => prevUsers.filter(user => user.account_id !== userToDelete)); + } else { + showToast(data.message || 'Failed to delete user', 'error'); + } + } catch (error) { + console.error('Error deleting user:', error); + showToast('Failed to delete user', 'error'); + } finally { + setUserToDelete(null); + setIsDeleteModalOpen(false); + } + }; + + const confirmDeleteUser = (accountId: string) => { + setUserToDelete(accountId); + setIsDeleteModalOpen(true); + }; + + const handleRecreateStatistics = async () => { + try { + setIsRecreatingStats(true); + const response = await fetch('/api/admin/statistics/rebuild', { + method: 'POST' + }); + + const data = await response.json(); + + if (response.ok && data.success) { + showToast('Statistics recreated successfully', 'success'); + } else { + showToast(data.message || 'Failed to recreate statistics', 'error'); + } + } catch (error) { + console.error('Error recreating statistics:', error); + showToast('Failed to recreate statistics', 'error'); + } finally { + setIsRecreatingStats(false); + } + }; + + const handleToggleAdminStatus = async (accountId: string) => { + try { + const response = await fetch(`/api/admin/users/${accountId}/admin`, { + method: 'POST', + }); + + const data = await response.json(); + + if (response.ok && data.success) { + showToast(data.message, 'success'); + setUsers(prevUsers => + prevUsers.map(user => + user.account_id === accountId + ? { ...user, is_admin: data.is_admin } + : user + ) + ); + } else { + showToast(data.message || 'Failed to toggle admin status', 'error'); + } + } catch (error) { + console.error('Error toggling admin status:', error); + showToast('Failed to toggle admin status', 'error'); + } + }; + + if (status === "loading" || loading) { + return
Loading...
; + } + + if (status === "unauthenticated" || (status === "authenticated" && !session?.user?.isAdmin)) { + return null; + } + + return ( +
+
+

Admin Dashboard

+
+ + + + +
+
+ +
+
+

Manage Users

+
+ setSearchTerm(e.target.value)} + className={styles.searchInput} + /> + {searchTerm && ( + + )} +
+
+ + {filteredUsers.length === 0 ? ( +
+ {searchTerm ? 'No users match your search' : 'No users available'} +
+ ) : ( +
+ + + + + + + + + + + {filteredUsers.map((user) => ( + + + + + + + ))} + +
Account IDCreatedRoleActions
{user.account_id}{user.created_at.toLocaleString()}{user.is_admin ? 'Admin' : 'User'} + + + + + {user.account_id !== session?.user?.accountId && ( + + )} + + {!user.is_admin && ( + + )} +
+
+ )} +
+ + setIsDeleteModalOpen(false)} + /> +
+ ); +} \ No newline at end of file diff --git a/src/app/admin/user/[accountId]/UserDetail.module.css b/src/app/admin/user/[accountId]/UserDetail.module.css new file mode 100644 index 0000000..ae9d355 --- /dev/null +++ b/src/app/admin/user/[accountId]/UserDetail.module.css @@ -0,0 +1,174 @@ +.container { + max-width: 75rem; + width: 100%; + margin: 0 auto; + padding: 2rem 1rem; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; +} + +.title { + font-size: 2rem; + color: var(--text-primary); + margin: 0; +} + +.actionButtons { + display: flex; + gap: 1rem; +} + +.backButton, .deleteButton { + padding: 0.5rem 1rem; + border-radius: 4px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + border: none; +} + +.backButton { + background-color: var(--accent); + color: white; +} + +.deleteButton { + background-color: var(--error); + color: white; +} + +.backButton:hover, .deleteButton:hover { + opacity: 0.9; + transform: translateY(-2px); +} + +.userInfo, .linksSection, .sessionsSection { + background-color: var(--card-bg); + border-radius: 8px; + padding: 1.5rem; + margin-bottom: 2rem; + border: 1px solid var(--border-color); +} + +.infoCard { + background-color: var(--card-secondary-bg); + border-radius: 6px; + padding: 1rem; + margin-top: 1rem; +} + +.infoRow { + display: flex; + margin-bottom: 0.5rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--border-color); +} + +.infoRow:last-child { + margin-bottom: 0; + padding-bottom: 0; + border-bottom: none; +} + +.infoLabel { + width: 120px; + font-weight: 500; + color: var(--text-secondary); +} + +.infoValue { + flex: 1; + color: var(--text-primary); +} + +.noItems { + text-align: center; + padding: 2rem; + color: var(--text-secondary); + font-style: italic; +} + +.tableContainer { + overflow-x: auto; +} + +.sessionsTable { + width: 100%; + border-collapse: collapse; + margin-top: 0.5rem; +} + +.sessionsTable th, +.sessionsTable td { + padding: 0.75rem; + text-align: left; + border-bottom: 1px solid var(--border-color); +} + +.sessionsTable th { + background-color: var(--table-header-bg); + color: var(--text-primary); + font-weight: 500; +} + +.sessionsTable tr:last-child td { + border-bottom: none; +} + +.sessionsTable tr:hover td { + background-color: var(--hover-bg); +} + +.revokeButton { + padding: 0.4rem 0.6rem; + border-radius: 4px; + font-size: 0.8rem; + background-color: var(--error); + color: white; + border: none; + cursor: pointer; + transition: all 0.2s ease; +} + +.revokeButton:hover { + opacity: 0.9; +} + +.revokeButton:disabled { + background-color: var(--disabled-bg); + color: var(--disabled-text); + cursor: not-allowed; +} + +.loading { + display: flex; + justify-content: center; + align-items: center; + padding: 2rem; + color: var(--text-secondary); + font-size: 1rem; +} + +.error { + text-align: center; + padding: 2rem; + color: var(--error); + font-size: 1rem; +} + +@media (max-width: 768px) { + .header { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + } + + .actionButtons { + width: 100%; + } +} \ No newline at end of file diff --git a/src/app/admin/user/[accountId]/links/[shortId]/AdminLinkDetail.module.css b/src/app/admin/user/[accountId]/links/[shortId]/AdminLinkDetail.module.css new file mode 100644 index 0000000..1c3eb34 --- /dev/null +++ b/src/app/admin/user/[accountId]/links/[shortId]/AdminLinkDetail.module.css @@ -0,0 +1,344 @@ +.container { + width: 100%; + max-width: 1200px; + margin: 0 auto; + padding: 2rem 1rem; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; +} + +.titleSection { + display: flex; + flex-direction: column; +} + +.title { + font-size: 2rem; + margin: 0; + color: var(--text-primary); +} + +.breadcrumbs { + font-size: 0.9rem; + color: var(--text-secondary); + margin-top: 0.5rem; +} + +.breadcrumbLink { + color: var(--accent); + text-decoration: none; + margin: 0 0.25rem; +} + +.breadcrumbLink:hover { + text-decoration: underline; +} + +.breadcrumbCurrent { + color: var(--text-primary); + margin: 0 0.25rem; +} + +.backLink { + padding: 0.5rem 1rem; + border-radius: 4px; + background-color: var(--accent); + color: var(--text-primary); + text-decoration: none; + font-weight: 500; + transition: all 0.2s ease; +} + +.backLink:hover { + opacity: 0.9; + transform: translateY(-2px); +} + +.userInfo { + background-color: var(--card-bg); + border-radius: 8px; + padding: 1rem 1.5rem; + margin-bottom: 1.5rem; + display: flex; + align-items: center; + border-left: 4px solid var(--accent); +} + +.label { + font-weight: 600; + color: var(--text-secondary); + margin-right: 0.5rem; +} + +.value { + color: var(--text-primary); + word-break: break-all; +} + +.linkInfo { + margin-bottom: 2rem; +} + +.linkCard { + background-color: var(--card-bg); + border-radius: 0.5rem; + padding: 1.5rem; + box-shadow: var(--card-shadow); +} + +.linkCard h2 { + margin-top: 0; + margin-bottom: 1.5rem; + color: var(--text-primary); + font-size: 1.5rem; +} + +.linkDetails { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.linkDetailItem { + display: flex; + flex-direction: column; +} + +.shortUrl { + color: var(--primary-color); + word-break: break-all; + text-decoration: none; +} + +.shortUrl:hover { + text-decoration: underline; +} + +.defaultButton { + background-color: transparent; + color: var(--primary-color); + border: 1px solid var(--primary-color); + border-radius: 0.25rem; + padding: 0.25rem 0.5rem; + font-size: 0.75rem; + cursor: pointer; +} + +.defaultButton:hover { + background-color: var(--primary-color); + color: white; +} + +.targetUrlSection { + margin-top: 0.5rem; +} + +.targetUrlHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; +} + +.editButton { + background-color: transparent; + color: var(--primary-color); + border: 1px solid var(--primary-color); + border-radius: 0.25rem; + padding: 0.25rem 0.5rem; + font-size: 0.75rem; + cursor: pointer; +} + +.editButton:hover { + background-color: var(--primary-color); + color: white; +} + +.targetUrl { + color: var(--text-primary); + word-break: break-all; + text-decoration: none; +} + +.targetUrl:hover { + text-decoration: underline; + color: var(--primary-color); +} + +.editForm { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.urlInput { + width: 100%; + padding: 0.5rem; + border: 1px solid var(--border-color); + border-radius: 0.25rem; + background-color: var(--input-bg); + color: var(--text-primary); +} + +.editActions { + display: flex; + gap: 0.5rem; + justify-content: flex-end; +} + +.cancelButton { + background-color: transparent; + color: var(--text-secondary); + border: 1px solid var(--border-color); + border-radius: 0.25rem; + padding: 0.5rem 0.75rem; + cursor: pointer; +} + +.cancelButton:hover { + background-color: var(--border-color); +} + +.saveButton { + background-color: var(--primary-color); + color: white; + border: none; + border-radius: 0.25rem; + padding: 0.5rem 0.75rem; + cursor: pointer; +} + +.saveButton:hover { + background-color: var(--primary-dark); +} + +.analyticsSection { + margin-top: 2rem; +} + +.analyticsHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; + flex-wrap: wrap; + gap: 1rem; +} + +.analyticsHeader h2 { + margin: 0; + color: var(--text-primary); + font-size: 1.5rem; +} + +.totalClicks { + color: var(--text-secondary); + font-weight: 600; +} + +.deleteAllButton { + background-color: transparent; + color: var(--error); + border: 1px solid var(--error); + border-radius: 0.25rem; + padding: 0.25rem 0.5rem; + font-size: 1rem; + cursor: pointer; +} + +.deleteAllButton:hover { + background-color: var(--error); + color: white; +} + +.graphs { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 1rem; + margin-bottom: 2rem; +} + +.graphCard { + background-color: var(--card-bg); + border-radius: 0.5rem; + padding: 1rem; + box-shadow: var(--card-shadow); +} + +.graphCard h3 { + margin-top: 0; + margin-bottom: 1rem; + color: var(--text-primary); + font-size: 1rem; + text-align: center; +} + +.noAnalytics { + display: flex; + justify-content: center; + align-items: center; + background-color: var(--card-bg); + border-radius: 0.5rem; + padding: 2rem; + box-shadow: var(--card-shadow); +} + +.noAnalytics p { + color: var(--text-secondary); + font-size: 1rem; + text-align: center; +} + +.loadingContainer { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + min-height: 300px; + gap: 1rem; +} + +.loader { + border: 4px solid rgba(0, 0, 0, 0.1); + border-left-color: var(--primary-color); + border-radius: 50%; + width: 40px; + height: 40px; + animation: spin 1s linear infinite; +} + +.error { + text-align: center; + color: var(--error); + font-size: 1.2rem; + margin-top: 2rem; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +@media (max-width: 768px) { + .header { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + } + + .analyticsHeader { + flex-direction: column; + align-items: flex-start; + } + + .graphs { + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/src/app/admin/user/[accountId]/links/[shortId]/page.tsx b/src/app/admin/user/[accountId]/links/[shortId]/page.tsx new file mode 100644 index 0000000..d70ffe0 --- /dev/null +++ b/src/app/admin/user/[accountId]/links/[shortId]/page.tsx @@ -0,0 +1,574 @@ +"use client"; + +import { useState, useEffect, useRef } from 'react'; +import { useSession } from 'next-auth/react'; +import { useParams, useRouter } from 'next/navigation'; +import Link from 'next/link'; +import { useToast } from '@/contexts/ToastContext'; +import ConfirmModal from '@/components/ui/ConfirmModal'; +import AnalyticsTable from '@/components/ui/dashboard/link/AnalyticsTable'; +import Graph from '@/components/ui/Graph'; +import styles from './AdminLinkDetail.module.css'; +import type { Link as LinkType } from '@/types/link'; +import type { Analytics } from '@/types/analytics'; +import type { StatItem } from '@/types/statistics'; + +export default function AdminLinkDetailPage() { + const { data: session, status } = useSession(); + const params = useParams(); + const router = useRouter(); + const { showToast } = useToast(); + const accountId = params.accountId as string; + const shortId = params.shortId as string; + + const [link, setLink] = useState(null); + const [targetUrl, setTargetUrl] = useState(''); + const [isEditing, setIsEditing] = useState(false); + const inputRef = useRef(null); + const [isLoading, setIsLoading] = useState(true); + const [allAnalytics, setAllAnalytics] = useState([]); + const [analytics, setAnalytics] = useState([]); + const [totalAnalytics, setTotalAnalytics] = useState(0); + const [page, setPage] = useState(1); + const [limit] = useState(15); + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [showDeleteAllModal, setShowDeleteAllModal] = useState(false); + const [analyticsToDelete, setAnalyticsToDelete] = useState(''); + const [isLoadingStats, setIsLoadingStats] = useState(true); + const isRedirecting = useRef(false); + + // Stats data + const [browserStats, setBrowserStats] = useState([]); + const [osStats, setOsStats] = useState([]); + const [countryStats, setCountryStats] = useState([]); + const [ipVersionStats, setIpVersionStats] = useState([]); + + useEffect(() => { + if (status === "unauthenticated" || (status === "authenticated" && !session?.user?.isAdmin)) { + router.push('/dashboard'); + } + }, [status, session, router]); + + function isValidUrl(urlStr: string) : boolean { + if(urlStr.trim() === "") { + return false; + } + + try { + const parsedUrl = new URL(urlStr); + return parsedUrl.protocol !== "" && parsedUrl.hostname !== ""; + } catch { + return false; + } + } + + useEffect(() => { + if (isEditing && inputRef.current) { + setTimeout(() => { + inputRef.current?.focus(); + }, 10); + } + }, [isEditing]); + + useEffect(() => { + if (isRedirecting.current || status !== "authenticated" || !session?.user?.isAdmin) return; + + async function fetchLinkData() { + try { + setIsLoading(true); + const response = await fetch(`/api/admin/users/${accountId}/links/${shortId}`); + if (!response.ok) { + showToast('Failed to load link details', 'error'); + isRedirecting.current = true; + router.push(`/admin/user/${accountId}`); + return; + } + + const data = await response.json(); + if (data.success && data.link) { + setLink(data.link); + setTargetUrl(data.link.target_url); + } else { + showToast(data.message || 'Link not found', 'error'); + isRedirecting.current = true; + router.push(`/admin/user/${accountId}`); + } + } catch { + showToast('An error occurred while loading link details', 'error'); + isRedirecting.current = true; + router.push(`/admin/user/${accountId}`); + } finally { + setIsLoading(false); + } + } + + fetchLinkData(); + }, [shortId, accountId, router, showToast, session, status]); + + useEffect(() => { + if (!link || status !== "authenticated" || !session?.user?.isAdmin) return; + + async function fetchAllAnalytics() { + try { + const response = await fetch(`/api/admin/users/${accountId}/links/${shortId}/analytics?all=true`); + if (!response.ok) { + showToast('Failed to load complete analytics data', 'error'); + return; + } + + const data = await response.json(); + if (data.success) { + setAllAnalytics(data.analytics); + setTotalAnalytics(data.pagination.total); + } + } catch { + showToast('An error occurred while loading complete analytics data', 'error'); + } + } + + fetchAllAnalytics(); + }, [link, shortId, accountId, showToast, session, status]); + + useEffect(() => { + if (!link || status !== "authenticated" || !session?.user?.isAdmin) return; + + async function fetchPaginatedAnalytics() { + try { + const response = await fetch(`/api/admin/users/${accountId}/links/${shortId}/analytics?page=${page}&limit=${limit}`); + if (!response.ok) { + showToast('Failed to load analytics page', 'error'); + return; + } + + const data = await response.json(); + if (data.success) { + setAnalytics(data.analytics); + } + } catch { + showToast('An error occurred while loading analytics page', 'error'); + } + } + + fetchPaginatedAnalytics(); + }, [link, shortId, accountId, page, limit, showToast, session, status]); + + useEffect(() => { + if (!link || allAnalytics.length === 0) return; + + async function generateStats() { + setIsLoadingStats(true); + try { + // Browser stats + const browsers = allAnalytics.reduce((acc: Record, item) => { + const browser = item.browser || 'Unknown'; + acc[browser] = (acc[browser] || 0) + 1; + return acc; + }, {}); + + // OS stats + const oses = allAnalytics.reduce((acc: Record, item) => { + const os = item.platform || 'Unknown'; + acc[os] = (acc[os] || 0) + 1; + return acc; + }, {}); + + // Country stats + const countries = allAnalytics.reduce((acc: Record, item) => { + const country = item.country || 'Unknown'; + acc[country] = (acc[country] || 0) + 1; + return acc; + }, {}); + + // IP version stats + const ipVersions = allAnalytics.reduce((acc: Record, item) => { + const ipVersion = item.ip_version || 'Unknown'; + acc[ipVersion] = (acc[ipVersion] || 0) + 1; + return acc; + }, {}); + + // Convert to StatItem[] and sort by count + setBrowserStats(Object.entries(browsers) + .map(([id, count]) => ({ id, count })) + .sort((a, b) => b.count - a.count)); + + setOsStats(Object.entries(oses) + .map(([id, count]) => ({ id, count })) + .sort((a, b) => b.count - a.count)); + + setCountryStats(Object.entries(countries) + .map(([id, count]) => ({ id, count })) + .sort((a, b) => b.count - a.count)); + + setIpVersionStats(Object.entries(ipVersions) + .map(([id, count]) => ({ id, count })) + .sort((a, b) => b.count - a.count)); + } catch { + showToast('An error occurred while processing analytics data', 'error'); + } finally { + setIsLoadingStats(false); + } + } + + generateStats(); + }, [allAnalytics, link, showToast]); + + const handlePageChange = (newPage: number) => { + setPage(newPage); + }; + + const handleEditLink = async () => { + if (!isValidUrl(targetUrl)) { + showToast('Please enter a valid URL', 'error'); + return; + } + + try { + const response = await fetch(`/api/admin/users/${accountId}/links/${shortId}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + target_url: targetUrl + }), + }); + + const data = await response.json(); + + if (data.success) { + showToast('Link updated successfully', 'success'); + setIsEditing(false); + if (link) { + setLink({ + ...link, + target_url: targetUrl, + last_modified: new Date() + }); + } + } else { + showToast(data.message || 'Failed to update link', 'error'); + } + } catch { + showToast('An error occurred while updating the link', 'error'); + } + }; + + const handleDeleteAnalytics = async () => { + try { + const response = await fetch(`/api/admin/users/${accountId}/links/${shortId}/analytics`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + analytics_id: analyticsToDelete + }), + }); + + const data = await response.json(); + + if (data.success) { + showToast('Analytics entry deleted successfully', 'success'); + + setAnalytics(analytics.filter(item => item._id?.toString() !== analyticsToDelete)); + + setTotalAnalytics(prev => prev - 1); + } else { + showToast(data.message || 'Failed to delete analytics entry', 'error'); + } + } catch { + showToast('An error occurred while deleting the analytics entry', 'error'); + } finally { + setShowDeleteModal(false); + setAnalyticsToDelete(''); + } + }; + + const handleDeleteAllAnalytics = async () => { + try { + const response = await fetch(`/api/admin/users/${accountId}/links/${shortId}/analytics`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + delete_all: true + }), + }); + + const data = await response.json(); + + if (data.success) { + showToast('All analytics entries deleted successfully', 'success'); + + setAnalytics([]); + setTotalAnalytics(0); + setBrowserStats([]); + setOsStats([]); + setCountryStats([]); + setIpVersionStats([]); + } else { + showToast(data.message || 'Failed to delete all analytics entries', 'error'); + } + } catch { + showToast('An error occurred while deleting all analytics entries', 'error'); + } finally { + setShowDeleteAllModal(false); + } + }; + + if (status === "loading" || isLoading) { + return ( +
+
+

Loading link details...

+
+ ); + } + + if (status === "unauthenticated" || (status === "authenticated" && !session?.user?.isAdmin)) { + return null; + } + + if (!link) { + return ( +
Link not found or you don't have permission to view it.
+ ); + } + + return ( +
+
+
+

Admin Link Management

+
+ Admin > + User > + Link {shortId} +
+
+ + Back to User + +
+ +
+ Managing link for User ID: + {accountId} +
+ +
+
+

Link Information

+
+
+ Short ID: + {shortId} +
+ +
+ + +
+ +
+ Created: + + {link.created_at instanceof Date + ? link.created_at.toLocaleString() + : new Date(link.created_at).toLocaleString()} + +
+ +
+ Last Modified: + + {link.last_modified instanceof Date + ? link.last_modified.toLocaleString() + : new Date(link.last_modified).toLocaleString()} + +
+ +
+
+ Target URL: + {!isEditing && ( + + )} +
+ + {isEditing ? ( +
{ + e.preventDefault(); + handleEditLink(); + }} + className={styles.editForm} + > + setTargetUrl(e.target.value)} + className={styles.urlInput} + placeholder="https://example.com" + /> +
+ + +
+
+ ) : ( + + {link.target_url} + + )} +
+
+
+
+ +
+
+

Analytics

+ + Total Clicks: {totalAnalytics} + + {totalAnalytics > 0 && ( + + )} +
+ + {totalAnalytics > 0 ? ( + <> +
+
+

Browsers

+ +
+ +
+

Operating Systems

+ +
+ +
+

Countries

+ +
+ +
+

IP Versions

+ +
+
+ + { + setAnalyticsToDelete(id); + setShowDeleteModal(true); + }} + /> + + ) : ( +
+

No clicks recorded yet for this link.

+
+ )} +
+ + {/* Confirm Delete Modal */} + { + setShowDeleteModal(false); + setAnalyticsToDelete(''); + }} + /> + + {/* Confirm Delete All Modal */} + setShowDeleteAllModal(false)} + /> +
+ ); +} \ No newline at end of file diff --git a/src/app/admin/user/[accountId]/page.tsx b/src/app/admin/user/[accountId]/page.tsx new file mode 100644 index 0000000..cb5b876 --- /dev/null +++ b/src/app/admin/user/[accountId]/page.tsx @@ -0,0 +1,241 @@ +"use client"; + +import { useState, useEffect } from 'react'; +import { useParams, useRouter } from 'next/navigation'; +import { useSession } from 'next-auth/react'; +import Link from 'next/link'; +import AdminLinkTable from '@/components/ui/admin/AdminLinkTable'; +import ConfirmModal from '@/components/ui/ConfirmModal'; +import { useToast } from '@/contexts/ToastContext'; +import styles from './UserDetail.module.css'; +import type { User } from '@/types/user'; +import type { SessionInfo } from '@/types/session'; + +export default function UserDetailPage() { + const params = useParams(); + const router = useRouter(); + const { data: session, status } = useSession(); + const { showToast } = useToast(); + const accountId = params.accountId as string; + + const [user, setUser] = useState(null); + const [links, setLinks] = useState([]); + const [sessions, setSessions] = useState([]); + const [loading, setLoading] = useState(true); + const [revoking, setRevoking] = useState(null); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + + useEffect(() => { + if (status === "unauthenticated" || (status === "authenticated" && !session?.user?.isAdmin)) { + router.push('/dashboard'); + } + }, [status, session, router]); + + useEffect(() => { + if (status === "authenticated" && session?.user?.isAdmin) { + fetchUserData(); + } + }, [status, session, accountId]); + + const fetchUserData = async () => { + try { + setLoading(true); + const userResponse = await fetch(`/api/admin/users/${accountId}`); + + if (!userResponse.ok) { + throw new Error('Failed to fetch user details'); + } + + const userData = await userResponse.json(); + if (userData.success) { + setUser(userData.user); + + const linksResponse = await fetch(`/api/admin/users/${accountId}/links`); + const linksData = await linksResponse.json(); + + if (linksResponse.ok && linksData.success) { + setLinks(linksData.links); + } + + const sessionsResponse = await fetch(`/api/admin/users/${accountId}/sessions`); + const sessionsData = await sessionsResponse.json(); + + if (sessionsResponse.ok && sessionsData.success) { + setSessions(sessionsData.sessions); + } + } else { + showToast(userData.message || 'Failed to load user details', 'error'); + router.push('/admin'); + } + } catch (error) { + console.error('Error fetching user data:', error); + showToast('Failed to load user details', 'error'); + router.push('/admin'); + } finally { + setLoading(false); + } + }; + + const handleRevokeSession = async (sessionId: string) => { + try { + setRevoking(sessionId); + + const response = await fetch(`/api/admin/users/${accountId}/sessions/revoke`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ sessionId, accountId }) + }); + + const data = await response.json(); + + if (response.ok && data.success) { + showToast('Session revoked successfully', 'success'); + setSessions(prevSessions => + prevSessions.filter(s => s.id !== sessionId) + ); + } else { + showToast(data.message || 'Failed to revoke session', 'error'); + } + } catch (error) { + console.error('Error revoking session:', error); + showToast('Failed to revoke session', 'error'); + } finally { + setRevoking(null); + } + }; + + const handleDeleteUser = async () => { + try { + const response = await fetch('/api/admin/users', { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ account_id: accountId }), + }); + + const data = await response.json(); + + if (response.ok && data.success) { + showToast('User deleted successfully', 'success'); + router.push('/admin'); + } else { + showToast(data.message || 'Failed to delete user', 'error'); + } + } catch (error) { + console.error('Error deleting user:', error); + showToast('Failed to delete user', 'error'); + } finally { + setIsDeleteModalOpen(false); + } + }; + + const formatDate = (dateString: Date) => { + const date = new Date(dateString); + return date.toLocaleString(); + }; + + if (status === "loading" || loading) { + return
Loading user details...
; + } + + if (!user) { + return
User not found
; + } + + return ( +
+
+

User Details

+
+ + + + {!user.is_admin && ( + + )} +
+
+ +
+

Account Information

+
+
+ Account ID: + {user.account_id} +
+
+ Created: + {formatDate(user.created_at)} +
+
+ Role: + {user.is_admin ? 'Admin' : 'User'} +
+
+
+ +
+

User Links

+ {links.length === 0 ? ( +
This user has not created any links yet.
+ ) : ( + + )} +
+ +
+

Active Sessions

+ {sessions.length === 0 ? ( +
This user has no active sessions.
+ ) : ( +
+ + + + + + + + + + + + {sessions.map((s) => ( + + + + + + + + ))} + +
Device & BrowserIP AddressLast ActiveCreatedActions
{s.userAgent.split(' ').slice(0, 3).join(' ')}{s.ipAddress}{formatDate(s.lastActive)}{formatDate(s.createdAt)} + +
+
+ )} +
+ + setIsDeleteModalOpen(false)} + /> +
+ ); +} \ No newline at end of file diff --git a/src/app/api/admin/statistics/rebuild/route.ts b/src/app/api/admin/statistics/rebuild/route.ts new file mode 100644 index 0000000..aba1e8c --- /dev/null +++ b/src/app/api/admin/statistics/rebuild/route.ts @@ -0,0 +1,31 @@ +import { NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; +import { updateStats } from '@/lib/statisticsdb'; +import logger from '@/lib/logger'; + +export async function POST() { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.isAdmin) { + return NextResponse.json({ + message: "Unauthorized", + success: false, + }, { status: 401 }); + } + + const result = await updateStats(); + + return NextResponse.json({ + message: result.status, + success: result.success, + }); + } catch (error) { + logger.error('Error rebuilding statistics:', { error }); + return NextResponse.json({ + message: "Failed to rebuild statistics", + success: false, + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/admin/users/[accountId]/admin/route.ts b/src/app/api/admin/users/[accountId]/admin/route.ts new file mode 100644 index 0000000..af3760d --- /dev/null +++ b/src/app/api/admin/users/[accountId]/admin/route.ts @@ -0,0 +1,67 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; +import { isUserAdmin, makeUserAdmin } from '@/lib/userdb'; +import { removeAllSessionsByAccountId } from '@/lib/sessiondb'; +import logger from '@/lib/logger'; + +export async function POST( + req: NextRequest, + { params }: { params: { accountId: string } } +) { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.isAdmin) { + return NextResponse.json({ + message: "Unauthorized", + success: false, + }, { status: 401 }); + } + + const { accountId } = params; + + if (!accountId) { + return NextResponse.json({ + message: "Account ID is required", + success: false, + }, { status: 400 }); + } + + if (accountId === session.user.accountId) { + return NextResponse.json({ + message: "You cannot change your own admin status", + success: false, + }, { status: 400 }); + } + + const isAdmin = await isUserAdmin(accountId); + const result = await makeUserAdmin(accountId, !isAdmin); + + if (!result.success) { + return NextResponse.json({ + message: result.status, + success: false, + }, { status: result.status === "User not found" ? 404 : 500 }); + } + + await removeAllSessionsByAccountId(accountId); + + logger.info(`Admin status toggled for user ${accountId}, all sessions revoked`, { + accountId, + newStatus: !isAdmin + }); + + return NextResponse.json({ + message: `${result.status} User will need to log in again.`, + is_admin: !isAdmin, + success: true, + }); + } catch (error) { + logger.error('Error toggling admin status:', { error }); + return NextResponse.json({ + message: "Failed to toggle admin status", + success: false, + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/admin/users/[accountId]/links/[shortId]/analytics/route.ts b/src/app/api/admin/users/[accountId]/links/[shortId]/analytics/route.ts new file mode 100644 index 0000000..c8f7820 --- /dev/null +++ b/src/app/api/admin/users/[accountId]/links/[shortId]/analytics/route.ts @@ -0,0 +1,116 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; +import { getAllAnalytics, removeAnalytics, removeAllAnalytics } from '@/lib/analyticsdb'; +import { sanitizeMongoDocument } from '@/lib/utils'; +import logger from '@/lib/logger'; + +export async function GET( + req: NextRequest, + { params }: { params: { accountId: string, shortId: string } } +) { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.isAdmin) { + return NextResponse.json({ + message: "Unauthorized", + success: false, + }, { status: 401 }); + } + + const { accountId, shortId } = await params; + + if (!accountId || !shortId) { + return NextResponse.json({ + message: "Account ID and Short ID are required", + success: false, + }, { status: 400 }); + } + + const url = new URL(req.url); + const page = parseInt(url.searchParams.get('page') || '1'); + const limit = parseInt(url.searchParams.get('limit') || '50'); + + const { analytics, total } = await getAllAnalytics( + accountId, + shortId, + { page, limit } + ); + + const sanitizedAnalytics = analytics.map(item => sanitizeMongoDocument(item)); + + return NextResponse.json({ + analytics: sanitizedAnalytics, + pagination: { + total, + page, + limit, + pages: Math.ceil(total / limit) + }, + success: true, + }); + } catch (error) { + logger.error('Error getting analytics:', { error, accountId: params.accountId, shortId: params.shortId }); + return NextResponse.json({ + message: "Failed to retrieve analytics", + success: false, + }, { status: 500 }); + } +} + +export async function DELETE( + req: NextRequest, + { params }: { params: { accountId: string, shortId: string } } +) { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.isAdmin) { + return NextResponse.json({ + message: "Unauthorized", + success: false, + }, { status: 401 }); + } + + const { accountId, shortId } = await params; + + if (!accountId || !shortId) { + return NextResponse.json({ + message: "Account ID and Short ID are required", + success: false, + }, { status: 400 }); + } + + const body = await req.json(); + + if (body.delete_all) { + const result = await removeAllAnalytics(accountId, shortId); + + return NextResponse.json({ + message: result.status, + success: result.success, + }, { status: result.success ? 200 : 400 }); + } + + if (body.analytics_id) { + const result = await removeAnalytics(accountId, shortId, body.analytics_id); + + return NextResponse.json({ + message: result.status, + success: result.success, + }, { status: result.success ? 200 : 400 }); + } + + return NextResponse.json({ + message: "Either delete_all or analytics_id must be provided", + success: false, + }, { status: 400 }); + } catch (error) { + logger.error('Error deleting analytics:', { error, accountId: params.accountId, shortId: params.shortId }); + return NextResponse.json({ + message: "Failed to delete analytics", + success: false, + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/admin/users/[accountId]/links/[shortId]/route.ts b/src/app/api/admin/users/[accountId]/links/[shortId]/route.ts new file mode 100644 index 0000000..6fdb5a8 --- /dev/null +++ b/src/app/api/admin/users/[accountId]/links/[shortId]/route.ts @@ -0,0 +1,137 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; +import { getLinkById, editLink, removeLink } from '@/lib/linkdb'; +import { sanitizeMongoDocument } from '@/lib/utils'; +import logger from '@/lib/logger'; + +export async function GET( + req: NextRequest, + { params }: { params: { accountId: string, shortId: string } } +) { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.isAdmin) { + return NextResponse.json({ + message: "Unauthorized", + success: false, + }, { status: 401 }); + } + + const { accountId, shortId } = await params; + + if (!accountId || !shortId) { + return NextResponse.json({ + message: "Account ID and Short ID are required", + success: false, + }, { status: 400 }); + } + + const { link, return: linkReturn } = await getLinkById(accountId, shortId); + + if (!linkReturn.success || !link) { + return NextResponse.json({ + message: linkReturn.status, + success: false, + }, { status: 404 }); + } + + return NextResponse.json({ + link: sanitizeMongoDocument(link), + success: true, + }); + } catch (error) { + logger.error('Error getting link details:', { error, accountId: params.accountId, shortId: params.shortId }); + return NextResponse.json({ + message: "Failed to retrieve link details", + success: false, + }, { status: 500 }); + } +} + +export async function PATCH( + req: NextRequest, + { params }: { params: { accountId: string, shortId: string } } +) { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.isAdmin) { + return NextResponse.json({ + message: "Unauthorized", + success: false, + }, { status: 401 }); + } + + const { accountId, shortId } = await params; + + if (!accountId || !shortId) { + return NextResponse.json({ + message: "Account ID and Short ID are required", + success: false, + }, { status: 400 }); + } + + const body = await req.json(); + const { target_url } = body; + + if (!target_url) { + return NextResponse.json({ + message: "Target URL is required", + success: false, + }, { status: 400 }); + } + + const result = await editLink(accountId, shortId, target_url); + + return NextResponse.json({ + message: result.status, + success: result.success, + }, { status: result.success ? 200 : 400 }); + } catch (error) { + logger.error('Error updating link:', { error, accountId: params.accountId, shortId: params.shortId }); + return NextResponse.json({ + message: "Failed to update link", + success: false, + }, { status: 500 }); + } +} + +export async function DELETE( + req: NextRequest, + { params }: { params: { accountId: string, shortId: string } } +) { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.isAdmin) { + return NextResponse.json({ + message: "Unauthorized", + success: false, + }, { status: 401 }); + } + + const { accountId, shortId } = await params; + + if (!accountId || !shortId) { + return NextResponse.json({ + message: "Account ID and Short ID are required", + success: false, + }, { status: 400 }); + } + + const result = await removeLink(accountId, shortId); + + return NextResponse.json({ + message: result.status, + success: result.success, + }, { status: result.success ? 200 : 400 }); + } catch (error) { + logger.error('Error deleting link:', { error, accountId: params.accountId, shortId: params.shortId }); + return NextResponse.json({ + message: "Failed to delete link", + success: false, + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/admin/users/[accountId]/links/route.ts b/src/app/api/admin/users/[accountId]/links/route.ts new file mode 100644 index 0000000..550c9f3 --- /dev/null +++ b/src/app/api/admin/users/[accountId]/links/route.ts @@ -0,0 +1,45 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; +import { getLinks } from '@/lib/linkdb'; +import { sanitizeMongoDocument } from '@/lib/utils'; +import logger from '@/lib/logger'; + +export async function GET( + req: NextRequest, + { params }: { params: { accountId: string } } +) { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.isAdmin) { + return NextResponse.json({ + message: "Unauthorized", + success: false, + }, { status: 401 }); + } + + const { accountId } = await params; + + if (!accountId) { + return NextResponse.json({ + message: "Account ID is required", + success: false, + }, { status: 400 }); + } + + const { links } = await getLinks(accountId); + const sanitizedLinks = links.map(link => (sanitizeMongoDocument(link))); + + return NextResponse.json({ + links: sanitizedLinks, + success: true, + }); + } catch (error) { + logger.error('Error getting user links:', { error }); + return NextResponse.json({ + message: "Failed to retrieve user links", + success: false, + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/admin/users/[accountId]/route.ts b/src/app/api/admin/users/[accountId]/route.ts new file mode 100644 index 0000000..87f2b37 --- /dev/null +++ b/src/app/api/admin/users/[accountId]/route.ts @@ -0,0 +1,53 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; +import { getUserById } from '@/lib/userdb'; +import logger from '@/lib/logger'; +import { sanitizeMongoDocument } from '@/lib/utils'; + +export async function GET( + req: NextRequest, + { params }: { params: { accountId: string } } +) { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.isAdmin) { + return NextResponse.json({ + message: "Unauthorized", + success: false, + }, { status: 401 }); + } + + const { accountId } = await params; + + if (!accountId) { + return NextResponse.json({ + message: "Account ID is required", + success: false, + }, { status: 400 }); + } + + const user = await getUserById(accountId); + + if (!user) { + return NextResponse.json({ + message: "User not found", + success: false, + }, { status: 404 }); + } + + const sanitizedUser = sanitizeMongoDocument(user); + + return NextResponse.json({ + user: sanitizedUser, + success: true, + }); + } catch (error) { + logger.error('Error getting user:', error); + return NextResponse.json({ + message: "Failed to retrieve user", + success: false, + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/admin/users/[accountId]/sessions/revoke/route.ts b/src/app/api/admin/users/[accountId]/sessions/revoke/route.ts new file mode 100644 index 0000000..d682462 --- /dev/null +++ b/src/app/api/admin/users/[accountId]/sessions/revoke/route.ts @@ -0,0 +1,39 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; +import { revokeSession } from '@/lib/sessiondb'; +import logger from '@/lib/logger'; + +export async function POST(req: NextRequest) { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.isAdmin) { + return NextResponse.json({ + message: "Unauthorized", + success: false, + }, { status: 401 }); + } + + const { sessionId, accountId } = await req.json(); + + if (!sessionId || !accountId) { + return NextResponse.json({ + message: "Session ID and Account ID are required", + success: false, + }, { status: 400 }); + } + + const result = await revokeSession(sessionId, accountId); + + return NextResponse.json({ + ...result, + }); + } catch (error) { + logger.error('Error revoking session:', { error }); + return NextResponse.json({ + message: "Failed to revoke session", + success: false, + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/admin/users/[accountId]/sessions/route.ts b/src/app/api/admin/users/[accountId]/sessions/route.ts new file mode 100644 index 0000000..c771bda --- /dev/null +++ b/src/app/api/admin/users/[accountId]/sessions/route.ts @@ -0,0 +1,47 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; +import { getSessions } from '@/lib/sessiondb'; +import { sanitizeMongoDocument } from '@/lib/utils'; +import logger from '@/lib/logger'; + +export async function GET( + req: NextRequest, + { params }: { params: { accountId: string } } +) { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.isAdmin) { + return NextResponse.json({ + message: "Unauthorized", + success: false, + }, { status: 401 }); + } + + const { accountId } = await params; + + if (!accountId) { + return NextResponse.json({ + message: "Account ID is required", + success: false, + }, { status: 400 }); + } + + const sessions = await getSessions(accountId); + const sanitizedSessions = Array.isArray(sessions) + ? sessions.map(session => sanitizeMongoDocument(session)) + : []; + + return NextResponse.json({ + sessions: sanitizedSessions, + success: true, + }); + } catch (error) { + logger.error('Error getting user sessions:', { error }); + return NextResponse.json({ + message: "Failed to retrieve user sessions", + success: false, + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/admin/users/route.ts b/src/app/api/admin/users/route.ts new file mode 100644 index 0000000..de4194b --- /dev/null +++ b/src/app/api/admin/users/route.ts @@ -0,0 +1,91 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; +import { listUsers, removeUser, isUserAdmin } from '@/lib/userdb'; +import { sanitizeMongoDocument } from '@/lib/utils'; +import logger from '@/lib/logger'; + +export async function GET() { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.isAdmin) { + return NextResponse.json({ + message: "Unauthorized", + success: false, + }, { status: 401 }); + } + + const result = await listUsers(); + + if (result.users) { + const sanitizedUsers = result.users.map(user => (sanitizeMongoDocument(user))); + + return NextResponse.json({ + users: sanitizedUsers, + total: result.total, + success: true, + }); + } + + return NextResponse.json({ + message: result.return.status, + success: result.return.success, + }, { status: result.return.success ? 200 : 500 }); + } catch (error) { + logger.error('Error getting users:', { error }); + return NextResponse.json({ + message: "Failed to retrieve users", + success: false, + }, { status: 500 }); + } +} + +export async function DELETE(req: NextRequest) { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.isAdmin) { + return NextResponse.json({ + message: "Unauthorized", + success: false, + }, { status: 401 }); + } + + const { account_id } = await req.json(); + + if (!account_id) { + return NextResponse.json({ + message: "Account ID is required", + success: false, + }, { status: 400 }); + } + + if (account_id === session.user.accountId) { + return NextResponse.json({ + message: "You cannot delete your own account from admin panel", + success: false, + }, { status: 400 }); + } + + if (await isUserAdmin(account_id)) { + return NextResponse.json({ + message: "Cannot delete admin accounts", + success: false, + }, { status: 400 }); + } + + const result = await removeUser(account_id); + + return NextResponse.json({ + message: result.status, + success: result.success, + }); + } catch (error) { + logger.error('Error deleting user:', { error }); + return NextResponse.json({ + message: "Failed to delete user", + success: false, + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/analytics/route.ts b/src/app/api/analytics/route.ts new file mode 100644 index 0000000..7fcfbe5 --- /dev/null +++ b/src/app/api/analytics/route.ts @@ -0,0 +1,185 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getAllAnalytics, removeAnalytics, removeAllAnalytics } from '@/lib/analyticsdb'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; +import logger from '@/lib/logger'; + +// View analytics for a specific link +// Example: /api/analytics?link_id=SHORT_ID&page=1&limit=50&startDate=2025-01-01&endDate=2025-12-31 +export async function GET(req: NextRequest) { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.accountId) { + logger.info('Analytics request failed due to unauthorized access', { url: req.url }); + return NextResponse.json({ + message: "Unauthorized. Please sign in first.", + success: false, + }, { status: 401 }); + } + + const account_id = session.user.accountId; + const url = new URL(req.url); + + const link_id = url.searchParams.get('link_id'); + if (!link_id) { + logger.info('Analytics request failed due to missing link_id', { account_id, url: req.url }); + return NextResponse.json({ + message: "Missing link_id parameter", + success: false, + }, { status: 400 }); + } + const all = url.searchParams.get('all') === 'true'; + + let page = 1; + let limit = 50; + + if (!all) { + page = parseInt(url.searchParams.get('page') || '1'); + limit = parseInt(url.searchParams.get('limit') || '50'); + } + + let startDate: Date | undefined; + let endDate: Date | undefined; + + if (url.searchParams.get('startDate')) { + startDate = new Date(url.searchParams.get('startDate')!); + } + + if (url.searchParams.get('endDate')) { + endDate = new Date(url.searchParams.get('endDate')!); + } + + const queryOptions = all + ? {} + : { + page, + limit, + startDate, + endDate + }; + + const { analytics, total } = await getAllAnalytics(account_id, link_id, queryOptions); + + return NextResponse.json({ + message: "Analytics retrieved successfully", + success: true, + analytics, + pagination: { + total, + page, + limit, + totalPages: Math.ceil(total / limit) + } + }, { status: 200 }); + } catch (error) { + logger.error('Analytics retrieval error:', { error, url: req.url }); + return NextResponse.json({ + message: "Failed to retrieve analytics", + success: false, + }, { status: 500 }); + } +} + +// Delete analytics record +export async function DELETE(req: NextRequest) { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.accountId) { + logger.info('Analytics deletion request failed due to unauthorized access', { url: req.url }); + return NextResponse.json({ + message: "Unauthorized. Please sign in first.", + success: false, + }, { status: 401 }); + } + + const account_id = session.user.accountId; + let link_id, analytics_id, delete_all; + + try { + const contentType = req.headers.get('content-type'); + if (contentType && contentType.includes('application/json') && req.body) { + const body = await req.json(); + link_id = body.link_id; + analytics_id = body.analytics_id; + delete_all = body.delete_all; + } + } catch { + logger.info('Analytics deletion request failed due to missing parameters', { url: req.url }); + return NextResponse.json({ + message: "Missing required parameters", + success: false, + }, { status: 400 }); + } + + if (!link_id) { + logger.info('Analytics deletion request failed due to missing link_id', { account_id, url: req.url }); + return NextResponse.json({ + message: "Missing link_id parameter", + success: false, + }, { status: 400 }); + } + + // Delete all analytics for a link + if (delete_all) { + const result = await removeAllAnalytics(account_id, link_id); + + if (result.success) { + logger.info('All analytics deletion request succeeded', { account_id, link_id, url: req.url }); + return NextResponse.json({ + message: "All analytics records deleted successfully", + success: true, + }, { status: 200 }); + } else { + logger.info('All analytics deletion request failed', { error: result.status, account_id, link_id, url: req.url }); + return NextResponse.json({ + message: result.status, + success: false, + }, { status: 404 }); + } + } + + // Delete single analytics record + if (!analytics_id) { + logger.info('Analytics deletion request failed due to missing analytics_id', { account_id, link_id, url: req.url }); + return NextResponse.json({ + message: "Missing analytics_id parameter for single record deletion", + success: false, + }, { status: 400 }); + } + + const result = await removeAnalytics(account_id, link_id, analytics_id); + + if (result.success) { + logger.info('Single analytics record deletion request succeeded', { + account_id, + link_id, + analytics_id, + url: req.url + }); + return NextResponse.json({ + message: "Analytics record deleted successfully", + success: true, + }, { status: 200 }); + } else { + logger.info('Single analytics record deletion request failed', { + error: result.status, + account_id, + link_id, + analytics_id, + url: req.url + }); + return NextResponse.json({ + message: result.status, + success: false, + }, { status: 404 }); + } + } catch (error) { + logger.error('Analytics deletion error:', { error, url: req.url }); + return NextResponse.json({ + message: "Failed to delete analytics", + success: false, + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..0401a1e --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,102 @@ +import NextAuth, { NextAuthOptions, User } from "next-auth"; +import CredentialsProvider from "next-auth/providers/credentials"; +import logger from '@/lib/logger'; +import { existsUser, isUserAdmin } from "@/lib/userdb"; +import { JWT } from "next-auth/jwt"; +import { Session } from "next-auth"; +import { createSession, updateSessionActivity, revokeSession } from '@/lib/sessiondb'; +import { headers } from 'next/headers'; + +export const authOptions: NextAuthOptions = { + providers: [ + CredentialsProvider({ + name: "Account ID", + credentials: { + accountId: { label: "Account ID", type: "text", placeholder: "Enter your Account ID" } + }, + async authorize(credentials) { + const { accountId } = credentials as { accountId: string }; + + const exists = await existsUser(accountId); + + if (exists) { + const isAdmin = await isUserAdmin(accountId); + + return { + id: accountId, + accountId, + isAdmin, + }; + } + + return null; + } + }), + ], + callbacks: { + async jwt({ token, user, trigger }: { token: JWT; user: User, trigger?: string }) { + if (user) { + token.accountId = user.accountId; + token.isAdmin = user.isAdmin; + + const headersList = await headers(); + const userAgent = headersList.get('user-agent') || 'Unknown'; + const ip = headersList.get('x-forwarded-for') || + headersList.get('x-real-ip') || + 'Unknown'; + + const { sessionId } = await createSession( + user.accountId, + userAgent, + ip + ); + + token.sessionId = sessionId; + } + + if (trigger === 'update' && token.sessionId) { + await updateSessionActivity(token.sessionId as string); + } + + return token; + }, + async session({ session, token }: { session: Session; token: JWT }) { + if (token) { + session.user = { + ...session.user, + accountId: token.accountId as string, + isAdmin: token.isAdmin as boolean, + sessionId: token.sessionId as string + }; + } + return session; + } + }, + events: { + async signOut({ token }) { + if (token?.sessionId) { + try { + await revokeSession(token.sessionId as string, token.accountId as string); + } catch (error) { + logger.error('Error terminating session on signOut:', error); + } + } + } + }, + pages: { + signIn: '/', + error: '/', + }, + session: { + strategy: "jwt" as const, + maxAge: 30 * 24 * 60 * 60, // 30 days (session lifetime) + }, + jwt: { + maxAge: 5 * 60, // 5 min (token lifetime) + }, + secret: process.env.NEXTAUTH_SECRET || "/JZ9N+lqRtvspbAfs0HK41RkthPYuUdqxb+cuimYOXw=", +}; + +const handler = NextAuth(authOptions); + +export { handler as GET, handler as POST }; \ No newline at end of file diff --git a/src/app/api/auth/check-session/route.ts b/src/app/api/auth/check-session/route.ts new file mode 100644 index 0000000..f01f762 --- /dev/null +++ b/src/app/api/auth/check-session/route.ts @@ -0,0 +1,43 @@ +import { NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; +import { isSessionValid } from '@/lib/sessiondb'; +import logger from '@/lib/logger'; + +export async function GET() { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.accountId || !session?.user?.sessionId) { + return NextResponse.json({ + valid: false, + message: "No active session" + }, { status: 401 }); + } + + const isValid = await isSessionValid( + session.user.sessionId, + session.user.accountId + ); + + if (!isValid) { + logger.info('Session check failed - revoked or expired session', { + sessionId: session.user.sessionId, + accountId: session.user.accountId + }); + + return NextResponse.json({ + valid: false, + message: "Session has been revoked" + }, { status: 401 }); + } + + return NextResponse.json({ valid: true }); + } catch (error) { + logger.error('Error checking session:', error); + return NextResponse.json({ + valid: false, + message: "Error checking session" + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/auth/register/route.ts b/src/app/api/auth/register/route.ts new file mode 100644 index 0000000..21a04c9 --- /dev/null +++ b/src/app/api/auth/register/route.ts @@ -0,0 +1,61 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { createUser, isUserAdmin } from '@/lib/userdb'; +import logger from '@/lib/logger'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; + +export async function POST(req: NextRequest) { + try { + let is_admin; + logger.info('Registration request', { url: req.url }); + const session = await getServerSession(authOptions); + + try { + const contentType = req.headers.get('content-type'); + if (contentType && contentType.includes('application/json') && req.body) { + const body = await req.json(); + is_admin = body.is_admin; + } + } catch { + logger.info('Registration request failed due to missing parameters', { url: req.url }); + return NextResponse.json({ + message: "Missing required parameters", + success: false, + }, { status: 400 }); + } + + if (is_admin) { + const isAuthorized = await isUserAdmin(session?.user?.accountId); + + if (isAuthorized) { + const account = await createUser(is_admin); + logger.info('Account creation request succeeded (admin)', { is_admin, url: req.url }); + return NextResponse.json({ + message: "Admin registration successful", + success: true, + account_id: account.account_id, + }, { status: 200 }); + } else { + logger.info('Registration request failed due to missing rights', { is_admin, url: req.url }); + return NextResponse.json({ + message: "Unauthorized admin registration attempt", + success: false, + }, { status: 401 }); + } + } + + const account = await createUser(false); + + return NextResponse.json({ + message: "Registration successful", + success: true, + account_id: account.account_id, + }); + } catch (error) { + logger.error('Registration error:', { error, url: req.url }); + return NextResponse.json({ + message: "Registration failed", + success: false, + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/auth/remove/route.ts b/src/app/api/auth/remove/route.ts new file mode 100644 index 0000000..81ec850 --- /dev/null +++ b/src/app/api/auth/remove/route.ts @@ -0,0 +1,47 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { removeUser, isUserAdmin } from '@/lib/userdb'; +import logger from '@/lib/logger'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; + +export async function DELETE(req: NextRequest) { + try { + let account_id; + logger.info('Account removal request', { url: req.url }); + const session = await getServerSession(authOptions); + + try { + const contentType = req.headers.get('content-type'); + if (contentType && contentType.includes('application/json') && req.body) { + const body = await req.json(); + account_id = body.account_id; + } + } catch { + logger.info('Account removal request failed due to missing parameters', { url: req.url }); + return NextResponse.json({ + message: "Missing required parameters", + success: false, + }, { status: 400 }); + } + + if (session?.user?.accountId === account_id || await isUserAdmin(session?.user?.accountId)) { + const accountRemovalResponse = await removeUser(account_id); + return NextResponse.json({ + message: accountRemovalResponse.status, + success: accountRemovalResponse.success, + }, { status: accountRemovalResponse.success ? 200 : 500 }); + } + + logger.info('Account removal request failed due to missing rights', { url: req.url }); + return NextResponse.json({ + message: "Unauthorized account removal attempt", + success: false, + }, { status: 401 }); + } catch (error) { + logger.error('Account removal error:', { error, url: req.url }); + return NextResponse.json({ + message: "Account removal failed", + success: false, + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/auth/sessions/revoke/route.ts b/src/app/api/auth/sessions/revoke/route.ts new file mode 100644 index 0000000..25e9f47 --- /dev/null +++ b/src/app/api/auth/sessions/revoke/route.ts @@ -0,0 +1,46 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth/next'; +import { revokeSession } from '@/lib/sessiondb'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; +import { signOut } from 'next-auth/react'; +import logger from '@/lib/logger'; + +export async function POST(req: NextRequest) { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.accountId) { + return NextResponse.json({ + message: "Unauthorized", + success: false, + }, { status: 401 }); + } + + const { sessionId } = await req.json(); + + if (!sessionId) { + return NextResponse.json({ + message: "Session ID is required", + success: false, + }, { status: 400 }); + } + + const result = await revokeSession(sessionId, session.user.accountId); + + const isCurrentSession = sessionId === session.user.sessionId; + if (isCurrentSession) { + signOut({ redirect: false }); + } + + return NextResponse.json({ + ...result, + isCurrentSession, + }); + } catch (error) { + logger.error('Error revoking session:', { error }); + return NextResponse.json({ + message: "Failed to revoking session", + success: false, + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/auth/sessions/route.ts b/src/app/api/auth/sessions/route.ts new file mode 100644 index 0000000..f66ade2 --- /dev/null +++ b/src/app/api/auth/sessions/route.ts @@ -0,0 +1,38 @@ +import { NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth/next'; +import { getSessions } from '@/lib/sessiondb'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; +import logger from '@/lib/logger'; + +export async function GET() { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.accountId) { + return NextResponse.json({ + message: "Unauthorized", + success: false, + }, { status: 401 }); + } + + const sessions = await getSessions(session.user.accountId); + + // Mark the current session + const currentSessionId = session.user.sessionId; + const sessionsWithCurrent = sessions.map(s => ({ + ...s, + isCurrentSession: s.id === currentSessionId + })); + + return NextResponse.json({ + sessions: sessionsWithCurrent, + success: true, + }); + } catch (error) { + logger.error('Error getting sessions:', { error }); + return NextResponse.json({ + message: "Failed to retrieve sessions", + success: false, + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/link/route.ts b/src/app/api/link/route.ts new file mode 100644 index 0000000..d65be89 --- /dev/null +++ b/src/app/api/link/route.ts @@ -0,0 +1,252 @@ +import { NextRequest, NextResponse } from 'next/server'; +import logger from '@/lib/logger'; +import { isValidUrl, sanitizeMongoDocument } from '@/lib/utils'; +import { getLinkById, createLink, editLink, removeLink } from '@/lib/linkdb'; +import { removeAllAnalytics } from '@/lib/analyticsdb'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; + +// View link +// Example: /api/link?shortId=SHORT_ID +export async function GET(req: NextRequest) { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.accountId) { + logger.info('Link retrieval request failed due to unauthorized access', { url: req.url }); + return NextResponse.json({ + message: "Unauthorized. Please sign in first.", + success: false, + }, { status: 401 }); + } + + const account_id = session.user.accountId; + + const url = new URL(req.url); + const shortId = url.searchParams.get('shortId'); + + if (!shortId) { + logger.info('Link retrieval request failed due to missing shortId', { account_id, url: req.url }); + return NextResponse.json({ + message: "Missing shortId parameter", + success: false, + }, { status: 400 }); + } + + const { link, return: returnValue } = await getLinkById(account_id, shortId); + + if (returnValue.success && link) { + return NextResponse.json({ + message: "Link retrieved successfully", + success: true, + link: sanitizeMongoDocument(link), + }, { status: 200 }); + } + + logger.info('Link retrieval request failed', { error: returnValue.status, account_id, shortId, url: req.url }); + return NextResponse.json({ + message: returnValue.status || "Link not found", + success: false, + }, { status: 404 }); + } catch (error) { + logger.error('Link retrieval error:', { error, url: req.url }); + return NextResponse.json({ + message: "Failed to retrieve link", + success: false, + }, { status: 500 }); + } +} + +// Create link +export async function POST(req: NextRequest) { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.accountId) { + logger.info('Link creation request failed due to unauthorized access', { url: req.url }); + return NextResponse.json({ + message: "Unauthorized. Please sign in first.", + success: false, + }, { status: 401 }); + } + + const account_id = session.user.accountId; + let target_url; + + try { + const contentType = req.headers.get('content-type'); + if (contentType && contentType.includes('application/json') && req.body) { + const body = await req.json(); + target_url = body.target_url; + } + } catch { + logger.info('Link creation request failed due to missing parameters', { url: req.url }); + return NextResponse.json({ + message: "Missing required parameters", + success: false, + }, { status: 400 }); + } + + if (!target_url || !isValidUrl(target_url)) { + logger.info('Link creation request failed due to invalid URL', { account_id, url: req.url }); + return NextResponse.json({ + message: "Invalid URL. Please provide a valid URL with http:// or https://", + success: false, + }, { status: 400 }); + } + + const { shortId, return: returnValue } = await createLink(account_id, target_url); + + if(returnValue.success) { + return NextResponse.json({ + message: "Link Creation succeeded", + success: true, + shortId, + }, { status: 200 }); + } + + logger.error('Link creation request failed', { error: returnValue.status, account_id, url: req.url }); + return NextResponse.json({ + message: returnValue.status || "Link creation failed", + success: false, + }, { status: 422 }); + } catch (error) { + logger.error('Link creation error:', { error, url: req.url }); + return NextResponse.json({ + message: "Link creation failed", + success: false, + }, { status: 500 }); + } +} + +// Edit link +export async function PATCH(req: NextRequest) { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.accountId) { + logger.info('Link edit request failed due to unauthorized access', { url: req.url }); + return NextResponse.json({ + message: "Unauthorized. Please sign in first.", + success: false, + }, { status: 401 }); + } + + const account_id = session.user.accountId; + let shortId, target_url; + + try { + const contentType = req.headers.get('content-type'); + if (contentType && contentType.includes('application/json') && req.body) { + const body = await req.json(); + shortId = body.shortId; + target_url = body.target_url; + } + } catch { + logger.info('Link edit request failed due to missing parameters', { url: req.url }); + return NextResponse.json({ + message: "Missing required parameters", + success: false, + }, { status: 400 }); + } + + if (!shortId) { + logger.info('Link edit request failed due to missing shortId', { account_id, url: req.url }); + return NextResponse.json({ + message: "Missing shortId parameter", + success: false, + }, { status: 400 }); + } + + if (!target_url || !isValidUrl(target_url)) { + logger.info('Link edit request failed due to invalid URL', { account_id, url: req.url }); + return NextResponse.json({ + message: "Invalid URL. Please provide a valid URL with http:// or https://", + success: false, + }, { status: 400 }); + } + + const returnValue = await editLink(account_id, shortId, target_url); + + if(returnValue.success) { + return NextResponse.json({ + message: "Link updated successfully", + success: true, + }, { status: 200 }); + } + + logger.error('Link edit request failed', { error: returnValue.status, account_id, shortId, url: req.url }); + return NextResponse.json({ + message: returnValue.status || "Link update failed", + success: false, + }, { status: 422 }); + } catch (error) { + logger.error('Link edit error:', { error, url: req.url }); + return NextResponse.json({ + message: "Link update failed", + success: false, + }, { status: 500 }); + } +} + +// Delete link +export async function DELETE(req: NextRequest) { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.accountId) { + logger.info('Link removal request failed due to unauthorized access', { url: req.url }); + return NextResponse.json({ + message: "Unauthorized. Please sign in first.", + success: false, + }, { status: 401 }); + } + + const account_id = session.user.accountId; + let shortId; + + try { + const contentType = req.headers.get('content-type'); + if (contentType && contentType.includes('application/json') && req.body) { + const body = await req.json(); + shortId = body.shortId; + } + } catch { + logger.info('Link removal request failed due to missing parameters', { url: req.url }); + return NextResponse.json({ + message: "Missing required parameters", + success: false, + }, { status: 400 }); + } + + if (!shortId) { + logger.info('Link removal request failed due to missing shortId', { account_id, url: req.url }); + return NextResponse.json({ + message: "Missing shortId parameter", + success: false, + }, { status: 400 }); + } + + await removeAllAnalytics(account_id, shortId); + const returnValue = await removeLink(account_id, shortId); + + if(returnValue.success) { + return NextResponse.json({ + message: "Link removal succeeded", + success: true, + }, { status: 200 }); + } + + logger.error('Link removal request failed', { error: returnValue.status, account_id, url: req.url }); + return NextResponse.json({ + message: returnValue.status || "Link removal failed", + success: false, + }, { status: 422 }); + } catch (error) { + logger.error('Link removal error:', { error, url: req.url }); + return NextResponse.json({ + message: "Link removal failed", + success: false, + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/links/route.ts b/src/app/api/links/route.ts new file mode 100644 index 0000000..da8cda1 --- /dev/null +++ b/src/app/api/links/route.ts @@ -0,0 +1,44 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getLinks } from '@/lib/linkdb'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; +import { sanitizeMongoDocument } from '@/lib/utils'; +import logger from '@/lib/logger'; + +// Get all links +export async function GET(req: NextRequest) { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.accountId) { + logger.info('Links retrieval request failed due to unauthorized access', { url: req.url }); + return NextResponse.json({ + message: "Unauthorized. Please sign in first.", + success: false, + }, { status: 401 }); + } + + const account_id = session.user.accountId; + const { links, return: returnValue } = await getLinks(account_id); + + if (returnValue.success) { + return NextResponse.json({ + message: "Links retrieved successfully", + success: true, + links: links.map(link => sanitizeMongoDocument(link)), + }, { status: 200 }); + } + + logger.info('Links retrieval request failed', { error: returnValue.status, account_id, url: req.url }); + return NextResponse.json({ + message: returnValue.status || "Failed to retrieve links", + success: false, + }, { status: 404 }); + } catch (error) { + logger.error('Links retrieval error:', { error, url: req.url }); + return NextResponse.json({ + message: "Failed to retrieve links", + success: false, + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/statistics/route.ts b/src/app/api/statistics/route.ts new file mode 100644 index 0000000..042d931 --- /dev/null +++ b/src/app/api/statistics/route.ts @@ -0,0 +1,33 @@ +import { NextResponse } from 'next/server'; +import { getAllStats, updateStats } from '@/lib/statisticsdb'; + +export async function GET() { + try { + let stats = await getAllStats(); + + if (!stats || !stats.last_updated || + (new Date().getTime() - new Date(stats.last_updated).getTime() > 5 * 60 * 1000)) { // 5min + await updateStats(); + stats = await getAllStats(); + } + + if (!stats) { + return NextResponse.json({ + message: "Failed to retrieve statistics", + success: false, + }, { status: 500 }); + } + + return NextResponse.json({ + message: "Statistics retrieved successfully", + success: true, + stats, + }); + } catch (error) { + console.error('Statistics retrieval error:', error); + return NextResponse.json({ + message: "An error occurred while retrieving statistics", + success: false, + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/dashboard/Dashboard.module.css b/src/app/dashboard/Dashboard.module.css new file mode 100644 index 0000000..a56b687 --- /dev/null +++ b/src/app/dashboard/Dashboard.module.css @@ -0,0 +1,163 @@ +.dashboardContainer { + max-width: 75rem; + width: 100%; + margin: 0 auto; + padding: 2rem 1rem; +} + +.dashboardHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; +} + +.dashboardTitle { + font-size: 2rem; + color: var(--text-primary); + margin: 0; +} + +.actionButtons { + display: flex; + gap: 1rem; +} + +.securityButton, .adminButton { + padding: 0.5rem 1rem; + border-radius: 4px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + border: none; +} + +.securityButton { + background-color: var(--accent); + color: white; +} + +.adminButton { + background-color: #8b5cf6; + color: white; +} + +.securityButton:hover, .adminButton:hover { + opacity: 0.9; + transform: translateY(-2px); +} + +.urlShortener { + background-color: var(--card-bg); + border-radius: 8px; + padding: 1.5rem; + margin-bottom: 2rem; + border: 1px solid var(--border-color); +} + +.createForm { + margin-top: 1rem; +} + +.inputGroup { + display: flex; + gap: 1rem; +} + +.urlInput { + flex: 1; + padding: 0.75rem 1rem; + border-radius: 4px; + border: 1px solid var(--border-color); + background-color: var(--input-bg); + color: var(--text-primary); + font-size: 1rem; +} + +.createButton { + padding: 0 1.5rem; + border-radius: 4px; + border: none; + background-color: var(--accent); + color: white; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s ease; +} + +.createButton:hover { + background-color: var(--accent-hover); +} + +.createButton:disabled { + background-color: var(--accent-disabled); + cursor: not-allowed; +} + +.linksSection { + background-color: var(--card-bg); + border-radius: 8px; + padding: 1.5rem; + border: 1px solid var(--border-color); +} + +.loading { + display: flex; + justify-content: center; + align-items: center; + padding: 2rem; + color: var(--text-secondary); + font-size: 1rem; +} + +.noLinks { + text-align: center; + padding: 2rem; + color: var(--text-secondary); + font-style: italic; +} + +@media (max-width: 1200px) { + .dashboardContainer { + max-width: 100%; + padding: 1.5rem; + } +} + +@media (max-width: 768px) { + .inputGroup { + flex-direction: column; + } + + .dashboardHeader { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + } + + .actionButtons { + width: 100%; + justify-content: flex-start; + } + + .urlShortener, + .linksSection { + padding: 1rem; + } +} + +@media (max-width: 480px) { + .dashboardContainer { + padding: 1rem 0.5rem; + } + + .dashboardTitle { + font-size: 1.5rem; + } + + .securityButton, + .adminButton { + padding: 0.4rem 0.8rem; + font-size: 0.9rem; + } +} \ No newline at end of file diff --git a/src/app/dashboard/link/[shortId]/LinkDetail.module.css b/src/app/dashboard/link/[shortId]/LinkDetail.module.css new file mode 100644 index 0000000..b15018b --- /dev/null +++ b/src/app/dashboard/link/[shortId]/LinkDetail.module.css @@ -0,0 +1,280 @@ +.container { + width: 100%; + max-width: 1200px; + margin: 0 auto; + padding: 2rem 1rem; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; +} + +.backLink { + padding: 0.5rem 1rem; + border-radius: 4px; + background-color: var(--accent); + color: var(--text-primary); + text-decoration: none; + font-weight: 500; + transition: all 0.2s ease; +} + +.backLink:hover { + opacity: 0.9; + transform: translateY(-2px); +} + +.title { + font-size: 2rem; + margin: 0; + color: var(--text-primary); +} + +.linkInfo { + margin-bottom: 2rem; +} + +.linkCard { + background-color: var(--card-bg); + border-radius: 0.5rem; + padding: 1.5rem; + box-shadow: var(--card-shadow); +} + +.linkCard h2 { + margin-top: 0; + margin-bottom: 1.5rem; + color: var(--text-primary); + font-size: 1.5rem; +} + +.linkDetails { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.linkDetailItem { + display: flex; + flex-direction: column; +} + +.label { + font-weight: 600; + color: var(--text-secondary); + margin-bottom: 0.25rem; +} + +.value { + color: var(--text-primary); + word-break: break-all; +} + +.shortUrl { + color: var(--primary-color); + word-break: break-all; + text-decoration: none; +} + +.shortUrl:hover { + text-decoration: underline; +} + +.defaultButton { + background-color: transparent; + color: var(--primary-color); + border: 1px solid var(--primary-color); + border-radius: 0.25rem; + padding: 0.25rem 0.5rem; + font-size: 0.75rem; + cursor: pointer; +} + +.defaultButton:hover { + background-color: var(--primary-color); + color: white; +} + +.targetUrlSection { + margin-top: 0.5rem; +} + +.targetUrlHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; +} + +.targetUrl { + color: var(--text-primary); + word-break: break-all; + text-decoration: none; +} + +.targetUrl:hover { + text-decoration: underline; + color: var(--primary-color); +} + +.editForm { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.urlInput { + width: 100%; + padding: 0.5rem; + border: 1px solid var(--border-color); + border-radius: 0.25rem; + background-color: var(--input-bg); + color: var(--text-primary); +} + +.editActions { + display: flex; + gap: 0.5rem; + justify-content: flex-end; +} + +.cancelButton { + background-color: transparent; + color: var(--text-secondary); + border: 1px solid var(--border-color); + border-radius: 0.25rem; + padding: 0.5rem 0.75rem; + cursor: pointer; +} + +.cancelButton:hover { + background-color: var(--border-color); +} + +.saveButton { + background-color: var(--primary-color); + color: white; + border: none; + border-radius: 0.25rem; + padding: 0.5rem 0.75rem; + cursor: pointer; +} + +.saveButton:hover { + background-color: var(--primary-dark); +} + +.analyticsSection { + margin-top: 2rem; +} + +.analyticsHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; + flex-wrap: wrap; + gap: 1rem; +} + +.analyticsHeader h2 { + margin: 0; + color: var(--text-primary); + font-size: 1.5rem; +} + +.totalClicks { + color: var(--text-secondary); + font-weight: 600; +} + +.deleteAllButton { + background-color: transparent; + color: var(--primary-color); + border: 1px solid var(--primary-color); + border-radius: 0.25rem; + padding: 0.25rem 0.5rem; + font-size: 1rem; + cursor: pointer; +} + +.deleteAllButton:hover { + background-color: var(--primary-color); + color: white; +} + +.graphs { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 1rem; + margin-bottom: 2rem; +} + +.graphCard { + background-color: var(--card-bg); + border-radius: 0.5rem; + padding: 1rem; + box-shadow: var(--card-shadow); +} + +.graphCard h3 { + margin-top: 0; + margin-bottom: 1rem; + color: var(--text-primary); + font-size: 1rem; + text-align: center; +} + +.noAnalytics { + display: flex; + justify-content: center; + align-items: center; + background-color: var(--card-bg); + border-radius: 0.5rem; + padding: 2rem; + box-shadow: var(--card-shadow); +} + +.noAnalytics p { + color: var(--text-secondary); + font-size: 1rem; + text-align: center; +} + +.loadingContainer { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + min-height: 300px; + gap: 1rem; +} + +.loader { + border: 4px solid rgba(0, 0, 0, 0.1); + border-left-color: var(--primary-color); + border-radius: 50%; + width: 40px; + height: 40px; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +@media (max-width: 768px) { + .analyticsHeader { + flex-direction: column; + align-items: flex-start; + } + + .graphs { + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/src/app/dashboard/link/[shortId]/page.tsx b/src/app/dashboard/link/[shortId]/page.tsx new file mode 100644 index 0000000..d695e88 --- /dev/null +++ b/src/app/dashboard/link/[shortId]/page.tsx @@ -0,0 +1,545 @@ +"use client"; + +import { useState, useEffect, useRef } from 'react'; +import { useParams, useRouter } from 'next/navigation'; +import Link from 'next/link'; +import { useToast } from '@/contexts/ToastContext'; +import Graph, { StatItem } from '@/components/ui/Graph'; +import ConfirmModal from '@/components/ui/ConfirmModal'; +import AnalyticsTable from '@/components/ui/dashboard/link/AnalyticsTable'; +import styles from './LinkDetail.module.css'; +import { Analytics } from '@/types/analytics'; +import { Link as LinkType } from '@/types/link'; + +export default function LinkDetailPage() { + const params = useParams(); + const router = useRouter(); + const { showToast } = useToast(); + const shortId = params.shortId as string; + + const [link, setLink] = useState(null); + const [targetUrl, setTargetUrl] = useState(''); + const [isEditing, setIsEditing] = useState(false); + const inputRef = useRef(null); + const [isLoading, setIsLoading] = useState(true); + const [allAnalytics, setAllAnalytics] = useState([]); + const [analytics, setAnalytics] = useState([]); + const [totalAnalytics, setTotalAnalytics] = useState(0); + const [page, setPage] = useState(1); + const [limit] = useState(15); + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [showDeleteAllModal, setShowDeleteAllModal] = useState(false); + const [analyticsToDelete, setAnalyticsToDelete] = useState(''); + const [isLoadingStats, setIsLoadingStats] = useState(true); + const isRedirecting = useRef(false); + + // Stats data + const [browserStats, setBrowserStats] = useState([]); + const [osStats, setOsStats] = useState([]); + const [countryStats, setCountryStats] = useState([]); + const [ipVersionStats, setIpVersionStats] = useState([]); + + function isValidUrl(urlStr: string) : boolean { + if(urlStr.trim() === "") { + return false; + } + + try { + const parsedUrl = new URL(urlStr); + return parsedUrl.protocol !== "" && parsedUrl.hostname !== ""; + } catch { + return false; + } + } + + useEffect(() => { + if (isEditing && inputRef.current) { + setTimeout(() => { + inputRef.current?.focus(); + }, 10); + } + }, [isEditing]); + + useEffect(() => { + if (isRedirecting.current) return; + + async function fetchLinkData() { + try { + const response = await fetch(`/api/link?shortId=${shortId}`); + if (!response.ok) { + showToast('Failed to load link details', 'error'); + isRedirecting.current = true; + router.push('/dashboard'); + return; + } + + const data = await response.json(); + if (data.success && data.link) { + setLink(data.link); + setTargetUrl(data.link.target_url); + } else { + showToast(data.message || 'Link not found', 'error'); + isRedirecting.current = true; + router.push('/dashboard'); + } + } catch { + showToast('An error occurred while loading link details', 'error'); + isRedirecting.current = true; + router.push('/dashboard'); + } finally { + setIsLoading(false); + } + } + + fetchLinkData(); + }, [shortId, router, showToast]); + + useEffect(() => { + if (!link) return; + + async function fetchAllAnalytics() { + try { + const response = await fetch(`/api/analytics?link_id=${shortId}&all=true`); + if (!response.ok) { + showToast('Failed to load complete analytics data', 'error'); + return; + } + + const data = await response.json(); + if (data.success) { + setAllAnalytics(data.analytics); + setTotalAnalytics(data.pagination.total); + } + } catch { + showToast('An error occurred while loading complete analytics data', 'error'); + } + } + + fetchAllAnalytics(); + }, [link, shortId, showToast]); + + useEffect(() => { + if (!link) return; + + async function fetchPaginatedAnalytics() { + try { + const response = await fetch(`/api/analytics?link_id=${shortId}&page=${page}&limit=${limit}`); + if (!response.ok) { + showToast('Failed to load analytics page', 'error'); + return; + } + + const data = await response.json(); + if (data.success) { + setAnalytics(data.analytics); + } + } catch { + showToast('An error occurred while loading analytics page', 'error'); + } + } + + fetchPaginatedAnalytics(); + }, [link, shortId, page, limit, showToast]); + + useEffect(() => { + if (!link || allAnalytics.length === 0) return; + + async function generateStats() { + setIsLoadingStats(true); + try { + // Browser stats + const browsers = allAnalytics.reduce((acc: Record, item) => { + const browser = item.browser || 'Unknown'; + acc[browser] = (acc[browser] || 0) + 1; + return acc; + }, {}); + + // OS stats + const oses = allAnalytics.reduce((acc: Record, item) => { + const os = item.platform || 'Unknown'; + acc[os] = (acc[os] || 0) + 1; + return acc; + }, {}); + + // Country stats + const countries = allAnalytics.reduce((acc: Record, item) => { + const country = item.country || 'Unknown'; + acc[country] = (acc[country] || 0) + 1; + return acc; + }, {}); + + // IP version stats + const ipVersions = allAnalytics.reduce((acc: Record, item) => { + const ipVersion = item.ip_version || 'Unknown'; + acc[ipVersion] = (acc[ipVersion] || 0) + 1; + return acc; + }, {}); + + // Convert to StatItem[] and sort by count + setBrowserStats(Object.entries(browsers) + .map(([id, count]) => ({ id, count })) + .sort((a, b) => b.count - a.count)); + + setOsStats(Object.entries(oses) + .map(([id, count]) => ({ id, count })) + .sort((a, b) => b.count - a.count)); + + setCountryStats(Object.entries(countries) + .map(([id, count]) => ({ id, count })) + .sort((a, b) => b.count - a.count)); + + setIpVersionStats(Object.entries(ipVersions) + .map(([id, count]) => ({ id, count })) + .sort((a, b) => b.count - a.count)); + } catch { + showToast('An error occurred while processing analytics data', 'error'); + } finally { + setIsLoadingStats(false); + } + } + + generateStats(); + }, [allAnalytics, link, showToast]); + + const handlePageChange = (newPage: number) => { + setPage(newPage); + }; + + const handleEditLink = async () => { + if (!isValidUrl(targetUrl)) { + showToast('Please enter a valid URL', 'error'); + return; + } + + try { + const response = await fetch(`/api/link`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + shortId: shortId, + target_url: targetUrl + }), + }); + + const data = await response.json(); + + if (data.success) { + showToast('Link updated successfully', 'success'); + setIsEditing(false); + if (link) { + setLink({ + ...link, + target_url: targetUrl, + last_modified: new Date() + }); + } + } else { + showToast(data.message || 'Failed to update link', 'error'); + } + } catch { + showToast('An error occurred while updating the link', 'error'); + } + }; + + const handleDeleteAnalytics = async () => { + try { + const response = await fetch('/api/analytics', { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + link_id: shortId, + analytics_id: analyticsToDelete + }), + }); + + const data = await response.json(); + + if (data.success) { + showToast('Analytics entry deleted successfully', 'success'); + + setAnalytics(analytics.filter(item => item._id?.toString() !== analyticsToDelete)); + + setTotalAnalytics(prev => prev - 1); + } else { + showToast(data.message || 'Failed to delete analytics entry', 'error'); + } + } catch { + showToast('An error occurred while deleting the analytics entry', 'error'); + } finally { + setShowDeleteModal(false); + setAnalyticsToDelete(''); + } + }; + + const handleDeleteAllAnalytics = async () => { + try { + const response = await fetch('/api/analytics', { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + link_id: shortId, + delete_all: true + }), + }); + + const data = await response.json(); + + if (data.success) { + showToast('All analytics entries deleted successfully', 'success'); + + setAnalytics([]); + setTotalAnalytics(0); + setBrowserStats([]); + setOsStats([]); + setCountryStats([]); + setIpVersionStats([]); + } else { + showToast(data.message || 'Failed to delete all analytics entries', 'error'); + } + } catch { + showToast('An error occurred while deleting all analytics entries', 'error'); + } finally { + setShowDeleteAllModal(false); + } + }; + + if (isLoading) { + return ( +
+
+

Loading link details...

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

Link Details

+ + Back to Dashboard + +
+ +
+
+

Link Information

+
+
+ Short ID: + {shortId} +
+ +
+ + +
+ +
+ Created: + + {link ? new Date(link.created_at).toLocaleString() : ''} + +
+ +
+ Last Modified: + + {link ? new Date(link.last_modified).toLocaleString() : ''} + +
+ +
+
+ Target URL: + {!isEditing && ( + + )} +
+ + {isEditing ? ( +
{ + e.preventDefault(); + handleEditLink(); + }} + className={styles.editForm} + > + setTargetUrl(e.target.value)} + className={styles.urlInput} + placeholder="https://example.com" + /> +
+ + +
+
+ ) : ( + + {link?.target_url} + + )} +
+
+
+
+ +
+
+

Analytics

+ + Total Clicks: {totalAnalytics} + + {totalAnalytics > 0 && ( + + )} +
+ + {totalAnalytics > 0 ? ( + <> +
+
+

Browsers

+ +
+ +
+

Operating Systems

+ +
+ +
+

Countries

+ +
+ +
+

IP Versions

+ +
+
+ + { + setAnalyticsToDelete(id); + setShowDeleteModal(true); + }} + /> + + ) : ( +
+

No clicks recorded yet for this link.

+
+ )} +
+ + {/* Confirm Delete Modal */} + { + setShowDeleteModal(false); + setAnalyticsToDelete(''); + }} + /> + + {/* Confirm Delete All Modal */} + setShowDeleteAllModal(false)} + /> +
+ ); +} \ No newline at end of file diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx new file mode 100644 index 0000000..d70f118 --- /dev/null +++ b/src/app/dashboard/page.tsx @@ -0,0 +1,164 @@ +"use client"; + +import { useState, useEffect } from 'react'; +import { useSession } from 'next-auth/react'; +import { useRouter } from 'next/navigation'; +import Link from 'next/link'; +import LinkTable from '@/components/ui/dashboard/LinkTable'; +import styles from './Dashboard.module.css'; +import { useToast } from '@/contexts/ToastContext'; + +export default function Dashboard() { + const { data: session, status } = useSession(); + const router = useRouter(); + const { showToast } = useToast(); + const [links, setLinks] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (status === "unauthenticated") { + router.push('/'); + } + }, [status, router]); + + useEffect(() => { + if (session?.user?.accountId) { + fetchLinks(); + } + }, [session]); + + const fetchLinks = async () => { + try { + setLoading(true); + const response = await fetch('/api/links'); + + if (!response.ok) { + throw new Error('Failed to fetch links'); + } + + const data = await response.json(); + if (data.success) { + setLinks(data.links); + } else { + showToast(data.message || 'Failed to load links', 'error'); + } + } catch (error) { + console.error('Error fetching links:', error); + showToast('Failed to load links', 'error'); + } finally { + setLoading(false); + } + }; + + if (status === "loading") { + return
Loading...
; + } + + if (status === "unauthenticated") { + return null; + } + + return ( +
+
+

Dashboard

+
+ + + + + {session?.user?.isAdmin && ( + + + + )} +
+
+ +
+

Create New Short Link

+ +
+ +
+

Your Shortened Links

+ {loading ? ( +
Loading your links...
+ ) : links.length === 0 ? ( +
+ You haven't created any links yet. Create your first short link above! +
+ ) : ( + + )} +
+
+ ); +} + +interface CreateLinkFormProps { + onLinkCreated: () => void; +} + +function CreateLinkForm({ onLinkCreated }: CreateLinkFormProps) { + const [url, setUrl] = useState(''); + const [creating, setCreating] = useState(false); + const { showToast } = useToast(); + + const handleSubmit = async (e: { preventDefault: () => void; }) => { + e.preventDefault(); + + if (!url.trim()) { + showToast('Please enter a URL', 'error'); + return; + } + + try { + setCreating(true); + const response = await fetch('/api/link', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ target_url: url }), + }); + + const data = await response.json(); + + if (response.ok && data.success) { + showToast('Link created successfully!', 'success'); + setUrl(''); + if (onLinkCreated) onLinkCreated(); + } else { + showToast(data.message || 'Failed to create link', 'error'); + } + } catch (error) { + console.error('Error creating link:', error); + showToast('Failed to create link', 'error'); + } finally { + setCreating(false); + } + }; + + return ( +
+
+ setUrl(e.target.value)} + placeholder="Enter URL to shorten" + className={styles.urlInput} + required + /> + +
+
+ ); +} \ No newline at end of file diff --git a/src/app/dashboard/security/Security.module.css b/src/app/dashboard/security/Security.module.css new file mode 100644 index 0000000..2c95f57 --- /dev/null +++ b/src/app/dashboard/security/Security.module.css @@ -0,0 +1,120 @@ +.container { + max-width: 75rem; + width: 100%; + margin: 0 auto; + padding: 2rem 1rem; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2rem; + color: var(--text-primary); + margin: 0; +} + +.backLink { + padding: 0.5rem 1rem; + border-radius: 4px; + background-color: var(--accent); + color: var(--text-primary); + text-decoration: none; + font-weight: 500; + transition: all 0.2s ease; +} + +.backLink:hover { + opacity: 0.9; + transform: translateY(-2px); +} + +.content { + display: flex; + flex-direction: column; + gap: 2rem; +} + +.sessionSection, +.dangerSection { + background-color: var(--card-bg); + border-radius: 8px; + padding: 1.5rem; + border: 1px solid var(--border-color); +} + +.sessionSection h2, +.dangerSection h2 { + font-size: 1.6rem; + margin: 0 0 1.5rem 0; + padding-bottom: 0.75rem; + border-bottom: 1px solid var(--border-color); + font-weight: 600; +} + +.dangerSection h2 { + color: #dc3545; + border-bottom-color: rgba(220, 53, 69, 0.3); +} + +.sessionSection { + margin-bottom: 1rem; +} + +.dangerCard { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1.5rem; + background-color: rgba(220, 53, 69, 0.05); + border: 1px solid rgba(220, 53, 69, 0.2); + border-radius: 4px; +} + +.dangerInfo h3 { + margin-top: 0; + color: #dc3545; +} + +.dangerInfo p { + color: var(--text-secondary); + margin-bottom: 0; +} + +.deleteAccountBtn { + padding: 0.5rem 1rem; + background-color: #dc3545; + color: white; + border: none; + border-radius: 4px; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s ease; +} + +.deleteAccountBtn:hover { + background-color: #c82333; +} + +/* Responsive styles */ +@media (max-width: 768px) { + .header { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + } + + .dangerCard { + flex-direction: column; + gap: 1.5rem; + align-items: flex-start; + } + + .deleteAccountBtn { + width: 100%; + } +} \ No newline at end of file diff --git a/src/app/dashboard/security/page.tsx b/src/app/dashboard/security/page.tsx new file mode 100644 index 0000000..fb34cc8 --- /dev/null +++ b/src/app/dashboard/security/page.tsx @@ -0,0 +1,96 @@ +"use client"; + +import { useState } from 'react'; +import { useSession, signOut } from 'next-auth/react'; +import { useRouter } from 'next/navigation'; +import Link from 'next/link'; +import { useToast } from '@/contexts/ToastContext'; +import SessionManager from '@/components/ui/dashboard/SessionManager'; +import ConfirmModal from '@/components/ui/ConfirmModal'; +import styles from './Security.module.css'; + +export default function SecurityPage() { + const router = useRouter(); + const { data: session } = useSession(); + const { showToast } = useToast(); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + + const handleAccountDeletion = async () => { + try { + setIsDeleting(true); + + const response = await fetch('/api/auth/remove', { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ account_id: session?.user?.accountId }), + }); + + const data = await response.json(); + + if (response.ok && data.success) { + showToast('Account deleted successfully', 'success'); + + await signOut({ redirect: false }); + router.push('/'); + } else { + showToast(data.message || 'Failed to delete account', 'error'); + setIsDeleteModalOpen(false); + } + } catch (error) { + console.error('Error deleting account:', error); + showToast('Failed to delete account', 'error'); + setIsDeleteModalOpen(false); + } finally { + setIsDeleting(false); + } + }; + + return ( +
+
+

Security Settings

+ + Back to Dashboard + +
+ +
+
+ +
+ +
+

Danger Zone

+
+
+

Delete Account

+

+ This will permanently delete your account and all associated data. + This action cannot be undone. +

+
+ +
+
+
+ + setIsDeleteModalOpen(false)} + /> +
+ ); +} \ No newline at end of file diff --git a/src/app/favicon.ico b/src/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..ff8fc09740fe852a7bee90f95fcb96db1c4ac24f GIT binary patch literal 41662 zcmeI5OOF&)6vyu{gdu7e9^#VbnYbXXhz1t+1UEYedQTPP33ltwHF&bAU6QT(*nhc7`Bu=LMom*Aicbe|1d%LS{Kk6Plbe+1V&g1{D z>Y}P{mrBj>=j2Jn-#gW(pD6X5QtEk>Rq8G(>qQnmRBF!)vFa7!uYf>6ARrJB2nYlO z0s;YnfIvVXAP^7;2$Y0C6h*V>OXw-IfnG(gqnFWh=uvc2N$Eq_1OYHUg5E%v&^-AM zhdbz5^r--1aR`8?@Nf?uRQ_PY3+RU8s;XWj@LP|*ixwq65NLPxkQ}~aaNCB;_f66A z7XcTBudq51z)QHeUo;U$ynk6fTpdIQ?-ZQM<|3sfaFRolM=k;<<#F*l;C_XEf&PN7 zpe_=QlP+!mZv*Ob?*1G9OX%LNP>OY1&=aU|V=I9Z14ux7-xfIhenR&doA+GAXCwMC zYAb;gkOVBbw_z*tZ?0`U3FxyD|0}k^@vA&1VDOzKUc$w*ePGXA&35lt<`6gGjxp^vJ%5hoiJUK-zN7l#MjRJTZuW~ajqAZi?oB;H zvqZ*o&)qKg5YLvAM=o(E-4|RAW)YE$T`Z$dOHQiB#~BMgc9p@0#I)d~YTs9H&4=$wWW<1(O9-da#wDI3dYi+eAb7O*$us>D>z_~Qkw6*%#Uwq35ki5*Tpy#gmb(YDJ4C#7C9 z5`PYrYeczLlxs%0cHCoEUB`=Us&vih%E?FIlY0%0&puS{opg&_pC0OAt->1mE$l2~~^K7;WoB%?G$UI2l<~AzNZXckwaxf47UgHE0 zoY*4l=6Ado-Ztw3CvHGndOz_#0vj$;p8+^oNVlD|PkMDd$6Hd+cDq#sCXSrQoR6JF zUoEqK;@ff}?}L20%vyZJrU#iXEOWT!Iy>*T4!A@zr{ObH z-Xk&NsYY8)&ibgwR}S#qj2=VJqw=g(-rsu-ZK7vTnd`9LS3PDP^2+^#Jh@|T6PC+E z03zi3HShgh;rD#yQSd}$_&9;)$O+)b1A`Mm0*j&aEW2K&)| zb0dfP{fSMFM>59#WLO`K`*q3N4x<9$9$zSu=a2J%F~*1DggC;*eIs7h;*n9|GRnLF;^nsv~b>KPw(3ro-k24_8hE&tc}=$@U_0o-gFR>BC|$Zrg*)Ae;!MgG%5cXD0wy zUz`Lka=1u`J#Z4ZNWr%Jgp)Go+=cUnB@d<>oa{htujO#f6`AW2{vBn4?6?em6EWw` zv8{=BhnKORb2srI57+?QY(~d&&fS15h0Ov20fB%(Kp-Fx5C{ka1Ofs9fq+0jV1f|f znzSuT)ZI2%7?G{&NfB}W6(uk25sB?3H4~eO-D(#dk2jTS#164dQ+2~$#jY9l3iiBV zFJmVO_4Z3@hmyB3BZ=K;CIx7FQAPaM8DE^iuhFqvv(a)=fc9_AMJuM=oR3!5*tIC? zwwv`%JXvma*^7;}}IX)PyH0E5sCX@5zN}-^ba5! z0X;6@v|+&6omhuP}s7mUw&>cCaO(9*-Dm&g*?3wwtry(UJmUyEPLn zHZ%pXt=Z8O#I_!iEQ!A!k}Qd>ha^j4>!FA&iLHksvZS^Q>3txMuZI|Gq~jE@*pMvA z_>!u(>XId~m({$gNtVQxJ+dZQ5K6)({xmxf(21wPX>{ymRWtD8r(9~T*}+r5QL2MC&0u% logger.error('Failed to save analytics', { error: err, shortId })); + + return NextResponse.redirect(new URL(link.target_url)); + } catch (error) { + logger.error('Link redirection error', { error }); + return NextResponse.redirect(new URL('/error', req.url)); + } +} \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..7e6de4e --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,52 @@ +import Providers from '@/components/Providers'; +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import Header from "@/components/header"; +import Footer from "@/components/footer"; +import { ToastProvider } from '@/contexts/ToastContext'; +import Toast from '@/components/ui/Toast'; +import ResponsiveLayout from '@/components/ResponsiveLayout'; +import SessionMonitor from '@/components/SessionMonitor'; +import "./globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "µLinkShortener", + description: "Create short links and see who accessed them!", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + + +
+
+ {children} +
+