Files
Ninjaserver/wiki/Entwicklerhandbuch.md
2025-09-23 14:13:24 +02:00

14 KiB

🔧 Entwicklerhandbuch

Technische Dokumentation für Entwickler des Ninja Cross Parkour Systems.

📋 Inhaltsverzeichnis

🏗️ System-Architektur

Übersicht

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   Frontend      │    │   Backend       │    │   Database      │
│   (Web UI)      │◄──►│   (Node.js)     │◄──►│   (PostgreSQL)  │
└─────────────────┘    └─────────────────┘    └─────────────────┘
         │                       │                       │
         │                       │                       │
         ▼                       ▼                       ▼
┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   RFID Reader   │    │   API Endpoints │    │   Achievement   │
│   (Hardware)    │    │   (REST)        │    │   System        │
└─────────────────┘    └─────────────────┘    └─────────────────┘

Technologie-Stack

  • Backend: Node.js + Express.js
  • Datenbank: PostgreSQL
  • Frontend: HTML5 + CSS3 + JavaScript
  • Authentifizierung: JWT + bcrypt
  • API: RESTful API
  • Maps: Leaflet.js + OpenStreetMap
  • RFID: Hardware-Integration

Projektstruktur

ninjaserver/
├── server.js              # Hauptserver-Datei
├── routes/
│   ├── api.js            # API-Routen
│   ├── public.js         # Öffentliche Routen
│   ├── private.js        # Private Routen
│   ├── web.js            # Web-Routen
│   └── admin.js          # Admin-Routen
├── middleware/
│   ├── auth.js           # Authentifizierung
│   ├── validation.js     # Eingabe-Validierung
│   └── logging.js        # Logging
├── models/
│   ├── Player.js         # Spieler-Modell
│   ├── Time.js           # Zeit-Modell
│   ├── Location.js       # Standort-Modell
│   └── Achievement.js    # Achievement-Modell
├── scripts/
│   ├── init-db.js        # Datenbankinitialisierung
│   ├── create-user.js    # Benutzer-Erstellung
│   └── daily_achievements.js # Tägliche Achievements
├── public/
│   ├── index.html        # Hauptanwendung
│   ├── login.html        # Login-Seite
│   ├── css/              # Stylesheets
│   └── js/               # JavaScript
├── test/
│   ├── api.test.js       # API-Tests
│   ├── unit.test.js      # Unit-Tests
│   └── integration.test.js # Integration-Tests
└── docs/
    ├── API.md            # API-Dokumentation
    ├── ACHIEVEMENTS.md   # Achievement-Dokumentation
    └── wiki/             # Wiki-Dokumentation

🛠️ Entwicklungsumgebung

Voraussetzungen

  • Node.js v16 oder höher
  • PostgreSQL 12 oder höher
  • Git für Versionskontrolle
  • npm oder yarn für Paketverwaltung

Setup

# Repository klonen
git clone <repository-url>
cd ninjaserver

# Abhängigkeiten installieren
npm install

# Umgebungsvariablen konfigurieren
cp .env.example .env
# .env-Datei bearbeiten

# Datenbank initialisieren
npm run init-db

# Entwicklungsserver starten
npm run dev

Entwicklungsskripte

# Entwicklungsserver mit Auto-Reload
npm run dev

# Tests ausführen
npm test

# Linting
npm run lint

# Datenbank zurücksetzen
npm run reset-db

# API-Dokumentation generieren
npm run docs

IDE-Empfehlungen

  • Visual Studio Code mit Extensions:
    • ES6 code snippets
    • PostgreSQL
    • REST Client
    • GitLens

📡 API-Integration

Authentifizierung

// API-Key Authentifizierung
const headers = {
  'Authorization': 'Bearer YOUR_API_KEY',
  'Content-Type': 'application/json'
};

// Session-basierte Authentifizierung
const session = await authenticateUser(username, password);

API-Client Beispiel

class NinjaParkourAPI {
  constructor(apiKey, baseURL = 'http://localhost:3000') {
    this.apiKey = apiKey;
    this.baseURL = baseURL;
  }

  async request(endpoint, options = {}) {
    const url = `${this.baseURL}${endpoint}`;
    const config = {
      headers: {
        'Authorization': `Bearer ${this.apiKey}`,
        'Content-Type': 'application/json',
        ...options.headers
      },
      ...options
    };

    const response = await fetch(url, config);
    return response.json();
  }

  // Spieler erstellen
  async createPlayer(playerData) {
    return this.request('/api/v1/public/players', {
      method: 'POST',
      body: JSON.stringify(playerData)
    });
  }

  // Zeit messen
  async recordTime(timeData) {
    return this.request('/api/v1/private/create-time', {
      method: 'POST',
      body: JSON.stringify(timeData)
    });
  }

  // Achievements abrufen
  async getAchievements(playerId) {
    return this.request(`/api/achievements/player/${playerId}`);
  }
}

// Verwendung
const api = new NinjaParkourAPI('your-api-key');
const player = await api.createPlayer({
  firstname: 'Max',
  lastname: 'Mustermann',
  birthdate: '1990-01-01',
  rfiduid: 'AA:BB:CC:DD'
});

WebSocket Integration

// Real-time Updates
const socket = io('http://localhost:3000');

socket.on('timeRecorded', (data) => {
  console.log('Neue Zeit:', data);
  updateLeaderboard(data);
});

socket.on('achievementEarned', (data) => {
  console.log('Neues Achievement:', data);
  showNotification(data);
});

🗄️ Datenbank-Schema

Tabellen-Übersicht

-- Spieler
CREATE TABLE players (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    firstname VARCHAR(50) NOT NULL,
    lastname VARCHAR(50) NOT NULL,
    birthdate DATE NOT NULL,
    rfiduid VARCHAR(20) UNIQUE,
    supabase_user_id UUID,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Standorte
CREATE TABLE locations (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) UNIQUE NOT NULL,
    latitude DECIMAL(10, 8) NOT NULL,
    longitude DECIMAL(11, 8) NOT NULL,
    time_threshold JSONB DEFAULT '{"seconds": 120}',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Zeiten
CREATE TABLE times (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    player_id UUID REFERENCES players(id),
    location_id UUID REFERENCES locations(id),
    recorded_time JSONB NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Achievements
CREATE TABLE achievements (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) NOT NULL,
    description TEXT,
    category VARCHAR(50) NOT NULL,
    condition_type VARCHAR(50) NOT NULL,
    condition_value INTEGER NOT NULL,
    icon VARCHAR(10),
    points INTEGER DEFAULT 0,
    is_active BOOLEAN DEFAULT true,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Spieler-Achievements
CREATE TABLE player_achievements (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    player_id UUID REFERENCES players(id),
    achievement_id UUID REFERENCES achievements(id),
    earned_at TIMESTAMP,
    progress INTEGER DEFAULT 0,
    is_completed BOOLEAN DEFAULT false,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Indizes

-- Performance-Indizes
CREATE INDEX idx_times_player_id ON times(player_id);
CREATE INDEX idx_times_location_id ON times(location_id);
CREATE INDEX idx_times_created_at ON times(created_at);
CREATE INDEX idx_player_achievements_player_id ON player_achievements(player_id);
CREATE INDEX idx_player_achievements_achievement_id ON player_achievements(achievement_id);

PostgreSQL Funktionen

-- Achievement-Prüfung
CREATE OR REPLACE FUNCTION check_all_achievements(player_uuid UUID)
RETURNS VOID AS $$
BEGIN
    PERFORM check_consistency_achievements(player_uuid);
    PERFORM check_improvement_achievements(player_uuid);
    PERFORM check_seasonal_achievements(player_uuid);
END;
$$ LANGUAGE plpgsql;

🔐 Authentifizierung

API-Key Authentifizierung

// Middleware für API-Key
const authenticateAPIKey = (req, res, next) => {
  const authHeader = req.headers.authorization;
  const token = authHeader && authHeader.split(' ')[1];

  if (!token) {
    return res.status(401).json({ error: 'API-Key erforderlich' });
  }

  // Token validieren
  const isValid = validateAPIKey(token);
  if (!isValid) {
    return res.status(401).json({ error: 'Ungültiger API-Key' });
  }

  req.apiKey = token;
  next();
};

Session-basierte Authentifizierung

// Session-Middleware
const authenticateSession = (req, res, next) => {
  if (!req.session || !req.session.userId) {
    return res.status(401).json({ error: 'Nicht authentifiziert' });
  }

  req.userId = req.session.userId;
  next();
};

JWT-Token

// JWT-Token generieren
const generateJWT = (user) => {
  return jwt.sign(
    { userId: user.id, username: user.username },
    process.env.JWT_SECRET,
    { expiresIn: '24h' }
  );
};

// JWT-Token validieren
const validateJWT = (token) => {
  try {
    return jwt.verify(token, process.env.JWT_SECRET);
  } catch (error) {
    return null;
  }
};

🧪 Testing

Unit-Tests

// test/unit/Player.test.js
const { Player } = require('../../models/Player');

describe('Player Model', () => {
  test('should create player with valid data', () => {
    const playerData = {
      firstname: 'Max',
      lastname: 'Mustermann',
      birthdate: '1990-01-01',
      rfiduid: 'AA:BB:CC:DD'
    };

    const player = new Player(playerData);
    expect(player.firstname).toBe('Max');
    expect(player.lastname).toBe('Mustermann');
  });
});

Integration-Tests

// test/integration/api.test.js
const request = require('supertest');
const app = require('../../server');

describe('API Endpoints', () => {
  test('POST /api/v1/public/players', async () => {
    const playerData = {
      firstname: 'Max',
      lastname: 'Mustermann',
      birthdate: '1990-01-01',
      rfiduid: 'AA:BB:CC:DD'
    };

    const response = await request(app)
      .post('/api/v1/public/players')
      .send(playerData)
      .expect(201);

    expect(response.body.success).toBe(true);
    expect(response.body.data.firstname).toBe('Max');
  });
});

API-Tests ausführen

# Alle Tests
npm test

# Unit-Tests
npm run test:unit

# Integration-Tests
npm run test:integration

# Coverage-Report
npm run test:coverage

🚀 Deployment

Produktionsumgebung

# Abhängigkeiten installieren
npm install --production

# Umgebungsvariablen setzen
export NODE_ENV=production
export DB_HOST=production-db-host
export DB_PASSWORD=secure-password

# Server starten
npm start

Docker-Container

# Dockerfile
FROM node:16-alpine

WORKDIR /app
COPY package*.json ./
RUN npm install --production

COPY . .
EXPOSE 3000

CMD ["npm", "start"]

Nginx-Konfiguration

server {
    listen 80;
    server_name ninja.reptilfpv.de;

    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}

SSL-Zertifikat

# Let's Encrypt
certbot --nginx -d ninja.reptilfpv.de

📊 Monitoring

Logging

// Winston Logger
const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
    new winston.transports.File({ filename: 'logs/combined.log' })
  ]
});

Health-Checks

// Health-Check Endpoint
app.get('/health', async (req, res) => {
  try {
    // Datenbank-Verbindung prüfen
    await db.query('SELECT 1');
    
    res.json({
      status: 'healthy',
      timestamp: new Date().toISOString(),
      uptime: process.uptime()
    });
  } catch (error) {
    res.status(500).json({
      status: 'unhealthy',
      error: error.message
    });
  }
});

Metriken

// Prometheus-Metriken
const prometheus = require('prom-client');

const httpRequestDuration = new prometheus.Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duration of HTTP requests in seconds',
  labelNames: ['method', 'route', 'status_code']
});

const activeConnections = new prometheus.Gauge({
  name: 'active_connections',
  help: 'Number of active connections'
});

🔧 Wartung

Datenbank-Backup

# Backup erstellen
pg_dump -h localhost -U username -d ninjaserver > backup.sql

# Backup wiederherstellen
psql -h localhost -U username -d ninjaserver < backup.sql

Log-Rotation

# Logrotate-Konfiguration
/var/log/ninjaserver/*.log {
    daily
    missingok
    rotate 30
    compress
    delaycompress
    notifempty
    create 644 node node
    postrotate
        systemctl reload ninjaserver
    endscript
}

Performance-Optimierung

-- Query-Performance analysieren
EXPLAIN ANALYZE SELECT * FROM times 
WHERE player_id = 'uuid' 
ORDER BY created_at DESC;

-- Indizes hinzufügen
CREATE INDEX CONCURRENTLY idx_times_player_created 
ON times(player_id, created_at DESC);

Sicherheits-Updates

# Abhängigkeiten aktualisieren
npm audit
npm audit fix

# Sicherheits-Updates
npm update

Hinweis: Für detaillierte API-Dokumentation siehe API Referenz und für Achievement-Details siehe Achievement System.