diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..76cc0ca --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +Dockerfile +.dockerignore +node_modules +npm-debug.log +README.md +.next +.git +.env* +*.db +dev.db +prisma/*.db +build_log.txt diff --git a/.gitignore b/.gitignore index 5ef6a52..10e3511 100644 --- a/.gitignore +++ b/.gitignore @@ -33,9 +33,16 @@ yarn-error.log* # env files (can opt-in for committing if needed) .env* +# database +*.db +dev.db +prisma/*.db + # vercel .vercel # typescript *.tsbuildinfo next-env.d.ts + +/src/generated/prisma diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2eee913 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,45 @@ +FROM node:20-bullseye-slim AS base +RUN apt-get update && apt-get install -y openssl && rm -rf /var/lib/apt/lists/* + +FROM base AS deps +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci + +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +ARG GOOGLE_MAPS_API_KEY +ENV GOOGLE_MAPS_API_KEY=$GOOGLE_MAPS_API_KEY + +RUN npx prisma generate +RUN npm run build + +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV production +ENV PORT 3000 +ENV HOSTNAME "0.0.0.0" + +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static +COPY --from=builder /app/prisma ./prisma +COPY --from=builder /app/package.json ./package.json + +COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma +COPY --from=builder /app/node_modules/@prisma ./node_modules/@prisma + +RUN chown -R nextjs:nodejs /app + +USER nextjs + +EXPOSE 3000 + +CMD ["sh", "-c", "npx prisma@5.22.0 db push && node server.js"] \ No newline at end of file diff --git a/next.config.ts b/next.config.ts index e9ffa30..c2dba98 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,29 @@ import type { NextConfig } from "next"; +const securityHeaders = [ + { key: "X-Content-Type-Options", value: "nosniff" }, + { key: "X-Frame-Options", value: "SAMEORIGIN" }, + { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }, + { + key: "Permissions-Policy", + value: "camera=(), microphone=(), geolocation=()", + }, + { + key: "Strict-Transport-Security", + value: "max-age=63072000; includeSubDomains; preload", + }, +]; + const nextConfig: NextConfig = { - /* config options here */ + output: "standalone", + async headers() { + return [ + { + source: "/(.*)", + headers: securityHeaders, + }, + ]; + }, }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 4c8b9ad..3a50314 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,16 +8,27 @@ "name": "portfolio_gsv", "version": "0.1.0", "dependencies": { + "@googlemaps/js-api-loader": "^2.0.2", + "@googlemaps/markerclusterer": "^2.6.2", + "@tmcw/togeojson": "^7.1.2", + "@types/geojson": "^7946.0.16", + "bootstrap": "^5.3.8", + "googleapis": "^171.4.0", + "jose": "^6.2.3", "next": "16.1.6", "react": "19.2.3", - "react-dom": "19.2.3" + "react-dom": "19.2.3", + "zod": "^4.4.3" }, "devDependencies": { + "@prisma/client": "^5.22.0", + "@types/google.maps": "^3.58.1", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "16.1.6", + "prisma": "^5.22.0", "typescript": "^5" } }, @@ -439,6 +450,26 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@googlemaps/js-api-loader": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@googlemaps/js-api-loader/-/js-api-loader-2.0.2.tgz", + "integrity": "sha512-bKVuTqatS8Jven5aFqVB7rCHF1VFEzpzyi0ruzO0GUR+A7m9oMqMgtnmpANj7kMYEvvhty8Fk7TnJ1MKjWHu+Q==", + "license": "Apache-2.0", + "dependencies": { + "@types/google.maps": "^3.53.1" + } + }, + "node_modules/@googlemaps/markerclusterer": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@googlemaps/markerclusterer/-/markerclusterer-2.6.2.tgz", + "integrity": "sha512-U6uVhq8iWhiIckA89sgRu8OK35mjd6/3CuoZKWakKEf0QmRRWpatlsPb3kqXkoWSmbcZkopRiI4dnW6DQSd7bQ==", + "license": "Apache-2.0", + "dependencies": { + "@types/supercluster": "^7.1.3", + "fast-equals": "^5.2.2", + "supercluster": "^8.0.1" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -957,6 +988,23 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1212,6 +1260,96 @@ "node": ">=12.4.0" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@prisma/client": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1228,6 +1366,15 @@ "tslib": "^2.8.0" } }, + "node_modules/@tmcw/togeojson": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@tmcw/togeojson/-/togeojson-7.1.2.tgz", + "integrity": "sha512-QKnFs9DAuqqBVj4d6c69tV1Dj2TspSBTqffivoN0YoBCVdP/JY1+WaYCJbzU49RkoU5NOSOJ3jtFHCdEUVh21A==", + "license": "BSD-2-Clause", + "engines": { + "node": "*" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -1246,6 +1393,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/google.maps": { + "version": "3.58.1", + "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.58.1.tgz", + "integrity": "sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==", + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1291,6 +1450,15 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/supercluster": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz", + "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.56.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz", @@ -1867,6 +2035,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", @@ -1884,11 +2061,22 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -2137,6 +2325,26 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", @@ -2149,6 +2357,34 @@ "node": ">=6.0.0" } }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bootstrap": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz", + "integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT", + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -2208,6 +2444,12 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -2231,7 +2473,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -2245,7 +2486,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -2315,7 +2555,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -2328,7 +2567,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/concat-map": { @@ -2349,7 +2587,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -2374,6 +2611,15 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -2432,7 +2678,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2516,7 +2761,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -2527,6 +2771,21 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.302", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", @@ -2538,7 +2797,6 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, "license": "MIT" }, "node_modules/es-abstract": { @@ -2614,7 +2872,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2624,7 +2881,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2662,7 +2918,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -2933,6 +3188,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -3172,6 +3428,12 @@ "node": ">=0.10.0" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3179,6 +3441,15 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-equals": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -3233,6 +3504,29 @@ "reusify": "^1.0.4" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -3313,11 +3607,53 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3354,6 +3690,35 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/generator-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", @@ -3378,7 +3743,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -3403,7 +3767,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -3444,6 +3807,27 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3457,6 +3841,42 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.6.tgz", + "integrity": "sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -3487,11 +3907,66 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/google-auth-library": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", + "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^8.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/googleapis": { + "version": "171.4.0", + "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-171.4.0.tgz", + "integrity": "sha512-xybFL2SmmUgIifgsbsRQYRdNrSAYwxWZDmkZTGjUIaRnX5jPqR8el/cEvo6rCqh7iaZx6MfEPS/lrDgZ0bymkg==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.2.0", + "googleapis-common": "^8.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/googleapis-common": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-8.0.1.tgz", + "integrity": "sha512-eCzNACUXPb1PW5l0ULTzMHaL/ltPRADoPgjBlT8jWsTbxkCp6siv+qKJ/1ldaybCthGwsYFYallF7u9AkU4L+A==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^7.0.0-rc.4", + "google-auth-library": "^10.1.0", + "qs": "^6.7.0", + "url-template": "^2.0.8" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3500,6 +3975,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "license": "MIT", + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -3556,7 +4044,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3585,7 +4072,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -3611,6 +4097,19 @@ "hermes-estree": "0.25.1" } }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3847,6 +4346,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -4089,7 +4597,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/iterator.prototype": { @@ -4110,6 +4617,30 @@ "node": ">= 0.4" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4143,6 +4674,15 @@ "node": ">=6" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -4193,6 +4733,33 @@ "node": ">=4.0" } }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kdbush": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz", + "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==", + "license": "ISC" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4287,7 +4854,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4340,11 +4906,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -4441,6 +5015,26 @@ } } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-exports-info": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", @@ -4460,6 +5054,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -4481,7 +5093,6 @@ "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4658,6 +5269,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -4685,7 +5302,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4698,6 +5314,28 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -4765,6 +5403,27 @@ "node": ">= 0.8.0" } }, + "node_modules/prisma": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@prisma/engines": "5.22.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -4787,6 +5446,21 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -4934,6 +5608,21 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -4978,6 +5667,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -5140,7 +5849,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -5153,7 +5861,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5163,7 +5870,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5183,7 +5889,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5200,7 +5905,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -5219,7 +5923,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -5235,6 +5938,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5265,6 +5980,65 @@ "node": ">= 0.4" } }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -5378,6 +6152,43 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -5424,6 +6235,15 @@ } } }, + "node_modules/supercluster": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz", + "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==", + "license": "ISC", + "dependencies": { + "kdbush": "^4.0.2" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -5789,11 +6609,25 @@ "punycode": "^2.1.0" } }, + "node_modules/url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", + "license": "BSD" + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -5904,6 +6738,94 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -5925,10 +6847,9 @@ } }, "node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "dev": true, + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", "peer": true, "funding": { diff --git a/package.json b/package.json index d466368..942c8a8 100644 --- a/package.json +++ b/package.json @@ -4,21 +4,32 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build", + "build": "npx prisma generate && next build", "start": "next start", "lint": "eslint" }, "dependencies": { + "@googlemaps/js-api-loader": "^2.0.2", + "@googlemaps/markerclusterer": "^2.6.2", + "@tmcw/togeojson": "^7.1.2", + "@types/geojson": "^7946.0.16", + "bootstrap": "^5.3.8", + "googleapis": "^171.4.0", + "jose": "^6.2.3", "next": "16.1.6", "react": "19.2.3", - "react-dom": "19.2.3" + "react-dom": "19.2.3", + "zod": "^4.4.3" }, "devDependencies": { + "@prisma/client": "^5.22.0", + "@types/google.maps": "^3.58.1", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "16.1.6", + "prisma": "^5.22.0", "typescript": "^5" } } diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..d3f26f1 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,21 @@ +generator client { + provider = "prisma-client-js" + binaryTargets = ["native", "debian-openssl-1.1.x", "debian-openssl-3.0.x"] +} + +datasource db { + provider = "sqlite" + url = env("DATABASE_URL") +} + +model Location { + id String @id @default(uuid()) + title String + city String + tags String + captureDate DateTime + visibility String @default("public") // 'public' or 'unlisted' + gpxData String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} diff --git a/src/app/admin/edit/[id]/page.tsx b/src/app/admin/edit/[id]/page.tsx new file mode 100644 index 0000000..b6bfff1 --- /dev/null +++ b/src/app/admin/edit/[id]/page.tsx @@ -0,0 +1,20 @@ +import AdminLocationForm from "@/components/AdminLocationForm"; + +export default async function EditLocationPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + + return ( +
+
+
Edit Location
+
+
+ +
+
+ ); +} diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx new file mode 100644 index 0000000..e445d18 --- /dev/null +++ b/src/app/admin/layout.tsx @@ -0,0 +1,80 @@ +"use client"; + +import Link from "next/link"; +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; + +export default function AdminLayout({ + children, +}: { + children: React.ReactNode; +}) { + const router = useRouter(); + + const handleLogout = async () => { + try { + await fetch("/api/auth/logout", { method: "POST" }); + router.push("/login"); + router.refresh(); + } catch (e) { + console.error("Logout failed", e); + } + }; + + const closeAdminSidebar = () => { + if (typeof window !== 'undefined' && typeof document !== 'undefined') { + const offcanvasEl = document.getElementById('adminSidebar'); + if (offcanvasEl && (window as any).bootstrap) { + try { + const bsOffcanvas = (window as any).bootstrap.Offcanvas.getInstance(offcanvasEl) || new (window as any).bootstrap.Offcanvas(offcanvasEl); + if (bsOffcanvas) { + bsOffcanvas.hide(); + } + } catch (e) { + console.error('Error closing admin offcanvas:', e); + offcanvasEl.classList.remove('show', 'showing'); + } + } + } + }; + return ( +
+ {/* Mobile Header with Toggle Button */} +
+ Admin Panel + +
+ + {/* Sidebar (Offcanvas on mobile, visible on desktop) */} +
+
+
Admin Panel
+ +
+ + Admin Panel + +
+ +
+ + {/* Main Content Area */} +
+
{children}
+
+
+ ); +} diff --git a/src/app/admin/new/page.tsx b/src/app/admin/new/page.tsx new file mode 100644 index 0000000..8801009 --- /dev/null +++ b/src/app/admin/new/page.tsx @@ -0,0 +1,14 @@ +import AdminLocationForm from "@/components/AdminLocationForm"; + +export default function NewLocationPage() { + return ( +
+
+
Add New Location
+
+
+ +
+
+ ); +} diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx new file mode 100644 index 0000000..8150800 --- /dev/null +++ b/src/app/admin/page.tsx @@ -0,0 +1,118 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Link from "next/link"; +import { Location } from "@prisma/client"; + +export default function AdminDashboard() { + const [locations, setLocations] = useState([]); + const [loading, setLoading] = useState(true); + + const fetchLocations = async () => { + try { + const res = await fetch("/api/admin/locations"); + if (res.ok) { + const data = await res.json(); + setLocations(data); + } + } catch (error) { + console.error("Failed to fetch locations", error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchLocations(); + }, []); + + const handleDelete = async (id: string) => { + if (!confirm("Are you sure you want to delete this location?")) return; + try { + const res = await fetch(`/api/admin/locations/${id}`, { + method: "DELETE", + }); + if (res.ok) { + fetchLocations(); + } else { + alert("Failed to delete"); + } + } catch (error) { + console.error(error); + } + }; + + if (loading) return
Loading...
; + + return ( + <> +
+
+
Manage Locations (Projects)
+ + + Add New Project + +
+
+
+ + + + + + + + + + + + {locations.map((loc) => ( + + + + + + + + ))} + {locations.length === 0 && ( + + + + )} + +
TitleCityGPX RouteVisibilityActions
{loc.title}{loc.city} + {loc.gpxData ? ( + Has Route + ) : ( + No Route + )} + + + {loc.visibility} + + + + Edit + + +
+ No locations found. Start by adding one. +
+
+
+
+ + ); +} diff --git a/src/app/api/admin/locations/[id]/route.ts b/src/app/api/admin/locations/[id]/route.ts new file mode 100644 index 0000000..548eb3d --- /dev/null +++ b/src/app/api/admin/locations/[id]/route.ts @@ -0,0 +1,72 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; +import { requireAdmin } from "@/lib/auth"; +import { parseLocationBody } from "@/lib/validation"; + +export const dynamic = "force-dynamic"; + +export async function PUT( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + const auth = await requireAdmin(); + if (!auth.authorized) return auth.response; + + try { + const { id } = await params; + const body = await request.json(); + const parsed = parseLocationBody(body); + + if (!parsed.success) { + return NextResponse.json( + { error: parsed.error.issues[0]?.message ?? "Invalid input" }, + { status: 400 } + ); + } + + const data = parsed.data; + const location = await prisma.location.update({ + where: { id }, + data: { + title: data.title, + city: data.city, + gpxData: data.gpxData ?? null, + tags: data.tags, + captureDate: new Date(data.captureDate), + visibility: data.visibility, + }, + }); + + return NextResponse.json(location); + } catch (error) { + console.error("Error updating location:", error); + return NextResponse.json( + { error: "Failed to update location" }, + { status: 500 } + ); + } +} + +export async function DELETE( + _request: Request, + { params }: { params: Promise<{ id: string }> } +) { + const auth = await requireAdmin(); + if (!auth.authorized) return auth.response; + + try { + const { id } = await params; + + await prisma.location.delete({ + where: { id }, + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("Error deleting location:", error); + return NextResponse.json( + { error: "Failed to delete location" }, + { status: 500 } + ); + } +} diff --git a/src/app/api/admin/locations/route.ts b/src/app/api/admin/locations/route.ts new file mode 100644 index 0000000..e986fe7 --- /dev/null +++ b/src/app/api/admin/locations/route.ts @@ -0,0 +1,64 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; +import { requireAdmin } from "@/lib/auth"; +import { parseLocationBody } from "@/lib/validation"; + +export const dynamic = "force-dynamic"; + +export async function GET() { + const auth = await requireAdmin(); + if (!auth.authorized) return auth.response; + + try { + const locations = await prisma.location.findMany({ + orderBy: { + createdAt: "desc", + }, + }); + + return NextResponse.json(locations); + } catch (error) { + console.error("Error fetching all locations:", error); + return NextResponse.json( + { error: "Failed to fetch locations" }, + { status: 500 } + ); + } +} + +export async function POST(request: Request) { + const auth = await requireAdmin(); + if (!auth.authorized) return auth.response; + + try { + const body = await request.json(); + const parsed = parseLocationBody(body); + + if (!parsed.success) { + return NextResponse.json( + { error: parsed.error.issues[0]?.message ?? "Invalid input" }, + { status: 400 } + ); + } + + const data = parsed.data; + const location = await prisma.location.create({ + data: { + title: data.title, + city: data.city, + gpxData: data.gpxData ?? null, + tags: data.tags, + captureDate: new Date(data.captureDate), + visibility: data.visibility, + }, + }); + + return NextResponse.json(location); + } catch (error) { + console.error("Error creating location:", error); + return NextResponse.json( + { error: "Failed to create location" }, + { status: 500 } + ); + } +} diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts new file mode 100644 index 0000000..17f3ab2 --- /dev/null +++ b/src/app/api/auth/login/route.ts @@ -0,0 +1,64 @@ +import { NextResponse } from "next/server"; +import { + createSessionToken, + getSessionCookieOptions, +} from "@/lib/session"; +import { safeCompare } from "@/lib/auth"; +import { checkRateLimit } from "@/lib/rate-limit"; + +function getClientIp(request: Request): string { + const forwarded = request.headers.get("x-forwarded-for"); + if (forwarded) return forwarded.split(",")[0].trim(); + return request.headers.get("x-real-ip") ?? "unknown"; +} + +export async function POST(request: Request) { + try { + const ip = getClientIp(request); + if (!checkRateLimit(`login:${ip}`)) { + return NextResponse.json( + { error: "Too many login attempts. Try again later." }, + { status: 429 } + ); + } + + const { username, password } = await request.json(); + const correctPassword = process.env.ADMIN_PASSWORD; + const correctUsername = process.env.ADMIN_USERNAME; + + if (!correctPassword || !correctUsername) { + console.error( + "ADMIN_PASSWORD and ADMIN_USERNAME environment variables must be set." + ); + return NextResponse.json( + { error: "Server Configuration Error" }, + { status: 500 } + ); + } + + if ( + typeof username !== "string" || + typeof password !== "string" || + !safeCompare(username, correctUsername) || + !safeCompare(password, correctPassword) + ) { + return NextResponse.json( + { error: "Invalid credentials." }, + { status: 401 } + ); + } + + const token = await createSessionToken(); + const response = NextResponse.json({ success: true }); + + response.cookies.set({ + ...getSessionCookieOptions(), + value: token, + }); + + return response; + } catch (error) { + console.error("Login error:", error); + return NextResponse.json({ error: "Invalid request." }, { status: 400 }); + } +} diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts new file mode 100644 index 0000000..a76a306 --- /dev/null +++ b/src/app/api/auth/logout/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from "next/server"; +import { getSessionCookieOptions, SESSION_COOKIE } from "@/lib/session"; + +export async function POST() { + const response = NextResponse.json({ success: true }); + + response.cookies.set({ + ...getSessionCookieOptions(0), + name: SESSION_COOKIE, + value: "", + }); + + return response; +} diff --git a/src/app/api/config/route.ts b/src/app/api/config/route.ts new file mode 100644 index 0000000..b3b6a83 --- /dev/null +++ b/src/app/api/config/route.ts @@ -0,0 +1,9 @@ +import { NextResponse } from "next/server"; + +export const dynamic = "force-dynamic"; + +export async function GET() { + return NextResponse.json({ + apiKey: process.env.GOOGLE_MAPS_API_KEY || "", + }); +} diff --git a/src/app/api/locations/route.ts b/src/app/api/locations/route.ts new file mode 100644 index 0000000..96b631f --- /dev/null +++ b/src/app/api/locations/route.ts @@ -0,0 +1,22 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; + +export const dynamic = "force-dynamic"; + +export async function GET() { + try { + const locations = await prisma.location.findMany({ + where: { + visibility: "public", + }, + orderBy: { + createdAt: "desc", + }, + }); + + return NextResponse.json(locations); + } catch (error) { + console.error("Error fetching public locations:", error); + return NextResponse.json({ error: "Failed to fetch locations" }, { status: 500 }); + } +} diff --git a/src/app/globals.css b/src/app/globals.css index e3734be..1eaf6da 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,42 +1,130 @@ -:root { - --background: #ffffff; - --foreground: #171717; -} - -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } -} - -html, body { - max-width: 100vw; - overflow-x: hidden; -} - -body { - color: var(--foreground); - background: var(--background); - font-family: Arial, Helvetica, sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -* { - box-sizing: border-box; - padding: 0; margin: 0; + padding: 0; + font-family: "Google Sans", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + background-color: #202124; + color: #e8eaed; } -a { - color: inherit; - text-decoration: none; +::-webkit-scrollbar { + width: 8px; + height: 8px; } -@media (prefers-color-scheme: dark) { - html { - color-scheme: dark; +::-webkit-scrollbar-track { + background: #202124; +} + +::-webkit-scrollbar-thumb { + background: #5f6368; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #80868b; +} + +.gsv-navbar { + background-color: #202124; + border-bottom: 1px solid #3c4043; +} + +.gsv-sidebar { + background-color: #202124; + border-right: 1px solid #3c4043; +} + +.gsv-search-container { + border-bottom: 1px solid #3c4043; +} + +.gsv-input { + background-color: #303134; + border: 1px solid #5f6368; + color: #e8eaed; +} + +.gsv-input:focus { + background-color: #303134; + border-color: #8ab4f8; + color: #e8eaed; + box-shadow: 0 0 0 1px #8ab4f8; +} + +.gsv-input::placeholder { + color: #9aa0a6; +} + +select.gsv-input option { + background-color: #303134; + color: #e8eaed; +} + +.gsv-card { + background-color: #303134; + border: 1px solid transparent; + color: #e8eaed; + transition: all 0.2s ease-in-out; + border-radius: 8px; +} + +.gsv-card:hover { + background-color: #3c4043; + transform: translateY(-2px); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); +} + +.gsv-card-active { + background-color: #42474b; + border: 1px solid #8ab4f8; +} + +.gsv-title { + color: #e8eaed; + font-weight: 500; +} + +.gsv-subtitle { + color: #9aa0a6; +} + +.gsv-btn-share { + color: #8ab4f8; + border: 1px solid #5f6368; + background-color: transparent; + transition: all 0.2s ease-in-out; +} + +.gsv-btn-share:hover { + background-color: rgba(138, 180, 248, 0.04); + color: #a8c7fa; + border-color: #8ab4f8; +} + +.gsv-btn-maps { + background-color: #8ab4f8; + color: #202124; + font-weight: 500; + border: none; + transition: all 0.2s ease-in-out; +} + +.gsv-btn-maps:hover { + background-color: #a8c7fa; + color: #202124; +} + +.offcanvas-backdrop { + opacity: 0 !important; + background-color: transparent !important; +} + +.gsv-search-wrapper { + max-width: 150px; +} + +@media (min-width: 768px) { + .gsv-search-wrapper { + max-width: 400px; } -} +} \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 42fc323..31291f8 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,20 +1,11 @@ import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; +import "bootstrap/dist/css/bootstrap.min.css"; import "./globals.css"; - -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); +import BootstrapClient from "@/components/BootstrapClient"; export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Street View Gallery", + description: "Public gallery of Google Street View panoramas", }; export default function RootLayout({ @@ -23,9 +14,12 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - - - {children} + + +
+ {children} +
+ ); diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx new file mode 100644 index 0000000..1c00b37 --- /dev/null +++ b/src/app/login/page.tsx @@ -0,0 +1,122 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; + +export default function LoginPage() { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + const router = useRouter(); + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + setError(""); + setLoading(true); + + try { + // const res = await fetch("/api/auth/login", { + // method: "POST", + // headers: { "Content-Type": "application/json" }, + // body: JSON.stringify({ password }), + // }); + const res = await fetch("/api/auth/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username, password }), + }); + + if (res.ok) { + router.push("/admin"); + router.refresh(); + } else { + const data = await res.json(); + // setError(data.error || "Invalid password"); + setError(data.error || "Invalid credentials"); + } + } catch (err) { + setError("Something went wrong. Please try again."); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+

Admin Portal

+ Portfolio Management +
+
+
+ {error && ( +
+ {error} +
+ )} +
+ + setUsername(e.target.value)} + placeholder="Enter username..." + required + autoFocus + /> +
+ {/* Original secure password input block: +
+ + setPassword(e.target.value)} + placeholder="Enter password..." + required + autoFocus + /> +
+ */} +
+ + setPassword(e.target.value)} + placeholder="Enter password..." + required + /> +
+ {/* Original button block: + + */} + +
+
+
+ +
+
+
+ ); +} diff --git a/src/app/page.module.css b/src/app/page.module.css index 59dea42..8f1b56e 100644 --- a/src/app/page.module.css +++ b/src/app/page.module.css @@ -93,7 +93,6 @@ a.secondary { border-color: var(--button-secondary-border); } -/* Enable hover only on non-touch devices */ @media (hover: hover) and (pointer: fine) { a.primary:hover { background: var(--button-primary-hover); diff --git a/src/app/page.tsx b/src/app/page.tsx index 7b947a2..044f44b 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,66 +1,5 @@ -import Image from "next/image"; -import styles from "./page.module.css"; +import Gallery from "@/components/Gallery"; export default function Home() { - return ( -
-
- Next.js logo -
-

To get started, edit the page.tsx file.

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

-
-
- - Vercel logomark - Deploy Now - - - Documentation - -
-
-
- ); + return ; } diff --git a/src/components/AdminLocationForm.tsx b/src/components/AdminLocationForm.tsx new file mode 100644 index 0000000..0da6ca0 --- /dev/null +++ b/src/components/AdminLocationForm.tsx @@ -0,0 +1,208 @@ +"use client"; + +import { useState, useEffect, useRef } from "react"; +import { useRouter } from "next/navigation"; +import { Location } from "@prisma/client"; +import { MAX_GPX_BYTES } from "@/lib/validation"; + +export default function AdminLocationForm({ + locationId, +}: { + locationId?: string; +}) { + const router = useRouter(); + const [loading, setLoading] = useState(false); + const [fetching, setFetching] = useState(!!locationId); + + const mapRef = useRef(null); + const googleMapObj = useRef(null); + const markerRef = useRef(null); + + const [formData, setFormData] = useState({ + title: "", + city: "", + gpxData: "", + tags: "", + captureDate: new Date().toISOString().split("T")[0], + visibility: "public", + }); + + useEffect(() => { + if (locationId) { + // Fetch entire list and find by ID (simple approach) or create a GET /api/admin/locations/[id] single route. + // Since we don't have a single GET endpoint, let's fetch all and filter. + fetch("/api/admin/locations") + .then((res) => res.json()) + .then((data: Location[]) => { + if (Array.isArray(data)) { + const loc = data.find((l) => l.id === locationId); + if (loc) { + setFormData({ + title: loc.title, + city: loc.city, + gpxData: loc.gpxData || "", + tags: loc.tags, + captureDate: new Date(loc.captureDate).toISOString().split("T")[0], + visibility: loc.visibility, + }); + } + } else { + console.error("API returned non-array data for locations:", data); + } + setFetching(false); + }) + .catch((err) => { + console.error("Error fetching admin locations:", err); + setFetching(false); + }); + } + }, [locationId]); + + const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + if (file.size > MAX_GPX_BYTES) { + alert(`GPX file must be under ${MAX_GPX_BYTES / (1024 * 1024)} MB`); + e.target.value = ""; + return; + } + + const reader = new FileReader(); + reader.onload = (event) => { + const text = event.target?.result as string; + setFormData((prev) => ({ ...prev, gpxData: text })); + }; + reader.readAsText(file); + }; + + const handleChange = ( + e: React.ChangeEvent + ) => { + setFormData((prev) => ({ ...prev, [e.target.name]: e.target.value })); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + + const endpoint = locationId + ? `/api/admin/locations/${locationId}` + : "/api/admin/locations"; + const method = locationId ? "PUT" : "POST"; + + try { + const res = await fetch(endpoint, { + method, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(formData), + }); + + if (res.ok) { + router.push("/admin"); + } else { + const errorData = await res.json().catch(() => ({})); + alert(`Operation failed: ${errorData.error || res.statusText}`); + } + } catch (error: any) { + console.error(error); + alert(`Network error: ${error.message}`); + } finally { + setLoading(false); + } + }; + + if (fetching) return
Loading...
; + + return ( +
+
+ + +
+
+ + +
+
+ + + Upload the .gpx file for this project's journey. + {formData.gpxData && ( +
+ ✓ GPX data loaded ({formData.gpxData.length} bytes) +
+ )} +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ ); +} diff --git a/src/components/BootstrapClient.tsx b/src/components/BootstrapClient.tsx new file mode 100644 index 0000000..97cfaac --- /dev/null +++ b/src/components/BootstrapClient.tsx @@ -0,0 +1,11 @@ +"use client"; + +import { useEffect } from "react"; + +export default function BootstrapClient() { + useEffect(() => { + require("bootstrap/dist/js/bootstrap.bundle.min.js"); + }, []); + + return null; +} diff --git a/src/components/Gallery.tsx b/src/components/Gallery.tsx new file mode 100644 index 0000000..45f3e3d --- /dev/null +++ b/src/components/Gallery.tsx @@ -0,0 +1,545 @@ +"use client"; + +import { useEffect, useState, useRef } from "react"; +import { importLibrary, setOptions } from "@googlemaps/js-api-loader"; +import { Location } from "@prisma/client"; +import { gpx } from "@tmcw/togeojson"; + +export default function Gallery() { + const [locations, setLocations] = useState([]); + const [filteredLocations, setFilteredLocations] = useState([]); + const [selectedLocation, setSelectedLocation] = useState(null); + const [search, setSearch] = useState(""); + const [viewingStreetView, setViewingStreetView] = useState(false); + const [iframeUrl, setIframeUrl] = useState(null); + const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false); + const [thumbnails, setThumbnails] = useState>({}); + const [toastMessage, setToastMessage] = useState(null); + const [totalMiles, setTotalMiles] = useState(0); + const [geometryLoaded, setGeometryLoaded] = useState(false); + const [runtimeApiKey, setRuntimeApiKey] = useState(""); + + const locationsRef = useRef([]); + + const mapRef = useRef(null); + const miniMapRef = useRef(null); + const googleMapObj = useRef(null); + const miniMapObj = useRef(null); + const miniMapMarkerObj = useRef(null); + const svServiceRef = useRef(null); + const activePolylinesRef = useRef<{ id: string; polyline: google.maps.Polyline; bounds: google.maps.LatLngBounds; firstCoord: google.maps.LatLngLiteral }[]>([]); + + // Fetch runtime API key from server (GOOGLE_MAPS_API_KEY env var) + useEffect(() => { + fetch("/api/config") + .then((res) => res.json()) + .then((data) => { + if (data.apiKey) setRuntimeApiKey(data.apiKey); + }) + .catch((err) => console.error("Error fetching config:", err)); + }, []); + + // Fetch locations + useEffect(() => { + const queryParams = new URLSearchParams(window.location.search); + const initialLocId = queryParams.get("locationId"); + + fetch("/api/locations") + .then((res) => res.json()) + .then((data) => { + if (Array.isArray(data)) { + setLocations(data); + locationsRef.current = data; + setFilteredLocations(data); + if (data.length > 0) { + const targetLoc = data.find((l: Location) => l.id === initialLocId) || data[0]; + setSelectedLocation(targetLoc); + } + } else { + console.error("API returned non-array data:", data); + } + }) + .catch((err) => console.error("Error fetching locations:", err)); + }, []); + + // Filter locations + useEffect(() => { + const term = search.toLowerCase(); + setFilteredLocations( + locations.filter((loc) => { + return loc.title.toLowerCase().includes(term) || loc.city.toLowerCase().includes(term) || (loc.tags && loc.tags.toLowerCase().includes(term)); + }) + ); + }, [search, locations]); + + // Load Google Maps API & Initialize + useEffect(() => { + if (!runtimeApiKey) return; + + setOptions({ + key: runtimeApiKey, + v: "weekly", + }); + + importLibrary("maps").then(async () => { + const { Map } = await google.maps.importLibrary("maps") as google.maps.MapsLibrary; + const { AdvancedMarkerElement } = await google.maps.importLibrary("marker") as google.maps.MarkerLibrary; + const { StreetViewPanorama } = await google.maps.importLibrary("streetView") as google.maps.StreetViewLibrary; + await google.maps.importLibrary("geometry"); // For distance calculations + + setGeometryLoaded(true); + + // Initialize Street View Service + svServiceRef.current = new google.maps.StreetViewService(); + + // Initialize Map + googleMapObj.current = new Map(mapRef.current as HTMLElement, { + center: { lat: 20, lng: 0 }, + zoom: 2, + mapId: "STREET_VIEW_GALLERY_MAP", + }); + + // Initialize Mini Map + miniMapObj.current = new Map(miniMapRef.current as HTMLElement, { + center: { lat: 20, lng: 0 }, + zoom: 14, + mapId: "STREET_VIEW_MINI_MAP", + disableDefaultUI: true, // This hides everything (pegman, map types, etc.) + zoomControl: false, + mapTypeControl: false, + streetViewControl: false, + fullscreenControl: false, + clickableIcons: false, + keyboardShortcuts: false + }); + + miniMapMarkerObj.current = new AdvancedMarkerElement({ + map: miniMapObj.current, + position: { lat: 0, lng: 0 }, + }); + + // Intercept Pegman Drops + const defaultStreetView = googleMapObj.current.getStreetView(); + // We must hide the close button since we are intercepting anyway, but just in case + defaultStreetView.setOptions({ enableCloseButton: false }); + + defaultStreetView.addListener("visible_changed", () => { + if (defaultStreetView.getVisible()) { + const pos = defaultStreetView.getPosition(); + // Intercept the drop: immediately hide the buggy WebGL canvas + defaultStreetView.setVisible(false); + + if (pos) { + // const apiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY || ""; + setIframeUrl(`https://www.google.com/maps/embed/v1/streetview?key=${runtimeApiKey}&location=${pos.lat()},${pos.lng()}&heading=0&pitch=0&fov=90`); + setViewingStreetView(true); + + if (miniMapObj.current && miniMapMarkerObj.current) { + miniMapObj.current.setCenter(pos); + miniMapMarkerObj.current.position = pos; + } + } + } + }); + }); + }, [runtimeApiKey]); + + // Draw all GPX routes for filtered locations + useEffect(() => { + if (!googleMapObj.current || typeof window === 'undefined' || !window.google || !geometryLoaded) return; + + // Clear existing routes + activePolylinesRef.current.forEach((p) => p.polyline.setMap(null)); + activePolylinesRef.current = []; + + const bounds = new google.maps.LatLngBounds(); + let hasPoints = false; + let calculatedMeters = 0; + const newThumbnails: Record = {}; + + filteredLocations.forEach((loc) => { + if (!loc.gpxData) return; + + const parser = new DOMParser(); + const xmlDoc = parser.parseFromString(loc.gpxData, "text/xml"); + const geoJson = gpx(xmlDoc); + + let coords: google.maps.LatLngLiteral[] = []; + let locBounds = new google.maps.LatLngBounds(); + + geoJson.features.forEach((feature) => { + if (feature.geometry?.type === "LineString") { + coords = coords.concat(feature.geometry.coordinates.map((c: any) => ({ lat: c[1], lng: c[0] }))); + } else if (feature.geometry?.type === "MultiLineString") { + feature.geometry.coordinates.forEach((line: any) => { + coords = coords.concat(line.map((c: any) => ({ lat: c[1], lng: c[0] }))); + }); + } else if (feature.geometry?.type === "Point") { + coords.push({ lat: feature.geometry.coordinates[1], lng: feature.geometry.coordinates[0] }); + } + }); + + if (coords.length > 0) { + // Calculate distance + calculatedMeters += google.maps.geometry.spherical.computeLength(coords); + + const polyline = new google.maps.Polyline({ + path: coords, + strokeColor: "#0000FF", + strokeOpacity: 0.6, + strokeWeight: 4, + clickable: true, + map: googleMapObj.current, + zIndex: selectedLocation?.id === loc.id ? 10 : 1, // Elevate selected outline + }); + + // Also draw on mini map + const miniPolyline = new google.maps.Polyline({ + path: coords, + strokeColor: "#0000FF", + strokeOpacity: 0.8, + strokeWeight: 4, + map: miniMapObj.current, + zIndex: 1, + clickable: true + }); + + coords.forEach((c) => { + bounds.extend(c); + locBounds.extend(c); + hasPoints = true; + }); + + activePolylinesRef.current.push({ + id: loc.id, + polyline, + bounds: locBounds, + firstCoord: coords[0] + }); + // Add Google Street View click detection (Main Map) + polyline.addListener("click", (clickEvent: google.maps.MapMouseEvent) => { + if (!clickEvent.latLng) return; + const clickPos = clickEvent.latLng; + // const apiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY || ""; + setIframeUrl(`https://www.google.com/maps/embed/v1/streetview?key=${runtimeApiKey}&location=${clickPos.lat()},${clickPos.lng()}&heading=0&pitch=0&fov=90`); + setViewingStreetView(true); + + if (miniMapObj.current && miniMapMarkerObj.current) { + miniMapObj.current.setCenter(clickPos); + miniMapMarkerObj.current.position = clickPos; + } + }); + + // Add Google Street View click detection (Mini Map) + miniPolyline.addListener("click", (clickEvent: google.maps.MapMouseEvent) => { + if (!clickEvent.latLng) return; + const clickPos = clickEvent.latLng; + // const apiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY || ""; + setIframeUrl(`https://www.google.com/maps/embed/v1/streetview?key=${runtimeApiKey}&location=${clickPos.lat()},${clickPos.lng()}&heading=0&pitch=0&fov=90`); + setViewingStreetView(true); + + if (miniMapObj.current && miniMapMarkerObj.current) { + miniMapObj.current.setCenter(clickPos); + miniMapMarkerObj.current.position = clickPos; + } + }); + + // Generate Static Map Thumbnail URL + try { + const maxThumbnailPoints = 150; + const step = Math.max(1, Math.floor(coords.length / maxThumbnailPoints)); + const simplifiedCoords = coords.filter((_, i) => i % step === 0); + const encodedPath = google.maps.geometry.encoding.encodePath(simplifiedCoords); + // const apiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY || ""; + // Requesting a high-res (scale=2) map, and a slightly taller size so the Google logo sits proportionally smaller at the bottom right. + newThumbnails[loc.id] = `https://maps.googleapis.com/maps/api/staticmap?size=600x250&scale=2&path=weight:3%7Ccolor:blue%7Cenc:${encodedPath}&key=${runtimeApiKey}`; + } catch (err) { + console.error("Failed to generate static map thumbnail", err); + } + } + }); + + setThumbnails(newThumbnails); + + // Update stats + setTotalMiles(calculatedMeters * 0.000621371); + + // Fit bounds for all visible filtered locations + if (hasPoints) { + googleMapObj.current.fitBounds(bounds); + } + + }, [filteredLocations, geometryLoaded, selectedLocation]); // Note: dependency on selectedLocation is REMOVED to prevent redrawing all routes! + + // Handle Selection Highlights and panning smoothly + useEffect(() => { + if (!googleMapObj.current || !geometryLoaded) return; + + let didPan = false; + + activePolylinesRef.current.forEach((item) => { + if (selectedLocation?.id === item.id) { + item.polyline.setOptions({ strokeColor: "#00FF00", strokeOpacity: 1.0, strokeWeight: 6, zIndex: 10 }); + // Smoothly fit bounds exactly like the initial filtered view + if (!didPan) { + googleMapObj.current!.fitBounds(item.bounds); + didPan = true; + } + } else { + item.polyline.setOptions({ strokeColor: "#0000FF", strokeOpacity: 0.6, strokeWeight: 4, zIndex: 1 }); + } + }); + }, [selectedLocation, geometryLoaded]); + + // Update URL when selection changes + useEffect(() => { + if (!selectedLocation) return; + if (typeof window !== "undefined") { + const url = new URL(window.location.href); + url.searchParams.set("locationId", selectedLocation.id); + window.history.pushState({}, "", url.toString()); + } + }, [selectedLocation]); + + + + const closeMobileSidebar = () => { + setIsMobileSidebarOpen(false); + }; + + const handleLocationSelect = (loc: Location) => { + // Switch to map view to show route, not street view directly + setViewingStreetView(false); + setIframeUrl(null); + + // If clicking the currently selected location, force a map pan/zoom because React state won't trigger the effect + if (selectedLocation?.id === loc.id) { + const item = activePolylinesRef.current.find(p => p.id === loc.id); + if (item && googleMapObj.current) { + googleMapObj.current.fitBounds(item.bounds); + } + } else { + setSelectedLocation(loc); + } + + // Auto-close standard Bootstrap offcanvas on mobile natively + closeMobileSidebar(); + }; + + return ( +
+ + +
+ {/* Left: List */} +
+
+
Locations
+ +
+ +
+
+ {filteredLocations.map((loc) => ( +
handleLocationSelect(loc)} + style={{ cursor: "pointer", overflow: "hidden" }} + > + {/* --- MOBILE LAYOUT (Horizontal) --- */} +
+ {/* Thumbnail Map (Left) */} +
+ {thumbnails[loc.id] ? ( + Route thumbnail + ) : ( +
+ Loading... +
+ )} +
+ {/* Content (Right) */} +
+
{loc.title}
+

+ {loc.city}
{new Date(loc.captureDate).toLocaleDateString()} +

+
+ {loc.tags && ( + {loc.tags} + )} + {!loc.gpxData && ( + No Route + )} +
+
+
+ + {/* --- DESKTOP LAYOUT (Vertical/Original) --- */} +
+ {/* Thumbnail Map (Top) */} +
+ {thumbnails[loc.id] ? ( + Route thumbnail + ) : ( + Loading Map... + )} +
+ {/* Content (Bottom) */} +
+
{loc.title}
+

+ {loc.city} • {new Date(loc.captureDate).toLocaleDateString()} +

+
+ {loc.tags} +
+
+ {!loc.gpxData && ( + No Route Data + )} +
+
+
+
+ ))} + {filteredLocations.length === 0 && ( +
No locations found.
+ )} + + {/* Oldest date indicator at the bottom */} + {locations.length > 0 && ( +
+ Images dating back to { + new Date(Math.min(...locations.map(l => new Date(l.captureDate).getTime()))).getFullYear() + } +
+ )} +
+
+
+ {/* Main Panel (Map or Street View) */} +
+
+ + {/* Overlay controls and mini-map, only shown when in Street View mode */} + {viewingStreetView && iframeUrl && ( + <> +
+ +
+ + + )} + +
+
+
+ + {/* Manual React Backdrop for Mobile Sidebar */} + {isMobileSidebarOpen && ( +
setIsMobileSidebarOpen(false)} + /> + )} +
+ ); +} diff --git a/src/lib/auth.ts b/src/lib/auth.ts new file mode 100644 index 0000000..0d4afee --- /dev/null +++ b/src/lib/auth.ts @@ -0,0 +1,27 @@ +import { timingSafeEqual } from "crypto"; +import { cookies } from "next/headers"; +import { NextResponse } from "next/server"; +import { SESSION_COOKIE, verifySessionToken } from "@/lib/session"; + +export function safeCompare(a: string, b: string): boolean { + const bufA = Buffer.from(a); + const bufB = Buffer.from(b); + if (bufA.length !== bufB.length) return false; + return timingSafeEqual(bufA, bufB); +} + +export async function requireAdmin(): Promise< + { authorized: true } | { authorized: false; response: NextResponse } +> { + const cookieStore = await cookies(); + const token = cookieStore.get(SESSION_COOKIE)?.value; + + if (!token || !(await verifySessionToken(token))) { + return { + authorized: false, + response: NextResponse.json({ error: "Unauthorized" }, { status: 401 }), + }; + } + + return { authorized: true }; +} diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts new file mode 100644 index 0000000..9fbde77 --- /dev/null +++ b/src/lib/prisma.ts @@ -0,0 +1,10 @@ +import { PrismaClient } from "@prisma/client"; + +// PrismaClient is attached to the `global` object in development to prevent +// exhausting your database connection limit. +const globalForPrisma = global as unknown as { prisma: PrismaClient }; + +export const prisma = + globalForPrisma.prisma || new PrismaClient(); + +if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; diff --git a/src/lib/rate-limit.ts b/src/lib/rate-limit.ts new file mode 100644 index 0000000..32c2373 --- /dev/null +++ b/src/lib/rate-limit.ts @@ -0,0 +1,31 @@ +const attempts = new Map(); + +const MAX_ENTRIES = 10_000; + +function pruneExpired(now: number) { + if (attempts.size <= MAX_ENTRIES) return; + for (const [key, entry] of attempts) { + if (now > entry.resetAt) attempts.delete(key); + } +} + +export function checkRateLimit( + key: string, + maxAttempts = 5, + windowMs = 15 * 60 * 1000 +): boolean { + const now = Date.now(); + pruneExpired(now); + + const entry = attempts.get(key); + + if (!entry || now > entry.resetAt) { + attempts.set(key, { count: 1, resetAt: now + windowMs }); + return true; + } + + if (entry.count >= maxAttempts) return false; + + entry.count++; + return true; +} diff --git a/src/lib/session.ts b/src/lib/session.ts new file mode 100644 index 0000000..9fb3fe9 --- /dev/null +++ b/src/lib/session.ts @@ -0,0 +1,40 @@ +import { SignJWT, jwtVerify } from "jose"; + +export const SESSION_COOKIE = "admin_session"; +export const SESSION_MAX_AGE = 60 * 60 * 24 * 7; // 7 days + +function getSecret(): Uint8Array { + const secret = process.env.SESSION_SECRET; + if (!secret || secret.length < 32) { + throw new Error("SESSION_SECRET must be set and at least 32 characters"); + } + return new TextEncoder().encode(secret); +} + +export function getSessionCookieOptions(maxAge = SESSION_MAX_AGE) { + return { + name: SESSION_COOKIE, + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax" as const, + maxAge, + path: "/", + }; +} + +export async function createSessionToken(): Promise { + return new SignJWT({ role: "admin" }) + .setProtectedHeader({ alg: "HS256" }) + .setIssuedAt() + .setExpirationTime(`${SESSION_MAX_AGE}s`) + .sign(getSecret()); +} + +export async function verifySessionToken(token: string): Promise { + try { + const { payload } = await jwtVerify(token, getSecret()); + return payload.role === "admin"; + } catch { + return false; + } +} diff --git a/src/lib/validation.ts b/src/lib/validation.ts new file mode 100644 index 0000000..16a062a --- /dev/null +++ b/src/lib/validation.ts @@ -0,0 +1,24 @@ +import { z } from "zod"; + +export const MAX_GPX_BYTES = 5 * 1024 * 1024; // 5 MB + +export const locationSchema = z.object({ + title: z.string().trim().min(1, "Title is required").max(200), + city: z.string().trim().min(1, "City is required").max(200), + tags: z.string().max(500).default(""), + captureDate: z + .string() + .refine((d) => !isNaN(Date.parse(d)), "Invalid capture date"), + visibility: z.enum(["public", "unlisted"]).default("public"), + gpxData: z + .string() + .max(MAX_GPX_BYTES, `GPX data must be under ${MAX_GPX_BYTES / (1024 * 1024)} MB`) + .optional() + .nullable(), +}); + +export type LocationInput = z.infer; + +export function parseLocationBody(body: unknown) { + return locationSchema.safeParse(body); +} diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..32f0ac7 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,42 @@ +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; +import { verifySessionToken } from "@/lib/session"; + +export async function middleware(request: NextRequest) { + const isAdminRoute = + request.nextUrl.pathname.startsWith("/admin") || + request.nextUrl.pathname.startsWith("/api/admin"); + + if (isAdminRoute) { + const adminSession = request.cookies.get("admin_session"); + const isValid = + adminSession?.value && + (await verifySessionToken(adminSession.value)); + + if (!isValid) { + if (request.nextUrl.pathname.startsWith("/api/")) { + return NextResponse.json( + { error: "Unauthorized" }, + { status: 401 } + ); + } + return NextResponse.redirect(new URL("/login", request.url)); + } + } + + if (request.nextUrl.pathname === "/login") { + const adminSession = request.cookies.get("admin_session"); + if ( + adminSession?.value && + (await verifySessionToken(adminSession.value)) + ) { + return NextResponse.redirect(new URL("/admin", request.url)); + } + } + + return NextResponse.next(); +} + +export const config = { + matcher: ["/admin/:path*", "/api/admin/:path*", "/login"], +};