This page provides documentation for the TerraGuessr API.
/api/stories/fetch
/api/stories
/api/stories/:id
/api/stories/:id
/api/stories/:id/secure
/api/scores
/api/views-votes/viewed
/api/views-votes/voted
/api/views-votes/stats/:id
This project contains a full-stack application for managing "stories" for TerraGuessr. It includes:
This application follows a classic client-server architecture:
Backend (API Server): Built with Node.js, Express, and TypeScript, the server is the core of the application. It handles all business logic, validates data, interacts with the MySQL database, and exposes a secure REST API for managing stories and scores.
Frontend (Admin Dashboard): A React-based Single-Page Application (SPA) that runs entirely in the browser. It provides a user-friendly interface for administrators to interact with the data by making requests to the backend API.
Database: A MySQL database is used to persist all story and score data. The server is responsible for all database queries.
Security: The API is protected by public API keys, which must be sent with every request. Each key has a configurable daily request quota to prevent abuse.
poweruser, storyadmin, and botFollow these steps to get the API server running:
Clone the repository and navigate to the project's root directory:
git clone <repository-url>
cd terraguessr-api-manager_beforeAdmin
Install Node.js dependencies:
npm install
Required packages (automatically installed):
Core Dependencies:
express - Web framework for Node.jsmysql2 - MySQL database driver with Promise supportbcrypt - Password hashing for securityjsonwebtoken - JWT token generation and verificationhelmet - Security headers middlewarecors - Cross-origin resource sharingexpress-rate-limit - Rate limiting middlewaremarked - Markdown parsing for documentationdotenv - Environment variables loaderuuid - UUID generation for unique IDsDevelopment Dependencies:
ts-node-dev - Development server with auto-restarttypescript - TypeScript compiler@types/* - TypeScript type definitionsCreate MySQL database:
CREATE DATABASE nous_terraguessr_prod;
CREATE USER 'nous_trgsr'@'localhost' IDENTIFIED BY 'your_secure_password';
GRANT ALL PRIVILEGES ON nous_terraguessr_prod.* TO 'nous_trgsr'@'localhost';
FLUSH PRIVILEGES;
Run database migrations (automatic on first server start): The server will automatically create all required tables on first startup in the correct order:
users tablestories table story_ownership table (with foreign keys)ticket_categories tabletickets table (with foreign keys)ticket_replies table (with foreign keys)⚠️ Important: If you get foreign key constraint errors, ensure your database is completely empty and let the server create all tables automatically.
Create .env file in the root directory:
touch .env
Configure environment variables in .env:
# --- Database Configuration ---
# Connection URL for your MySQL database
# Format: mysql://USER:PASSWORD@HOST:PORT/DATABASE
DATABASE_URL="mysql://nous_trgsr:your_secure_password@localhost:3306/nous_terraguessr_prod"
# --- Server Configuration ---
# Port for the API server (default: 3001)
PORT=3001
# --- Authentication Configuration ---
# JWT secret for admin authentication (REQUIRED - change this!)
JWT_SECRET="your_very_long_and_secure_random_string_here_at_least_32_characters"
# --- Frontend Configuration ---
# Frontend URL for CORS (optional, for admin dashboard)
FRONTEND_URL="http://localhost:3000"
# --- Stripe Configuration ---
# Stripe secret key for payment processing
STRIPE_SECRET_KEY="sk_live_your_stripe_secret_key_here"
# Stripe webhook secret for payment confirmation
STRIPE_WEBHOOK_SECRET="whsec_your_webhook_secret_here"
# --- Email Configuration (SMTP) ---
# SMTP settings for sending emails (activation, password reset, etc.)
SMTP_HOST="smtp.gmail.com"
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER="your-email@gmail.com"
SMTP_PASS="your-app-password"
SMTP_FROM_NAME="TerraGuessr"
SMTP_FROM_EMAIL="noreply@terraguessr.org"
# --- AI Configuration ---
# Google Gemini AI API key for AI chat functionality (REQUIRED for AI features)
GEMINI_API_KEY="your_gemini_api_key_here"
📋 Configuration Examples: See env.example for complete configuration examples with different SMTP providers (Gmail, SendGrid, Mailgun, OVH).
⚠️ Important:
your_secure_password with your actual database passwordyour_very_long_and_secure_random_string_here_at_least_32_characters with a secure random stringyour_gemini_api_key_here with your actual Gemini AI API key.env file secure and never commit it to version controlapi/public_keys.json:{
"keys": [
{
"key": "your_public_api_key_here",
"quota": 1000,
"description": "Main API key"
}
]
}
Start the development server:
npm run dev
Alternative commands:
# Development mode with auto-restart
npm run dev
# Production build
npm run build
npm start
# Direct TypeScript execution
npx ts-node api/server.ts
Verify the server is running:
http://localhost:3001🚀 Server is running on http://localhost:3001
Loaded 2 public API keys.
Connected to the MySQL database
Create initial admin users (see see.txt for detailed SQL instructions):
-- Create poweruser
INSERT INTO users (id, username, email, password_hash, role, created_at, date_rgpd, nom_public, description, status)
VALUES ('<UUID-POWERUSER>', 'admin', 'admin@example.com', '<BCRYPT_HASH>', 'poweruser', NOW(), NOW(), 'Administrator', 'System administrator', 'verified');
-- Create storyadmin
INSERT INTO users (id, username, email, password_hash, role, created_at, date_rgpd, nom_public, description, status)
VALUES ('<UUID-STORYADMIN>', 'storyadmin', 'story@example.com', '<BCRYPT_HASH>', 'storyadmin', NOW(), NOW(), 'Story Admin', 'Story administrator', 'verified');
Test the API:
# Test public endpoint
curl -X POST "http://localhost:3001/api/stories/fetch" \
-H "Content-Type: application/json" \
-d '{"key":"NOWyouknowLesothoYOUSTUPID"}'
# Test admin login
curl -X POST "http://localhost:3001/api/auth/login" \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}'
PORT in .env or kill existing processesDATABASE_URL in .envJWT_SECRET to .envFRONTEND_URL in .envThe project includes several npm scripts for different purposes:
# Development (recommended for development)
npm run dev # Start with auto-restart and TypeScript compilation
# Production
npm run build # Compile TypeScript to JavaScript
npm start # Run the compiled JavaScript (production)
# Manual execution
npx ts-node api/server.ts # Direct TypeScript execution
| Variable | Required | Default | Description |
|---|---|---|---|
DATABASE_URL |
✅ Yes | - | MySQL connection string |
JWT_SECRET |
✅ Yes | - | Secret for JWT token signing |
GEMINI_API_KEY |
✅ Yes | - | Google Gemini AI API key for AI chat functionality |
PORT |
❌ No | 3001 |
Server port |
FRONTEND_URL |
❌ No | - | CORS origin for frontend |
terraguessr-api-manager_beforeAdmin/
├── api/ # Backend API code
│ ├── server.ts # Main server file
│ ├── db.ts # Database connection
│ ├── schema.ts # Database schema
│ ├── *.controller.ts # Route controllers
│ ├── *.middleware.ts # Express middleware
│ └── public_keys.json # API keys configuration
├── components/ # React components (frontend)
├── services/ # Frontend services
├── types.ts # TypeScript type definitions
├── package.json # Dependencies and scripts
├── tsconfig.json # TypeScript configuration
├── .env # Environment variables (create this)
└── see.txt # Database setup instructions
Content-Type Handling for JSON
This server is configured to parse request bodies as JSON even if they are sent with a Content-Type: text/plain header. This is a workaround for clients that might send JSON data with an incorrect content type.
The configuration in api/server.ts uses the type option of the express.json() middleware:
// api/server.ts
app.use(express.json({ type: ['application/json', 'text/plain'] }));
This ensures that payloads are correctly parsed, allowing the API key validation to function as expected regardless of the Content-Type header being application/json or text/plain. No additional installation is needed for this feature.
Set up the Database:
Connect to your MySQL database and run the SQL commands from the Database Schema section to create the stories and scores tables.
Run the server:
npm run dev
npm run build
Then, start the server:npm run start
The server will start, and you can access the admin dashboard at http://localhost:3001.
The frontend application is an administration tool designed for moderating and managing user-submitted stories.
"submitted" status. This is your primary work queue, showing all stories that require moderation.Pending Submission: Shows stories with submitted status.Published: Shows stories with verified or validated status.All Stories: Shows all stories regardless of their status.fr, en).verified.validated.storiesIf you are setting up the database for the first time, use the following SQL command to create the stories table:
CREATE TABLE `stories` (
`id` VARCHAR(36) NOT NULL,
`language` VARCHAR(10) DEFAULT NULL,
`source` VARCHAR(255) DEFAULT NULL,
`indicator` VARCHAR(255) DEFAULT NULL,
`category` VARCHAR(100) DEFAULT NULL,
`title` VARCHAR(255) NOT NULL,
`yearevents` TEXT DEFAULT NULL,
`email` VARCHAR(255) DEFAULT NULL,
`pseudo` VARCHAR(100) DEFAULT NULL,
`oganisation` VARCHAR(255) DEFAULT NULL,
`url` TEXT,
`description` TEXT,
`image` TEXT,
`creation_date` DATETIME NOT NULL,
`modif_date` DATETIME DEFAULT NULL,
`password` VARCHAR(255) DEFAULT NULL,
`prestatus` VARCHAR(50) DEFAULT NULL,
`status` VARCHAR(50) NOT NULL,
`votes` INT DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
If you have an existing stories table, you may need to apply updates:
yearevents:ALTER TABLE stories MODIFY COLUMN yearevents TEXT;
scoresUse the following command to create the scores table for the leaderboard:
CREATE TABLE `scores` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`timestamp` DATETIME NOT NULL,
`pseudo` VARCHAR(100) NOT NULL,
`pays` VARCHAR(100) DEFAULT NULL,
`score` INT NOT NULL,
`resume` TEXT DEFAULT NULL,
`niveau` VARCHAR(100) DEFAULT NULL,
`champion` VARCHAR(100) DEFAULT NULL,
INDEX `score_index` (`score` DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Several security measures have been implemented to protect the API from common attacks and abuse.
To prevent abuse, each public API key has a daily request limit.
api/public_keys.json file using the quota property for each key.X-RateLimit-Limit: The maximum number of requests allowed in the window.X-RateLimit-Remaining: The number of requests remaining in the current window.X-RateLimit-Reset: The UTC timestamp for when the quota will reset.429 Too Many Requests HTTP response.In addition to key-based quotas, the API also uses IP-based rate limiting to prevent brute-force attacks.
POST /api/stories) has a stricter limit of 10 requests per 15 minutes per IP to prevent spam submissions.This application uses the helmet middleware to set various security-related HTTP headers automatically. This helps protect against well-known web vulnerabilities such as Cross-Site Scripting (XSS), clickjacking, and more.
The API implements an intelligent in-memory caching system to improve performance and reduce database load for frequently accessed data.
?noCache=true parameter to force fresh data retrievalThe following routes benefit from caching:
GET /api/stories/:id - Individual story retrievalPOST /api/stories/fetch - Story list with filtersGET /api/scores - Leaderboard dataPOST /api/scores - Score queries (read-only operations)GET /api/views-votes/stats/:id - View and vote statisticsThe API returns cache status headers for debugging:
X-Cache: HIT - Data served from cacheX-Cache: MISS - Data fetched from database and cachedX-Cache: BYPASS - Cache bypassed (JWT auth or noCache parameter)Cache is automatically invalidated when:
The cache system includes comprehensive statistics and monitoring capabilities:
Statistics Available:
Administrative Endpoints (Poweruser only):
GET /api/admin/cache/stats - Detailed cache statisticsGET /api/admin/cache/health - Cache health statusPOST /api/admin/cache/clear - Clear all cached dataPOST /api/admin/cache/reset-stats - Reset statistics countersLogging Configuration:
Set CACHE_LOGS_ENABLED=true in your .env file to enable detailed cache logs:
Example Statistics Response:
{
"success": true,
"stats": {
"hits": 1570,
"miss": 430,
"bypass": 89,
"invalidations": 23,
"hitRate": 78.5,
"totalRequests": 2089,
"cacheSize": 45,
"uptime": "2h 15m",
"uptimeMs": 8100000,
"startedAt": "2025-10-29T10:30:00.000Z"
}
}
To protect public submission routes, the API supports server-side validation of Google reCAPTCHA v2 Invisible.
Environment Variable:
RECAPTCHA_SECRET — Your server-side secret key (never expose to clients)Client Payload (Story Editor):
recaptchaToken: token from client reCAPTCHArecaptchaVersion: 'v2_invisible' (informational)Server Behaviour:
POST /api/stories, the server verifies the token with Google at https://www.google.com/recaptcha/api/siteverify.400 with details; otherwise the usual creation flow continues.Notes:
X-Forwarded-For when present (first IP), or fallback to socket IP.error-codes on failure for diagnostics.The API implements a content moderation system to prevent the use of inappropriate language in public user names (nom_public).
Data Source:
Supported Languages: The filter supports all 12 languages of the TerraGuessr interface:
Validation Behavior:
nom_public is validated on creation and updatenom_public is allowed (validation skipped)Error Response (400):
{
"error": "nom_public contains inappropriate content",
"details": "Contains forbidden words: word1, word2"
}
Implementation:
data/badwords/ (one file per language)Validation Endpoints:
POST /api/users - User creationPOST /api/users (HMAC) - User creation via WooCommerce/Stripe (INTERNAL USE ONLY)PATCH /api/users/:id - User profile updatePOST /api/users/register - User registrationThe API implements a pre-moderation scoring system to help PowerUsers identify potentially problematic content in stories and quizzes.
Score Types:
Bad Words Score (moderation_badwords_score): Number of occurrences of forbidden words found in all textual fields
title, description, yearevents, pseudo, organisation, official_title, official_description, official_moreLLM Score (moderation_llm_score): Future score based on LLM analysis
NULL (not implemented yet)When Scores Are Calculated:
title, description, yearevents, pseudo, organisation, or official_* fields)status, type, level)Response Format: Stories returned by admin endpoints include moderation scores:
{
"id": "uuid",
"title": "Ma story",
"description": "...",
"moderation_badwords_score": 3,
"moderation_llm_score": null,
"moderation_badwords_calculated_at": "2025-01-15T10:30:00Z",
...
}
Admin Endpoints:
GET /api/admin/poweruser/stories - Returns stories with moderation scoresGET /api/admin/stories/:id - Returns story with moderation scoresPATCH /api/admin/poweruser/stories/:id - Updates story and recalculates score if textual fields changedPATCH /api/admin/stories/:id - Updates story and recalculates score if textual fields changedUsage for PowerUsers:
moderation_badwords_score to quickly identify stories/quizzes with inappropriate languagemoderation_llm_score will provide additional context for content qualityThere are two authentication modes depending on the route type:
/api/stories/*, /api/scores/*) require a public API key/api/auth/*, /api/admin/*) require a JWT Bearer tokenPublic routes require a public API key sent with each request. The key can be provided in one of three ways:
key field in the request body (for POST, PATCH, etc.).{
"key": "your_api_key_here",
...
}
?key=your_api_key_here to the URL. This is necessary for GET requests.X-API-Key header.X-API-Key: your_api_key_herePOST /api/admin/stories/check-attachment (JWT required){ "id": "<story_uuid>" } or { "url": "https://terraguessr.org/?story=<uuid>" }{ success: true, storyId, attached: boolean, owner_user_id: string|null }The system supports three user roles:
poweruser: Full administrative access (create, read, update, delete users and stories)storyadmin: Can manage their own stories and view their profilebot: Can create users (for e-commerce integration)The API supports multi-language content for emails and HTML pages. The system automatically detects the user's preferred language and serves content accordingly.
The system supports 12 languages:
The system detects the user's preferred language in the following order:
?lang=fr (for HTML pages)langue_preferee column in users tableAll user-facing content is automatically translated:
Arabic language includes RTL (Right-to-Left) support:
dir="rtl" attributeEmail with language detection:
// The system automatically detects language from:
// 1. User's langue_preferee in database
// 2. Accept-Language header
// 3. Query parameter ?lang=fr
await emailService.sendActivationEmail(email, firstName, token, userId, language);
HTML page with language detection:
// GET /activate?token=abc123&user=456&lang=fr
// Automatically serves French content
API response with translations:
// Error messages are automatically translated
res.json({
error: i18nService.t('api.errors.emailSendError', language)
});
Admin routes are protected by JWT and must use the Authorization: Bearer <token> header. Public routes are unchanged and still use the public API key.
JWT_SECRET to your .env.# --- Auth Configuration ---
JWT_SECRET="change_me_to_a_long_random_secret"
Endpoints:
POST /api/auth/register → Create an admin user
{ "username": string, "email": string, "password": string, "role": "poweruser" | "storyadmin" }{ success: true, id }POST /api/auth/login → Get a JWT
{ "username": string, "password": string }{ token, user: { id, role } }POST /api/auth/forgot-password → Placeholder (200 OK)
{ "email": string }Token payload: { userId: string, role: "poweruser" | "storyadmin" }, expiration 12h.
Login sequence (client):
Authorization: Bearer <token>Examples (cURL):
# 1) Register (once)
curl -X POST http://localhost:3001/api/auth/register \
-H 'Content-Type: application/json' \
-d '{
"username": "editor1",
"email": "editor1@example.com",
"password": "changeme",
"role": "storyadmin"
}'
# 2) Login
TOKEN=$(curl -s -X POST http://localhost:3001/api/auth/login \
-H 'Content-Type: application/json' \
-d '{
"username": "editor1",
"password": "changeme"
}' | jq -r .token)
# 3a) Admin: update a story (poweruser or owner)
curl -X PATCH http://localhost:3001/api/admin/stories/<STORY_ID> \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "title": "New title" }'
# 3b) Admin: claim a story (storyadmin only)
curl -X POST http://localhost:3001/api/admin/stories/<STORY_ID>/claim \
-H "Authorization: Bearer $TOKEN"
Access control:
story_ownership) or after /claimJWT-protected routes for user management:
POST /api/users → create user (poweruser, bot)GET /api/users → list all users with pagination (poweruser only)GET /api/users/search → search users (poweruser only)GET /api/users/:id → get user profile (own profile or poweruser)PATCH /api/users/:id → update user profile (own profile or poweruser)DELETE /api/users/:id → delete user (poweruser only)Public routes for payment processing and user registration:
POST /api/stripe/create-checkout-session → create Stripe checkout sessionPOST /api/stripe/webhook → handle Stripe webhook eventsTypes de paiement gérés :
Configuration webhook Stripe requise :
checkout.session.completed → Paiements normauxsetup_intent.succeeded → Empreintes de cartepayment_intent.succeeded → Paiements avec capture manuelleDétails techniques :
setup : Session Stripe sans line_items pour les empreintespayment : Session Stripe avec line_items pour les paiementsPOST /api/users/register → NOUVELLE ROUTE UNIFIÉE - create complete account with optional paymentPOST /api/users/hmac → create user via HMAC (INTERNAL USE ONLY - Stripe webhooks)GET /activate → display account activation pagePOST /api/users/activate → finalize account activation with passwordPOST /api/users/forgot-password → request password reset (supports email OR username)POST /api/users/reset-password → reset password with tokenThe forgotPassword route now supports both email and username identification:
Request Body:
{
"username": "user@example.com" // Can be either email or username
}
Automatic Detection:
@, searches by email field@ is found, searches by username fieldExamples:
# Using email
curl -X POST http://localhost:3001/api/users/forgot-password \
-H 'Content-Type: application/json' \
-d '{"username": "user@example.com"}'
# Using username
curl -X POST http://localhost:3001/api/users/forgot-password \
-H 'Content-Type: application/json' \
-d '{"username": "john_doe"}'
Response:
{
"message": "Si cet email ou nom d'utilisateur existe dans notre système, vous recevrez un lien de réinitialisation"
}
JWT-protected routes for support ticket management:
POST /api/tickets → create ticketGET /api/tickets/categories → list ticket categoriesGET /api/tickets → list user's own tickets (with pagination)GET /api/tickets/:id → get single ticket with repliesPOST /api/tickets/:id/reply → reply to ticketPATCH /api/tickets/:id/close → close own ticketGET /api/tickets/admin/all → list all tickets (with pagination and filters)PATCH /api/tickets/admin/:id → update any ticketPOST /api/tickets/categories → create ticket categoryPATCH /api/tickets/categories/:id → update ticket categoryDELETE /api/tickets/categories/:id → delete ticket categoryNOUVEAU FLUX UNIFIÉ - Création de compte avec paiement immédiat :
# Création de compte avec paiement normal (≥ 0.50€)
curl -X POST http://localhost:3001/api/users/register \
-H 'Content-Type: application/json' \
-d '{
"username": "john_doe",
"email": "user@example.com",
"password": "superSecret123",
"subscription_type": "lifetime",
"amount": 99.99,
"defer_payment": false
}'
NOUVEAU FLUX UNIFIÉ - Création de compte avec empreinte de carte :
# Création de compte avec empreinte de carte (0€)
curl -X POST http://localhost:3001/api/users/register \
-H 'Content-Type: application/json' \
-d '{
"username": "jane_smith",
"email": "jane@example.com",
"password": "superSecret123",
"subscription_type": "annual",
"amount": 0,
"defer_payment": false
}'
NOUVEAU FLUX UNIFIÉ - Création de compte avec validation différée :
# Création de compte sans paiement immédiat
curl -X POST http://localhost:3001/api/users/register \
-H 'Content-Type: application/json' \
-d '{
"username": "bob_wilson",
"email": "bob@example.com",
"password": "superSecret123",
"subscription_type": "annual",
"defer_payment": true
}'
curl -X POST http://localhost:3001/api/users/forgot-password
-H 'Content-Type: application/json'
-d '{
"username": "user@example.com"
}'
curl -X POST http://localhost:3001/api/users/forgot-password
-H 'Content-Type: application/json'
-d '{
"username": "john_doe"
}'
curl -X POST http://localhost:3001/api/users/reset-password
-H 'Content-Type: application/json'
-d '{
"token": "reset_token_from_email",
"password": "new_password123"
}'
#### User Management Examples
```bash
# Create a new user (poweruser or bot)
curl -X POST http://localhost:3001/api/users \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"username": "newuser",
"email": "user@example.com",
"password": "password123",
"role": "storyadmin",
"nom_public": "Public Name",
"description": "User description",
"lien": "https://example.com",
"langue_preferee": "fr",
"status": "submitted"
}'
# List all users with pagination
curl -H "Authorization: Bearer $TOKEN" "http://localhost:3001/api/users?page=1&limit=20"
# Search users
curl -H "Authorization: Bearer $TOKEN" "http://localhost:3001/api/users/search?q=john&limit=10"
# Get user profile
curl -H "Authorization: Bearer $TOKEN" http://localhost:3001/api/users/<USER_ID>
# Update user profile
curl -X PATCH http://localhost:3001/api/users/<USER_ID> \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"nom_public": "Updated Public Name",
"description": "Updated description",
"status": "verified"
}'
# Delete user
curl -X DELETE http://localhost:3001/api/users/<USER_ID> \
-H "Authorization: Bearer $TOKEN"
# Create a new ticket
curl -X POST http://localhost:3001/api/tickets \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"title": "Bug in story creation",
"category_id": 1,
"message": "I cannot create a new story, getting an error",
"story_id": "<STORY_ID>",
"is_private": true
}'
# List ticket categories
curl -H "Authorization: Bearer $TOKEN" http://localhost:3001/api/tickets/categories
# List my tickets
curl -H "Authorization: Bearer $TOKEN" "http://localhost:3001/api/tickets?page=1&limit=10"
# Get single ticket with replies
curl -H "Authorization: Bearer $TOKEN" http://localhost:3001/api/tickets/<TICKET_ID>
# Reply to ticket
curl -X POST http://localhost:3001/api/tickets/<TICKET_ID>/reply \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"message": "Thank you for the information, I will look into this"}'
# Close my ticket
curl -X PATCH http://localhost:3001/api/tickets/<TICKET_ID>/close \
-H "Authorization: Bearer $TOKEN"
# Poweruser: List all tickets
curl -H "Authorization: Bearer $TOKEN" "http://localhost:3001/api/tickets/admin/all?status=submitted&page=1&limit=20"
# Poweruser: Update ticket status
curl -X PATCH http://localhost:3001/api/tickets/admin/<TICKET_ID> \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"status": "in_progress"}'
# Poweruser: Create ticket category
curl -X POST http://localhost:3001/api/tickets/categories \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"name": "Technical Issue",
"description": "Technical problems and bugs",
"color": "#dc3545"
}'
# Poweruser: Update ticket category
curl -X PATCH http://localhost:3001/api/tickets/categories/<CATEGORY_ID> \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"name": "Bug",
"description": "Repeated technical error message. Please copy the error message in your question.",
"color": "#ed333b"
}'
# Poweruser: Delete ticket category
curl -X DELETE http://localhost:3001/api/tickets/categories/<CATEGORY_ID> \
-H "Authorization: Bearer $TOKEN"
Advanced admin routes for poweruser role only:
GET /api/admin/poweruser/stories → all stories with owner info?ownerId=<user_id>&language=<lang>&status=<status>PATCH /api/admin/poweruser/stories/:id → update story metadata and reassign ownership{ "title": "...", "ownerId": "<user_id>", "status": "..." }Examples (cURL):
# List all stories with owner info
curl -H "Authorization: Bearer $TOKEN" http://localhost:3001/api/admin/poweruser/stories
# Filter by owner
curl -H "Authorization: Bearer $TOKEN" "http://localhost:3001/api/admin/poweruser/stories?ownerId=<USER_ID>"
# Filter by language and status
curl -H "Authorization: Bearer $TOKEN" "http://localhost:3001/api/admin/poweruser/stories?language=fr&status=verified"
# Update story and reassign to another user
curl -X PATCH http://localhost:3001/api/admin/poweruser/stories/<STORY_ID> \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "title": "Updated by poweruser", "ownerId": "<NEW_USER_ID>", "status": "validated" }'
# Remove ownership (set ownerId to null/empty)
curl -X PATCH http://localhost:3001/api/admin/poweruser/stories/<STORY_ID> \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "ownerId": "" }'
JWT-protected routes for story admins:
GET /api/admin/stories → poweruser: all stories (all statuses); storyadmin: stories owned by current userGET /api/admin/stories/:id → get a single story (poweruser: any story; storyadmin: only owned stories)POST /api/admin/stories/attach { url, password, id? } → attach a story by URL+password or by ID+passwordPATCH /api/admin/stories/:id → update a story (poweruser or owner) - can change status to any valueDELETE /api/admin/stories/:id → delete story completely (poweruser or owner)DELETE /api/admin/stories/:id/detach → detach the story (remove ownership only)POST /api/admin/stories/attach)The attach route allows story admins to claim ownership of existing stories by providing authentication credentials.
Request Body Options:
{ "url": "story-url", "password": "story-password" }{ "id": "story-uuid", "password": "story-password" }Password Authentication: The route supports multiple password formats for backward compatibility:
$2)URL Processing:
http:// and https://)?story=UUID in URLs)Success Response:
{
"success": true,
"storyId": "attached-story-uuid"
}
Error Responses:
400 Bad Request: Missing URL/ID or password403 Forbidden: Invalid password for this story404 Not Found: Story not found for provided URL or IDExamples (cURL):
# List my stories
curl -H "Authorization: Bearer $TOKEN" http://localhost:3001/api/admin/stories
# Get a single story
curl -H "Authorization: Bearer $TOKEN" http://localhost:3001/api/admin/stories/<STORY_ID>
# Attach a story by URL
curl -X POST http://localhost:3001/api/admin/stories/attach \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "url": "https://example.com/story-1", "password": "secret" }'
# Attach by ID (for stories without URLs)
curl -X POST http://localhost:3001/api/admin/stories/attach \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "id": "<STORY_ID>", "password": "secret" }'
# Attach story without password (empty password)
curl -X POST http://localhost:3001/api/admin/stories/attach \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "id": "<STORY_ID>", "password": "" }'
# Update one of my stories
curl -X PATCH http://localhost:3001/api/admin/stories/<STORY_ID> \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "title": "Updated by owner" }'
# Change story status (moderation)
curl -X PATCH http://localhost:3001/api/admin/stories/<STORY_ID> \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "status": "verified" }'
# Delete a story completely
curl -X DELETE http://localhost:3001/api/admin/stories/<STORY_ID> \
-H "Authorization: Bearer $TOKEN"
# Detach a story (remove ownership only)
curl -X DELETE http://localhost:3001/api/admin/stories/<STORY_ID>/detach \
-H "Authorization: Bearer $TOKEN"
The system supports the following story statuses:
draft: Story in draft mode (not published, not accessible via public routes)submitted: Story submitted by user, awaiting moderationverified: Story verified by moderator (published)validated: Story validated by moderator (published)banned: Story banned (not accessible via public routes)quarantine: Story in quarantine (not accessible via public routes)private: Story is private (not accessible via public routes)The system automatically saves versions of stories when they are modified, with different retention strategies based on story status:
For verified/validated stories (hybrid retention):
For other stories (simple retention):
API Endpoints:
Version Management:
GET /api/admin/stories/:id/versions - List all versions of a story (requires ownership or poweruser)GET /api/admin/stories/:id/versions/:versionId - Get a specific versionPOST /api/admin/stories/:id/versions/:versionId/restore - Restore a version (poweruser only)Statistics Endpoints:
GET /api/admin/stories/versions/stats - Global versioning statistics (poweruser only)startDate, endDate, storyId (optional)GET /api/admin/stories/versions/activity - Recent version activity feed (poweruser only)limit (default: 50), offset (default: 0)GET /api/admin/stories/:id/versions/stats - Statistics for a specific story (poweruser or owner)The system supports the following story types:
story: Traditional story content (default for existing stories)quizz: Quiz/quiz content for interactive learningNote: All existing stories in the database are automatically marked as "story" type during migration.
The system supports the following user fields:
id: Unique user identifier (UUID)username: Login username (unique)email: User email address (unique)password_hash: Hashed password (bcrypt)role: User role (poweruser, storyadmin, bot)created_at: Account creation timestamp (auto-set by server, format: YYYY-MM-DD HH:MM:SS)date_modif: Last modification timestamp (auto-updated by server on any change, format: YYYY-MM-DD HH:MM:SS)logo: Profile image URL (PNG, WebP, JPG, GIF)description: Long text descriptionnom_public: Public display namelien: User website/link URLlangue_preferee: Preferred language (default: 'fr')organisation: User organization namestatus: User status (same as story statuses)date_rgpd: GDPR acceptance dateid: Unique story identifier (UUID)title: Story title (required)description: Story descriptionlanguage: Language code (e.g., 'fr', 'en')source: Story sourceindicator: Story indicatorcategory: Story categoryyearevents: Year events textemail: Contact emailpseudo: Author pseudonymorganisation: Organization nameurl: Story URLimage: Story image URLcreation_date: Creation timestampmodif_date: Last modification timestamppassword: Story password (for secure updates)prestatus: Previous statusstatus: Current status (draft, submitted, verified, validated, banned, quarantine, private)type: Story type (story, quizz)votes: Number of voteslevel: Optional level field (integer)token: Optional token field for story identification (can be set during creation or by admin)// 1. Create story with password
const storyData = {
key: "public-api-key",
title: "My Story",
description: "Story description",
password: "my-secure-password", // Set password during creation
token: "my-identifier"
};
const response = await fetch('/api/stories', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(storyData)
});
// 2. Attach story to user account
const attachData = {
id: storyId,
password: "my-secure-password"
};
await fetch('/api/admin/stories/attach', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${jwtToken}`
},
body: JSON.stringify(attachData)
});
// 1. Create story without password
const storyData = {
key: "public-api-key",
title: "My Story",
description: "Story description",
token: "my-identifier"
// No password field
};
// 2. Attach story with empty password
const attachData = {
id: storyId,
password: "" // Empty password for stories without passwords
};
The system supports the following ticket statuses:
submitted: Ticket submitted by user, awaiting reviewseen: Ticket has been seen by support teamdiscussion: Active discussion between user and supportin_progress: Support team is working on the issueresolved: Issue has been resolvedclosed: Ticket closed by user or supportdeleted: Ticket deleted (soft delete)The system supports the following ticket fields:
id: Unique ticket identifier (UUID)title: Ticket title (max 200 characters)category_id: Reference to ticket categorymessage: Initial ticket messageuser_id: Creator of the ticketstory_id: Optional linked story (UUID)is_private: Privacy setting (true/false)status: Current ticket statuscreated_at: Ticket creation timestamp (auto-set by server)date_modif: Last modification timestamp (auto-updated by server)id: Unique reply identifier (UUID)ticket_id: Reference to parent ticketuser_id: Author of the replymessage: Reply message contentcreated_at: Reply creation timestamp (auto-set by server)The server automatically manages timestamps:
created_at: Set to current timestamp when user is created (cannot be modified)date_modif: Automatically updated to current timestamp on any user updatedate_rgpd: Set to current timestamp when user is created (can be updated manually)Important: These date fields are managed by the server and should not be included in request bodies for POST or PATCH operations.
POST /api/stories/fetch"verified" or "validated" (published content only)"verified", "validated", or "submitted" (includes pending moderation)"banned", "quarantine", "onhold"): Only accessible via admin routesstatus (string, optional): Filters stories by status (e.g., submitted, draft). If set to "published", it returns stories with a status of either "verified" or "validated".language (string, optional): Filters stories by language code (e.g., fr).type (string, optional): Filters stories by type. Valid values: "story" or "quizz".categoryIds (array of numbers, optional): Filter stories by one or more category IDs.sortBy (string, optional): Sort by "views", "votes", or "creation_date" (default: "creation_date").sortOrder (string, optional): Sort order "asc" or "desc" (default: "desc").{
"key": "your_api_key_here",
"status": "published",
"language": "fr",
"type": "story"
}
200 OK):{
"count": 1,
"stories": [
{
"id": "some-uuid-1234",
"title": "A Validated Story",
"status": "validated",
...
}
]
}
GET /api/stories/:idid (string, required): The UUID of the story.200 OK):{
"id": "some-uuid-1234",
"title": "A Validated Story",
...
}
404 Not Found):{ "error": "Story not found" }
Stories now support extended official metadata fields that can be used by both public and private routes:
official_title (string, optional, max 255 chars): Official title for the storyofficial_description (string, optional, long text): Extended official descriptionofficial_range (string, optional, max 20 chars): Official time range or periodofficial_source (string, optional, max 255 chars): Official data sourceofficial_more (string, optional, long text): Additional official informationThese fields are:
official_description and official_more support unlimited textExample with Official Fields:
{
"title": "Climate Change Story",
"description": "User description",
"official_title": "Global Temperature Trends 2020-2024",
"official_description": "Comprehensive analysis of global temperature data from multiple sources including satellite measurements, ground stations, and ocean temperature records.",
"official_range": "2020-2024",
"official_source": "NASA Climate Data",
"official_more": "Data collected from 15,000+ weather stations worldwide, validated through peer review process, updated monthly."
}
The system supports multiple categories per story, allowing better organization and filtering. Categories are fully multilingual, supporting 24 languages.
Categories support translations for 24 languages:
en, fr, es, de, it, pt, ru, zh, ja, ar, hi, ko, tr, nl, pl, sv, da, fi, no, cs, hu, ro, el, he
The API automatically detects the user's preferred language using:
?lang=frlangue_preferee preference (if authenticated)Accept-Language headeren) or French (fr)GET /api/admin/stories/categories?lang=fr - List all active categories (JWT required)name and description translated according to the detected languagelanguage field indicating the language usedPOST /api/admin/stories/categories - Create a new category (poweruser only, JWT required){ "translations": { "en": { "name": "Geography", "description": "..." }, "fr": { "name": "Géographie", "description": "..." } }, "color": "#007bff" }translations.en.name)PATCH /api/admin/stories/categories/:id - Update a category (poweruser only, JWT required){ "translations": { "es": { "name": "Geografía", "description": "..." } }, "color": "#ff0000", "is_active": true }DELETE /api/admin/stories/categories/:id - Delete a category (poweruser only, JWT required)Category Response Structure (translated according to language):
{
"categories": [
{
"id": 1,
"name": "Géographie",
"description": "Histoires sur la géographie",
"color": "#007bff",
"is_active": true,
"created_at": "2025-01-15T10:30:00.000Z"
}
],
"language": "fr"
}
Translation Format (stored in database):
{
"translations": {
"en": { "name": "Geography", "description": "Stories about geography" },
"fr": { "name": "Géographie", "description": "Histoires sur la géographie" },
"es": { "name": "Geografía", "description": "Historias sobre geografía" }
}
}
Fallback Behavior:
en)fr)When creating or updating a story, use categoryIds (array of category IDs):
Create Story with Categories:
{
"key": "public-api-key",
"title": "My Story",
"categoryIds": [1, 2, 3]
}
Update Story Categories:
{
"categoryIds": [1, 5]
}
Story Response includes categories:
{
"id": "story-uuid",
"title": "My Story",
"categories": [
{ "id": 1, "name": "Géographie", "color": "#007bff" },
{ "id": 2, "name": "Histoire", "color": "#28a745" }
]
}
The POST /api/stories/fetch endpoint supports filtering by categories and sorting by views/votes:
Request Body:
{
"key": "your_api_key_here",
"categoryIds": [1, 2],
"sortBy": "views",
"sortOrder": "desc"
}
Parameters:
categoryIds (array of numbers, optional): Filter stories by one or more categoriessortBy (string, optional): "views" | "votes" | "creation_date" (default: "creation_date")sortOrder (string, optional): "asc" | "desc" (default: "desc")Examples:
// Get stories in category 1, sorted by views (descending)
{
"categoryIds": [1],
"sortBy": "views",
"sortOrder": "desc"
}
// Get stories in categories 1 or 2, sorted by votes (ascending)
{
"categoryIds": [1, 2],
"sortBy": "votes",
"sortOrder": "asc"
}
Note: The old category field (string) is deprecated but still supported for backward compatibility. Use categoryIds (array) for the new system.
POST /api/storiescreation_date are automatically generated.title is required. Optional fields include token for story identification, password for future attachment, level for story level, status for initial status (only "draft" or "submitted" allowed), categoryIds (array of category IDs) for categories, and official fields for extended metadata.{
"key": "public-api-key",
"title": "My Story Title",
"description": "Story description",
"language": "fr",
"level": 5,
"status": "draft",
"token": "my-story-identifier-123",
"password": "my-story-password",
"official_title": "Official Story Title",
"official_description": "Extended official description with detailed information",
"official_range": "2020-2024",
"official_source": "Official Data Source",
"official_more": "Additional official information and context"
}
status is "draft" or "submitted", it will be used as specifiedstatus is any other value or not provided, it defaults to "submitted"201 Created):{
"success": true,
"id": "newly-generated-uuid-5678"
}
PATCH /api/stories/:idmodif_date is automatically updated.200 OK):{
"success": true,
"id": "some-uuid-1234",
"updated": ["status", "title"]
}
Endpoint: PATCH /api/stories/:id/secure
Description: Updates a story only if the provided password matches the one stored for that story.
Request Body: A JSON object with the fields to update, plus a mandatory password field.
Success Response (200 OK):
{
"success": true,
"id": "some-uuid-1234",
"updated": ["title", "description"]
}
Endpoint: POST /api/stories/update-with-token
Description: Updates a story by matching the provided token field with the story's token field in the database. Only allows updating specific fields and clears the token field after successful update.
Request Body:
{
"id": "story-uuid",
"token": "story-token-value",
"key": "public-api-key",
"language": "fr",
"source": "Updated source",
"indicator": "Updated indicator",
"category": "Updated category",
"yearevents": "Updated events",
"description": "Updated description"
}
Allowed Fields: Only language, source, indicator, category, yearevents, and description can be updated.
Success Response (200 OK):
{
"success": true,
"id": "story-uuid",
"updated": ["language", "description"],
"message": "Story updated successfully and token cleared"
}
Error Responses:
400 Bad Request: Missing required fields or no valid fields to update403 Forbidden: Token field doesn't match404 Not Found: Story not foundThis is a multi-purpose endpoint that mimics the functionality of the original Google Apps Script API for scores.
GET or POST /api/scorespseudo and score parameters.pseudo (string, required)score (number, required)pays, resume, niveau, champion (string, optional)OK with a 200 status code.testScore parameter.200 OK):{
"testScore": 500,
"rank": 101,
"total": 5000
}
scoreContext parameter.200 OK):{
"score": 450,
"rank": 150,
"context": [
{ "rank": 147, "pseudo": "PlayerA", "score": 455, "isSelf": false, ... },
{ "rank": 150, "pseudo": "PlayerB", "score": 450, "isSelf": true, ... },
{ "rank": 151, "pseudo": "PlayerC", "score": 449, ... }
]
}
start (number, optional, default: 1): The starting rank.end (number, optional, default: 100): The ending rank.200 OK): An array of score objects.[
{ "rank": 1, "pseudo": "Champ", "score": 999, ... },
{ "rank": 2, "pseudo": "RunnerUp", "score": 990, ... }
]
JWT-protected routes for PowerUsers to manage game scores:
GET /api/admin/poweruser/scores → List all scores with pagination and filtersDELETE /api/admin/poweruser/scores/:id → Delete a specific score by IDAuthentication: JWT with poweruser role required
Query Parameters:
page (optional, default: 1): Page numberlimit (optional, default: 50, max: 200): Results per pagepseudo (optional): Filter by pseudo (partial match)pays (optional): Filter by country (exact match)minScore (optional): Minimum scoremaxScore (optional): Maximum scoresortBy (optional, default: 'score'): Sort by 'score', 'timestamp', or 'pseudo'sortOrder (optional, default: 'DESC'): 'ASC' or 'DESC'Response (200 OK):
{
"scores": [
{
"id": 123,
"rank": 1,
"timestamp": "2025-01-15T10:30:00Z",
"pseudo": "Player1",
"pays": "France",
"score": 1000,
"resume": "...",
"niveau": "...",
"champion": "..."
}
],
"pagination": {
"page": 1,
"limit": 50,
"total": 5000,
"totalPages": 100
}
}
Example:
# List first page
curl -H "Authorization: Bearer $TOKEN" http://localhost:3001/api/admin/poweruser/scores
# Filter by pseudo and paginate
curl -H "Authorization: Bearer $TOKEN" "http://localhost:3001/api/admin/poweruser/scores?pseudo=Player&page=2&limit=25"
# Filter by score range
curl -H "Authorization: Bearer $TOKEN" "http://localhost:3001/api/admin/poweruser/scores?minScore=500&maxScore=1000"
Authentication: JWT with poweruser role required
Parameters: id in URL path (score ID to delete)
Response (200 OK):
{
"success": true,
"message": "Score deleted successfully",
"deletedId": 123
}
Errors:
400: Invalid score ID404: Score not found500: Server errorExample:
curl -X DELETE \
-H "Authorization: Bearer $TOKEN" \
http://localhost:3001/api/admin/poweruser/scores/123
Security Notes:
poweruser role can access these routesThis endpoint provides AI-powered chat functionality using Google's Gemini AI model.
POST /api/ai/chatAuthorization: Bearer <JWT_TOKEN> (required)Content-Type: application/json{
"prompt": "Your question or prompt for the AI"
}
Success Response (200 OK):
{
"success": true,
"response": "AI generated response text",
"model": "gemini-2.5-flash",
"duration": 1500,
"timestamp": "2024-01-15T10:30:00.000Z"
}
Error Responses:
400 Bad Request: Missing or invalid prompt401 Unauthorized: Missing or invalid JWT token500 Internal Server Error: AI service configuration error504 Gateway Timeout: AI request timeout (after 4 minutes)These endpoints provide view tracking and voting functionality for stories.
Endpoint: POST /api/views-votes/viewed
Authentication: Public API key required
Description: Increments the view count for a specific story.
Request Body:
{
"id_story": "story-uuid-here",
"key": "your-api-key"
}
Success Response (200 OK):
{
"success": true,
"id_story": "story-uuid-here",
"vues": 15,
"last_view": "2024-01-15T10:30:00.000Z"
}
Endpoint: POST /api/views-votes/voted
Authentication: Public API key required
Description: Records a vote for a story (0-5 stars). Prevents duplicate votes from the same identifier.
Request Body:
{
"id_story": "story-uuid-here",
"vote_value": 4,
"identifiant": "user123_session456",
"key": "your-api-key"
}
Success Response (200 OK) - New vote:
{
"success": true,
"id_story": "story-uuid-here",
"vote_value": 4,
"identifiant": "user123_session456",
"vote_date": "2024-01-15T10:30:00.000Z",
"message": "Vote successfully recorded",
"vote_recorded": true
}
Success Response (200 OK) - Duplicate vote:
{
"success": true,
"id_story": "story-uuid-here",
"vote_value": 3,
"identifiant": "user123_session456",
"vote_date": "2024-01-15T10:30:00.000Z",
"message": "Vote not counted - this identifier has already voted for this story",
"vote_recorded": false
}
Endpoint: GET /api/views-votes/stats/:id
Authentication: Public API key required
Description: Retrieves view and vote statistics for a specific story.
URL Parameters:
id (string, required): The UUID of the story.Success Response (200 OK):
{
"success": true,
"id_story": "story-uuid-here",
"views": {
"total": 150,
"last_view": "2024-01-15T10:30:00.000Z"
},
"votes": {
"total": 25,
"average_rating": 4.2
}
}
Pour configurer automatiquement Stripe (produits, webhooks, Customer Portal) :
# 1. Configurer les variables d'environnement de base
STRIPE_SECRET_KEY=sk_test_...
API_URL=https://api.terraguessr.org
# 2. Lancer l'initialisation
npm run stripe:setup
Pour vérifier la configuration actuelle :
npm run stripe:check
Pour réinitialiser la configuration de test :
npm run stripe:reset
.stripe-config.test.json : Configuration pour l'environnement de test.stripe-config.live.json : Configuration pour l'environnement de productionLe script détecte automatiquement l'environnement via la clé API :
sk_test_... : Environnement de testsk_live_... : Environnement de production.stripe-config.{environment}.jsonprice_data pour les prix libres.
├── api/ # Backend source code
│ ├── db.ts # Database connection pool
│ ├── middleware.ts # Express middlewares (e.g., public key check)
│ ├── public_keys.json # Manages public API keys and quotas
│ ├── quotaManager.ts # Logic for key loading and quota tracking
│ ├── server.ts # Express server setup and entry point
│ ├── schema.ts # Database schema creation
│ ├── auth.middleware.ts # JWT authentication middleware
│ ├── auth.controller.ts # User authentication (login, register)
│ ├── users.controller.ts # User management
│ ├── tickets.controller.ts # Support tickets system
│ ├── stories.controller.ts # Public stories routes
│ ├── admin.stories.controller.ts # Admin stories management
│ ├── mystories.controller.ts # User story ownership
│ ├── poweruser.controller.ts # Poweruser admin functions
│ └── scores.controller.ts # Scores and statistics
├── components/ # Frontend React components
...