Update
This commit is contained in:
589
wiki/Entwicklerhandbuch.md
Normal file
589
wiki/Entwicklerhandbuch.md
Normal file
@@ -0,0 +1,589 @@
|
||||
# 🔧 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).
|
||||
Reference in New Issue
Block a user