feat(match3): Erweiterung der Match3-Admin-Funktionalitäten und -Modelle

- Implementierung neuer Endpunkte für die Verwaltung von Match3-Kampagnen, Levels, Objectives und Tile-Typen im Admin-Bereich.
- Anpassung der Admin-Services zur Unterstützung von Benutzerberechtigungen und Fehlerbehandlung.
- Einführung von neuen Modellen und Assoziationen für Match3-Levels und Tile-Typen in der Datenbank.
- Verbesserung der Internationalisierung für Match3-spezifische Texte in Deutsch und Englisch.
- Aktualisierung der Frontend-Routen und -Komponenten zur Verwaltung von Match3-Inhalten.
This commit is contained in:
Torsten Schulz (local)
2025-08-23 06:00:29 +02:00
parent 3eb7ae4e93
commit e168adeb51
40 changed files with 6474 additions and 1007 deletions

View File

@@ -0,0 +1,177 @@
# Datenbank-Schema-Updates
## Problem
Sequelize erstellt bei jedem `sync()` mit `alter: true` die Constraints neu, was zu Duplikaten und Problemen führen kann.
## Lösung
### 1. Intelligente Schema-Synchronisation (Standard)
```javascript
// In utils/sequelize.js - syncModelsWithUpdates()
// Prüft automatisch ob Schema-Updates nötig sind
// Verwendet alter: true nur bei Bedarf
await syncModelsWithUpdates(models);
```
- **Automatische Erkennung** von Schema-Updates
- **alter: true nur bei Bedarf** - verhindert Duplikate
- **Fallback** zu alter: false wenn keine Updates nötig
- **Sicher und effizient**
### 2. Normale Synchronisation (ohne Updates)
```javascript
// In utils/sequelize.js - syncModels()
await model.sync({ alter: false, force: false });
```
- **Keine Schema-Änderungen**
- **Keine Constraints werden neu erstellt**
- **Schnell und sicher**
### 3. Manuelle Schema-Updates (nur bei Bedarf)
```javascript
// In utils/sequelize.js - updateSchema()
await model.sync({ alter: true, force: false });
```
- **Nur bei expliziten Schema-Änderungen verwenden**
- **Kann zu Duplikaten führen**
- **Nach Update: Zurück zu intelligenter Synchronisation**
### 4. Vollständiger Reset (nur in Entwicklung)
```javascript
await model.sync({ force: true });
```
- **Löscht alle Daten!**
- **Nur in Entwicklungsumgebung verwenden**
## Aktuelle Implementierung
### Intelligente Synchronisation
```javascript
// syncModelsWithUpdates() prüft automatisch:
// 1. Alle verfügbaren Schemas (community, falukant_data, match3, etc.)
// 2. Alle Tabellen in jedem Schema
// 3. Alle Spalten in jeder Tabelle
// 4. Datentypen, Constraints und Standardwerte
// 5. Verwendet alter: true nur wenn nötig
```
### Universelle Schema-Prüfung
```javascript
// checkSchemaUpdates() prüft alle Schemas:
// - community (Benutzer, Rechte, etc.)
// - falukant_data (Spieldaten)
// - falukant_type (Spieltypen)
// - falukant_predefine (Vordefinierte Daten)
// - falukant_log (Spiel-Logs)
// - chat (Chat-System)
// - forum (Forum-System)
// - match3 (Match3-Spiel)
// - logs (Allgemeine Logs)
// - type (Allgemeine Typen)
// - service (Service-Daten)
```
### Detaillierte Tabellen-Prüfung
```javascript
// checkTableForUpdates() prüft jede Tabelle:
// - Fehlende Spalten
// - Neue Spalten
// - Geänderte Spalten
// - Model-zu-Tabelle-Zuordnung
```
### Umfassende Spalten-Prüfung
```javascript
// checkColumnForUpdates() prüft jede Spalte:
// - Datentyp-Änderungen (INTEGER → BIGINT)
// - NULL/NOT NULL Constraints
// - Standardwerte
// - Längen-Änderungen (VARCHAR)
```
## Workflow für Schema-Updates
### Schritt 1: Schema ändern
```javascript
// In models/.../model.js
// Neue Felder, Constraints, etc. hinzufügen
```
### Schritt 2: Automatische Erkennung
```javascript
// syncModelsWithUpdates() erkennt automatisch:
// - Welche Updates nötig sind
// - Verwendet alter: true nur bei Bedarf
// - Läuft bei jedem Start sicher
```
### Schritt 3: Keine manuellen Änderungen nötig
```javascript
// Die intelligente Synchronisation:
// - Erkennt Änderungen automatisch
// - Wendet sie sicher an
// - Verhindert Duplikate
```
## Constraint-Bereinigung
### Analyse ausführen
```bash
cd backend
node utils/cleanupDatabaseConstraints.js
```
### Manuelle Bereinigung
```sql
-- Doppelte Foreign Keys entfernen
ALTER TABLE schema.table_name DROP CONSTRAINT constraint_name;
-- Doppelte Indexe entfernen
DROP INDEX IF EXISTS index_name;
```
## Best Practices
1. **Intelligente Synchronisation verwenden** - `syncModelsWithUpdates()`
2. **Keine manuellen Schema-Updates** bei jedem Start
3. **updateSchema() nur bei komplexen Änderungen** verwenden
4. **Constraints regelmäßig prüfen** auf Duplikate
5. **Backup vor Schema-Updates** erstellen
## Aktuelle Konfiguration
- **Standard-Synchronisation**: `syncModelsWithUpdates()` (intelligent)
- **Schema-Updates**: Automatisch erkannt und angewendet
- **Manuelle Updates**: Separate `updateSchema()` Funktion
- **Automatische Updates**: Aktiviert und sicher
- **Manuelle Kontrolle**: Verfügbar bei Bedarf
## Troubleshooting
### Problem: Duplizierte Constraints
```bash
# Analyse ausführen
node utils/cleanupDatabaseConstraints.js
# Manuell bereinigen
# Siehe SQL-Beispiele oben
```
### Problem: Schema-Synchronisation schlägt fehl
```javascript
// Intelligente Synchronisation sollte das automatisch handhaben
// Bei Problemen: updateSchema() verwenden
await updateSchema(models);
```
### Problem: Performance-Probleme
- **Intelligente Synchronisation** läuft nur bei Bedarf
- **alter: true** wird nur verwendet wenn nötig
- **Normale Starts** sind schnell und sicher
## Vorteile der neuen Lösung
1. **Automatisch**: Erkennt Schema-Updates automatisch
2. **Sicher**: Verwendet alter: true nur bei Bedarf
3. **Effizient**: Normale Starts ohne Schema-Updates
4. **Flexibel**: Kann für verschiedene Modelle erweitert werden
5. **Wartbar**: Klare Trennung zwischen Update-Logik und Synchronisation

View File

@@ -139,41 +139,73 @@ class AdminController {
async getRoomTypes(req, res) { async getRoomTypes(req, res) {
try { try {
const types = await AdminService.getRoomTypes(); const userId = req.headers.userid;
if (!userId) {
return res.status(401).json({ error: 'User ID fehlt' });
}
const types = await AdminService.getRoomTypes(userId);
res.status(200).json(types); res.status(200).json(types);
} catch (error) { } catch (error) {
console.log(error); if (error.message === 'noaccess') {
res.status(500).json({ error: error.message }); res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
} else {
console.log(error);
res.status(500).json({ error: error.message });
}
} }
} }
async getGenderRestrictions(req, res) { async getGenderRestrictions(req, res) {
try { try {
const restrictions = await AdminService.getGenderRestrictions(); const userId = req.headers.userid;
if (!userId) {
return res.status(401).json({ error: 'User ID fehlt' });
}
const restrictions = await AdminService.getGenderRestrictions(userId);
res.status(200).json(restrictions); res.status(200).json(restrictions);
} catch (error) { } catch (error) {
console.log(error); if (error.message === 'noaccess') {
res.status(500).json({ error: error.message }); res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
} else {
console.log(error);
res.status(500).json({ error: error.message });
}
} }
} }
async getUserRights(req, res) { async getUserRights(req, res) {
try { try {
const rights = await AdminService.getUserRights(); const userId = req.headers.userid;
if (!userId) {
return res.status(401).json({ error: 'User ID fehlt' });
}
const rights = await AdminService.getUserRights(userId);
res.status(200).json(rights); res.status(200).json(rights);
} catch (error) { } catch (error) {
console.log(error); if (error.message === 'noaccess') {
res.status(500).json({ error: error.message }); res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
} else {
console.log(error);
res.status(500).json({ error: error.message });
}
} }
} }
async getRooms(req, res) { async getRooms(req, res) {
try { try {
const rooms = await AdminService.getRooms(); const userId = req.headers.userid;
if (!userId) {
return res.status(401).json({ error: 'User ID fehlt' });
}
const rooms = await AdminService.getRooms(userId);
res.status(200).json(rooms); res.status(200).json(rooms);
} catch (error) { } catch (error) {
console.log(error); if (error.message === 'noaccess') {
res.status(500).json({ error: error.message }); res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
} else {
console.log(error);
res.status(500).json({ error: error.message });
}
} }
} }
@@ -251,6 +283,350 @@ class AdminController {
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
} }
} }
// --- Match3 Admin Methods ---
async getMatch3Campaigns(req, res) {
try {
const userId = req.headers.userid;
if (!userId) {
return res.status(401).json({ error: 'User ID fehlt' });
}
const campaigns = await AdminService.getMatch3Campaigns(userId);
res.status(200).json(campaigns);
} catch (error) {
if (error.message === 'noaccess') {
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
} else {
console.error('Error in getMatch3Campaigns:', error);
res.status(500).json({ error: error.message });
}
}
}
async getMatch3Campaign(req, res) {
try {
const userId = req.headers.userid;
if (!userId) {
return res.status(401).json({ error: 'User ID fehlt' });
}
const campaign = await AdminService.getMatch3Campaign(userId, req.params.id);
res.status(200).json(campaign);
} catch (error) {
if (error.message === 'noaccess') {
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
} else {
console.error('Error in getMatch3Campaign:', error);
res.status(500).json({ error: error.message });
}
}
}
async createMatch3Campaign(req, res) {
try {
const userId = req.headers.userid;
if (!userId) {
return res.status(401).json({ error: 'User ID fehlt' });
}
const campaign = await AdminService.createMatch3Campaign(userId, req.body);
res.status(201).json(campaign);
} catch (error) {
if (error.message === 'noaccess') {
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
} else {
console.error('Error in createMatch3Campaign:', error);
res.status(500).json({ error: error.message });
}
}
}
async updateMatch3Campaign(req, res) {
try {
const userId = req.headers.userid;
if (!userId) {
return res.status(401).json({ error: 'User ID fehlt' });
}
const campaign = await AdminService.updateMatch3Campaign(userId, req.params.id, req.body);
res.status(200).json(campaign);
} catch (error) {
if (error.message === 'noaccess') {
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
} else {
console.error('Error in updateMatch3Campaign:', error);
res.status(500).json({ error: error.message });
}
}
}
async deleteMatch3Campaign(req, res) {
try {
const userId = req.headers.userid;
if (!userId) {
return res.status(401).json({ error: 'User ID fehlt' });
}
await AdminService.deleteMatch3Campaign(userId, req.params.id);
res.sendStatus(204);
} catch (error) {
if (error.message === 'noaccess') {
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
} else {
console.error('Error in deleteMatch3Campaign:', error);
res.status(500).json({ error: error.message });
}
}
}
async getMatch3Levels(req, res) {
try {
const userId = req.headers.userid;
if (!userId) {
return res.status(401).json({ error: 'User ID fehlt' });
}
const levels = await AdminService.getMatch3Levels(userId);
res.status(200).json(levels);
} catch (error) {
if (error.message === 'noaccess') {
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
} else {
console.error('Error in getMatch3Levels:', error);
res.status(500).json({ error: error.message });
}
}
}
async getMatch3Level(req, res) {
try {
const userId = req.headers.userid;
if (!userId) {
return res.status(401).json({ error: 'User ID fehlt' });
}
const level = await AdminService.getMatch3Level(userId, req.params.id);
res.status(200).json(level);
} catch (error) {
if (error.message === 'noaccess') {
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
} else {
console.error('Error in getMatch3Level:', error);
res.status(500).json({ error: error.message });
}
}
}
async createMatch3Level(req, res) {
try {
const userId = req.headers.userid;
if (!userId) {
return res.status(401).json({ error: 'User ID fehlt' });
}
const level = await AdminService.createMatch3Level(userId, req.body);
res.status(201).json(level);
} catch (error) {
if (error.message === 'noaccess') {
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
} else {
console.error('Error in createMatch3Level:', error);
res.status(500).json({ error: error.message });
}
}
}
async updateMatch3Level(req, res) {
try {
const userId = req.headers.userid;
if (!userId) {
return res.status(401).json({ error: 'User ID fehlt' });
}
const level = await AdminService.updateMatch3Level(userId, req.params.id, req.body);
res.status(200).json(level);
} catch (error) {
if (error.message === 'noaccess') {
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
} else {
console.error('Error in updateMatch3Level:', error);
res.status(500).json({ error: error.message });
}
}
}
async deleteMatch3Level(req, res) {
try {
const userId = req.headers.userid;
if (!userId) {
return res.status(401).json({ error: 'User ID fehlt' });
}
await AdminService.deleteMatch3Level(userId, req.params.id);
res.sendStatus(204);
} catch (error) {
if (error.message === 'noaccess') {
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
} else {
console.error('Error in deleteMatch3Level:', error);
res.status(500).json({ error: error.message });
}
}
}
// Match3 Objectives
async getMatch3Objectives(req, res) {
try {
const userId = req.headers.userid;
if (!userId) {
return res.status(401).json({ error: 'User ID fehlt' });
}
const objectives = await AdminService.getMatch3Objectives(userId);
res.status(200).json(objectives);
} catch (error) {
if (error.message === 'noaccess') {
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
} else {
console.error('Error in getMatch3Objectives:', error);
res.status(500).json({ error: error.message });
}
}
}
async getMatch3Objective(req, res) {
try {
const userId = req.headers.userid;
if (!userId) {
return res.status(401).json({ error: 'User ID fehlt' });
}
const objective = await AdminService.getMatch3Objective(userId, req.params.id);
res.status(200).json(objective);
} catch (error) {
if (error.message === 'noaccess') {
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
} else {
console.error('Error in getMatch3Objective:', error);
res.status(500).json({ error: error.message });
}
}
}
async createMatch3Objective(req, res) {
try {
const userId = req.headers.userid;
if (!userId) {
return res.status(401).json({ error: 'User ID fehlt' });
}
const objective = await AdminService.createMatch3Objective(userId, req.body);
res.status(201).json(objective);
} catch (error) {
if (error.message === 'noaccess') {
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
} else {
console.error('Error in createMatch3Objective:', error);
res.status(500).json({ error: error.message });
}
}
}
async updateMatch3Objective(req, res) {
try {
const userId = req.headers.userid;
if (!userId) {
return res.status(401).json({ error: 'User ID fehlt' });
}
const objective = await AdminService.updateMatch3Objective(userId, req.params.id, req.body);
res.status(200).json(objective);
} catch (error) {
if (error.message === 'noaccess') {
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
} else {
console.error('Error in updateMatch3Objective:', error);
res.status(500).json({ error: error.message });
}
}
}
async deleteMatch3Objective(req, res) {
try {
const userId = req.headers.userid;
if (!userId) {
return res.status(401).json({ error: 'User ID fehlt' });
}
await AdminService.deleteMatch3Objective(userId, req.params.id);
res.sendStatus(204);
} catch (error) {
if (error.message === 'noaccess') {
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
} else {
console.error('Error in deleteMatch3Objective:', error);
res.status(500).json({ error: error.message });
}
}
}
async getMatch3TileTypes(req, res) {
try {
const userId = req.headers.userid;
if (!userId) {
return res.status(401).json({ error: 'User ID fehlt' });
}
const tileTypes = await AdminService.getMatch3TileTypes(userId);
res.status(200).json(tileTypes);
} catch (error) {
if (error.message === 'noaccess') {
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
} else {
console.error('Error in getMatch3TileTypes:', error);
res.status(500).json({ error: error.message });
}
}
}
async createMatch3TileType(req, res) {
try {
const userId = req.headers.userid;
if (!userId) {
return res.status(401).json({ error: 'User ID fehlt' });
}
const tileType = await AdminService.createMatch3TileType(userId, req.body);
res.status(201).json(tileType);
} catch (error) {
if (error.message === 'noaccess') {
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
} else {
console.error('Error in createMatch3TileType:', error);
res.status(500).json({ error: error.message });
}
}
}
async updateMatch3TileType(req, res) {
try {
const userId = req.headers.userid;
if (!userId) {
return res.status(401).json({ error: 'User ID fehlt' });
}
const tileType = await AdminService.updateMatch3TileType(userId, req.params.id, req.body);
res.status(200).json(tileType);
} catch (error) {
if (error.message === 'noaccess') {
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
} else {
console.error('Error in updateMatch3TileType:', error);
res.status(500).json({ error: error.message });
}
}
}
async deleteMatch3TileType(req, res) {
try {
const userId = req.headers.userid;
if (!userId) {
return res.status(401).json({ error: 'User ID fehlt' });
}
await AdminService.deleteMatch3TileType(userId, req.params.id);
res.sendStatus(204);
} catch (error) {
if (error.message === 'noaccess') {
res.status(403).json({ error: 'Keine Berechtigung für diese Aktion' });
} else {
console.error('Error in deleteMatch3TileType:', error);
res.status(500).json({ error: error.message });
}
}
}
} }
export default AdminController; export default AdminController;

View File

@@ -1,128 +1,212 @@
import match3Service from '../services/match3Service.js'; import Match3Service from '../services/match3Service.js';
class Match3Controller { function extractHashedUserId(req) {
/** return req.headers?.userid;
* Lädt alle aktiven Kampagnen
*/
async getCampaigns(req, res) {
try {
const campaigns = await match3Service.getActiveCampaigns();
res.json({ success: true, data: campaigns });
} catch (error) {
console.error('Error in getCampaigns:', error);
res.status(500).json({ success: false, message: 'Fehler beim Laden der Kampagnen' });
}
}
/**
* Lädt eine spezifische Kampagne
*/
async getCampaign(req, res) {
try {
const { campaignId } = req.params;
const campaign = await match3Service.getCampaign(campaignId);
if (!campaign) {
return res.status(404).json({ success: false, message: 'Kampagne nicht gefunden' });
}
res.json({ success: true, data: campaign });
} catch (error) {
console.error('Error in getCampaign:', error);
res.status(500).json({ success: false, message: 'Fehler beim Laden der Kampagne' });
}
}
/**
* Lädt den Benutzerfortschritt für eine Kampagne
*/
async getUserProgress(req, res) {
try {
const { campaignId } = req.params;
const userId = req.headers.userid || req.user?.id;
if (!userId) {
return res.status(401).json({ success: false, message: 'Benutzer-ID nicht gefunden' });
}
const userProgress = await match3Service.getUserProgress(userId, campaignId);
res.json({ success: true, data: userProgress });
} catch (error) {
console.error('Error in getUserProgress:', error);
res.status(500).json({ success: false, message: 'Fehler beim Laden des Fortschritts' });
}
}
/**
* Aktualisiert den Level-Fortschritt eines Benutzers
*/
async updateLevelProgress(req, res) {
try {
const { campaignId, levelId } = req.params;
const userId = req.headers.userid || req.user?.id;
const { score, moves, time, stars, isCompleted } = req.body;
if (!userId) {
return res.status(401).json({ success: false, message: 'Benutzer-ID nicht gefunden' });
}
if (!score || !moves || !stars) {
return res.status(400).json({
success: false,
message: 'Alle erforderlichen Felder müssen ausgefüllt werden'
});
}
const levelData = {
score: parseInt(score),
moves: parseInt(moves),
time: time ? parseInt(time) : 0,
stars: parseInt(stars),
isCompleted: Boolean(isCompleted)
};
const result = await match3Service.updateLevelProgress(userId, campaignId, levelId, levelData);
res.json({ success: true, data: result });
} catch (error) {
console.error('Error in updateLevelProgress:', error);
res.status(500).json({ success: false, message: 'Fehler beim Aktualisieren des Fortschritts' });
}
}
/**
* Lädt die Bestenliste für eine Kampagne
*/
async getLeaderboard(req, res) {
try {
const { campaignId } = req.params;
const { limit = 10 } = req.query;
const leaderboard = await match3Service.getLeaderboard(campaignId, parseInt(limit));
res.json({ success: true, data: leaderboard });
} catch (error) {
console.error('Error in getLeaderboard:', error);
res.status(500).json({ success: false, message: 'Fehler beim Laden der Bestenliste' });
}
}
/**
* Lädt Statistiken für einen Benutzer
*/
async getUserStats(req, res) {
try {
const userId = req.headers.userid || req.user?.id;
if (!userId) {
return res.status(401).json({ success: false, message: 'Benutzer-ID nicht gefunden' });
}
const stats = await match3Service.getUserStats(userId);
res.json({ success: true, data: stats });
} catch (error) {
console.error('Error in getUserStats:', error);
res.status(500).json({ success: false, message: 'Fehler beim Laden der Statistiken' });
}
}
} }
export default new Match3Controller(); class Match3Controller {
constructor() {
this.service = Match3Service;
// Binde alle Methoden an die Klasse
this.getCampaigns = this.getCampaigns.bind(this);
this.getCampaign = this.getCampaign.bind(this);
this.getLevel = this.getLevel.bind(this);
this.getUserProgress = this.getUserProgress.bind(this);
this.updateLevelProgress = this.updateLevelProgress.bind(this);
this.getUserStats = this.getUserStats.bind(this);
this.cleanupUserProgress = this.cleanupUserProgress.bind(this);
}
// Kampagnen-Endpunkte
async getCampaigns(req, res) {
try {
const result = await this.service.getCampaigns();
res.json({
success: true,
data: result
});
} catch (error) {
console.error('Error in getCampaigns:', error);
const status = error.status || 500;
const message = error.message || 'Internal server error';
res.status(status).json({
success: false,
message: message
});
}
}
async getCampaign(req, res) {
try {
const result = await this.service.getCampaign(req.params.id);
res.json({
success: true,
data: result
});
} catch (error) {
console.error('Error in getCampaign:', error);
const status = error.status || 500;
const message = error.message || 'Internal server error';
res.status(status).json({
success: false,
message: message
});
}
}
async getLevel(req, res) {
try {
const result = await this.service.getLevel(req.params.id);
res.json({
success: true,
data: result
});
} catch (error) {
console.error('Error in getLevel:', error);
const status = error.status || 500;
const message = error.message || 'Internal server error';
res.status(status).json({
success: false,
message: message
});
}
}
// Benutzer-spezifische Endpunkte
async getUserProgress(req, res) {
try {
const userId = extractHashedUserId(req);
if (!userId) {
return res.status(401).json({
success: false,
message: 'User ID required'
});
}
const result = await this.service.getUserProgress(userId, req.params.campaignId);
res.json({
success: true,
data: result
});
} catch (error) {
console.error('Error in getUserProgress:', error);
const status = error.status || 500;
const message = error.message || 'Internal server error';
res.status(status).json({
success: false,
message: message
});
}
}
async updateLevelProgress(req, res) {
try {
const userId = extractHashedUserId(req);
if (!userId) {
return res.status(401).json({
success: false,
message: 'User ID required'
});
}
const { score, moves, time, stars, securityHash, timestamp } = req.body;
// Prüfe ob alle erforderlichen Sicherheitsdaten vorhanden sind
if (!securityHash || !timestamp) {
return res.status(400).json({
success: false,
message: 'Security validation data missing'
});
}
const result = await this.service.updateLevelProgress(
userId,
req.params.campaignId,
req.params.levelId,
score,
moves,
time,
stars,
securityHash,
timestamp
);
res.status(201).json({
success: true,
data: result
});
} catch (error) {
console.error('Error in updateLevelProgress:', error);
const status = error.status || 500;
const message = error.message || 'Internal server error';
res.status(status).json({
success: false,
message: message
});
}
}
// Bereinige falsche Level-Abschlüsse für einen Benutzer
async cleanupUserProgress(req, res) {
try {
const userId = extractHashedUserId(req);
if (!userId) {
return res.status(401).json({
success: false,
message: 'User ID required'
});
}
const result = await this.service.cleanupUserProgress(userId, req.params.campaignId);
if (result.success) {
res.json({
success: true,
data: result
});
} else {
res.status(400).json({
success: false,
message: result.message
});
}
} catch (error) {
console.error('Error in cleanupUserProgress:', error);
const status = error.status || 500;
const message = error.message || 'Internal server error';
res.status(status).json({
success: false,
message: message
});
}
}
async getUserStats(req, res) {
try {
const userId = extractHashedUserId(req);
if (!userId) {
return res.status(401).json({
success: false,
message: 'User ID required'
});
}
const result = await this.service.getUserStats(userId);
res.json({
success: true,
data: result
});
} catch (error) {
console.error('Error in getUserStats:', error);
const status = error.status || 500;
const message = error.message || 'Internal server error';
res.status(status).json({
success: false,
message: message
});
}
}
}
export default Match3Controller;

View File

@@ -258,6 +258,15 @@ const menuStructure = {
path: "/admin/falukant/database" path: "/admin/falukant/database"
}, },
} }
},
minigames: {
visible: ["mainadmin", "match3"],
children: {
match3: {
visible: ["mainadmin", "match3"],
path: "/admin/minigames/match3"
}
}
} }
} }
} }
@@ -284,7 +293,7 @@ class NavigationController {
const hasFalukantAccount = await this.hasFalukantAccount(userId); const hasFalukantAccount = await this.hasFalukantAccount(userId);
for (const [key, value] of Object.entries(menu)) { for (const [key, value] of Object.entries(menu)) {
if (value.visible.includes("all") if (value.visible.includes("all")
|| value.visible.some(v => rights.includes(v) || (value.visible.includes("anyadmin") && rights.length > 0)) || value.visible.some(v => rights.includes(v)) || (value.visible.includes("anyadmin") && rights.length > 0)
|| (value.visible.includes("over14") && age >= 14) || (value.visible.includes("over14") && age >= 14)
|| (value.visible.includes("over12") && age >= 12) || (value.visible.includes("over12") && age >= 12)
|| (value.visible.includes("over18") && age >= 18) || (value.visible.includes("over18") && age >= 18)

View File

@@ -107,6 +107,8 @@ import Match3Level from './match3/level.js';
import Match3Objective from './match3/objective.js'; import Match3Objective from './match3/objective.js';
import Match3UserProgress from './match3/userProgress.js'; import Match3UserProgress from './match3/userProgress.js';
import Match3UserLevelProgress from './match3/userLevelProgress.js'; import Match3UserLevelProgress from './match3/userLevelProgress.js';
import Match3TileType from './match3/tileType.js';
import Match3LevelTileType from './match3/levelTileType.js';
export default function setupAssociations() { export default function setupAssociations() {
// RoomType 1:n Room // RoomType 1:n Room
@@ -841,4 +843,23 @@ export default function setupAssociations() {
foreignKey: 'levelId', foreignKey: 'levelId',
as: 'level' as: 'level'
}); });
// Match3 Tile Type associations
Match3Level.hasMany(Match3LevelTileType, {
foreignKey: 'levelId',
as: 'levelTileTypes'
});
Match3LevelTileType.belongsTo(Match3Level, {
foreignKey: 'levelId',
as: 'level'
});
Match3TileType.hasMany(Match3LevelTileType, {
foreignKey: 'tileTypeId',
as: 'levelTileTypes'
});
Match3LevelTileType.belongsTo(Match3TileType, {
foreignKey: 'tileTypeId',
as: 'tileType'
});
} }

View File

@@ -97,6 +97,8 @@ import Match3Level from './match3/level.js';
import Match3Objective from './match3/objective.js'; import Match3Objective from './match3/objective.js';
import Match3UserProgress from './match3/userProgress.js'; import Match3UserProgress from './match3/userProgress.js';
import Match3UserLevelProgress from './match3/userLevelProgress.js'; import Match3UserLevelProgress from './match3/userLevelProgress.js';
import Match3TileType from './match3/tileType.js';
import Match3LevelTileType from './match3/levelTileType.js';
// — Politische Ämter (Politics) — // — Politische Ämter (Politics) —
import PoliticalOfficeType from './falukant/type/political_office_type.js'; import PoliticalOfficeType from './falukant/type/political_office_type.js';
@@ -214,6 +216,8 @@ const models = {
Match3Objective, Match3Objective,
Match3UserProgress, Match3UserProgress,
Match3UserLevelProgress, Match3UserLevelProgress,
Match3TileType,
Match3LevelTileType,
PoliticalOfficeType, PoliticalOfficeType,
PoliticalOfficeRequirement, PoliticalOfficeRequirement,
PoliticalOfficeBenefitType, PoliticalOfficeBenefitType,

View File

@@ -13,6 +13,7 @@ const Login = sequelize.define('login', {
} }
}, { }, {
schema: 'logs', schema: 'logs',
underscored: true,
tableName: 'login' tableName: 'login'
}); });

View File

@@ -34,7 +34,8 @@ const Campaign = sequelize.define('Campaign', {
}, { }, {
tableName: 'match3_campaigns', tableName: 'match3_campaigns',
schema: 'match3', schema: 'match3',
timestamps: true timestamps: true,
underscored: true // WICHTIG: Alle Datenbankfelder im snake_case Format
}); });
export default Campaign; export default Campaign;

View File

@@ -1,7 +1,7 @@
import { sequelize } from '../../utils/sequelize.js';
import { DataTypes } from 'sequelize'; import { DataTypes } from 'sequelize';
import { sequelize } from '../../utils/sequelize.js';
const Level = sequelize.define('Level', { const Match3Level = sequelize.define('Match3Level', {
id: { id: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
primaryKey: true, primaryKey: true,
@@ -9,7 +9,11 @@ const Level = sequelize.define('Level', {
}, },
campaignId: { campaignId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false allowNull: false,
references: {
model: 'match3_campaigns',
key: 'id'
}
}, },
name: { name: {
type: DataTypes.STRING(255), type: DataTypes.STRING(255),
@@ -21,41 +25,51 @@ const Level = sequelize.define('Level', {
}, },
order: { order: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
defaultValue: 1 allowNull: false
}, },
boardSize: { boardLayout: {
type: DataTypes.TEXT,
allowNull: true, // Ändern zu true, da bereits existierende Datensätze vorhanden sind
defaultValue: 'xxxxxx\nxxxxxx\nxxxxxx\nxxxxxx\nxxxxxx\nxxxxxx', // Standard-Layout für neue Level
comment: 'Level-Form als String (o = kein Feld, x = Feld, Zeilen durch \n getrennt)'
},
boardWidth: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
defaultValue: 8 allowNull: true, // Ändern zu true, da bereits existierende Datensätze vorhanden sind
defaultValue: 6, // Standardwert für neue Level
comment: 'Breite des Level-Boards'
},
boardHeight: {
type: DataTypes.INTEGER,
allowNull: true, // Ändern zu true, da bereits existierende Datensätze vorhanden sind
defaultValue: 6, // Standardwert für neue Level
comment: 'Höhe des Level-Boards'
}, },
tileTypes: { tileTypes: {
type: DataTypes.JSON, type: DataTypes.JSON,
allowNull: false, allowNull: true, // Ändern zu true, da wir jetzt eine Verknüpfungstabelle haben
defaultValue: ['gem', 'star', 'heart', 'diamond', 'circle', 'square'] comment: 'Legacy: Array der verfügbaren Tile-Typen (wird durch levelTileTypes ersetzt)'
}, },
moveLimit: { moveLimit: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: true allowNull: false,
defaultValue: 20
}, },
timeLimit: { timeLimit: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: true allowNull: true,
comment: 'Zeitlimit in Sekunden (null = kein Limit)'
}, },
isActive: { isActive: {
type: DataTypes.BOOLEAN, type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true defaultValue: true
},
createdAt: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
},
updatedAt: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
} }
}, { }, {
tableName: 'match3_levels', tableName: 'match3_levels',
schema: 'match3', schema: 'match3',
timestamps: true timestamps: true,
underscored: true // WICHTIG: Alle Datenbankfelder im snake_case Format
}); });
export default Level; export default Match3Level;

View File

@@ -0,0 +1,53 @@
import { DataTypes } from 'sequelize';
import { sequelize } from '../../utils/sequelize.js';
const Match3LevelTileType = sequelize.define('Match3LevelTileType', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
levelId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: 'match3_levels',
key: 'id'
},
comment: 'Referenz auf den Level'
},
tileTypeId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: 'match3_tile_types',
key: 'id'
},
comment: 'Referenz auf den Tile-Typ'
},
weight: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 1,
comment: 'Gewichtung für die Wahrscheinlichkeit, dass dieser Tile-Typ erscheint'
},
isActive: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true,
comment: 'Ob dieser Tile-Typ in diesem Level aktiv ist'
}
}, {
tableName: 'match3_level_tile_types',
schema: 'match3',
timestamps: true,
underscored: true, // WICHTIG: Alle Datenbankfelder im snake_case Format
indexes: [
{
unique: true,
fields: ['level_id', 'tile_type_id'] // WICHTIG: Bei underscored: true müssen snake_case Namen verwendet werden
}
]
});
export default Match3LevelTileType;

View File

@@ -46,7 +46,8 @@ const Objective = sequelize.define('Objective', {
}, { }, {
tableName: 'match3_objectives', tableName: 'match3_objectives',
schema: 'match3', schema: 'match3',
timestamps: true timestamps: true,
underscored: true // WICHTIG: Alle Datenbankfelder im snake_case Format
}); });
export default Objective; export default Objective;

View File

@@ -0,0 +1,61 @@
import { DataTypes } from 'sequelize';
import { sequelize } from '../../utils/sequelize.js';
const Match3TileType = sequelize.define('Match3TileType', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: {
type: DataTypes.STRING(50),
allowNull: false,
comment: 'Eindeutiger Name des Tile-Typs (z.B. "gem", "star", "heart")'
},
displayName: {
type: DataTypes.STRING(100),
allowNull: false,
comment: 'Anzeigename des Tile-Typs (z.B. "Juwel", "Stern", "Herz")'
},
symbol: {
type: DataTypes.STRING(10),
allowNull: false,
comment: 'Unicode-Symbol für den Tile-Typ (z.B. "💎", "⭐", "❤️")'
},
color: {
type: DataTypes.STRING(7),
allowNull: true,
comment: 'Hex-Farbe für den Tile-Typ (z.B. "#ff6b6b")'
},
rarity: {
type: DataTypes.STRING(20),
allowNull: false,
defaultValue: 'common',
comment: 'Seltenheit des Tile-Typs (common, uncommon, rare, epic, legendary)'
},
points: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 10,
comment: 'Punkte, die dieser Tile-Typ beim Matchen gibt'
},
isActive: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true,
comment: 'Ob dieser Tile-Typ aktiv ist'
}
}, {
tableName: 'match3_tile_types',
schema: 'match3',
timestamps: true,
underscored: true, // WICHTIG: Alle Datenbankfelder im snake_case Format
indexes: [
{
unique: true,
fields: ['name']
}
]
});
export default Match3TileType;

View File

@@ -67,10 +67,11 @@ const UserLevelProgress = sequelize.define('UserLevelProgress', {
tableName: 'match3_user_level_progress', tableName: 'match3_user_level_progress',
schema: 'match3', schema: 'match3',
timestamps: true, timestamps: true,
underscored: true, // WICHTIG: Alle Datenbankfelder im snake_case Format
indexes: [ indexes: [
{ {
unique: true, unique: true,
fields: ['userProgressId', 'levelId'] fields: ['user_progress_id', 'level_id'] // WICHTIG: Bei underscored: true müssen snake_case Namen verwendet werden
} }
] ]
}); });

View File

@@ -51,10 +51,11 @@ const UserProgress = sequelize.define('UserProgress', {
tableName: 'match3_user_progress', tableName: 'match3_user_progress',
schema: 'match3', schema: 'match3',
timestamps: true, timestamps: true,
underscored: true, // WICHTIG: Alle Datenbankfelder im snake_case Format
indexes: [ indexes: [
{ {
unique: true, unique: true,
fields: ['userId', 'campaignId'] fields: ['user_id', 'campaign_id'] // WICHTIG: Bei underscored: true müssen snake_case Namen verwendet werden
} }
] ]
}); });

View File

@@ -25,4 +25,29 @@ router.post('/falukant/searchuser', authenticate, adminController.searchUser);
router.get('/falukant/getuser/:id', authenticate, adminController.getFalukantUserById); router.get('/falukant/getuser/:id', authenticate, adminController.getFalukantUserById);
router.post('/falukant/edituser', authenticate, adminController.changeFalukantUser); router.post('/falukant/edituser', authenticate, adminController.changeFalukantUser);
// --- Minigames Admin ---
router.get('/minigames/match3/campaigns', authenticate, adminController.getMatch3Campaigns);
router.get('/minigames/match3/campaigns/:id', authenticate, adminController.getMatch3Campaign);
router.post('/minigames/match3/campaigns', authenticate, adminController.createMatch3Campaign);
router.put('/minigames/match3/campaigns/:id', authenticate, adminController.updateMatch3Campaign);
router.delete('/minigames/match3/campaigns/:id', authenticate, adminController.deleteMatch3Campaign);
router.get('/minigames/match3/levels', authenticate, adminController.getMatch3Levels);
router.get('/minigames/match3/levels/:id', authenticate, adminController.getMatch3Level);
router.post('/minigames/match3/levels', authenticate, adminController.createMatch3Level);
router.put('/minigames/match3/levels/:id', authenticate, adminController.updateMatch3Level);
router.delete('/minigames/match3/levels/:id', authenticate, adminController.deleteMatch3Level);
// Objectives
router.get('/minigames/match3/objectives', authenticate, adminController.getMatch3Objectives);
router.get('/minigames/match3/objectives/:id', authenticate, adminController.getMatch3Objective);
router.post('/minigames/match3/objectives', authenticate, adminController.createMatch3Objective);
router.put('/minigames/match3/objectives/:id', authenticate, adminController.updateMatch3Objective);
router.delete('/minigames/match3/objectives/:id', authenticate, adminController.deleteMatch3Objective);
router.get('/minigames/match3/tile-types', authenticate, adminController.getMatch3TileTypes);
router.post('/minigames/match3/tile-types', authenticate, adminController.createMatch3TileType);
router.put('/minigames/match3/tile-types/:id', authenticate, adminController.updateMatch3TileType);
router.delete('/minigames/match3/tile-types/:id', authenticate, adminController.deleteMatch3TileType);
export default router; export default router;

View File

@@ -1,22 +1,26 @@
import express from 'express'; import express from 'express';
import match3Controller from '../controllers/match3Controller.js';
import { authenticate } from '../middleware/authMiddleware.js'; import { authenticate } from '../middleware/authMiddleware.js';
import Match3Controller from '../controllers/match3Controller.js';
const router = express.Router(); const router = express.Router();
const match3Controller = new Match3Controller();
// Alle Routen erfordern Authentifizierung // Alle Routen benötigen Authentifizierung
router.use(authenticate); router.use(authenticate);
// Kampagnen-Routen // Kampagnen-Routen
router.get('/campaigns', match3Controller.getCampaigns); router.get('/campaigns', match3Controller.getCampaigns);
router.get('/campaigns/:campaignId', match3Controller.getCampaign); router.get('/campaigns/:id', match3Controller.getCampaign);
// Benutzer-Fortschritt // Level-Routen
router.get('/levels/:id', match3Controller.getLevel);
// Fortschritt-Routen
router.get('/campaigns/:campaignId/progress', match3Controller.getUserProgress); router.get('/campaigns/:campaignId/progress', match3Controller.getUserProgress);
router.post('/campaigns/:campaignId/levels/:levelId/progress', match3Controller.updateLevelProgress); router.post('/campaigns/:campaignId/levels/:levelId/progress', match3Controller.updateLevelProgress);
router.post('/campaigns/:campaignId/cleanup', match3Controller.cleanupUserProgress);
// Bestenliste und Statistiken // Statistiken-Routen
router.get('/campaigns/:campaignId/leaderboard', match3Controller.getLeaderboard);
router.get('/stats', match3Controller.getUserStats); router.get('/stats', match3Controller.getUserStats);
export default router; export default router;

View File

@@ -292,22 +292,34 @@ class AdminService {
} }
// --- Chat Room Admin --- // --- Chat Room Admin ---
async getRoomTypes() { async getRoomTypes(userId) {
if (!(await this.hasUserAccess(userId, 'chatrooms'))) {
throw new Error('noaccess');
}
return await RoomType.findAll(); return await RoomType.findAll();
} }
async getGenderRestrictions() { async getGenderRestrictions(userId) {
if (!(await this.hasUserAccess(userId, 'chatrooms'))) {
throw new Error('noaccess');
}
// Find the UserParamType for gender restriction (e.g. description = 'gender') // Find the UserParamType for gender restriction (e.g. description = 'gender')
const genderType = await UserParamType.findOne({ where: { description: 'gender' } }); const genderType = await UserParamType.findOne({ where: { description: 'gender' } });
if (!genderType) return []; if (!genderType) return [];
return await UserParamValue.findAll({ where: { userParamTypeId: genderType.id } }); return await UserParamValue.findAll({ where: { userParamTypeId: genderType.id } });
} }
async getUserRights() { async getUserRights(userId) {
if (!(await this.hasUserAccess(userId, 'chatrooms'))) {
throw new Error('noaccess');
}
return await ChatRight.findAll(); return await ChatRight.findAll();
} }
async getRooms() { async getRooms(userId) {
if (!(await this.hasUserAccess(userId, 'chatrooms'))) {
throw new Error('noaccess');
}
// Only return necessary fields to the frontend // Only return necessary fields to the frontend
return await Room.findAll({ return await Room.findAll({
attributes: [ attributes: [
@@ -329,20 +341,301 @@ class AdminService {
}); });
} }
async updateRoom(id, data) { async updateRoom(userId, id, data) {
if (!(await this.hasUserAccess(userId, 'chatrooms'))) {
throw new Error('noaccess');
}
const room = await Room.findByPk(id); const room = await Room.findByPk(id);
if (!room) throw new Error('Room not found'); if (!room) throw new Error('Room not found');
await room.update(data); await room.update(data);
return room; return room;
} }
async createRoom(data) { async createRoom(userId, data) {
if (!(await this.hasUserAccess(userId, 'chatrooms'))) {
throw new Error('noaccess');
}
return await Room.create(data); return await Room.create(data);
} }
async deleteRoom(id) { async deleteRoom(userId, id) {
if (!(await this.hasUserAccess(userId, 'chatrooms'))) {
throw new Error('noaccess');
}
return await Room.destroy({ where: { id } }); return await Room.destroy({ where: { id } });
} }
// --- Match3 Admin Methods ---
async getMatch3Campaigns(userId) {
if (!(await this.hasUserAccess(userId, 'match3'))) {
throw new Error('noaccess');
}
const Match3Campaign = (await import('../models/match3/campaign.js')).default;
return await Match3Campaign.findAll({
include: [{
model: (await import('../models/match3/level.js')).default,
as: 'levels',
include: [{
model: (await import('../models/match3/objective.js')).default,
as: 'objectives',
required: false
}],
required: false
}]
});
}
async getMatch3Campaign(userId, id) {
if (!(await this.hasUserAccess(userId, 'match3'))) {
throw new Error('noaccess');
}
const Match3Campaign = (await import('../models/match3/campaign.js')).default;
return await Match3Campaign.findByPk(id, {
include: [{
model: (await import('../models/match3/level.js')).default,
as: 'levels',
include: [{
model: (await import('../models/match3/objective.js')).default,
as: 'objectives',
required: false
}],
required: false
}]
});
}
async createMatch3Campaign(userId, data) {
if (!(await this.hasUserAccess(userId, 'match3'))) {
throw new Error('noaccess');
}
const Match3Campaign = (await import('../models/match3/campaign.js')).default;
return await Match3Campaign.create(data);
}
async updateMatch3Campaign(userId, id, data) {
if (!(await this.hasUserAccess(userId, 'match3'))) {
throw new Error('noaccess');
}
const Match3Campaign = (await import('../models/match3/campaign.js')).default;
const campaign = await Match3Campaign.findByPk(id);
if (!campaign) throw new Error('Campaign not found');
await campaign.update(data);
return campaign;
}
async deleteMatch3Campaign(userId, id) {
if (!(await this.hasUserAccess(userId, 'match3'))) {
throw new Error('noaccess');
}
const Match3Campaign = (await import('../models/match3/campaign.js')).default;
return await Match3Campaign.destroy({ where: { id } });
}
async getMatch3Levels(userId) {
if (!(await this.hasUserAccess(userId, 'match3'))) {
throw new Error('noaccess');
}
const Match3Level = (await import('../models/match3/level.js')).default;
return await Match3Level.findAll({
include: [
{
model: (await import('../models/match3/campaign.js')).default,
as: 'campaign',
required: false
},
{
model: (await import('../models/match3/objective.js')).default,
as: 'objectives',
required: false
}
],
order: [['order', 'ASC']]
});
}
async getMatch3Level(userId, id) {
if (!(await this.hasUserAccess(userId, 'match3'))) {
throw new Error('noaccess');
}
const Match3Level = (await import('../models/match3/level.js')).default;
return await Match3Level.findByPk(id, {
include: [
{
model: (await import('../models/match3/campaign.js')).default,
as: 'campaign',
required: false
},
{
model: (await import('../models/match3/objective.js')).default,
as: 'objectives',
required: false
}
]
});
}
async createMatch3Level(userId, data) {
if (!(await this.hasUserAccess(userId, 'match3'))) {
throw new Error('noaccess');
}
const Match3Level = (await import('../models/match3/level.js')).default;
// Wenn keine campaignId gesetzt ist, setze eine Standard-Campaign-ID
if (!data.campaignId) {
// Versuche eine Standard-Campaign zu finden oder erstelle eine
const Match3Campaign = (await import('../models/match3/campaign.js')).default;
let defaultCampaign = await Match3Campaign.findOne({ where: { isActive: true } });
if (!defaultCampaign) {
// Erstelle eine Standard-Campaign falls keine existiert
defaultCampaign = await Match3Campaign.create({
name: 'Standard Campaign',
description: 'Standard Campaign für Match3 Levels',
isActive: true,
order: 1
});
}
data.campaignId = defaultCampaign.id;
}
// Validiere, dass campaignId gesetzt ist
if (!data.campaignId) {
throw new Error('CampaignId ist erforderlich');
}
return await Match3Level.create(data);
}
async updateMatch3Level(userId, id, data) {
if (!(await this.hasUserAccess(userId, 'match3'))) {
throw new Error('noaccess');
}
const Match3Level = (await import('../models/match3/level.js')).default;
const level = await Match3Level.findByPk(id);
if (!level) throw new Error('Level not found');
await level.update(data);
return level;
}
async deleteMatch3Level(userId, id) {
if (!(await this.hasUserAccess(userId, 'match3'))) {
throw new Error('noaccess');
}
const Match3Level = (await import('../models/match3/level.js')).default;
return await Match3Level.destroy({ where: { id } });
}
// Match3 Objectives
async getMatch3Objectives(userId) {
if (!(await this.hasUserAccess(userId, 'match3'))) {
throw new Error('noaccess');
}
const Match3Objective = (await import('../models/match3/objective.js')).default;
return await Match3Objective.findAll({
include: [{
model: (await import('../models/match3/level.js')).default,
as: 'level',
required: false
}],
order: [['order', 'ASC']]
});
}
async getMatch3Objective(userId, id) {
if (!(await this.hasUserAccess(userId, 'match3'))) {
throw new Error('noaccess');
}
const Match3Objective = (await import('../models/match3/objective.js')).default;
return await Match3Objective.findByPk(id, {
include: [{
model: (await import('../models/match3/level.js')).default,
as: 'level',
required: false
}]
});
}
async createMatch3Objective(userId, data) {
if (!(await this.hasUserAccess(userId, 'match3'))) {
throw new Error('noaccess');
}
const Match3Objective = (await import('../models/match3/objective.js')).default;
// Validiere, dass levelId gesetzt ist
if (!data.levelId) {
throw new Error('LevelId ist erforderlich');
}
// Validiere, dass target eine ganze Zahl ist
if (data.target && !Number.isInteger(Number(data.target))) {
throw new Error('Target muss eine ganze Zahl sein');
}
return await Match3Objective.create(data);
}
async updateMatch3Objective(userId, id, data) {
if (!(await this.hasUserAccess(userId, 'match3'))) {
throw new Error('noaccess');
}
const Match3Objective = (await import('../models/match3/objective.js')).default;
const objective = await Match3Objective.findByPk(id);
if (!objective) throw new Error('Objective not found');
// Validiere, dass target eine ganze Zahl ist
if (data.target && !Number.isInteger(Number(data.target))) {
throw new Error('Target muss eine ganze Zahl sein');
}
await objective.update(data);
return objective;
}
async deleteMatch3Objective(userId, id) {
if (!(await this.hasUserAccess(userId, 'match3'))) {
throw new Error('noaccess');
}
const Match3Objective = (await import('../models/match3/objective.js')).default;
return await Match3Objective.destroy({ where: { id } });
}
async getMatch3TileTypes(userId) {
if (!(await this.hasUserAccess(userId, 'match3'))) {
throw new Error('noaccess');
}
const Match3TileType = (await import('../models/match3/tileType.js')).default;
return await Match3TileType.findAll({
order: [['name', 'ASC']]
});
}
async createMatch3TileType(userId, data) {
if (!(await this.hasUserAccess(userId, 'match3'))) {
throw new Error('noaccess');
}
const Match3TileType = (await import('../models/match3/tileType.js')).default;
return await Match3TileType.create(data);
}
async updateMatch3TileType(userId, id, data) {
if (!(await this.hasUserAccess(userId, 'match3'))) {
throw new Error('noaccess');
}
const Match3TileType = (await import('../models/match3/tileType.js')).default;
const tileType = await Match3TileType.findByPk(id);
if (!tileType) throw new Error('Tile type not found');
await tileType.update(data);
return tileType;
}
async deleteMatch3TileType(userId, id) {
if (!(await this.hasUserAccess(userId, 'match3'))) {
throw new Error('noaccess');
}
const Match3TileType = (await import('../models/match3/tileType.js')).default;
return await Match3TileType.destroy({ where: { id } });
}
} }
export default new AdminService(); export default new AdminService();

View File

@@ -3,311 +3,501 @@ import Match3Level from '../models/match3/level.js';
import Match3Objective from '../models/match3/objective.js'; import Match3Objective from '../models/match3/objective.js';
import Match3UserProgress from '../models/match3/userProgress.js'; import Match3UserProgress from '../models/match3/userProgress.js';
import Match3UserLevelProgress from '../models/match3/userLevelProgress.js'; import Match3UserLevelProgress from '../models/match3/userLevelProgress.js';
import Match3TileType from '../models/match3/tileType.js';
import Match3LevelTileType from '../models/match3/levelTileType.js';
class Match3Service { class Match3Service {
/** // Lade alle aktiven Kampagnen
* Lädt alle aktiven Kampagnen async getCampaigns() {
*/ const campaigns = await Match3Campaign.findAll({
async getActiveCampaigns() {
try {
const campaigns = await Match3Campaign.findAll({
where: { isActive: true },
include: [
{
model: Match3Level,
as: 'levels',
where: { isActive: true }, where: { isActive: true },
required: false,
include: [ include: [
{ {
model: Match3Objective,
as: 'objectives',
required: false,
order: [['order', 'ASC']]
}
],
order: [['order', 'ASC']]
}
],
order: [['order', 'ASC']]
});
return campaigns;
} catch (error) {
console.error('Error loading active campaigns:', error);
throw error;
}
}
/**
* Lädt eine spezifische Kampagne mit allen Leveln
*/
async getCampaign(campaignId) {
try {
const campaign = await Match3Campaign.findByPk(campaignId, {
include: [
{
model: Match3Level,
as: 'levels',
where: { isActive: true },
required: false,
include: [
{
model: Match3Objective,
as: 'objectives',
required: false,
order: [['order', 'ASC']]
}
],
order: [['order', 'ASC']]
}
]
});
return campaign;
} catch (error) {
console.error('Error loading campaign:', error);
throw error;
}
}
/**
* Lädt den Benutzerfortschritt für eine Kampagne
*/
async getUserProgress(userId, campaignId) {
try {
let userProgress = await Match3UserProgress.findOne({
where: { userId, campaignId },
include: [
{
model: Match3UserLevelProgress,
as: 'levelProgress',
include: [
{
model: Match3Level,
as: 'level'
}
]
}
]
});
if (!userProgress) {
// Erstelle neuen Fortschritt wenn noch nicht vorhanden
userProgress = await Match3UserProgress.create({
userId,
campaignId,
totalScore: 0,
totalStars: 0,
levelsCompleted: 0,
currentLevel: 1,
isCompleted: false
});
} else {
// Validiere und korrigiere bestehende currentLevel-Werte
if (userProgress.currentLevel < 1 || userProgress.currentLevel > 1000) {
console.warn(`Invalid currentLevel detected for user ${userId}: ${userProgress.currentLevel}, correcting to ${userProgress.levelsCompleted + 1}`);
// Korrigiere den ungültigen Wert
await userProgress.update({
currentLevel: userProgress.levelsCompleted + 1
});
// Lade den aktualisierten Datensatz
userProgress = await Match3UserProgress.findByPk(userProgress.id, {
include: [
{
model: Match3UserLevelProgress,
as: 'levelProgress',
include: [
{
model: Match3Level, model: Match3Level,
as: 'level' as: 'levels',
} where: { isActive: true },
] required: false,
} include: [
{
model: Match3Objective,
as: 'objectives',
where: { isRequired: true },
required: false
}
]
}
],
order: [
['id', 'ASC'],
[{ model: Match3Level, as: 'levels' }, 'order', 'ASC'],
[{ model: Match3Level, as: 'levels' }, { model: Match3Objective, as: 'objectives' }, 'order', 'ASC']
] ]
}); });
return campaigns;
}
// Lade eine spezifische Kampagne
async getCampaign(campaignId) {
try {
// Lade zuerst die Kampagne ohne levelTileTypes
const campaign = await Match3Campaign.findByPk(campaignId, {
include: [
{
model: Match3Level,
as: 'levels',
where: { isActive: true },
required: false,
include: [
{
model: Match3Objective,
as: 'objectives',
where: { isRequired: true },
required: false
}
]
}
],
order: [
[{ model: Match3Level, as: 'levels' }, 'order', 'ASC'],
[{ model: Match3Level, as: 'levels' }, { model: Match3Objective, as: 'objectives' }, 'order', 'ASC']
]
});
if (!campaign) {
throw { status: 404, message: 'Campaign not found' };
}
// Versuche levelTileTypes zu laden, aber mache es optional
try {
const levelsWithTileTypes = await Promise.all(
campaign.levels.map(async (level) => {
try {
const levelTileTypes = await Match3LevelTileType.findAll({
where: {
levelId: level.id,
isActive: true
},
include: [
{
model: Match3TileType,
as: 'tileType',
where: { isActive: true },
required: true
}
],
order: [['weight', 'DESC']]
});
return {
...level.toJSON(),
levelTileTypes: levelTileTypes
};
} catch (error) {
console.log(`Warnung: Konnte levelTileTypes für Level ${level.id} nicht laden:`, error.message);
// Fallback: Verwende die alten tileTypes
return {
...level.toJSON(),
levelTileTypes: []
};
}
})
);
campaign.levels = levelsWithTileTypes;
} catch (error) {
console.log('Warnung: Konnte levelTileTypes nicht laden, verwende Fallback:', error.message);
// Fallback: Verwende die alten tileTypes
campaign.levels = campaign.levels.map(level => ({
...level.toJSON(),
levelTileTypes: []
}));
}
return campaign;
} catch (error) {
console.error('Fehler beim Laden der Kampagne:', error);
throw error;
} }
}
// Lade ein spezifisches Level
async getLevel(levelId) {
const level = await Match3Level.findByPk(levelId, {
include: [
{
model: Match3Objective,
as: 'objectives',
where: { isRequired: true },
required: false,
order: [['order', 'ASC']]
}
]
});
if (!level) {
throw { status: 404, message: 'Level not found' };
}
return level;
}
// Lade Benutzer-Fortschritt für eine Kampagne
async getUserProgress(userId, campaignId) {
let userProgress = await Match3UserProgress.findOne({
where: { userId, campaignId },
include: [
{
model: Match3UserLevelProgress,
as: 'levelProgress',
include: [
{
model: Match3Level,
as: 'level'
}
]
}
]
});
if (!userProgress) {
// Erstelle neuen Fortschritt wenn keiner existiert
userProgress = await Match3UserProgress.create({
userId,
campaignId,
totalScore: 0,
totalStars: 0,
levelsCompleted: 0,
currentLevel: 1,
isCompleted: false
});
}
// Validiere und korrigiere currentLevel falls nötig
if (userProgress.currentLevel < 1 || userProgress.currentLevel > 1000) {
const correctLevel = userProgress.levelsCompleted + 1;
await userProgress.update({ currentLevel: correctLevel });
}
return userProgress;
}
// Aktualisiere Level-Fortschritt
async updateLevelProgress(userId, campaignId, levelId, score, moves, time, stars, securityHash, timestamp) {
// ANTI-CHEAT-VALIDIERUNG
if (!this.validateProgressHash(userId, campaignId, levelId, score, moves, time, stars, securityHash, timestamp)) {
throw { status: 403, message: 'Progress validation failed - possible cheating detected' };
} }
return userProgress; // Lade oder erstelle Benutzer-Fortschritt
} catch (error) {
console.error('Error loading user progress:', error);
throw error;
}
}
/**
* Aktualisiert den Level-Fortschritt eines Benutzers
*/
async updateLevelProgress(userId, campaignId, levelId, levelData) {
try {
// Lade oder erstelle Benutzerfortschritt
let userProgress = await Match3UserProgress.findOne({ let userProgress = await Match3UserProgress.findOne({
where: { userId, campaignId } where: { userId, campaignId }
}); });
if (!userProgress) { if (!userProgress) {
userProgress = await Match3UserProgress.create({ userProgress = await Match3UserProgress.create({
userId, userId,
campaignId, campaignId,
totalScore: 0, totalScore: 0,
totalStars: 0, totalStars: 0,
levelsCompleted: 0, levelsCompleted: 0,
currentLevel: 1, currentLevel: 1,
isCompleted: false isCompleted: false
}); });
} }
// Lade oder erstelle Level-Fortschritt // Lade oder erstelle Level-Fortschritt
let levelProgress = await Match3UserLevelProgress.findOne({ let levelProgress = await Match3UserLevelProgress.findOne({
where: { userProgressId: userProgress.id, levelId } where: { userProgressId: userProgress.id, levelId }
});
if (!levelProgress) {
levelProgress = await Match3UserLevelProgress.create({
userProgressId: userProgress.id,
levelId,
score: 0,
moves: 0,
time: 0,
stars: 0,
isCompleted: false,
attempts: 0
});
}
// Aktualisiere Level-Fortschritt
const updateData = {
score: Math.max(levelProgress.bestScore, levelData.score),
moves: levelData.moves,
time: levelData.time || 0,
stars: Math.max(levelProgress.stars, levelData.stars),
isCompleted: levelData.isCompleted || false,
attempts: levelProgress.attempts + 1
};
if (levelData.isCompleted) {
updateData.completedAt = new Date();
}
await levelProgress.update(updateData);
// Aktualisiere Bestwerte
if (levelData.score > levelProgress.bestScore) {
await levelProgress.update({ bestScore: levelData.score });
}
if (levelData.moves < levelProgress.bestMoves || levelProgress.bestMoves === 0) {
await levelProgress.update({ bestMoves: levelData.moves });
}
if (levelData.time < levelProgress.bestTime || levelProgress.bestTime === 0) {
await levelProgress.update({ bestTime: levelData.time });
}
// Aktualisiere Kampagnen-Fortschritt
if (levelData.isCompleted) {
const totalScore = await Match3UserLevelProgress.sum('score', {
where: { userProgressId: userProgress.id, isCompleted: true }
}); });
const totalStars = await Match3UserLevelProgress.sum('stars', { if (!levelProgress) {
where: { userProgressId: userProgress.id, isCompleted: true } levelProgress = await Match3UserLevelProgress.create({
userProgressId: userProgress.id,
levelId,
score: 0,
moves: 0,
time: 0,
stars: 0,
isCompleted: false,
attempts: 0,
bestScore: 0,
bestMoves: 0,
bestTime: 0
});
}
// Aktualisiere Level-Fortschritt
// WICHTIG: Ein Level ist nur abgeschlossen, wenn es mindestens 1 Stern gibt UND der Score > 0 ist
// Das verhindert, dass unvollständige Level als abgeschlossen markiert werden
const isCompleted = stars > 0 && score > 0;
const attempts = levelProgress.attempts + 1;
await levelProgress.update({
score: Math.max(score, levelProgress.bestScore),
moves: moves,
time: time,
stars: Math.max(stars, levelProgress.stars),
// WICHTIG: isCompleted wird NUR auf true gesetzt, wenn das Level tatsächlich abgeschlossen ist
// Der alte Status wird NICHT beibehalten, da das zu falschen Abschlüssen führen kann
isCompleted: isCompleted,
attempts: attempts,
bestScore: Math.max(score, levelProgress.bestScore),
bestMoves: Math.min(moves, levelProgress.bestMoves || moves),
bestTime: Math.min(time, levelProgress.bestTime || time),
completedAt: isCompleted ? new Date() : levelProgress.completedAt
}); });
// Aktualisiere Gesamt-Fortschritt
// WICHTIG: Nur wenn das Level abgeschlossen ist, werden Score und Stars zum Gesamtfortschritt hinzugefügt
// Das verhindert, dass unvollständige Level den Gesamtfortschritt beeinflussen
let totalScore = userProgress.totalScore;
let totalStars = userProgress.totalStars;
if (isCompleted) {
totalScore += score;
totalStars += stars;
}
// Berechne neue currentLevel
// WICHTIG: Zähle nur Level, die tatsächlich abgeschlossen sind
const levelsCompleted = await Match3UserLevelProgress.count({ const levelsCompleted = await Match3UserLevelProgress.count({
where: { userProgressId: userProgress.id, isCompleted: true } where: {
userProgressId: userProgress.id,
isCompleted: true
}
}); });
// Korrigiere currentLevel: Es sollte immer levelsCompleted + 1 sein // Zusätzliche Sicherheit: Stelle sicher, dass levelsCompleted nicht größer als die verfügbaren Level ist
const correctCurrentLevel = levelsCompleted + 1; if (levelsCompleted > 10) { // Angenommen, es gibt maximal 10 Level
console.warn(`User ${userId} hat ungewöhnlich viele abgeschlossene Level: ${levelsCompleted}`);
}
// WICHTIG: currentLevel sollte nur aktualisiert werden, wenn das Level tatsächlich abgeschlossen ist
// Wenn das Level nicht abgeschlossen ist, behalte den aktuellen currentLevel bei
let newCurrentLevel = userProgress.currentLevel;
if (isCompleted) {
// Nur wenn das Level abgeschlossen ist, setze currentLevel auf das nächste Level
newCurrentLevel = levelsCompleted + 1;
}
await userProgress.update({
totalScore,
totalStars,
levelsCompleted,
currentLevel: newCurrentLevel,
lastPlayed: new Date()
});
return {
levelProgress,
userProgress: {
totalScore,
totalStars,
levelsCompleted,
currentLevel: newCurrentLevel
}
};
}
// Bereinige falsche Level-Abschlüsse für einen Benutzer
async cleanupUserProgress(userId, campaignId) {
try {
// Lade Benutzer-Fortschritt
const userProgress = await Match3UserProgress.findOne({
where: { userId, campaignId }
});
if (!userProgress) {
return { success: false, message: 'User progress not found' };
}
// Lade alle Level-Fortschritte
const levelProgresses = await Match3UserLevelProgress.findAll({
where: { userProgressId: userProgress.id }
});
let cleanedCount = 0;
let totalScore = 0;
let totalStars = 0;
let levelsCompleted = 0;
// Bereinige jeden Level-Fortschritt
for (const levelProgress of levelProgresses) {
// Ein Level ist nur abgeschlossen, wenn es mindestens 1 Stern UND Score > 0 hat
const shouldBeCompleted = levelProgress.stars > 0 && levelProgress.score > 0;
if (levelProgress.isCompleted !== shouldBeCompleted) {
await levelProgress.update({
isCompleted: shouldBeCompleted,
completedAt: shouldBeCompleted ? levelProgress.completedAt : null
});
cleanedCount++;
}
// Sammle korrekte Statistiken
if (shouldBeCompleted) {
totalScore += levelProgress.score;
totalStars += levelProgress.stars;
levelsCompleted++;
}
}
// Aktualisiere Gesamt-Fortschritt
await userProgress.update({ await userProgress.update({
totalScore, totalScore,
totalStars, totalStars,
levelsCompleted, levelsCompleted,
currentLevel: correctCurrentLevel, // Verwende den korrigierten Wert currentLevel: levelsCompleted + 1
lastPlayed: new Date()
}); });
// Prüfe ob Kampagne abgeschlossen ist return {
const totalLevels = await Match3Level.count({ success: true,
where: { campaignId, isActive: true } message: `Cleaned ${cleanedCount} level progress entries`,
}); cleanedCount,
totalScore,
if (levelsCompleted >= totalLevels) { totalStars,
await userProgress.update({ isCompleted: true }); levelsCompleted,
} currentLevel: levelsCompleted + 1
};
} catch (error) {
console.error('Error cleaning user progress:', error);
return { success: false, message: error.message };
} }
return { userProgress, levelProgress };
} catch (error) {
console.error('Error updating level progress:', error);
throw error;
} }
}
/** // ANTI-CHEAT: Validiere den Progress-Hash
* Lädt die Bestenliste für eine Kampagne validateProgressHash(userId, campaignId, levelId, score, moves, time, stars, securityHash, timestamp) {
*/ try {
async getLeaderboard(campaignId, limit = 10) { // Prüfe ob der Timestamp nicht zu alt ist (5 Minuten)
try { const now = Date.now();
const leaderboard = await Match3UserProgress.findAll({ if (now - timestamp > 5 * 60 * 1000) {
where: { campaignId }, console.warn(`Progress validation failed: Timestamp too old for user ${userId}`);
include: [ return false;
{ }
model: Match3UserLevelProgress,
as: 'levelProgress', // Lade Level-Daten für Hash-Validierung
where: { isCompleted: true }, return Match3Level.findByPk(levelId).then(level => {
required: false if (!level) {
console.warn(`Progress validation failed: Level ${levelId} not found`);
return false;
} }
],
order: [
['totalScore', 'DESC'],
['totalStars', 'DESC'],
['levelsCompleted', 'DESC']
],
limit
});
return leaderboard; // Erstelle den gleichen Hash wie im Frontend
} catch (error) { const dataString = `${levelId}|${score}|${moves}|${stars}|true|${level.boardLayout}|${level.moveLimit}`;
console.error('Error loading leaderboard:', error);
throw error; // Einfache Hash-Funktion (muss mit Frontend übereinstimmen)
let hash = 0;
for (let i = 0; i < dataString.length; i++) {
const char = dataString.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
// Salt hinzufügen (muss mit Frontend übereinstimmen)
const salt = 'YourPart3_Match3_Security_2024';
const saltedString = `${dataString}|${salt}`;
let saltedHash = 0;
for (let i = 0; i < saltedString.length; i++) {
const char = saltedString.charCodeAt(i);
saltedHash = ((saltedHash << 5) - hash) + char;
saltedHash = saltedHash & saltedHash;
}
const expectedHash = Math.abs(saltedHash).toString(16);
// Vergleiche Hash
if (expectedHash !== securityHash) {
console.warn(`Progress validation failed: Hash mismatch for user ${userId}. Expected: ${expectedHash}, Got: ${securityHash}`);
return false;
}
// Zusätzliche Validierungen
if (score < 0 || moves < 0 || stars < 0 || stars > 3) {
console.warn(`Progress validation failed: Invalid values for user ${userId}`);
return false;
}
// Prüfe ob der Score realistisch ist (basierend auf Moves und Level)
const maxPossibleScore = moves * 100 * levelId; // Vereinfachte Berechnung
if (score > maxPossibleScore * 2) { // Erlaube 2x den maximalen Score
console.warn(`Progress validation failed: Unrealistic score for user ${userId}`);
return false;
}
return true;
});
} catch (error) {
console.error('Progress validation error:', error);
return false;
}
} }
}
/** // Lade Benutzer-Statistiken
* Lädt Statistiken für einen Benutzer async getUserStats(userId) {
*/ const userProgress = await Match3UserProgress.findAll({
async getUserStats(userId) { where: { userId },
try {
const stats = await Match3UserProgress.findAll({
where: { userId },
include: [
{
model: Match3Campaign,
as: 'campaign'
},
{
model: Match3UserLevelProgress,
as: 'levelProgress',
include: [ include: [
{ {
model: Match3Level, model: Match3UserLevelProgress,
as: 'level' as: 'levelProgress',
} include: [
{
model: Match3Level,
as: 'level'
}
]
}
] ]
} });
]
});
return stats; if (!userProgress || userProgress.length === 0) {
} catch (error) { return {
console.error('Error loading user stats:', error); totalScore: 0,
throw error; totalStars: 0,
levelsCompleted: 0,
totalPlayTime: 0,
averageScore: 0,
bestLevel: null
};
}
const totalScore = userProgress.reduce((sum, campaign) => sum + campaign.totalScore, 0);
const totalStars = userProgress.reduce((sum, campaign) => sum + campaign.totalStars, 0);
const levelsCompleted = userProgress.reduce((sum, campaign) => sum + campaign.levelsCompleted, 0);
let totalPlayTime = 0;
let totalLevels = 0;
let bestLevel = null;
let bestScore = 0;
userProgress.forEach(campaign => {
campaign.levelProgress.forEach(level => {
if (level.time) {
totalPlayTime += level.time;
totalLevels++;
}
if (level.score > bestScore) {
bestScore = level.score;
bestLevel = level.level;
}
});
});
return {
totalScore,
totalStars,
levelsCompleted,
totalPlayTime,
averageScore: totalLevels > 0 ? Math.round(totalScore / totalLevels) : 0,
bestLevel: bestLevel ? {
name: bestLevel.name,
score: bestScore
} : null
};
} }
} };
}
export default new Match3Service(); export default new Match3Service();

View File

@@ -0,0 +1,61 @@
import { sequelize } from './sequelize.js';
async function checkRightsTable() {
try {
console.log('🔍 Überprüfe den aktuellen Zustand der chat.rights Tabelle...');
// Überprüfe die Constraints der chat.rights Tabelle
const rightsConstraints = await sequelize.query(`
SELECT
tc.constraint_name,
tc.constraint_type,
kcu.column_name
FROM information_schema.table_constraints tc
LEFT JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
WHERE tc.table_schema = 'chat'
AND tc.table_name = 'rights'
ORDER BY tc.constraint_type, kcu.column_name;
`, { type: sequelize.QueryTypes.SELECT });
console.log(`📊 Chat Rights Constraints: ${rightsConstraints.length} gefunden`);
rightsConstraints.forEach(constraint => {
console.log(` - ${constraint.constraint_type} (${constraint.constraint_name}) auf Spalte: ${constraint.column_name}`);
});
// Überprüfe speziell die UNIQUE Constraints auf der tr-Spalte
const trUniqueConstraints = rightsConstraints.filter(c =>
c.constraint_type === 'UNIQUE' && c.column_name === 'tr'
);
console.log(`\n🎯 UNIQUE Constraints auf der tr-Spalte: ${trUniqueConstraints.length}`);
if (trUniqueConstraints.length === 1) {
console.log('✅ Perfekt! Es gibt nur noch einen UNIQUE Constraint auf der tr-Spalte.');
} else if (trUniqueConstraints.length === 0) {
console.log('⚠️ Es gibt keinen UNIQUE Constraint auf der tr-Spalte!');
} else {
console.log(`❌ Es gibt immer noch ${trUniqueConstraints.length} UNIQUE Constraints auf der tr-Spalte.`);
}
console.log('\n✅ Überprüfung abgeschlossen');
} catch (error) {
console.error('❌ Fehler bei der Überprüfung:', error);
throw error;
}
}
// Führe das Skript aus, wenn es direkt aufgerufen wird
if (import.meta.url === `file://${process.argv[1]}`) {
checkRightsTable()
.then(() => {
console.log('🎯 Überprüfung der chat.rights Tabelle abgeschlossen');
process.exit(0);
})
.catch((error) => {
console.error('💥 Überprüfung fehlgeschlagen:', error);
process.exit(1);
});
}
export default checkRightsTable;

View File

@@ -0,0 +1,234 @@
import { sequelize } from './sequelize.js';
/**
* Bereinigt doppelte Constraints und Indexe in der Datenbank
* Dies sollte nur einmal ausgeführt werden, um bestehende Probleme zu beheben
*/
async function cleanupDatabaseConstraints() {
try {
console.log('🧹 Starte Bereinigung der Datenbank-Constraints...');
// 1. Doppelte UNIQUE Constraints entfernen
console.log('🔍 Suche nach doppelten UNIQUE Constraints...');
const duplicateUniqueConstraints = await sequelize.query(`
SELECT
tc.table_schema,
tc.table_name,
kcu.column_name,
COUNT(*) as constraint_count,
array_agg(tc.constraint_name) as constraint_names
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
WHERE tc.constraint_type = 'UNIQUE'
AND tc.table_schema IN ('match3', 'community', 'falukant_data', 'falukant_type', 'falukant_predefine', 'falukant_log', 'chat', 'forum', 'logs', 'type', 'service')
GROUP BY tc.table_schema, tc.table_name, kcu.column_name
HAVING COUNT(*) > 1
ORDER BY tc.table_name, kcu.column_name;
`, { type: sequelize.QueryTypes.SELECT });
console.log(`📊 Gefunden: ${duplicateUniqueConstraints.length} Spalten mit doppelten UNIQUE Constraints`);
// Entferne doppelte UNIQUE Constraints
for (const duplicate of duplicateUniqueConstraints) {
console.log(`🗑️ Entferne doppelte UNIQUE Constraints für ${duplicate.table_schema}.${duplicate.table_name}.${duplicate.column_name}`);
// Behalte den ersten Constraint, entferne die anderen
const constraintNames = duplicate.constraint_names;
for (let i = 1; i < constraintNames.length; i++) {
const constraintName = constraintNames[i];
try {
await sequelize.query(`
ALTER TABLE ${duplicate.table_schema}.${duplicate.table_name}
DROP CONSTRAINT IF EXISTS "${constraintName}"
`);
console.log(` ✅ Entfernt: ${constraintName}`);
} catch (error) {
console.log(` ⚠️ Konnte nicht entfernen: ${constraintName} - ${error.message}`);
}
}
}
// 2. Doppelte CHECK Constraints entfernen (korrigierte Abfrage)
console.log('🔍 Suche nach doppelten CHECK Constraints...');
try {
const duplicateCheckConstraints = await sequelize.query(`
SELECT
ttc.table_schema,
ttc.table_name,
tc.constraint_name,
tc.check_clause
FROM information_schema.check_constraints tc
JOIN information_schema.table_constraints ttc
ON tc.constraint_name = ttc.constraint_name
WHERE ttc.table_schema IN ('match3', 'community', 'falukant_data', 'falukant_type', 'falukant_predefine', 'falukant_log', 'chat', 'forum', 'logs', 'type', 'service')
ORDER BY ttc.table_name, tc.constraint_name;
`, { type: sequelize.QueryTypes.SELECT });
console.log(`📊 Gefunden: ${duplicateCheckConstraints.length} CHECK Constraints`);
} catch (error) {
console.log(`⚠️ Konnte CHECK Constraints nicht abfragen: ${error.message}`);
}
// 3. Doppelte Foreign Key Constraints entfernen
console.log('🔍 Suche nach doppelten Foreign Key Constraints...');
const duplicateFKs = await sequelize.query(`
SELECT
tc.constraint_name,
tc.table_name,
tc.table_schema,
kcu.column_name,
ccu.table_name AS foreign_table_name,
ccu.column_name AS foreign_column_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage ccu
ON ccu.constraint_name = tc.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
AND tc.table_schema IN ('match3', 'community', 'falukant_data', 'falukant_type', 'falukant_predefine', 'falukant_log', 'chat', 'forum', 'logs', 'type', 'service')
ORDER BY tc.table_name, tc.constraint_name;
`, { type: sequelize.QueryTypes.SELECT });
console.log(`📊 Gefunden: ${duplicateFKs.length} Foreign Key Constraints`);
// 4. Doppelte Indexe entfernen
console.log('🔍 Suche nach doppelten Indexen...');
const duplicateIndexes = await sequelize.query(`
SELECT
schemaname,
tablename,
indexname,
indexdef
FROM pg_indexes
WHERE schemaname IN ('match3', 'community', 'falukant_data', 'falukant_type', 'falukant_predefine', 'falukant_log', 'chat', 'forum', 'logs', 'type', 'service')
AND indexname LIKE '%_index_%'
ORDER BY tablename, indexname;
`, { type: sequelize.QueryTypes.SELECT });
console.log(`📊 Gefunden: ${duplicateIndexes.length} potenziell doppelte Indexe`);
// 5. Spezifische Match3-Constraints prüfen
console.log('🔍 Prüfe Match3-spezifische Constraints...');
const match3Constraints = await sequelize.query(`
SELECT
constraint_name,
table_name,
constraint_type
FROM information_schema.table_constraints
WHERE table_schema = 'match3'
ORDER BY table_name, constraint_type;
`, { type: sequelize.QueryTypes.SELECT });
console.log(`📊 Match3 Constraints: ${match3Constraints.length} gefunden`);
match3Constraints.forEach(constraint => {
console.log(` - ${constraint.table_name}: ${constraint.constraint_type} (${constraint.constraint_name})`);
});
// 6. Spezifische Chat-Constraints prüfen (da das Problem dort auftritt)
console.log('🔍 Prüfe Chat-spezifische Constraints...');
try {
const chatConstraints = await sequelize.query(`
SELECT
tc.constraint_name,
tc.table_name,
tc.constraint_type,
kcu.column_name
FROM information_schema.table_constraints tc
LEFT JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
WHERE tc.table_schema = 'chat'
ORDER BY tc.table_name, tc.constraint_type, kcu.column_name;
`, { type: sequelize.QueryTypes.SELECT });
console.log(`📊 Chat Constraints: ${chatConstraints.length} gefunden`);
chatConstraints.forEach(constraint => {
console.log(` - ${constraint.table_name}: ${constraint.constraint_type} (${constraint.constraint_name}) auf Spalte: ${constraint.column_name}`);
});
} catch (error) {
console.log(`⚠️ Konnte Chat Constraints nicht abfragen: ${error.message}`);
}
// 7. Spezifische Überprüfung der chat.rights Tabelle
console.log('🔍 Spezielle Überprüfung der chat.rights Tabelle...');
try {
const rightsConstraints = await sequelize.query(`
SELECT
tc.constraint_name,
tc.constraint_type,
kcu.column_name
FROM information_schema.table_constraints tc
LEFT JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
WHERE tc.table_schema = 'chat'
AND tc.table_name = 'rights'
ORDER BY tc.constraint_type, kcu.column_name;
`, { type: sequelize.QueryTypes.SELECT });
console.log(`📊 Chat Rights Constraints: ${rightsConstraints.length} gefunden`);
rightsConstraints.forEach(constraint => {
console.log(` - ${constraint.constraint_type} (${constraint.constraint_name}) auf Spalte: ${constraint.column_name}`);
});
// Entferne alle doppelten UNIQUE Constraints auf der tr-Spalte
const trUniqueConstraints = rightsConstraints.filter(c =>
c.constraint_type === 'UNIQUE' && c.column_name === 'tr'
);
if (trUniqueConstraints.length > 1) {
console.log(`🗑️ Entferne ${trUniqueConstraints.length - 1} doppelte UNIQUE Constraints auf chat.rights.tr`);
// Behalte den ersten, entferne die anderen
for (let i = 1; i < trUniqueConstraints.length; i++) {
const constraintName = trUniqueConstraints[i].constraint_name;
try {
await sequelize.query(`
ALTER TABLE chat.rights
DROP CONSTRAINT IF EXISTS "${constraintName}"
`);
console.log(` ✅ Entfernt: ${constraintName}`);
} catch (error) {
console.log(` ⚠️ Konnte nicht entfernen: ${constraintName} - ${error.message}`);
}
}
}
} catch (error) {
console.log(`⚠️ Konnte chat.rights Constraints nicht abfragen: ${error.message}`);
}
// 8. Empfehlungen ausgeben
console.log('\n💡 Empfehlungen:');
console.log('1. Überprüfe die oben gelisteten Constraints auf Duplikate');
console.log('2. Verwende updateSchema() nur bei expliziten Schema-Änderungen');
console.log('3. Normale syncModels() läuft jetzt ohne alter: true');
console.log('4. Bei Problemen: Manuelle Bereinigung der doppelten Constraints');
console.log('✅ Datenbank-Constraint-Bereinigung abgeschlossen');
} catch (error) {
console.error('❌ Fehler bei der Constraint-Bereinigung:', error);
throw error;
}
}
// Führe das Skript aus, wenn es direkt aufgerufen wird
if (import.meta.url === `file://${process.argv[1]}`) {
cleanupDatabaseConstraints()
.then(() => {
console.log('🎯 Datenbank-Constraint-Bereinigung abgeschlossen');
process.exit(0);
})
.catch((error) => {
console.error('💥 Datenbank-Constraint-Bereinigung fehlgeschlagen:', error);
process.exit(1);
});
}
export default cleanupDatabaseConstraints;

View File

@@ -1,56 +1,133 @@
import { sequelize } from './sequelize.js';
import Match3Campaign from '../models/match3/campaign.js'; import Match3Campaign from '../models/match3/campaign.js';
import Match3Level from '../models/match3/level.js'; import Match3Level from '../models/match3/level.js';
import Match3Objective from '../models/match3/objective.js'; import Match3Objective from '../models/match3/objective.js';
import Match3TileType from '../models/match3/tileType.js';
import Match3LevelTileType from '../models/match3/levelTileType.js';
export const initializeMatch3Data = async () => { /**
* Initialisiert die Match3-Daten in der Datenbank
*/
async function initializeMatch3Data() {
try { try {
console.log('🎯 Initialisiere Match3-Daten...');
// Prüfe ob bereits Daten vorhanden sind // Prüfe ob bereits Daten vorhanden sind
const existingCampaigns = await Match3Campaign.count(); const existingCampaigns = await Match3Campaign.count();
if (existingCampaigns > 0) { if (existingCampaigns > 0) {
console.log('Match3 data already exists, skipping initialization'); console.log('Match3-Daten bereits vorhanden, überspringe Initialisierung');
return; return;
} }
console.log('Initializing Match3 data...'); // Lösche existierende Level und erstelle sie neu
console.log('🔄 Lösche existierende Level...');
await Match3Level.destroy({ where: { campaignId: campaign.id } });
console.log('✅ Existierende Level gelöscht');
// Erstelle erste Kampagne console.log('🎯 Erstelle neue Level...');
// Erstelle Kampagne
const campaign = await Match3Campaign.create({ const campaign = await Match3Campaign.create({
name: 'Juwelen-Meister', name: 'Juwelen-Meister',
description: 'Meistere die Kunst des Juwelen-Matchings', description: 'Meistere die Kunst des Juwelen-Matchings mit einzigartigen Level-Formen',
isActive: true, isActive: true
order: 1
}); });
// Erstelle erste Level console.log('✅ Kampagne erstellt:', campaign.name);
// Erstelle Level 1: Einfaches 5x5 Feld
const level1 = await Match3Level.create({ const level1 = await Match3Level.create({
campaignId: campaign.id, campaignId: campaign.id,
name: 'Der Anfang', name: 'Der Anfang',
description: 'Lerne die Grundlagen des Spiels', description: 'Lerne die Grundlagen mit einem einfachen 5x5 Feld',
order: 1, order: 1,
boardSize: 6, boardLayout: 'xxxxx\nxxxxx\nxxxxx\nxxxxx\nxxxxx',
boardWidth: 5,
boardHeight: 5,
tileTypes: ['gem', 'star', 'heart'], tileTypes: ['gem', 'star', 'heart'],
moveLimit: 15, moveLimit: 15,
isActive: true isActive: true
}); });
// Erstelle Level 2: 7x6 Feld
const level2 = await Match3Level.create({ const level2 = await Match3Level.create({
campaignId: campaign.id, campaignId: campaign.id,
name: 'Erste Herausforderung', name: 'Erste Herausforderung',
description: 'Erweitere deine Fähigkeiten', description: 'Ein größeres 7x6 Feld stellt dich vor neue Herausforderungen',
order: 2, order: 2,
boardSize: 7, boardLayout: 'xxxxxxx\nxxxxxxx\nxxxxxxx\nxxxxxxx\nxxxxxxx\nxxxxxxx',
boardWidth: 7,
boardHeight: 6,
tileTypes: ['gem', 'star', 'heart', 'diamond'], tileTypes: ['gem', 'star', 'heart', 'diamond'],
moveLimit: 20, moveLimit: 20,
isActive: true isActive: true
}); });
// Erstelle Level 3: L-Form mit festen Gems
const level3 = await Match3Level.create({
campaignId: campaign.id,
name: 'Spielzug',
description: 'Sei ein Profi',
order: 3,
boardLayout: 'xxxxx\nxooxx\nxxxgx\nxxxxx\nxxxgx',
boardWidth: 5,
boardHeight: 5,
tileTypes: ['gem', 'star', 'heart', 'diamond'],
moveLimit: 15,
isActive: true
});
// Erstelle Level 4: H-Form
const level4 = await Match3Level.create({
campaignId: campaign.id,
name: 'H-Form',
description: 'Eine H-Form mit vielen Ecken und Kanten',
order: 4,
boardLayout: 'xxxxxoooxxxxx\nxxxxxoooxxxxx\nxxxxxoooxxxxx\nxxxxxoooxxxxx\nxxxxxoooxxxxx\nxxxxxoooxxxxx\nxxxxxoooxxxxx\nxxxxxoooxxxxx\nxxxxxoooxxxxx\nxxxxxoooxxxxx\nxxxxxoooxxxxx\nxxxxxoooxxxxx\nxxxxxoooxxxxx',
boardWidth: 13,
boardHeight: 13,
tileTypes: ['gem', 'star', 'heart', 'diamond', 'crown', 'moon'],
moveLimit: 30,
isActive: true
});
// Erstelle Level 5: Diamant-Form
const level5 = await Match3Level.create({
campaignId: campaign.id,
name: 'Diamant-Form',
description: 'Eine elegante Diamant-Form für Fortgeschrittene',
order: 5,
boardLayout: 'oooxxxooo\nooxxxxxxoo\nooxxxxxxoo\nooxxxxxxoo\nooxxxxxxoo\nooxxxxxxoo\nooxxxxxxoo\nooxxxxxxoo\nooxxxxxxoo\noooxxxooo',
boardWidth: 9,
boardHeight: 10,
tileTypes: ['gem', 'star', 'heart', 'diamond', 'crown', 'moon', 'star'],
moveLimit: 35,
isActive: true
});
// Erstelle Level 6: Spiral-Form
const level6 = await Match3Level.create({
campaignId: campaign.id,
name: 'Spiral-Form',
description: 'Eine komplexe Spiral-Form für Meister',
order: 6,
boardLayout: 'xxxxxxxxxxxxx\nxooooooooooox\nxoxxxxxxxxxox\nxoxoooooooxox\nxoxoxxxxxoxox\nxoxoxoooxoxox\nxoxoxoxoxoxox\nxoxoxoooxoxox\nxoxoxxxxxoxox\nxoxoooooooxox\nxoxxxxxxxxxox\nxooooooooooox\nxxxxxxxxxxxxx',
boardWidth: 13,
boardHeight: 13,
tileTypes: ['gem', 'star', 'heart', 'diamond', 'crown', 'moon', 'star', 'crystal'],
moveLimit: 40,
isActive: true
});
console.log('✅ Alle Level erstellt');
// Erstelle Objectives für Level 1 // Erstelle Objectives für Level 1
await Match3Objective.bulkCreate([ await Match3Objective.bulkCreate([
{ {
levelId: level1.id, levelId: level1.id,
type: 'score', type: 'score',
description: 'Sammle 100 Punkte', description: 'Sammle 150 Punkte',
target: 100, target: 150,
operator: '>=', operator: '>=',
order: 1, order: 1,
isRequired: true isRequired: true
@@ -58,8 +135,8 @@ export const initializeMatch3Data = async () => {
{ {
levelId: level1.id, levelId: level1.id,
type: 'matches', type: 'matches',
description: 'Mache 3 Matches', description: 'Mache 5 Matches',
target: 3, target: 5,
operator: '>=', operator: '>=',
order: 2, order: 2,
isRequired: true isRequired: true
@@ -71,8 +148,8 @@ export const initializeMatch3Data = async () => {
{ {
levelId: level2.id, levelId: level2.id,
type: 'score', type: 'score',
description: 'Sammle 200 Punkte', description: 'Sammle 250 Punkte',
target: 200, target: 250,
operator: '>=', operator: '>=',
order: 1, order: 1,
isRequired: true isRequired: true
@@ -80,8 +157,8 @@ export const initializeMatch3Data = async () => {
{ {
levelId: level2.id, levelId: level2.id,
type: 'matches', type: 'matches',
description: 'Mache 5 Matches', description: 'Mache 8 Matches',
target: 5, target: 8,
operator: '>=', operator: '>=',
order: 2, order: 2,
isRequired: true isRequired: true
@@ -97,8 +174,137 @@ export const initializeMatch3Data = async () => {
} }
]); ]);
console.log('Match3 data initialized successfully'); // Erstelle Objectives für Level 3
await Match3Objective.bulkCreate([
{
levelId: level3.id,
type: 'score',
description: 'Sammle 400 Punkte',
target: 400,
operator: '>=',
order: 1,
isRequired: true
},
{
levelId: level3.id,
type: 'matches',
description: 'Mache 12 Matches',
target: 12,
operator: '>=',
order: 2,
isRequired: true
},
{
levelId: level3.id,
type: 'moves',
description: 'Verwende weniger als 25 Züge',
target: 25,
operator: '<=',
order: 3,
isRequired: true
}
]);
// Erstelle Objectives für Level 4
await Match3Objective.bulkCreate([
{
levelId: level4.id,
type: 'score',
description: 'Sammle 600 Punkte',
target: 600,
operator: '>=',
order: 1,
isRequired: true
},
{
levelId: level4.id,
type: 'matches',
description: 'Mache 15 Matches',
target: 15,
operator: '>=',
order: 2,
isRequired: true
},
{
levelId: level4.id,
type: 'moves',
description: 'Verwende weniger als 30 Züge',
target: 30,
operator: '<=',
order: 3,
isRequired: true
}
]);
// Erstelle Objectives für Level 5
await Match3Objective.bulkCreate([
{
levelId: level5.id,
type: 'score',
description: 'Sammle 800 Punkte',
target: 800,
operator: '>=',
order: 1,
isRequired: true
},
{
levelId: level5.id,
type: 'matches',
description: 'Mache 18 Matches',
target: 18,
operator: '>=',
order: 2,
isRequired: true
},
{
levelId: level5.id,
type: 'moves',
description: 'Verwende weniger als 35 Züge',
target: 35,
operator: '<=',
order: 3,
isRequired: true
}
]);
// Erstelle Objectives für Level 6
await Match3Objective.bulkCreate([
{
levelId: level6.id,
type: 'score',
description: 'Sammle 1000 Punkte',
target: 1000,
operator: '>=',
order: 1,
isRequired: true
},
{
levelId: level6.id,
type: 'matches',
description: 'Mache 25 Matches',
target: 25,
operator: '>=',
order: 2,
isRequired: true
},
{
levelId: level6.id,
type: 'moves',
description: 'Verwende weniger als 40 Züge',
target: 40,
operator: '<=',
order: 3,
isRequired: true
}
]);
console.log('✅ Alle Objectives erstellt');
console.log('🎯 Match3-Daten erfolgreich initialisiert');
} catch (error) { } catch (error) {
console.error('Error initializing Match3 data:', error); console.error('❌ Fehler beim Initialisieren der Match3-Daten:', error);
throw error;
} }
}; }
export default initializeMatch3Data;

View File

@@ -0,0 +1,119 @@
import { sequelize } from './sequelize.js';
import Match3TileType from '../models/match3/tileType.js';
async function initializeMatch3TileTypes() {
try {
console.log('🚀 Initialisiere Match3 Tile-Typen...');
// Synchronisiere das TileType-Modell
await Match3TileType.sync({ alter: true });
console.log('✅ TileType-Modell synchronisiert');
// Standard-Tile-Typen definieren
const defaultTileTypes = [
{
name: 'gem',
displayName: 'Juwel',
symbol: '💎',
color: '#ff6b6b',
rarity: 'common',
points: 10
},
{
name: 'star',
displayName: 'Stern',
symbol: '⭐',
color: '#feca57',
rarity: 'common',
points: 15
},
{
name: 'heart',
displayName: 'Herz',
symbol: '❤️',
color: '#ff9ff3',
rarity: 'common',
points: 12
},
{
name: 'diamond',
displayName: 'Diamant',
symbol: '🔷',
color: '#54a0ff',
rarity: 'uncommon',
points: 20
},
{
name: 'circle',
displayName: 'Kreis',
symbol: '⭕',
color: '#5f27cd',
rarity: 'uncommon',
points: 18
},
{
name: 'square',
displayName: 'Quadrat',
symbol: '🟦',
color: '#00d2d3',
rarity: 'rare',
points: 25
},
{
name: 'crown',
displayName: 'Krone',
symbol: '👑',
color: '#ff9f43',
rarity: 'epic',
points: 35
},
{
name: 'rainbow',
displayName: 'Regenbogen',
symbol: '🌈',
color: '#ff6348',
rarity: 'legendary',
points: 50
}
];
// Tile-Typen erstellen oder aktualisieren
for (const tileType of defaultTileTypes) {
const [tileTypeInstance, created] = await Match3TileType.findOrCreate({
where: { name: tileType.name },
defaults: tileType
});
if (created) {
console.log(`✅ Tile-Typ "${tileType.displayName}" erstellt`);
} else {
// Aktualisiere bestehende Tile-Typen
await Match3TileType.update(tileType, {
where: { name: tileType.name }
});
console.log(`🔄 Tile-Typ "${tileType.displayName}" aktualisiert`);
}
}
console.log('🎉 Alle Match3 Tile-Typen erfolgreich initialisiert!');
} catch (error) {
console.error('❌ Fehler beim Initialisieren der Match3 Tile-Typen:', error);
throw error;
}
}
// Führe die Initialisierung aus, wenn das Skript direkt aufgerufen wird
if (import.meta.url === `file://${process.argv[1]}`) {
initializeMatch3TileTypes()
.then(() => {
console.log('✅ Tile-Typen-Initialisierung abgeschlossen');
process.exit(0);
})
.catch((error) => {
console.error('❌ Fehler bei der Tile-Typen-Initialisierung:', error);
process.exit(1);
});
}
export default initializeMatch3TileTypes;

View File

@@ -34,6 +34,10 @@ const initializeUserRights = async() => {
where: { title: "developer"}, where: { title: "developer"},
defaults: { title: "developer"} defaults: { title: "developer"}
}); });
await UserRightType.findOrCreate({
where: { title: "match3"},
defaults: { title: "match3"}
});
}; };
export default initializeUserRights; export default initializeUserRights;

View File

@@ -7,7 +7,9 @@ const sequelize = new Sequelize(process.env.DB_NAME, process.env.DB_USER, proces
host: process.env.DB_HOST, host: process.env.DB_HOST,
dialect: 'postgres', dialect: 'postgres',
define: { define: {
timestamps: false timestamps: false,
underscored: true, // WICHTIG: Alle Datenbankfelder im snake_case Format
freezeTableName: true // Verhindert Pluralisierung der Tabellennamen
}, },
}); });
@@ -32,9 +34,290 @@ const initializeDatabase = async () => {
}; };
const syncModels = async (models) => { const syncModels = async (models) => {
for (const model of Object.values(models)) {
// Verwende force: false und alter: false, um Constraints nicht neu zu erstellen
// Nur beim ersten Mal oder bei expliziten Schema-Änderungen sollte alter: true verwendet werden
await model.sync({ alter: false, force: false });
}
};
// Intelligente Schema-Synchronisation - prüft ob Updates nötig sind
const syncModelsWithUpdates = async (models) => {
console.log('🔍 Prüfe ob Schema-Updates nötig sind...');
try {
// Prüfe ob neue Felder existieren müssen
const needsUpdate = await checkSchemaUpdates(models);
if (needsUpdate) {
console.log('🔄 Schema-Updates nötig - verwende alter: true');
for (const model of Object.values(models)) {
await model.sync({ alter: true, force: false });
}
console.log('✅ Schema-Updates abgeschlossen');
} else {
console.log('✅ Keine Schema-Updates nötig - verwende alter: false');
for (const model of Object.values(models)) {
await model.sync({ alter: false, force: false });
}
}
} catch (error) {
console.error('❌ Fehler bei Schema-Synchronisation:', error);
// Fallback: Normale Synchronisation ohne Updates
console.log('🔄 Fallback: Normale Synchronisation ohne Updates');
for (const model of Object.values(models)) {
await model.sync({ alter: false, force: false });
}
}
};
// Prüft ob Schema-Updates nötig sind
const checkSchemaUpdates = async (models) => {
try {
console.log('🔍 Prüfe alle Schemas auf Updates...');
// Alle verfügbaren Schemas
const schemas = [
'community', 'logs', 'type', 'service', 'forum',
'falukant_data', 'falukant_type', 'falukant_predefine', 'falukant_log',
'chat', 'match3'
];
let needsUpdate = false;
// Prüfe jedes Schema
for (const schema of schemas) {
const schemaNeedsUpdate = await checkSchemaForUpdates(schema, models);
if (schemaNeedsUpdate) {
needsUpdate = true;
}
}
return needsUpdate;
} catch (error) {
console.error('❌ Fehler bei Schema-Prüfung:', error);
return false; // Im Zweifelsfall: Keine Updates
}
};
// Prüft ein spezifisches Schema auf Updates
const checkSchemaForUpdates = async (schemaName, models) => {
try {
console.log(`🔍 Prüfe Schema: ${schemaName}`);
// Hole alle Tabellen in diesem Schema
const tables = await sequelize.query(`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = :schemaName
ORDER BY table_name
`, {
replacements: { schemaName },
type: sequelize.QueryTypes.SELECT
});
if (tables.length === 0) {
console.log(` 📊 Schema ${schemaName}: Keine Tabellen gefunden`);
return false;
}
console.log(` 📊 Schema ${schemaName}: ${tables.length} Tabellen gefunden`);
// Prüfe jede Tabelle auf Updates
for (const table of tables) {
const tableName = table.table_name;
const tableNeedsUpdate = await checkTableForUpdates(schemaName, tableName, models);
if (tableNeedsUpdate) {
console.log(` 🔄 Tabelle ${tableName} braucht Updates`);
return true;
}
}
// Prüfe auf fehlende Tabellen (neue Models)
const missingTables = await checkForMissingTables(schemaName, models);
if (missingTables.length > 0) {
console.log(` 🔄 Neue Tabellen gefunden: ${missingTables.join(', ')}`);
return true;
}
console.log(` ✅ Schema ${schemaName}: Keine Updates nötig`);
return false;
} catch (error) {
console.error(`❌ Fehler beim Prüfen von Schema ${schemaName}:`, error);
return false;
}
};
// Prüft auf fehlende Tabellen (neue Models)
const checkForMissingTables = async (schemaName, models) => {
try {
const missingTables = [];
// Hole alle erwarteten Tabellen aus den Models
for (const [modelName, model] of Object.entries(models)) {
if (model._schema === schemaName) {
const tableExists = await sequelize.query(`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = :schemaName
AND table_name = :tableName
);
`, {
replacements: { schemaName, tableName: model.tableName },
type: sequelize.QueryTypes.SELECT
});
if (!tableExists[0]?.exists) {
missingTables.push(model.tableName);
}
}
}
return missingTables;
} catch (error) {
console.error(`❌ Fehler beim Prüfen fehlender Tabellen:`, error);
return [];
}
};
// Prüft eine spezifische Tabelle auf Updates
const checkTableForUpdates = async (schemaName, tableName, models) => {
try {
// Finde das entsprechende Model
const model = findModelForTable(schemaName, tableName, models);
if (!model) {
console.log(` ⚠️ Kein Model für Tabelle ${schemaName}.${tableName} gefunden`);
return false;
}
// Hole aktuelle Spalten der Tabelle
const currentColumns = await sequelize.query(`
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_schema = :schemaName
AND table_name = :tableName
ORDER BY ordinal_position
`, {
replacements: { schemaName, tableName },
type: sequelize.QueryTypes.SELECT
});
// Hole erwartete Spalten aus dem Model
const expectedColumns = Object.keys(model.rawAttributes);
// Vergleiche aktuelle und erwartete Spalten
const missingColumns = expectedColumns.filter(expectedCol => {
return !currentColumns.some(currentCol =>
currentCol.column_name === expectedCol
);
});
if (missingColumns.length > 0) {
console.log(` 📊 Fehlende Spalten in ${tableName}: ${missingColumns.join(', ')}`);
return true;
}
// Prüfe auf geänderte Spalten-Typen oder Constraints
for (const expectedCol of expectedColumns) {
const currentCol = currentColumns.find(col => col.column_name === expectedCol);
if (currentCol) {
const needsUpdate = await checkColumnForUpdates(
schemaName, tableName, expectedCol, currentCol, model.rawAttributes[expectedCol]
);
if (needsUpdate) {
return true;
}
}
}
return false;
} catch (error) {
console.error(`❌ Fehler beim Prüfen von Tabelle ${schemaName}.${tableName}:`, error);
return false;
}
};
// Prüft eine spezifische Spalte auf Updates
const checkColumnForUpdates = async (schemaName, tableName, columnName, currentColumn, expectedAttribute) => {
try {
// Prüfe Datentyp-Änderungen
if (currentColumn.data_type !== getExpectedDataType(expectedAttribute)) {
console.log(` 🔄 Spalte ${columnName}: Datentyp geändert (${currentColumn.data_type}${getExpectedDataType(expectedAttribute)})`);
return true;
}
// Prüfe NULL/NOT NULL Änderungen
const currentNullable = currentColumn.is_nullable === 'YES';
const expectedNullable = expectedAttribute.allowNull !== false;
if (currentNullable !== expectedNullable) {
console.log(` 🔄 Spalte ${columnName}: NULL-Constraint geändert (${currentNullable}${expectedNullable})`);
return true;
}
// Prüfe Standardwert-Änderungen
if (expectedAttribute.defaultValue !== undefined &&
currentColumn.column_default !== getExpectedDefaultValue(expectedAttribute.defaultValue)) {
console.log(` 🔄 Spalte ${columnName}: Standardwert geändert`);
return true;
}
return false;
} catch (error) {
console.error(`❌ Fehler beim Prüfen von Spalte ${columnName}:`, error);
return false;
}
};
// Hilfsfunktion: Findet Model für eine Tabelle
const findModelForTable = (schemaName, tableName, models) => {
// Suche nach dem Model basierend auf Schema und Tabellenname
for (const [modelName, model] of Object.entries(models)) {
if (model.tableName === tableName &&
model._schema === schemaName) {
return model;
}
}
return null;
};
// Hilfsfunktion: Konvertiert Sequelize-Datentyp zu PostgreSQL-Datentyp
const getExpectedDataType = (attribute) => {
const type = attribute.type;
if (type instanceof sequelize.DataTypes.INTEGER) return 'integer';
if (type instanceof sequelize.DataTypes.STRING) return 'character varying';
if (type instanceof sequelize.DataTypes.TEXT) return 'text';
if (type instanceof sequelize.DataTypes.BOOLEAN) return 'boolean';
if (type instanceof sequelize.DataTypes.DATE) return 'timestamp without time zone';
if (type instanceof sequelize.DataTypes.JSON) return 'json';
if (type instanceof sequelize.DataTypes.DECIMAL) return 'numeric';
// Fallback
return 'text';
};
// Hilfsfunktion: Konvertiert Sequelize-Default zu PostgreSQL-Default
const getExpectedDefaultValue = (defaultValue) => {
if (defaultValue === null) return null;
if (typeof defaultValue === 'string') return `'${defaultValue}'`;
if (typeof defaultValue === 'number') return defaultValue.toString();
if (typeof defaultValue === 'boolean') return defaultValue.toString();
if (defaultValue === sequelize.literal('CURRENT_TIMESTAMP')) return 'CURRENT_TIMESTAMP';
// Fallback
return defaultValue?.toString() || null;
};
// Separate Funktion für Schema-Updates (nur bei Bedarf aufrufen)
const updateSchema = async (models) => {
console.log('🔄 Aktualisiere Datenbankschema...');
for (const model of Object.values(models)) { for (const model of Object.values(models)) {
await model.sync({ alter: true, force: false }); await model.sync({ alter: true, force: false });
} }
console.log('✅ Datenbankschema aktualisiert');
}; };
async function updateFalukantUserMoney(falukantUserId, moneyChange, activity, changedBy = null) { async function updateFalukantUserMoney(falukantUserId, moneyChange, activity, changedBy = null) {
@@ -70,4 +353,4 @@ async function updateFalukantUserMoney(falukantUserId, moneyChange, activity, ch
} }
} }
export { sequelize, initializeDatabase, syncModels, updateFalukantUserMoney }; export { sequelize, initializeDatabase, syncModels, syncModelsWithUpdates, updateSchema, updateFalukantUserMoney };

View File

@@ -1,6 +1,6 @@
// syncDatabase.js // syncDatabase.js
import { initializeDatabase, syncModels } from './sequelize.js'; import { initializeDatabase, syncModelsWithUpdates } from './sequelize.js';
import initializeTypes from './initializeTypes.js'; import initializeTypes from './initializeTypes.js';
import initializeSettings from './initializeSettings.js'; import initializeSettings from './initializeSettings.js';
import initializeUserRights from './initializeUserRights.js'; import initializeUserRights from './initializeUserRights.js';
@@ -11,7 +11,8 @@ import models from '../models/index.js';
import { createTriggers } from '../models/trigger.js'; import { createTriggers } from '../models/trigger.js';
import initializeForum from './initializeForum.js'; import initializeForum from './initializeForum.js';
import initializeChat from './initializeChat.js'; import initializeChat from './initializeChat.js';
import { initializeMatch3Data } from './initializeMatch3.js'; import initializeMatch3Data from './initializeMatch3.js';
import updateExistingMatch3Levels from './updateExistingMatch3Levels.js';
const syncDatabase = async () => { const syncDatabase = async () => {
try { try {
@@ -19,7 +20,7 @@ const syncDatabase = async () => {
await initializeDatabase(); await initializeDatabase();
console.log("Synchronizing models..."); console.log("Synchronizing models...");
await syncModels(models); await syncModelsWithUpdates(models);
console.log("Setting up associations..."); console.log("Setting up associations...");
setupAssociations(); setupAssociations();
@@ -48,6 +49,10 @@ const syncDatabase = async () => {
console.log("Initializing chat..."); console.log("Initializing chat...");
await initializeChat(); await initializeChat();
// Match3-Initialisierung NACH der Model-Synchronisation
console.log("Updating existing Match3 levels...");
await updateExistingMatch3Levels();
console.log("Initializing Match3..."); console.log("Initializing Match3...");
await initializeMatch3Data(); await initializeMatch3Data();

View File

@@ -0,0 +1,74 @@
import { sequelize } from './sequelize.js';
import Match3Level from '../models/match3/level.js';
/**
* Aktualisiert existierende Match3-Level mit Standard-Layouts
* und neuen Feldern
*/
async function updateExistingMatch3Levels() {
try {
console.log('🔧 Aktualisiere existierende Match3-Level...');
// Finde alle existierenden Level ohne boardLayout
const existingLevels = await Match3Level.findAll({
where: {
boardLayout: null
}
});
console.log(`📊 Gefunden: ${existingLevels.length} Level ohne boardLayout`);
if (existingLevels.length === 0) {
console.log('✅ Alle Level haben bereits boardLayout');
return;
}
// Aktualisiere jeden Level mit Standard-Layout
for (const level of existingLevels) {
const oldBoardSize = level.boardSize || 6;
// Erstelle Standard-Layout basierend auf alter boardSize
let boardLayout = '';
for (let i = 0; i < oldBoardSize; i++) {
for (let j = 0; j < oldBoardSize; j++) {
boardLayout += 'x';
}
if (i < oldBoardSize - 1) boardLayout += '\n';
}
// Aktualisiere den Level mit allen neuen Feldern
await level.update({
boardLayout: boardLayout,
boardWidth: oldBoardSize,
boardHeight: oldBoardSize,
// Stelle sicher, dass alle erforderlichen Felder gesetzt sind
tileTypes: level.tileTypes || ['gem', 'star', 'heart'],
moveLimit: level.moveLimit || 20,
isActive: level.isActive !== undefined ? level.isActive : true
});
console.log(`🔧 Level ${level.id} aktualisiert: ${oldBoardSize}x${oldBoardSize} → alle neuen Felder gesetzt`);
}
console.log('✅ Alle existierenden Level wurden aktualisiert');
} catch (error) {
console.error('❌ Fehler beim Aktualisieren der Match3-Level:', error);
throw error;
}
}
// Führe das Skript aus, wenn es direkt aufgerufen wird
if (import.meta.url === `file://${process.argv[1]}`) {
updateExistingMatch3Levels()
.then(() => {
console.log('🎯 Match3-Level-Update abgeschlossen');
process.exit(0);
})
.catch((error) => {
console.error('💥 Match3-Level-Update fehlgeschlagen:', error);
process.exit(1);
});
}
export default updateExistingMatch3Levels;

View File

@@ -0,0 +1,115 @@
import { sequelize } from './sequelize.js';
import Match3Level from '../models/match3/level.js';
import Match3TileType from '../models/match3/tileType.js';
import Match3LevelTileType from '../models/match3/levelTileType.js';
/**
* Aktualisiert bestehende Match3 Level mit den neuen Tile-Typen
*/
async function updateMatch3LevelsWithTileTypes() {
try {
console.log('🔄 Aktualisiere Match3 Level mit neuen Tile-Typen...');
// Synchronisiere die neuen Modelle
await Match3TileType.sync({ alter: true });
await Match3LevelTileType.sync({ alter: true });
console.log('✅ Neue Modelle synchronisiert');
// Hole alle Level
const levels = await Match3Level.findAll();
console.log(`📊 ${levels.length} Level gefunden`);
// Hole alle verfügbaren Tile-Typen
const tileTypes = await Match3TileType.findAll();
console.log(`🎯 ${tileTypes.length} Tile-Typen verfügbar`);
// Erstelle eine Mapping-Tabelle für die alten tileTypes Arrays
const tileTypeMapping = {
'gem': tileTypes.find(t => t.name === 'gem'),
'star': tileTypes.find(t => t.name === 'star'),
'heart': tileTypes.find(t => t.name === 'heart'),
'diamond': tileTypes.find(t => t.name === 'diamond'),
'circle': tileTypes.find(t => t.name === 'circle'),
'square': tileTypes.find(t => t.name === 'square'),
'crown': tileTypes.find(t => t.name === 'crown'),
'rainbow': tileTypes.find(t => t.name === 'rainbow')
};
// Gehe durch alle Level und erstelle Verknüpfungen
for (const level of levels) {
console.log(`🔄 Verarbeite Level ${level.order}: ${level.name}`);
// Lösche bestehende Verknüpfungen für dieses Level
await Match3LevelTileType.destroy({
where: { levelId: level.id }
});
// Verwende die alten tileTypes Arrays als Fallback
let levelTileTypes = [];
if (level.tileTypes && Array.isArray(level.tileTypes)) {
levelTileTypes = level.tileTypes;
} else {
// Fallback: Verwende Standard-Tile-Typen basierend auf der Level-Nummer
switch (level.order) {
case 1:
levelTileTypes = ['gem', 'star', 'heart'];
break;
case 2:
levelTileTypes = ['gem', 'star', 'heart', 'diamond'];
break;
case 3:
levelTileTypes = ['gem', 'star', 'heart', 'diamond', 'circle'];
break;
case 4:
levelTileTypes = ['gem', 'star', 'heart', 'diamond', 'circle', 'square'];
break;
case 5:
levelTileTypes = ['gem', 'star', 'heart', 'diamond', 'circle', 'square', 'crown'];
break;
case 6:
levelTileTypes = ['gem', 'star', 'heart', 'diamond', 'circle', 'square', 'crown', 'rainbow'];
break;
default:
levelTileTypes = ['gem', 'star', 'heart'];
}
}
// Erstelle Verknüpfungen für jeden Tile-Typ
for (const tileTypeName of levelTileTypes) {
const tileType = tileTypeMapping[tileTypeName];
if (tileType) {
await Match3LevelTileType.create({
levelId: level.id,
tileTypeId: tileType.id,
weight: 1, // Standard-Gewichtung
isActive: true
});
console.log(` ✅ Verknüpft mit ${tileType.displayName}`);
} else {
console.warn(` ⚠️ Tile-Typ "${tileTypeName}" nicht gefunden`);
}
}
}
console.log('🎉 Alle Match3 Level erfolgreich mit Tile-Typen verknüpft!');
} catch (error) {
console.error('❌ Fehler beim Aktualisieren der Match3 Level:', error);
throw error;
}
}
// Führe die Aktualisierung aus, wenn das Skript direkt aufgerufen wird
if (import.meta.url === `file://${process.argv[1]}`) {
updateMatch3LevelsWithTileTypes()
.then(() => {
console.log('✅ Level-Tile-Typ-Aktualisierung abgeschlossen');
process.exit(0);
})
.catch((error) => {
console.error('❌ Fehler bei der Level-Tile-Typ-Aktualisierung:', error);
process.exit(1);
});
}
export default updateMatch3LevelsWithTileTypes;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<title>YourPart</title>
<script type="module" crossorigin src="/assets/index-C2z2YL6l.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BLkCbvUa.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

View File

@@ -93,6 +93,61 @@
"systemmessage": "Systemnachricht" "systemmessage": "Systemnachricht"
}, },
"confirmDelete": "Soll dieser Chatraum wirklich gelöscht werden?" "confirmDelete": "Soll dieser Chatraum wirklich gelöscht werden?"
},
"match3": {
"title": "Match3 Level verwalten",
"newLevel": "Neues Level erstellen",
"editLevel": "Level bearbeiten",
"deleteLevel": "Level löschen",
"confirmDelete": "Möchtest du dieses Level wirklich löschen?",
"levelName": "Name",
"levelDescription": "Beschreibung",
"boardWidth": "Breite",
"boardHeight": "Höhe",
"moveLimit": "Zug-Limit",
"levelOrder": "Reihenfolge",
"boardLayout": "Board-Layout",
"tileTypes": "Verfügbare Tile-Typen",
"actions": "Aktionen",
"edit": "Bearbeiten",
"delete": "Löschen",
"save": "Speichern",
"cancel": "Abbrechen",
"update": "Aktualisieren",
"create": "Erstellen",
"boardControls": {
"fillAll": "Alle aktivieren",
"clearAll": "Alle deaktivieren",
"invert": "Invertieren"
},
"loading": "Lade Level...",
"retry": "Erneut versuchen",
"availableLevels": "Verfügbare Level: {count}",
"levelFormat": "Level {number}: {name}",
"levelObjectives": "Level-Objekte",
"objectivesTitle": "Siegvoraussetzungen",
"addObjective": "Objektiv hinzufügen",
"removeObjective": "Entfernen",
"objectiveType": "Typ",
"objectiveTypeScore": "Punkte sammeln",
"objectiveTypeMatches": "Matches machen",
"objectiveTypeMoves": "Züge verwenden",
"objectiveTypeTime": "Zeit einhalten",
"objectiveTypeSpecial": "Spezialziel",
"objectiveOperator": "Operator",
"operatorGreaterEqual": "Größer oder gleich (≥)",
"operatorLessEqual": "Kleiner oder gleich (≤)",
"operatorEqual": "Gleich (=)",
"operatorGreater": "Größer als (>)",
"operatorLess": "Kleiner als (<)",
"objectiveTarget": "Zielwert",
"objectiveTargetPlaceholder": "z.B. 100",
"objectiveOrder": "Reihenfolge",
"objectiveOrderPlaceholder": "1, 2, 3...",
"objectiveDescription": "Beschreibung",
"objectiveDescriptionPlaceholder": "z.B. Sammle 100 Punkte",
"objectiveRequired": "Erforderlich für Level-Abschluss",
"noObjectives": "Keine Siegvoraussetzungen definiert. Klicke auf 'Objektiv hinzufügen' um welche zu erstellen."
} }
} }
} }

View File

@@ -53,7 +53,12 @@
"logentries": "Log-Einträge", "logentries": "Log-Einträge",
"edituser": "Benutzer bearbeiten", "edituser": "Benutzer bearbeiten",
"database": "Datenbank" "database": "Datenbank"
} },
"minigames": "Minispiele",
"m-minigames": {
"match3": "Match3 Level"
},
"chatrooms": "Chaträume"
}, },
"m-friends": { "m-friends": {
"manageFriends": "Freunde verwalten", "manageFriends": "Freunde verwalten",

View File

@@ -1,3 +1,59 @@
{ {
"admin": {
"match3": {
"title": "Manage Match3 Levels",
"newLevel": "Create New Level",
"editLevel": "Edit Level",
"deleteLevel": "Delete Level",
"confirmDelete": "Do you really want to delete this level?",
"levelName": "Name",
"levelDescription": "Description",
"boardWidth": "Width",
"boardHeight": "Height",
"moveLimit": "Move Limit",
"levelOrder": "Order",
"boardLayout": "Board Layout",
"tileTypes": "Available Tile Types",
"actions": "Actions",
"edit": "Edit",
"delete": "Delete",
"save": "Save",
"cancel": "Cancel",
"update": "Update",
"create": "Create",
"boardControls": {
"fillAll": "Activate All",
"clearAll": "Deactivate All",
"invert": "Invert"
},
"loading": "Loading levels...",
"retry": "Retry",
"availableLevels": "Available Levels: {count}",
"levelFormat": "Level {number}: {name}",
"levelObjectives": "Level Objectives",
"objectivesTitle": "Victory Conditions",
"addObjective": "Add Objective",
"removeObjective": "Remove",
"objectiveType": "Type",
"objectiveTypeScore": "Collect Score",
"objectiveTypeMatches": "Make Matches",
"objectiveTypeMoves": "Use Moves",
"objectiveTypeTime": "Keep Time",
"objectiveTypeSpecial": "Special Goal",
"objectiveOperator": "Operator",
"operatorGreaterEqual": "Greater or equal (≥)",
"operatorLessEqual": "Less or equal (≤)",
"operatorEqual": "Equal (=)",
"operatorGreater": "Greater than (>)",
"operatorLess": "Less than (<)",
"objectiveTarget": "Target Value",
"objectiveTargetPlaceholder": "e.g. 100",
"objectiveOrder": "Order",
"objectiveOrderPlaceholder": "1, 2, 3...",
"objectiveDescription": "Description",
"objectiveDescriptionPlaceholder": "e.g. Collect 100 points",
"objectiveRequired": "Required for level completion",
"noObjectives": "No victory conditions defined. Click 'Add Objective' to create some."
}
}
} }

View File

@@ -2,7 +2,8 @@ import AdminInterestsView from '../views/admin/InterestsView.vue';
import AdminContactsView from '../views/admin/ContactsView.vue'; import AdminContactsView from '../views/admin/ContactsView.vue';
import RoomsView from '../views/admin/RoomsView.vue'; import RoomsView from '../views/admin/RoomsView.vue';
import ForumAdminView from '../dialogues/admin/ForumAdminView.vue'; import ForumAdminView from '../dialogues/admin/ForumAdminView.vue';
import AdminFalukantEditUserView from '../views/admin/falukant/EditUserView.vue' import AdminFalukantEditUserView from '../views/admin/falukant/EditUserView.vue';
import AdminMinigamesView from '../views/admin/MinigamesView.vue';
const adminRoutes = [ const adminRoutes = [
{ {
@@ -34,6 +35,12 @@ const adminRoutes = [
name: 'AdminFalukantEditUserView', name: 'AdminFalukantEditUserView',
component: AdminFalukantEditUserView, component: AdminFalukantEditUserView,
meta: { requiresAuth: true } meta: { requiresAuth: true }
},
{
path: '/admin/minigames/match3',
name: 'AdminMinigames',
component: AdminMinigamesView,
meta: { requiresAuth: true }
} }
]; ];

View File

@@ -10,10 +10,19 @@ const apiClient = axios.create({
apiClient.interceptors.request.use(config => { apiClient.interceptors.request.use(config => {
const user = store.getters.user; const user = store.getters.user;
console.log('🔑 Axios Interceptor - User:', user);
if (user && user.authCode) { if (user && user.authCode) {
config.headers['userId'] = user.id; config.headers['userid'] = user.id;
config.headers['authCode'] = user.authCode; config.headers['authcode'] = user.authCode; // Kleinschreibung!
console.log('📡 Setze Headers:', {
userid: user.id,
authcode: user.authCode
});
} else {
console.log('⚠️ Keine User-Daten verfügbar');
} }
return config; return config;
}, error => { }, error => {
return Promise.reject(error); return Promise.reject(error);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff