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

590 lines
14 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 🔧 Entwicklerhandbuch
Technische Dokumentation für Entwickler des Ninja Cross Parkour Systems.
## 📋 Inhaltsverzeichnis
- [🏗️ System-Architektur](#-system-architektur)
- [🛠️ Entwicklungsumgebung](#-entwicklungsumgebung)
- [📡 API-Integration](#-api-integration)
- [🗄️ Datenbank-Schema](#-datenbank-schema)
- [🔐 Authentifizierung](#-authentifizierung)
- [🧪 Testing](#-testing)
- [🚀 Deployment](#-deployment)
- [📊 Monitoring](#-monitoring)
- [🔧 Wartung](#-wartung)
## 🏗️ 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
```bash
# 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
```bash
# 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
```javascript
// 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
```javascript
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
```javascript
// 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
```sql
-- 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
```sql
-- 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
```sql
-- 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
```javascript
// 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
```javascript
// 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
```javascript
// 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
```javascript
// 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
```javascript
// 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
```bash
# Alle Tests
npm test
# Unit-Tests
npm run test:unit
# Integration-Tests
npm run test:integration
# Coverage-Report
npm run test:coverage
```
## 🚀 Deployment
### Produktionsumgebung
```bash
# 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
# Dockerfile
FROM node:16-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
```
### Nginx-Konfiguration
```nginx
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
```bash
# Let's Encrypt
certbot --nginx -d ninja.reptilfpv.de
```
## 📊 Monitoring
### Logging
```javascript
// 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
```javascript
// 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
```javascript
// 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
```bash
# 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
```bash
# Logrotate-Konfiguration
/var/log/ninjaserver/*.log {
daily
missingok
rotate 30
compress
delaycompress
notifempty
create 644 node node
postrotate
systemctl reload ninjaserver
endscript
}
```
### Performance-Optimierung
```sql
-- 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
```bash
# Abhängigkeiten aktualisieren
npm audit
npm audit fix
# Sicherheits-Updates
npm update
```
---
**Hinweis:** Für detaillierte API-Dokumentation siehe [API Referenz](API-Referenz) und für Achievement-Details siehe [Achievement System](Achievement-System).