/
Coderdev
/
web-nodejs-labs
Обзор
Документация
Войти
/
Coderdev
/
web-nodejs-labs
Код
Запросы
0
Задачи
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
public/lab11/task2/index.html
393 строки
18 KB
Coderdev
1
12 май 2026, 11:10
12 май 2026, 11:10
d528236
Код
Авторство
О чём код?
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <title>Задание 2 — Управление пользователями</title> <style> *{box-sizing:border-box;margin:0;padding:0} body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; background:#f0f2f5;color:#1e293b} a.back{display:inline-block;margin:16px 20px 0;color:#2563eb;font-size:0.88rem;text-decoration:none} .page{max-width:1000px;margin:0 auto;padding:20px} h1{font-size:1.4rem;font-weight:700;margin-bottom:4px} .sub{color:#64748b;font-size:0.85rem;margin-bottom:20px} /* Auth strip */ .auth-strip{background:#fff;border-radius:12px;border:1px solid #e2e8f0; padding:14px 16px;margin-bottom:16px;display:flex;gap:10px; align-items:flex-end;flex-wrap:wrap} .auth-strip label{font-size:0.78rem;color:#64748b;font-weight:500; display:flex;flex-direction:column;gap:3px} input[type=text],input[type=password]{ padding:7px 10px;border:1px solid #e2e8f0;border-radius:8px;font-size:0.85rem} .auth-info{margin-left:auto;display:flex;align-items:center;gap:10px;font-size:0.85rem} .role-badge{padding:3px 10px;border-radius:20px;font-size:0.78rem;font-weight:600} .role-admin{background:#fef9c3;color:#854d0e} .role-user{background:#dbeafe;color:#1d4ed8} .role-guest{background:#f1f5f9;color:#64748b} button{padding:7px 14px;border:none;border-radius:8px;cursor:pointer; font-size:0.85rem;font-weight:500;transition:background .15s} .btn{background:#2563eb;color:#fff}.btn:hover{background:#1d4ed8} .btn-red{background:#dc2626;color:#fff}.btn-red:hover{background:#b91c1c} .btn-outline{background:#fff;border:1px solid #e2e8f0;color:#475569} .btn-outline:hover{background:#f8fafc} .btn-sm{padding:4px 9px;font-size:0.78rem} .btn-green{background:#16a34a;color:#fff}.btn-green:hover{background:#15803d} button:disabled{opacity:.4;cursor:not-allowed} .layout{display:grid;grid-template-columns:1fr 300px;gap:16px;align-items:start} @media(max-width:800px){.layout{grid-template-columns:1fr}} /* Table */ .table-card{background:#fff;border-radius:14px;border:1px solid #e2e8f0;overflow:hidden} .table-head{padding:14px 16px;display:flex;align-items:center;justify-content:space-between; border-bottom:1px solid #f1f5f9} .table-head h2{font-size:1rem;font-weight:700} table{width:100%;border-collapse:collapse} th{background:#f8fafc;padding:9px 12px;text-align:left;font-size:0.78rem; color:#64748b;font-weight:600;border-bottom:1px solid #f1f5f9} td{padding:10px 12px;border-bottom:1px solid #f8fafc;font-size:0.85rem;vertical-align:middle} tr:last-child td{border-bottom:none} tr:hover td{background:#fafafa} .empty{padding:30px;text-align:center;color:#94a3b8;font-size:0.88rem} /* Form card */ .form-card{background:#fff;border-radius:14px;border:1px solid #e2e8f0; padding:18px;display:flex;flex-direction:column;gap:10px} .form-card h2{font-size:1rem;font-weight:700;border-bottom:1px solid #f1f5f9;padding-bottom:8px} .form-card label{font-size:0.78rem;color:#64748b;font-weight:500; display:flex;flex-direction:column;gap:3px} .form-card input,.form-card select{width:100%} select{padding:7px 10px;border:1px solid #e2e8f0;border-radius:8px; font-size:0.85rem;background:#fff} .status{padding:7px 10px;border-radius:8px;font-size:0.82rem;margin-top:2px} .ok{background:#dcfce7;color:#15803d} .err{background:#fee2e2;color:#b91c1c} .info{background:#dbeafe;color:#1d4ed8} .gen-pwd{background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px; padding:8px 12px;font-size:0.85rem;font-family:monospace; display:flex;justify-content:space-between;align-items:center} .access-note{background:#fef9c3;border:1px solid #fde68a;border-radius:8px; padding:10px;font-size:0.82rem;color:#854d0e} </style> </head> <body> <a class="back" href="/lab11/">← Назад</a> <div class="page"> <h1>Задание 2 — Управление пользователями</h1> <p class="sub">3 уровня доступа: неавторизованный · авторизованный пользователь · администратор</p> <!-- Панель авторизации --> <div class="auth-strip"> <label>Логин <input type="text" id="a-login" placeholder="логин" style="width:130px"></label> <label>Пароль <input type="password" id="a-pass" placeholder="пароль" style="width:130px"></label> <button class="btn" onclick="doLogin()">Войти</button> <button class="btn-outline" onclick="doLogout()">Выйти</button> <div class="auth-info"> <span id="auth-label" class="role-badge role-guest">👤 Гость</span> </div> <div id="auth-status" style="font-size:0.82rem"></div> </div> <div class="layout"> <!-- Таблица пользователей --> <div class="table-card"> <div class="table-head"> <h2>Пользователи</h2> <button class="btn-outline btn-sm" onclick="loadUsers()">Обновить</button> </div> <div id="access-note" class="access-note" style="margin:10px 16px;display:none"></div> <table> <thead> <tr> <th>ID</th> <th>Логин</th> <th id="th-role" style="display:none">Роль</th> <th id="th-date" style="display:none">Создан</th> <th id="th-actions" style="display:none">Действия</th> </tr> </thead> <tbody id="usersTbody"><tr><td colspan="5" class="empty">Загрузка...</td></tr></tbody> </table> </div> <!-- Форма добавления (только admin) --> <div class="form-card" id="addForm"> <h2>+ Добавить пользователя</h2> <div id="form-access-note" class="access-note">🔒 Только администратор может создавать пользователей</div> <div id="form-fields" style="display:none;flex-direction:column;gap:10px"> <label>Логин <input type="text" id="f-login" placeholder="минимум 3 символа"></label> <label>Пароль <input type="password" id="f-pass" placeholder="минимум 6 символов"></label> <label>Роль <select id="f-role"><option value="user">user</option><option value="admin">admin</option></select> </label> <button class="btn" onclick="createUser()">Создать</button> </div> <div id="form-status"></div> <!-- Генерация пароля --> <div id="genPwdBlock" style="display:none"> <hr style="border:none;border-top:1px solid #f1f5f9;margin:4px 0"> <h2 style="margin-top:4px">🎲 Генерация пароля</h2> <button class="btn-outline" onclick="genPassword()">Сгенерировать</button> <div id="gen-result" style="margin-top:8px"></div> </div> </div> </div> </div> <!-- Модальное окно смены роли --> <div id="roleModal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.5); z-index:100;align-items:center;justify-content:center"> <div style="background:#fff;border-radius:14px;padding:24px;max-width:340px;width:90%"> <h3 style="margin-bottom:14px">Изменить роль</h3> <input type="hidden" id="modal-uid"> <label style="font-size:0.8rem;color:#64748b;display:flex;flex-direction:column;gap:4px"> Новая роль <select id="modal-role" style="padding:7px;border:1px solid #e2e8f0;border-radius:8px"> <option value="user">user</option> <option value="admin">admin</option> </select> </label> <div style="display:flex;gap:8px;margin-top:14px"> <button class="btn" style="flex:1" onclick="saveRole()">Сохранить</button> <button class="btn-outline" onclick="closeRoleModal()">Отмена</button> </div> </div> </div> <script> const API = '/api/lab11'; let token = localStorage.getItem('lab11_token') || null; let currentRole = null; // null=guest, 'user', 'admin' // ── Авторизация ─────────────────────────────────────────────────────── async function doLogin() { const login = document.getElementById('a-login').value.trim(); const password = document.getElementById('a-pass').value; const res = await fetch(`${API}/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ login, password }) }); const data = await res.json(); const el = document.getElementById('auth-status'); if (res.ok && data.token) { token = data.token; localStorage.setItem('lab11_token', token); currentRole = data.user.role; el.style.color = '#16a34a'; el.textContent = `✓ Вход как ${data.user.login}`; updateAuthLabel(); loadUsers(); } else { el.style.color = '#dc2626'; el.textContent = '✕ ' + (data.error || 'Ошибка'); } } function doLogout() { token = null; currentRole = null; localStorage.removeItem('lab11_token'); document.getElementById('auth-status').textContent = ''; updateAuthLabel(); loadUsers(); } function updateAuthLabel() { const el = document.getElementById('auth-label'); if (!currentRole) { el.className = 'role-badge role-guest'; el.textContent = '👤 Гость'; } else if (currentRole === 'admin') { el.className = 'role-badge role-admin'; el.textContent = '🛡 Администратор'; } else { el.className = 'role-badge role-user'; el.textContent = '✓ Авторизован'; } // Показываем/скрываем форму добавления const note = document.getElementById('form-access-note'); const fields = document.getElementById('form-fields'); const genBlock = document.getElementById('genPwdBlock'); if (currentRole === 'admin') { note.style.display = 'none'; fields.style.display = 'flex'; genBlock.style.display = ''; } else { note.style.display = ''; fields.style.display = 'none'; genBlock.style.display = 'none'; } } // ── Загрузка пользователей ──────────────────────────────────────────── async function loadUsers() { const headers = { 'Content-Type': 'application/json' }; if (token) headers['Authorization'] = `Bearer ${token}`; const res = await fetch(`${API}/users`, { headers }); const data = await res.json(); // Попытка получить роль из токена если не логинились if (token && !currentRole) { try { const payload = JSON.parse(atob(token.split('.')[1])); currentRole = payload.role; updateAuthLabel(); } catch(_) {} } renderUsers(data); updateTableCols(); } function updateTableCols() { const showRole = !!currentRole; const showDate = currentRole === 'admin'; const showActions = currentRole === 'admin'; document.getElementById('th-role').style.display = showRole ? '' : 'none'; document.getElementById('th-date').style.display = showDate ? '' : 'none'; document.getElementById('th-actions').style.display = showActions ? '' : 'none'; // Примечание об уровне доступа const note = document.getElementById('access-note'); if (!currentRole) { note.style.display = ''; note.textContent = '👁 Вы не авторизованы — отображается только список логинов'; } else if (currentRole === 'user') { note.style.display = ''; note.textContent = '✓ Вы авторизованы — отображается список с ролями. Редактирование недоступно'; } else { note.style.display = 'none'; } } function renderUsers(users) { const tbody = document.getElementById('usersTbody'); if (!users.length) { tbody.innerHTML = '<tr><td colspan="5" class="empty">Пользователей нет</td></tr>'; return; } const isAdmin = currentRole === 'admin'; const hasRole = !!currentRole; const hasDate = isAdmin; tbody.innerHTML = users.map(u => ` <tr data-id="${u.id}"> <td>${u.id}</td> <td><strong>${u.login}</strong></td> ${hasRole ? `<td>${roleBadge(u.role)}</td>` : ''} ${hasDate ? `<td style="color:#64748b;font-size:0.78rem">${fmtDate(u.created_at)}</td>` : ''} ${isAdmin ? `<td style="white-space:nowrap;display:flex;gap:4px"> <button class="btn-outline btn-sm" onclick="openRoleModal(${u.id},'${u.role}')">Роль</button> <button class="btn-green btn-sm" onclick="resetPwd(${u.id})">🔑</button> <button class="btn-red btn-sm" onclick="removeUser(${u.id})">✕</button> </td>` : ''} </tr>`).join(''); } function roleBadge(role) { if (role === 'admin') return '<span class="role-badge role-admin">admin</span>'; return '<span class="role-badge role-user">user</span>'; } function fmtDate(s) { if (!s) return '—'; return new Date(s).toLocaleDateString('ru-RU', { day:'2-digit', month:'2-digit', year:'numeric' }); } // ── Создание пользователя ───────────────────────────────────────────── async function createUser() { const login = document.getElementById('f-login').value.trim(); const password = document.getElementById('f-pass').value; const role = document.getElementById('f-role').value; const el = document.getElementById('form-status'); if (!login || !password) { el.className = 'status err'; el.textContent = 'Заполните все поля'; return; } const res = await fetch(`${API}/users`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ login, password, role }) }); const data = await res.json(); if (res.ok) { el.className = 'status ok'; el.textContent = `✓ Создан: ${login}`; document.getElementById('f-login').value = ''; document.getElementById('f-pass').value = ''; loadUsers(); } else { el.className = 'status err'; el.textContent = '✕ ' + (data.error || 'Ошибка'); } } // ── Смена роли ──────────────────────────────────────────────────────── function openRoleModal(id, currentR) { document.getElementById('modal-uid').value = id; document.getElementById('modal-role').value = currentR; document.getElementById('roleModal').style.display = 'flex'; } function closeRoleModal() { document.getElementById('roleModal').style.display = 'none'; } async function saveRole() { const id = document.getElementById('modal-uid').value; const role = document.getElementById('modal-role').value; const res = await fetch(`${API}/users/${id}/role`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ role }) }); closeRoleModal(); if (res.ok) loadUsers(); } // ── Сброс пароля ────────────────────────────────────────────────────── async function resetPwd(id) { if (!confirm(`Сгенерировать новый пароль для пользователя #${id}?`)) return; const res = await fetch(`${API}/users/${id}/reset-password`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}` } }); const data = await res.json(); if (res.ok) alert(`Новый пароль: ${data.newPassword}`); } // ── Удаление ────────────────────────────────────────────────────────── async function removeUser(id) { if (!confirm(`Удалить пользователя #${id}?`)) return; const res = await fetch(`${API}/users/${id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) loadUsers(); else { const d = await res.json(); alert(d.error || 'Ошибка'); } } // ── Генерация пароля ────────────────────────────────────────────────── function genPassword() { const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789!@#$%'; let pwd = ''; for (let i = 0; i < 10; i++) pwd += chars[Math.floor(Math.random() * chars.length)]; const el = document.getElementById('gen-result'); el.innerHTML = `<div class="gen-pwd"> <span id="gpwd">${pwd}</span> <button class="btn-outline btn-sm" onclick="copyPwd()">Копировать</button> </div>`; // Подставим в форму document.getElementById('f-pass').value = pwd; } function copyPwd() { navigator.clipboard.writeText(document.getElementById('gpwd').textContent); } // ── Инициализация ───────────────────────────────────────────────────── if (token) { try { const payload = JSON.parse(atob(token.split('.')[1])); currentRole = payload.role; updateAuthLabel(); } catch(_) {} } loadUsers(); </script> </body> </html>