# HIDDEN SPECTRE — server edition 👻 An augmented-reality ghost-hunting web app — a fan-made tribute to the AR play concept of the LEGO® Hidden Side™ app. This version adds a **Node/Express backend** with a secure admin panel for managing ghost GIFs and QR-based set recognition. > **Fan-made. Not affiliated with, sponsored by, or endorsed by the LEGO Group.** LEGO® and Hidden Side™ are trademarks of the LEGO Group. This project does not use LEGO logos or artwork. ## What's here - **Game** (`public/`) — WebXR world-tracked AR with a camera+gyro fallback. Ghosts roam and you trap them by holding the reticle on them. Ghosts render as **animated GIF billboards** when a set is scanned, or as procedural wisps in free-hunt mode. - **QR set recognition** — scan a set's QR code; the game asks the API which ghosts that set unlocks and spawns them (weighted by rarity). - **Admin panel** (`/admin`) — JWT-protected login to upload/enable/disable/delete ghost GIFs and create/manage sets and their QR codes. - **Backend** (`server/`) — Express 5, SQLite (`better-sqlite3`), bcrypt password hashing, JWT auth, image-only uploads via multer. ## Architecture ``` Browser ──► nginx (HTTPS) ──► Node/Express (localhost:3000) ├─ / game (static) ├─ /admin admin UI (static) ├─ /api/login bcrypt + JWT ├─ /api/ghosts CRUD + GIF upload (auth) ├─ /api/sets CRUD + ghost links (auth) ├─ /api/scan/:code public — game reads roster └─ /uploads/* served GIFs └─ SQLite: server/data/spectre.db ``` ## Security notes - Admin writes require a valid JWT (8h expiry). The public game only hits the read-only `/api/scan/:code`. - Passwords are bcrypt-hashed (cost 12). The first admin is created by the seed script from `.env`. - `JWT_SECRET` **must** be set or the server refuses to start. - Uploads are restricted to GIF/PNG/WebP, max 8 MB, stored with random filenames. - `.env`, the database, and uploaded files are git-ignored. --- ## Deploy on Ubuntu behind nginx ### 1. Install Node + build tools `better-sqlite3` is a native module and needs a compiler toolchain to build: ```bash sudo apt update sudo apt install -y nodejs npm build-essential python3 # (or install Node 20+ from NodeSource for a newer runtime) ``` ### 2. Get the code and install ```bash git clone /opt/hidden-spectre cd /opt/hidden-spectre npm install # compiles better-sqlite3 — needs build-essential ``` ### 3. Configure secrets ```bash cp .env.example .env # generate a strong secret: node -e "console.log('JWT_SECRET=' + require('crypto').randomBytes(48).toString('hex'))" >> .env # then edit .env to set ADMIN_USER / ADMIN_PASS (used once for seeding) nano .env ``` ### 4. Create the first admin ```bash npm run seed # reads ADMIN_USER / ADMIN_PASS from .env ``` ### 5. Run it as a service Create `/etc/systemd/system/hidden-spectre.service`: ```ini [Unit] Description=Hidden Spectre server After=network.target [Service] Type=simple WorkingDirectory=/opt/hidden-spectre ExecStart=/usr/bin/node server/index.js Restart=on-failure Environment=NODE_ENV=production User=www-data [Install] WantedBy=multi-user.target ``` ```bash sudo chown -R www-data:www-data /opt/hidden-spectre sudo systemctl daemon-reload sudo systemctl enable --now hidden-spectre ``` ### 6. Reverse-proxy with nginx In your existing HTTPS server block: ```nginx server { listen 443 ssl; server_name spectre.hideawaygaming.com.au; ssl_certificate /etc/letsencrypt/live/spectre.hideawaygaming.com.au/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/spectre.hideawaygaming.com.au/privkey.pem; client_max_body_size 10M; # allow GIF uploads location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } ``` ```bash sudo nginx -t && sudo systemctl reload nginx ``` HTTPS is required — the camera, gyro, and WebXR all refuse to run on an insecure origin. ## Using it 1. Go to `https://your-host/admin`, log in, upload a few ghost GIFs. 2. Create a set with a code like `SET-GRAVEYARD` and tick which ghosts it unlocks. 3. Make a QR code whose payload is exactly that code (any QR generator works) and print/display it. 4. On a phone, open `https://your-host/`, tap **Scan a Set**, scan the QR — those ghosts spawn. Or tap **Free hunt** for procedural ghosts with no set. ## Asset tips for ghost GIFs - Transparent background (GIF or, better, animated WebP/PNG for alpha) reads best floating in space. - Roughly square or portrait framing; the billboard auto-fits the image aspect. - Keep file size modest (a few hundred KB) so they load fast on mobile data. - `.obj` 3D minifigure ghosts are a planned future mode; this version is GIF/image billboards. ## Files ``` server/index.js Express app + all routes server/db.js SQLite schema server/auth.js JWT issue/verify server/upload.js multer image upload config server/seed.js first-admin creation public/ the game + /admin panel public/js/qr.js QR scanning (BarcodeDetector + manual fallback) ```