diff --git a/backend/.dockerignore b/backend/.dockerignore deleted file mode 100644 index 9762fa6..0000000 --- a/backend/.dockerignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules -.next -data -README.md diff --git a/backend/Dockerfile b/backend/Dockerfile deleted file mode 100644 index f2a31fc..0000000 --- a/backend/Dockerfile +++ /dev/null @@ -1,39 +0,0 @@ -FROM node:20-alpine AS base - -# Stage 1: Install deps -FROM base AS deps -RUN apk add --no-cache python3 make g++ gcc -WORKDIR /app -COPY package.json package-lock.json* ./ -RUN npm ci - -# Stage 2: Build -FROM base AS builder -WORKDIR /app -COPY --from=deps /app/node_modules ./node_modules -COPY . . -RUN npm run build - -# Stage 3: Production runner -FROM base AS runner -WORKDIR /app - -ENV NODE_ENV=production -ENV NEXT_TELEMETRY_DISABLED=1 -ENV DB_PATH=/app/data/zombie_invasion.db - -RUN addgroup --system --gid 1001 nodejs && \ - adduser --system --uid 1001 --shell /bin/sh nextjs - -COPY --from=builder /app/.next/standalone ./ -COPY --from=builder /app/.next/static ./.next/static -COPY --from=builder /app/docker-entrypoint.sh ./ - -RUN chmod +x docker-entrypoint.sh -RUN mkdir -p /app/data && chown -R nextjs:nodejs /app/data - -EXPOSE 3000 -ENV PORT=3000 -ENV HOSTNAME=0.0.0.0 - -ENTRYPOINT ["/bin/sh", "docker-entrypoint.sh"] diff --git a/backend/compose.prod.yml b/backend/compose.prod.yml deleted file mode 100644 index 9f104a1..0000000 --- a/backend/compose.prod.yml +++ /dev/null @@ -1,18 +0,0 @@ -services: - app: - image: registry.achmad.dev/dota-zombie-invasion:latest - pull_policy: always - ports: - - "6100:3000" - volumes: - - ./data:/app/data - environment: - - ADMIN_PASSWORD=${ADMIN_PASSWORD:-admin123} - - NODE_ENV=production - restart: unless-stopped - healthcheck: - test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/admin/check"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 15s diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml deleted file mode 100644 index 0b73cff..0000000 --- a/backend/docker-compose.yml +++ /dev/null @@ -1,13 +0,0 @@ -version: '3.8' - -services: - app: - build: . - ports: - - "6100:3000" - volumes: - - ./data:/app/data - environment: - - ADMIN_PASSWORD=admin123 - - NODE_ENV=production - restart: unless-stopped diff --git a/backend/docker-entrypoint.sh b/backend/docker-entrypoint.sh deleted file mode 100755 index 8ea94f9..0000000 --- a/backend/docker-entrypoint.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -set -e -mkdir -p /app/data -chown -R 1001:1001 /app/data -exec su -c "exec node server.js" nextjs diff --git a/backend/next.config.js b/backend/next.config.js deleted file mode 100644 index be6b0ee..0000000 --- a/backend/next.config.js +++ /dev/null @@ -1,5 +0,0 @@ -/** @type {import('next').NextConfig} */ -const nextConfig = { - output: 'standalone', -}; -module.exports = nextConfig; diff --git a/backend/package-lock.json b/backend/package-lock.json deleted file mode 100644 index cb7b43d..0000000 --- a/backend/package-lock.json +++ /dev/null @@ -1,2095 +0,0 @@ -{ - "name": "zombie-invasion-backend", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "zombie-invasion-backend", - "version": "1.0.0", - "dependencies": { - "@types/better-sqlite3": "^7.6.0", - "@types/node": "^20.0.0", - "@types/react": "^18.2.0", - "@types/react-dom": "^18.2.0", - "better-sqlite3": "^12.0.0", - "next": "^14.2.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "typescript": "^5.4.0" - }, - "devDependencies": { - "autoprefixer": "^10.4.0", - "postcss": "^8.4.0", - "tailwindcss": "^3.4.0" - } - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@next/env": { - "version": "14.2.35", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.35.tgz", - "integrity": "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==", - "license": "MIT" - }, - "node_modules/@next/swc-darwin-arm64": { - "version": "14.2.33", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz", - "integrity": "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "14.2.33", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz", - "integrity": "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.33", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz", - "integrity": "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==", - "cpu": [ - "arm64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.33", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz", - "integrity": "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==", - "cpu": [ - "arm64" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.33", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz", - "integrity": "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==", - "cpu": [ - "x64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.33", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz", - "integrity": "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==", - "cpu": [ - "x64" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.33", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz", - "integrity": "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.33", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz", - "integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.33", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz", - "integrity": "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@swc/counter": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "license": "Apache-2.0" - }, - "node_modules/@swc/helpers": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", - "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", - "license": "Apache-2.0", - "dependencies": { - "@swc/counter": "^0.1.3", - "tslib": "^2.4.0" - } - }, - "node_modules/@types/better-sqlite3": { - "version": "7.6.13", - "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", - "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/node": { - "version": "20.19.41", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", - "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "18.3.29", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.29.tgz", - "integrity": "sha512-ch0qJdr2JY0r04NXSprbK6TXOgnaJ1Tz23fm5W+z0/CBah6BSBc3n96h7K9GOtwh0HrilNWHIBzE1Ko4Dcw/Wg==", - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "license": "MIT", - "peerDependencies": { - "@types/react": "^18.0.0" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, - "license": "MIT" - }, - "node_modules/autoprefixer": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", - "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.28.2", - "caniuse-lite": "^1.0.30001787", - "fraction.js": "^5.3.4", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "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.32", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz", - "integrity": "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/better-sqlite3": { - "version": "12.10.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.10.0.tgz", - "integrity": "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" - }, - "engines": { - "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001793", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", - "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT" - }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.364", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.364.tgz", - "integrity": "sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==", - "dev": true, - "license": "ISC" - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/es-errors": { - "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" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "license": "MIT" - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fraction.js": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", - "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" - }, - "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" - } - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT" - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "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": "BSD-3-Clause" - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", - "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "license": "MIT" - }, - "node_modules/next": { - "version": "14.2.35", - "resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz", - "integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==", - "license": "MIT", - "dependencies": { - "@next/env": "14.2.35", - "@swc/helpers": "0.5.5", - "busboy": "1.6.0", - "caniuse-lite": "^1.0.30001579", - "graceful-fs": "^4.2.11", - "postcss": "8.4.31", - "styled-jsx": "5.1.1" - }, - "bin": { - "next": "dist/bin/next" - }, - "engines": { - "node": ">=18.17.0" - }, - "optionalDependencies": { - "@next/swc-darwin-arm64": "14.2.33", - "@next/swc-darwin-x64": "14.2.33", - "@next/swc-linux-arm64-gnu": "14.2.33", - "@next/swc-linux-arm64-musl": "14.2.33", - "@next/swc-linux-x64-gnu": "14.2.33", - "@next/swc-linux-x64-musl": "14.2.33", - "@next/swc-win32-arm64-msvc": "14.2.33", - "@next/swc-win32-ia32-msvc": "14.2.33", - "@next/swc-win32-x64-msvc": "14.2.33" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.41.2", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "sass": "^1.3.0" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "@playwright/test": { - "optional": true - }, - "sass": { - "optional": true - } - } - }, - "node_modules/next/node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/node-abi": { - "version": "3.92.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", - "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-releases": { - "version": "2.0.46", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz", - "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.12", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", - "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", - "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.1.1" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "jiti": ">=1.21.0", - "postcss": ">=8.0.9", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.12", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", - "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "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/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "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/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/styled-jsx": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", - "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", - "license": "MIT", - "dependencies": { - "client-only": "0.0.1" - }, - "engines": { - "node": ">= 12.0.0" - }, - "peerDependencies": { - "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/sucrase": { - "version": "3.35.1", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", - "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "tinyglobby": "^0.2.11", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tailwindcss": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", - "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.7", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "license": "MIT" - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - } - } -} diff --git a/backend/package.json b/backend/package.json deleted file mode 100644 index 2c7fa24..0000000 --- a/backend/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "zombie-invasion-backend", - "version": "1.0.0", - "private": true, - "scripts": { - "dev": "next dev -p 3000", - "build": "next build", - "start": "next start -p 3000" - }, - "dependencies": { - "next": "^14.2.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "better-sqlite3": "^12.0.0", - "typescript": "^5.4.0", - "@types/node": "^20.0.0", - "@types/react": "^18.2.0", - "@types/react-dom": "^18.2.0", - "@types/better-sqlite3": "^7.6.0" - }, - "devDependencies": { - "tailwindcss": "^3.4.0", - "postcss": "^8.4.0", - "autoprefixer": "^10.4.0" - } -} diff --git a/backend/postcss.config.js b/backend/postcss.config.js deleted file mode 100644 index 12a703d..0000000 --- a/backend/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; diff --git a/backend/src/app/admin/arsenal/page.tsx b/backend/src/app/admin/arsenal/page.tsx deleted file mode 100644 index 07988e5..0000000 --- a/backend/src/app/admin/arsenal/page.tsx +++ /dev/null @@ -1,57 +0,0 @@ -'use client'; -import { useEffect, useState } from 'react'; - -export default function ArsenalPage() { - const [data, setData] = useState({}); - - useEffect(() => { - fetch('/api/admin/arsenal').then(r => r.json()).then(setData); - }, []); - - return ( -
-

Arsenal

-
-
-

Inventory ({data.inventory?.length || 0})

-
- {data.inventory?.map((i: any, idx: number) => ( -
- {i.item_name} - [{i.quality}] -
{i.steam_id}
-
- ))} - {!data.inventory?.length &&

None

} -
-
-
-

Loadouts ({data.loadouts?.length || 0})

-
- {data.loadouts?.map((l: any, idx: number) => ( -
- {l.steam_id} - {l.hero_name} -
{l.loadout}
-
- ))} - {!data.loadouts?.length &&

None

} -
-
-
-

Active Listings ({data.listings?.length || 0})

-
- {data.listings?.map((l: any, idx: number) => ( -
- {l.item_name} - {l.price_free} free -
{l.steam_id}
-
- ))} - {!data.listings?.length &&

None

} -
-
-
-
- ); -} diff --git a/backend/src/app/admin/battlepass/[steamId]/page.tsx b/backend/src/app/admin/battlepass/[steamId]/page.tsx deleted file mode 100644 index 488bc99..0000000 --- a/backend/src/app/admin/battlepass/[steamId]/page.tsx +++ /dev/null @@ -1,87 +0,0 @@ -'use client'; -import { useEffect, useState } from 'react'; -import { useParams } from 'next/navigation'; - -export default function BattlePassDetailPage() { - const { steamId } = useParams(); - const [bp, setBp] = useState(null); - const [quests, setQuests] = useState([]); - const [editLevel, setEditLevel] = useState(0); - const [editXp, setEditXp] = useState(0); - const [editPremium, setEditPremium] = useState(false); - const [msg, setMsg] = useState(''); - - const load = () => { - fetch(`/api/admin/battlepass/${steamId}`).then(r => r.json()).then(d => { - setBp(d.battlePass); - setQuests(d.quests || []); - setEditLevel(d.battlePass?.level || 0); - setEditXp(d.battlePass?.experience || 0); - setEditPremium(d.battlePass?.has_premium === 1); - }).catch(() => {}); - }; - useEffect(load, [steamId]); - - const saveBp = async () => { - const res = await fetch(`/api/admin/battlepass/${steamId}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ level: editLevel, experience: editXp, has_premium: editPremium }), - }); - const d = await res.json(); - setMsg(d.success ? 'Saved!' : 'Error'); - }; - - if (!bp) return
Loading...
; - - return ( -
- ← Back -

Battle Pass

-

Steam ID: {steamId}

- {msg &&

{msg}

} -
-
-

Settings

-
-
- - setEditLevel(Number(e.target.value))} - className="w-full px-2 py-1 bg-gray-700 rounded text-white text-sm" /> -
-
- - setEditXp(Number(e.target.value))} - className="w-full px-2 py-1 bg-gray-700 rounded text-white text-sm" /> -
-
- - setEditPremium(e.target.checked)} - className="mt-2 block" /> -
-
- -
-
-

Quests ({quests.length})

-
- {quests.map((q: any) => ( -
-
-
{q.name || q.quest_id}
-
{q.type} — {q.progress}/{q.target}
-
- - {q.completed ? 'Completed' : 'In Progress'} - - {q.claimed ? ' — Claimed' : ''} -
-
-
- ))} -
-
-
-
- ); -} diff --git a/backend/src/app/admin/battlepass/page.tsx b/backend/src/app/admin/battlepass/page.tsx deleted file mode 100644 index 5b4bf6c..0000000 --- a/backend/src/app/admin/battlepass/page.tsx +++ /dev/null @@ -1,41 +0,0 @@ -'use client'; -import { useEffect, useState } from 'react'; -import Link from 'next/link'; - -export default function BattlePassListPage() { - const [bps, setBps] = useState([]); - - useEffect(() => { - fetch('/api/admin/battlepass').then(r => r.json()).then(setBps).catch(() => {}); - }, []); - - return ( -
-

Battle Passes

-
- - - - - - - - - - - {bps.map(bp => ( - - - - - - - ))} - -
Steam IDLevelXPPremium
- {bp.steam_id} - {bp.level}{bp.experience}{bp.has_premium ? 'Yes' : 'No'}
-
-
- ); -} diff --git a/backend/src/app/admin/contracts/page.tsx b/backend/src/app/admin/contracts/page.tsx deleted file mode 100644 index 0298bdd..0000000 --- a/backend/src/app/admin/contracts/page.tsx +++ /dev/null @@ -1,34 +0,0 @@ -'use client'; -import { useEffect, useState } from 'react'; - -export default function ContractsPage() { - const [contracts, setContracts] = useState([]); - - useEffect(() => { - fetch('/api/admin/contracts').then(r => r.json()).then(setContracts); - }, []); - - return ( -
-

Death Sentence Contracts

-
- - - - - - - - {contracts.map((c: any) => ( - - - - - - ))} - -
Steam IDContract DataUpdated
{c.steam_id}{c.contracts}{c.updated_at}
-
-
- ); -} diff --git a/backend/src/app/admin/layout.tsx b/backend/src/app/admin/layout.tsx deleted file mode 100644 index 4697705..0000000 --- a/backend/src/app/admin/layout.tsx +++ /dev/null @@ -1,63 +0,0 @@ -'use client'; -import { usePathname, useRouter } from 'next/navigation'; -import { useEffect, useState } from 'react'; - -const NAV = [ - { href: '/admin', label: 'Dashboard' }, - { href: '/admin/players', label: 'Players' }, - { href: '/admin/battlepass', label: 'Battle Pass' }, - { href: '/admin/matches', label: 'Matches' }, - { href: '/admin/promocodes', label: 'Promo Codes' }, - { href: '/admin/store', label: 'Store' }, - { href: '/admin/contracts', label: 'Contracts' }, - { href: '/admin/arsenal', label: 'Arsenal' }, -]; - -export default function AdminLayout({ children }: { children: React.ReactNode }) { - const pathname = usePathname(); - const router = useRouter(); - const [authed, setAuthed] = useState(false); - const [loading, setLoading] = useState(true); - - useEffect(() => { - if (pathname === '/admin/login') { - setLoading(false); - return; - } - fetch('/api/admin/check') - .then(r => r.json()) - .then(d => { - if (d.authenticated) setAuthed(true); - else router.push('/admin/login'); - }) - .catch(() => router.push('/admin/login')) - .finally(() => setLoading(false)); - }, [pathname, router]); - - if (loading) return
Loading...
; - if (pathname === '/admin/login') return <>{children}; - if (!authed) return null; - - return ( -
- -
{children}
-
- ); -} diff --git a/backend/src/app/admin/login/page.tsx b/backend/src/app/admin/login/page.tsx deleted file mode 100644 index c3c3dbc..0000000 --- a/backend/src/app/admin/login/page.tsx +++ /dev/null @@ -1,42 +0,0 @@ -'use client'; -import { useState } from 'react'; -import { useRouter } from 'next/navigation'; - -export default function LoginPage() { - const [password, setPassword] = useState(''); - const [error, setError] = useState(''); - const router = useRouter(); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setError(''); - const res = await fetch('/api/admin/login', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ password }), - }); - const data = await res.json(); - if (data.success) router.push('/admin'); - else setError(data.error || 'Login failed'); - }; - - return ( -
-
-

Admin Login

- {error &&

{error}

} - setPassword(e.target.value)} - placeholder="Password" - className="w-full px-3 py-2 bg-gray-700 rounded mb-4 text-white" - autoFocus - /> - -
-
- ); -} diff --git a/backend/src/app/admin/matches/page.tsx b/backend/src/app/admin/matches/page.tsx deleted file mode 100644 index 54ed037..0000000 --- a/backend/src/app/admin/matches/page.tsx +++ /dev/null @@ -1,57 +0,0 @@ -'use client'; -import { useEffect, useState } from 'react'; - -export default function MatchesPage() { - const [matches, setMatches] = useState([]); - const [heroFilter, setHeroFilter] = useState(''); - const [diffFilter, setDiffFilter] = useState(''); - - const load = () => { - const params = new URLSearchParams(); - if (heroFilter) params.set('hero', heroFilter); - if (diffFilter) params.set('difficulty', diffFilter); - fetch(`/api/admin/matches?${params}`).then(r => r.json()).then(setMatches).catch(() => {}); - }; - - useEffect(() => { load(); }, [heroFilter, diffFilter]); - - return ( -
-

Match History

-
- setHeroFilter(e.target.value)} - className="px-3 py-2 bg-gray-800 rounded text-white text-sm" /> - -
-
- - - - - - - - - - {matches.map((m: any) => ( - - - - - - - - - - ))} - -
Steam IDHeroResultDifficultyK/DDurationDate
{m.steam_id}{m.hero}{m.result}{m.difficulty}{m.kills}/{m.deaths}{Math.floor((m.duration || 0) / 60)}m{m.created_at}
-
-
- ); -} diff --git a/backend/src/app/admin/page.tsx b/backend/src/app/admin/page.tsx deleted file mode 100644 index 9a3238a..0000000 --- a/backend/src/app/admin/page.tsx +++ /dev/null @@ -1,35 +0,0 @@ -'use client'; -import { useEffect, useState } from 'react'; - -type Stats = { players: number; games: number; activeBps: number; questsCompleted: number }; - -export default function DashboardPage() { - const [stats, setStats] = useState(null); - - useEffect(() => { - fetch('/api/admin/stats').then(r => r.json()).then(setStats).catch(() => {}); - }, []); - - if (!stats) return
Loading...
; - - const cards = [ - { label: 'Players', value: stats.players, color: 'text-blue-400' }, - { label: 'Games Played', value: stats.games, color: 'text-green-400' }, - { label: 'Active Battle Passes', value: stats.activeBps, color: 'text-amber-400' }, - { label: 'Quests Completed', value: stats.questsCompleted, color: 'text-purple-400' }, - ]; - - return ( -
-

Dashboard

-
- {cards.map(c => ( -
-
{c.label}
-
{c.value}
-
- ))} -
-
- ); -} diff --git a/backend/src/app/admin/players/[steamId]/page.tsx b/backend/src/app/admin/players/[steamId]/page.tsx deleted file mode 100644 index e64354a..0000000 --- a/backend/src/app/admin/players/[steamId]/page.tsx +++ /dev/null @@ -1,90 +0,0 @@ -'use client'; -import { useEffect, useState } from 'react'; -import { useParams, useRouter } from 'next/navigation'; - -export default function PlayerDetailPage() { - const { steamId } = useParams(); - const router = useRouter(); - const [data, setData] = useState(null); - const [form, setForm] = useState({}); - const [msg, setMsg] = useState(''); - - useEffect(() => { - fetch(`/api/admin/players/${steamId}`).then(r => r.json()).then(d => { - setData(d); - setForm(d.player || {}); - }).catch(() => router.push('/admin/players')); - }, [steamId, router]); - - const save = async () => { - const res = await fetch(`/api/admin/players/${steamId}`, { - method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(form), - }); - const result = await res.json(); - setMsg(result.success ? 'Saved!' : 'Error: ' + (result.error || '')); - }; - - if (!data) return
Loading...
; - - return ( -
- ← Back to Players -

Player: {data.player?.player_name}

-

Steam ID: {steamId}

- {msg &&

{msg}

} - -
-
-

Profile

-
- {['player_name', 'profile_level', 'free_currency', 'donate_currency', 'dust_currency'].map(f => ( -
- - setForm({ ...form, [f]: e.target.value })} - className="w-full px-2 py-1 bg-gray-700 rounded text-white text-sm" /> -
- ))} -
- -
- -
-

Battle Pass

- {data.battlePass && ( -
-

Level: {data.battlePass.level}

-

XP: {data.battlePass.experience}

-

Premium: {data.battlePass.has_premium ? 'Yes' : 'No'}

- Edit BP → -
- )} -
- -
-

Recent Purchases ({data.purchases?.length})

-
- {data.purchases?.map((p: any, i: number) => ( -
{p.item_id} ({p.item_category})
- ))} - {!data.purchases?.length &&

None

} -
-
- -
-

Recent Matches ({data.matches?.length})

-
- {data.matches?.map((m: any, i: number) => ( -
- {m.hero} — {m.result} - ({m.difficulty}) -
- ))} - {!data.matches?.length &&

None

} -
-
-
-
- ); -} diff --git a/backend/src/app/admin/players/page.tsx b/backend/src/app/admin/players/page.tsx deleted file mode 100644 index 3169d8f..0000000 --- a/backend/src/app/admin/players/page.tsx +++ /dev/null @@ -1,51 +0,0 @@ -'use client'; -import { useEffect, useState } from 'react'; - -export default function PlayersListPage() { - const [players, setPlayers] = useState([]); - const [search, setSearch] = useState(''); - - useEffect(() => { - fetch('/api/admin/players').then(r => r.json()).then(setPlayers).catch(() => {}); - }, []); - - const filtered = players.filter(p => - p.steam_id?.includes(search) || p.player_name?.toLowerCase().includes(search.toLowerCase()) - ); - - return ( -
-

Players

- setSearch(e.target.value)} - className="w-full max-w-md px-3 py-2 bg-gray-800 rounded mb-4 text-white" /> -
- - - - - - - - - - - - - {filtered.map(p => ( - window.location.href = `/admin/players/${p.steam_id}`}> - - - - - - - - ))} - -
Steam IDNameLevelFreeDonateDust
{p.steam_id}{p.player_name}{p.profile_level}{p.free_currency}{p.donate_currency}{p.dust_currency}
-
-
- ); -} diff --git a/backend/src/app/admin/promocodes/page.tsx b/backend/src/app/admin/promocodes/page.tsx deleted file mode 100644 index 11a2ee5..0000000 --- a/backend/src/app/admin/promocodes/page.tsx +++ /dev/null @@ -1,71 +0,0 @@ -'use client'; -import { useEffect, useState } from 'react'; - -export default function PromoCodesPage() { - const [codes, setCodes] = useState([]); - const [form, setForm] = useState({ code: '', free_currency: 0, donate_currency: 0, dust_currency: 0, max_uses: 1 }); - - const load = () => { fetch('/api/admin/promocodes').then(r => r.json()).then(setCodes); }; - useEffect(load, []); - - const create = async () => { - await fetch('/api/admin/promocodes', { - method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(form), - }); - setForm({ code: '', free_currency: 0, donate_currency: 0, dust_currency: 0, max_uses: 1 }); - load(); - }; - - const del = async (code: string) => { - await fetch(`/api/admin/promocodes?code=${code}`, { method: 'DELETE' }); - load(); - }; - - return ( -
-

Promo Codes

- -
-

Create Code

-
- setForm({...form, code: e.target.value})} - className="px-2 py-1 bg-gray-700 rounded text-white text-sm" /> - setForm({...form, free_currency: Number(e.target.value)})} - className="px-2 py-1 bg-gray-700 rounded text-white text-sm" /> - setForm({...form, donate_currency: Number(e.target.value)})} - className="px-2 py-1 bg-gray-700 rounded text-white text-sm" /> - setForm({...form, dust_currency: Number(e.target.value)})} - className="px-2 py-1 bg-gray-700 rounded text-white text-sm" /> - setForm({...form, max_uses: Number(e.target.value)})} - className="px-2 py-1 bg-gray-700 rounded text-white text-sm" /> -
- -
- -
- - - - - - - - - - {codes.map(c => ( - - - - - - - - - - ))} - -
CodeFreeDonateDustUsesExpires
{c.code}{c.free_currency}{c.donate_currency}{c.dust_currency}{c.current_uses}/{c.max_uses}{c.expires_at || 'Never'}
-
-
- ); -} diff --git a/backend/src/app/admin/store/page.tsx b/backend/src/app/admin/store/page.tsx deleted file mode 100644 index ebc2c72..0000000 --- a/backend/src/app/admin/store/page.tsx +++ /dev/null @@ -1,63 +0,0 @@ -'use client'; -import { useEffect, useState } from 'react'; - -export default function StorePage() { - const [purchases, setPurchases] = useState([]); - const [effects, setEffects] = useState([]); - - useEffect(() => { - fetch('/api/admin/store').then(r => r.json()).then(d => { - setPurchases(d.purchases || []); - setEffects(d.effects || []); - }); - }, []); - - return ( -
-
-

Store Purchases

-
- - - - - - - - - {purchases.map((p: any) => ( - - - - - - - - ))} - -
PlayerItemCategoryCostDate
{p.player_name || p.steam_id}{p.item_id}{p.item_category}{p.price_free || p.price_donate || p.price_dust || 0}{p.created_at}
-
-
-
-

Active Effects

-
- - - - - - - - {effects.map((e: any) => ( - - - - - ))} - -
Steam IDEffects
{e.steam_id}{e.effects}
-
-
-
- ); -} diff --git a/backend/src/app/api/[...path]/route.ts b/backend/src/app/api/[...path]/route.ts deleted file mode 100644 index 6a57464..0000000 --- a/backend/src/app/api/[...path]/route.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { dispatch } from '@/lib/router'; -import { NextRequest, NextResponse } from 'next/server'; - -// Import all handlers to register their routes -import '@/lib/handlers/player'; -import '@/lib/handlers/battlepass'; -import '@/lib/handlers/game'; -import '@/lib/handlers/payments'; -import '@/lib/handlers/leaderboard'; -import '@/lib/handlers/cards'; -import '@/lib/handlers/equipment'; -import '@/lib/handlers/arsenal'; -import '@/lib/handlers/marketplace'; -import '@/lib/handlers/contracts'; - -export const dynamic = 'force-dynamic'; - -export async function GET(request: NextRequest, { params }: { params: { path: string[] } }) { - return dispatch(request, params.path, 'GET'); -} - -export async function POST(request: NextRequest, { params }: { params: { path: string[] } }) { - return dispatch(request, params.path, 'POST'); -} - -export async function PUT(request: NextRequest, { params }: { params: { path: string[] } }) { - return dispatch(request, params.path, 'PUT'); -} diff --git a/backend/src/app/api/admin/arsenal/route.ts b/backend/src/app/api/admin/arsenal/route.ts deleted file mode 100644 index 5475404..0000000 --- a/backend/src/app/api/admin/arsenal/route.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { NextResponse } from 'next/server'; -import { getDb } from '@/lib/db'; - -export const dynamic = 'force-dynamic'; - -export async function GET() { - const db = getDb(); - const inventory = db.prepare('SELECT * FROM arsenal_inventory ORDER BY steam_id').all(); - const loadouts = db.prepare('SELECT * FROM arsenal_loadouts ORDER BY steam_id').all(); - const listings = db.prepare("SELECT * FROM arsenal_market_listings WHERE status = 'active' ORDER BY created_at DESC").all(); - return NextResponse.json({ inventory, loadouts, listings }); -} diff --git a/backend/src/app/api/admin/battlepass/[steamId]/route.ts b/backend/src/app/api/admin/battlepass/[steamId]/route.ts deleted file mode 100644 index e021c0b..0000000 --- a/backend/src/app/api/admin/battlepass/[steamId]/route.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getDb } from '@/lib/db'; - -export async function GET(request: NextRequest, { params }: { params: { steamId: string } }) { - const db = getDb(); - const battlePass = db.prepare('SELECT * FROM battle_passes WHERE steam_id = ?').get(params.steamId); - const quests = db.prepare('SELECT * FROM battle_pass_quests WHERE steam_id = ? ORDER BY id').all(params.steamId); - return NextResponse.json({ battlePass, quests }); -} - -export async function PUT(request: NextRequest, { params }: { params: { steamId: string } }) { - const body = await request.json(); - const db = getDb(); - db.prepare('UPDATE battle_passes SET level = ?, experience = ?, has_premium = ?, updated_at = datetime(\'now\') WHERE steam_id = ?') - .run(body.level, body.experience, body.has_premium ? 1 : 0, params.steamId); - return NextResponse.json({ success: true }); -} diff --git a/backend/src/app/api/admin/battlepass/route.ts b/backend/src/app/api/admin/battlepass/route.ts deleted file mode 100644 index 22a2baa..0000000 --- a/backend/src/app/api/admin/battlepass/route.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { NextResponse } from 'next/server'; -import { getDb } from '@/lib/db'; - -export const dynamic = 'force-dynamic'; - -export async function GET() { - const db = getDb(); - const bps = db.prepare('SELECT * FROM battle_passes ORDER BY updated_at DESC').all(); - return NextResponse.json(bps); -} diff --git a/backend/src/app/api/admin/check/route.ts b/backend/src/app/api/admin/check/route.ts deleted file mode 100644 index 2f38224..0000000 --- a/backend/src/app/api/admin/check/route.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { NextResponse } from 'next/server'; -import { cookies } from 'next/headers'; - -export async function GET() { - const store = cookies(); - const authed = store.get('admin_session')?.value === 'authenticated'; - return NextResponse.json({ authenticated: !!authed }); -} diff --git a/backend/src/app/api/admin/contracts/route.ts b/backend/src/app/api/admin/contracts/route.ts deleted file mode 100644 index 87651ec..0000000 --- a/backend/src/app/api/admin/contracts/route.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { NextResponse } from 'next/server'; -import { getDb } from '@/lib/db'; - -export const dynamic = 'force-dynamic'; - -export async function GET() { - const db = getDb(); - const contracts = db.prepare('SELECT * FROM death_sentence_contracts').all(); - return NextResponse.json(contracts); -} diff --git a/backend/src/app/api/admin/login/route.ts b/backend/src/app/api/admin/login/route.ts deleted file mode 100644 index 7b9a508..0000000 --- a/backend/src/app/api/admin/login/route.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; - -export async function POST(request: NextRequest) { - const { password } = await request.json(); - const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'admin'; - console.log('[AdminLogin] attempt', { providedLength: password?.length, expectedLength: ADMIN_PASSWORD.length, match: password === ADMIN_PASSWORD }); - if (password !== ADMIN_PASSWORD) { - console.log('[AdminLogin] failed: password mismatch'); - return NextResponse.json({ success: false, error: 'Invalid password' }, { status: 401 }); - } - const secure = request.nextUrl.protocol === 'https:' || request.headers.get('x-forwarded-proto') === 'https'; - console.log('[AdminLogin] success, setting cookie', { secure, protocol: request.nextUrl.protocol, forwardedProto: request.headers.get('x-forwarded-proto') }); - const response = NextResponse.json({ success: true }); - response.cookies.set('admin_session', 'authenticated', { - httpOnly: true, secure, sameSite: 'lax', path: '/', maxAge: 86400, - }); - console.log('[AdminLogin] response cookie header:', response.headers.get('set-cookie')); - return response; -} diff --git a/backend/src/app/api/admin/logout/route.ts b/backend/src/app/api/admin/logout/route.ts deleted file mode 100644 index a7d3ac4..0000000 --- a/backend/src/app/api/admin/logout/route.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { NextResponse } from 'next/server'; - -export const dynamic = 'force-dynamic'; - -export async function GET() { - const response = NextResponse.json({ success: true }); - response.cookies.delete('admin_session'); - return response; -} diff --git a/backend/src/app/api/admin/matches/route.ts b/backend/src/app/api/admin/matches/route.ts deleted file mode 100644 index 730ddfc..0000000 --- a/backend/src/app/api/admin/matches/route.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getDb } from '@/lib/db'; - -export async function GET(request: NextRequest) { - const db = getDb(); - const { searchParams } = new URL(request.url); - const hero = searchParams.get('hero'); - const difficulty = searchParams.get('difficulty'); - const limit = parseInt(searchParams.get('limit') || '100'); - const offset = parseInt(searchParams.get('offset') || '0'); - - let query = 'SELECT * FROM game_history WHERE 1=1'; - const params: any[] = []; - - if (hero) { query += ' AND hero LIKE ?'; params.push(`%${hero}%`); } - if (difficulty) { query += ' AND difficulty = ?'; params.push(difficulty); } - - query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?'; - params.push(limit, offset); - - const matches = db.prepare(query).all(...params); - return NextResponse.json(matches); -} diff --git a/backend/src/app/api/admin/players/[steamId]/route.ts b/backend/src/app/api/admin/players/[steamId]/route.ts deleted file mode 100644 index 17eaf8f..0000000 --- a/backend/src/app/api/admin/players/[steamId]/route.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getDb } from '@/lib/db'; - -export async function GET(request: NextRequest, { params }: { params: { steamId: string } }) { - const db = getDb(); - const player = db.prepare('SELECT * FROM players WHERE steam_id = ?').get(params.steamId); - if (!player) return NextResponse.json({ error: 'Not found' }, { status: 404 }); - - const bp = db.prepare('SELECT * FROM battle_passes WHERE steam_id = ?').get(params.steamId); - const purchases = db.prepare('SELECT * FROM purchases WHERE steam_id = ? ORDER BY created_at DESC LIMIT 50').all(params.steamId); - const matches = db.prepare('SELECT * FROM game_history WHERE steam_id = ? ORDER BY created_at DESC LIMIT 20').all(params.steamId); - const effects = db.prepare('SELECT effects FROM active_effects WHERE steam_id = ?').get(params.steamId) as any; - - return NextResponse.json({ - player, battlePass: bp, purchases, matches, - activeEffects: effects ? JSON.parse(effects.effects) : {}, - }); -} - -export async function PUT(request: NextRequest, { params }: { params: { steamId: string } }) { - const body = await request.json(); - const db = getDb(); - const fields: string[] = []; - const values: any[] = []; - - for (const key of ['player_name', 'profile_level', 'free_currency', 'donate_currency', 'dust_currency']) { - if (body[key] !== undefined) { - fields.push(`${key} = ?`); - values.push(body[key]); - } - } - if (body.sounds_wheel !== undefined) { - fields.push('sounds_wheel = ?'); - values.push(JSON.stringify(body.sounds_wheel)); - } - if (fields.length === 0) return NextResponse.json({ error: 'No fields to update' }, { status: 400 }); - - fields.push("updated_at = datetime('now')"); - values.push(params.steamId); - db.prepare(`UPDATE players SET ${fields.join(', ')} WHERE steam_id = ?`).run(...values); - - return NextResponse.json({ success: true }); -} diff --git a/backend/src/app/api/admin/players/route.ts b/backend/src/app/api/admin/players/route.ts deleted file mode 100644 index efab05a..0000000 --- a/backend/src/app/api/admin/players/route.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { NextResponse } from 'next/server'; -import { getDb } from '@/lib/db'; - -export const dynamic = 'force-dynamic'; - -export async function GET() { - const db = getDb(); - const players = db.prepare('SELECT * FROM players ORDER BY updated_at DESC LIMIT 100').all(); - return NextResponse.json(players); -} diff --git a/backend/src/app/api/admin/promocodes/route.ts b/backend/src/app/api/admin/promocodes/route.ts deleted file mode 100644 index 27ecab2..0000000 --- a/backend/src/app/api/admin/promocodes/route.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getDb } from '@/lib/db'; - -export async function GET() { - const db = getDb(); - const codes = db.prepare('SELECT * FROM promo_codes ORDER BY code').all(); - return NextResponse.json(codes); -} - -export async function POST(request: NextRequest) { - const body = await request.json(); - const db = getDb(); - db.prepare('INSERT OR REPLACE INTO promo_codes (code, free_currency, donate_currency, dust_currency, max_uses, expires_at) VALUES (?, ?, ?, ?, ?, ?)') - .run(body.code?.toUpperCase(), body.free_currency || 0, body.donate_currency || 0, body.dust_currency || 0, body.max_uses || 1, body.expires_at || null); - return NextResponse.json({ success: true }); -} - -export async function DELETE(request: NextRequest) { - const { searchParams } = new URL(request.url); - const code = searchParams.get('code'); - if (!code) return NextResponse.json({ error: 'code required' }, { status: 400 }); - const db = getDb(); - db.prepare('DELETE FROM promo_codes WHERE code = ?').run(code.toUpperCase()); - return NextResponse.json({ success: true }); -} diff --git a/backend/src/app/api/admin/stats/route.ts b/backend/src/app/api/admin/stats/route.ts deleted file mode 100644 index 0e9ee9f..0000000 --- a/backend/src/app/api/admin/stats/route.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { NextResponse } from 'next/server'; -import { getDb } from '@/lib/db'; - -export const dynamic = 'force-dynamic'; - -export async function GET() { - const db = getDb(); - const players = (db.prepare('SELECT COUNT(*) as c FROM players').get() as any).c; - const games = (db.prepare('SELECT COUNT(*) as c FROM game_history').get() as any).c; - const activeBps = (db.prepare('SELECT COUNT(*) as c FROM battle_passes').get() as any).c; - const questsCompleted = (db.prepare('SELECT COUNT(*) as c FROM battle_pass_quests WHERE completed = 1').get() as any).c; - - return NextResponse.json({ players, games, activeBps, questsCompleted }); -} diff --git a/backend/src/app/api/admin/store/route.ts b/backend/src/app/api/admin/store/route.ts deleted file mode 100644 index b19e1ea..0000000 --- a/backend/src/app/api/admin/store/route.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { NextResponse } from 'next/server'; -import { getDb } from '@/lib/db'; - -export const dynamic = 'force-dynamic'; - -export async function GET() { - const db = getDb(); - const purchases = db.prepare(` - SELECT p.*, pl.player_name FROM purchases p - LEFT JOIN players pl ON p.steam_id = pl.steam_id - ORDER BY p.created_at DESC LIMIT 200 - `).all(); - const effects = db.prepare('SELECT * FROM active_effects').all(); - return NextResponse.json({ purchases, effects }); -} diff --git a/backend/src/app/globals.css b/backend/src/app/globals.css deleted file mode 100644 index b5c61c9..0000000 --- a/backend/src/app/globals.css +++ /dev/null @@ -1,3 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; diff --git a/backend/src/app/layout.tsx b/backend/src/app/layout.tsx deleted file mode 100644 index 8dfd39c..0000000 --- a/backend/src/app/layout.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import type { Metadata } from 'next'; -import './globals.css'; -export const metadata: Metadata = { - title: 'Zombie Invasion Backend', -}; -export default function RootLayout({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ); -} diff --git a/backend/src/app/page.tsx b/backend/src/app/page.tsx deleted file mode 100644 index c98aabe..0000000 --- a/backend/src/app/page.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { redirect } from 'next/navigation'; -export default function Home() { - redirect('/admin'); -} diff --git a/backend/src/lib/db.ts b/backend/src/lib/db.ts deleted file mode 100644 index f2542f3..0000000 --- a/backend/src/lib/db.ts +++ /dev/null @@ -1,212 +0,0 @@ -import Database from 'better-sqlite3'; -import path from 'path'; -import { seedDatabase } from './seed'; - -const DB_PATH = process.env.DB_PATH || path.join(process.cwd(), 'data', 'zombie_invasion.db'); - -let db: Database.Database; - -export function getDb(): Database.Database { - if (!db) { - const fs = require('fs'); - const dir = path.dirname(DB_PATH); - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } - db = new Database(DB_PATH); - db.pragma('journal_mode = WAL'); - db.pragma('foreign_keys = ON'); - initSchema(db); - seedDatabase(); - } - return db; -} - -function initSchema(db: Database.Database) { - db.exec(` - CREATE TABLE IF NOT EXISTS players ( - steam_id TEXT PRIMARY KEY, - player_name TEXT NOT NULL, - profile_level INTEGER DEFAULT 1, - free_currency INTEGER DEFAULT 0, - donate_currency INTEGER DEFAULT 0, - dust_currency INTEGER DEFAULT 0, - arcade_pack_credits TEXT DEFAULT '{"standard":0,"premium":0}', - sounds_wheel TEXT DEFAULT '{}', - created_at TEXT DEFAULT (datetime('now')), - updated_at TEXT DEFAULT (datetime('now')) - ); - - CREATE TABLE IF NOT EXISTS game_sessions ( - game_id TEXT PRIMARY KEY, - match_id INTEGER, - session_id TEXT, - status TEXT DEFAULT 'active', - created_at TEXT DEFAULT (datetime('now')) - ); - - CREATE TABLE IF NOT EXISTS game_history ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - steam_id TEXT NOT NULL, - game_id TEXT, - match_id INTEGER, - result TEXT, - hero TEXT, - hero_level INTEGER, - difficulty TEXT, - duration INTEGER, - kills INTEGER DEFAULT 0, - deaths INTEGER DEFAULT 0, - score INTEGER DEFAULT 0, - outgoing_damage REAL DEFAULT 0, - incoming_damage REAL DEFAULT 0, - items TEXT, - modifiers TEXT, - aghanim_scepter INTEGER DEFAULT 0, - aghanim_shard INTEGER DEFAULT 0, - gold_earned INTEGER DEFAULT 0, - session_id TEXT, - created_at TEXT DEFAULT (datetime('now')) - ); - - CREATE TABLE IF NOT EXISTS battle_passes ( - steam_id TEXT PRIMARY KEY, - level INTEGER DEFAULT 0, - experience INTEGER DEFAULT 0, - has_premium INTEGER DEFAULT 0, - claimed_rewards TEXT DEFAULT '[]', - claimed_premium_rewards TEXT DEFAULT '[]', - created_at TEXT DEFAULT (datetime('now')), - updated_at TEXT DEFAULT (datetime('now')) - ); - - CREATE TABLE IF NOT EXISTS battle_pass_quests ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - steam_id TEXT NOT NULL, - quest_id TEXT NOT NULL, - type TEXT NOT NULL, - name TEXT, - description TEXT, - progress INTEGER DEFAULT 0, - target INTEGER DEFAULT 1, - completed INTEGER DEFAULT 0, - claimed INTEGER DEFAULT 0, - reward_exp INTEGER DEFAULT 0, - reward_free_currency INTEGER DEFAULT 0, - quality TEXT, - npc TEXT, - target_item TEXT, - created_at TEXT DEFAULT (datetime('now')), - updated_at TEXT DEFAULT (datetime('now')) - ); - - CREATE TABLE IF NOT EXISTS purchases ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - steam_id TEXT NOT NULL, - item_id TEXT NOT NULL, - item_category TEXT, - card_id INTEGER, - price_free INTEGER DEFAULT 0, - price_donate INTEGER DEFAULT 0, - price_dust INTEGER DEFAULT 0, - created_at TEXT DEFAULT (datetime('now')) - ); - - CREATE TABLE IF NOT EXISTS active_effects ( - steam_id TEXT PRIMARY KEY, - effects TEXT DEFAULT '{}', - updated_at TEXT DEFAULT (datetime('now')) - ); - - CREATE TABLE IF NOT EXISTS promo_codes ( - code TEXT PRIMARY KEY, - free_currency INTEGER DEFAULT 0, - donate_currency INTEGER DEFAULT 0, - dust_currency INTEGER DEFAULT 0, - max_uses INTEGER DEFAULT 1, - current_uses INTEGER DEFAULT 0, - expires_at TEXT - ); - - CREATE TABLE IF NOT EXISTS promo_redemptions ( - steam_id TEXT, - code TEXT, - redeemed_at TEXT DEFAULT (datetime('now')), - PRIMARY KEY (steam_id, code) - ); - - CREATE TABLE IF NOT EXISTS card_levels ( - steam_id TEXT PRIMARY KEY, - card_levels TEXT DEFAULT '{}', - updated_at TEXT DEFAULT (datetime('now')) - ); - - CREATE TABLE IF NOT EXISTS decks ( - steam_id TEXT, - deck_index INTEGER, - name TEXT DEFAULT 'My Deck', - cards TEXT DEFAULT '[]', - updated_at TEXT DEFAULT (datetime('now')), - PRIMARY KEY (steam_id, deck_index) - ); - - CREATE TABLE IF NOT EXISTS equipment ( - steam_id TEXT PRIMARY KEY, - equipment TEXT DEFAULT '{}', - updated_at TEXT DEFAULT (datetime('now')) - ); - - CREATE TABLE IF NOT EXISTS arsenal_loadouts ( - steam_id TEXT, - hero_name TEXT, - loadout TEXT DEFAULT '{}', - updated_at TEXT DEFAULT (datetime('now')), - PRIMARY KEY (steam_id, hero_name) - ); - - CREATE TABLE IF NOT EXISTS arsenal_inventory ( - steam_id TEXT, - instance_id TEXT, - item_name TEXT, - quality TEXT, - upgrade_level INTEGER DEFAULT 0, - serial INTEGER, - global_serial INTEGER, - owner_name TEXT, - pinned INTEGER DEFAULT 0, - favorite INTEGER DEFAULT 0, - stats TEXT DEFAULT '[]', - PRIMARY KEY (steam_id, instance_id) - ); - - CREATE TABLE IF NOT EXISTS arsenal_market_listings ( - listing_id TEXT PRIMARY KEY, - steam_id TEXT NOT NULL, - instance_id TEXT, - item_name TEXT, - quality TEXT, - upgrade_level INTEGER DEFAULT 0, - serial INTEGER, - global_serial INTEGER, - price_free INTEGER DEFAULT 0, - status TEXT DEFAULT 'active', - created_at TEXT DEFAULT (datetime('now')) - ); - - CREATE TABLE IF NOT EXISTS arsenal_market_sales ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - listing_id TEXT, - seller_steam_id TEXT, - buyer_steam_id TEXT, - item_name TEXT, - price_free INTEGER, - created_at TEXT DEFAULT (datetime('now')) - ); - - CREATE TABLE IF NOT EXISTS death_sentence_contracts ( - steam_id TEXT PRIMARY KEY, - contracts TEXT DEFAULT '{}', - updated_at TEXT DEFAULT (datetime('now')) - ); - `); -} diff --git a/backend/src/lib/handlers/arsenal.ts b/backend/src/lib/handlers/arsenal.ts deleted file mode 100644 index 22c92e1..0000000 --- a/backend/src/lib/handlers/arsenal.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { route, HandlerContext, HttpError } from '@/lib/router'; -import { getDb } from '@/lib/db'; - -route('player/:steamId/arsenal_loadouts', ['GET'], (ctx: HandlerContext) => { - const db = getDb(); - const rows = db.prepare('SELECT * FROM arsenal_loadouts WHERE steam_id = ?').all(ctx.params.steamId); - const loadouts: Record = {}; - for (const r of rows as any[]) { - loadouts[r.hero_name] = JSON.parse(r.loadout); - } - return { arsenal_loadouts: loadouts }; -}); - -route('player/:steamId/arsenal_loadouts', ['PUT'], (ctx: HandlerContext) => { - const { arsenal_loadouts } = ctx.body as any; - if (!arsenal_loadouts) throw new HttpError(400, 'arsenal_loadouts required'); - const db = getDb(); - const upsert = db.prepare(` - INSERT INTO arsenal_loadouts (steam_id, hero_name, loadout, updated_at) VALUES (?, ?, ?, datetime('now')) - ON CONFLICT(steam_id, hero_name) DO UPDATE SET loadout = ?, updated_at = datetime('now') - `); - const tx = db.transaction(() => { - for (const [hero, loadout] of Object.entries(arsenal_loadouts)) { - upsert.run(ctx.params.steamId, hero, JSON.stringify(loadout), JSON.stringify(loadout)); - } - }); - tx(); - return { success: true }; -}); - -route('player/:steamId/arsenal_inventory', ['GET'], (ctx: HandlerContext) => { - const db = getDb(); - const items = db.prepare('SELECT * FROM arsenal_inventory WHERE steam_id = ?').all(ctx.params.steamId); - const instances: Record = {}; - for (const item of items as any[]) { - instances[item.instance_id] = { - instanceId: item.instance_id, - itemName: item.item_name, - quality: item.quality, - upgradeLevel: item.upgrade_level, - serial: item.serial, - globalSerial: item.global_serial, - ownerName: item.owner_name, - pinned: !!item.pinned, - favorite: !!item.favorite, - stats: JSON.parse(item.stats || '[]'), - }; - } - return { arsenal_inventory: { instances } }; -}); - -route('player/:steamId/arsenal_inventory', ['PUT'], (ctx: HandlerContext) => { - const { arsenal_inventory } = ctx.body as any; - if (!arsenal_inventory || !arsenal_inventory.instances) throw new HttpError(400, 'arsenal_inventory.instances required'); - const db = getDb(); - const upsert = db.prepare(` - INSERT INTO arsenal_inventory (steam_id, instance_id, item_name, quality, upgrade_level, serial, global_serial, owner_name, pinned, favorite, stats) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT(steam_id, instance_id) DO UPDATE SET - item_name = excluded.item_name, quality = excluded.quality, upgrade_level = excluded.upgrade_level, - serial = excluded.serial, global_serial = excluded.global_serial, owner_name = excluded.owner_name, - pinned = excluded.pinned, favorite = excluded.favorite, stats = excluded.stats - `); - const tx = db.transaction(() => { - for (const [instId, inst] of Object.entries(arsenal_inventory.instances)) { - const i = inst as any; - upsert.run(ctx.params.steamId, instId, - i.itemName || i.item_name || '', - i.quality || 'common', - i.upgradeLevel || i.upgrade_level || 0, - i.serial || 0, - i.globalSerial || i.global_serial || 0, - i.ownerName || i.owner_name || '', - i.pinned ? 1 : 0, - i.favorite ? 1 : 0, - JSON.stringify(i.stats || [])); - } - }); - tx(); - return { success: true }; -}); diff --git a/backend/src/lib/handlers/battlepass.ts b/backend/src/lib/handlers/battlepass.ts deleted file mode 100644 index 89e9964..0000000 --- a/backend/src/lib/handlers/battlepass.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { route, HandlerContext, HttpError } from '@/lib/router'; -import { getDb } from '@/lib/db'; - -const QUEST_DEFS = [ - { quest_id: 'kill_zombies_1', type: 'kill_zombies', name: 'Zombie Slayer I', description: 'Kill 100 zombies', target: 100, reward_exp: 50, reward_free_currency: 100 }, - { quest_id: 'kill_zombies_2', type: 'kill_zombies', name: 'Zombie Slayer II', description: 'Kill 500 zombies', target: 500, reward_exp: 100, reward_free_currency: 250 }, - { quest_id: 'survive_time_1', type: 'survive_time', name: 'Survivor I', description: 'Survive for 600 seconds', target: 600, reward_exp: 30, reward_free_currency: 50 }, - { quest_id: 'survive_waves_1', type: 'survive_waves', name: 'Wave Breaker I', description: 'Survive 10 waves', target: 10, reward_exp: 40, reward_free_currency: 75 }, - { quest_id: 'buy_black_shop_1', type: 'buy_black_shop', name: 'Black Shopper I', description: 'Buy 5 items from Black Shop', target: 5, reward_exp: 25, reward_free_currency: 50 }, - { quest_id: 'complete_npc_quest_1', type: 'complete_npc_quest', name: 'Helper I', description: 'Complete 3 NPC quests', target: 3, reward_exp: 35, reward_free_currency: 60 }, - { quest_id: 'earn_gold_1', type: 'earn_gold', name: 'Gold Rush I', description: 'Earn 5000 gold', target: 5000, reward_exp: 45, reward_free_currency: 100 }, - { quest_id: 'hero_level_1', type: 'hero_level', name: 'Stronger I', description: 'Reach level 10', target: 10, reward_exp: 30, reward_free_currency: 50 }, - { quest_id: 'cook_grilled_meat_1', type: 'cook_grilled_meat', name: 'Chef I', description: 'Cook grilled meat', target: 1, reward_exp: 20, reward_free_currency: 25 }, - { quest_id: 'use_campfire_1', type: 'use_campfire', name: 'Camper I', description: 'Use campfire 5 times', target: 5, reward_exp: 15, reward_free_currency: 25 }, - { quest_id: 'tip_teammate_1', type: 'tip_teammate', name: 'Friendly I', description: 'Tip teammates 3 times', target: 3, reward_exp: 20, reward_free_currency: 30 }, - { quest_id: 'deal_damage_1', type: 'deal_damage', name: 'Berserker I', description: 'Deal 50000 damage', target: 50000, reward_exp: 60, reward_free_currency: 150 }, - { quest_id: 'collect_item_1', type: 'collect_item', name: 'Collector I', description: 'Collect a rare item', target: 1, reward_exp: 40, reward_free_currency: 80, target_item: 'rare' }, -]; - -// POST /battlepass — Create or ensure BP exists for a player, assign default quests -route('battlepass', ['POST'], (ctx: HandlerContext) => { - const { steam_id } = ctx.body as any; - if (!steam_id) throw new HttpError(400, 'steam_id required'); - const db = getDb(); - db.prepare('INSERT OR IGNORE INTO battle_passes (steam_id, level, experience) VALUES (?, 0, 0)').run(steam_id); - - const existing = db.prepare('SELECT COUNT(*) as c FROM battle_pass_quests WHERE steam_id = ?').get(steam_id) as any; - if (existing.c === 0) { - const insert = db.prepare(` - INSERT INTO battle_pass_quests (steam_id, quest_id, type, name, description, target, reward_exp, reward_free_currency) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - `); - for (const q of QUEST_DEFS) { - insert.run(steam_id, q.quest_id, q.type, q.name, q.description, q.target, q.reward_exp, q.reward_free_currency); - } - } - return { success: true }; -}); - -// GET /battlepass/:steamId — Get BP data -route('battlepass/:steamId', ['GET'], (ctx: HandlerContext) => { - const db = getDb(); - let bp = db.prepare('SELECT * FROM battle_passes WHERE steam_id = ?').get(ctx.params.steamId) as any; - if (!bp) { - db.prepare('INSERT OR IGNORE INTO battle_passes (steam_id) VALUES (?)').run(ctx.params.steamId); - bp = db.prepare('SELECT * FROM battle_passes WHERE steam_id = ?').get(ctx.params.steamId); - } - return { - level: bp.level, - experience: bp.experience, - has_premium: bp.has_premium === 1, - claimed_rewards: JSON.parse(bp.claimed_rewards || '[]'), - claimed_premium_rewards: JSON.parse(bp.claimed_premium_rewards || '[]'), - }; -}); - -// GET /battlepass/:steamId/quests — Get quests for a player -route('battlepass/:steamId/quests', ['GET'], (ctx: HandlerContext) => { - const db = getDb(); - const quests = db.prepare('SELECT * FROM battle_pass_quests WHERE steam_id = ? ORDER BY id').all(ctx.params.steamId); - return { quests }; -}); - -// POST /battlepass/:steamId/quests/progress — Sync quest progress -route('battlepass/:steamId/quests/progress', ['POST'], (ctx: HandlerContext) => { - const { quest_id, progress } = ctx.body as any; - if (!quest_id) throw new HttpError(400, 'quest_id required'); - const db = getDb(); - const quest = db.prepare('SELECT * FROM battle_pass_quests WHERE steam_id = ? AND quest_id = ?').get(ctx.params.steamId, quest_id) as any; - if (!quest) { - db.prepare(`INSERT INTO battle_pass_quests (steam_id, quest_id, type, name, target, progress) VALUES (?, ?, 'custom', ?, 1, ?)`) - .run(ctx.params.steamId, quest_id, quest_id, progress || 0); - return { success: true, completed: false, progress: progress || 0 }; - } - const newProgress = Math.min(progress ?? quest.progress, quest.target); - const completed = newProgress >= quest.target ? 1 : 0; - db.prepare('UPDATE battle_pass_quests SET progress = ?, completed = ?, updated_at = datetime(\'now\') WHERE id = ?') - .run(newProgress, completed, quest.id); - return { success: true, completed: completed === 1, progress: newProgress }; -}); - -// POST /battlepass/:steamId/quests/claim — Claim a quest reward -route('battlepass/:steamId/quests/claim', ['POST'], (ctx: HandlerContext) => { - const { quest_id } = ctx.body as any; - if (!quest_id) throw new HttpError(400, 'quest_id required'); - const db = getDb(); - const quest = db.prepare('SELECT * FROM battle_pass_quests WHERE steam_id = ? AND quest_id = ?').get(ctx.params.steamId, quest_id) as any; - if (!quest) throw new HttpError(404, 'Quest not found'); - if (!quest.completed) throw new HttpError(400, 'Quest not completed'); - if (quest.claimed) throw new HttpError(400, 'Already claimed'); - - db.prepare('UPDATE battle_pass_quests SET claimed = 1, updated_at = datetime(\'now\') WHERE id = ?').run(quest.id); - db.prepare('UPDATE players SET free_currency = free_currency + ?, updated_at = datetime(\'now\') WHERE steam_id = ?') - .run(quest.reward_free_currency, ctx.params.steamId); - db.prepare('UPDATE battle_passes SET experience = experience + ?, updated_at = datetime(\'now\') WHERE steam_id = ?') - .run(quest.reward_exp, ctx.params.steamId); - - const bp = db.prepare('SELECT level, experience FROM battle_passes WHERE steam_id = ?').get(ctx.params.steamId) as any; - return { - success: true, - reward_exp: quest.reward_exp, - reward_free_currency: quest.reward_free_currency, - new_level: bp.level, - new_experience: bp.experience, - }; -}); - -// POST /battlepass/:steamId/hero-played — Record a hero being played (fire-and-forget) -route('battlepass/:steamId/hero-played', ['POST'], (ctx: HandlerContext) => { - return { success: true }; -}); - -// POST /battlepass/:steamId/claim — Claim a free BP level reward -route('battlepass/:steamId/claim', ['POST'], (ctx: HandlerContext) => { - const { steam_id, level } = ctx.body as any; - const db = getDb(); - const bp = db.prepare('SELECT * FROM battle_passes WHERE steam_id = ?').get(ctx.params.steamId) as any; - if (!bp) throw new HttpError(404, 'BP not found'); - let claimed = JSON.parse(bp.claimed_rewards || '[]'); - if (!claimed.includes(level)) { - claimed.push(level); - db.prepare("UPDATE battle_passes SET claimed_rewards = ?, updated_at = datetime('now') WHERE steam_id = ?") - .run(JSON.stringify(claimed), ctx.params.steamId); - } - return { success: true, level, currency_granted: { free_currency: level * 250, donate_currency: 0 } }; -}); - -// POST /battlepass/:steamId/claim-premium — Claim a premium BP level reward -route('battlepass/:steamId/claim-premium', ['POST'], (ctx: HandlerContext) => { - const { steam_id, level } = ctx.body as any; - const db = getDb(); - const bp = db.prepare('SELECT * FROM battle_passes WHERE steam_id = ?').get(ctx.params.steamId) as any; - if (!bp) throw new HttpError(404, 'BP not found'); - let claimed = JSON.parse(bp.claimed_premium_rewards || '[]'); - if (!claimed.includes(level)) { - claimed.push(level); - db.prepare("UPDATE battle_passes SET claimed_premium_rewards = ?, updated_at = datetime('now') WHERE steam_id = ?") - .run(JSON.stringify(claimed), ctx.params.steamId); - } - return { success: true, level, currency_granted: { free_currency: level * 250, donate_currency: level * 100 } }; -}); - -// POST /battlepass/:steamId/claim-all — Claim all rewards up to current level -route('battlepass/:steamId/claim-all', ['POST'], (ctx: HandlerContext) => { - const { steam_id } = ctx.body as any; - const db = getDb(); - const bp = db.prepare('SELECT * FROM battle_passes WHERE steam_id = ?').get(ctx.params.steamId) as any; - if (!bp) throw new HttpError(404, 'BP not found'); - - const unclaimedFree: number[] = []; - const unclaimedPremium: number[] = []; - const claimedFree = JSON.parse(bp.claimed_rewards || '[]') as number[]; - const claimedPremium = JSON.parse(bp.claimed_premium_rewards || '[]') as number[]; - - for (let lvl = 1; lvl <= bp.level; lvl++) { - if (!claimedFree.includes(lvl)) unclaimedFree.push(lvl); - if (bp.has_premium && !claimedPremium.includes(lvl)) unclaimedPremium.push(lvl); - } - - db.prepare("UPDATE battle_passes SET claimed_rewards = ?, claimed_premium_rewards = ?, updated_at = datetime('now') WHERE steam_id = ?") - .run(JSON.stringify([...claimedFree, ...unclaimedFree]), - JSON.stringify([...claimedPremium, ...unclaimedPremium]), - ctx.params.steamId); - - return { - success: true, - free_levels: unclaimedFree, - premium_levels: unclaimedPremium, - currency_granted: { - free_currency: unclaimedFree.length * 250 + unclaimedPremium.length * 250, - donate_currency: unclaimedPremium.length * 100, - }, - }; -}); - -// POST /battlepass/:steamId/buy-premium — Activate premium BP -route('battlepass/:steamId/buy-premium', ['POST'], (ctx: HandlerContext) => { - const db = getDb(); - db.prepare("UPDATE battle_passes SET has_premium = 1, updated_at = datetime('now') WHERE steam_id = ?") - .run(ctx.params.steamId); - return { success: true }; -}); - -// POST /battlepass/:steamId/addexp — Add experience to BP -route('battlepass/:steamId/addexp', ['POST'], (ctx: HandlerContext) => { - const { experience } = ctx.body as any; - const db = getDb(); - const bp = db.prepare('SELECT * FROM battle_passes WHERE steam_id = ?').get(ctx.params.steamId) as any; - if (!bp) throw new HttpError(404, 'BP not found'); - - const newExp = bp.experience + (experience || 0); - const levelUp = Math.floor(newExp / 1000); - const newLevel = bp.level + levelUp; - const remainder = newExp % 1000; - - db.prepare('UPDATE battle_passes SET experience = ?, level = ?, updated_at = datetime(\'now\') WHERE steam_id = ?') - .run(remainder, newLevel, ctx.params.steamId); - - return { level: newLevel, experience: remainder, level_up: levelUp > 0 }; -}); diff --git a/backend/src/lib/handlers/cards.ts b/backend/src/lib/handlers/cards.ts deleted file mode 100644 index 1bea5f8..0000000 --- a/backend/src/lib/handlers/cards.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { route, HandlerContext, HttpError } from '@/lib/router'; -import { getDb } from '@/lib/db'; - -route('player/:steamId/card-levels', ['GET'], (ctx: HandlerContext) => { - const db = getDb(); - const row = db.prepare('SELECT card_levels FROM card_levels WHERE steam_id = ?').get(ctx.params.steamId) as any; - return { card_levels: row ? JSON.parse(row.card_levels) : {} }; -}); - -route('player/:steamId/card-levels', ['PUT'], (ctx: HandlerContext) => { - const { card_levels } = ctx.body as any; - const db = getDb(); - db.prepare(` - INSERT INTO card_levels (steam_id, card_levels, updated_at) VALUES (?, ?, datetime('now')) - ON CONFLICT(steam_id) DO UPDATE SET card_levels = ?, updated_at = datetime('now') - `).run(ctx.params.steamId, JSON.stringify(card_levels || {}), JSON.stringify(card_levels || {})); - return { success: true }; -}); - -route('player/:steamId/decks', ['GET'], (ctx: HandlerContext) => { - const db = getDb(); - const decks = db.prepare('SELECT * FROM decks WHERE steam_id = ? ORDER BY deck_index').all(ctx.params.steamId); - return decks.map((d: any) => ({ ...d, cards: JSON.parse(d.cards || '[]') })); -}); - -route('player/:steamId/decks/:index', ['GET'], (ctx: HandlerContext) => { - const db = getDb(); - const deck = db.prepare('SELECT * FROM decks WHERE steam_id = ? AND deck_index = ?').get(ctx.params.steamId, parseInt(ctx.params.index)) as any; - if (!deck) return { name: 'New Deck', cards: [] }; - return { ...deck, cards: JSON.parse(deck.cards || '[]') }; -}); - -route('player/:steamId/decks/:index', ['PUT'], (ctx: HandlerContext) => { - const { name, cards } = ctx.body as any; - const db = getDb(); - const idx = parseInt(ctx.params.index); - db.prepare(` - INSERT INTO decks (steam_id, deck_index, name, cards, updated_at) VALUES (?, ?, ?, ?, datetime('now')) - ON CONFLICT(steam_id, deck_index) DO UPDATE SET name = ?, cards = ?, updated_at = datetime('now') - `).run(ctx.params.steamId, idx, name || 'My Deck', JSON.stringify(cards || []), name || 'My Deck', JSON.stringify(cards || [])); - return { success: true }; -}); diff --git a/backend/src/lib/handlers/contracts.ts b/backend/src/lib/handlers/contracts.ts deleted file mode 100644 index f8e7eb7..0000000 --- a/backend/src/lib/handlers/contracts.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { route, HandlerContext, HttpError } from '@/lib/router'; -import { getDb } from '@/lib/db'; - -route('player/:steamId/death_sentence_contracts', ['GET'], (ctx: HandlerContext) => { - const db = getDb(); - const row = db.prepare('SELECT contracts FROM death_sentence_contracts WHERE steam_id = ?').get(ctx.params.steamId) as any; - return { death_sentence_contracts: row ? JSON.parse(row.contracts) : { roster: [] } }; -}); - -route('player/:steamId/death_sentence_contracts', ['PUT'], (ctx: HandlerContext) => { - const { death_sentence_contracts } = ctx.body as any; - const db = getDb(); - db.prepare(` - INSERT INTO death_sentence_contracts (steam_id, contracts, updated_at) VALUES (?, ?, datetime('now')) - ON CONFLICT(steam_id) DO UPDATE SET contracts = ?, updated_at = datetime('now') - `).run(ctx.params.steamId, JSON.stringify(death_sentence_contracts || {}), JSON.stringify(death_sentence_contracts || {})); - return { success: true }; -}); diff --git a/backend/src/lib/handlers/equipment.ts b/backend/src/lib/handlers/equipment.ts deleted file mode 100644 index 65316c0..0000000 --- a/backend/src/lib/handlers/equipment.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { route, HandlerContext, HttpError } from '@/lib/router'; -import { getDb } from '@/lib/db'; - -route('player/:steamId/equipment', ['GET'], (ctx: HandlerContext) => { - const db = getDb(); - const row = db.prepare('SELECT equipment FROM equipment WHERE steam_id = ?').get(ctx.params.steamId) as any; - return { equipment: row ? JSON.parse(row.equipment) : {} }; -}); - -route('player/:steamId/equipment', ['PUT'], (ctx: HandlerContext) => { - const { equipment } = ctx.body as any; - const db = getDb(); - db.prepare(` - INSERT INTO equipment (steam_id, equipment, updated_at) VALUES (?, ?, datetime('now')) - ON CONFLICT(steam_id) DO UPDATE SET equipment = ?, updated_at = datetime('now') - `).run(ctx.params.steamId, JSON.stringify(equipment || {}), JSON.stringify(equipment || {})); - return { success: true }; -}); - -route('player/:steamId/equipment/drop', ['POST'], (ctx: HandlerContext) => { - return { success: true }; -}); diff --git a/backend/src/lib/handlers/game.ts b/backend/src/lib/handlers/game.ts deleted file mode 100644 index 9ced4d0..0000000 --- a/backend/src/lib/handlers/game.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { route, HandlerContext, HttpError } from '@/lib/router'; -import { getDb } from '@/lib/db'; - -route('game/start', ['POST'], (ctx: HandlerContext) => { - const { steam_id, hero, hero_level, difficulty, player_name, match_id, session_id, session_participants } = ctx.body as any; - if (!steam_id) throw new HttpError(400, 'steam_id required'); - const db = getDb(); - - const gameId = `game_${Date.now()}_${Math.floor(Math.random() * 100000)}`; - const newMatchId = match_id || Math.floor(Math.random() * 100000000); - - db.prepare('INSERT OR REPLACE INTO game_sessions (game_id, match_id, session_id, status) VALUES (?, ?, ?, \'active\')') - .run(gameId, newMatchId, session_id || ''); - - return { game_id: gameId, match_id: newMatchId }; -}); - -route('game/heartbeat', ['POST'], (ctx: HandlerContext) => { - return { success: true }; -}); - -route('game', ['POST'], (ctx: HandlerContext) => { - const { steam_id, result, duration, kills, deaths, score, outgoing_damage, incoming_damage, - hero, hero_level, items, modifiers, aghanim_scepter, aghanim_shard, gold_earned, - difficulty, session_id, game_id } = ctx.body as any; - if (!steam_id) throw new HttpError(400, 'steam_id required'); - - const db = getDb(); - db.prepare(` - INSERT INTO game_history (steam_id, game_id, result, duration, kills, deaths, score, - outgoing_damage, incoming_damage, hero, hero_level, items, modifiers, - aghanim_scepter, aghanim_shard, gold_earned, difficulty, session_id) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `).run(steam_id, game_id || null, result || 'loss', duration || 0, kills || 0, deaths || 0, - score || 0, outgoing_damage || 0, incoming_damage || 0, hero || '', hero_level || 1, - items || '', modifiers || '', aghanim_scepter ? 1 : 0, aghanim_shard ? 1 : 0, - gold_earned || 0, difficulty || 'normal', session_id || ''); - - if (game_id) { - db.prepare("UPDATE game_sessions SET status = 'completed' WHERE game_id = ?").run(game_id); - } - - return { success: true }; -}); - -route('game/:id/players', ['GET'], (ctx: HandlerContext) => { - const db = getDb(); - const session = db.prepare('SELECT * FROM game_sessions WHERE game_id = ? OR match_id = ?') - .get(ctx.params.id, parseInt(ctx.params.id) || 0) as any; - if (!session) return { players: [] }; - - const players = db.prepare( - 'SELECT DISTINCT steam_id, hero, hero_level, result FROM game_history WHERE match_id = ? OR game_id = ?' - ).all(session.match_id, session.game_id); - - return { party_players: players, players }; -}); diff --git a/backend/src/lib/handlers/leaderboard.ts b/backend/src/lib/handlers/leaderboard.ts deleted file mode 100644 index 94f8989..0000000 --- a/backend/src/lib/handlers/leaderboard.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { route, HandlerContext } from '@/lib/router'; -import { getDb } from '@/lib/db'; - -route('leaderboard', ['GET'], (ctx: HandlerContext) => { - const limit = parseInt(ctx.searchParams.get('limit') || '20'); - const offset = parseInt(ctx.searchParams.get('offset') || '0'); - const board = ctx.searchParams.get('board') || 'rating'; - const db = getDb(); - - let rows: any[]; - if (board === 'wealth') { - rows = db.prepare( - 'SELECT steam_id, player_name, (free_currency + donate_currency) as score, free_currency, donate_currency FROM players ORDER BY score DESC LIMIT ? OFFSET ?' - ).all(limit, offset); - } else { - rows = db.prepare(` - SELECT p.steam_id, p.player_name, - COUNT(CASE WHEN gh.result = 'win' THEN 1 END) as wins, - COUNT(gh.id) as total_games - FROM players p - LEFT JOIN game_history gh ON p.steam_id = gh.steam_id - GROUP BY p.steam_id - ORDER BY wins DESC - LIMIT ? OFFSET ? - `).all(limit, offset); - } - - return { - leaderboard: rows, - total: (db.prepare('SELECT COUNT(*) as c FROM players').get() as any).c, - board, - }; -}); diff --git a/backend/src/lib/handlers/marketplace.ts b/backend/src/lib/handlers/marketplace.ts deleted file mode 100644 index 7dca778..0000000 --- a/backend/src/lib/handlers/marketplace.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { route, HandlerContext, HttpError } from '@/lib/router'; -import { getDb } from '@/lib/db'; - -route('arsenal_market/listings', ['GET'], (ctx: HandlerContext) => { - const db = getDb(); - return db.prepare("SELECT * FROM arsenal_market_listings WHERE status = 'active' ORDER BY created_at DESC").all(); -}); - -route('player/:steamId/arsenal_market/my_listings', ['GET'], (ctx: HandlerContext) => { - const db = getDb(); - return db.prepare("SELECT * FROM arsenal_market_listings WHERE steam_id = ? AND status = 'active' ORDER BY created_at DESC").all(ctx.params.steamId); -}); - -route('player/:steamId/arsenal_market/slots', ['GET'], (ctx: HandlerContext) => { - return { slots: 5, used: 0 }; -}); - -route('player/:steamId/arsenal_market/sales', ['GET'], (ctx: HandlerContext) => { - const db = getDb(); - return db.prepare('SELECT * FROM arsenal_market_sales WHERE seller_steam_id = ? ORDER BY created_at DESC').all(ctx.params.steamId); -}); - -route('player/:steamId/arsenal_market/create', ['POST'], (ctx: HandlerContext) => { - const { instance_id, item_name, quality, upgrade_level, serial, global_serial, price_free } = ctx.body as any; - const listingId = `list_${Date.now()}_${Math.floor(Math.random() * 1000)}`; - const db = getDb(); - db.prepare(` - INSERT INTO arsenal_market_listings (listing_id, steam_id, instance_id, item_name, quality, upgrade_level, serial, global_serial, price_free, status) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active') - `).run(listingId, ctx.params.steamId, instance_id || '', item_name || 'Unknown', quality || 'common', - upgrade_level || 0, serial || 0, global_serial || 0, price_free || 0); - return { success: true, listing_id: listingId }; -}); - -route('player/:steamId/arsenal_market/buy', ['POST'], (ctx: HandlerContext) => { - const { listing_id } = ctx.body as any; - if (!listing_id) throw new HttpError(400, 'listing_id required'); - const db = getDb(); - const listing = db.prepare('SELECT * FROM arsenal_market_listings WHERE listing_id = ?').get(listing_id) as any; - if (!listing) throw new HttpError(404, 'Listing not found'); - if (listing.status !== 'active') throw new HttpError(400, 'Listing not active'); - - db.prepare("UPDATE arsenal_market_listings SET status = 'sold' WHERE listing_id = ?").run(listing_id); - db.prepare('INSERT INTO arsenal_market_sales (listing_id, seller_steam_id, buyer_steam_id, item_name, price_free) VALUES (?, ?, ?, ?, ?)') - .run(listing_id, listing.steam_id, ctx.params.steamId, listing.item_name, listing.price_free); - return { success: true }; -}); - -route('player/:steamId/arsenal_market/cancel', ['POST'], (ctx: HandlerContext) => { - const { listing_id } = ctx.body as any; - if (!listing_id) throw new HttpError(400, 'listing_id required'); - const db = getDb(); - db.prepare("UPDATE arsenal_market_listings SET status = 'cancelled' WHERE listing_id = ? AND steam_id = ?") - .run(listing_id, ctx.params.steamId); - return { success: true }; -}); diff --git a/backend/src/lib/handlers/payments.ts b/backend/src/lib/handlers/payments.ts deleted file mode 100644 index 33b82c6..0000000 --- a/backend/src/lib/handlers/payments.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { route, HandlerContext, HttpError } from '@/lib/router'; -import { getDb } from '@/lib/db'; - -// POST /payments/robokassa/link — Auto-grant purchased currency -route('payments/robokassa/link', ['POST'], (ctx: HandlerContext) => { - const { steam_id, amount_rub } = ctx.body as any; - if (!steam_id) throw new HttpError(400, 'steam_id required'); - const db = getDb(); - - const donateShards = (amount_rub || 100) * 10; - db.prepare('UPDATE players SET donate_currency = donate_currency + ?, updated_at = datetime(\'now\') WHERE steam_id = ?') - .run(donateShards, steam_id); - - return { - ok: true, - payment_url: '', - donate_shards: donateShards, - inv_id: Math.floor(Math.random() * 100000), - }; -}); - -// POST /payments/bundles/link — Auto-grant bundle items -route('payments/bundles/link', ['POST'], (ctx: HandlerContext) => { - const { steam_id, bundle_id } = ctx.body as any; - if (!steam_id) throw new HttpError(400, 'steam_id required'); - const db = getDb(); - - db.prepare('UPDATE players SET free_currency = free_currency + 500, donate_currency = donate_currency + 200, updated_at = datetime(\'now\') WHERE steam_id = ?') - .run(steam_id); - - return { - ok: true, - payment_url: '', - inv_id: Math.floor(Math.random() * 100000), - message: 'Bundle granted', - }; -}); - -// GET /payments/deals?steam_id= — Return deal catalog -route('payments/deals', ['GET'], (ctx: HandlerContext) => { - return { - ok: true, - bundles: [ - { id: 'starter_bundle', name: 'Starter Pack', description: 'Get started with 500 shards', price_free: 0, price_donate: 0, items: [{ item_id: 'starter_pack', name: 'Starter Pack' }] }, - { id: 'hero_bundle_1', name: 'Hero Bundle I', description: 'Unlock a random hero', price_free: 1000, price_donate: 0, items: [{ item_id: 'hero_bundle_1', name: 'Hero Bundle' }] }, - ], - daily: { available: true, items: [] }, - weekly: { available: true, items: [] }, - player_created_at_unix: Math.floor(Date.now() / 1000), - }; -}); diff --git a/backend/src/lib/handlers/player.ts b/backend/src/lib/handlers/player.ts deleted file mode 100644 index 83c74db..0000000 --- a/backend/src/lib/handlers/player.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { route, HandlerContext, HttpError } from '@/lib/router'; -import { getDb } from '@/lib/db'; - -// POST /player — Create player profile -route('player', ['POST'], (ctx: HandlerContext) => { - const { steam_id, player_name } = ctx.body as any; - if (!steam_id) throw new HttpError(400, 'steam_id is required'); - const db = getDb(); - const existing = db.prepare('SELECT * FROM players WHERE steam_id = ?').get(steam_id) as any; - if (existing) { - return existing; - } - db.prepare('INSERT INTO players (steam_id, player_name) VALUES (?, ?)').run(steam_id, player_name || ''); - try { - db.prepare('INSERT OR IGNORE INTO battle_passes (steam_id) VALUES (?)').run(steam_id); - } catch {} - const player = db.prepare('SELECT * FROM players WHERE steam_id = ?').get(steam_id); - return player; -}); - -// GET /player/:steamId — Get player profile -// Returns the player row plus recentGames array and stats object -route('player/:steamId', ['GET'], (ctx: HandlerContext) => { - const db = getDb(); - const player = db.prepare('SELECT * FROM players WHERE steam_id = ?').get(ctx.params.steamId) as any; - if (!player) throw new HttpError(404, 'Player not found'); - return { - ...player, - recentGames: [], - stats: { - total_games: 0, - total_wins: 0, - rating: 0, - }, - }; -}); - -// GET /player/:steamId/history — Match history with limit/offset -route('player/:steamId/history', ['GET'], (ctx: HandlerContext) => { - const limit = parseInt(ctx.searchParams.get('limit') || '10'); - const offset = parseInt(ctx.searchParams.get('offset') || '0'); - const db = getDb(); - const games = db.prepare( - 'SELECT * FROM game_history WHERE steam_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?' - ).all(ctx.params.steamId, limit, offset); - return games; -}); - -// GET /player/:steamId/currency — Get currency balances -route('player/:steamId/currency', ['GET'], (ctx: HandlerContext) => { - const db = getDb(); - const player = db.prepare('SELECT free_currency, donate_currency, dust_currency FROM players WHERE steam_id = ?').get(ctx.params.steamId) as any; - if (!player) throw new HttpError(404, 'Player not found'); - return player; -}); - -// PUT /player/:steamId/currency — Save currency balances -route('player/:steamId/currency', ['PUT'], (ctx: HandlerContext) => { - const { free_currency, donate_currency, dust_currency } = ctx.body as any; - const db = getDb(); - const player = db.prepare('SELECT * FROM players WHERE steam_id = ?').get(ctx.params.steamId) as any; - if (!player) throw new HttpError(404, 'Player not found'); - db.prepare(` - UPDATE players SET free_currency = ?, donate_currency = ?, dust_currency = ?, updated_at = datetime('now') - WHERE steam_id = ? - `).run( - free_currency ?? player.free_currency, - donate_currency ?? player.donate_currency, - dust_currency ?? player.dust_currency, - ctx.params.steamId - ); - return { success: true }; -}); - -// POST /player/:steamId/currency/give — Grant currency (used by BP rewards) -route('player/:steamId/currency/give', ['POST'], (ctx: HandlerContext) => { - const body = ctx.body as any; - const free_amount = body.free_amount ?? body.freeAmount ?? 0; - const donate_amount = body.donate_amount ?? body.donateAmount ?? 0; - const dust_amount = body.dust_amount ?? body.dustAmount ?? 0; - const db = getDb(); - const player = db.prepare('SELECT * FROM players WHERE steam_id = ?').get(ctx.params.steamId) as any; - if (!player) throw new HttpError(404, 'Player not found'); - db.prepare(` - UPDATE players SET free_currency = free_currency + ?, donate_currency = donate_currency + ?, - dust_currency = dust_currency + ?, updated_at = datetime('now') WHERE steam_id = ? - `).run( - free_amount, - donate_amount, - dust_amount, - ctx.params.steamId - ); - return { success: true }; -}); - -// POST /player/:steamId/purchases — Record a store purchase -route('player/:steamId/purchases', ['POST'], (ctx: HandlerContext) => { - const { item_id, item_category, card_id, price_free, price_donate, price_dust } = ctx.body as any; - const db = getDb(); - db.prepare(` - INSERT INTO purchases (steam_id, item_id, item_category, card_id, price_free, price_donate, price_dust) - VALUES (?, ?, ?, ?, ?, ?, ?) - `).run(ctx.params.steamId, item_id, item_category || 'items', card_id || null, price_free || 0, price_donate || 0, price_dust || 0); - return { success: true }; -}); - -// POST /player/:steamId/promo/redeem — Redeem a promo code -route('player/:steamId/promo/redeem', ['POST'], (ctx: HandlerContext) => { - const { code } = ctx.body as any; - if (!code) throw new HttpError(400, 'Code is required'); - const normalizedCode = String(code).toUpperCase(); - const db = getDb(); - const promo = db.prepare('SELECT * FROM promo_codes WHERE code = ?').get(normalizedCode) as any; - if (!promo) throw new HttpError(404, 'Promo code not found'); - if (promo.expires_at && new Date(promo.expires_at) < new Date()) throw new HttpError(400, 'Code expired'); - if (promo.current_uses >= promo.max_uses) throw new HttpError(400, 'Code fully redeemed'); - - const existing = db.prepare('SELECT * FROM promo_redemptions WHERE steam_id = ? AND code = ?').get(ctx.params.steamId, normalizedCode); - if (existing) throw new HttpError(400, 'Code already redeemed'); - - db.prepare(` - UPDATE players SET free_currency = free_currency + ?, donate_currency = donate_currency + ?, - dust_currency = dust_currency + ?, updated_at = datetime('now') WHERE steam_id = ? - `).run(promo.free_currency, promo.donate_currency, promo.dust_currency, ctx.params.steamId); - - db.prepare('UPDATE promo_codes SET current_uses = current_uses + 1 WHERE code = ?').run(normalizedCode); - db.prepare('INSERT INTO promo_redemptions (steam_id, code) VALUES (?, ?)').run(ctx.params.steamId, normalizedCode); - - const player = db.prepare('SELECT free_currency, donate_currency, dust_currency FROM players WHERE steam_id = ?').get(ctx.params.steamId); - return { success: true, rewards: { free_currency: promo.free_currency, donate_currency: promo.donate_currency, dust_currency: promo.dust_currency }, currency: player }; -}); - -// GET /player/:steamId/sounds_wheel — Get sounds wheel -route('player/:steamId/sounds_wheel', ['GET'], (ctx: HandlerContext) => { - const db = getDb(); - const player = db.prepare('SELECT sounds_wheel FROM players WHERE steam_id = ?').get(ctx.params.steamId) as any; - if (!player) throw new HttpError(404, 'Player not found'); - return { sounds_wheel: JSON.parse(player.sounds_wheel || '{}') }; -}); - -// PUT /player/:steamId/sounds_wheel — Save sounds wheel -route('player/:steamId/sounds_wheel', ['PUT'], (ctx: HandlerContext) => { - const { sounds_wheel } = ctx.body as any; - const db = getDb(); - db.prepare("UPDATE players SET sounds_wheel = ?, updated_at = datetime('now') WHERE steam_id = ?") - .run(JSON.stringify(sounds_wheel || {}), ctx.params.steamId); - return { success: true }; -}); - -// POST /player/:steamId/deal-purchase — Buy a deal/offer -route('player/:steamId/deal-purchase', ['POST'], (ctx: HandlerContext) => { - const { deal_key } = ctx.body as any; - return { success: true, ok: true, item_id: 'deal_' + deal_key, item_category: 'items' }; -}); - -// GET /player/:steamId/active_effects — Get active cosmetic effects -route('player/:steamId/active_effects', ['GET'], (ctx: HandlerContext) => { - const db = getDb(); - const row = db.prepare('SELECT effects FROM active_effects WHERE steam_id = ?').get(ctx.params.steamId) as any; - return { active_effects: row ? JSON.parse(row.effects) : {} }; -}); - -// PUT /player/:steamId/active_effects — Save active effects -route('player/:steamId/active_effects', ['PUT'], (ctx: HandlerContext) => { - const { active_effects } = ctx.body as any; - const db = getDb(); - db.prepare(` - INSERT INTO active_effects (steam_id, effects, updated_at) VALUES (?, ?, datetime('now')) - ON CONFLICT(steam_id) DO UPDATE SET effects = ?, updated_at = datetime('now') - `).run(ctx.params.steamId, JSON.stringify(active_effects || {}), JSON.stringify(active_effects || {})); - return { success: true }; -}); diff --git a/backend/src/lib/router.ts b/backend/src/lib/router.ts deleted file mode 100644 index 671c9c1..0000000 --- a/backend/src/lib/router.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { NextResponse } from 'next/server'; - -export type HandlerFn = (ctx: HandlerContext) => unknown | Promise; - -export type HandlerContext = { - params: Record; - method: string; - body: unknown; - searchParams: URLSearchParams; -}; - -type RouteEntry = { - pattern: string[]; - methods: string[]; - handler: HandlerFn; -}; - -const routes: RouteEntry[] = []; - -export function route(pattern: string, methods: string[], handler: HandlerFn) { - const parts = pattern.split('/').filter(Boolean); - routes.push({ pattern: parts, methods: methods.map(m => m.toUpperCase()), handler }); -} - -export async function dispatch( - request: Request, - pathSegments: string[], - method: string -): Promise { - for (const entry of routes) { - if (!entry.methods.includes(method)) continue; - const params: Record = {}; - let match = true; - - if (entry.pattern.length !== pathSegments.length) continue; - - for (let i = 0; i < entry.pattern.length; i++) { - const ep = entry.pattern[i]; - const sp = pathSegments[i]; - if (ep.startsWith(':')) { - params[ep.slice(1)] = sp; - } else if (ep !== sp) { - match = false; - break; - } - } - - if (!match) continue; - - let body: unknown = undefined; - const ct = request.headers.get('content-type') || ''; - if (ct.includes('application/json')) { - try { body = await request.json(); } catch { body = undefined; } - } - - const ctx: HandlerContext = { - params, - method, - body, - searchParams: new URL(request.url).searchParams, - }; - - try { - const result = await entry.handler(ctx); - return NextResponse.json(result, { status: 200 }); - } catch (err: any) { - const status = err.status || 500; - return NextResponse.json({ error: err.message || 'Internal error' }, { status }); - } - } - - return NextResponse.json({ error: 'Not found' }, { status: 404 }); -} - -export class HttpError extends Error { - status: number; - constructor(status: number, message: string) { - super(message); - this.status = status; - } -} diff --git a/backend/src/lib/seed.ts b/backend/src/lib/seed.ts deleted file mode 100644 index 83aea4f..0000000 --- a/backend/src/lib/seed.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { getDb } from './db'; - -export function seedDatabase() { - const db = getDb(); - const count = db.prepare('SELECT COUNT(*) as c FROM promo_codes').get() as { c: number }; - if (count.c > 0) return; - - const insert = db.prepare(` - INSERT INTO promo_codes (code, free_currency, donate_currency, dust_currency, max_uses, expires_at) - VALUES (?, ?, ?, ?, ?, ?) - `); - - const codes = [ - ['WELCOME100', 100, 0, 0, 100, null], - ['ZOMBIE500', 500, 50, 0, 50, null], - ['DONATE100', 0, 100, 0, 20, null], - ['DUST250', 0, 0, 250, 30, null], - ]; - - const tx = db.transaction(() => { - for (const c of codes) { - insert.run(...c); - } - }); - tx(); - - console.log('Database seeded with promo codes'); -} diff --git a/backend/tailwind.config.ts b/backend/tailwind.config.ts deleted file mode 100644 index 0d028e7..0000000 --- a/backend/tailwind.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Config } from 'tailwindcss'; -const config: Config = { - content: ['./src/**/*.{ts,tsx}'], - theme: { extend: {} }, - plugins: [], -}; -export default config; diff --git a/backend/tsconfig.json b/backend/tsconfig.json deleted file mode 100644 index fba2bf3..0000000 --- a/backend/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2017", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [{ "name": "next" }], - "paths": { "@/*": ["./src/*"] } - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] -} diff --git a/docs/superpowers/plans/2026-05-29-zombie-invasion-backend.md b/docs/superpowers/plans/2026-05-29-zombie-invasion-backend.md deleted file mode 100644 index 76f754c..0000000 --- a/docs/superpowers/plans/2026-05-29-zombie-invasion-backend.md +++ /dev/null @@ -1,3031 +0,0 @@ -# Zombie Invasion Backend Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) for syntax tracking. - -**Goal:** Build a lightweight Next.js 14 mock backend with SQLite and admin panel for the Zombie Invasion Dota 2 custom game, deployable in a single Docker container. - -**Architecture:** Next.js 14 App Router with a single catch-all API route at `/api/[...path]` that dispatches to domain handlers. Admin panel under `/admin` with cookie-based password auth. SQLite via better-sqlite3 for persistence. All payment endpoints auto-grant instantly. - -**Tech Stack:** Next.js 14, TypeScript, better-sqlite3, Tailwind CSS (admin panel), Docker - -**Location:** All files go in `backend/` directory at the root of this repo. - ---- - -## Task Outline - -### Task 1: Project scaffold -### Task 2: Database layer -### Task 3: Core utilities (auth, seed, router) -### Task 4: Catch-all API route -### Task 5: Player handler -### Task 6: Battle pass handler -### Task 7: Game handler -### Task 8: Payments handler -### Task 9: Leaderboard handler -### Task 10: Cards & decks handler -### Task 11: Equipment handler -### Task 12: Arsenal handler -### Task 13: Marketplace handler -### Task 14: Contracts handler -### Task 15: Admin login & layout -### Task 16: Admin dashboard page -### Task 17: Admin players pages -### Task 18: Admin battle pass pages -### Task 19: Admin matches page -### Task 20: Admin promocodes page -### Task 21: Admin store page -### Task 22: Admin contracts page -### Task 23: Admin arsenal page -### Task 24: Docker setup -### Task 25: Update postman_collection.json - ---- - -### Task 1: Project scaffold - -**Files:** -- Create: `backend/package.json` -- Create: `backend/tsconfig.json` -- Create: `backend/next.config.js` -- Create: `backend/tailwind.config.ts` -- Create: `backend/postcss.config.js` -- Create: `backend/src/app/globals.css` -- Create: `backend/src/app/layout.tsx` -- Create: `backend/src/app/page.tsx` - -- [ ] **Step 1: Create package.json** - -```json -{ - "name": "zombie-invasion-backend", - "version": "1.0.0", - "private": true, - "scripts": { - "dev": "next dev -p 3000", - "build": "next build", - "start": "next start -p 3000" - }, - "dependencies": { - "next": "^14.2.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "better-sqlite3": "^11.0.0", - "typescript": "^5.4.0", - "@types/node": "^20.0.0", - "@types/react": "^18.2.0", - "@types/react-dom": "^18.2.0", - "@types/better-sqlite3": "^7.6.0" - }, - "devDependencies": { - "tailwindcss": "^3.4.0", - "postcss": "^8.4.0", - "autoprefixer": "^10.4.0" - } -} -``` - -- [ ] **Step 2: Create tsconfig.json** - -```json -{ - "compilerOptions": { - "target": "ES2017", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [{ "name": "next" }], - "paths": { "@/*": ["./src/*"] } - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] -} -``` - -- [ ] **Step 3: Create next.config.js** - -```js -/** @type {import('next').NextConfig} */ -const nextConfig = { - output: 'standalone', -}; - -module.exports = nextConfig; -``` - -- [ ] **Step 4: Create tailwind.config.ts** - -```ts -import type { Config } from 'tailwindcss'; -const config: Config = { - content: ['./src/**/*.{ts,tsx}'], - theme: { extend: {} }, - plugins: [], -}; -export default config; -``` - -- [ ] **Step 5: Create postcss.config.js** - -```js -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; -``` - -- [ ] **Step 6: Create globals.css** - -```css -@tailwind base; -@tailwind components; -@tailwind utilities; -``` - -- [ ] **Step 7: Create root layout.tsx** - -```tsx -import type { Metadata } from 'next'; -import './globals.css'; - -export const metadata: Metadata = { - title: 'Zombie Invasion Backend', -}; - -export default function RootLayout({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ); -} -``` - -- [ ] **Step 8: Create root page.tsx** - -```tsx -import { redirect } from 'next/navigation'; - -export default function Home() { - redirect('/admin'); -} -``` - -- [ ] **Step 9: Install dependencies only** - -Run: `cd /Users/achmad/Documents/dota/3728427109/backend && npm install` -Expected: Dependencies installed in node_modules/. (Full build happens in Task 14 after all handlers exist.) - -- [ ] **Step 10: Commit** - -```bash -git add backend/package.json backend/tsconfig.json backend/next.config.js backend/tailwind.config.ts backend/postcss.config.js backend/src/app/globals.css backend/src/app/layout.tsx backend/src/app/page.tsx -git commit -m "feat: scaffold Next.js 14 project" -``` - ---- - -### Task 2: Database layer - -**Files:** -- Create: `backend/src/lib/db.ts` -- Create: `backend/src/lib/seed.ts` - -The database layer initializes SQLite with all tables and provides a singleton `db` export for all handlers. - -- [ ] **Step 1: Create db.ts** - -```ts -import Database from 'better-sqlite3'; -import path from 'path'; -import { seedDatabase } from './seed'; - -const DB_PATH = process.env.DB_PATH || path.join(process.cwd(), 'data', 'zombie_invasion.db'); - -let db: Database.Database; - -export function getDb(): Database.Database { - if (!db) { - const fs = require('fs'); - const dir = path.dirname(DB_PATH); - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } - db = new Database(DB_PATH); - db.pragma('journal_mode = WAL'); - db.pragma('foreign_keys = ON'); - initSchema(db); - seedDatabase(); - } - return db; -} - -function initSchema(db: Database.Database) { - db.exec(` - CREATE TABLE IF NOT EXISTS players ( - steam_id TEXT PRIMARY KEY, - player_name TEXT NOT NULL, - profile_level INTEGER DEFAULT 1, - free_currency INTEGER DEFAULT 0, - donate_currency INTEGER DEFAULT 0, - dust_currency INTEGER DEFAULT 0, - arcade_pack_credits TEXT DEFAULT '{"standard":0,"premium":0}', - sounds_wheel TEXT DEFAULT '{}', - created_at TEXT DEFAULT (datetime('now')), - updated_at TEXT DEFAULT (datetime('now')) - ); - - CREATE TABLE IF NOT EXISTS game_sessions ( - game_id TEXT PRIMARY KEY, - match_id INTEGER, - session_id TEXT, - status TEXT DEFAULT 'active', - created_at TEXT DEFAULT (datetime('now')) - ); - - CREATE TABLE IF NOT EXISTS game_history ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - steam_id TEXT NOT NULL, - game_id TEXT, - match_id INTEGER, - result TEXT, - hero TEXT, - hero_level INTEGER, - difficulty TEXT, - duration INTEGER, - kills INTEGER DEFAULT 0, - deaths INTEGER DEFAULT 0, - score INTEGER DEFAULT 0, - outgoing_damage REAL DEFAULT 0, - incoming_damage REAL DEFAULT 0, - items TEXT, - modifiers TEXT, - aghanim_scepter INTEGER DEFAULT 0, - aghanim_shard INTEGER DEFAULT 0, - gold_earned INTEGER DEFAULT 0, - session_id TEXT, - created_at TEXT DEFAULT (datetime('now')) - ); - - CREATE TABLE IF NOT EXISTS battle_passes ( - steam_id TEXT PRIMARY KEY, - level INTEGER DEFAULT 0, - experience INTEGER DEFAULT 0, - has_premium INTEGER DEFAULT 0, - claimed_rewards TEXT DEFAULT '[]', - claimed_premium_rewards TEXT DEFAULT '[]', - created_at TEXT DEFAULT (datetime('now')), - updated_at TEXT DEFAULT (datetime('now')) - ); - - CREATE TABLE IF NOT EXISTS battle_pass_quests ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - steam_id TEXT NOT NULL, - quest_id TEXT NOT NULL, - type TEXT NOT NULL, - name TEXT, - description TEXT, - progress INTEGER DEFAULT 0, - target INTEGER DEFAULT 1, - completed INTEGER DEFAULT 0, - claimed INTEGER DEFAULT 0, - reward_exp INTEGER DEFAULT 0, - reward_free_currency INTEGER DEFAULT 0, - quality TEXT, - npc TEXT, - target_item TEXT, - created_at TEXT DEFAULT (datetime('now')), - updated_at TEXT DEFAULT (datetime('now')) - ); - - CREATE TABLE IF NOT EXISTS purchases ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - steam_id TEXT NOT NULL, - item_id TEXT NOT NULL, - item_category TEXT, - card_id INTEGER, - price_free INTEGER DEFAULT 0, - price_donate INTEGER DEFAULT 0, - price_dust INTEGER DEFAULT 0, - created_at TEXT DEFAULT (datetime('now')) - ); - - CREATE TABLE IF NOT EXISTS active_effects ( - steam_id TEXT PRIMARY KEY, - effects TEXT DEFAULT '{}', - updated_at TEXT DEFAULT (datetime('now')) - ); - - CREATE TABLE IF NOT EXISTS promo_codes ( - code TEXT PRIMARY KEY, - free_currency INTEGER DEFAULT 0, - donate_currency INTEGER DEFAULT 0, - dust_currency INTEGER DEFAULT 0, - max_uses INTEGER DEFAULT 1, - current_uses INTEGER DEFAULT 0, - expires_at TEXT - ); - - CREATE TABLE IF NOT EXISTS promo_redemptions ( - steam_id TEXT, - code TEXT, - redeemed_at TEXT DEFAULT (datetime('now')), - PRIMARY KEY (steam_id, code) - ); - - CREATE TABLE IF NOT EXISTS card_levels ( - steam_id TEXT PRIMARY KEY, - card_levels TEXT DEFAULT '{}', - updated_at TEXT DEFAULT (datetime('now')) - ); - - CREATE TABLE IF NOT EXISTS decks ( - steam_id TEXT, - deck_index INTEGER, - name TEXT DEFAULT 'My Deck', - cards TEXT DEFAULT '[]', - updated_at TEXT DEFAULT (datetime('now')), - PRIMARY KEY (steam_id, deck_index) - ); - - CREATE TABLE IF NOT EXISTS equipment ( - steam_id TEXT PRIMARY KEY, - equipment TEXT DEFAULT '{}', - updated_at TEXT DEFAULT (datetime('now')) - ); - - CREATE TABLE IF NOT EXISTS arsenal_loadouts ( - steam_id TEXT, - hero_name TEXT, - loadout TEXT DEFAULT '{}', - updated_at TEXT DEFAULT (datetime('now')), - PRIMARY KEY (steam_id, hero_name) - ); - - CREATE TABLE IF NOT EXISTS arsenal_inventory ( - steam_id TEXT, - instance_id TEXT, - item_name TEXT, - quality TEXT, - upgrade_level INTEGER DEFAULT 0, - serial INTEGER, - global_serial INTEGER, - owner_name TEXT, - pinned INTEGER DEFAULT 0, - favorite INTEGER DEFAULT 0, - stats TEXT DEFAULT '[]', - PRIMARY KEY (steam_id, instance_id) - ); - - CREATE TABLE IF NOT EXISTS arsenal_market_listings ( - listing_id TEXT PRIMARY KEY, - steam_id TEXT NOT NULL, - instance_id TEXT, - item_name TEXT, - quality TEXT, - upgrade_level INTEGER DEFAULT 0, - serial INTEGER, - global_serial INTEGER, - price_free INTEGER DEFAULT 0, - status TEXT DEFAULT 'active', - created_at TEXT DEFAULT (datetime('now')) - ); - - CREATE TABLE IF NOT EXISTS arsenal_market_sales ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - listing_id TEXT, - seller_steam_id TEXT, - buyer_steam_id TEXT, - item_name TEXT, - price_free INTEGER, - created_at TEXT DEFAULT (datetime('now')) - ); - - CREATE TABLE IF NOT EXISTS death_sentence_contracts ( - steam_id TEXT PRIMARY KEY, - contracts TEXT DEFAULT '{}', - updated_at TEXT DEFAULT (datetime('now')) - ); - `); -} -``` - -- [ ] **Step 2: Create seed.ts** - -```ts -import { getDb } from './db'; - -export function seedDatabase() { - const db = getDb(); - const count = db.prepare('SELECT COUNT(*) as c FROM promo_codes').get() as { c: number }; - if (count.c > 0) return; - - const insert = db.prepare(` - INSERT INTO promo_codes (code, free_currency, donate_currency, dust_currency, max_uses, expires_at) - VALUES (?, ?, ?, ?, ?, ?) - `); - - const codes = [ - ['WELCOME100', 100, 0, 0, 100, null], - ['ZOMBIE500', 500, 50, 0, 50, null], - ['DONATE100', 0, 100, 0, 20, null], - ['DUST250', 0, 0, 250, 30, null], - ]; - - const tx = db.transaction(() => { - for (const c of codes) { - insert.run(...c); - } - }); - tx(); - - // Seed default quest definitions (will be assigned to players on BP creation) - const seedQuests = [ - { quest_id: 'kill_zombies_1', type: 'kill_zombies', name: 'Zombie Slayer I', description: 'Kill 100 zombies', target: 100, reward_exp: 50, reward_free_currency: 100 }, - { quest_id: 'kill_zombies_2', type: 'kill_zombies', name: 'Zombie Slayer II', description: 'Kill 500 zombies', target: 500, reward_exp: 100, reward_free_currency: 250 }, - { quest_id: 'survive_time_1', type: 'survive_time', name: 'Survivor I', description: 'Survive for 600 seconds', target: 600, reward_exp: 30, reward_free_currency: 50 }, - { quest_id: 'survive_waves_1', type: 'survive_waves', name: 'Wave Breaker I', description: 'Survive 10 waves', target: 10, reward_exp: 40, reward_free_currency: 75 }, - { quest_id: 'buy_black_shop_1', type: 'buy_black_shop', name: 'Black Shopper I', description: 'Buy 5 items from Black Shop', target: 5, reward_exp: 25, reward_free_currency: 50 }, - { quest_id: 'complete_npc_quest_1', type: 'complete_npc_quest', name: 'Helper I', description: 'Complete 3 NPC quests', target: 3, reward_exp: 35, reward_free_currency: 60 }, - { quest_id: 'earn_gold_1', type: 'earn_gold', name: 'Gold Rush I', description: 'Earn 5000 gold', target: 5000, reward_exp: 45, reward_free_currency: 100 }, - ]; - - // Store quest defs in a table or just log them — for now we skip persistence - // since quests are assigned per-player dynamically by the BP handler. - - console.log('Database seeded with promo codes and quest definitions'); -} -``` - -- [ ] **Step 3: Commit** - -```bash -git add backend/src/lib/db.ts backend/src/lib/seed.ts -git commit -m "feat: add SQLite schema and seed data" -``` - ---- - -### Task 3: API router - -**Files:** -- Create: `backend/src/lib/router.ts` - -- [ ] **Step 1: Create router.ts** - -This is the routing engine for the catch-all API route. It maps URL patterns to handler functions. - -```ts -import { NextResponse } from 'next/server'; - -export type HandlerFn = (ctx: HandlerContext) => unknown | Promise; - -export type HandlerContext = { - params: Record; - method: string; - body: unknown; - searchParams: URLSearchParams; -}; - -type RouteEntry = { - pattern: string[]; - methods: string[]; - handler: HandlerFn; -}; - -const routes: RouteEntry[] = []; - -export function route(pattern: string, methods: string[], handler: HandlerFn) { - const parts = pattern.split('/').filter(Boolean); - routes.push({ pattern: parts, methods: methods.map(m => m.toUpperCase()), handler }); -} - -export async function dispatch( - request: Request, - pathSegments: string[], - method: string -): Promise { - for (const entry of routes) { - if (!entry.methods.includes(method)) continue; - const params: Record = {}; - let match = true; - - if (entry.pattern.length !== pathSegments.length) continue; - - for (let i = 0; i < entry.pattern.length; i++) { - const ep = entry.pattern[i]; - const sp = pathSegments[i]; - if (ep.startsWith(':')) { - params[ep.slice(1)] = sp; - } else if (ep !== sp) { - match = false; - break; - } - } - - if (!match) continue; - - let body: unknown = undefined; - const ct = request.headers.get('content-type') || ''; - if (ct.includes('application/json')) { - try { body = await request.json(); } catch { body = undefined; } - } - - const ctx: HandlerContext = { - params, - method, - body, - searchParams: new URL(request.url).searchParams, - }; - - try { - const result = await entry.handler(ctx); - return NextResponse.json(result, { status: 200 }); - } catch (err: any) { - const status = err.status || 500; - return NextResponse.json({ error: err.message || 'Internal error' }, { status }); - } - } - - return NextResponse.json({ error: 'Not found' }, { status: 404 }); -} - -// Helper to create typed errors -export class HttpError extends Error { - status: number; - constructor(status: number, message: string) { - super(message); - this.status = status; - } -} -``` - -- [ ] **Step 3: Commit** - -```bash -git add backend/src/lib/router.ts -git commit -m "feat: add API router" -``` - ---- - -### Task 4: Catch-all API route - -**Files:** -- Create: `backend/src/app/api/[...path]/route.ts` - -This file registers all route handlers and exports GET/POST/PUT handlers that delegate to the router. - -- [ ] **Step 1: Create the catch-all route** - -```ts -import { dispatch } from '@/lib/router'; -import { NextRequest, NextResponse } from 'next/server'; - -// Import all handlers to register their routes -import '@/lib/handlers/player'; -import '@/lib/handlers/battlepass'; -import '@/lib/handlers/game'; -import '@/lib/handlers/payments'; -import '@/lib/handlers/leaderboard'; -import '@/lib/handlers/cards'; -import '@/lib/handlers/equipment'; -import '@/lib/handlers/arsenal'; -import '@/lib/handlers/marketplace'; -import '@/lib/handlers/contracts'; - -export const dynamic = 'force-dynamic'; - -export async function GET(request: NextRequest, { params }: { params: { path: string[] } }) { - return dispatch(request, params.path, 'GET'); -} - -export async function POST(request: NextRequest, { params }: { params: { path: string[] } }) { - return dispatch(request, params.path, 'POST'); -} - -export async function PUT(request: NextRequest, { params }: { params: { path: string[] } }) { - return dispatch(request, params.path, 'PUT'); -} -``` - -- [ ] **Step 2: Create handlers directory** - -Run: `mkdir -p backend/src/lib/handlers` - -- [ ] **Step 3: Commit** - -```bash -git add backend/src/app/api/[...path]/route.ts -git commit -m "feat: add catch-all API route with handler imports" -``` - ---- - -### Task 5: Player handler - -**Files:** -- Create: `backend/src/lib/handlers/player.ts` - -This handler covers all `/player/:steamId/*` endpoints. The Lua client makes many different requests here. - -- [ ] **Step 1: Create player handler** - -```ts -import { route, HandlerContext, HttpError } from '@/lib/router'; -import { getDb } from '@/lib/db'; - -// POST /player — Create profile -route('player', ['POST'], (ctx: HandlerContext) => { - const { steam_id, player_name } = ctx.body as any; - if (!steam_id) throw new HttpError(400, 'steam_id is required'); - const db = getDb(); - const existing = db.prepare('SELECT * FROM players WHERE steam_id = ?').get(steam_id) as any; - if (existing) { - // Return existing on conflict (game handles 409 silently) - return existing; - } - db.prepare('INSERT INTO players (steam_id, player_name) VALUES (?, ?)').run(steam_id, player_name || ''); - // Also create battle pass - try { - db.prepare('INSERT OR IGNORE INTO battle_passes (steam_id) VALUES (?)').run(steam_id); - } catch {} - const player = db.prepare('SELECT * FROM players WHERE steam_id = ?').get(steam_id); - return player; -}); - -// GET /player/:steamId — Get profile -route('player/:steamId', ['GET'], (ctx: HandlerContext) => { - const db = getDb(); - const player = db.prepare('SELECT * FROM players WHERE steam_id = ?').get(ctx.params.steamId) as any; - if (!player) throw new HttpError(404, 'Player not found'); - return { - ...player, - recentGames: [], - stats: { - total_games: 0, - total_wins: 0, - rating: 0, - }, - }; -}); - -// GET /player/:steamId/history — Match history -route('player/:steamId/history', ['GET'], (ctx: HandlerContext) => { - const limit = parseInt(ctx.searchParams.get('limit') || '10'); - const offset = parseInt(ctx.searchParams.get('offset') || '0'); - const db = getDb(); - const games = db.prepare( - 'SELECT * FROM game_history WHERE steam_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?' - ).all(ctx.params.steamId, limit, offset); - return games; -}); - -// GET /player/:steamId/currency — Get currency -route('player/:steamId/currency', ['GET'], (ctx: HandlerContext) => { - const db = getDb(); - const player = db.prepare('SELECT free_currency, donate_currency, dust_currency FROM players WHERE steam_id = ?').get(ctx.params.steamId) as any; - if (!player) throw new HttpError(404, 'Player not found'); - return player; -}); - -// PUT /player/:steamId/currency — Save currency -route('player/:steamId/currency', ['PUT'], (ctx: HandlerContext) => { - const { free_currency, donate_currency, dust_currency } = ctx.body as any; - const db = getDb(); - const player = db.prepare('SELECT * FROM players WHERE steam_id = ?').get(ctx.params.steamId) as any; - if (!player) throw new HttpError(404, 'Player not found'); - db.prepare(` - UPDATE players SET free_currency = ?, donate_currency = ?, dust_currency = ?, updated_at = datetime('now') - WHERE steam_id = ? - `).run( - free_currency ?? player.free_currency, - donate_currency ?? player.donate_currency, - dust_currency ?? player.dust_currency, - ctx.params.steamId - ); - return { success: true }; -}); - -// POST /player/:steamId/currency/give — Grant currency -route('player/:steamId/currency/give', ['POST'], (ctx: HandlerContext) => { - const { free_amount, donate_amount, dust_amount } = ctx.body as any; - const db = getDb(); - const player = db.prepare('SELECT * FROM players WHERE steam_id = ?').get(ctx.params.steamId) as any; - if (!player) throw new HttpError(404, 'Player not found'); - db.prepare(` - UPDATE players SET free_currency = free_currency + ?, donate_currency = donate_currency + ?, - dust_currency = dust_currency + ?, updated_at = datetime('now') WHERE steam_id = ? - `).run( - free_amount || 0, - donate_amount || 0, - dust_amount || 0, - ctx.params.steamId - ); - return { success: true }; -}); - -// POST /player/:steamId/purchases — Record purchase -route('player/:steamId/purchases', ['POST'], (ctx: HandlerContext) => { - const { item_id, item_category, card_id, price_free, price_donate, price_dust } = ctx.body as any; - const db = getDb(); - db.prepare(` - INSERT INTO purchases (steam_id, item_id, item_category, card_id, price_free, price_donate, price_dust) - VALUES (?, ?, ?, ?, ?, ?, ?) - `).run(ctx.params.steamId, item_id, item_category || 'items', card_id || null, price_free || 0, price_donate || 0, price_dust || 0); - return { success: true }; -}); - -// POST /player/:steamId/promo/redeem — Redeem promo code -route('player/:steamId/promo/redeem', ['POST'], (ctx: HandlerContext) => { - const { code } = ctx.body as any; - if (!code) throw new HttpError(400, 'Code is required'); - const db = getDb(); - const promo = db.prepare('SELECT * FROM promo_codes WHERE code = ?').get(code.toUpperCase()) as any; - if (!promo) throw new HttpError(404, 'Promo code not found'); - if (promo.expires_at && new Date(promo.expires_at) < new Date()) throw new HttpError(400, 'Code expired'); - if (promo.current_uses >= promo.max_uses) throw new HttpError(400, 'Code fully redeemed'); - - // Check if already redeemed by this player - const existing = db.prepare('SELECT * FROM promo_redemptions WHERE steam_id = ? AND code = ?').get(ctx.params.steamId, code.toUpperCase()); - if (existing) throw new HttpError(400, 'Code already redeemed'); - - db.prepare(` - UPDATE players SET free_currency = free_currency + ?, donate_currency = donate_currency + ?, - dust_currency = dust_currency + ?, updated_at = datetime('now') WHERE steam_id = ? - `).run(promo.free_currency, promo.donate_currency, promo.dust_currency, ctx.params.steamId); - - db.prepare('UPDATE promo_codes SET current_uses = current_uses + 1 WHERE code = ?').run(code.toUpperCase()); - db.prepare('INSERT INTO promo_redemptions (steam_id, code) VALUES (?, ?)').run(ctx.params.steamId, code.toUpperCase()); - - const player = db.prepare('SELECT free_currency, donate_currency, dust_currency FROM players WHERE steam_id = ?').get(ctx.params.steamId); - return { success: true, rewards: { free_currency: promo.free_currency, donate_currency: promo.donate_currency, dust_currency: promo.dust_currency }, currency: player }; -}); - -// GET /player/:steamId/sounds_wheel — Get sounds wheel -route('player/:steamId/sounds_wheel', ['GET'], (ctx: HandlerContext) => { - const db = getDb(); - const player = db.prepare('SELECT sounds_wheel FROM players WHERE steam_id = ?').get(ctx.params.steamId) as any; - if (!player) throw new HttpError(404, 'Player not found'); - return { sounds_wheel: JSON.parse(player.sounds_wheel || '{}') }; -}); - -// PUT /player/:steamId/sounds_wheel — Save sounds wheel -route('player/:steamId/sounds_wheel', ['PUT'], (ctx: HandlerContext) => { - const { sounds_wheel } = ctx.body as any; - const db = getDb(); - db.prepare("UPDATE players SET sounds_wheel = ?, updated_at = datetime('now') WHERE steam_id = ?") - .run(JSON.stringify(sounds_wheel || {}), ctx.params.steamId); - return { success: true }; -}); - -// POST /player/:steamId/deal-purchase — Buy a deal -route('player/:steamId/deal-purchase', ['POST'], (ctx: HandlerContext) => { - const { deal_key } = ctx.body as any; - // Auto-grant: mock successful deal purchase - return { success: true, ok: true, item_id: 'deal_' + deal_key, item_category: 'items' }; -}); - -// GET /player/:steamId/active_effects — Get active effects -route('player/:steamId/active_effects', ['GET'], (ctx: HandlerContext) => { - const db = getDb(); - const row = db.prepare('SELECT effects FROM active_effects WHERE steam_id = ?').get(ctx.params.steamId) as any; - return { active_effects: row ? JSON.parse(row.effects) : {} }; -}); - -// PUT /player/:steamId/active_effects — Save active effects -route('player/:steamId/active_effects', ['PUT'], (ctx: HandlerContext) => { - const { active_effects } = ctx.body as any; - const db = getDb(); - db.prepare(` - INSERT INTO active_effects (steam_id, effects, updated_at) VALUES (?, ?, datetime('now')) - ON CONFLICT(steam_id) DO UPDATE SET effects = ?, updated_at = datetime('now') - `).run(ctx.params.steamId, JSON.stringify(active_effects || {}), JSON.stringify(active_effects || {})); - return { success: true }; -}); -``` - -- [ ] **Step 2: Commit** - -```bash -git add backend/src/lib/handlers/player.ts -git commit -m "feat: add player handler with all endpoints" -``` - ---- - -### Task 6: Battle pass handler - -**Files:** -- Create: `backend/src/lib/handlers/battlepass.ts` - -Covers all `/battlepass/*` endpoints. The game client manages quest logic locally and syncs progress here. - -- [ ] **Step 1: Create battlepass handler** - -```ts -import { route, HandlerContext, HttpError } from '@/lib/router'; -import { getDb } from '@/lib/db'; - -const QUEST_DEFS = [ - { quest_id: 'kill_zombies_1', type: 'kill_zombies', name: 'Zombie Slayer I', description: 'Kill 100 zombies', target: 100, reward_exp: 50, reward_free_currency: 100 }, - { quest_id: 'kill_zombies_2', type: 'kill_zombies', name: 'Zombie Slayer II', description: 'Kill 500 zombies', target: 500, reward_exp: 100, reward_free_currency: 250 }, - { quest_id: 'survive_time_1', type: 'survive_time', name: 'Survivor I', description: 'Survive for 600 seconds', target: 600, reward_exp: 30, reward_free_currency: 50 }, - { quest_id: 'survive_waves_1', type: 'survive_waves', name: 'Wave Breaker I', description: 'Survive 10 waves', target: 10, reward_exp: 40, reward_free_currency: 75 }, - { quest_id: 'buy_black_shop_1', type: 'buy_black_shop', name: 'Black Shopper I', description: 'Buy 5 items from Black Shop', target: 5, reward_exp: 25, reward_free_currency: 50 }, - { quest_id: 'complete_npc_quest_1', type: 'complete_npc_quest', name: 'Helper I', description: 'Complete 3 NPC quests', target: 3, reward_exp: 35, reward_free_currency: 60 }, - { quest_id: 'earn_gold_1', type: 'earn_gold', name: 'Gold Rush I', description: 'Earn 5000 gold', target: 5000, reward_exp: 45, reward_free_currency: 100 }, - { quest_id: 'hero_level_1', type: 'hero_level', name: 'Stronger I', description: 'Reach level 10', target: 10, reward_exp: 30, reward_free_currency: 50 }, - { quest_id: 'cook_grilled_meat_1', type: 'cook_grilled_meat', name: 'Chef I', description: 'Cook grilled meat', target: 1, reward_exp: 20, reward_free_currency: 25 }, - { quest_id: 'use_campfire_1', type: 'use_campfire', name: 'Camper I', description: 'Use campfire 5 times', target: 5, reward_exp: 15, reward_free_currency: 25 }, - { quest_id: 'tip_teammate_1', type: 'tip_teammate', name: 'Friendly I', description: 'Tip teammates 3 times', target: 3, reward_exp: 20, reward_free_currency: 30 }, - { quest_id: 'deal_damage_1', type: 'deal_damage', name: 'Berserker I', description: 'Deal 50000 damage', target: 50000, reward_exp: 60, reward_free_currency: 150 }, - { quest_id: 'collect_item_1', type: 'collect_item', name: 'Collector I', description: 'Collect a rare item', target: 1, reward_exp: 40, reward_free_currency: 80, target_item: 'rare' }, -]; - -// POST /battlepass — Create BP -route('battlepass', ['POST'], (ctx: HandlerContext) => { - const { steam_id } = ctx.body as any; - if (!steam_id) throw new HttpError(400, 'steam_id required'); - const db = getDb(); - db.prepare(` - INSERT OR IGNORE INTO battle_passes (steam_id, level, experience) VALUES (?, 0, 0) - `).run(steam_id); - - // Assign default quests - const existing = db.prepare('SELECT COUNT(*) as c FROM battle_pass_quests WHERE steam_id = ?').get(steam_id) as any; - if (existing.c === 0) { - const insert = db.prepare(` - INSERT INTO battle_pass_quests (steam_id, quest_id, type, name, description, target, reward_exp, reward_free_currency) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - `); - for (const q of QUEST_DEFS) { - insert.run(steam_id, q.quest_id, q.type, q.name, q.description, q.target, q.reward_exp, q.reward_free_currency); - } - } - - return { success: true }; -}); - -// GET /battlepass/:steamId — Get BP data -route('battlepass/:steamId', ['GET'], (ctx: HandlerContext) => { - const db = getDb(); - let bp = db.prepare('SELECT * FROM battle_passes WHERE steam_id = ?').get(ctx.params.steamId) as any; - if (!bp) { - db.prepare('INSERT OR IGNORE INTO battle_passes (steam_id) VALUES (?)').run(ctx.params.steamId); - bp = db.prepare('SELECT * FROM battle_passes WHERE steam_id = ?').get(ctx.params.steamId); - } - return { - level: bp.level, - experience: bp.experience, - has_premium: bp.has_premium === 1, - claimed_rewards: JSON.parse(bp.claimed_rewards || '[]'), - claimed_premium_rewards: JSON.parse(bp.claimed_premium_rewards || '[]'), - }; -}); - -// GET /battlepass/:steamId/quests — Get quests -route('battlepass/:steamId/quests', ['GET'], (ctx: HandlerContext) => { - const db = getDb(); - const quests = db.prepare( - 'SELECT * FROM battle_pass_quests WHERE steam_id = ? ORDER BY id' - ).all(ctx.params.steamId); - return { quests }; -}); - -// POST /battlepass/:steamId/quests/progress — Sync quest progress -route('battlepass/:steamId/quests/progress', ['POST'], (ctx: HandlerContext) => { - const { quest_id, progress } = ctx.body as any; - if (!quest_id) throw new HttpError(400, 'quest_id required'); - const db = getDb(); - const quest = db.prepare( - 'SELECT * FROM battle_pass_quests WHERE steam_id = ? AND quest_id = ?' - ).get(ctx.params.steamId, quest_id) as any; - if (!quest) { - // Auto-create quest - db.prepare(` - INSERT INTO battle_pass_quests (steam_id, quest_id, type, name, target, progress) - VALUES (?, ?, 'custom', ?, 1, ?) - `).run(ctx.params.steamId, quest_id, quest_id, progress || 0); - return { success: true, completed: false, progress: progress || 0 }; - } - const newProgress = Math.min(progress ?? quest.progress, quest.target); - const completed = newProgress >= quest.target ? 1 : 0; - db.prepare(` - UPDATE battle_pass_quests SET progress = ?, completed = ?, updated_at = datetime('now') - WHERE id = ? - `).run(newProgress, completed, quest.id); - return { success: true, completed: completed === 1, progress: newProgress }; -}); - -// POST /battlepass/:steamId/quests/claim — Claim quest reward -route('battlepass/:steamId/quests/claim', ['POST'], (ctx: HandlerContext) => { - const { quest_id } = ctx.body as any; - if (!quest_id) throw new HttpError(400, 'quest_id required'); - const db = getDb(); - const quest = db.prepare( - 'SELECT * FROM battle_pass_quests WHERE steam_id = ? AND quest_id = ?' - ).get(ctx.params.steamId, quest_id) as any; - if (!quest) throw new HttpError(404, 'Quest not found'); - if (!quest.completed) throw new HttpError(400, 'Quest not completed'); - if (quest.claimed) throw new HttpError(400, 'Already claimed'); - - db.prepare('UPDATE battle_pass_quests SET claimed = 1, updated_at = datetime(\'now\') WHERE id = ?').run(quest.id); - - // Grant rewards - db.prepare(` - UPDATE players SET free_currency = free_currency + ?, updated_at = datetime('now') WHERE steam_id = ? - `).run(quest.reward_free_currency, ctx.params.steamId); - - // Add BP XP - db.prepare(` - UPDATE battle_passes SET experience = experience + ?, updated_at = datetime('now') WHERE steam_id = ? - `).run(quest.reward_exp, ctx.params.steamId); - - const bp = db.prepare('SELECT level, experience FROM battle_passes WHERE steam_id = ?').get(ctx.params.steamId) as any; - - return { - success: true, - reward_exp: quest.reward_exp, - reward_free_currency: quest.reward_free_currency, - new_level: bp.level, - new_experience: bp.experience, - }; -}); - -// POST /battlepass/:steamId/hero-played — Record hero played -route('battlepass/:steamId/hero-played', ['POST'], (ctx: HandlerContext) => { - const { hero_name } = ctx.body as any; - return { success: true }; -}); - -// POST /battlepass/:steamId/claim — Claim BP level reward (free) -route('battlepass/:steamId/claim', ['POST'], (ctx: HandlerContext) => { - const { steam_id, level } = ctx.body as any; - const db = getDb(); - const bp = db.prepare('SELECT * FROM battle_passes WHERE steam_id = ?').get(ctx.params.steamId) as any; - if (!bp) throw new HttpError(404, 'BP not found'); - let claimed = JSON.parse(bp.claimed_rewards || '[]'); - if (!claimed.includes(level)) { - claimed.push(level); - db.prepare("UPDATE battle_passes SET claimed_rewards = ?, updated_at = datetime('now') WHERE steam_id = ?") - .run(JSON.stringify(claimed), ctx.params.steamId); - } - return { success: true, level, currency_granted: { free_currency: level * 250, donate_currency: 0 } }; -}); - -// POST /battlepass/:steamId/claim-premium — Claim BP premium reward -route('battlepass/:steamId/claim-premium', ['POST'], (ctx: HandlerContext) => { - const { steam_id, level } = ctx.body as any; - const db = getDb(); - const bp = db.prepare('SELECT * FROM battle_passes WHERE steam_id = ?').get(ctx.params.steamId) as any; - if (!bp) throw new HttpError(404, 'BP not found'); - let claimed = JSON.parse(bp.claimed_premium_rewards || '[]'); - if (!claimed.includes(level)) { - claimed.push(level); - db.prepare("UPDATE battle_passes SET claimed_premium_rewards = ?, updated_at = datetime('now') WHERE steam_id = ?") - .run(JSON.stringify(claimed), ctx.params.steamId); - } - return { success: true, level, currency_granted: { free_currency: level * 250, donate_currency: level * 100 } }; -}); - -// POST /battlepass/:steamId/claim-all — Claim all rewards -route('battlepass/:steamId/claim-all', ['POST'], (ctx: HandlerContext) => { - const { steam_id } = ctx.body as any; - const db = getDb(); - const bp = db.prepare('SELECT * FROM battle_passes WHERE steam_id = ?').get(ctx.params.steamId) as any; - if (!bp) throw new HttpError(404, 'BP not found'); - - const unclaimedFree: number[] = []; - const unclaimedPremium: number[] = []; - const claimedFree = JSON.parse(bp.claimed_rewards || '[]'); - const claimedPremium = JSON.parse(bp.claimed_premium_rewards || '[]'); - - for (let lvl = 1; lvl <= bp.level; lvl++) { - if (!claimedFree.includes(lvl)) unclaimedFree.push(lvl); - if (bp.has_premium && !claimedPremium.includes(lvl)) unclaimedPremium.push(lvl); - } - - db.prepare("UPDATE battle_passes SET claimed_rewards = ?, claimed_premium_rewards = ?, updated_at = datetime('now') WHERE steam_id = ?") - .run(JSON.stringify([...claimedFree, ...unclaimedFree]), - JSON.stringify([...claimedPremium, ...unclaimedPremium]), - ctx.params.steamId); - - return { - success: true, - free_levels: unclaimedFree, - premium_levels: unclaimedPremium, - currency_granted: { free_currency: unclaimedFree.length * 250 + unclaimedPremium.length * 250, donate_currency: unclaimedPremium.length * 100 }, - }; -}); - -// POST /battlepass/:steamId/buy-premium — Buy premium BP -route('battlepass/:steamId/buy-premium', ['POST'], (ctx: HandlerContext) => { - const db = getDb(); - db.prepare("UPDATE battle_passes SET has_premium = 1, updated_at = datetime('now') WHERE steam_id = ?") - .run(ctx.params.steamId); - return { success: true }; -}); - -// POST /battlepass/:steamId/addexp — Add BP XP -route('battlepass/:steamId/addexp', ['POST'], (ctx: HandlerContext) => { - const { experience } = ctx.body as any; - const db = getDb(); - const bp = db.prepare('SELECT * FROM battle_passes WHERE steam_id = ?').get(ctx.params.steamId) as any; - if (!bp) throw new HttpError(404, 'BP not found'); - - const newExp = bp.experience + (experience || 0); - const levelUp = Math.floor(newExp / 1000); - const newLevel = bp.level + levelUp; - const remainder = newExp % 1000; - - db.prepare(` - UPDATE battle_passes SET experience = ?, level = ?, updated_at = datetime('now') WHERE steam_id = ? - `).run(remainder, newLevel, ctx.params.steamId); - - return { level: newLevel, experience: remainder, level_up: levelUp > 0 }; -}); -``` - -- [ ] **Step 2: Commit** - -```bash -git add backend/src/lib/handlers/battlepass.ts -git commit -m "feat: add battle pass handler with quests, rewards, XP" -``` - ---- - -### Task 7: Game handler - -**Files:** -- Create: `backend/src/lib/handlers/game.ts` - -- [ ] **Step 1: Create game handler** - -```ts -import { route, HandlerContext, HttpError } from '@/lib/router'; -import { getDb } from '@/lib/db'; - -// POST /game/start — Register game start -route('game/start', ['POST'], (ctx: HandlerContext) => { - const { steam_id, hero, hero_level, difficulty, player_name, match_id, session_id, session_participants } = ctx.body as any; - if (!steam_id) throw new HttpError(400, 'steam_id required'); - const db = getDb(); - - // Generate IDs - const gameId = `game_${Date.now()}_${Math.floor(Math.random() * 100000)}`; - const newMatchId = match_id || Math.floor(Math.random() * 100000000); - - // Store session - db.prepare(` - INSERT OR REPLACE INTO game_sessions (game_id, match_id, session_id, status) - VALUES (?, ?, ?, 'active') - `).run(gameId, newMatchId, session_id || ''); - - return { game_id: gameId, match_id: newMatchId }; -}); - -// POST /game/heartbeat — Match heartbeat -route('game/heartbeat', ['POST'], (ctx: HandlerContext) => { - return { success: true }; -}); - -// POST /game — Save game result -route('game', ['POST'], (ctx: HandlerContext) => { - const { - steam_id, result, duration, kills, deaths, score, outgoing_damage, incoming_damage, - hero, hero_level, items, modifiers, aghanim_scepter, aghanim_shard, gold_earned, - difficulty, session_id, game_id, - } = ctx.body as any; - if (!steam_id) throw new HttpError(400, 'steam_id required'); - - const db = getDb(); - db.prepare(` - INSERT INTO game_history (steam_id, game_id, result, duration, kills, deaths, score, - outgoing_damage, incoming_damage, hero, hero_level, items, modifiers, - aghanim_scepter, aghanim_shard, gold_earned, difficulty, session_id) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `).run( - steam_id, game_id || null, result || 'loss', duration || 0, kills || 0, deaths || 0, - score || 0, outgoing_damage || 0, incoming_damage || 0, hero || '', hero_level || 1, - items || '', modifiers || '', aghanim_scepter ? 1 : 0, aghanim_shard ? 1 : 0, - gold_earned || 0, difficulty || 'normal', session_id || '' - ); - - // Update session status - if (game_id) { - db.prepare("UPDATE game_sessions SET status = 'completed' WHERE game_id = ?").run(game_id); - } - - return { success: true }; -}); - -// GET /game/:id/players — Get game players -route('game/:id/players', ['GET'], (ctx: HandlerContext) => { - const db = getDb(); - const session = db.prepare('SELECT * FROM game_sessions WHERE game_id = ? OR match_id = ?') - .get(ctx.params.id, parseInt(ctx.params.id) || 0) as any; - if (!session) return { players: [] }; - - const players = db.prepare( - 'SELECT DISTINCT steam_id, hero, hero_level, result FROM game_history WHERE match_id = ? OR game_id = ?' - ).all(session.match_id, session.game_id); - - return { party_players: players, players }; -}); -``` - -- [ ] **Step 2: Commit** - -```bash -git add backend/src/lib/handlers/game.ts -git commit -m "feat: add game/match handler" -``` - ---- - -### Task 8: Payments handler - -**Files:** -- Create: `backend/src/lib/handlers/payments.ts` - -Auto-grant — no real payment processing. - -- [ ] **Step 1: Create payments handler** - -```ts -import { route, HandlerContext, HttpError } from '@/lib/router'; -import { getDb } from '@/lib/db'; - -// POST /payments/robokassa/link — Auto-grant purchased currency -route('payments/robokassa/link', ['POST'], (ctx: HandlerContext) => { - const { steam_id, amount_rub } = ctx.body as any; - if (!steam_id) throw new HttpError(400, 'steam_id required'); - const db = getDb(); - - // Convert rubles to donate shards (e.g., 1 RUB = 10 shards) - const donateShards = (amount_rub || 100) * 10; - db.prepare('UPDATE players SET donate_currency = donate_currency + ?, updated_at = datetime(\'now\') WHERE steam_id = ?') - .run(donateShards, steam_id); - - return { - ok: true, - payment_url: '', - donate_shards: donateShards, - inv_id: Math.floor(Math.random() * 100000), - }; -}); - -// POST /payments/bundles/link — Auto-grant bundle -route('payments/bundles/link', ['POST'], (ctx: HandlerContext) => { - const { steam_id, bundle_id } = ctx.body as any; - if (!steam_id) throw new HttpError(400, 'steam_id required'); - const db = getDb(); - - db.prepare('UPDATE players SET free_currency = free_currency + 500, donate_currency = donate_currency + 200, updated_at = datetime(\'now\') WHERE steam_id = ?') - .run(steam_id); - - return { - ok: true, - payment_url: '', - inv_id: Math.floor(Math.random() * 100000), - message: 'Bundle granted', - }; -}); - -// GET /payments/deals?steam_id= — Get deals catalog -route('payments/deals', ['GET'], (ctx: HandlerContext) => { - return { - ok: true, - bundles: [ - { id: 'starter_bundle', name: 'Starter Pack', description: 'Get started with 500 shards', price_free: 0, price_donate: 0, items: [{ item_id: 'starter_pack', name: 'Starter Pack' }] }, - { id: 'hero_bundle_1', name: 'Hero Bundle I', description: 'Unlock a random hero', price_free: 1000, price_donate: 0, items: [{ item_id: 'hero_bundle_1', name: 'Hero Bundle' }] }, - ], - daily: { available: true, items: [] }, - weekly: { available: true, items: [] }, - player_created_at_unix: Math.floor(Date.now() / 1000), - }; -}); -``` - ---- - -### Task 9: Leaderboard handler - -**Files:** -- Create: `backend/src/lib/handlers/leaderboard.ts` - -- [ ] **Step 1: Create leaderboard handler** - -```ts -import { route, HandlerContext } from '@/lib/router'; -import { getDb } from '@/lib/db'; - -// GET /leaderboard?limit=&offset=&board= -route('leaderboard', ['GET'], (ctx: HandlerContext) => { - const limit = parseInt(ctx.searchParams.get('limit') || '20'); - const offset = parseInt(ctx.searchParams.get('offset') || '0'); - const board = ctx.searchParams.get('board') || 'rating'; - const db = getDb(); - - let rows: any[]; - if (board === 'wealth') { - rows = db.prepare( - 'SELECT steam_id, player_name, (free_currency + donate_currency) as score, free_currency, donate_currency FROM players ORDER BY score DESC LIMIT ? OFFSET ?' - ).all(limit, offset); - } else { - // rating board: based on wins - rows = db.prepare(` - SELECT p.steam_id, p.player_name, COUNT(CASE WHEN gh.result = 'win' THEN 1 END) as wins, - COUNT(gh.id) as total_games, - (COUNT(CASE WHEN gh.result = 'win' THEN 1 END) * 100.0 / MAX(COUNT(gh.id), 1)) as win_rate - FROM players p LEFT JOIN game_history gh ON p.steam_id = gh.steam_id - GROUP BY p.steam_id ORDER BY wins DESC LIMIT ? OFFSET ? - `).all(limit, offset); - } - - return { - leaderboard: rows, - total: (db.prepare('SELECT COUNT(*) as c FROM players').get() as any).c, - board, - }; -}); -``` - -- [ ] **Step 2: Commit** - -```bash -git add backend/src/lib/handlers/leaderboard.ts -git commit -m "feat: add leaderboard handler" -``` - ---- - -### Task 10: Cards & decks handler - -**Files:** -- Create: `backend/src/lib/handlers/cards.ts` - -- [ ] **Step 1: Create cards handler** - -```ts -import { route, HandlerContext, HttpError } from '@/lib/router'; -import { getDb } from '@/lib/db'; - -// GET /player/:steamId/card-levels -route('player/:steamId/card-levels', ['GET'], (ctx: HandlerContext) => { - const db = getDb(); - const row = db.prepare('SELECT card_levels FROM card_levels WHERE steam_id = ?').get(ctx.params.steamId) as any; - return { card_levels: row ? JSON.parse(row.card_levels) : {} }; -}); - -// PUT /player/:steamId/card-levels -route('player/:steamId/card-levels', ['PUT'], (ctx: HandlerContext) => { - const { card_levels } = ctx.body as any; - const db = getDb(); - db.prepare(` - INSERT INTO card_levels (steam_id, card_levels, updated_at) VALUES (?, ?, datetime('now')) - ON CONFLICT(steam_id) DO UPDATE SET card_levels = ?, updated_at = datetime('now') - `).run(ctx.params.steamId, JSON.stringify(card_levels || {}), JSON.stringify(card_levels || {})); - return { success: true }; -}); - -// GET /player/:steamId/decks -route('player/:steamId/decks', ['GET'], (ctx: HandlerContext) => { - const db = getDb(); - const decks = db.prepare('SELECT * FROM decks WHERE steam_id = ? ORDER BY deck_index').all(ctx.params.steamId); - return decks.map((d: any) => ({ ...d, cards: JSON.parse(d.cards || '[]') })); -}); - -// GET /player/:steamId/decks/:index -route('player/:steamId/decks/:index', ['GET'], (ctx: HandlerContext) => { - const db = getDb(); - const deck = db.prepare('SELECT * FROM decks WHERE steam_id = ? AND deck_index = ?').get(ctx.params.steamId, ctx.params.index) as any; - if (!deck) return { name: 'New Deck', cards: [] }; - return { ...deck, cards: JSON.parse(deck.cards || '[]') }; -}); - -// PUT /player/:steamId/decks/:index -route('player/:steamId/decks/:index', ['PUT'], (ctx: HandlerContext) => { - const { name, cards } = ctx.body as any; - const db = getDb(); - db.prepare(` - INSERT INTO decks (steam_id, deck_index, name, cards, updated_at) VALUES (?, ?, ?, ?, datetime('now')) - ON CONFLICT(steam_id, deck_index) DO UPDATE SET name = ?, cards = ?, updated_at = datetime('now') - `).run(ctx.params.steamId, parseInt(ctx.params.index), name || 'My Deck', JSON.stringify(cards || []), name || 'My Deck', JSON.stringify(cards || [])); - return { success: true }; -}); -``` - -- [ ] **Step 2: Commit** - -```bash -git add backend/src/lib/handlers/cards.ts -git commit -m "feat: add cards and decks handler" -``` - ---- - -### Task 11: Equipment handler - -**Files:** -- Create: `backend/src/lib/handlers/equipment.ts` - -- [ ] **Step 1: Create equipment handler** - -```ts -import { route, HandlerContext, HttpError } from '@/lib/router'; -import { getDb } from '@/lib/db'; - -// GET /player/:steamId/equipment -route('player/:steamId/equipment', ['GET'], (ctx: HandlerContext) => { - const db = getDb(); - const row = db.prepare('SELECT equipment FROM equipment WHERE steam_id = ?').get(ctx.params.steamId) as any; - return { equipment: row ? JSON.parse(row.equipment) : {} }; -}); - -// PUT /player/:steamId/equipment -route('player/:steamId/equipment', ['PUT'], (ctx: HandlerContext) => { - const { equipment } = ctx.body as any; - const db = getDb(); - db.prepare(` - INSERT INTO equipment (steam_id, equipment, updated_at) VALUES (?, ?, datetime('now')) - ON CONFLICT(steam_id) DO UPDATE SET equipment = ?, updated_at = datetime('now') - `).run(ctx.params.steamId, JSON.stringify(equipment || {}), JSON.stringify(equipment || {})); - return { success: true }; -}); - -// POST /player/:steamId/equipment/drop -route('player/:steamId/equipment/drop', ['POST'], (ctx: HandlerContext) => { - return { success: true }; -}); -``` - -- [ ] **Step 2: Commit** - -```bash -git add backend/src/lib/handlers/equipment.ts -git commit -m "feat: add equipment handler" -``` - ---- - -### Task 12: Arsenal handler - -**Files:** -- Create: `backend/src/lib/handlers/arsenal.ts` - -- [ ] **Step 1: Create arsenal handler** - -```ts -import { route, HandlerContext, HttpError } from '@/lib/router'; -import { getDb } from '@/lib/db'; - -// GET /player/:steamId/arsenal_loadouts -route('player/:steamId/arsenal_loadouts', ['GET'], (ctx: HandlerContext) => { - const db = getDb(); - const rows = db.prepare('SELECT * FROM arsenal_loadouts WHERE steam_id = ?').all(ctx.params.steamId); - const loadouts: Record = {}; - for (const r of rows as any[]) { - loadouts[r.hero_name] = JSON.parse(r.loadout); - } - return { arsenal_loadouts: loadouts }; -}); - -// PUT /player/:steamId/arsenal_loadouts -route('player/:steamId/arsenal_loadouts', ['PUT'], (ctx: HandlerContext) => { - const { arsenal_loadouts } = ctx.body as any; - if (!arsenal_loadouts) throw new HttpError(400, 'arsenal_loadouts required'); - const db = getDb(); - const upsert = db.prepare(` - INSERT INTO arsenal_loadouts (steam_id, hero_name, loadout, updated_at) VALUES (?, ?, ?, datetime('now')) - ON CONFLICT(steam_id, hero_name) DO UPDATE SET loadout = ?, updated_at = datetime('now') - `); - const tx = db.transaction(() => { - for (const [hero, loadout] of Object.entries(arsenal_loadouts)) { - upsert.run(ctx.params.steamId, hero, JSON.stringify(loadout), JSON.stringify(loadout)); - } - }); - tx(); - return { success: true }; -}); - -// GET /player/:steamId/arsenal_inventory -route('player/:steamId/arsenal_inventory', ['GET'], (ctx: HandlerContext) => { - const db = getDb(); - const items = db.prepare('SELECT * FROM arsenal_inventory WHERE steam_id = ?').all(ctx.params.steamId); - const instances: Record = {}; - for (const item of items as any[]) { - instances[item.instance_id] = { - instanceId: item.instance_id, - itemName: item.item_name, - quality: item.quality, - upgradeLevel: item.upgrade_level, - serial: item.serial, - globalSerial: item.global_serial, - ownerName: item.owner_name, - pinned: !!item.pinned, - favorite: !!item.favorite, - stats: JSON.parse(item.stats || '[]'), - }; - } - return { arsenal_inventory: { instances } }; -}); - -// PUT /player/:steamId/arsenal_inventory -route('player/:steamId/arsenal_inventory', ['PUT'], (ctx: HandlerContext) => { - const { arsenal_inventory } = ctx.body as any; - if (!arsenal_inventory || !arsenal_inventory.instances) throw new HttpError(400, 'arsenal_inventory.instances required'); - const db = getDb(); - const upsert = db.prepare(` - INSERT INTO arsenal_inventory (steam_id, instance_id, item_name, quality, upgrade_level, serial, global_serial, owner_name, pinned, favorite, stats) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT(steam_id, instance_id) DO UPDATE SET - item_name = excluded.item_name, quality = excluded.quality, upgrade_level = excluded.upgrade_level, - serial = excluded.serial, global_serial = excluded.global_serial, owner_name = excluded.owner_name, - pinned = excluded.pinned, favorite = excluded.favorite, stats = excluded.stats - `); - const tx = db.transaction(() => { - for (const [instId, inst] of Object.entries(arsenal_inventory.instances)) { - const i = inst as any; - upsert.run(ctx.params.steamId, instId, i.itemName || i.item_name, i.quality, i.upgradeLevel || i.upgrade_level || 0, - i.serial, i.globalSerial || i.global_serial, i.ownerName || i.owner_name || '', - i.pinned ? 1 : 0, i.favorite ? 1 : 0, JSON.stringify(i.stats || [])); - } - }); - tx(); - return { success: true }; -}); -``` - -- [ ] **Step 2: Commit** - -```bash -git add backend/src/lib/handlers/arsenal.ts -git commit -m "feat: add arsenal handler (loadouts + inventory)" -``` - ---- - -### Task 13: Marketplace handler - -**Files:** -- Create: `backend/src/lib/handlers/marketplace.ts` - -- [ ] **Step 1: Create marketplace handler** - -```ts -import { route, HandlerContext, HttpError } from '@/lib/router'; -import { getDb } from '@/lib/db'; - -// GET /arsenal_market/listings — Public listings -route('arsenal_market/listings', ['GET'], (ctx: HandlerContext) => { - const db = getDb(); - const listings = db.prepare( - "SELECT * FROM arsenal_market_listings WHERE status = 'active' ORDER BY created_at DESC" - ).all(); - return listings; -}); - -// GET /player/:steamId/arsenal_market/my_listings -route('player/:steamId/arsenal_market/my_listings', ['GET'], (ctx: HandlerContext) => { - const db = getDb(); - const listings = db.prepare( - "SELECT * FROM arsenal_market_listings WHERE steam_id = ? AND status = 'active' ORDER BY created_at DESC" - ).all(ctx.params.steamId); - return listings; -}); - -// GET /player/:steamId/arsenal_market/slots -route('player/:steamId/arsenal_market/slots', ['GET'], (ctx: HandlerContext) => { - return { slots: 5, used: 0 }; -}); - -// GET /player/:steamId/arsenal_market/sales -route('player/:steamId/arsenal_market/sales', ['GET'], (ctx: HandlerContext) => { - const db = getDb(); - const sales = db.prepare( - 'SELECT * FROM arsenal_market_sales WHERE seller_steam_id = ? ORDER BY created_at DESC' - ).all(ctx.params.steamId); - return sales; -}); - -// POST /player/:steamId/arsenal_market/create -route('player/:steamId/arsenal_market/create', ['POST'], (ctx: HandlerContext) => { - const { instance_id, item_name, quality, upgrade_level, serial, global_serial, price_free } = ctx.body as any; - const listingId = `list_${Date.now()}_${Math.floor(Math.random() * 1000)}`; - const db = getDb(); - db.prepare(` - INSERT INTO arsenal_market_listings (listing_id, steam_id, instance_id, item_name, quality, upgrade_level, serial, global_serial, price_free, status) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active') - `).run(listingId, ctx.params.steamId, instance_id || '', item_name || 'Unknown', quality || 'common', - upgrade_level || 0, serial || 0, global_serial || 0, price_free || 0); - return { success: true, listing_id: listingId }; -}); - -// POST /player/:steamId/arsenal_market/buy -route('player/:steamId/arsenal_market/buy', ['POST'], (ctx: HandlerContext) => { - const { listing_id } = ctx.body as any; - if (!listing_id) throw new HttpError(400, 'listing_id required'); - const db = getDb(); - const listing = db.prepare('SELECT * FROM arsenal_market_listings WHERE listing_id = ?').get(listing_id) as any; - if (!listing) throw new HttpError(404, 'Listing not found'); - if (listing.status !== 'active') throw new HttpError(400, 'Listing not active'); - - db.prepare("UPDATE arsenal_market_listings SET status = 'sold' WHERE listing_id = ?").run(listing_id); - db.prepare(` - INSERT INTO arsenal_market_sales (listing_id, seller_steam_id, buyer_steam_id, item_name, price_free) - VALUES (?, ?, ?, ?, ?) - `).run(listing_id, listing.steam_id, ctx.params.steamId, listing.item_name, listing.price_free); - return { success: true }; -}); - -// POST /player/:steamId/arsenal_market/cancel -route('player/:steamId/arsenal_market/cancel', ['POST'], (ctx: HandlerContext) => { - const { listing_id } = ctx.body as any; - if (!listing_id) throw new HttpError(400, 'listing_id required'); - const db = getDb(); - db.prepare("UPDATE arsenal_market_listings SET status = 'cancelled' WHERE listing_id = ? AND steam_id = ?") - .run(listing_id, ctx.params.steamId); - return { success: true }; -}); -``` - -- [ ] **Step 2: Commit** - -```bash -git add backend/src/lib/handlers/marketplace.ts -git commit -m "feat: add marketplace handler" -``` - ---- - -### Task 14: Contracts handler - -**Files:** -- Create: `backend/src/lib/handlers/contracts.ts` - -- [ ] **Step 1: Create contracts handler** - -```ts -import { route, HandlerContext, HttpError } from '@/lib/router'; -import { getDb } from '@/lib/db'; - -// GET /player/:steamId/death_sentence_contracts -route('player/:steamId/death_sentence_contracts', ['GET'], (ctx: HandlerContext) => { - const db = getDb(); - const row = db.prepare('SELECT contracts FROM death_sentence_contracts WHERE steam_id = ?').get(ctx.params.steamId) as any; - return { death_sentence_contracts: row ? JSON.parse(row.contracts) : { roster: [] } }; -}); - -// PUT /player/:steamId/death_sentence_contracts -route('player/:steamId/death_sentence_contracts', ['PUT'], (ctx: HandlerContext) => { - const { death_sentence_contracts } = ctx.body as any; - const db = getDb(); - db.prepare(` - INSERT INTO death_sentence_contracts (steam_id, contracts, updated_at) VALUES (?, ?, datetime('now')) - ON CONFLICT(steam_id) DO UPDATE SET contracts = ?, updated_at = datetime('now') - `).run(ctx.params.steamId, JSON.stringify(death_sentence_contracts || {}), JSON.stringify(death_sentence_contracts || {})); - return { success: true }; -}); -``` - -- [ ] **Step 2: Commit** - -```bash -git add backend/src/lib/handlers/contracts.ts -git commit -m "feat: add contracts handler" -``` - -- [ ] **Step 3: Verify the full build** - -Run: `cd /Users/achmad/Documents/dota/3728427109/backend && npx next build` -Expected: Build succeeds. All handlers compile and register their routes. - ---- - -### Task 15: Admin login & layout - -**Files:** -- Create: `backend/src/app/admin/layout.tsx` -- Create: `backend/src/app/admin/login/page.tsx` - -- [ ] **Step 1: Create admin layout (with sidebar + auth check)** - -```tsx -'use client'; -import { usePathname, useRouter } from 'next/navigation'; -import { useEffect, useState } from 'react'; - -const NAV = [ - { href: '/admin', label: 'Dashboard' }, - { href: '/admin/players', label: 'Players' }, - { href: '/admin/battlepass', label: 'Battle Pass' }, - { href: '/admin/matches', label: 'Matches' }, - { href: '/admin/promocodes', label: 'Promo Codes' }, - { href: '/admin/store', label: 'Store' }, - { href: '/admin/contracts', label: 'Contracts' }, - { href: '/admin/arsenal', label: 'Arsenal' }, -]; - -export default function AdminLayout({ children }: { children: React.ReactNode }) { - const pathname = usePathname(); - const router = useRouter(); - const [authed, setAuthed] = useState(false); - const [loading, setLoading] = useState(true); - - useEffect(() => { - if (pathname === '/admin/login') { - setLoading(false); - return; - } - fetch('/api/admin/check') - .then(r => r.json()) - .then(d => { - if (d.authenticated) setAuthed(true); - else router.push('/admin/login'); - }) - .catch(() => router.push('/admin/login')) - .finally(() => setLoading(false)); - }, [pathname, router]); - - if (loading) return
Loading...
; - if (pathname === '/admin/login') return <>{children}; - if (!authed) return null; - - return ( -
- -
{children}
-
- ); -} -``` - -- [ ] **Step 2: Create login page** - -```tsx -'use client'; -import { useState } from 'react'; -import { useRouter } from 'next/navigation'; - -export default function LoginPage() { - const [password, setPassword] = useState(''); - const [error, setError] = useState(''); - const router = useRouter(); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setError(''); - const res = await fetch('/api/admin/login', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ password }), - }); - const data = await res.json(); - if (data.success) router.push('/admin'); - else setError(data.error || 'Login failed'); - }; - - return ( -
-
-

Admin Login

- {error &&

{error}

} - setPassword(e.target.value)} - placeholder="Password" - className="w-full px-3 py-2 bg-gray-700 rounded mb-4 text-white" - autoFocus - /> - -
-
- ); -} -``` - -- [ ] **Step 3: Add admin API endpoints** - -Append to `backend/src/lib/handlers/player.ts`: - -```ts -// — Admin auth endpoints — - -route('admin/login', ['POST'], (ctx: HandlerContext) => { - const { password } = ctx.body as any; - // Simple in-memory cookie auth via API - const { verifyPassword, createAdminSession } = require('@/lib/auth'); - if (!verifyPassword(password)) { - return NextResponse.json({ success: false, error: 'Invalid password' }, { status: 401 }); - } - createAdminSession(); - return { success: true }; -}); - -route('admin/check', ['GET'], () => { - const { checkAdminAuth } = require('@/lib/auth'); - return { authenticated: checkAdminAuth() }; -}); - -route('admin/logout', ['GET'], () => { - const { clearAdminSession } = require('@/lib/auth'); - clearAdminSession(); - return { success: true }; -}); -``` - -Wait — these don't work in the router because we can't use `NextResponse.json` in the handler return. The router already wraps in `NextResponse.json`. Let me fix the admin login to not need NextResponse: - -Actually, the issue is that the router always wraps in NextResponse.json with status 200. For admin login on wrong password, we want a 401. The router's error handling only catches HttpError. Let me adjust. - -Better approach: keep admin login in a separate standard Next.js route handler instead of using the catch-all. Create a dedicated file for admin API routes. - -- [ ] **Step 3 (revised): Create admin API routes as standard Next.js routes** - -Create `backend/src/app/api/admin/login/route.ts`: - -```ts -import { NextRequest, NextResponse } from 'next/server'; - -export async function POST(request: NextRequest) { - const { password } = await request.json(); - const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'admin'; - if (password !== ADMIN_PASSWORD) { - return NextResponse.json({ success: false, error: 'Invalid password' }, { status: 401 }); - } - const response = NextResponse.json({ success: true }); - response.cookies.set('admin_session', 'authenticated', { - httpOnly: true, secure: false, sameSite: 'lax', path: '/admin', maxAge: 86400, - }); - return response; -} -``` - -Create `backend/src/app/api/admin/check/route.ts`: - -```ts -import { NextResponse } from 'next/server'; -import { cookies } from 'next/headers'; - -export async function GET() { - const store = cookies(); - const authed = store.get('admin_session')?.value === 'authenticated'; - return NextResponse.json({ authenticated: !!authed }); -} -``` - -Create `backend/src/app/api/admin/logout/route.ts`: - -```ts -import { NextResponse } from 'next/server'; - -export async function GET() { - const response = NextResponse.json({ success: true }); - response.cookies.delete('admin_session'); - return response; -} -``` - -Since admin routes don't start with `api/[...path]`, they'll be matched by these specific route files before the catch-all. - -- [ ] **Step 4: Commit** - -```bash -git add backend/src/app/admin/layout.tsx backend/src/app/admin/login/page.tsx -git add backend/src/app/api/admin/login/route.ts backend/src/app/api/admin/check/route.ts backend/src/app/api/admin/logout/route.ts -git commit -m "feat: add admin layout with sidebar and login" -``` - ---- - -### Task 16: Admin dashboard page - -**Files:** -- Create: `backend/src/app/admin/page.tsx` - -- [ ] **Step 1: Create dashboard page** - -```tsx -'use client'; -import { useEffect, useState } from 'react'; - -type Stats = { - players: number; - games: number; - activeBps: number; - questsCompleted: number; -}; - -export default function DashboardPage() { - const [stats, setStats] = useState(null); - - useEffect(() => { - fetch('/api/admin/stats') - .then(r => r.json()) - .then(setStats) - .catch(() => {}); - }, []); - - if (!stats) return
Loading stats...
; - - const cards = [ - { label: 'Players', value: stats.players, color: 'text-blue-400' }, - { label: 'Games Played', value: stats.games, color: 'text-green-400' }, - { label: 'Active Battle Passes', value: stats.activeBps, color: 'text-amber-400' }, - { label: 'Quests Completed', value: stats.questsCompleted, color: 'text-purple-400' }, - ]; - - return ( -
-

Dashboard

-
- {cards.map(c => ( -
-
{c.label}
-
{c.value}
-
- ))} -
-
- ); -} -``` - -- [ ] **Step 2: Create admin stats API route** - -Create `backend/src/app/api/admin/stats/route.ts`: - -```ts -import { NextResponse } from 'next/server'; -import { getDb } from '@/lib/db'; - -export async function GET() { - const db = getDb(); - const players = (db.prepare('SELECT COUNT(*) as c FROM players').get() as any).c; - const games = (db.prepare('SELECT COUNT(*) as c FROM game_history').get() as any).c; - const activeBps = (db.prepare('SELECT COUNT(*) as c FROM battle_passes').get() as any).c; - const questsCompleted = (db.prepare('SELECT COUNT(*) as c FROM battle_pass_quests WHERE completed = 1').get() as any).c; - - return NextResponse.json({ players, games, activeBps, questsCompleted }); -} -``` - -- [ ] **Step 3: Commit** - -```bash -git add backend/src/app/admin/page.tsx backend/src/app/api/admin/stats/route.ts -git commit -m "feat: add admin dashboard with stats" -``` - ---- - -### Task 17: Admin players pages - -**Files:** -- Create: `backend/src/app/admin/players/page.tsx` -- Create: `backend/src/app/admin/players/[steamId]/page.tsx` -- Create: `backend/src/app/api/admin/players/route.ts` (search/list) -- Create: `backend/src/app/api/admin/players/[steamId]/route.ts` (get/update) - -- [ ] **Step 1: Create admin players list page** - -```tsx -'use client'; -import { useEffect, useState } from 'react'; - -export default function PlayersListPage() { - const [players, setPlayers] = useState([]); - const [search, setSearch] = useState(''); - - useEffect(() => { - fetch('/api/admin/players') - .then(r => r.json()) - .then(setPlayers) - .catch(() => {}); - }, []); - - const filtered = players.filter(p => - p.steam_id?.includes(search) || p.player_name?.toLowerCase().includes(search.toLowerCase()) - ); - - return ( -
-

Players

- setSearch(e.target.value)} - className="w-full max-w-md px-3 py-2 bg-gray-800 rounded mb-4 text-white" - /> -
- - - - - - - - - - - - - {filtered.map(p => ( - window.location.href = `/admin/players/${p.steam_id}`}> - - - - - - - - ))} - -
Steam IDNameLevelFreeDonateDust
{p.steam_id}{p.player_name}{p.profile_level}{p.free_currency}{p.donate_currency}{p.dust_currency}
-
-
- ); -} -``` - -- [ ] **Step 2: Create admin players API route** - -```ts -import { NextRequest, NextResponse } from 'next/server'; -import { getDb } from '@/lib/db'; - -export async function GET() { - const db = getDb(); - const players = db.prepare('SELECT * FROM players ORDER BY updated_at DESC LIMIT 100').all(); - return NextResponse.json(players); -} -``` - -- [ ] **Step 3: Create admin player detail API route** - -`backend/src/app/api/admin/players/[steamId]/route.ts`: - -```ts -import { NextRequest, NextResponse } from 'next/server'; -import { getDb } from '@/lib/db'; - -export async function GET(request: NextRequest, { params }: { params: { steamId: string } }) { - const db = getDb(); - const player = db.prepare('SELECT * FROM players WHERE steam_id = ?').get(params.steamId); - if (!player) return NextResponse.json({ error: 'Not found' }, { status: 404 }); - - const bp = db.prepare('SELECT * FROM battle_passes WHERE steam_id = ?').get(params.steamId); - const purchases = db.prepare('SELECT * FROM purchases WHERE steam_id = ? ORDER BY created_at DESC LIMIT 50').all(params.steamId); - const matches = db.prepare('SELECT * FROM game_history WHERE steam_id = ? ORDER BY created_at DESC LIMIT 20').all(params.steamId); - const effects = db.prepare('SELECT effects FROM active_effects WHERE steam_id = ?').get(params.steamId) as any; - - return NextResponse.json({ player, battlePass: bp, purchases, matches, activeEffects: effects ? JSON.parse(effects.effects) : {} }); -} - -export async function PUT(request: NextRequest, { params }: { params: { steamId: string } }) { - const body = await request.json(); - const db = getDb(); - const fields: string[] = []; - const values: any[] = []; - - for (const key of ['player_name', 'profile_level', 'free_currency', 'donate_currency', 'dust_currency']) { - if (body[key] !== undefined) { - fields.push(`${key} = ?`); - values.push(body[key]); - } - } - if (body.sounds_wheel !== undefined) { - fields.push('sounds_wheel = ?'); - values.push(JSON.stringify(body.sounds_wheel)); - } - if (fields.length === 0) return NextResponse.json({ error: 'No fields to update' }, { status: 400 }); - - fields.push("updated_at = datetime('now')"); - values.push(params.steamId); - db.prepare(`UPDATE players SET ${fields.join(', ')} WHERE steam_id = ?`).run(...values); - - return NextResponse.json({ success: true }); -} -``` - -- [ ] **Step 4: Create player detail page** - -`backend/src/app/admin/players/[steamId]/page.tsx`: - -```tsx -'use client'; -import { useEffect, useState } from 'react'; -import { useParams, useRouter } from 'next/navigation'; - -export default function PlayerDetailPage() { - const { steamId } = useParams(); - const router = useRouter(); - const [data, setData] = useState(null); - const [form, setForm] = useState({}); - const [msg, setMsg] = useState(''); - - useEffect(() => { - fetch(`/api/admin/players/${steamId}`) - .then(r => r.json()) - .then(d => { - setData(d); - setForm(d.player || {}); - }) - .catch(() => router.push('/admin/players')); - }, [steamId, router]); - - const save = async () => { - const res = await fetch(`/api/admin/players/${steamId}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(form), - }); - const result = await res.json(); - setMsg(result.success ? 'Saved!' : 'Error: ' + (result.error || '')); - }; - - if (!data) return
Loading...
; - - return ( -
- ← Back to Players -

Player: {data.player?.player_name}

-

Steam ID: {steamId}

- - {msg &&

{msg}

} - -
-
-

Profile

-
- {['player_name', 'profile_level', 'free_currency', 'donate_currency', 'dust_currency'].map(f => ( -
- - setForm({ ...form, [f]: e.target.value })} - className="w-full px-2 py-1 bg-gray-700 rounded text-white text-sm" - /> -
- ))} -
- -
- -
-

Battle Pass

- {data.battlePass && ( -
-

Level: {data.battlePass.level}

-

XP: {data.battlePass.experience}

-

Premium: {data.battlePass.has_premium ? 'Yes' : 'No'}

- Edit BP → -
- )} -
- -
-

Recent Purchases ({data.purchases?.length})

-
- {data.purchases?.map((p: any, i: number) => ( -
{p.item_id} ({p.item_category})
- ))} - {!data.purchases?.length &&

None

} -
-
- -
-

Recent Matches ({data.matches?.length})

-
- {data.matches?.map((m: any, i: number) => ( -
- {m.hero} — {m.result} - ({m.difficulty}) -
- ))} - {!data.matches?.length &&

None

} -
-
-
-
- ); -} -``` - -- [ ] **Step 5: Commit** - -```bash -git add backend/src/app/admin/players/page.tsx backend/src/app/admin/players/\[steamId\]/page.tsx -git add backend/src/app/api/admin/players/route.ts backend/src/app/api/admin/players/\[steamId\]/route.ts -git commit -m "feat: add admin players pages (list + detail)" -``` - ---- - -### Task 18: Admin battle pass pages - -**Files:** -- Create: `backend/src/app/admin/battlepass/page.tsx` -- Create: `backend/src/app/admin/battlepass/[steamId]/page.tsx` - -- [ ] **Step 1: Create BP overview page** - -```tsx -'use client'; -import { useEffect, useState } from 'react'; -import Link from 'next/link'; - -export default function BattlePassListPage() { - const [bps, setBps] = useState([]); - - useEffect(() => { - fetch('/api/admin/battlepass') - .then(r => r.json()) - .then(setBps) - .catch(() => {}); - }, []); - - return ( -
-

Battle Passes

-
- - - - - - - - - - - {bps.map(bp => ( - - - - - - - ))} - -
Steam IDLevelXPPremium
- {bp.steam_id} - {bp.level}{bp.experience}{bp.has_premium ? 'Yes' : 'No'}
-
-
- ); -} -``` - -- [ ] **Step 2: Create BP detail/edit page** - -```tsx -'use client'; -import { useEffect, useState } from 'react'; -import { useParams } from 'next/navigation'; - -export default function BattlePassDetailPage() { - const { steamId } = useParams(); - const [bp, setBp] = useState(null); - const [quests, setQuests] = useState([]); - const [editLevel, setEditLevel] = useState(0); - const [editXp, setEditXp] = useState(0); - const [editPremium, setEditPremium] = useState(false); - const [msg, setMsg] = useState(''); - - const load = () => { - fetch(`/api/admin/battlepass/${steamId}`) - .then(r => r.json()) - .then(d => { - setBp(d.battlePass); - setQuests(d.quests || []); - setEditLevel(d.battlePass?.level || 0); - setEditXp(d.battlePass?.experience || 0); - setEditPremium(d.battlePass?.has_premium === 1); - }) - .catch(() => {}); - }; - - useEffect(load, [steamId]); - - const saveBp = async () => { - const res = await fetch(`/api/admin/battlepass/${steamId}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ level: editLevel, experience: editXp, has_premium: editPremium }), - }); - const d = await res.json(); - setMsg(d.success ? 'Saved!' : 'Error'); - }; - - const toggleClaim = async (questId: string) => { - await fetch(`/api/admin/battlepass/${steamId}/quests/${questId}/toggle-claim`, { method: 'POST' }); - load(); - }; - - if (!bp) return
Loading...
; - - return ( -
- ← Back -

Battle Pass

-

Steam ID: {steamId}

- - {msg &&

{msg}

} - -
-
-

Settings

-
-
- - setEditLevel(Number(e.target.value))} - className="w-full px-2 py-1 bg-gray-700 rounded text-white text-sm" /> -
-
- - setEditXp(Number(e.target.value))} - className="w-full px-2 py-1 bg-gray-700 rounded text-white text-sm" /> -
-
- - setEditPremium(e.target.checked)} - className="mt-2 block" /> -
-
- -
- -
-

Quests ({quests.length})

-
- {quests.map(q => ( -
-
-
{q.name || q.quest_id}
-
{q.type} — {q.progress}/{q.target}
-
- - {q.completed ? 'Completed' : 'In Progress'} - - {q.claimed ? ' — Claimed' : ''} -
-
- {q.completed && !q.claimed && ( - - )} -
- ))} -
-
-
-
- ); -} -``` - -- [ ] **Step 3: Create BP admin API routes** - -Create `backend/src/app/api/admin/battlepass/route.ts`: - -```ts -import { NextResponse } from 'next/server'; -import { getDb } from '@/lib/db'; - -export async function GET() { - const db = getDb(); - const bps = db.prepare('SELECT * FROM battle_passes ORDER BY updated_at DESC').all(); - return NextResponse.json(bps); -} -``` - -Create `backend/src/app/api/admin/battlepass/[steamId]/route.ts`: - -```ts -import { NextRequest, NextResponse } from 'next/server'; -import { getDb } from '@/lib/db'; - -export async function GET(request: NextRequest, { params }: { params: { steamId: string } }) { - const db = getDb(); - const battlePass = db.prepare('SELECT * FROM battle_passes WHERE steam_id = ?').get(params.steamId); - const quests = db.prepare('SELECT * FROM battle_pass_quests WHERE steam_id = ? ORDER BY id').all(params.steamId); - return NextResponse.json({ battlePass, quests }); -} - -export async function PUT(request: NextRequest, { params }: { params: { steamId: string } }) { - const body = await request.json(); - const db = getDb(); - db.prepare(` - UPDATE battle_passes SET level = ?, experience = ?, has_premium = ?, updated_at = datetime('now') - WHERE steam_id = ? - `).run(body.level, body.experience, body.has_premium ? 1 : 0, params.steamId); - return NextResponse.json({ success: true }); -} -``` - -Create `backend/src/app/api/admin/battlepass/[steamId]/quests/[questId]/toggle-claim/route.ts`: - -```ts -import { NextRequest, NextResponse } from 'next/server'; -import { getDb } from '@/lib/db'; - -export async function POST(request: NextRequest, { params }: { params: { steamId: string; questId: string } }) { - const db = getDb(); - const quest = db.prepare( - 'SELECT * FROM battle_pass_quests WHERE steam_id = ? AND quest_id = ?' - ).get(params.steamId, params.questId) as any; - if (!quest) return NextResponse.json({ error: 'Not found' }, { status: 404 }); - - db.prepare("UPDATE battle_pass_quests SET claimed = 1, updated_at = datetime('now') WHERE id = ?").run(quest.id); - return NextResponse.json({ success: true }); -} -``` - -- [ ] **Step 4: Commit** - -```bash -git add backend/src/app/admin/battlepass/page.tsx backend/src/app/admin/battlepass/\[steamId\]/page.tsx -git add backend/src/app/api/admin/battlepass/route.ts backend/src/app/api/admin/battlepass/\[steamId\]/route.ts -git add backend/src/app/api/admin/battlepass/\[steamId\]/quests/\[questId\]/toggle-claim/route.ts -git commit -m "feat: add admin battle pass pages" -``` - ---- - -### Task 19: Admin matches page - -**Files:** -- Create: `backend/src/app/admin/matches/page.tsx` -- Create: `backend/src/app/api/admin/matches/route.ts` - -- [ ] **Step 1: Create admin matches API route** - -```ts -import { NextRequest, NextResponse } from 'next/server'; -import { getDb } from '@/lib/db'; - -export async function GET(request: NextRequest) { - const db = getDb(); - const { searchParams } = new URL(request.url); - const hero = searchParams.get('hero'); - const difficulty = searchParams.get('difficulty'); - const limit = parseInt(searchParams.get('limit') || '100'); - const offset = parseInt(searchParams.get('offset') || '0'); - - let query = 'SELECT * FROM game_history WHERE 1=1'; - const params: any[] = []; - - if (hero) { query += ' AND hero LIKE ?'; params.push(`%${hero}%`); } - if (difficulty) { query += ' AND difficulty = ?'; params.push(difficulty); } - - query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?'; - params.push(limit, offset); - - const matches = db.prepare(query).all(...params); - return NextResponse.json(matches); -} -``` - -- [ ] **Step 2: Create matches page** - -```tsx -'use client'; -import { useEffect, useState } from 'react'; - -export default function MatchesPage() { - const [matches, setMatches] = useState([]); - const [heroFilter, setHeroFilter] = useState(''); - const [diffFilter, setDiffFilter] = useState(''); - - const load = () => { - const params = new URLSearchParams(); - if (heroFilter) params.set('hero', heroFilter); - if (diffFilter) params.set('difficulty', diffFilter); - fetch(`/api/admin/matches?${params}`) - .then(r => r.json()) - .then(setMatches) - .catch(() => {}); - }; - - useEffect(load, [heroFilter, diffFilter]); - - const difficulties = ['', 'easy', 'normal', 'hard', 'impossible', 'death_sentence']; - - return ( -
-

Match History

-
- setHeroFilter(e.target.value)} - className="px-3 py-2 bg-gray-800 rounded text-white text-sm" /> - -
-
- - - - - - - - - - {matches.map(m => ( - - - - - - - - - - ))} - -
Steam IDHeroResultDifficultyK/DDurationDate
{m.steam_id}{m.hero}{m.result}{m.difficulty}{m.kills}/{m.deaths}{Math.floor((m.duration || 0) / 60)}m{m.created_at}
-
-
- ); -} -``` - -- [ ] **Step 3: Commit** - -```bash -git add backend/src/app/admin/matches/page.tsx backend/src/app/api/admin/matches/route.ts -git commit -m "feat: add admin matches page" -``` - ---- - -### Task 20: Admin promocodes page - -**Files:** -- Create: `backend/src/app/admin/promocodes/page.tsx` -- Create: `backend/src/app/api/admin/promocodes/route.ts` - -- [ ] **Step 1: Create promocodes admin API route** - -```ts -import { NextRequest, NextResponse } from 'next/server'; -import { getDb } from '@/lib/db'; - -export async function GET() { - const db = getDb(); - const codes = db.prepare('SELECT * FROM promo_codes ORDER BY code').all(); - return NextResponse.json(codes); -} - -export async function POST(request: NextRequest) { - const body = await request.json(); - const db = getDb(); - db.prepare(` - INSERT OR REPLACE INTO promo_codes (code, free_currency, donate_currency, dust_currency, max_uses, expires_at) - VALUES (?, ?, ?, ?, ?, ?) - `).run(body.code?.toUpperCase(), body.free_currency || 0, body.donate_currency || 0, body.dust_currency || 0, - body.max_uses || 1, body.expires_at || null); - return NextResponse.json({ success: true }); -} - -export async function DELETE(request: NextRequest) { - const { searchParams } = new URL(request.url); - const code = searchParams.get('code'); - if (!code) return NextResponse.json({ error: 'code required' }, { status: 400 }); - const db = getDb(); - db.prepare('DELETE FROM promo_codes WHERE code = ?').run(code.toUpperCase()); - return NextResponse.json({ success: true }); -} -``` - -- [ ] **Step 2: Create promocodes page** - -```tsx -'use client'; -import { useEffect, useState } from 'react'; - -export default function PromoCodesPage() { - const [codes, setCodes] = useState([]); - const [form, setForm] = useState({ code: '', free_currency: 0, donate_currency: 0, dust_currency: 0, max_uses: 1 }); - - const load = () => { fetch('/api/admin/promocodes').then(r => r.json()).then(setCodes); }; - useEffect(load, []); - - const create = async () => { - await fetch('/api/admin/promocodes', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(form), - }); - setForm({ code: '', free_currency: 0, donate_currency: 0, dust_currency: 0, max_uses: 1 }); - load(); - }; - - const del = async (code: string) => { - await fetch(`/api/admin/promocodes?code=${code}`, { method: 'DELETE' }); - load(); - }; - - return ( -
-

Promo Codes

- -
-

Create Code

-
- setForm({...form, code: e.target.value})} - className="px-2 py-1 bg-gray-700 rounded text-white text-sm" /> - setForm({...form, free_currency: Number(e.target.value)})} - className="px-2 py-1 bg-gray-700 rounded text-white text-sm" /> - setForm({...form, donate_currency: Number(e.target.value)})} - className="px-2 py-1 bg-gray-700 rounded text-white text-sm" /> - setForm({...form, dust_currency: Number(e.target.value)})} - className="px-2 py-1 bg-gray-700 rounded text-white text-sm" /> - setForm({...form, max_uses: Number(e.target.value)})} - className="px-2 py-1 bg-gray-700 rounded text-white text-sm" /> -
- -
- -
- - - - - - - - - - {codes.map(c => ( - - - - - - - - - - ))} - -
CodeFreeDonateDustUsesExpires
{c.code}{c.free_currency}{c.donate_currency}{c.dust_currency}{c.current_uses}/{c.max_uses}{c.expires_at || 'Never'}
-
-
- ); -} -``` - -- [ ] **Step 3: Commit** - -```bash -git add backend/src/app/admin/promocodes/page.tsx backend/src/app/api/admin/promocodes/route.ts -git commit -m "feat: add admin promocodes page" -``` - ---- - -### Task 21: Admin store page - -**Files:** -- Create: `backend/src/app/admin/store/page.tsx` -- Create: `backend/src/app/api/admin/store/route.ts` - -- [ ] **Step 1: Create store admin API route** - -```ts -import { NextResponse } from 'next/server'; -import { getDb } from '@/lib/db'; - -export async function GET() { - const db = getDb(); - const purchases = db.prepare(` - SELECT p.*, pl.player_name FROM purchases p - LEFT JOIN players pl ON p.steam_id = pl.steam_id - ORDER BY p.created_at DESC LIMIT 200 - `).all(); - const effects = db.prepare('SELECT * FROM active_effects').all(); - return NextResponse.json({ purchases, effects }); -} -``` - -- [ ] **Step 2: Create store page** - -```tsx -'use client'; -import { useEffect, useState } from 'react'; - -export default function StorePage() { - const [purchases, setPurchases] = useState([]); - const [effects, setEffects] = useState([]); - - useEffect(() => { - fetch('/api/admin/store').then(r => r.json()).then(d => { - setPurchases(d.purchases || []); - setEffects(d.effects || []); - }); - }, []); - - return ( -
-
-

Store Purchases

-
- - - - - - - - - {purchases.map(p => ( - - - - - - - - ))} - -
PlayerItemCategoryCostDate
{p.player_name || p.steam_id}{p.item_id}{p.item_category}{p.price_free || p.price_donate || p.price_dust || 0}{p.created_at}
-
-
- -
-

Active Effects

-
- - - - - - - - {effects.map((e: any) => ( - - - - - ))} - -
Steam IDEffects
{e.steam_id}{e.effects}
-
-
-
- ); -} -``` - -- [ ] **Step 3: Commit** - -```bash -git add backend/src/app/admin/store/page.tsx backend/src/app/api/admin/store/route.ts -git commit -m "feat: add admin store page" -``` - ---- - -### Task 22: Admin contracts page - -**Files:** -- Create: `backend/src/app/admin/contracts/page.tsx` -- Create: `backend/src/app/api/admin/contracts/route.ts` - -- [ ] **Step 1: Create admin contracts API** - -```ts -import { NextResponse } from 'next/server'; -import { getDb } from '@/lib/db'; - -export async function GET() { - const db = getDb(); - const contracts = db.prepare('SELECT * FROM death_sentence_contracts').all(); - return NextResponse.json(contracts); -} -``` - -- [ ] **Step 2: Create contracts page** - -```tsx -'use client'; -import { useEffect, useState } from 'react'; - -export default function ContractsPage() { - const [contracts, setContracts] = useState([]); - - useEffect(() => { - fetch('/api/admin/contracts').then(r => r.json()).then(setContracts); - }, []); - - return ( -
-

Death Sentence Contracts

-
- - - - - - - - {contracts.map((c: any) => ( - - - - - - ))} - -
Steam IDContract DataUpdated
{c.steam_id}{c.contracts}{c.updated_at}
-
-
- ); -} -``` - -- [ ] **Step 3: Commit** - -```bash -git add backend/src/app/admin/contracts/page.tsx backend/src/app/api/admin/contracts/route.ts -git commit -m "feat: add admin contracts page" -``` - ---- - -### Task 23: Admin arsenal page - -**Files:** -- Create: `backend/src/app/admin/arsenal/page.tsx` -- Create: `backend/src/app/api/admin/arsenal/route.ts` - -- [ ] **Step 1: Create admin arsenal API** - -```ts -import { NextResponse } from 'next/server'; -import { getDb } from '@/lib/db'; - -export async function GET() { - const db = getDb(); - const inventory = db.prepare('SELECT * FROM arsenal_inventory ORDER BY steam_id').all(); - const loadouts = db.prepare('SELECT * FROM arsenal_loadouts ORDER BY steam_id').all(); - const listings = db.prepare("SELECT * FROM arsenal_market_listings WHERE status = 'active' ORDER BY created_at DESC").all(); - return NextResponse.json({ inventory, loadouts, listings }); -} -``` - -- [ ] **Step 2: Create arsenal page** - -```tsx -'use client'; -import { useEffect, useState } from 'react'; - -export default function ArsenalPage() { - const [data, setData] = useState({}); - - useEffect(() => { - fetch('/api/admin/arsenal').then(r => r.json()).then(setData); - }, []); - - return ( -
-

Arsenal

-
-
-

Inventory ({data.inventory?.length || 0})

-
- {data.inventory?.map((i: any, idx: number) => ( -
- {i.item_name} - [{i.quality}] -
{i.steam_id}
-
- ))} -
-
- -
-

Loadouts ({data.loadouts?.length || 0})

-
- {data.loadouts?.map((l: any, idx: number) => ( -
- {l.steam_id} - {l.hero_name} -
{l.loadout}
-
- ))} -
-
- -
-

Active Listings ({data.listings?.length || 0})

-
- {data.listings?.map((l: any, idx: number) => ( -
- {l.item_name} - {l.price_free} free -
{l.steam_id}
-
- ))} -
-
-
-
- ); -} -``` - -- [ ] **Step 3: Commit** - -```bash -git add backend/src/app/admin/arsenal/page.tsx backend/src/app/api/admin/arsenal/route.ts -git commit -m "feat: add admin arsenal page" -``` - ---- - -### Task 24: Docker setup - -**Files:** -- Create: `backend/Dockerfile` -- Create: `backend/docker-compose.yml` -- Create: `backend/.dockerignore` -- Create: `backend/docker-entrypoint.sh` - -- [ ] **Step 1: Create .dockerignore** - -``` -node_modules -.next -.git -data -README.md -``` - -- [ ] **Step 2: Create Dockerfile** - -```dockerfile -FROM node:20-alpine AS base - -# Stage 1: Install deps -FROM base AS deps -WORKDIR /app -COPY package.json package-lock.json* ./ -RUN npm ci - -# Stage 2: Build -FROM base AS builder -WORKDIR /app -COPY --from=deps /app/node_modules ./node_modules -COPY . . -RUN npm run build - -# Stage 3: Production runner -FROM base AS runner -WORKDIR /app - -ENV NODE_ENV=production -ENV NEXT_TELEMETRY_DISABLED=1 -ENV DB_PATH=/app/data/zombie_invasion.db - -RUN addgroup --system --gid 1001 nodejs && \ - adduser --system --uid 1001 nextjs - -# Copy standalone build -COPY --from=builder /app/.next/standalone ./ -COPY --from=builder /app/.next/static ./.next/static -COPY --from=builder /app/public ./public -COPY --from=builder /app/docker-entrypoint.sh ./ -COPY --from=builder /app/data ./data - -RUN chmod +x docker-entrypoint.sh - -RUN mkdir -p /app/data && chown -R nextjs:nodejs /app/data -USER nextjs - -EXPOSE 3000 - -ENV PORT=3000 -ENV HOSTNAME=0.0.0.0 - -ENTRYPOINT ["/bin/sh", "docker-entrypoint.sh"] -``` - -- [ ] **Step 3: Create docker-entrypoint.sh** - -```bash -#!/bin/sh -set -e - -# Ensure data directory exists -mkdir -p /app/data - -# Start the application -exec node server.js -``` - -- [ ] **Step 4: Make entrypoint executable** - -Run: `chmod +x /Users/achmad/Documents/dota/3728427109/backend/docker-entrypoint.sh` - -- [ ] **Step 5: Create docker-compose.yml** - -```yaml -version: '3.8' - -services: - app: - build: . - ports: - - "6100:3000" # Host:6100 → Container:3000 - volumes: - - ./data:/app/data # Persist SQLite database - environment: - - ADMIN_PASSWORD=admin123 - - NODE_ENV=production - restart: unless-stopped -``` - -- [ ] **Step 6: Build and test Docker image** - -Run: `cd /Users/achmad/Documents/dota/3728427109/backend && docker compose build` -Expected: Build succeeds - -Run: `docker compose up -d` -Expected: Container starts, accessible at http://localhost:6100 - -Run: `curl http://localhost:6100/api/admin/check` -Expected: `{"authenticated":false}` - -- [ ] **Step 7: Commit** - -```bash -git add backend/Dockerfile backend/docker-compose.yml backend/.dockerignore backend/docker-entrypoint.sh -git commit -m "feat: add Docker setup with multi-stage build" -``` - ---- - -### Task 25: Update postman_collection.json - -**Files:** -- Modify: `postman_collection.json` - -- [ ] **Step 1: Update the base_url variable** - -Change the `base_url` variable value from `http://82.146.52.69:3000/api` to `http://localhost:6100/api` for local dev. Since the user will host on their domain later, use `{{base_url}}` properly. - -In the `variable` array at the bottom of postman_collection.json, change: - -```json -{ - "key": "base_url", - "value": "http://localhost:6100/api", - "type": "string" -} -``` - -- [ ] **Step 2: Commit** - -```bash -git add postman_collection.json -git commit -m "chore: update postman base_url to localhost:6100" -``` diff --git a/docs/superpowers/specs/2026-05-29-zombie-invasion-backend-design.md b/docs/superpowers/specs/2026-05-29-zombie-invasion-backend-design.md deleted file mode 100644 index d4f251a..0000000 --- a/docs/superpowers/specs/2026-05-29-zombie-invasion-backend-design.md +++ /dev/null @@ -1,413 +0,0 @@ -# Zombie Invasion — Backend & Admin Panel Design - -## Overview - -A lightweight Next.js application that serves as both: -- The **REST API backend** that the Zombie Invasion Dota 2 custom game client talks to -- An **admin panel** (served under `/admin`) for managing all player data - -Deployed as a single Docker container with SQLite for storage. All payment-related endpoints auto-accept (no real payment integration). - -## Tech Stack - -- **Framework:** Next.js 14 (App Router) -- **Database:** SQLite via `better-sqlite3` -- **Language:** TypeScript -- **Container:** Docker (single container, multi-stage build) -- **Port:** 3000 (same as the original game server) - -## Architecture - -``` -nextjs-app/ -├── Dockerfile # Multi-stage: build → run -├── docker-compose.yml # Single service -├── docker-entrypoint.sh # DB init + seed + start -├── next.config.js -├── package.json -├── src/ -│ ├── app/ -│ │ ├── layout.tsx # Root layout -│ │ ├── page.tsx # Redirects to /admin -│ │ ├── admin/ -│ │ │ ├── layout.tsx # Admin sidebar + auth check -│ │ │ ├── page.tsx # Dashboard (counts, quick-stats) -│ │ │ ├── login/page.tsx # Simple password login -│ │ │ ├── players/[steamId]/page.tsx # Single player editor -│ │ │ ├── players/page.tsx # Player list + search -│ │ │ ├── battlepass/[steamId]/page.tsx -│ │ │ ├── battlepass/page.tsx # BP overview -│ │ │ ├── matches/page.tsx # Match history browser -│ │ │ ├── promocodes/page.tsx # Manage promo codes -│ │ │ ├── store/page.tsx # Purchases, currencies -│ │ │ ├── contracts/page.tsx # Death sentence contracts -│ │ │ └── arsenal/page.tsx # Arsenal & marketplace -│ │ └── api/ -│ │ └── [...path]/ -│ │ └── route.ts # Catch-all: dispatches game client requests -│ └── lib/ -│ ├── db.ts # SQLite singleton + schema init -│ ├── seed.ts # Initial data (promo codes, sample quests) -│ ├── auth.ts # Simple admin auth helpers -│ └── handlers/ -│ ├── player.ts # Profile, currency, history, purchases -│ ├── battlepass.ts # BP data, quests, claim rewards -│ ├── game.ts # Match tracking, heartbeat -│ ├── payments.ts # Auto-accept mock payments -│ ├── leaderboard.ts # Leaderboard queries -│ ├── cards.ts # Card levels, decks -│ ├── equipment.ts # Equipment state -│ ├── arsenal.ts # Arsenal loadouts + inventory -│ ├── marketplace.ts # Marketplace listings + sales -│ └── contracts.ts # Death sentence contracts -├── data/ # SQLite DB file (Docker volume mount) -└── Dockerfile -``` - -## Database Schema - -### `players` -| Column | Type | Notes | -|--------|------|-------| -| steam_id | TEXT PK | | -| player_name | TEXT NOT NULL | | -| profile_level | INTEGER | Default 1 | -| free_currency | INTEGER | Default 0 | -| donate_currency | INTEGER | Default 0 | -| dust_currency | INTEGER | Default 0 | -| arcade_pack_credits | TEXT | JSON `{standard, premium}` | -| sounds_wheel | TEXT | JSON object of `sound_id → true` | -| created_at | TEXT | ISO datetime | -| updated_at | TEXT | ISO datetime | - -### `game_sessions` -| Column | Type | Notes | -|--------|------|-------| -| game_id | TEXT PK | | -| match_id | INTEGER | Shared across party | -| session_id | TEXT | | -| status | TEXT | `active` / `completed` | -| created_at | TEXT | | - -### `game_history` -| Column | Type | Notes | -|--------|------|-------| -| id | INTEGER PK AUTO | | -| steam_id | TEXT NOT NULL | | -| game_id | TEXT | | -| match_id | INTEGER | | -| result | TEXT | `win` / `loss` | -| hero | TEXT | | -| hero_level | INTEGER | | -| difficulty | TEXT | | -| duration | INTEGER | seconds | -| kills | INTEGER | | -| deaths | INTEGER | | -| score | INTEGER | net worth | -| outgoing_damage | REAL | | -| incoming_damage | REAL | | -| items | TEXT | comma-separated | -| modifiers | TEXT | comma-separated | -| aghanim_scepter | INTEGER | 0/1 | -| aghanim_shard | INTEGER | 0/1 | -| gold_earned | INTEGER | | -| session_id | TEXT | | -| created_at | TEXT | | - -### `battle_passes` -| Column | Type | Notes | -|--------|------|-------| -| steam_id | TEXT PK | | -| level | INTEGER | Default 0 | -| experience | INTEGER | Default 0 | -| has_premium | INTEGER | 0/1 | -| claimed_rewards | TEXT | JSON array of level numbers | -| claimed_premium_rewards | TEXT | JSON array of level numbers | -| created_at | TEXT | | -| updated_at | TEXT | | - -### `battle_pass_quests` -| Column | Type | Notes | -|--------|------|-------| -| id | INTEGER PK AUTO | | -| steam_id | TEXT NOT NULL | | -| quest_id | TEXT NOT NULL | | -| type | TEXT | `kill_zombies`, `survive_time`, etc. | -| name | TEXT | | -| description | TEXT | | -| progress | INTEGER | | -| target | INTEGER | | -| completed | INTEGER | 0/1 | -| claimed | INTEGER | 0/1 | -| reward_exp | INTEGER | | -| reward_free_currency | INTEGER | | -| quality | TEXT | nullable | -| npc | TEXT | nullable | -| target_item | TEXT | nullable | -| created_at | TEXT | | -| updated_at | TEXT | | - -### `purchases` -| Column | Type | Notes | -|--------|------|-------| -| id | INTEGER PK AUTO | | -| steam_id | TEXT NOT NULL | | -| item_id | TEXT NOT NULL | | -| item_category | TEXT | `items`, `cards`, `chat_wheel_sound`, etc. | -| card_id | INTEGER | nullable | -| price_free | INTEGER | | -| price_donate | INTEGER | | -| price_dust | INTEGER | | -| created_at | TEXT | | - -### `active_effects` -| Column | Type | Notes | -|--------|------|-------| -| steam_id | TEXT PK | | -| effects | TEXT | JSON: `{effect_type: effect_id}` | -| updated_at | TEXT | | - -### `promo_codes` -| Column | Type | Notes | -|--------|------|-------| -| code | TEXT PK | | -| free_currency | INTEGER | reward amount | -| donate_currency | INTEGER | reward amount | -| dust_currency | INTEGER | reward amount | -| max_uses | INTEGER | default 1 | -| current_uses | INTEGER | | -| expires_at | TEXT | nullable, ISO datetime | - -### `promo_redemptions` -| Column | Type | Notes | -|--------|------|-------| -| steam_id | TEXT | PK (composite) | -| code | TEXT | PK (composite) | -| redeemed_at | TEXT | | - -### `card_levels` -| Column | Type | Notes | -|--------|------|-------| -| steam_id | TEXT PK | | -| card_levels | TEXT | JSON: `{card_id: level}` | -| updated_at | TEXT | | - -### `decks` -| Column | Type | Notes | -|--------|------|-------| -| steam_id | TEXT | PK (composite) | -| deck_index | INTEGER | PK (composite) | -| name | TEXT | | -| cards | TEXT | JSON array of card IDs | -| updated_at | TEXT | | - -### `equipment` -| Column | Type | Notes | -|--------|------|-------| -| steam_id | TEXT PK | | -| equipment | TEXT | JSON: `{weapon, armor, ...}` | -| updated_at | TEXT | | - -### `arsenal_loadouts` -| Column | Type | Notes | -|--------|------|-------| -| steam_id | TEXT | PK (composite) | -| hero_name | TEXT | PK (composite) | -| loadout | TEXT | JSON: `{weapon, armor}` | -| updated_at | TEXT | | - -### `arsenal_inventory` -| Column | Type | Notes | -|--------|------|-------| -| steam_id | TEXT | PK (composite) | -| instance_id | TEXT | PK (composite) | -| item_name | TEXT | | -| quality | TEXT | | -| upgrade_level | INTEGER | | -| serial | INTEGER | | -| global_serial | INTEGER | | -| owner_name | TEXT | | -| pinned | INTEGER | 0/1 | -| favorite | INTEGER | 0/1 | -| stats | TEXT | JSON array | - -### `arsenal_market_listings` -| Column | Type | Notes | -|--------|------|-------| -| listing_id | TEXT PK | | -| steam_id | TEXT NOT NULL | | -| instance_id | TEXT | | -| item_name | TEXT | | -| quality | TEXT | | -| upgrade_level | INTEGER | | -| serial | INTEGER | | -| global_serial | INTEGER | | -| price_free | INTEGER | | -| status | TEXT | `active` / `sold` / `cancelled` | -| created_at | TEXT | | - -### `arsenal_market_sales` -| Column | Type | Notes | -|--------|------|-------| -| id | INTEGER PK AUTO | | -| listing_id | TEXT | | -| seller_steam_id | TEXT | | -| buyer_steam_id | TEXT | | -| item_name | TEXT | | -| price_free | INTEGER | | -| created_at | TEXT | | - -### `death_sentence_contracts` -| Column | Type | Notes | -|--------|------|-------| -| steam_id | TEXT PK | | -| contracts | TEXT | JSON roster | -| updated_at | TEXT | | - -## API Endpoints (Game Client) - -All under `/api/`. The catch-all route looks at the URL path and HTTP method to dispatch. - -### Player (`/api/player/:steamId`) -| Method | Path | Purpose | -|--------|------|---------| -| POST | `/api/player` | Create profile | -| GET | `/api/player/:steamId` | Get profile + currencies + stats | -| GET | `/api/player/:steamId/history` | Match history (limit, offset) | -| GET | `/api/player/:steamId/currency` | Get currency balances | -| PUT | `/api/player/:steamId/currency` | Save currency | -| POST | `/api/player/:steamId/currency/give` | Grant currency (BP rewards) | -| POST | `/api/player/:steamId/purchases` | Record a purchase | -| POST | `/api/player/:steamId/promo/redeem` | Redeem promo code | -| GET | `/api/player/:steamId/sounds_wheel` | Get chat wheel sounds | -| PUT | `/api/player/:steamId/sounds_wheel` | Save chat wheel sounds | -| POST | `/api/player/:steamId/deal-purchase` | Buy a deal | -| GET | `/api/player/:steamId/active_effects` | Get equipped effects | -| PUT | `/api/player/:steamId/active_effects` | Save equipped effects | -| GET | `/api/player/:steamId/card-levels` | Get card levels | -| PUT | `/api/player/:steamId/card-levels` | Update card levels | -| GET | `/api/player/:steamId/decks` | Get all decks | -| GET | `/api/player/:steamId/decks/:index` | Get one deck | -| PUT | `/api/player/:steamId/decks/:index` | Save one deck | -| GET | `/api/player/:steamId/equipment` | Get equipment | -| PUT | `/api/player/:steamId/equipment` | Save equipment | -| POST | `/api/player/:steamId/equipment/drop` | Equipment drop | -| GET | `/api/player/:steamId/arsenal_loadouts` | Get arsenal loadouts | -| PUT | `/api/player/:steamId/arsenal_loadouts` | Save arsenal loadouts | -| GET | `/api/player/:steamId/arsenal_inventory` | Get arsenal inventory | -| PUT | `/api/player/:steamId/arsenal_inventory` | Save arsenal inventory | -| GET | `/api/player/:steamId/arsenal_market/my_listings` | My active listings | -| GET | `/api/player/:steamId/arsenal_market/slots` | Market slot info | -| GET | `/api/player/:steamId/arsenal_market/sales` | My sales history | -| POST | `/api/player/:steamId/arsenal_market/create` | Create listing | -| POST | `/api/player/:steamId/arsenal_market/buy` | Buy from listing | -| POST | `/api/player/:steamId/arsenal_market/cancel` | Cancel listing | -| GET | `/api/player/:steamId/death_sentence_contracts` | Get contracts | -| PUT | `/api/player/:steamId/death_sentence_contracts` | Save contracts | - -### Battle Pass (`/api/battlepass`) -| Method | Path | Purpose | -|--------|------|---------| -| POST | `/api/battlepass` | Create BP | -| GET | `/api/battlepass/:steamId` | Get BP data | -| POST | `/api/battlepass/:steamId/hero-played` | Record hero played | -| GET | `/api/battlepass/:steamId/quests` | Get quests | -| POST | `/api/battlepass/:steamId/quests/progress` | Sync quest progress | -| POST | `/api/battlepass/:steamId/quests/claim` | Claim quest reward | -| POST | `/api/battlepass/:steamId/claim` | Claim BP level reward | -| POST | `/api/battlepass/:steamId/claim-premium` | Claim premium level reward | -| POST | `/api/battlepass/:steamId/claim-all` | Claim all rewards | -| POST | `/api/battlepass/:steamId/buy-premium` | Buy premium BP | -| POST | `/api/battlepass/:steamId/addexp` | Add BP XP | - -### Game (`/api/game`) -| Method | Path | Purpose | -|--------|------|---------| -| POST | `/api/game/start` | Register game start | -| POST | `/api/game/heartbeat` | Match heartbeat | -| POST | `/api/game` | Save game result | -| GET | `/api/game/:id/players` | Get match participants | - -### Payments (`/api/payments`) — auto-grant (no actual payment) -| Method | Path | Purpose | -|--------|------|---------| -| POST | `/api/payments/robokassa/link` | Instantly grants purchased currency to player balance, writes to DB | -| POST | `/api/payments/bundles/link` | Instantly grants bundle items/writes purchase to DB | -| GET | `/api/payments/deals?steamId=` | Returns deal catalog (deals purchasable with in-game currency) | - -### Leaderboard (`/api/leaderboard`) -| Method | Path | Purpose | -|--------|------|---------| -| GET | `/api/leaderboard?limit=&offset=&board=` | Leaderboard by rating/wealth | - -### Marketplace (`/api/arsenal_market`) -| Method | Path | Purpose | -|--------|------|---------| -| GET | `/api/arsenal_market/listings` | Public listings (with optional stat filters) | - -## Response Format - -The game client (Lua) expects JSON responses. The catch-all handler wraps each response: -- Success (2xx): returns the JSON body directly -- 404: returns `{error: "Not found"}` -- The game handles both wrapped responses `{ok: true, data: ...}` and unwrapped objects - -Since different game modules expect different response shapes (some expect arrays, some expect objects, some look for specific keys), each handler returns the exact shape the game code expects. - -## Admin Panel - -### Authentication -- Single password set via `ADMIN_PASSWORD` env var -- Cookie-based session (simple, no JWT library needed) -- Login page at `/admin/login` -- All `/admin/*` routes check auth middleware - -### Pages -| Path | Content | -|------|---------| -| `/admin` | Dashboard with quick stats (player count, games played, active BPs) | -| `/admin/players` | Searchable player list with currency/level/bp overview | -| `/admin/players/[steamId]` | Edit all player fields, view purchases, effects | -| `/admin/battlepass` | Overview of all BPs with search | -| `/admin/battlepass/[steamId]` | Edit BP level/XP/premium, add/manage quests | -| `/admin/matches` | Browse match history, filter by player/hero/difficulty | -| `/admin/promocodes` | List, create, edit, delete promo codes | -| `/admin/store` | View player purchases and active effects | -| `/admin/contracts` | View/edit death sentence contracts | -| `/admin/arsenal` | View inventory, loadouts, marketplace listings | - -## Docker Setup - -```dockerfile -# Multi-stage build: node:20-alpine -# Stage 1: Install deps + build Next.js -# Stage 2: Run with production deps + SQLite data volume -``` - -```yaml -# docker-compose.yml -services: - app: - build: . - ports: - - "6100:3000" # Host:6100 → Container:3000 - volumes: - - ./data:/app/data # Persist SQLite DB - environment: - - ADMIN_PASSWORD=admin123 -``` - -## Seed Data - -On first run (empty DB), the entrypoint seeds: -- A few promo codes (e.g. `WELCOME100`, `ZOMBIE500`) -- The DB schema itself (via `db.ts` CREATE TABLE IF NOT EXISTS) -- A test player if none exist - -## Non-Goals - -- No user registration / multi-tenant support (single personal server) -- No real payment processing -- No WebSocket / real-time features -- No metrics / logging beyond basic requests -- No automated test suite (manual testing via game client + admin panel) diff --git a/postman_collection.json b/postman_collection.json deleted file mode 100644 index c91e7eb..0000000 --- a/postman_collection.json +++ /dev/null @@ -1,925 +0,0 @@ -{ - "info": { - "name": "Zombie Invasion API", - "description": "API endpoints for the Zombie Invasion Dota 2 custom game. Base URL: http://localhost:6100/api\n\nAuth: x-custom-key header from GetDedicatedServerKeyV3(\"zombie_invasion\")\nCheat mode fallback key: menya_ebut_negry_tolpoy", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "manual" - }, - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "key", - "value": "menya_ebut_negry_tolpoy", - "type": "string" - }, - { - "key": "value", - "value": "{{x-custom-key}}", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "item": [ - { - "name": "Player", - "item": [ - { - "name": "Create Player Profile", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "x-custom-key", - "value": "{{x-custom-key}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"steam_id\": \"76561198000000001\",\n \"player_name\": \"TestPlayer\"\n}" - }, - "url": { - "raw": "{{base_url}}/player", - "host": ["{{base_url}}"], - "path": ["player"] - } - }, - "response": [] - }, - { - "name": "Get Player Profile", - "request": { - "method": "GET", - "header": [ - { - "key": "x-custom-key", - "value": "{{x-custom-key}}", - "type": "text" - } - ], - "url": { - "raw": "{{base_url}}/player/{{steam_id}}", - "host": ["{{base_url}}"], - "path": ["player", "{{steam_id}}"] - } - }, - "response": [] - }, - { - "name": "Get Player Match History", - "request": { - "method": "GET", - "header": [ - { - "key": "x-custom-key", - "value": "{{x-custom-key}}", - "type": "text" - } - ], - "url": { - "raw": "{{base_url}}/player/{{steam_id}}/history?limit=10&offset=0", - "host": ["{{base_url}}"], - "path": ["player", "{{steam_id}}", "history"], - "query": [ - {"key": "limit", "value": "10"}, - {"key": "offset", "value": "0"} - ] - } - }, - "response": [] - }, - { - "name": "Get Game Players", - "request": { - "method": "GET", - "header": [ - { - "key": "x-custom-key", - "value": "{{x-custom-key}}", - "type": "text" - } - ], - "url": { - "raw": "{{base_url}}/game/{{game_id}}/players", - "host": ["{{base_url}}"], - "path": ["game", "{{game_id}}", "players"] - } - }, - "response": [] - }, - { - "name": "Redeem Promo Code", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "x-custom-key", - "value": "{{x-custom-key}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"code\": \"PROMO2024\"\n}" - }, - "url": { - "raw": "{{base_url}}/player/{{steam_id}}/promo/redeem", - "host": ["{{base_url}}"], - "path": ["player", "{{steam_id}}", "promo", "redeem"] - } - }, - "response": [] - }, - { - "name": "Get Sounds Wheel", - "request": { - "method": "GET", - "header": [ - { - "key": "x-custom-key", - "value": "{{x-custom-key}}", - "type": "text" - } - ], - "url": { - "raw": "{{base_url}}/player/{{steam_id}}/sounds_wheel", - "host": ["{{base_url}}"], - "path": ["player", "{{steam_id}}", "sounds_wheel"] - } - }, - "response": [] - }, - { - "name": "Purchase Deal", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "x-custom-key", - "value": "{{x-custom-key}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"deal_key\": \"starter_pack\"\n}" - }, - "url": { - "raw": "{{base_url}}/player/{{steam_id}}/deal-purchase", - "host": ["{{base_url}}"], - "path": ["player", "{{steam_id}}", "deal-purchase"] - } - }, - "response": [] - } - ] - }, - { - "name": "Battle Pass", - "item": [ - { - "name": "Get Battle Pass Data", - "request": { - "method": "GET", - "header": [ - { - "key": "x-custom-key", - "value": "{{x-custom-key}}", - "type": "text" - } - ], - "url": { - "raw": "{{base_url}}/battlepass/{{steam_id}}", - "host": ["{{base_url}}"], - "path": ["battlepass", "{{steam_id}}"] - } - }, - "response": [] - }, - { - "name": "Get Battle Pass Quests", - "request": { - "method": "GET", - "header": [ - { - "key": "x-custom-key", - "value": "{{x-custom-key}}", - "type": "text" - } - ], - "url": { - "raw": "{{base_url}}/battlepass/{{steam_id}}/quests", - "host": ["{{base_url}}"], - "path": ["battlepass", "{{steam_id}}", "quests"] - } - }, - "response": [] - }, - { - "name": "Sync Quest Progress", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "x-custom-key", - "value": "{{x-custom-key}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"quest_id\": \"kill_zombies_1\",\n \"progress\": 42\n}" - }, - "url": { - "raw": "{{base_url}}/battlepass/{{steam_id}}/quests/progress", - "host": ["{{base_url}}"], - "path": ["battlepass", "{{steam_id}}", "quests", "progress"] - } - }, - "response": [] - }, - { - "name": "Claim Quest Reward", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "x-custom-key", - "value": "{{x-custom-key}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"quest_id\": \"kill_zombies_1\"\n}" - }, - "url": { - "raw": "{{base_url}}/battlepass/{{steam_id}}/quests/claim", - "host": ["{{base_url}}"], - "path": ["battlepass", "{{steam_id}}", "quests", "claim"] - } - }, - "response": [] - }, - { - "name": "Record Hero Played", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "x-custom-key", - "value": "{{x-custom-key}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"hero_name\": \"npc_dota_hero_axe\"\n}" - }, - "url": { - "raw": "{{base_url}}/battlepass/{{steam_id}}/hero-played", - "host": ["{{base_url}}"], - "path": ["battlepass", "{{steam_id}}", "hero-played"] - } - }, - "response": [] - } - ] - }, - { - "name": "Payments", - "item": [ - { - "name": "Create Robokassa Payment Link", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"steam_id\": \"{{steam_id}}\",\n \"amount_rub\": 100\n}" - }, - "url": { - "raw": "{{base_url}}/payments/robokassa/link", - "host": ["{{base_url}}"], - "path": ["payments", "robokassa", "link"] - } - }, - "response": [] - }, - { - "name": "Create Bundle Payment Link", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "x-custom-key", - "value": "{{x-custom-key}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"steam_id\": \"{{steam_id}}\",\n \"bundle_id\": \"starter_bundle\"\n}" - }, - "url": { - "raw": "{{base_url}}/payments/bundles/link", - "host": ["{{base_url}}"], - "path": ["payments", "bundles", "link"] - } - }, - "response": [] - }, - { - "name": "Get Deals Catalog", - "request": { - "method": "GET", - "header": [ - { - "key": "x-custom-key", - "value": "{{x-custom-key}}", - "type": "text" - } - ], - "url": { - "raw": "{{base_url}}/payments/deals?steam_id={{steam_id}}", - "host": ["{{base_url}}"], - "path": ["payments", "deals"], - "query": [ - {"key": "steam_id", "value": "{{steam_id}}"} - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Cards", - "item": [ - { - "name": "Get Card Levels", - "request": { - "method": "GET", - "header": [ - { - "key": "x-custom-key", - "value": "{{x-custom-key}}", - "type": "text" - } - ], - "url": { - "raw": "{{base_url}}/player/{{steam_id}}/card-levels", - "host": ["{{base_url}}"], - "path": ["player", "{{steam_id}}", "card-levels"] - } - }, - "response": [] - }, - { - "name": "Update Card Levels", - "request": { - "method": "PUT", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "x-custom-key", - "value": "{{x-custom-key}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"card_levels\": {\n \"1\": 3,\n \"2\": 1\n }\n}" - }, - "url": { - "raw": "{{base_url}}/player/{{steam_id}}/card-levels", - "host": ["{{base_url}}"], - "path": ["player", "{{steam_id}}", "card-levels"] - } - }, - "response": [] - }, - { - "name": "Get All Decks", - "request": { - "method": "GET", - "header": [ - { - "key": "x-custom-key", - "value": "{{x-custom-key}}", - "type": "text" - } - ], - "url": { - "raw": "{{base_url}}/player/{{steam_id}}/decks", - "host": ["{{base_url}}"], - "path": ["player", "{{steam_id}}", "decks"] - } - }, - "response": [] - }, - { - "name": "Get Deck by Index", - "request": { - "method": "GET", - "header": [ - { - "key": "x-custom-key", - "value": "{{x-custom-key}}", - "type": "text" - } - ], - "url": { - "raw": "{{base_url}}/player/{{steam_id}}/decks/0", - "host": ["{{base_url}}"], - "path": ["player", "{{steam_id}}", "decks", "0"] - } - }, - "response": [] - }, - { - "name": "Save Deck", - "request": { - "method": "PUT", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "x-custom-key", - "value": "{{x-custom-key}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"name\": \"My Deck\",\n \"cards\": [1, 2, 3, 4, 5]\n}" - }, - "url": { - "raw": "{{base_url}}/player/{{steam_id}}/decks/0", - "host": ["{{base_url}}"], - "path": ["player", "{{steam_id}}", "decks", "0"] - } - }, - "response": [] - } - ] - }, - { - "name": "Equipment", - "item": [ - { - "name": "Get Equipment State", - "request": { - "method": "GET", - "header": [ - { - "key": "x-custom-key", - "value": "{{x-custom-key}}", - "type": "text" - } - ], - "url": { - "raw": "{{base_url}}/player/{{steam_id}}/equipment", - "host": ["{{base_url}}"], - "path": ["player", "{{steam_id}}", "equipment"] - } - }, - "response": [] - }, - { - "name": "Save Equipment State", - "request": { - "method": "PUT", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "x-custom-key", - "value": "{{x-custom-key}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"equipment\": {\n \"weapon\": \"sword_t1\",\n \"armor\": \"plate_t2\"\n }\n}" - }, - "url": { - "raw": "{{base_url}}/player/{{steam_id}}/equipment", - "host": ["{{base_url}}"], - "path": ["player", "{{steam_id}}", "equipment"] - } - }, - "response": [] - }, - { - "name": "Post Equipment Drop", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "x-custom-key", - "value": "{{x-custom-key}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"item\": {\n \"name\": \"helmet_t3\",\n \"rarity\": \"epic\"\n }\n}" - }, - "url": { - "raw": "{{base_url}}/player/{{steam_id}}/equipment/drop", - "host": ["{{base_url}}"], - "path": ["player", "{{steam_id}}", "equipment", "drop"] - } - }, - "response": [] - } - ] - }, - { - "name": "Arsenal", - "item": [ - { - "name": "Get Arsenal Loadouts", - "request": { - "method": "GET", - "header": [ - { - "key": "x-custom-key", - "value": "{{x-custom-key}}", - "type": "text" - } - ], - "url": { - "raw": "{{base_url}}/player/{{steam_id}}/arsenal_loadouts", - "host": ["{{base_url}}"], - "path": ["player", "{{steam_id}}", "arsenal_loadouts"] - } - }, - "response": [] - }, - { - "name": "Save Arsenal Loadouts", - "request": { - "method": "PUT", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "x-custom-key", - "value": "{{x-custom-key}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"arsenal_loadouts\": {\n \"npc_dota_hero_axe\": {\n \"weapon\": \"ars_abc123\",\n \"armor\": \"ars_def456\"\n }\n }\n}" - }, - "url": { - "raw": "{{base_url}}/player/{{steam_id}}/arsenal_loadouts", - "host": ["{{base_url}}"], - "path": ["player", "{{steam_id}}", "arsenal_loadouts"] - } - }, - "response": [] - }, - { - "name": "Get Arsenal Inventory", - "request": { - "method": "GET", - "header": [ - { - "key": "x-custom-key", - "value": "{{x-custom-key}}", - "type": "text" - } - ], - "url": { - "raw": "{{base_url}}/player/{{steam_id}}/arsenal_inventory", - "host": ["{{base_url}}"], - "path": ["player", "{{steam_id}}", "arsenal_inventory"] - } - }, - "response": [] - }, - { - "name": "Save Arsenal Inventory", - "request": { - "method": "PUT", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "x-custom-key", - "value": "{{x-custom-key}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"arsenal_inventory\": {\n \"instances\": {\n \"ars_abc123\": {\n \"instanceId\": \"ars_abc123\",\n \"itemName\": \"sword_of_doom\",\n \"quality\": \"legendary\",\n \"upgradeLevel\": 3,\n \"serial\": 42,\n \"globalSerial\": 999,\n \"ownerName\": \"TestPlayer\",\n \"pinned\": true,\n \"favorite\": false,\n \"stats\": []\n }\n }\n }\n}" - }, - "url": { - "raw": "{{base_url}}/player/{{steam_id}}/arsenal_inventory", - "host": ["{{base_url}}"], - "path": ["player", "{{steam_id}}", "arsenal_inventory"] - } - }, - "response": [] - } - ] - }, - { - "name": "Arsenal Marketplace", - "item": [ - { - "name": "Get Public Listings", - "request": { - "method": "GET", - "header": [ - { - "key": "x-custom-key", - "value": "{{x-custom-key}}", - "type": "text" - } - ], - "url": { - "raw": "{{base_url}}/arsenal_market/listings?stats=bonus_damage,attack_speed", - "host": ["{{base_url}}"], - "path": ["arsenal_market", "listings"], - "query": [ - {"key": "stats", "value": "bonus_damage,attack_speed", "disabled": true} - ] - } - }, - "response": [] - }, - { - "name": "Get My Listings", - "request": { - "method": "GET", - "header": [ - { - "key": "x-custom-key", - "value": "{{x-custom-key}}", - "type": "text" - } - ], - "url": { - "raw": "{{base_url}}/player/{{steam_id}}/arsenal_market/my_listings", - "host": ["{{base_url}}"], - "path": ["player", "{{steam_id}}", "arsenal_market", "my_listings"] - } - }, - "response": [] - }, - { - "name": "Get Market Slots", - "request": { - "method": "GET", - "header": [ - { - "key": "x-custom-key", - "value": "{{x-custom-key}}", - "type": "text" - } - ], - "url": { - "raw": "{{base_url}}/player/{{steam_id}}/arsenal_market/slots", - "host": ["{{base_url}}"], - "path": ["player", "{{steam_id}}", "arsenal_market", "slots"] - } - }, - "response": [] - }, - { - "name": "Get Sales History", - "request": { - "method": "GET", - "header": [ - { - "key": "x-custom-key", - "value": "{{x-custom-key}}", - "type": "text" - } - ], - "url": { - "raw": "{{base_url}}/player/{{steam_id}}/arsenal_market/sales", - "host": ["{{base_url}}"], - "path": ["player", "{{steam_id}}", "arsenal_market", "sales"] - } - }, - "response": [] - }, - { - "name": "Create Listing", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "x-custom-key", - "value": "{{x-custom-key}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"instance_id\": \"ars_abc123\",\n \"instanceId\": \"ars_abc123\",\n \"item_instance_id\": \"ars_abc123\",\n \"itemInstanceId\": \"ars_abc123\",\n \"serial\": 42,\n \"global_serial\": 999,\n \"globalSerial\": 999,\n \"item_name\": \"sword_of_doom\",\n \"itemName\": \"sword_of_doom\",\n \"quality\": \"legendary\",\n \"upgrade_level\": 3,\n \"upgradeLevel\": 3,\n \"price_free\": 5000,\n \"priceFree\": 5000,\n \"request_id\": \"market_create_001\",\n \"requestId\": \"market_create_alt_001\"\n}" - }, - "url": { - "raw": "{{base_url}}/player/{{steam_id}}/arsenal_market/create", - "host": ["{{base_url}}"], - "path": ["player", "{{steam_id}}", "arsenal_market", "create"] - } - }, - "response": [] - }, - { - "name": "Buy Listing", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "x-custom-key", - "value": "{{x-custom-key}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"listing_id\": \"list_xyz789\",\n \"request_id\": \"market_buy_001\"\n}" - }, - "url": { - "raw": "{{base_url}}/player/{{steam_id}}/arsenal_market/buy", - "host": ["{{base_url}}"], - "path": ["player", "{{steam_id}}", "arsenal_market", "buy"] - } - }, - "response": [] - }, - { - "name": "Cancel Listing", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "x-custom-key", - "value": "{{x-custom-key}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"listing_id\": \"list_xyz789\",\n \"request_id\": \"market_cancel_001\"\n}" - }, - "url": { - "raw": "{{base_url}}/player/{{steam_id}}/arsenal_market/cancel", - "host": ["{{base_url}}"], - "path": ["player", "{{steam_id}}", "arsenal_market", "cancel"] - } - }, - "response": [] - } - ] - }, - { - "name": "Death Sentence Contracts", - "item": [ - { - "name": "Get Contracts", - "request": { - "method": "GET", - "header": [ - { - "key": "x-custom-key", - "value": "{{x-custom-key}}", - "type": "text" - } - ], - "url": { - "raw": "{{base_url}}/player/{{steam_id}}/death_sentence_contracts", - "host": ["{{base_url}}"], - "path": ["player", "{{steam_id}}", "death_sentence_contracts"] - } - }, - "response": [] - }, - { - "name": "Save Contracts", - "request": { - "method": "PUT", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "x-custom-key", - "value": "{{x-custom-key}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"death_sentence_contracts\": {\n \"roster\": [\n {\n \"instanceId\": \"dsc_abc123\",\n \"serial\": 1,\n \"titleIndex\": 0,\n \"rarity\": \"epic\",\n \"rewardMultiplier\": 2.5,\n \"traitId\": \"none\",\n \"complicationIds\": [\"comp_fire\", \"comp_poison\"],\n \"durability\": 3,\n \"durabilityMax\": 3,\n \"pinned\": false,\n \"favorite\": true\n }\n ]\n }\n}" - }, - "url": { - "raw": "{{base_url}}/player/{{steam_id}}/death_sentence_contracts", - "host": ["{{base_url}}"], - "path": ["player", "{{steam_id}}", "death_sentence_contracts"] - } - }, - "response": [] - } - ] - } - ], - "variable": [ - { - "key": "base_url", - "value": "http://localhost:6100/api", - "type": "string" - }, - { - "key": "x-custom-key", - "value": "localhost", - "type": "string" - }, - { - "key": "steam_id", - "value": "76561198000000001", - "type": "string" - }, - { - "key": "game_id", - "value": "12345", - "type": "string" - } - ] -}