14 KiB
14 KiB
🔧 Entwicklerhandbuch
Technische Dokumentation für Entwickler des Ninja Cross Parkour Systems.
📋 Inhaltsverzeichnis
- 🏗️ System-Architektur
- 🛠️ Entwicklungsumgebung
- 📡 API-Integration
- 🗄️ Datenbank-Schema
- 🔐 Authentifizierung
- 🧪 Testing
- 🚀 Deployment
- 📊 Monitoring
- 🔧 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
# 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.