Some checks failed
🚀 Continuous Integration / 🔧 Backend Tests (18.x) (push) Has been cancelled
🚀 Continuous Integration / 🔧 Backend Tests (20.x) (push) Has been cancelled
🚀 Continuous Integration / 🎨 Frontend Tests (18.x) (push) Has been cancelled
🚀 Continuous Integration / 🎨 Frontend Tests (20.x) (push) Has been cancelled
🚀 Continuous Integration / 🔍 Code Quality (push) Has been cancelled
🚀 Continuous Integration / 🔒 Security Checks (push) Has been cancelled
🚀 Continuous Integration / 🎨 Theme Tests (push) Has been cancelled
🚀 Continuous Integration / ♿ Accessibility Tests (push) Has been cancelled
🚀 Continuous Integration / 📱 Cross-Browser Tests (push) Has been cancelled
🚀 Continuous Integration / 🏗️ Build Tests (push) Has been cancelled
🚀 Continuous Integration / 📊 Performance Tests (push) Has been cancelled
🚀 Continuous Integration / 🎯 Integration Tests (push) Has been cancelled
🚀 Continuous Integration / ✅ All Tests Passed (push) Has been cancelled
275 lines
5.8 KiB
JavaScript
275 lines
5.8 KiB
JavaScript
const mongoose = require('mongoose');
|
|
const bcrypt = require('bcryptjs');
|
|
|
|
// Schema für Kinder-Profile
|
|
const childSchema = new mongoose.Schema({
|
|
name: {
|
|
type: String,
|
|
required: true,
|
|
trim: true,
|
|
maxlength: 50
|
|
},
|
|
age: {
|
|
type: Number,
|
|
required: true,
|
|
min: 3,
|
|
max: 18
|
|
},
|
|
avatar: {
|
|
type: String,
|
|
default: 'default-child.png',
|
|
enum: [
|
|
'default-child.png',
|
|
'superhero-boy.png',
|
|
'superhero-girl.png',
|
|
'pirate-boy.png',
|
|
'pirate-girl.png',
|
|
'princess.png',
|
|
'knight.png',
|
|
'astronaut.png',
|
|
'detective.png',
|
|
'artist.png'
|
|
]
|
|
},
|
|
parent: {
|
|
type: mongoose.Schema.Types.ObjectId,
|
|
ref: 'User',
|
|
required: true
|
|
},
|
|
// Authentifizierung für Kinder
|
|
username: {
|
|
type: String,
|
|
required: true,
|
|
unique: true,
|
|
trim: true,
|
|
minlength: 3,
|
|
maxlength: 20,
|
|
match: /^[a-zA-Z0-9_]+$/
|
|
},
|
|
pin: {
|
|
type: String,
|
|
required: true,
|
|
minlength: 4,
|
|
maxlength: 6,
|
|
match: /^[0-9]+$/
|
|
},
|
|
// Für sichere PIN-Speicherung (gehashed)
|
|
hashedPin: {
|
|
type: String,
|
|
required: true
|
|
},
|
|
points: {
|
|
total: {
|
|
type: Number,
|
|
default: 0,
|
|
min: 0
|
|
},
|
|
available: {
|
|
type: Number,
|
|
default: 0,
|
|
min: 0
|
|
},
|
|
spent: {
|
|
type: Number,
|
|
default: 0,
|
|
min: 0
|
|
}
|
|
},
|
|
level: {
|
|
current: {
|
|
type: Number,
|
|
default: 1,
|
|
min: 1
|
|
},
|
|
title: {
|
|
type: String,
|
|
default: 'Nachwuchs-Held'
|
|
}
|
|
},
|
|
achievements: [{
|
|
name: String,
|
|
description: String,
|
|
icon: String,
|
|
earnedAt: {
|
|
type: Date,
|
|
default: Date.now
|
|
}
|
|
}],
|
|
preferences: {
|
|
theme: {
|
|
type: String,
|
|
enum: ['colorful', 'nature', 'space', 'ocean', 'forest'],
|
|
default: 'colorful'
|
|
},
|
|
favoriteColor: {
|
|
type: String,
|
|
default: '#4CAF50'
|
|
},
|
|
soundEffects: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
animations: {
|
|
type: Boolean,
|
|
default: true
|
|
}
|
|
},
|
|
stats: {
|
|
tasksCompleted: {
|
|
type: Number,
|
|
default: 0
|
|
},
|
|
tasksCompletedToday: {
|
|
type: Number,
|
|
default: 0
|
|
},
|
|
streak: {
|
|
current: {
|
|
type: Number,
|
|
default: 0
|
|
},
|
|
longest: {
|
|
type: Number,
|
|
default: 0
|
|
},
|
|
lastCompletionDate: Date
|
|
}
|
|
},
|
|
isActive: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
createdAt: {
|
|
type: Date,
|
|
default: Date.now
|
|
},
|
|
lastActivity: {
|
|
type: Date,
|
|
default: Date.now
|
|
}
|
|
});
|
|
|
|
// Indizes für bessere Performance
|
|
childSchema.index({ parent: 1, isActive: 1 });
|
|
childSchema.index({ 'points.total': -1 });
|
|
childSchema.index({ username: 1 }, { unique: true });
|
|
|
|
// Methode zum Punkte hinzufügen
|
|
childSchema.methods.addPoints = function(points) {
|
|
this.points.total += points;
|
|
this.points.available += points;
|
|
this.lastActivity = new Date();
|
|
|
|
// Level-System aktualisieren
|
|
this.updateLevel();
|
|
|
|
return this.save();
|
|
};
|
|
|
|
// PIN-Hashing vor dem Speichern
|
|
childSchema.pre('save', async function(next) {
|
|
// Nur hashen wenn PIN geändert wurde
|
|
if (!this.isModified('pin')) return next();
|
|
|
|
try {
|
|
// PIN hashen
|
|
const salt = await bcrypt.genSalt(10);
|
|
this.hashedPin = await bcrypt.hash(this.pin, salt);
|
|
|
|
// Klartext-PIN entfernen (nicht in DB speichern)
|
|
this.pin = undefined;
|
|
next();
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// PIN-Verifikation
|
|
childSchema.methods.verifyPin = async function(pin) {
|
|
return await bcrypt.compare(pin, this.hashedPin);
|
|
};
|
|
|
|
// Sichere Daten für Frontend (ohne hashedPin)
|
|
childSchema.methods.toSafeObject = function() {
|
|
const obj = this.toObject();
|
|
delete obj.hashedPin;
|
|
delete obj.pin;
|
|
return obj;
|
|
};
|
|
|
|
// Methode zum Punkte ausgeben
|
|
childSchema.methods.spendPoints = function(points) {
|
|
if (this.points.available >= points) {
|
|
this.points.available -= points;
|
|
this.points.spent += points;
|
|
this.lastActivity = new Date();
|
|
return this.save();
|
|
}
|
|
throw new Error('Nicht genügend Punkte verfügbar');
|
|
};
|
|
|
|
// Level-System aktualisieren
|
|
childSchema.methods.updateLevel = function() {
|
|
const totalPoints = this.points.total;
|
|
const newLevel = Math.floor(totalPoints / 100) + 1; // Alle 100 Punkte ein Level
|
|
|
|
if (newLevel > this.level.current) {
|
|
this.level.current = newLevel;
|
|
this.level.title = this.getLevelTitle(newLevel);
|
|
}
|
|
};
|
|
|
|
// Level-Titel bestimmen
|
|
childSchema.methods.getLevelTitle = function(level) {
|
|
const titles = {
|
|
1: 'Nachwuchs-Held',
|
|
2: 'Aufgaben-Entdecker',
|
|
3: 'Fleißiger Helfer',
|
|
4: 'Verantwortungs-Champion',
|
|
5: 'Familien-Superheld',
|
|
6: 'Aufgaben-Meister',
|
|
7: 'Pflichten-Profi',
|
|
8: 'Haushalts-Held',
|
|
9: 'Verantwortungs-Legende',
|
|
10: 'Familien-Champion'
|
|
};
|
|
|
|
return titles[level] || `Super-Held Level ${level}`;
|
|
};
|
|
|
|
// Streak aktualisieren
|
|
childSchema.methods.updateStreak = function() {
|
|
const today = new Date();
|
|
today.setHours(0, 0, 0, 0);
|
|
|
|
const lastCompletion = this.stats.streak.lastCompletionDate;
|
|
|
|
if (!lastCompletion) {
|
|
// Erste Aufgabe
|
|
this.stats.streak.current = 1;
|
|
this.stats.streak.lastCompletionDate = today;
|
|
} else {
|
|
const lastDate = new Date(lastCompletion);
|
|
lastDate.setHours(0, 0, 0, 0);
|
|
|
|
const daysDiff = (today - lastDate) / (1000 * 60 * 60 * 24);
|
|
|
|
if (daysDiff === 1) {
|
|
// Aufeinanderfolgende Tage
|
|
this.stats.streak.current += 1;
|
|
this.stats.streak.lastCompletionDate = today;
|
|
} else if (daysDiff > 1) {
|
|
// Streak unterbrochen
|
|
this.stats.streak.current = 1;
|
|
this.stats.streak.lastCompletionDate = today;
|
|
}
|
|
// daysDiff === 0 bedeutet heute schon eine Aufgabe erledigt
|
|
}
|
|
|
|
// Längste Streak aktualisieren
|
|
if (this.stats.streak.current > this.stats.streak.longest) {
|
|
this.stats.streak.longest = this.stats.streak.current;
|
|
}
|
|
};
|
|
|
|
module.exports = mongoose.model('Child', childSchema); |