/
Cooolprogrammers
/
web-nodejs-labs
Обзор
Документация
Войти
/
Cooolprogrammers
/
web-nodejs-labs
Код
Запросы
0
Задачи
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
public/lab11/task2/index.html
881 строка
28 KB
darkvoid6@mail.ru
ЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫ
18 дек 2025, 07:59
18 дек 2025, 07:59
f2c4f06
Код
Авторство
О чём код?
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>ЛР11. Задание 2 - Админ-панель</title> <style> :root { --primary: #10b981; --primary-hover: #0d9c6d; --bg-body: #0a0e14; --bg-card: #1f2937; --text-main: #10b981; --text-muted: #34d399; --border: #374151; --danger: #ef4444; --success: #10b981; } * { box-sizing: border-box; } body { font-family: 'JetBrains Mono', 'Courier New', monospace; background: var(--bg-body); color: var(--text-main); margin: 0; line-height: 1.6; background-image: radial-gradient(circle at 10% 20%, rgba(16, 185, 129, 0.05) 0%, transparent 20%), radial-gradient(circle at 90% 80%, rgba(16, 185, 129, 0.05) 0%, transparent 20%); } /* --- Top Bar --- */ .top-bar { background: var(--bg-card); border-bottom: 1px solid var(--border); padding: 15px 30px; display: flex; justify-content: space-between; align-items: center; position: sticky; top: 0; z-index: 10; backdrop-filter: blur(10px); } .brand { font-weight: 700; font-size: 1.2rem; display: flex; align-items: center; gap: 10px; color: var(--primary); letter-spacing: 0.5px; } .user-controls { display: flex; align-items: center; gap: 20px; } .user-info { font-size: 0.9rem; color: var(--text-muted); display: flex; align-items: center; gap: 8px; background: rgba(16, 185, 129, 0.1); padding: 8px 15px; border-radius: 6px; border: 1px solid var(--border); } .status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; } .status-dot.online { background: var(--success); box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.3); } .status-dot.offline { background: var(--text-muted); } /* --- Layout --- */ .container { max-width: 1000px; margin: 30px auto; padding: 0 20px; } .toolbar { display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 25px; } .toolbar h2 { margin: 0; font-size: 1.8rem; color: var(--primary); border-bottom: 1px solid var(--border); padding-bottom: 10px; } .subtitle { margin: 0; color: var(--text-muted); font-size: 0.9rem; margin-top: 5px; } /* --- Table --- */ .table-card { background: var(--bg-card); border-radius: 12px; box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3); border: 1px solid var(--border); overflow: hidden; backdrop-filter: blur(10px); } .table-responsive { overflow-x: auto; } .users-table { width: 100%; border-collapse: collapse; font-size: 0.95rem; } .users-table th { background: rgba(16, 185, 129, 0.08); padding: 15px; text-align: left; font-weight: 700; color: var(--text-muted); border-bottom: 1px solid var(--border); text-transform: uppercase; letter-spacing: 1px; } .users-table td { padding: 15px; border-bottom: 1px solid var(--border); vertical-align: middle; color: #e5e7eb; } .users-table tr:last-child td { border-bottom: none; } .users-table tr:hover { background: rgba(16, 185, 129, 0.05); } /* --- Badges --- */ .role-badge { padding: 6px 12px; border-radius: 6px; font-size: 0.8rem; font-weight: 600; text-transform: uppercase; border: 1px solid currentColor; letter-spacing: 0.5px; } .role-admin { background: rgba(16, 185, 129, 0.15); color: #10b981; border-color: #10b981; } .role-user { background: rgba(16, 185, 129, 0.1); color: #34d399; border-color: #34d399; } /* --- Buttons --- */ .btn { padding: 10px 20px; border-radius: 8px; border: 1px solid transparent; cursor: pointer; font-weight: 600; font-size: 0.9rem; transition: all 0.3s; display: inline-flex; align-items: center; gap: 8px; font-family: 'JetBrains Mono', monospace; letter-spacing: 0.5px; } .btn:hover { transform: translateY(-2px); box-shadow: 0 6px 15px rgba(0, 0, 0, 0.3); } .btn-primary { background: transparent; color: var(--primary); border-color: var(--primary); } .btn-primary:hover { background: rgba(16, 185, 129, 0.1); } .btn-outline { border-color: var(--border); background: transparent; color: var(--text-main); } .btn-outline:hover { border-color: var(--primary); color: var(--primary); background: rgba(16, 185, 129, 0.1); } .btn-secondary { background: transparent; color: #9ca3af; border-color: #4b5563; } .btn-secondary:hover { background: rgba(156, 163, 175, 0.1); } .btn-icon { padding: 6px; border-radius: 6px; color: var(--text-muted); background: transparent; border: 1px solid var(--border); } .btn-icon:hover { background: rgba(16, 185, 129, 0.1); color: var(--primary); } .btn-icon.delete:hover { color: var(--danger); background: rgba(239, 68, 68, 0.1); border-color: var(--danger); } .btn-text { background: transparent; color: var(--text-muted); } .btn-text:hover { color: var(--text-main); } .full-width { width: 100%; justify-content: center; } /* --- Modals --- */ .modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.8); backdrop-filter: blur(5px); display: flex; justify-content: center; align-items: center; z-index: 100; opacity: 0; pointer-events: none; transition: opacity 0.3s; } .modal.active { opacity: 1; pointer-events: auto; } .modal-content { background: var(--bg-card); border-radius: 12px; width: 90%; max-width: 500px; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4); transform: translateY(20px); transition: transform 0.3s; border: 1px solid var(--border); } .modal.active .modal-content { transform: translateY(0); } .small-modal { max-width: 380px; } .modal-header { padding: 20px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; background: rgba(16, 185, 129, 0.08); border-radius: 12px 12px 0 0; } .modal-header h3 { margin: 0; font-size: 1.3rem; color: var(--primary); } .close-btn { background: transparent; border: 1px solid var(--border); font-size: 1.5rem; cursor: pointer; color: var(--text-muted); width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; border-radius: 50%; } .close-btn:hover { background: rgba(16, 185, 129, 0.1); color: var(--primary); } form { padding: 20px; } .form-group { margin-bottom: 20px; } .form-group label { display: block; margin-bottom: 8px; font-weight: 600; font-size: 0.9rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; } .form-group input, .form-group select { width: 100%; padding: 12px; border: 1px solid var(--border); border-radius: 8px; font-size: 0.95rem; background: #0a0e14; color: #a7f3d0; font-family: 'JetBrains Mono', monospace; transition: all 0.3s; } .form-group input:focus, .form-group select:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.2); } .input-group { display: flex; gap: 10px; } .hint { color: var(--text-muted); font-size: 0.8rem; display: block; margin-top: 5px; } .modal-footer { display: flex; justify-content: flex-end; gap: 15px; margin-top: 25px; padding-top: 20px; border-top: 1px solid var(--border); } /* --- Utilities --- */ .hidden { display: none !important; } .text-right { text-align: right; } .loader { padding: 40px; text-align: center; color: var(--text-muted); font-size: 1.1rem; } .alert-box { margin-top: 20px; padding: 15px; background: rgba(245, 158, 11, 0.1); color: #f59e0b; border: 1px solid #f59e0b; border-radius: 8px; font-size: 0.9rem; display: flex; align-items: center; gap: 10px; } .required { color: #ef4444; } /* Animation for table rows */ @keyframes fadeInRow { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .users-table tbody tr { animation: fadeInRow 0.3s ease forwards; opacity: 0; } .users-table tbody tr:nth-child(1) { animation-delay: 0.05s; } .users-table tbody tr:nth-child(2) { animation-delay: 0.1s; } .users-table tbody tr:nth-child(3) { animation-delay: 0.15s; } .users-table tbody tr:nth-child(4) { animation-delay: 0.2s; } .users-table tbody tr:nth-child(5) { animation-delay: 0.25s; } .users-table tbody tr:nth-child(6) { animation-delay: 0.3s; } .users-table tbody tr:nth-child(7) { animation-delay: 0.35s; } .users-table tbody tr:nth-child(8) { animation-delay: 0.4s; } .users-table tbody tr:nth-child(9) { animation-delay: 0.45s; } .users-table tbody tr:nth-child(10) { animation-delay: 0.5s; } /* Scrollbar styling */ ::-webkit-scrollbar { width: 8px; height: 8px; } ::-webkit-scrollbar-track { background: #1f2937; border-radius: 4px; } ::-webkit-scrollbar-thumb { background: #374151; border-radius: 4px; } ::-webkit-scrollbar-thumb:hover { background: #4b5563; } </style> </head> <body> <!-- ВЕРХНЯЯ ПАНЕЛЬ --> <header class="top-bar"> <div class="brand"> <span class="icon">👥</span> <span>User Management System</span> </div> <div class="user-controls"> <div id="userInfo" class="user-info"> <span class="status-dot offline"></span> Гость (Ограниченный просмотр) </div> <button id="authBtn" class="btn btn-outline" onclick="toggleAuthModal()">🔐 Войти</button> </div> </header> <main class="container"> <!-- ИНСТРУМЕНТЫ --> <div class="toolbar"> <div> <h2>📋 Список пользователей</h2> <p class="subtitle" id="listSubtitle">Публичный список (данные скрыты)</p> </div> <!-- Кнопка только для Админа --> <button id="addUserBtn" class="btn btn-primary hidden" onclick="openModal('create')"> <span>➕</span> Добавить пользователя </button> </div> <!-- ТАБЛИЦА --> <div class="table-card"> <div class="table-responsive"> <table class="users-table"> <thead> <tr> <th width="50">ID</th> <th>Логин</th> <th>Роль</th> <th>Дата регистрации</th> <th id="actionsHeader" class="text-right hidden">Действия</th> </tr> </thead> <tbody id="usersTableBody"> <!-- Данные загрузятся сюда --> </tbody> </table> </div> <div id="loader" class="loader"> <div class="spinner"></div> Загрузка данных... </div> </div> <!-- УВЕДОМЛЕНИЕ ДЛЯ ГОСТЕЙ --> <div id="guestWarning" class="alert-box"> ℹ️ Вы просматриваете демо-версию списка. Авторизуйтесь, чтобы видеть полные данные. </div> </main> <!-- МОДАЛКА: ВХОД --> <div id="authModal" class="modal hidden"> <div class="modal-content small-modal"> <div class="modal-header"> <h3>🔐 Вход в систему</h3> <button class="close-btn" onclick="toggleAuthModal()">×</button> </div> <form id="loginForm"> <div class="form-group"> <label>Логин</label> <input type="text" name="login" placeholder="admin" required> </div> <div class="form-group"> <label>Пароль</label> <input type="password" name="password" placeholder="admin123" required> </div> <div class="modal-footer"> <button type="submit" class="btn btn-primary full-width">Войти</button> </div> </form> </div> </div> <!-- МОДАЛКА: ПОЛЬЗОВАТЕЛЬ (Create/Edit) --> <div id="userModal" class="modal hidden"> <div class="modal-content"> <div class="modal-header"> <h3 id="modalTitle">Пользователь</h3> <button class="close-btn" onclick="closeModal()">×</button> </div> <form id="userForm"> <input type="hidden" name="id"> <div class="form-group"> <label>Логин <span class="required">*</span></label> <input type="text" name="login" required minlength="3"> </div> <div class="form-group"> <label>Роль</label> <select name="role" id="roleSelect"> <option value="user">👤 User (Пользователь)</option> <option value="admin">👑 Admin (Администратор)</option> </select> </div> <div class="form-group"> <label>Пароль</label> <div class="input-group"> <input type="text" name="password" id="passInput" placeholder="Введите пароль"> <button type="button" class="btn btn-secondary" onclick="generatePassword()" title="Сгенерировать">🎲</button> </div> <small class="hint" id="passHint">Оставьте пустым, если не хотите менять пароль</small> </div> <div class="modal-footer"> <button type="button" class="btn btn-text" onclick="closeModal()">❌ Отмена</button> <button type="submit" class="btn btn-primary">💾 Сохранить</button> </div> </form> </div> </div> <script> // Инициализация токена и роли let token = localStorage.getItem('token'); let currentUser = null; // Проверка токена при загрузке if (token) { try { const payload = JSON.parse(atob(token.split('.')[1])); currentUser = payload; updateUIForUser(payload); } catch (e) { localStorage.removeItem('token'); token = null; } } // Функция обновления интерфейса для пользователя function updateUIForUser(user) { const userInfo = document.getElementById('userInfo'); const authBtn = document.getElementById('authBtn'); if (user) { userInfo.innerHTML = ` <span class="status-dot online"></span> ${user.login} (${user.role}) `; authBtn.textContent = '🚪 Выйти'; authBtn.onclick = logout; // Показываем/скрываем админские элементы if (user.role === 'admin') { document.getElementById('addUserBtn').classList.remove('hidden'); document.getElementById('actionsHeader').classList.remove('hidden'); } document.getElementById('listSubtitle').textContent = `Полный список (${user.role} доступ)`; document.getElementById('guestWarning').classList.add('hidden'); } } // Функция выхода function logout() { localStorage.removeItem('token'); token = null; currentUser = null; location.reload(); } // Функции для модальных окон function toggleAuthModal() { if (currentUser) { logout(); } else { document.getElementById('authModal').classList.toggle('active'); } } function openModal(mode, user = null) { const modal = document.getElementById('userModal'); const title = document.getElementById('modalTitle'); const form = document.getElementById('userForm'); if (mode === 'create') { title.textContent = '➕ Добавить пользователя'; form.reset(); form.querySelector('[name="id"]').value = ''; document.getElementById('passHint').textContent = 'Введите пароль для нового пользователя'; } else { title.textContent = '✏️ Редактировать пользователя'; form.querySelector('[name="id"]').value = user.id; form.querySelector('[name="login"]').value = user.login; form.querySelector('[name="role"]').value = user.role; document.getElementById('passInput').value = ''; document.getElementById('passHint').textContent = 'Оставьте пустым, если не хотите менять пароль'; } modal.classList.add('active'); } function closeModal() { document.getElementById('authModal').classList.remove('active'); document.getElementById('userModal').classList.remove('active'); } // Генерация пароля function generatePassword() { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*'; let password = ''; for (let i = 0; i < 12; i++) { password += chars.charAt(Math.floor(Math.random() * chars.length)); } document.getElementById('passInput').value = password; } // Обработка формы входа document.getElementById('loginForm').addEventListener('submit', async (e) => { e.preventDefault(); const formData = new FormData(e.target); const data = Object.fromEntries(formData.entries()); try { const response = await fetch('/api/lab11/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); if (response.ok) { const result = await response.json(); localStorage.setItem('token', result.token); token = result.token; const payload = JSON.parse(atob(result.token.split('.')[1])); currentUser = payload; updateUIForUser(payload); closeModal(); loadUsers(); } else { alert('❌ Ошибка авторизации'); } } catch (error) { console.error('Login error:', error); alert('❌ Ошибка сети'); } }); // Загрузка списка пользователей async function loadUsers() { const loader = document.getElementById('loader'); const tbody = document.getElementById('usersTableBody'); loader.style.display = 'block'; tbody.innerHTML = ''; try { const headers = {}; if (token) { headers['Authorization'] = `Bearer ${token}`; } const response = await fetch('/api/lab11/users', { headers }); const users = await response.json(); tbody.innerHTML = users.map(user => { let actions = ''; if (currentUser && currentUser.role === 'admin' && user.id !== currentUser.id) { actions = ` <td class="text-right"> <button class="btn-icon" onclick="openModal('edit', ${JSON.stringify(user)})" title="Редактировать"> ✏️ </button> <button class="btn-icon delete" onclick="deleteUser(${user.id})" title="Удалить"> 🗑️ </button> </td> `; } else { actions = '<td></td>'; } return ` <tr> <td>${user.id}</td> <td>${currentUser ? user.login : '***'}</td> <td><span class="role-badge role-${user.role}">${user.role}</span></td> <td>${user.created_at ? new Date(user.created_at).toLocaleDateString('ru-RU') : '-'}</td> ${actions} </tr> `; }).join(''); } catch (error) { console.error('Load users error:', error); tbody.innerHTML = '<tr><td colspan="5" style="text-align: center; color: #ef4444;">Ошибка загрузки данных</td></tr>'; } finally { loader.style.display = 'none'; } } // Удаление пользователя async function deleteUser(id) { if (!confirm('Удалить пользователя?')) return; try { const response = await fetch(`/api/lab11/users/${id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }); if (response.ok) { loadUsers(); } else { alert('❌ Ошибка удаления'); } } catch (error) { console.error('Delete error:', error); alert('❌ Ошибка сети'); } } // Обработка формы пользователя document.getElementById('userForm').addEventListener('submit', async (e) => { e.preventDefault(); const formData = new FormData(e.target); const data = Object.fromEntries(formData.entries()); const id = data.id; try { const url = id ? `/api/lab11/users/${id}` : '/api/lab11/users'; const method = id ? 'PUT' : 'POST'; // Убираем id из данных для отправки if (id) delete data.id; // Если пароль пустой, удаляем его из данных if (!data.password) delete data.password; const response = await fetch(url, { method, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(data) }); if (response.ok) { closeModal(); loadUsers(); } else { const error = await response.json(); alert(`❌ Ошибка: ${error.error || 'Неизвестная ошибка'}`); } } catch (error) { console.error('Save user error:', error); alert('❌ Ошибка сети'); } }); // Закрытие модалок по клику на фон document.querySelectorAll('.modal').forEach(modal => { modal.addEventListener('click', (e) => { if (e.target === modal) { modal.classList.remove('active'); } }); }); // Загрузка начальных данных loadUsers(); </script> </body> </html>