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) {
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);
} catch (error) {
console.log(error);
res.status(500).json({ error: error.message });
if (error.message === 'noaccess') {
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) {
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);
} catch (error) {
console.log(error);
res.status(500).json({ error: error.message });
if (error.message === 'noaccess') {
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) {
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);
} catch (error) {
console.log(error);
res.status(500).json({ error: error.message });
if (error.message === 'noaccess') {
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) {
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);
} catch (error) {
console.log(error);
res.status(500).json({ error: error.message });
if (error.message === 'noaccess') {
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 });
}
}
// --- 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;

View File

@@ -1,128 +1,212 @@
import match3Service from '../services/match3Service.js';
import Match3Service from '../services/match3Service.js';
class Match3Controller {
/**
* 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' });
}
}
function extractHashedUserId(req) {
return req.headers?.userid;
}
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"
},
}
},
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);
for (const [key, value] of Object.entries(menu)) {
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("over12") && age >= 12)
|| (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 Match3UserProgress from './match3/userProgress.js';
import Match3UserLevelProgress from './match3/userLevelProgress.js';
import Match3TileType from './match3/tileType.js';
import Match3LevelTileType from './match3/levelTileType.js';
export default function setupAssociations() {
// RoomType 1:n Room
@@ -841,4 +843,23 @@ export default function setupAssociations() {
foreignKey: 'levelId',
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 Match3UserProgress from './match3/userProgress.js';
import Match3UserLevelProgress from './match3/userLevelProgress.js';
import Match3TileType from './match3/tileType.js';
import Match3LevelTileType from './match3/levelTileType.js';
// — Politische Ämter (Politics) —
import PoliticalOfficeType from './falukant/type/political_office_type.js';
@@ -214,6 +216,8 @@ const models = {
Match3Objective,
Match3UserProgress,
Match3UserLevelProgress,
Match3TileType,
Match3LevelTileType,
PoliticalOfficeType,
PoliticalOfficeRequirement,
PoliticalOfficeBenefitType,

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import { sequelize } from '../../utils/sequelize.js';
import { DataTypes } from 'sequelize';
import { sequelize } from '../../utils/sequelize.js';
const Level = sequelize.define('Level', {
const Match3Level = sequelize.define('Match3Level', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
@@ -9,7 +9,11 @@ const Level = sequelize.define('Level', {
},
campaignId: {
type: DataTypes.INTEGER,
allowNull: false
allowNull: false,
references: {
model: 'match3_campaigns',
key: 'id'
}
},
name: {
type: DataTypes.STRING(255),
@@ -21,41 +25,51 @@ const Level = sequelize.define('Level', {
},
order: {
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,
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: {
type: DataTypes.JSON,
allowNull: false,
defaultValue: ['gem', 'star', 'heart', 'diamond', 'circle', 'square']
allowNull: true, // Ändern zu true, da wir jetzt eine Verknüpfungstabelle haben
comment: 'Legacy: Array der verfügbaren Tile-Typen (wird durch levelTileTypes ersetzt)'
},
moveLimit: {
type: DataTypes.INTEGER,
allowNull: true
allowNull: false,
defaultValue: 20
},
timeLimit: {
type: DataTypes.INTEGER,
allowNull: true
allowNull: true,
comment: 'Zeitlimit in Sekunden (null = kein Limit)'
},
isActive: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true
},
createdAt: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
},
updatedAt: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
}
}, {
tableName: 'match3_levels',
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',
schema: 'match3',
timestamps: true
timestamps: true,
underscored: true // WICHTIG: Alle Datenbankfelder im snake_case Format
});
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',
schema: 'match3',
timestamps: true,
underscored: true, // WICHTIG: Alle Datenbankfelder im snake_case Format
indexes: [
{
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',
schema: 'match3',
timestamps: true,
underscored: true, // WICHTIG: Alle Datenbankfelder im snake_case Format
indexes: [
{
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.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;

View File

@@ -1,22 +1,26 @@
import express from 'express';
import match3Controller from '../controllers/match3Controller.js';
import { authenticate } from '../middleware/authMiddleware.js';
import Match3Controller from '../controllers/match3Controller.js';
const router = express.Router();
const match3Controller = new Match3Controller();
// Alle Routen erfordern Authentifizierung
// Alle Routen benötigen Authentifizierung
router.use(authenticate);
// Kampagnen-Routen
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.post('/campaigns/:campaignId/levels/:levelId/progress', match3Controller.updateLevelProgress);
router.post('/campaigns/:campaignId/cleanup', match3Controller.cleanupUserProgress);
// Bestenliste und Statistiken
router.get('/campaigns/:campaignId/leaderboard', match3Controller.getLeaderboard);
// Statistiken-Routen
router.get('/stats', match3Controller.getUserStats);
export default router;

View File

@@ -292,22 +292,34 @@ class AdminService {
}
// --- Chat Room Admin ---
async getRoomTypes() {
async getRoomTypes(userId) {
if (!(await this.hasUserAccess(userId, 'chatrooms'))) {
throw new Error('noaccess');
}
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')
const genderType = await UserParamType.findOne({ where: { description: 'gender' } });
if (!genderType) return [];
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();
}
async getRooms() {
async getRooms(userId) {
if (!(await this.hasUserAccess(userId, 'chatrooms'))) {
throw new Error('noaccess');
}
// Only return necessary fields to the frontend
return await Room.findAll({
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);
if (!room) throw new Error('Room not found');
await room.update(data);
return room;
}
async createRoom(data) {
async createRoom(userId, data) {
if (!(await this.hasUserAccess(userId, 'chatrooms'))) {
throw new Error('noaccess');
}
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 } });
}
// --- 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();

View File

@@ -3,311 +3,501 @@ import Match3Level from '../models/match3/level.js';
import Match3Objective from '../models/match3/objective.js';
import Match3UserProgress from '../models/match3/userProgress.js';
import Match3UserLevelProgress from '../models/match3/userLevelProgress.js';
import Match3TileType from '../models/match3/tileType.js';
import Match3LevelTileType from '../models/match3/levelTileType.js';
class Match3Service {
/**
* Lädt alle aktiven Kampagnen
*/
async getActiveCampaigns() {
try {
const campaigns = await Match3Campaign.findAll({
where: { isActive: true },
include: [
{
model: Match3Level,
as: 'levels',
// Lade alle aktiven Kampagnen
async getCampaigns() {
const campaigns = await Match3Campaign.findAll({
where: { isActive: true },
required: false,
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,
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;
} 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
// Lade oder erstelle Benutzer-Fortschritt
let userProgress = await Match3UserProgress.findOne({
where: { userId, campaignId }
});
if (!userProgress) {
userProgress = await Match3UserProgress.create({
userId,
campaignId,
totalScore: 0,
totalStars: 0,
levelsCompleted: 0,
currentLevel: 1,
isCompleted: false
});
}
if (!userProgress) {
userProgress = await Match3UserProgress.create({
userId,
campaignId,
totalScore: 0,
totalStars: 0,
levelsCompleted: 0,
currentLevel: 1,
isCompleted: false
});
}
// Lade oder erstelle Level-Fortschritt
let levelProgress = await Match3UserLevelProgress.findOne({
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 }
// Lade oder erstelle Level-Fortschritt
let levelProgress = await Match3UserLevelProgress.findOne({
where: { userProgressId: userProgress.id, levelId }
});
const totalStars = await Match3UserLevelProgress.sum('stars', {
where: { userProgressId: userProgress.id, isCompleted: true }
});
if (!levelProgress) {
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
});
}
const levelsCompleted = await Match3UserLevelProgress.count({
where: { userProgressId: userProgress.id, isCompleted: true }
});
// Korrigiere currentLevel: Es sollte immer levelsCompleted + 1 sein
const correctCurrentLevel = levelsCompleted + 1;
// 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({
where: {
userProgressId: userProgress.id,
isCompleted: true
}
});
// Zusätzliche Sicherheit: Stelle sicher, dass levelsCompleted nicht größer als die verfügbaren Level ist
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({
totalScore,
totalStars,
levelsCompleted,
currentLevel: correctCurrentLevel, // Verwende den korrigierten Wert
lastPlayed: new Date()
currentLevel: levelsCompleted + 1
});
// Prüfe ob Kampagne abgeschlossen ist
const totalLevels = await Match3Level.count({
where: { campaignId, isActive: true }
});
if (levelsCompleted >= totalLevels) {
await userProgress.update({ isCompleted: true });
}
return {
success: true,
message: `Cleaned ${cleanedCount} level progress entries`,
cleanedCount,
totalScore,
totalStars,
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;
}
}
/**
* Lädt die Bestenliste für eine Kampagne
*/
async getLeaderboard(campaignId, limit = 10) {
try {
const leaderboard = await Match3UserProgress.findAll({
where: { campaignId },
include: [
{
model: Match3UserLevelProgress,
as: 'levelProgress',
where: { isCompleted: true },
required: false
// ANTI-CHEAT: Validiere den Progress-Hash
validateProgressHash(userId, campaignId, levelId, score, moves, time, stars, securityHash, timestamp) {
try {
// Prüfe ob der Timestamp nicht zu alt ist (5 Minuten)
const now = Date.now();
if (now - timestamp > 5 * 60 * 1000) {
console.warn(`Progress validation failed: Timestamp too old for user ${userId}`);
return false;
}
// Lade Level-Daten für Hash-Validierung
return Match3Level.findByPk(levelId).then(level => {
if (!level) {
console.warn(`Progress validation failed: Level ${levelId} not found`);
return false;
}
],
order: [
['totalScore', 'DESC'],
['totalStars', 'DESC'],
['levelsCompleted', 'DESC']
],
limit
});
return leaderboard;
} catch (error) {
console.error('Error loading leaderboard:', error);
throw error;
// Erstelle den gleichen Hash wie im Frontend
const dataString = `${levelId}|${score}|${moves}|${stars}|true|${level.boardLayout}|${level.moveLimit}`;
// 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;
}
}
}
/**
* Lädt Statistiken für einen Benutzer
*/
async getUserStats(userId) {
try {
const stats = await Match3UserProgress.findAll({
where: { userId },
include: [
{
model: Match3Campaign,
as: 'campaign'
},
{
model: Match3UserLevelProgress,
as: 'levelProgress',
// Lade Benutzer-Statistiken
async getUserStats(userId) {
const userProgress = await Match3UserProgress.findAll({
where: { userId },
include: [
{
model: Match3Level,
as: 'level'
}
{
model: Match3UserLevelProgress,
as: 'levelProgress',
include: [
{
model: Match3Level,
as: 'level'
}
]
}
]
}
]
});
});
return stats;
} catch (error) {
console.error('Error loading user stats:', error);
throw error;
if (!userProgress || userProgress.length === 0) {
return {
totalScore: 0,
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();

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 Match3Level from '../models/match3/level.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 {
console.log('🎯 Initialisiere Match3-Daten...');
// Prüfe ob bereits Daten vorhanden sind
const existingCampaigns = await Match3Campaign.count();
if (existingCampaigns > 0) {
console.log('Match3 data already exists, skipping initialization');
console.log('Match3-Daten bereits vorhanden, überspringe Initialisierung');
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({
name: 'Juwelen-Meister',
description: 'Meistere die Kunst des Juwelen-Matchings',
isActive: true,
order: 1
description: 'Meistere die Kunst des Juwelen-Matchings mit einzigartigen Level-Formen',
isActive: true
});
// Erstelle erste Level
console.log('✅ Kampagne erstellt:', campaign.name);
// Erstelle Level 1: Einfaches 5x5 Feld
const level1 = await Match3Level.create({
campaignId: campaign.id,
name: 'Der Anfang',
description: 'Lerne die Grundlagen des Spiels',
description: 'Lerne die Grundlagen mit einem einfachen 5x5 Feld',
order: 1,
boardSize: 6,
boardLayout: 'xxxxx\nxxxxx\nxxxxx\nxxxxx\nxxxxx',
boardWidth: 5,
boardHeight: 5,
tileTypes: ['gem', 'star', 'heart'],
moveLimit: 15,
isActive: true
});
// Erstelle Level 2: 7x6 Feld
const level2 = await Match3Level.create({
campaignId: campaign.id,
name: 'Erste Herausforderung',
description: 'Erweitere deine Fähigkeiten',
description: 'Ein größeres 7x6 Feld stellt dich vor neue Herausforderungen',
order: 2,
boardSize: 7,
boardLayout: 'xxxxxxx\nxxxxxxx\nxxxxxxx\nxxxxxxx\nxxxxxxx\nxxxxxxx',
boardWidth: 7,
boardHeight: 6,
tileTypes: ['gem', 'star', 'heart', 'diamond'],
moveLimit: 20,
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
await Match3Objective.bulkCreate([
{
levelId: level1.id,
type: 'score',
description: 'Sammle 100 Punkte',
target: 100,
description: 'Sammle 150 Punkte',
target: 150,
operator: '>=',
order: 1,
isRequired: true
@@ -58,8 +135,8 @@ export const initializeMatch3Data = async () => {
{
levelId: level1.id,
type: 'matches',
description: 'Mache 3 Matches',
target: 3,
description: 'Mache 5 Matches',
target: 5,
operator: '>=',
order: 2,
isRequired: true
@@ -71,8 +148,8 @@ export const initializeMatch3Data = async () => {
{
levelId: level2.id,
type: 'score',
description: 'Sammle 200 Punkte',
target: 200,
description: 'Sammle 250 Punkte',
target: 250,
operator: '>=',
order: 1,
isRequired: true
@@ -80,8 +157,8 @@ export const initializeMatch3Data = async () => {
{
levelId: level2.id,
type: 'matches',
description: 'Mache 5 Matches',
target: 5,
description: 'Mache 8 Matches',
target: 8,
operator: '>=',
order: 2,
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) {
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"},
defaults: { title: "developer"}
});
await UserRightType.findOrCreate({
where: { title: "match3"},
defaults: { title: "match3"}
});
};
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,
dialect: 'postgres',
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) => {
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)) {
await model.sync({ alter: true, force: false });
}
console.log('✅ Datenbankschema aktualisiert');
};
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
import { initializeDatabase, syncModels } from './sequelize.js';
import { initializeDatabase, syncModelsWithUpdates } from './sequelize.js';
import initializeTypes from './initializeTypes.js';
import initializeSettings from './initializeSettings.js';
import initializeUserRights from './initializeUserRights.js';
@@ -11,7 +11,8 @@ import models from '../models/index.js';
import { createTriggers } from '../models/trigger.js';
import initializeForum from './initializeForum.js';
import initializeChat from './initializeChat.js';
import { initializeMatch3Data } from './initializeMatch3.js';
import initializeMatch3Data from './initializeMatch3.js';
import updateExistingMatch3Levels from './updateExistingMatch3Levels.js';
const syncDatabase = async () => {
try {
@@ -19,7 +20,7 @@ const syncDatabase = async () => {
await initializeDatabase();
console.log("Synchronizing models...");
await syncModels(models);
await syncModelsWithUpdates(models);
console.log("Setting up associations...");
setupAssociations();
@@ -48,6 +49,10 @@ const syncDatabase = async () => {
console.log("Initializing chat...");
await initializeChat();
// Match3-Initialisierung NACH der Model-Synchronisation
console.log("Updating existing Match3 levels...");
await updateExistingMatch3Levels();
console.log("Initializing Match3...");
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;