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
477 lines
12 KiB
JavaScript
477 lines
12 KiB
JavaScript
const express = require('express');
|
|
const { body, validationResult } = require('express-validator');
|
|
const Child = require('../models/Child');
|
|
const { auth } = require('../middleware/auth');
|
|
|
|
const router = express.Router();
|
|
|
|
// @route GET /api/children
|
|
// @desc Alle Kinder des angemeldeten Elternteils abrufen
|
|
// @access Private
|
|
router.get('/', auth, async (req, res) => {
|
|
try {
|
|
const children = await Child.find({
|
|
parent: req.user.id,
|
|
isActive: true
|
|
}).sort({ createdAt: 1 });
|
|
|
|
// Sichere Daten für Frontend (ohne hashedPin)
|
|
const safeChildren = children.map(child => child.toSafeObject());
|
|
|
|
res.json({
|
|
success: true,
|
|
count: safeChildren.length,
|
|
children: safeChildren
|
|
});
|
|
} catch (error) {
|
|
console.error('Kinder-Abruf-Fehler:', error.message);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Server-Fehler beim Abrufen der Kinder'
|
|
});
|
|
}
|
|
});
|
|
|
|
// @route GET /api/children/:id
|
|
// @desc Einzelnes Kind abrufen
|
|
// @access Private
|
|
router.get('/:id', auth, async (req, res) => {
|
|
try {
|
|
const child = await Child.findOne({
|
|
_id: req.params.id,
|
|
parent: req.user.id,
|
|
isActive: true
|
|
});
|
|
|
|
if (!child) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Kind nicht gefunden'
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
child: child.toSafeObject()
|
|
});
|
|
} catch (error) {
|
|
console.error('Kind-Abruf-Fehler:', error.message);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Server-Fehler beim Abrufen des Kindes'
|
|
});
|
|
}
|
|
});
|
|
|
|
// @route POST /api/children
|
|
// @desc Neues Kind erstellen
|
|
// @access Private
|
|
router.post('/', [
|
|
auth,
|
|
body('name')
|
|
.trim()
|
|
.isLength({ min: 2, max: 50 })
|
|
.withMessage('Name muss zwischen 2 und 50 Zeichen lang sein'),
|
|
body('age')
|
|
.isInt({ min: 3, max: 18 })
|
|
.withMessage('Alter muss zwischen 3 und 18 Jahren liegen'),
|
|
body('username')
|
|
.trim()
|
|
.isLength({ min: 3, max: 20 })
|
|
.withMessage('Benutzername muss zwischen 3 und 20 Zeichen lang sein')
|
|
.matches(/^[a-zA-Z0-9_]+$/)
|
|
.withMessage('Benutzername darf nur Buchstaben, Zahlen und Unterstriche enthalten'),
|
|
body('pin')
|
|
.isLength({ min: 4, max: 6 })
|
|
.withMessage('PIN muss zwischen 4 und 6 Zeichen lang sein')
|
|
.matches(/^[0-9]+$/)
|
|
.withMessage('PIN darf nur Zahlen enthalten'),
|
|
body('avatar')
|
|
.optional()
|
|
.isIn([
|
|
'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'
|
|
])
|
|
.withMessage('Ungültiger Avatar ausgewählt')
|
|
], async (req, res) => {
|
|
try {
|
|
// Validierungsfehler prüfen
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Validierungsfehler',
|
|
errors: errors.array()
|
|
});
|
|
}
|
|
|
|
const { name, age, username, pin, avatar, preferences } = req.body;
|
|
|
|
// Prüfen ob bereits ein Kind mit diesem Namen existiert
|
|
const existingChild = await Child.findOne({
|
|
parent: req.user.id,
|
|
name: name.trim(),
|
|
isActive: true
|
|
});
|
|
|
|
if (existingChild) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Ein Kind mit diesem Namen existiert bereits'
|
|
});
|
|
}
|
|
|
|
// Prüfen ob Benutzername bereits existiert
|
|
const existingUsername = await Child.findOne({
|
|
username: username.toLowerCase(),
|
|
isActive: true
|
|
});
|
|
|
|
if (existingUsername) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Dieser Benutzername ist bereits vergeben'
|
|
});
|
|
}
|
|
|
|
// Neues Kind erstellen
|
|
const child = new Child({
|
|
name: name.trim(),
|
|
age,
|
|
username: username.toLowerCase(),
|
|
pin, // Wird automatisch gehashed durch pre-save Hook
|
|
avatar: avatar || 'default-child.png',
|
|
parent: req.user.id,
|
|
preferences: preferences || {}
|
|
});
|
|
|
|
await child.save();
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
message: `${name} wurde erfolgreich als neuer Familien-Held hinzugefügt! 🎉\nBenutzername: ${username}\nPIN: Sicher gespeichert`,
|
|
child: child.toSafeObject()
|
|
});
|
|
} catch (error) {
|
|
console.error('Kind-Erstellungs-Fehler:', error.message);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Server-Fehler beim Erstellen des Kindes'
|
|
});
|
|
}
|
|
});
|
|
|
|
// @route PUT /api/children/:id
|
|
// @desc Kind aktualisieren
|
|
// @access Private
|
|
router.put('/:id', [
|
|
auth,
|
|
body('name')
|
|
.optional()
|
|
.trim()
|
|
.isLength({ min: 2, max: 50 })
|
|
.withMessage('Name muss zwischen 2 und 50 Zeichen lang sein'),
|
|
body('age')
|
|
.optional()
|
|
.isInt({ min: 3, max: 18 })
|
|
.withMessage('Alter muss zwischen 3 und 18 Jahren liegen'),
|
|
body('avatar')
|
|
.optional()
|
|
.isIn([
|
|
'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'
|
|
])
|
|
.withMessage('Ungültiger Avatar ausgewählt')
|
|
], async (req, res) => {
|
|
try {
|
|
// Validierungsfehler prüfen
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Validierungsfehler',
|
|
errors: errors.array()
|
|
});
|
|
}
|
|
|
|
const { name, age, avatar, preferences } = req.body;
|
|
const updateFields = {};
|
|
|
|
if (name) updateFields.name = name.trim();
|
|
if (age) updateFields.age = age;
|
|
if (avatar) updateFields.avatar = avatar;
|
|
if (preferences) updateFields.preferences = { ...preferences };
|
|
|
|
const child = await Child.findOneAndUpdate(
|
|
{ _id: req.params.id, parent: req.user.id, isActive: true },
|
|
{ $set: updateFields },
|
|
{ new: true, runValidators: true }
|
|
);
|
|
|
|
if (!child) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Kind nicht gefunden'
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
message: `${child.name}s Profil wurde erfolgreich aktualisiert! ✅`,
|
|
child
|
|
});
|
|
} catch (error) {
|
|
console.error('Kind-Update-Fehler:', error.message);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Server-Fehler beim Aktualisieren des Kindes'
|
|
});
|
|
}
|
|
});
|
|
|
|
// @route DELETE /api/children/:id
|
|
// @desc Kind deaktivieren (soft delete)
|
|
// @access Private
|
|
router.delete('/:id', auth, async (req, res) => {
|
|
try {
|
|
const child = await Child.findOneAndUpdate(
|
|
{ _id: req.params.id, parent: req.user.id, isActive: true },
|
|
{ $set: { isActive: false } },
|
|
{ new: true }
|
|
);
|
|
|
|
if (!child) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Kind nicht gefunden'
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
message: `${child.name} wurde aus der Familie entfernt`
|
|
});
|
|
} catch (error) {
|
|
console.error('Kind-Löschungs-Fehler:', error.message);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Server-Fehler beim Entfernen des Kindes'
|
|
});
|
|
}
|
|
});
|
|
|
|
// @route GET /api/children/:id/stats
|
|
// @desc Statistiken für ein Kind abrufen
|
|
// @access Private
|
|
router.get('/:id/stats', auth, async (req, res) => {
|
|
try {
|
|
const child = await Child.findOne({
|
|
_id: req.params.id,
|
|
parent: req.user.id,
|
|
isActive: true
|
|
});
|
|
|
|
if (!child) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Kind nicht gefunden'
|
|
});
|
|
}
|
|
|
|
// Zusätzliche Statistiken berechnen
|
|
const Task = require('../models/Task');
|
|
const { RewardRequest } = require('../models/Reward');
|
|
|
|
const [completedTasks, pendingTasks, rewardRequests] = await Promise.all([
|
|
Task.countDocuments({ assignedTo: child._id, status: 'approved' }),
|
|
Task.countDocuments({ assignedTo: child._id, status: 'pending' }),
|
|
RewardRequest.countDocuments({ child: child._id })
|
|
]);
|
|
|
|
const stats = {
|
|
points: child.points,
|
|
level: child.level,
|
|
achievements: child.achievements,
|
|
tasks: {
|
|
completed: completedTasks,
|
|
pending: pendingTasks,
|
|
completedToday: child.stats.tasksCompletedToday
|
|
},
|
|
streak: child.stats.streak,
|
|
rewards: {
|
|
totalRequests: rewardRequests
|
|
},
|
|
memberSince: child.createdAt,
|
|
lastActivity: child.lastActivity
|
|
};
|
|
|
|
res.json({
|
|
success: true,
|
|
stats
|
|
});
|
|
} catch (error) {
|
|
console.error('Kind-Statistik-Fehler:', error.message);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Server-Fehler beim Abrufen der Statistiken'
|
|
});
|
|
}
|
|
});
|
|
|
|
// @route POST /api/children/:id/points
|
|
// @desc Punkte manuell hinzufügen/abziehen (für Eltern)
|
|
// @access Private
|
|
router.post('/:id/points', [
|
|
auth,
|
|
body('points')
|
|
.isInt({ min: -1000, max: 1000 })
|
|
.withMessage('Punkte müssen zwischen -1000 und 1000 liegen'),
|
|
body('reason')
|
|
.trim()
|
|
.isLength({ min: 3, max: 200 })
|
|
.withMessage('Grund muss zwischen 3 und 200 Zeichen lang sein')
|
|
], async (req, res) => {
|
|
try {
|
|
// Validierungsfehler prüfen
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Validierungsfehler',
|
|
errors: errors.array()
|
|
});
|
|
}
|
|
|
|
const { points, reason } = req.body;
|
|
|
|
const child = await Child.findOne({
|
|
_id: req.params.id,
|
|
parent: req.user.id,
|
|
isActive: true
|
|
});
|
|
|
|
if (!child) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Kind nicht gefunden'
|
|
});
|
|
}
|
|
|
|
// Punkte hinzufügen oder abziehen
|
|
if (points > 0) {
|
|
await child.addPoints(points);
|
|
} else {
|
|
// Negative Punkte (Abzug)
|
|
const absPoints = Math.abs(points);
|
|
if (child.points.available < absPoints) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Nicht genügend Punkte zum Abziehen verfügbar'
|
|
});
|
|
}
|
|
child.points.available -= absPoints;
|
|
child.points.total -= absPoints;
|
|
await child.save();
|
|
}
|
|
|
|
const action = points > 0 ? 'hinzugefügt' : 'abgezogen';
|
|
const emoji = points > 0 ? '🎉' : '⚠️';
|
|
|
|
res.json({
|
|
success: true,
|
|
message: `${Math.abs(points)} Punkte wurden ${child.name} ${action}! ${emoji}`,
|
|
reason,
|
|
child: {
|
|
id: child._id,
|
|
name: child.name,
|
|
points: child.points,
|
|
level: child.level
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Punkte-Änderungs-Fehler:', error.message);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Server-Fehler beim Ändern der Punkte'
|
|
});
|
|
}
|
|
});
|
|
|
|
// @route POST /api/children/:id/achievements
|
|
// @desc Achievement hinzufügen
|
|
// @access Private
|
|
router.post('/:id/achievements', [
|
|
auth,
|
|
body('name')
|
|
.trim()
|
|
.isLength({ min: 3, max: 50 })
|
|
.withMessage('Achievement-Name muss zwischen 3 und 50 Zeichen lang sein'),
|
|
body('description')
|
|
.trim()
|
|
.isLength({ min: 5, max: 200 })
|
|
.withMessage('Beschreibung muss zwischen 5 und 200 Zeichen lang sein'),
|
|
body('icon')
|
|
.optional()
|
|
.trim()
|
|
.isLength({ min: 1, max: 20 })
|
|
.withMessage('Icon muss zwischen 1 und 20 Zeichen lang sein')
|
|
], async (req, res) => {
|
|
try {
|
|
// Validierungsfehler prüfen
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Validierungsfehler',
|
|
errors: errors.array()
|
|
});
|
|
}
|
|
|
|
const { name, description, icon } = req.body;
|
|
|
|
const child = await Child.findOne({
|
|
_id: req.params.id,
|
|
parent: req.user.id,
|
|
isActive: true
|
|
});
|
|
|
|
if (!child) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Kind nicht gefunden'
|
|
});
|
|
}
|
|
|
|
// Achievement hinzufügen
|
|
const achievement = {
|
|
name: name.trim(),
|
|
description: description.trim(),
|
|
icon: icon || '🏆',
|
|
earnedAt: new Date()
|
|
};
|
|
|
|
child.achievements.push(achievement);
|
|
await child.save();
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
message: `${child.name} hat ein neues Achievement erhalten: ${name}! 🏆`,
|
|
achievement,
|
|
child: {
|
|
id: child._id,
|
|
name: child.name,
|
|
achievements: child.achievements
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Achievement-Hinzufügungs-Fehler:', error.message);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Server-Fehler beim Hinzufügen des Achievements'
|
|
});
|
|
}
|
|
});
|
|
|
|
module.exports = router; |