| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558 |
- 'use strict';
- // ---------- ДАННЫЕ ----------
- const ACTS = {
- 1: {
- title: 'Акт I. Идентификация',
- scenes: [
- {
- system: 'Приветствую. Для начала работы мне нужно понять, кто вы. Представьтесь, пожалуйста.',
- choices: [
- { text: 'Назвать свое имя: «Я — Алексей Иванов, стажер».', correct: true,
- feedback: 'Принято. Алексей Иванов. Я зафиксировала ваше имя в журнале посещений.',
- edu: 'ИДЕНТИФИКАЦИЯ — это присвоение субъекту доступа уникального имени (логина, ID, номера пропуска). Это заявление: «Я — Вася». Важно: На этом этапе система еще не доверяет вам, она просто знает, как вас зовут.'
- },
- { text: 'Попытаться пройти молча.', correct: false,
- feedback: 'Ошибка. Идентификация не пройдена. Я не могу работать с анонимом. Доступ запрещен.'
- },
- { text: 'Сказать: «Я — директор банка».', correct: false,
- feedback: 'Стоп. Вы назвались директором, но в базе нет такого стажера. Это ложная идентификация. Вызов охраны!'
- }
- ]
- }
- ]
- },
- 2: {
- title: 'Акт II. Аутентификация',
- scenes: [
- {
- system: 'Алексей, я вижу ваше имя, но мне нужно убедиться, что вы — это действительно вы. У вас есть 3 попытки. Ваш ключ доступа — это дата вашего рождения + код из СМС.',
- choices: [
- { text: 'Ввести пароль, который вы придумали сами (например, «12345»).', correct: false,
- feedback: 'Проверка... Пароль слишком простой. Взломщик подобрал его за 2 секунды. Это плохая аутентификация. Доступ заблокирован.'
- },
- { text: 'Приложить отпечаток пальца к сканеру (Биометрия).', correct: true,
- feedback: 'Отпечаток совпадает с базой. Отлично! Вы доказали, что вы — это вы. Процесс АУТЕНТИФИКАЦИИ пройден. Теперь я доверяю этому сеансу.',
- edu: 'АУТЕНТИФИКАЦИЯ — это проверка подлинности. Доказательство того, что субъект действительно тот, за кого себя выдает. Факторы аутентификации: Знание (пароль, PIN). Владение (токен, телефон, смарт-карта). Биометрия (палец, лицо, сетчатка).'
- },
- { text: 'Попросить коллегу приложить свой пропуск.', correct: false,
- feedback: 'Чужой пропуск? Это попытка обмана. Аутентификация не пройдена.'
- }
- ]
- }
- ]
- },
- 3: {
- title: 'Акт III. Авторизация',
- scenes: [
- {
- system: 'Алексей, вы успешно вошли. Но теперь мне нужно определить ваши права. Кто вы в системе?',
- choices: [
- { text: 'Администратор (все права).', correct: false,
- feedback: 'Попытка повышения привилегий! Вы стажер, но пытаетесь получить права админа. Это нарушение политики безопасности. Инцидент записан в лог.'
- },
- { text: 'Кассир (доступ к деньгам).', correct: false,
- feedback: 'Кассир? Ваша роль — стажер. Несоответствие прав. Доступ к деньгам запрещен.'
- },
- { text: 'Стажер (только просмотр).', correct: true,
- feedback: 'Роль подтверждена. Вам разрешен только просмотр курса валют. Кнопка перевода денег для вас неактивна (серая). Это АВТОРИЗАЦИЯ.',
- edu: 'АВТОРИЗАЦИЯ — это предоставление субъекту прав на доступ к ресурсам и определение разрешенных действий. Простыми словами: Аутентификация говорит «Ты — Вася», а Авторизация говорит «Васе можно только читать, но нельзя писать».'
- }
- ]
- },
- {
- system: 'Вдруг экран мигает. Появляется «Тень»: «Я украл логин Алексея (Идентификация) и подобрал пароль (Аутентификация). Теперь я войду в систему!»',
- choices: [
- { text: 'Ничего, он войдет.', correct: false,
- feedback: 'Нет. Даже зная пароль, без второго фактора Хакер не пройдет Аутентификацию.'
- },
- { text: 'Двухфакторная аутентификация (2FA).', correct: true,
- feedback: 'Верно! Даже зная пароль, без второго фактора (СМС или отпечатка) Хакер не пройдет Аутентификацию. А если бы и прошел — Авторизация ограничила бы его права только просмотром, и он не смог бы украсть деньги.',
- edu: 'Двухфакторная аутентификация (2FA) — метод защиты, при котором для входа требуется два разных фактора: например, пароль (знание) и код из СМС (владение). Это значительно усложняет взлом.'
- },
- { text: 'Авторизация.', correct: false,
- feedback: 'Авторизация происходит после аутентификации. Но без аутентификации хакер не получит даже прав.'
- }
- ]
- }
- ]
- }
- };
- const QUIZ = [
- {
- q: 'Что такое идентификация?',
- options: [
- 'Присвоение субъекту уникального имени (логина, ID)',
- 'Проверка подлинности субъекта',
- 'Предоставление прав доступа к ресурсам',
- 'Шифрование данных'
- ],
- correct: 0,
- explain: 'Идентификация — это заявление «Я — Вася». Система просто знает, как вас зовут.'
- },
- {
- q: 'Какой из перечисленных факторов НЕ относится к аутентификации?',
- options: [
- 'Знание (пароль, PIN)',
- 'Владение (токен, телефон)',
- 'Биометрия (отпечаток пальца)',
- 'Роль в системе (стажёр, администратор)'
- ],
- correct: 3,
- explain: 'Роль в системе — это авторизация. Аутентификация проверяет, что вы — это вы.'
- },
- {
- q: 'Что означает авторизация?',
- options: [
- 'Проверка подлинности пользователя',
- 'Присвоение пользователю уникального имени',
- 'Предоставление прав на доступ к ресурсам и определение разрешённых действий',
- 'Двухфакторная проверка'
- ],
- correct: 2,
- explain: 'Авторизация говорит: «Васе можно только читать, но нельзя писать».'
- },
- {
- q: 'Хакер украл логин и пароль. Что помешает ему войти в систему?',
- options: [
- 'Ничего, он войдет',
- 'Двухфакторная аутентификация (2FA)',
- 'Авторизация',
- 'Идентификация'
- ],
- correct: 1,
- explain: '2FA требует второй фактор (СМС, отпечаток), которого у хакера нет.'
- },
- {
- q: 'В какой последовательности происходит доступ к защищённой системе?',
- options: [
- 'Авторизация → Аутентификация → Идентификация',
- 'Идентификация → Аутентификация → Авторизация',
- 'Аутентификация → Идентификация → Авторизация',
- 'Идентификация → Авторизация → Аутентификация'
- ],
- correct: 1,
- explain: 'Сначала система узнаёт имя (идентификация), затем проверяет подлинность (аутентификация), затем даёт права (авторизация).'
- }
- ];
- // ---------- СОСТОЯНИЕ ----------
- const state = {
- screen: 'title',
- act: 1,
- scene: 0,
- actCompleted: { 1: false, 2: false, 3: false },
- quizAnswers: [],
- quizIndex: 0,
- quizScore: 0,
- quizDone: false,
- sound: true,
- voice: true,
- journal: [],
- audioCtx: null,
- };
- // ---------- DOM ----------
- const $ = (sel, ctx = document) => ctx.querySelector(sel);
- const $$ = (sel, ctx = document) => [...ctx.querySelectorAll(sel)];
- const hud = $('#hud');
- const hudAct = $('#hudAct');
- const hudFill = $('#hudFill');
- const tSound = $('#tSound');
- const tVoice = $('#tVoice');
- const tJournal = $('#tJournal');
- const journal = $('#journal');
- const journalBody = $('#journalBody');
- const jClose = $('#jClose');
- const scrim = $('#scrim');
- const screens = {
- title: $('#s-title'),
- brief: $('#s-brief'),
- act1: $('#s-act1'),
- act2: $('#s-act2'),
- act3: $('#s-act3'),
- final: $('#s-final'),
- quiz: $('#s-quiz'),
- result: $('#s-result'),
- };
- // ---------- УТИЛИТЫ ----------
- function showScreen(id) {
- Object.values(screens).forEach(s => s.classList.remove('is-active'));
- screens[id].classList.add('is-active');
- state.screen = id;
- hud.hidden = (id === 'title');
- updateHUD();
- window.scrollTo(0, 0);
- }
- function updateHUD() {
- let label = `АКТ ${state.act}`;
- if (state.screen === 'quiz') label = 'ТЕСТ';
- if (state.screen === 'result') label = 'ИТОГ';
- if (state.screen === 'final') label = 'ФИНАЛ';
- hudAct.textContent = label;
- const total = 4; // 3 акта + тест
- let completed = 0;
- if (state.actCompleted[1]) completed++;
- if (state.actCompleted[2]) completed++;
- if (state.actCompleted[3]) completed++;
- if (state.quizDone) completed++;
- const pct = (completed / total) * 100;
- hudFill.style.width = pct + '%';
- }
- function logJournal(text, type = 'info') {
- state.journal.push({ text, type, time: new Date().toLocaleTimeString() });
- if (state.journal.length > 50) state.journal.shift();
- renderJournal();
- }
- function renderJournal() {
- if (state.journal.length === 0) {
- journalBody.innerHTML = '<p class="journal__empty">Записей пока нет.</p>';
- return;
- }
- journalBody.innerHTML = state.journal.map(e => {
- const cls = e.type === 'ok' ? 'journal__entry--ok' : e.type === 'err' ? 'journal__entry--err' : '';
- return `<div class="journal__entry ${cls}">[${e.time}] ${e.text}</div>`;
- }).join('');
- journalBody.scrollTop = journalBody.scrollHeight;
- }
- // ---------- ЗВУК ----------
- function ensureAudio() {
- if (!state.audioCtx) {
- state.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
- }
- if (state.audioCtx.state === 'suspended') state.audioCtx.resume();
- }
- function playTone(freq, duration = 0.15, type = 'sine') {
- if (!state.sound) return;
- ensureAudio();
- const ctx = state.audioCtx;
- const osc = ctx.createOscillator();
- const gain = ctx.createGain();
- osc.type = type;
- osc.frequency.value = freq;
- gain.gain.setValueAtTime(0.15, ctx.currentTime);
- gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + duration);
- osc.connect(gain).connect(ctx.destination);
- osc.start();
- osc.stop(ctx.currentTime + duration);
- }
- function speak(text) {
- if (!state.voice || !window.speechSynthesis) return;
- window.speechSynthesis.cancel();
- const utter = new SpeechSynthesisUtterance(text);
- utter.lang = 'ru-RU';
- utter.rate = 1;
- utter.pitch = 1;
- window.speechSynthesis.speak(utter);
- }
- // ---------- РЕНДЕР АКТОВ ----------
- function renderActScreen(screenId, actNum, sceneIndex) {
- const act = ACTS[actNum];
- const scene = act.scenes[sceneIndex];
- const screen = screens[`act${actNum}`];
- screen.innerHTML = `
- <div class="page">
- <div class="act-head">
- <span class="act-num">Акт ${actNum}</span>
- <h2>${act.title}</h2>
- </div>
- <div class="scene">
- <div class="system-bubble">
- <div class="system-avatar">С</div>
- <div class="system-text" id="sysText">${scene.system}</div>
- </div>
- <div class="choices" id="choices">
- ${scene.choices.map((c, i) => `
- <button class="choice" data-index="${i}">${c.text}</button>
- `).join('')}
- </div>
- <div class="feedback" id="feedback" hidden></div>
- <div class="edu" id="eduBlock" hidden>
- <div class="note">
- <div class="note__head"><span class="note__mark">§</span>Обучающий блок</div>
- <div class="note__body" id="eduText"></div>
- </div>
- <button class="btn" id="continueBtn">Продолжить</button>
- </div>
- </div>
- </div>
- `;
- screen.querySelectorAll('.choice').forEach(btn => {
- btn.addEventListener('click', () => {
- handleChoice(actNum, sceneIndex, parseInt(btn.dataset.index));
- });
- });
- speak(scene.system);
- }
- function handleChoice(actNum, sceneIndex, choiceIndex) {
- const act = ACTS[actNum];
- const scene = act.scenes[sceneIndex];
- const choice = scene.choices[choiceIndex];
- const screen = screens[`act${actNum}`];
- const sysText = screen.querySelector('#sysText');
- const choicesDiv = screen.querySelector('#choices');
- const feedbackDiv = screen.querySelector('#feedback');
- const eduBlock = screen.querySelector('#eduBlock');
- if (choice.correct) {
- choicesDiv.querySelectorAll('.choice').forEach(b => b.disabled = true);
- sysText.textContent = choice.feedback;
- playTone(880, 0.12, 'sine');
- logJournal(`✅ ${choice.feedback}`, 'ok');
- if (choice.edu) {
- eduBlock.hidden = false;
- screen.querySelector('#eduText').innerHTML = choice.edu;
- speak(choice.edu);
- const contBtn = screen.querySelector('#continueBtn');
- contBtn.addEventListener('click', () => nextScene());
- } else {
- feedbackDiv.hidden = false;
- feedbackDiv.innerHTML = `<button class="btn" id="continueBtn">Продолжить</button>`;
- feedbackDiv.querySelector('#continueBtn').addEventListener('click', () => nextScene());
- }
- } else {
- playTone(220, 0.25, 'sawtooth');
- sysText.textContent = choice.feedback;
- speak(choice.feedback);
- logJournal(`❌ ${choice.feedback}`, 'err');
- choicesDiv.querySelectorAll('.choice').forEach(b => b.disabled = true);
- feedbackDiv.hidden = false;
- feedbackDiv.innerHTML = `<button class="btn btn--small" id="retryBtn">Попробовать снова</button>`;
- feedbackDiv.querySelector('#retryBtn').addEventListener('click', () => {
- renderActScreen(`s-act${actNum}`, actNum, sceneIndex);
- });
- }
- }
- function nextScene() {
- const actNum = state.act;
- const act = ACTS[actNum];
- if (state.scene < act.scenes.length - 1) {
- state.scene++;
- renderActScreen(`s-act${actNum}`, actNum, state.scene);
- } else {
- state.actCompleted[actNum] = true;
- updateHUD();
- if (actNum < 3) {
- state.act = actNum + 1;
- state.scene = 0;
- showScreen(`act${state.act}`);
- renderActScreen(`s-act${state.act}`, state.act, 0);
- } else {
- showFinal();
- }
- }
- }
- // ---------- ФИНАЛЬНЫЙ ЭКРАН (ИТОГИ) ----------
- function showFinal() {
- showScreen('final');
- screens.final.innerHTML = `
- <div class="page">
- <div class="act-head">
- <span class="act-num">Финал</span>
- <h2>Цепочка безопасности</h2>
- </div>
- <div class="cards">
- <article class="card">
- <h3>Идентификация</h3>
- <p>«Кто ты?» — ввод логина, предъявление имени. Система узнаёт, как вас зовут.</p>
- </article>
- <article class="card">
- <h3>Аутентификация</h3>
- <p>«Докажи!» — ввод пароля, отпечаток, СМС. Система убеждается, что это действительно вы.</p>
- </article>
- <article class="card">
- <h3>Авторизация</h3>
- <p>«Что тебе можно?» — права доступа, роль, чтение/запись. Система решает, что вам разрешено.</p>
- </article>
- </div>
- <div class="note">
- <div class="note__head"><span class="note__mark">§</span>Главный вывод</div>
- <div class="note__body">
- <p><b>Безопасность строится на цепочке: Идентификация → Аутентификация → Авторизация.</b> Разрыв любого звена ведёт к уязвимости.</p>
- </div>
- </div>
- <div class="actions">
- <button class="btn btn--big" id="toQuiz">Итоговое Тестирование</button>
- </div>
- </div>
- `;
- screens.final.querySelector('#toQuiz').addEventListener('click', startQuiz);
- }
- // ---------- ТЕСТ ----------
- function startQuiz() {
- state.quizAnswers = [];
- state.quizIndex = 0;
- state.quizScore = 0;
- state.quizDone = false;
- showScreen('quiz');
- renderQuizQuestion();
- }
- function renderQuizQuestion() {
- const q = QUIZ[state.quizIndex];
- screens.quiz.innerHTML = `
- <div class="page">
- <div class="act-head">
- <span class="act-num">Тест</span>
- <h2>Вопрос ${state.quizIndex + 1} из ${QUIZ.length}</h2>
- </div>
- <div class="quiz-question">
- <h3>${q.q}</h3>
- <div class="quiz-options" id="quizOptions">
- ${q.options.map((opt, i) => `
- <button class="quiz-option" data-index="${i}">${opt}</button>
- `).join('')}
- </div>
- <div class="quiz-feedback" id="quizFeedback" hidden></div>
- </div>
- </div>
- `;
- screens.quiz.querySelectorAll('.quiz-option').forEach(btn => {
- btn.addEventListener('click', () => handleQuizAnswer(parseInt(btn.dataset.index)));
- });
- }
- function handleQuizAnswer(index) {
- const q = QUIZ[state.quizIndex];
- const buttons = screens.quiz.querySelectorAll('.quiz-option');
- buttons.forEach(b => b.disabled = true);
- const feedback = screens.quiz.querySelector('#quizFeedback');
- const isCorrect = index === q.correct;
- if (isCorrect) {
- state.quizScore++;
- playTone(880, 0.12, 'sine');
- buttons[index].classList.add('correct');
- } else {
- playTone(220, 0.2, 'sawtooth');
- buttons[index].classList.add('wrong');
- buttons[q.correct].classList.add('correct');
- }
- feedback.hidden = false;
- feedback.innerHTML = `<b>${isCorrect ? 'Верно!' : 'Неверно.'}</b> ${q.explain}`;
- state.quizAnswers.push({ question: q.q, correct: isCorrect });
- const nextBtn = document.createElement('button');
- nextBtn.className = 'btn';
- nextBtn.textContent = state.quizIndex < QUIZ.length - 1 ? 'Следующий вопрос' : 'Показать результат';
- nextBtn.style.marginTop = '1rem';
- nextBtn.addEventListener('click', () => {
- if (state.quizIndex < QUIZ.length - 1) {
- state.quizIndex++;
- renderQuizQuestion();
- } else {
- showResult();
- }
- });
- feedback.appendChild(nextBtn);
- }
- function showResult() {
- state.quizDone = true;
- updateHUD();
- showScreen('result');
- const score = state.quizScore;
- const total = QUIZ.length;
- const percent = Math.round((score / total) * 100);
- let rank = 'Стажёр';
- if (percent >= 90) rank = 'Агент безопасности';
- else if (percent >= 70) rank = 'Сотрудник';
- else if (percent >= 50) rank = 'Практикант';
- else rank = 'Требуется переподготовка';
- screens.result.innerHTML = `
- <div class="page">
- <div class="act-head">
- <span class="act-num">Результат</span>
- <h2>Золотой пропуск</h2>
- </div>
- <div class="result-pass">
- <p>Банк «Кристалл» · отдел информационной безопасности</p>
- <h3>ПРОПУСК №1957-К</h3>
- <p>Выдан: Алексею Иванову</p>
- <p class="chain">Идентификация → Аутентификация → Авторизация</p>
- <p class="score">${score} из ${total} (${percent}%)</p>
- <p>Звание: <b>${rank}</b></p>
- </div>
- <div class="note">
- <div class="note__head"><span class="note__mark">§</span>Шпаргалка</div>
- <div class="note__body">
- <p><b>Идентификация</b> — «Кто ты?» (Ввод логина, предъявление имени).</p>
- <p><b>Аутентификация</b> — «Докажи!» (Ввод пароля, отпечаток, СМС).</p>
- <p><b>Авторизация</b> — «Что тебе можно?» (Права доступа, роль, чтение/запись).</p>
- <p><b>Главный вывод:</b> Безопасность строится на цепочке: Идентификация → Аутентификация → Авторизация. Разрыв любого звена ведёт к уязвимости.</p>
- </div>
- </div>
- <div class="actions">
- <button class="btn" id="restartBtn">Пройти заново</button>
- </div>
- </div>
- `;
- screens.result.querySelector('#restartBtn').addEventListener('click', () => {
- location.reload();
- });
- logJournal(`📋 Тест завершён: ${score}/${total}`, 'ok');
- }
- // ---------- ИНИЦИАЛИЗАЦИЯ ----------
- function init() {
- hud.hidden = true;
- showScreen('title');
- $('#startBtn').addEventListener('click', () => {
- ensureAudio();
- showScreen('brief');
- });
- $('#toAct1').addEventListener('click', () => {
- state.act = 1;
- state.scene = 0;
- showScreen('act1');
- renderActScreen('s-act1', 1, 0);
- });
- tSound.addEventListener('click', () => {
- state.sound = !state.sound;
- tSound.setAttribute('aria-pressed', state.sound);
- tSound.textContent = state.sound ? 'звук' : 'звук ✕';
- });
- tVoice.addEventListener('click', () => {
- state.voice = !state.voice;
- tVoice.setAttribute('aria-pressed', state.voice);
- tVoice.textContent = state.voice ? 'голос' : 'голос ✕';
- if (!state.voice) window.speechSynthesis.cancel();
- });
- tJournal.addEventListener('click', () => {
- journal.classList.add('is-open');
- scrim.hidden = false;
- });
- jClose.addEventListener('click', closeJournal);
- scrim.addEventListener('click', closeJournal);
- function closeJournal() {
- journal.classList.remove('is-open');
- scrim.hidden = true;
- }
- updateHUD();
- }
- document.addEventListener('DOMContentLoaded', init);
|