horey sheet

This commit is contained in:
2025-10-02 02:40:11 -07:00
parent f4607214ce
commit 54e483c49c
248 changed files with 29759 additions and 255 deletions
+36
View File
@@ -0,0 +1,36 @@
# Dependencies
node_modules
# Build output
dist
# Environment files
.env
.env.*
!.env.example
# Git
.git
.gitignore
# IDE
.vscode
.idea
# OS
.DS_Store
Thumbs.db
# Logs
*.log
logs
# Test
coverage
.nyc_output
# Misc
*.md
!README.md
.husky
.github
+18
View File
@@ -0,0 +1,18 @@
# Docker environment configuration
# Copy this to .env when using docker-compose
# Application
PORT=3000
# Database (MySQL container)
MYSQL_ROOT_PASSWORD=rootpassword
MYSQL_DATABASE=openplace
MYSQL_USER=openplace
MYSQL_PASSWORD=openplacepassword
MYSQL_PORT=3306
# Database URL (used by Prisma)
DATABASE_URL="mysql://openplace:openplacepassword@mysql:3306/openplace"
# JWT Secret (CHANGE THIS IN PRODUCTION!)
JWT_SECRET="your-secret-key-change-in-production"
+113
View File
@@ -0,0 +1,113 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Openplace is an unofficial open-source backend for wplace (a collaborative pixel art canvas), built with TypeScript, tinyhttp, Prisma, and MySQL. The system manages user authentication, pixel painting with charge-based rate limiting, alliances, leaderboards, and moderation features.
## Development Commands
### Essential Commands
- `pnpm dev` - Run development server with hot reload (watches src/ directory)
- `pnpm build` - Compile TypeScript to dist/
- `pnpm start` - Run production build from dist/
- `pnpm test` - Run tests once
- `pnpm test:watch` - Run tests in watch mode
- `pnpm lint` - Check for linting issues
- `pnpm lint:fix` - Auto-fix linting issues
### Database Commands
- `pnpm db:push` - Push schema changes to database and regenerate Prisma client
- `pnpm db:generate` - Regenerate Prisma client only
- `pnpm db:migrate` - Create and run a new migration
- `pnpm seed` - Seed database with initial data
### Requirements
- Node.js >= 24
- pnpm >= 10
- MySQL/MariaDB
## Architecture
### Backend Structure
**Entry Point**: [src/index.ts](src/index.ts) - Sets up tinyhttp app with middleware (CORS, cookie parser, JSON body parsing, logging) and registers all route modules.
**Authentication Flow**: JWT tokens stored in `j` cookie → validated by [src/middleware/auth.ts](src/middleware/auth.ts) → checks session validity in database → attaches `req.user` with `{ id, sessionId }`
**Database**: Prisma ORM with MySQL. Global instance exported from [src/config/database.ts](src/config/database.ts) and injected into request via middleware.
**Route Organization**: Each feature module exports a function that registers routes on the tinyhttp app:
- [src/routes/auth.ts](src/routes/auth.ts) - Registration, login, logout
- [src/routes/pixel.ts](src/routes/pixel.ts) - Pixel painting, tile generation, pixel info
- [src/routes/alliance.ts](src/routes/alliance.ts) - Alliance CRUD, invites, bans, leaderboards
- [src/routes/me.ts](src/routes/me.ts) - User profile, favorite locations, settings
- [src/routes/admin.ts](src/routes/admin.ts) - User management, bans, timeouts, tickets
- [src/routes/moderator.ts](src/routes/moderator.ts) - Moderation actions
- [src/routes/leaderboard.ts](src/routes/leaderboard.ts) - Global leaderboards
- [src/routes/store.ts](src/routes/store.ts) - Purchase colors and flags
**Service Layer**: Business logic isolated in service classes:
- [src/services/pixel.ts](src/services/pixel.ts) - `PixelService` handles pixel painting with charge validation, color unlocking checks, tile image generation using @napi-rs/canvas, and level calculation
- [src/services/alliance.ts](src/services/alliance.ts) - `AllianceService` handles alliance creation, member management, bans
- [src/services/user.ts](src/services/user.ts) - `UserService` handles user profile updates, favorites
**Core Systems**:
1. **Charge System** ([src/utils/charges.ts](src/utils/charges.ts)): Rate-limiting mechanism where users have `maxCharges` (default 20) that regenerate every `chargesCooldownMs` (default 30s). Painting consumes charges. Function `calculateChargeRecharge()` computes current charge based on time elapsed.
2. **Bitmap System** ([src/utils/bitmap.ts](src/utils/bitmap.ts)): `WplaceBitMap` class stores boolean flags as packed bytes for efficient storage (used for unlocked colors, flags). Stored in database as Bytes, converted to base64 for API responses.
3. **Color Palette** ([src/utils/colors.ts](src/utils/colors.ts)): Defines available colors with RGB values and whether they're paid. `checkColorUnlocked()` validates if user has purchased a color by checking bitmap.
4. **Tile System**: Canvas divided into 1000x1000 tiles. Each pixel has coordinates `(tileX, tileY, x, y)`. Tile images dynamically generated on-demand from pixel data.
5. **Regions** ([src/config/regions.ts](src/config/regions.ts)): Maps coordinates to geographic regions/countries. Users get 10% charge discount when painting in their equipped flag's region. **Currently returns placeholder data - implementation needed.**
### Database Schema
Key models in [prisma/schema.prisma](prisma/schema.prisma):
- **User**: Core user data, charge state, pixels painted, level, alliance membership, equipped flag, unlocked colors bitmap
- **Pixel**: Individual pixel with coordinates (tileX, tileY, x, y), colorId, paintedBy userId, timestamp
- **Tile**: Metadata for 1000x1000 tile regions, has many Pixels
- **Alliance**: Groups with members, bans, invites, HQ coordinates, total pixels painted
- **Session**: JWT session tracking with expiration
- **Ticket**: Moderation reports with evidence
- **UserNote**: Moderator notes on users
### Frontend
Pre-built SvelteKit frontend in [frontend/](frontend/) directory (served as static files, not part of development workflow). Backend serves 404.html for unmatched routes.
## Key Implementation Patterns
1. **Route Pattern**: Routes validate input → call service method → return JSON or handle service errors via `handleServiceError()`
2. **Service Pattern**: Services receive Prisma client in constructor, contain business logic, throw descriptive errors that are caught by error handler middleware
3. **Bulk Pixel Insert**: Painting uses raw SQL `INSERT ... ON DUPLICATE KEY UPDATE` for performance when updating multiple pixels
4. **Level Calculation**: `Math.floor(Math.sqrt(pixelsPainted / 100)) + 1`
5. **Validation**: Separate validator functions in [src/validators/](src/validators/) for common input patterns (seasons, coordinates, pagination)
6. **Error Responses**: Standardized via `createErrorResponse()` and HTTP_STATUS constants in [src/utils/response.ts](src/utils/response.ts)
## Environment Setup
Copy `.env.example` to `.env` and configure:
- `DATABASE_URL` - MySQL connection string (format: `mysql://user:password@host/database`)
- `JWT_SECRET` - Secret key for JWT signing
- `PORT` - Server port (default 3000)
## Important Notes
- The project is a work-in-progress with incomplete features (see README.md warnings)
- Region lookup system is stubbed and returns placeholder data - needs implementation
- Authentication uses JWT cookies named `j`
- All API responses use JSON format
- The backend is designed to work with the wplace.live frontend protocol
- Production deployment requires SSL/HTTPS (enforced by design)
- Use `pnpm` as package manager (not npm)
+222
View File
@@ -0,0 +1,222 @@
# Docker Setup Guide
This guide explains how to build and run Openplace using Docker.
## Quick Start
### Prerequisites
- Docker Engine 20.10+
- Docker Compose 2.0+
### Basic Usage
1. **Build and start all services:**
```bash
docker-compose up -d
```
2. **View logs:**
```bash
docker-compose logs -f app
```
3. **Stop services:**
```bash
docker-compose down
```
4. **Stop and remove volumes (deletes database):**
```bash
docker-compose down -v
```
## Configuration
### Environment Variables
The easiest way to configure the application is to copy `.env.docker` to `.env`:
```bash
cp .env.docker .env
```
Then edit `.env` to customize:
- `PORT` - Application port (default: 3000)
- `MYSQL_*` - Database configuration
- `JWT_SECRET` - **IMPORTANT:** Change this in production!
### Production Deployment
For production, you should:
1. **Change the JWT secret:**
```env
JWT_SECRET="your-secure-random-secret-here"
```
2. **Change database passwords:**
```env
MYSQL_ROOT_PASSWORD="secure-root-password"
MYSQL_PASSWORD="secure-app-password"
```
3. **Use a reverse proxy (nginx/traefik) for SSL/HTTPS**
4. **Set up regular database backups**
## Docker Commands
### Building
```bash
# Build the application image
docker-compose build
# Build without cache
docker-compose build --no-cache
```
#### Using the precompiled frontend
If you want to serve the legacy bundle in `frontend-backup/` instead of rebuilding `frontend-src`, set the build argument while building the image:
```bash
USE_FRONTEND_BACKUP=true docker-compose build
```
With `USE_FRONTEND_BACKUP=true` the Dockerfile skips the frontend build step and copies the existing `frontend-backup/` files into `/app/frontend` inside the image.
### Running
```bash
# Start in foreground
docker-compose up
# Start in background
docker-compose up -d
# Start only specific services
docker-compose up -d mysql
```
### Monitoring
```bash
# View logs
docker-compose logs -f
# View app logs only
docker-compose logs -f app
# Check service status
docker-compose ps
```
### Database Management
```bash
# Access MySQL shell
docker-compose exec mysql mysql -u openplace -p
# Run migrations
docker-compose exec app pnpm db:push
# Seed database
docker-compose exec app pnpm seed
# Backup database
docker-compose exec mysql mysqldump -u openplace -popenplacepassword openplace > backup.sql
# Restore database
docker-compose exec -T mysql mysql -u openplace -popenplacepassword openplace < backup.sql
```
### Maintenance
```bash
# Restart services
docker-compose restart
# Restart specific service
docker-compose restart app
# View resource usage
docker stats openplace-app openplace-mysql
# Clean up unused images
docker image prune
```
## Standalone Docker Build
If you prefer to build without docker-compose:
```bash
# Build image
docker build -t openplace:latest .
# Run container (requires existing MySQL)
docker run -d \
--name openplace \
-p 3000:3000 \
-e DATABASE_URL="mysql://user:pass@host:3306/openplace" \
-e JWT_SECRET="your-secret" \
openplace:latest
```
## Development with Docker
For development, you may want to mount your source code:
```bash
docker-compose -f docker-compose.dev.yml up
```
Or use the regular local development setup:
```bash
pnpm install
pnpm dev
```
## Troubleshooting
### Database connection fails
Wait a few seconds for MySQL to initialize on first run. Check logs:
```bash
docker-compose logs mysql
```
### Port already in use
Change the port in `.env`:
```env
PORT=3001
```
### Permission issues
On Linux, you may need to adjust file permissions:
```bash
sudo chown -R $(id -u):$(id -g) .
```
### Clear everything and restart
```bash
docker-compose down -v
docker-compose up -d --build
```
## Architecture
The Docker setup consists of:
- **openplace-app**: Node.js application container running the backend
- **openplace-mysql**: MySQL 8.0 database container
- **mysql-data**: Persistent volume for database storage
- **openplace-network**: Bridge network for container communication
The application automatically runs database migrations on startup.
+79
View File
@@ -0,0 +1,79 @@
ARG USE_FRONTEND_BACKUP=false
# Build stage
FROM node:24-alpine AS builder
# Install pnpm
RUN corepack enable && corepack prepare pnpm@10 --activate
# Set working directory
WORKDIR /app
# Copy package files for backend
COPY package.json pnpm-lock.yaml ./
# Install backend dependencies
RUN pnpm install --frozen-lockfile
# Copy source code and prisma schema
COPY . .
# Generate Prisma client
RUN pnpm db:generate
# Build TypeScript backend
RUN pnpm build
# Build frontend (or reuse compiled backup)
WORKDIR /app
ARG USE_FRONTEND_BACKUP
RUN if [ "$USE_FRONTEND_BACKUP" = "true" ]; then \
rm -rf /app/frontend && mkdir -p /app/frontend && cp -R /app/frontend-backup/. /app/frontend/; \
else \
cd frontend-src && npm install && npm run build; \
fi
# Create login.html from join.html if it doesn't exist
RUN if [ -f /app/frontend/join.html ] && [ ! -f /app/frontend/login.html ]; then \
cp /app/frontend/join.html /app/frontend/login.html; \
fi
# Production stage
FROM node:24-alpine
ARG USE_FRONTEND_BACKUP
# Install pnpm
RUN corepack enable && corepack prepare pnpm@10 --activate
# Set working directory
WORKDIR /app
# Copy package files
COPY package.json pnpm-lock.yaml ./
# Install production dependencies only
RUN pnpm install --frozen-lockfile --prod
# Copy prisma schema for migrations
COPY prisma ./prisma
# Generate Prisma client
RUN pnpm db:generate
# Copy built application from builder stage
COPY --from=builder /app/dist ./dist
# Copy built frontend from builder stage
COPY --from=builder /app/frontend ./frontend
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/api/v1/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
# Start the application
CMD ["node", "dist/index.js"]
+72
View File
@@ -0,0 +1,72 @@
version: '3.8'
services:
mysql:
image: mysql:8.0
container_name: openplace-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-rootpassword}
MYSQL_DATABASE: ${MYSQL_DATABASE:-openplace}
MYSQL_USER: ${MYSQL_USER:-openplace}
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-openplacepassword}
# No ports exposed - only accessible within Docker network
volumes:
- mysql-data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD:-rootpassword}"]
interval: 10s
timeout: 5s
retries: 5
networks:
- openplace-network
adminer:
image: adminer:latest
container_name: openplace-adminer
restart: unless-stopped
ports:
- "${ADMINER_PORT:-8080}:8080"
environment:
ADMINER_DEFAULT_SERVER: mysql
depends_on:
- mysql
networks:
- openplace-network
app:
build:
context: .
dockerfile: Dockerfile
args:
USE_FRONTEND_BACKUP: ${USE_FRONTEND_BACKUP:-false}
container_name: openplace-app
restart: unless-stopped
ports:
- "${PORT:-3000}:3000"
environment:
PORT: 3000
DATABASE_URL: "mysql://${MYSQL_USER:-openplace}:${MYSQL_PASSWORD:-openplacepassword}@mysql:3306/${MYSQL_DATABASE:-openplace}"
JWT_SECRET: ${JWT_SECRET:-change-this-secret-in-production}
NODE_ENV: production
depends_on:
mysql:
condition: service_healthy
networks:
- openplace-network
command: >
sh -c "
echo 'Waiting for database to be ready...' &&
sleep 5 &&
echo 'Running database migrations...' &&
pnpm db:push &&
echo 'Starting application...' &&
node dist/index.js
"
volumes:
mysql-data:
networks:
openplace-network:
driver: bridge
+127
View File
@@ -0,0 +1,127 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="/_app/immutable/assets/0.DQCxyt33.css" rel="stylesheet">
<link rel="modulepreload" href="/_app/immutable/entry/start.CqSbdZXc.js">
<link rel="modulepreload" href="/_app/immutable/chunks/B4HM4TqG.js">
<link rel="modulepreload" href="/_app/immutable/chunks/4WsUhDWi.js">
<link rel="modulepreload" href="/_app/immutable/chunks/BDALf20I.js">
<link rel="modulepreload" href="/_app/immutable/chunks/4k6DpCgf.js">
<link rel="modulepreload" href="/_app/immutable/chunks/BUhRjcOt.js">
<link rel="modulepreload" href="/_app/immutable/entry/app.CuVZ6Ons.js">
<link rel="modulepreload" href="/_app/immutable/chunks/x1RL6Wqy.js">
<link rel="modulepreload" href="/_app/immutable/chunks/DM9nRpoa.js">
<link rel="modulepreload" href="/_app/immutable/chunks/B2cHk4HI.js">
<link rel="modulepreload" href="/_app/immutable/chunks/Bke_korE.js">
<link rel="modulepreload" href="/_app/immutable/chunks/ChY_8ULT.js">
<link rel="modulepreload" href="/_app/immutable/chunks/BrZ10JY-.js">
<link rel="modulepreload" href="/_app/immutable/nodes/0.DIpSCqpd.js">
<link rel="modulepreload" href="/_app/immutable/chunks/DffDvEhl.js">
<link rel="modulepreload" href="/_app/immutable/chunks/DklPLC_x.js">
<link rel="modulepreload" href="/_app/immutable/chunks/BvbG2Lay.js">
<link rel="modulepreload" href="/_app/immutable/chunks/CZW2bcQi.js">
<link rel="modulepreload" href="/_app/immutable/chunks/BNZUboE0.js">
<link rel="modulepreload" href="/_app/immutable/chunks/cUtKXcx3.js">
<link rel="modulepreload" href="/_app/immutable/nodes/5.lvNarnfM.js">
<link rel="modulepreload" href="/_app/immutable/chunks/CYItkO2S.js">
<meta property="og:image" content="https://wplace.live/img/og-image.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:url" content="https://wplace.live/" />
<meta property="og:type" content="website" />
<meta
name="description"
content="Wplace is a collaborative, real-time pixel canvas layered over the world map, where anyone can paint and create art together."
/>
<meta
itemprop="description"
content="Wplace is a collaborative, real-time pixel canvas layered over the world map, where anyone can paint and create art together."
/>
<meta
property="og:description"
content="Wplace is a collaborative, real-time pixel canvas layered over the world map, where anyone can paint and create art together."
/>
<meta
name="twitter:description"
content="Wplace is a collaborative, real-time pixel canvas layered over the world map, where anyone can paint and create art together."
/>
<meta name="twitter:image" content="https://wplace.live/img/og-image.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="keywords" content="wplace, pixel art, real-time, game, world map, art" />
<meta name="apple-mobile-web-app-title" content="Wplace" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="mobile-web-app-capable" content="yes" />
<meta
http-equiv="Content-Security-Policy"
content="script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' https://challenges.cloudflare.com blob:"
/>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebApplication",
"name": "Wplace",
"url": "https://wplace.live"
}
</script>
<link
rel="icon"
type="image/png"
href="/img/favicon-96x96.png"
sizes="96x96"
/>
<link rel="shortcut icon" href="/favicon.ico" />
<link
rel="apple-touch-icon"
sizes="180x180"
href="/img/apple-touch-icon.png"
/>
<link rel="manifest" href="/site.webmanifest" />
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents"><!--[--><!--[--><!----><span class="hidden">Version: 1759175263375</span> <!--[!--><!----><div class="flex h-full flex-col items-center justify-center gap-6"><a href="/"><div class="flex items-center gap-1.5 "><img class="pixelated size-20" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAAXNSR0IArs4c6QAAABJQTFRFAQEBAAAAHGHnRcxVStlbMXLnk8SHtQAAAAF0Uk5TAEDm2GYAAABMSURBVHjadc9JCgAhDERRa7r/lZs0ikawdv+tkvEYALS07U2QawmOTo1oQBKr8/cUMLY7JLEPYLW0oISSNLtgiojRBfv0AuB67vH3B+FjAY/0rrGiAAAAAElFTkSuQmCC" alt="Wplace logo"/> <!--[--><span class="text-base-content font-pixel text-5xl">wplace</span><!--]--></div><!----></a> <p class="max-w-3xl text-center font-medium sm:text-xl">Not found</p> <a class="btn btn-primary btn-lg" href="/">Go to map</a></div><!----><!--]--><!----> <section aria-label="Notifications alt+T" tabindex="-1" aria-live="polite" aria-relevant="additions text" aria-atomic="false" class="svelte-tppj9g"><!--[!--><!--]--></section><!----><!----><!--]--> <!--[!--><!--]--><!--]-->
<script>
{
__sveltekit_1jtafcq = {
base: new URL(".", location).pathname.slice(0, -1)
};
const element = document.currentScript.parentElement;
Promise.all([
import("/_app/immutable/entry/start.CqSbdZXc.js"),
import("/_app/immutable/entry/app.CuVZ6Ons.js")
]).then(([kit, app]) => {
kit.start(app, element, {
node_ids: [0, 5],
data: [null,null],
form: null,
error: null
});
});
if ('serviceWorker' in navigator) {
addEventListener('load', function () {
navigator.serviceWorker.register('/service-worker.js');
});
}
}
</script>
</div>
</body>
<script>
window.addEventListener('beforeinstallprompt', (event) => {
event.preventDefault();
window.pwaInstallPrompt = event;
});
</script>
</html>
File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Some files were not shown because too many files have changed in this diff Show More