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:
@@ -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();
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user