'use strict'; /* ─────────────────────────────────────────────────────────────────────────── systemReset.js — общая логика «чистого запуска» (используют и CLI backend/scripts/reset-system.js, и админ-эндпоинт POST /api/admin/reset-system). ⚠️ ДЕСТРУКТИВНО. Перед вызовом runReset ОБЯЗАТЕЛЬНО сделать бэкап БД. Идея: сохранить ОДНОГО админа, переназначить ему авторский контент, стереть всех остальных пользователей + всю активность/организацию, сохранить контент/конфиг. Классифицируем ВСЕ таблицы; неизвестные НЕ трогаем. ─────────────────────────────────────────────────────────────────────────── */ /* Контент-таблицы: владелец переписывается на сохранённого админа (колонка у каждой своя). */ const REASSIGN = { courses: 'created_by', tests: 'created_by', flashcard_decks: 'user_id', custom_sims: 'owner_id', course_templates: 'created_by', lesson_templates: 'created_by', assignment_templates: 'created_by', lab_sim_links: 'created_by', classroom_templates: 'teacher_id', folders: 'created_by', files: 'uploaded_by', }; /* Активность/организация — полностью очищается. */ const WIPE = new Set([ 'test_sessions', 'session_questions', 'user_answers', 'exam_attempts', 'exam_mock_sessions', 'exam_user_plan', 'assignments', 'assignment_sessions', 'assignment_completion', 'submissions', 'submission_log', 'classes', 'class_members', 'class_courses', 'classroom_sessions', 'classroom_attendance', 'classroom_chat', 'classroom_chat_reactions', 'classroom_draw_permissions', 'classroom_hands', 'classroom_invites', 'classroom_muted', 'classroom_notes', 'classroom_pages', 'classroom_strokes', 'live_sessions', 'live_answers', 'content_access', 'xp_log', 'coin_log', 'user_achievements', 'daily_goals', 'challenges', 'user_purchases', 'notifications', 'parent_notifications', 'parent_links', 'student_materials', 'material_collections', 'game_progress', 'lesson_progress', 'lesson_comments', 'lesson_notes', 'textbook_progress', 'textbook_bookmarks', 'bookmarks', 'flashcard_reviews', 'flashcard_deck_access', 'bio_user_challenges', 'bio_user_molecules', 'bio_user_pathway', 'rb_user_collection', 'rb_user_quests', 'rb_sightings', 'assistant_seen', 'assistant_memory', 'assistant_feedback', 'assistant_usage', 'assistant_cache', 'imggen_usage', 'folder_access', 'file_access', 'avatar_requests', 'geometry_submissions', 'geometry_tasks', 'security_events', 'error_log', 'admin_audit_log', 'student_prep', 'announcements', 'teacher_students', 'user_permissions', 'user_preferences', ]); /* Контент/конфиг — НЕ трогаем (явный список, чтобы ловить «неизвестные» таблицы). */ const KEEP = new Set([ 'subjects', 'questions', 'options', 'topics', 'textbooks', 'textbook_chunks', 'lessons', 'lesson_blocks', 'course_sections', 'exam_tasks', 'exam_topics', 'exam_tracks', 'exam9_variant_tests', 'test_questions', 'flashcard_cards', 'lab_sims', 'bio_challenges', 'bio_elements', 'bio_molecules', 'bio_pathways', 'bio_reactions', 'rb_food_web', 'rb_groups', 'rb_habitats', 'rb_population_data', 'rb_quests', 'rb_species', 'rb_species_regions', 'shop_items', 'achievements', 'roles', 'role_permissions', 'app_settings', '_migrations', ]); const ADMIN_RESET_SQL = `UPDATE users SET xp = 0, level = 1, coins = 0, streak_current = 0, streak_best = 0, streak_date = NULL, goal_tier = 0, lab_experiments = 0, lab_reactions = 0, pet_petting_streak = 0 WHERE id = ?`; function allTables(db) { return db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name") .all().map(r => r.name); } function rowCount(db, t) { try { return db.prepare(`SELECT COUNT(*) c FROM "${t}"`).get().c; } catch { return null; } } /** Кандидат-админ по умолчанию (минимальный id). null если админов нет. */ function pickKeptAdmin(db) { return db.prepare("SELECT id, email, name FROM users WHERE role = 'admin' ORDER BY id LIMIT 1").get() || null; } /** План (без изменений): что переназначится / сотрётся / неизвестно. */ function classify(db) { const tables = allTables(db); const reassign = Object.entries(REASSIGN).map(([table, col]) => ({ table, col, rows: rowCount(db, table) })); const wipe = [...WIPE].map(table => ({ table, rows: rowCount(db, table) })); const unknown = tables.filter(t => t !== 'users' && !REASSIGN[t] && !WIPE.has(t) && !KEEP.has(t)); const totalUsers = rowCount(db, 'users'); const wipeRows = wipe.reduce((a, w) => a + (typeof w.rows === 'number' ? w.rows : 0), 0); return { reassign, wipe, unknown, keepCount: KEEP.size, totalUsers, wipeRows }; } /** * Выполнить сброс. db — экземпляр node:sqlite DatabaseSync. keptAdminId — id админа, * которого сохраняем (ему переназначается контент). Возвращает сводку. * ⚠️ Бэкап делает ВЫЗЫВАЮЩИЙ код ДО вызова. */ function runReset(db, keptAdminId) { const admin = db.prepare("SELECT id, role FROM users WHERE id = ?").get(keptAdminId); if (!admin || admin.role !== 'admin') throw new Error('keptAdminId не является админом'); db.exec('PRAGMA foreign_keys = OFF'); // управляем удалением вручную, детерминированно db.exec('BEGIN'); try { for (const [t, col] of Object.entries(REASSIGN)) { try { db.prepare(`UPDATE "${t}" SET "${col}" = ? WHERE "${col}" IS NOT NULL AND "${col}" != ?`).run(keptAdminId, keptAdminId); } catch { /* нет таблицы/колонки — пропуск */ } } for (const t of WIPE) { try { db.prepare(`DELETE FROM "${t}"`).run(); } catch { /* нет таблицы — пропуск */ } } const del = db.prepare('DELETE FROM users WHERE id != ?').run(keptAdminId); db.prepare(ADMIN_RESET_SQL).run(keptAdminId); db.exec('COMMIT'); var deletedUsers = del.changes; } catch (e) { db.exec('ROLLBACK'); db.exec('PRAGMA foreign_keys = ON'); throw e; } db.exec('PRAGMA foreign_keys = ON'); let fkBad = 0; try { fkBad = db.prepare('PRAGMA foreign_key_check').all().length; } catch {} try { db.exec('VACUUM'); } catch {} return { ok: true, keptAdminId, deletedUsers, remainingUsers: rowCount(db, 'users'), fkDangling: fkBad, kept: { textbooks: rowCount(db, 'textbooks'), questions: rowCount(db, 'questions'), tests: rowCount(db, 'tests'), courses: rowCount(db, 'courses'), exam_tasks: rowCount(db, 'exam_tasks'), }, }; } module.exports = { REASSIGN, WIPE, KEEP, classify, pickKeptAdmin, runReset };