import { readBody } from 'h3' import { requireUserSession } from '../../utils/auth' import { LearnProfile } from '../../models/LearnProfile' import type { LearnConfig, LearnProgress, LessonProgress } from '~~/shared/learn/config' import { createDefaultLearnConfig } from '~~/shared/learn/config' interface LearnStateUpdateBody { xp?: number progress?: LearnProgress config?: Partial unlockedModules?: string[] } function sanitizeProgress(input: LearnProgress | undefined): LearnProgress | undefined { if (!input || typeof input !== 'object') { return undefined } const sanitized: LearnProgress = {} for (const [moduleId, lessons] of Object.entries(input)) { if (!lessons || typeof lessons !== 'object') { continue } const lessonProgress: Record = {} for (const [lessonId, value] of Object.entries(lessons as Record)) { if (!value || typeof value !== 'object') { continue } const bestRaw = (value as any).best const doneRaw = (value as any).done const best = typeof bestRaw === 'number' && Number.isFinite(bestRaw) ? Math.max(0, Math.min(100, Math.round(bestRaw))) : 0 const done = Boolean(doneRaw) lessonProgress[lessonId] = { best, done } } sanitized[moduleId] = lessonProgress } return sanitized } function sanitizeConfig(input: Partial | undefined): LearnConfig | undefined { if (!input || typeof input !== 'object') { return undefined } const config = createDefaultLearnConfig() if (typeof input.tts === 'boolean') { config.tts = input.tts } if (typeof input.audioChallenge === 'boolean') { config.audioChallenge = input.audioChallenge } if (typeof input.radioLevel === 'number' && Number.isFinite(input.radioLevel)) { const level = Math.round(input.radioLevel) config.radioLevel = Math.min(5, Math.max(1, level)) } if (typeof input.audioSpeed === 'number' && Number.isFinite(input.audioSpeed)) { const rounded = Math.round(input.audioSpeed * 20) / 20 config.audioSpeed = Math.min(1.3, Math.max(0.7, rounded)) } if (typeof input.voice === 'string') { config.voice = input.voice.slice(0, 120) } return config } function sanitizeUnlockedModules(input: string[] | undefined): string[] | undefined { if (!Array.isArray(input)) { return undefined } const sanitized = input .filter(value => typeof value === 'string') .map(value => value.trim()) .filter(value => value.length > 0) return Array.from(new Set(sanitized)) } export default defineEventHandler(async (event) => { const user = await requireUserSession(event) const body = await readBody(event) let profile = await LearnProfile.findOne({ user: user._id }) if (!profile) { profile = new LearnProfile({ user: user._id }) } if (typeof body.xp === 'number' && Number.isFinite(body.xp)) { profile.xp = Math.max(0, Math.round(body.xp)) } const progress = sanitizeProgress(body.progress) if (progress !== undefined) { profile.progress = progress } const config = sanitizeConfig(body.config) if (config !== undefined) { profile.config = config } const unlockedModules = sanitizeUnlockedModules(body.unlockedModules) if (unlockedModules !== undefined) { profile.unlockedModules = unlockedModules } await profile.save() const responseConfig = { ...createDefaultLearnConfig(), ...(profile.config || {}) } const responseProgress = profile.progress && typeof profile.progress === 'object' ? JSON.parse(JSON.stringify(profile.progress)) : {} const responseUnlockedModules = Array.isArray(profile.unlockedModules) ? Array.from(new Set((profile.unlockedModules as unknown[]).filter(item => typeof item === 'string').map(item => item.trim()))) : [] return { xp: typeof profile.xp === 'number' ? profile.xp : 0, progress: responseProgress, config: responseConfig, unlockedModules: responseUnlockedModules, } })