API Documentation

This page provides documentation for the TerraGuessr API.

API Endpoints

TerraGuessr Story Manager & API

This project contains a full-stack application for managing "stories" for TerraGuessr. It includes:

  1. A Node.js/Express API server written in TypeScript that connects to a MySQL database.
  2. A React-based admin dashboard to moderate and manage stories.

📑 Table des matières


🏛️ How It Works: Architecture Overview

This application follows a classic client-server architecture:

🆕 Latest Features

Payment System & User Registration

Email System

Internationalization (i18n) System

User Management System

Support Tickets System

Enhanced Security

Story Types & Quizz System

Official Story Fields

Views and Votes System


🚀 Setup and Installation

Prerequisites

1. Backend API Server Setup

Follow these steps to get the API server running:

Step 1: Clone and Install

  1. Clone the repository and navigate to the project's root directory:

    git clone <repository-url>
    cd terraguessr-api-manager_beforeAdmin
    
  2. Install Node.js dependencies:

    npm install
    

    Required packages (automatically installed):

    Core Dependencies:

    • express - Web framework for Node.js
    • mysql2 - MySQL database driver with Promise support
    • bcrypt - Password hashing for security
    • jsonwebtoken - JWT token generation and verification
    • helmet - Security headers middleware
    • cors - Cross-origin resource sharing
    • express-rate-limit - Rate limiting middleware
    • marked - Markdown parsing for documentation
    • dotenv - Environment variables loader
    • uuid - UUID generation for unique IDs

    Development Dependencies:

    • ts-node-dev - Development server with auto-restart
    • typescript - TypeScript compiler
    • @types/* - TypeScript type definitions

Step 2: Database Setup

  1. Create 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;
    
  2. Run database migrations (automatic on first server start): The server will automatically create all required tables on first startup in the correct order:

    1. users table
    2. stories table
    3. story_ownership table (with foreign keys)
    4. ticket_categories table
    5. tickets table (with foreign keys)
    6. 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.

Step 3: Environment Configuration

  1. Create .env file in the root directory:

    touch .env
    
  2. 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:

    • Replace your_secure_password with your actual database password
    • Replace your_very_long_and_secure_random_string_here_at_least_32_characters with a secure random string
    • Replace your_gemini_api_key_here with your actual Gemini AI API key
    • Keep the .env file secure and never commit it to version control

Step 4: API Keys Configuration

  1. Configure Public API Keys in api/public_keys.json:
    {
      "keys": [
        {
          "key": "your_public_api_key_here",
          "quota": 1000,
          "description": "Main API key"
        }
      ]
    }
    

Step 5: Launch the Server

  1. 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
    
  2. Verify the server is running:

    • Open your browser and go to http://localhost:3001
    • You should see the API documentation page
    • Check the terminal for startup messages:
      🚀 Server is running on http://localhost:3001
      Loaded 2 public API keys.
      Connected to the MySQL database
      

Step 6: Initial Setup (First Time Only)

  1. 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');
    
  2. 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"}'
    

2. Troubleshooting

Common Issues:

Development Tips:

3. Available Scripts

The 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

4. Environment Variables Reference

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

5. File Structure

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
  1. 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.

  2. 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.

  3. Run the server:

    • For development (with automatic reloading):
      npm run dev
      
    • For production: First, build the TypeScript code into JavaScript:
      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.


🖥️ Frontend Admin Dashboard

The frontend application is an administration tool designed for moderating and managing user-submitted stories.

Workflow & Features


📋 Database Schema

Table: stories

If 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:

Table: scores

Use 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;

🔒 Security

Several security measures have been implemented to protect the API from common attacks and abuse.

API Key Quotas

To prevent abuse, each public API key has a daily request limit.

IP-Based Rate Limiting

In addition to key-based quotas, the API also uses IP-based rate limiting to prevent brute-force attacks.

HTTP Header Security (Helmet)

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.

Caching System

The API implements an intelligent in-memory caching system to improve performance and reduce database load for frequently accessed data.

Cache Features

Cached Routes

The following routes benefit from caching:

Cache Headers

The API returns cache status headers for debugging:

Cache Invalidation

Cache is automatically invalidated when:

Cache Statistics & Monitoring

The cache system includes comprehensive statistics and monitoring capabilities:

Statistics Available:

Administrative Endpoints (Poweruser only):

Logging 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"
  }
}

reCAPTCHA Server-side Validation

To protect public submission routes, the API supports server-side validation of Google reCAPTCHA v2 Invisible.

Environment Variable:

Client Payload (Story Editor):

Server Behaviour:

Notes:

Bad Words Filter (Content Moderation)

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:

Error Response (400):

{
  "error": "nom_public contains inappropriate content",
  "details": "Contains forbidden words: word1, word2"
}

Implementation:

Validation Endpoints:

Content Moderation & Pre-moderation Scores

The API implements a pre-moderation scoring system to help PowerUsers identify potentially problematic content in stories and quizzes.

Score Types:

  1. Bad Words Score (moderation_badwords_score): Number of occurrences of forbidden words found in all textual fields

    • Calculated automatically on story/quiz creation and update
    • All textual fields are analyzed: title, description, yearevents, pseudo, organisation, official_title, official_description, official_more
    • Each occurrence is counted (e.g., if a word appears 3 times, score increases by 3)
    • Score = 0 means no forbidden words found
    • Higher scores indicate more occurrences of inappropriate language
  2. LLM Score (moderation_llm_score): Future score based on LLM analysis

    • Currently NULL (not implemented yet)
    • Field prepared for future implementation

When Scores Are Calculated:

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:

Usage for PowerUsers:


📖 API Documentation

Authentication

There are two authentication modes depending on the route type:

Public API Key

Public routes require a public API key sent with each request. The key can be provided in one of three ways:

  1. JSON Body: Include a key field in the request body (for POST, PATCH, etc.).
    {
      "key": "your_api_key_here",
      ...
    }
    
  2. Query Parameter: Append ?key=your_api_key_here to the URL. This is necessary for GET requests.
  3. HTTP Header: Send the key in an X-API-Key header.
    • X-API-Key: your_api_key_here

Admin Stories - Attachment Utilities

User Roles

The system supports three user roles:

Internationalization (i18n) System

The API supports multi-language content for emails and HTML pages. The system automatically detects the user's preferred language and serves content accordingly.

Supported Languages

The system supports 12 languages:

Language Detection

The system detects the user's preferred language in the following order:

  1. Query parameter: ?lang=fr (for HTML pages)
  2. User database preference: langue_preferee column in users table
  3. HTTP Accept-Language header: Parsed from browser settings
  4. Default fallback: English (en)

Translated Content

All user-facing content is automatically translated:

RTL Support

Arabic language includes RTL (Right-to-Left) support:

Usage Examples

Email 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 Authentication (JWT)

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.

# --- Auth Configuration ---
JWT_SECRET="change_me_to_a_long_random_secret"

Endpoints:

Token payload: { userId: string, role: "poweruser" | "storyadmin" }, expiration 12h.

Login sequence (client):

  1. Register (one-time) or ensure the user exists
  2. Login to obtain a JWT
  3. Call admin endpoints with 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:

User Management Routes

JWT-protected routes for user management:

Payment & User Registration Routes

Public routes for payment processing and user registration:

Stripe Payment Routes

Types de paiement gérés :

Configuration webhook Stripe requise :

Détails techniques :

User Registration Routes

Password Reset with Email/Username Support

The forgotPassword route now supports both email and username identification:

Request Body:

{
  "username": "user@example.com"  // Can be either email or username
}

Automatic Detection:

Examples:

# 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"
}

Support Tickets Routes

JWT-protected routes for support ticket management:

All Roles (poweruser, storyadmin, bot)

Poweruser Only

Payment Flow Examples

NOUVEAU 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
  }'

2. Request password reset (supports email OR username)

curl -X POST http://localhost:3001/api/users/forgot-password
-H 'Content-Type: application/json'
-d '{ "username": "user@example.com" }'

Alternative: using username instead of email

curl -X POST http://localhost:3001/api/users/forgot-password
-H 'Content-Type: application/json'
-d '{ "username": "john_doe" }'

3. Reset password with token

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"

Support Tickets Examples

# 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"

Poweruser Admin Routes

Advanced admin routes for poweruser role only:

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": "" }'

Admin Stories (ownership)

JWT-protected routes for story admins:

Story Attachment (POST /api/admin/stories/attach)

The attach route allows story admins to claim ownership of existing stories by providing authentication credentials.

Request Body Options:

Password Authentication: The route supports multiple password formats for backward compatibility:

URL Processing:

Success Response:

{
  "success": true,
  "storyId": "attached-story-uuid"
}

Error Responses:

Examples (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"

Story Statuses

The system supports the following story statuses:

Story Versioning

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:

Statistics Endpoints:

Story Types

The system supports the following story types:

Note: All existing stories in the database are automatically marked as "story" type during migration.

User Fields

The system supports the following user fields:

Story Fields

Recommended Client Workflows

Workflow A: Create Story with Password (Recommended)

// 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)
});

Workflow B: Create Story without Password

// 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
};

Ticket Statuses

The system supports the following ticket statuses:

Ticket Fields

The system supports the following ticket fields:

Ticket Reply Fields

Date Management

The server automatically manages timestamps:

Important: These date fields are managed by the server and should not be included in request bodies for POST or PATCH operations.

Story Routes

1. Fetch Stories


2. Get Story by ID


Official Story Fields

Stories now support extended official metadata fields that can be used by both public and private routes:

These fields are:

Example 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."
}

Story Categories

The system supports multiple categories per story, allowing better organization and filtering. Categories are fully multilingual, supporting 24 languages.

Multilingual Support

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:

  1. Query parameter ?lang=fr
  2. User's langue_preferee preference (if authenticated)
  3. HTTP Accept-Language header
  4. Fallback to English (en) or French (fr)

Category Management (Poweruser Only)

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:

Using Categories in Stories

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" }
  ]
}

Filtering and Sorting Stories by Category

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:

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.


3. Create a New Story


4. Update a Story (Admin)


5. Securely Update a Story (User)


Score / Leaderboard Route

This is a multi-purpose endpoint that mimics the functionality of the original Google Apps Script API for scores.

Action 1: Record a New Score

Action 2: Get Hypothetical Rank

Action 3: Get Score Context

Action 4: Get Leaderboard (Default)

Admin Scores Routes (PowerUser only)

JWT-protected routes for PowerUsers to manage game scores:

GET /api/admin/poweruser/scores

Authentication: JWT with poweruser role required

Query Parameters:

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"

DELETE /api/admin/poweruser/scores/:id

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:

Example:

curl -X DELETE \
  -H "Authorization: Bearer $TOKEN" \
  http://localhost:3001/api/admin/poweruser/scores/123

Security Notes:


AI Chat Route

This endpoint provides AI-powered chat functionality using Google's Gemini AI model.

Request

Response

Features


Views and Votes Routes

These endpoints provide view tracking and voting functionality for stories.

1. Record a View

2. Record a Vote

3. Get Story Statistics

Features


Configuration Stripe Automatique

Initialisation

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

Vérification

Pour vérifier la configuration actuelle :

npm run stripe:check

Reset (développement uniquement)

Pour réinitialiser la configuration de test :

npm run stripe:reset

Produits créés

  1. Abonnement annuel récurrent : Renouvellement automatique avec prix libre
  2. Paiement one-shot 1 an : Accès 1 an sans renouvellement avec prix libre

Fichiers générés

Environnements

Le script détecte automatiquement l'environnement via la clé API :

Notes importantes


📁 Project Structure

.
├── 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
...