Files
yourpart3/backend/services/match3Service.js
Torsten Schulz (local) e168adeb51 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.
2025-08-23 06:00:29 +02:00

504 lines
18 KiB
JavaScript

import Match3Campaign from '../models/match3/campaign.js';
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 {
// Lade alle aktiven Kampagnen
async getCampaigns() {
const campaigns = await Match3Campaign.findAll({
where: { isActive: true },
include: [
{
model: Match3Level,
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' };
}
// 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
});
}
// 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,
bestScore: 0,
bestMoves: 0,
bestTime: 0
});
}
// Aktualisiere Level-Fortschritt
// WICHTIG: Ein Level ist nur abgeschlossen, wenn es mindestens 1 Stern gibt UND der Score > 0 ist
// Das verhindert, dass unvollständige Level als abgeschlossen markiert werden
const isCompleted = stars > 0 && score > 0;
const attempts = levelProgress.attempts + 1;
await levelProgress.update({
score: Math.max(score, levelProgress.bestScore),
moves: moves,
time: time,
stars: Math.max(stars, levelProgress.stars),
// WICHTIG: isCompleted wird NUR auf true gesetzt, wenn das Level tatsächlich abgeschlossen ist
// Der alte Status wird NICHT beibehalten, da das zu falschen Abschlüssen führen kann
isCompleted: isCompleted,
attempts: attempts,
bestScore: Math.max(score, levelProgress.bestScore),
bestMoves: Math.min(moves, levelProgress.bestMoves || moves),
bestTime: Math.min(time, levelProgress.bestTime || time),
completedAt: isCompleted ? new Date() : levelProgress.completedAt
});
// Aktualisiere Gesamt-Fortschritt
// WICHTIG: Nur wenn das Level abgeschlossen ist, werden Score und Stars zum Gesamtfortschritt hinzugefügt
// Das verhindert, dass unvollständige Level den Gesamtfortschritt beeinflussen
let totalScore = userProgress.totalScore;
let totalStars = userProgress.totalStars;
if (isCompleted) {
totalScore += score;
totalStars += stars;
}
// Berechne neue currentLevel
// WICHTIG: Zähle nur Level, die tatsächlich abgeschlossen sind
const levelsCompleted = await Match3UserLevelProgress.count({
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: levelsCompleted + 1
});
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 };
}
}
// 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;
}
// 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;
}
}
// Lade Benutzer-Statistiken
async getUserStats(userId) {
const userProgress = await Match3UserProgress.findAll({
where: { userId },
include: [
{
model: Match3UserLevelProgress,
as: 'levelProgress',
include: [
{
model: Match3Level,
as: 'level'
}
]
}
]
});
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();