diff --git a/backend/src/controllers/gamification/api.js b/backend/src/controllers/gamification/api.js index 640a3bf..4191fd9 100644 --- a/backend/src/controllers/gamification/api.js +++ b/backend/src/controllers/gamification/api.js @@ -145,8 +145,24 @@ function claimChallenge(req, res) { res.json({ xp: c.xp_reward }); } +/* POST /api/gamification/self-award — начисление XP из учебника */ +function selfAward(req, res) { + const amount = Number(req.body.amount); + const source = String(req.body.source || ''); + if (!Number.isInteger(amount) || amount < 1 || amount > 50) { + return res.status(400).json({ error: 'amount должен быть целым числом от 1 до 50' }); + } + if (!/^[a-z0-9_-]{1,60}$/i.test(source)) { + return res.status(400).json({ error: 'source неверный формат (a-z, 0-9, _, -, до 60 символов)' }); + } + awardXP(req.user.id, amount, 'tb:' + source); + const info = getXPInfo(req.user.id); + res.json(info); +} + module.exports = { getMe, getFrames, setFrame, setGoalTier, getAchievements, getLeaderboard, getXPHistory, getChallenges, claimChallenge, + selfAward, }; diff --git a/backend/src/routes/gamification.js b/backend/src/routes/gamification.js index 6b207ac..5db65ee 100644 --- a/backend/src/routes/gamification.js +++ b/backend/src/routes/gamification.js @@ -5,13 +5,22 @@ const rateLimit = require('../middleware/rateLimit'); const { getMe, getAchievements, getLeaderboard, getXPHistory, getChallenges, claimChallenge, setGoalTier, getFrames, setFrame, - onLabExperiment, + onLabExperiment, selfAward, adminAward, adminReset, adminGamStats, adminGetUser } = require('../controllers/gamificationController'); const labLimiter = rateLimit({ windowMs: 60_000, max: 30, message: 'Слишком частые запросы лаборатории' }); const labSchema = { body: { reactionsDiscovered: { type: 'number', min: 0, max: 100, integer: true } } }; +const tbLimiter = rateLimit({ windowMs: 60_000, max: 30, message: 'Слишком частые начисления XP' }); +const selfAwardSchema = { + body: { + amount: { type: 'number', required: true, min: 1, max: 50, integer: true }, + source: { type: 'string', required: true, minLen: 1, maxLen: 60, + match: /^[a-z0-9_-]{1,60}$/i }, + }, +}; + router.use(authMiddleware); router.get('/me', getMe); @@ -24,6 +33,9 @@ router.post('/goal-tier', requirePermission('gamification.challenges'), setGoalT router.get('/frames', getFrames); router.post('/frame', requirePermission('shop.purchase'), setFrame); +/* Учебник — начисление XP от пользователя */ +router.post('/self-award', tbLimiter, validate(selfAwardSchema), selfAward); + /* Lab experiment tracking */ router.post('/lab-activity', requirePermission('simulations.access'), labLimiter, validate(labSchema), (req, res) => { const discovered = Number(req.body.reactionsDiscovered) || 0; diff --git a/frontend/textbooks/algebra_8.html b/frontend/textbooks/algebra_8.html index 382bdaa..9e69f88 100644 --- a/frontend/textbooks/algebra_8.html +++ b/frontend/textbooks/algebra_8.html @@ -12,6 +12,8 @@ + +