From 7f5f30228e0e8b6d48f8b6d52618fdd457bfea2b Mon Sep 17 00:00:00 2001 From: kalugin66 Date: Sun, 14 Dec 2025 14:22:26 +0500 Subject: [PATCH] r --- admin-server.js | 251 ++++++++++++++++++++++ package.json | 4 +- public/admin-script.js | 386 +++++++++++++++++++++++++++++++++ public/admin.html | 472 +++++++++++++++++++++++++++++++++++++++++ public/index.html | 1 + public/style.css | 251 ++++++++++++++++++++++ server.js | 10 +- 7 files changed, 1372 insertions(+), 3 deletions(-) create mode 100644 admin-server.js create mode 100644 public/admin-script.js create mode 100644 public/admin.html diff --git a/admin-server.js b/admin-server.js new file mode 100644 index 0000000..b7123a9 --- /dev/null +++ b/admin-server.js @@ -0,0 +1,251 @@ +const express = require('express'); +const router = express.Router(); +const { db } = require('./database'); + +const requireAdmin = (req, res, next) => { + if (!req.session.user) { + return res.status(401).json({ error: 'Требуется аутентификация' }); + } + + if (req.session.user.role !== 'admin') { + return res.status(403).json({ error: 'Недостаточно прав' }); + } + + next(); +}; + +router.get('/admin/users', requireAdmin, (req, res) => { + const search = req.query.search || ''; + + let query = ` + SELECT id, login, name, email, role, auth_type, groups, + description, created_at, last_login, updated_at + FROM users + WHERE 1=1 + `; + + const params = []; + + if (search) { + query += ` AND (login LIKE ? OR name LIKE ? OR email LIKE ?)`; + const searchPattern = `%${search}%`; + params.push(searchPattern, searchPattern, searchPattern); + } + + query += " ORDER BY name"; + + db.all(query, params, (err, users) => { + if (err) { + res.status(500).json({ error: err.message }); + return; + } + + res.json(users); + }); +}); + +router.get('/admin/users/:id', requireAdmin, (req, res) => { + const { id } = req.params; + + db.get("SELECT id, login, name, email, role, auth_type, groups, description FROM users WHERE id = ?", [id], (err, user) => { + if (err) { + res.status(500).json({ error: err.message }); + return; + } + + if (!user) { + res.status(404).json({ error: 'Пользователь не найден' }); + return; + } + + res.json(user); + }); +}); + +router.put('/admin/users/:id', requireAdmin, (req, res) => { + const { id } = req.params; + const { login, name, email, role, auth_type, groups, description } = req.body; + + if (!login || !name || !email) { + return res.status(400).json({ error: 'Логин, имя и email обязательны' }); + } + + if (req.session.user.id === parseInt(id) && role !== 'admin') { + return res.status(400).json({ error: 'Нельзя снять права администратора с самого себя' }); + } + + db.get("SELECT id FROM users WHERE (login = ? OR email = ?) AND id != ?", + [login, email, id], (err, existingUser) => { + if (err) { + res.status(500).json({ error: err.message }); + return; + } + + if (existingUser) { + return res.status(400).json({ error: 'Пользователь с таким логином или email уже существует' }); + } + + db.run( + `UPDATE users SET + login = ?, name = ?, email = ?, role = ?, auth_type = ?, + groups = ?, description = ?, updated_at = datetime('now') + WHERE id = ?`, + [ + login, + name, + email, + role || 'teacher', + auth_type || 'local', + groups || '[]', + description || '', + id + ], + function(err) { + if (err) { + res.status(500).json({ error: err.message }); + return; + } + + if (this.changes === 0) { + return res.status(404).json({ error: 'Пользователь не найден' }); + } + + res.json({ + success: true, + message: 'Пользователь обновлен' + }); + } + ); + } + ); +}); + +router.delete('/admin/users/:id', requireAdmin, (req, res) => { + const { id } = req.params; + + if (req.session.user.id === parseInt(id)) { + return res.status(400).json({ error: 'Нельзя удалить самого себя' }); + } + + db.get("SELECT COUNT(*) as task_count FROM tasks WHERE created_by = ?", [id], (err, result) => { + if (err) { + res.status(500).json({ error: err.message }); + return; + } + + if (result.task_count > 0) { + return res.status(400).json({ + error: 'Нельзя удалить пользователя, который создавал задачи. Сначала удалите или переназначьте его задачи.' + }); + } + + db.run("DELETE FROM users WHERE id = ?", [id], function(err) { + if (err) { + res.status(500).json({ error: err.message }); + return; + } + + if (this.changes === 0) { + return res.status(404).json({ error: 'Пользователь не найден' }); + } + + res.json({ + success: true, + message: 'Пользователь удален' + }); + }); + }); +}); + +router.get('/admin/stats', requireAdmin, (req, res) => { + const stats = {}; + + const queries = [ + // Общая статистика по задачам + `SELECT COUNT(*) as total_tasks FROM tasks`, + `SELECT COUNT(*) as active_tasks FROM tasks WHERE status = 'active' AND closed_at IS NULL`, + `SELECT COUNT(*) as closed_tasks FROM tasks WHERE closed_at IS NOT NULL`, + `SELECT COUNT(*) as deleted_tasks FROM tasks WHERE status = 'deleted'`, + `SELECT COUNT(DISTINCT created_by) as unique_creators FROM tasks`, + + // Статистика по статусам назначений + `SELECT COUNT(*) as total_assignments FROM task_assignments`, + `SELECT COUNT(*) as assigned_count FROM task_assignments WHERE status = 'assigned'`, + `SELECT COUNT(*) as in_progress_count FROM task_assignments WHERE status = 'in_progress'`, + `SELECT COUNT(*) as completed_count FROM task_assignments WHERE status = 'completed'`, + `SELECT COUNT(*) as overdue_count FROM task_assignments WHERE status = 'overdue'`, + `SELECT COUNT(*) as rework_count FROM task_assignments WHERE status = 'rework'`, + + // Статистика по пользователям + `SELECT COUNT(*) as total_users FROM users`, + `SELECT COUNT(*) as admin_users FROM users WHERE role = 'admin'`, + `SELECT COUNT(*) as teacher_users FROM users WHERE role = 'teacher'`, + `SELECT COUNT(*) as ldap_users FROM users WHERE auth_type = 'ldap'`, + `SELECT COUNT(*) as local_users FROM users WHERE auth_type = 'local'`, + + // Статистика по файлам + `SELECT COUNT(*) as total_files FROM task_files`, + `SELECT COALESCE(SUM(file_size), 0) as total_files_size FROM task_files`, + + // Последние созданные задачи + `SELECT t.id, t.title, t.created_at, u.name as creator_name + FROM tasks t + LEFT JOIN users u ON t.created_by = u.id + WHERE t.status = 'active' + ORDER BY t.created_at DESC LIMIT 5` + ]; + + const queryPromises = queries.map((query, index) => { + return new Promise((resolve, reject) => { + db.all(query, [], (err, rows) => { + if (err) { + reject(err); + } else { + resolve({ index, rows }); + } + }); + }); + }); + + Promise.all(queryPromises) + .then(results => { + const [ + totalTasks, activeTasks, closedTasks, deletedTasks, uniqueCreators, + totalAssignments, assignedCount, inProgressCount, completedCount, overdueCount, reworkCount, + totalUsers, adminUsers, teacherUsers, ldapUsers, localUsers, + totalFiles, totalFilesSize, + recentTasks + ] = results; + + stats.totalTasks = totalTasks.rows[0].total_tasks; + stats.activeTasks = activeTasks.rows[0].active_tasks; + stats.closedTasks = closedTasks.rows[0].closed_tasks; + stats.deletedTasks = deletedTasks.rows[0].deleted_tasks; + stats.uniqueCreators = uniqueCreators.rows[0].unique_creators; + + stats.totalAssignments = totalAssignments.rows[0].total_assignments; + stats.assignedCount = assignedCount.rows[0].assigned_count; + stats.inProgressCount = inProgressCount.rows[0].in_progress_count; + stats.completedCount = completedCount.rows[0].completed_count; + stats.overdueCount = overdueCount.rows[0].overdue_count; + stats.reworkCount = reworkCount.rows[0].rework_count; + + stats.totalUsers = totalUsers.rows[0].total_users; + stats.adminUsers = adminUsers.rows[0].admin_users; + stats.teacherUsers = teacherUsers.rows[0].teacher_users; + stats.ldapUsers = ldapUsers.rows[0].ldap_users; + stats.localUsers = localUsers.rows[0].local_users; + + stats.totalFiles = totalFiles.rows[0].total_files; + stats.totalFilesSize = totalFilesSize.rows[0].total_files_size; + + stats.recentTasks = recentTasks.rows; + + res.json(stats); + }) + .catch(err => { + res.status(500).json({ error: err.message }); + }); +}); + +module.exports = router; \ No newline at end of file diff --git a/package.json b/package.json index 0f3398c..53dbe46 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "school-crm", - "version": "1.0.0", - "description": "CRM система для школы с управлением задачами", + "version": "1.2.0", + "description": "CRM система для школы с управлением задачами и админ-панелью", "main": "server.js", "scripts": { "start": "node server.js", diff --git a/public/admin-script.js b/public/admin-script.js new file mode 100644 index 0000000..65fccce --- /dev/null +++ b/public/admin-script.js @@ -0,0 +1,386 @@ +let currentUser = null; +let users = []; +let filteredUsers = []; + +document.addEventListener('DOMContentLoaded', function() { + checkAuth(); + setupEventListeners(); +}); + +async function checkAuth() { + try { + const response = await fetch('/api/user'); + if (response.ok) { + const data = await response.json(); + currentUser = data.user; + + if (currentUser.role !== 'admin') { + window.location.href = '/'; + return; + } + + showAdminInterface(); + } else { + showLoginInterface(); + } + } catch (error) { + showLoginInterface(); + } +} + +function showLoginInterface() { + document.getElementById('login-modal').style.display = 'block'; + document.querySelector('.admin-container').style.display = 'none'; +} + +function showAdminInterface() { + document.getElementById('login-modal').style.display = 'none'; + document.querySelector('.admin-container').style.display = 'block'; + + let userInfo = `Администратор: ${currentUser.name}`; + if (currentUser.auth_type === 'ldap') { + userInfo += ` (LDAP)`; + } + + document.getElementById('current-user').textContent = userInfo; + + loadUsers(); + loadDashboardStats(); +} + +function setupEventListeners() { + document.getElementById('login-form').addEventListener('submit', login); + document.getElementById('edit-user-form').addEventListener('submit', updateUser); +} + +async function login(event) { + event.preventDefault(); + + const login = document.getElementById('login').value; + const password = document.getElementById('password').value; + + try { + const response = await fetch('/api/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ login, password }) + }); + + if (response.ok) { + const data = await response.json(); + currentUser = data.user; + + if (currentUser.role !== 'admin') { + window.location.href = '/'; + return; + } + + showAdminInterface(); + } else { + const error = await response.json(); + alert(error.error || 'Ошибка входа'); + } + } catch (error) { + console.error('Ошибка:', error); + alert('Ошибка подключения к серверу'); + } +} + +async function logout() { + try { + await fetch('/api/logout', { method: 'POST' }); + currentUser = null; + showLoginInterface(); + } catch (error) { + console.error('Ошибка выхода:', error); + } +} + +function showAdminSection(sectionName) { + document.querySelectorAll('.admin-tab').forEach(tab => { + tab.classList.remove('active'); + }); + + document.querySelectorAll('.admin-section').forEach(section => { + section.classList.remove('active'); + }); + + document.querySelector(`.admin-tab[onclick="showAdminSection('${sectionName}')"]`).classList.add('active'); + document.getElementById(`admin-${sectionName}`).classList.add('active'); + + if (sectionName === 'users') { + loadUsers(); + } else if (sectionName === 'dashboard') { + loadDashboardStats(); + } +} + +async function loadUsers() { + try { + const response = await fetch('/admin/users'); + if (!response.ok) { + throw new Error('Ошибка загрузки пользователей'); + } + users = await response.json(); + filteredUsers = [...users]; + renderUsersTable(); + } catch (error) { + console.error('Ошибка загрузки пользователей:', error); + showError('users-table-body', 'Ошибка загрузки пользователей'); + } +} + +async function loadDashboardStats() { + try { + const response = await fetch('/admin/stats'); + if (!response.ok) { + throw new Error('Ошибка загрузки статистики'); + } + + const stats = await response.json(); + updateStatsUI(stats); + } catch (error) { + console.error('Ошибка загрузки статистики:', error); + } +} + +function updateStatsUI(stats) { + // Задачи + document.getElementById('total-tasks').textContent = stats.totalTasks; + document.getElementById('active-tasks').textContent = stats.activeTasks; + document.getElementById('closed-tasks').textContent = stats.closedTasks; + document.getElementById('deleted-tasks').textContent = stats.deletedTasks; + + // Процент активных задач + if (stats.totalTasks > 0) { + const activePercentage = Math.round((stats.activeTasks / stats.totalTasks) * 100); + document.getElementById('active-tasks-bar').style.width = `${activePercentage}%`; + } + + // Назначения + document.getElementById('total-assignments').textContent = stats.totalAssignments; + document.getElementById('assigned-count').textContent = stats.assignedCount; + document.getElementById('in-progress-count').textContent = stats.inProgressCount; + document.getElementById('completed-count').textContent = stats.completedCount; + document.getElementById('overdue-count').textContent = stats.overdueCount; + document.getElementById('rework-count').textContent = stats.reworkCount; + + // Пользователи + document.getElementById('total-users').textContent = stats.totalUsers; + document.getElementById('admin-users').textContent = stats.adminUsers; + document.getElementById('teacher-users').textContent = stats.teacherUsers; + document.getElementById('ldap-users').textContent = stats.ldapUsers; + document.getElementById('local-users').textContent = stats.localUsers; + + // Файлы + document.getElementById('total-files').textContent = stats.totalFiles; + const fileSizeMB = (stats.totalFilesSize / 1024 / 1024).toFixed(2); + document.getElementById('total-files-size').textContent = `${fileSizeMB} MB`; + + // Последние задачи + renderRecentTasks(stats.recentTasks || []); +} + +function renderRecentTasks(tasks) { + const container = document.getElementById('recent-tasks-list'); + + if (!tasks || tasks.length === 0) { + container.innerHTML = '
Нет недавних задач
'; + return; + } + + container.innerHTML = tasks.map(task => ` +
+
+
${task.title}
+
+ Создал: ${task.creator_name} | ${formatDateTime(task.created_at)} +
+
+ +
+ `).join(''); +} + +function searchUsers() { + const search = document.getElementById('user-search').value.toLowerCase(); + filteredUsers = users.filter(user => + user.login.toLowerCase().includes(search) || + user.name.toLowerCase().includes(search) || + user.email.toLowerCase().includes(search) || + user.role.toLowerCase().includes(search) || + user.auth_type.toLowerCase().includes(search) + ); + renderUsersTable(); +} + +function renderUsersTable() { + const tbody = document.getElementById('users-table-body'); + + if (!filteredUsers || filteredUsers.length === 0) { + tbody.innerHTML = 'Пользователи не найдены'; + return; + } + + tbody.innerHTML = filteredUsers.map(user => ` + + ${user.id} + + ${user.login} + ${user.auth_type === 'ldap' ? 'LDAP' : ''} + + ${user.name} + ${user.email} + + ${user.role === 'admin' ? 'Администратор' : 'Учитель'} + ${user.role === 'admin' ? 'ADMIN' : ''} + + ${user.auth_type === 'ldap' ? 'LDAP' : 'Локальная'} + ${formatDate(user.created_at)} + ${user.last_login ? formatDateTime(user.last_login) : 'Никогда'} + + + + + + `).join(''); +} + +async function openEditUserModal(userId) { + try { + const response = await fetch(`/admin/users/${userId}`); + if (!response.ok) { + throw new Error('Ошибка загрузки пользователя'); + } + + const user = await response.json(); + + document.getElementById('edit-user-id').value = user.id; + document.getElementById('edit-login').value = user.login; + document.getElementById('edit-name').value = user.name; + document.getElementById('edit-email').value = user.email; + document.getElementById('edit-role').value = user.role; + document.getElementById('edit-auth-type').value = user.auth_type; + document.getElementById('edit-groups').value = user.groups || '[]'; + document.getElementById('edit-description').value = user.description || ''; + + document.getElementById('edit-user-modal').style.display = 'block'; + } catch (error) { + console.error('Ошибка:', error); + alert('Ошибка загрузки пользователя'); + } +} + +function closeEditUserModal() { + document.getElementById('edit-user-modal').style.display = 'none'; +} + +async function updateUser(event) { + event.preventDefault(); + + const userId = document.getElementById('edit-user-id').value; + const login = document.getElementById('edit-login').value; + const name = document.getElementById('edit-name').value; + const email = document.getElementById('edit-email').value; + const role = document.getElementById('edit-role').value; + const auth_type = document.getElementById('edit-auth-type').value; + const groups = document.getElementById('edit-groups').value; + const description = document.getElementById('edit-description').value; + + if (!login || !name || !email) { + alert('Заполните обязательные поля'); + return; + } + + const userData = { + login, + name, + email, + role, + auth_type, + groups: groups || '[]', + description + }; + + try { + const response = await fetch(`/admin/users/${userId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(userData) + }); + + if (response.ok) { + alert('Пользователь успешно обновлен!'); + closeEditUserModal(); + loadUsers(); + loadDashboardStats(); + } else { + const error = await response.json(); + alert(error.error || 'Ошибка обновления пользователя'); + } + } catch (error) { + console.error('Ошибка:', error); + alert('Ошибка обновления пользователя'); + } +} + +async function deleteUser(userId) { + if (userId === currentUser.id) { + alert('Нельзя удалить самого себя'); + return; + } + + if (!confirm('Вы уверены, что хотите удалить этого пользователя?')) { + return; + } + + try { + const response = await fetch(`/admin/users/${userId}`, { + method: 'DELETE' + }); + + if (response.ok) { + alert('Пользователь успешно удален!'); + loadUsers(); + loadDashboardStats(); + } else { + const error = await response.json(); + alert(error.error || 'Ошибка удаления пользователя'); + } + } catch (error) { + console.error('Ошибка:', error); + alert('Ошибка удаления пользователя'); + } +} + +function formatDateTime(dateTimeString) { + if (!dateTimeString) return ''; + const date = new Date(dateTimeString); + return date.toLocaleString('ru-RU'); +} + +function formatDate(dateString) { + if (!dateString) return ''; + const date = new Date(dateString); + return date.toLocaleDateString('ru-RU'); +} + +function showError(elementId, message) { + const element = document.getElementById(elementId); + if (element) { + element.innerHTML = `${message}`; + } +} + +// Автоматическое обновление статистики каждые 30 секунд +setInterval(() => { + if (document.getElementById('admin-dashboard').classList.contains('active')) { + loadDashboardStats(); + } +}, 30000); \ No newline at end of file diff --git a/public/admin.html b/public/admin.html new file mode 100644 index 0000000..cfead24 --- /dev/null +++ b/public/admin.html @@ -0,0 +1,472 @@ + + + + + + School CRM - Административная панель + + + + + + +
+
+

Административная панель

+ +
+ +
+ + +
+ +
+

Статистика системы

+ +
+
+

Задачи

+
0
+
Всего задач в системе
+
+
+
+
+
+ Активные: + 0 +
+
+ Закрытые: + 0 +
+
+ Удаленные: + 0 +
+
+
+ +
+

Статусы назначений

+
0
+
Всего назначений
+
+
+ Назначено: + 0 +
+
+ В работе: + 0 +
+
+ Выполнено: + 0 +
+
+ Просрочено: + 0 +
+
+ На доработке: + 0 +
+
+
+ +
+

Пользователи

+
0
+
Зарегистрировано пользователей
+
+
+ Администраторы: + 0 +
+
+ Учителя: + 0 +
+
+ LDAP: + 0 +
+
+ Локальные: + 0 +
+
+
+ +
+

Файлы

+
0
+
Всего загружено файлов
+
0 MB
+
+
+ +
+

Последние созданные задачи

+
Загрузка...
+
+
+ +
+

Управление пользователями

+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + +
IDЛогинИмяEmailРольТипДата созданияПоследний входДействия
Загрузка пользователей...
+
+
+ + + + + + \ No newline at end of file diff --git a/public/index.html b/public/index.html index 6525f46..8d92a26 100644 --- a/public/index.html +++ b/public/index.html @@ -41,6 +41,7 @@ + diff --git a/public/style.css b/public/style.css index c030e5a..dc5e415 100644 --- a/public/style.css +++ b/public/style.css @@ -1022,4 +1022,255 @@ button.reopen-btn:hover { /* В существующие стили добавляем */ .show-deleted-label[style*="display: none"] { display: none !important; +} +.deadline-badge { + padding: 4px 12px; + border-radius: 20px; + font-size: 0.8rem; + margin-left: 10px; + font-weight: 600; +} + +.deadline-48h { + background: linear-gradient(135deg, #ffc107, #e0a800); + color: #856404; +} + +.deadline-24h { + background: linear-gradient(135deg, #fd7e14, #e8590c); + color: white; +} + +.deadline-indicator { + padding: 2px 8px; + border-radius: 12px; + font-size: 0.7rem; + margin-left: 8px; + font-weight: 600; +} + +.task-title .deadline-badge { + vertical-align: middle; +} + +.filter-group select { + min-width: 150px; +} + +.filters { + gap: 15px; + margin-bottom: 15px; +} + +@media (max-width: 768px) { + .filters { + flex-direction: column; + gap: 10px; + } + + .filter-group select { + width: 100%; + } +} +/* Админ-стили */ +.admin-container { + max-width: 1400px; + margin: 0 auto; + padding: 20px; + display: none; +} + +.admin-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; + padding-bottom: 20px; + border-bottom: 2px solid #e9ecef; +} + +.admin-tabs { + display: flex; + gap: 10px; + margin-bottom: 20px; + border-bottom: 2px solid #e9ecef; + padding-bottom: 10px; +} + +.admin-tab { + padding: 10px 20px; + background: #f8f9fa; + border: none; + border-radius: 8px 8px 0 0; + cursor: pointer; + font-weight: 600; + color: #495057; + transition: all 0.3s ease; +} + +.admin-tab:hover { + background: #e9ecef; +} + +.admin-tab.active { + background: #3498db; + color: white; +} + +.admin-section { + display: none; + padding: 20px; + background: white; + border-radius: 10px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); +} + +.admin-section.active { + display: block; + animation: fadeIn 0.3s ease; +} + +.users-table { + width: 100%; + border-collapse: collapse; + margin-top: 20px; +} + +.users-table th, +.users-table td { + padding: 12px; + text-align: left; + border-bottom: 1px solid #e9ecef; +} + +.users-table th { + background: #f8f9fa; + font-weight: 600; + color: #495057; +} + +.users-table tr:hover { + background: #f8f9fa; +} + +.user-actions { + display: flex; + gap: 8px; +} + +.user-actions button { + padding: 6px 12px; + font-size: 0.85rem; + border-radius: 6px; +} + +.ldap-badge { + background: #3498db; + color: white; + padding: 3px 8px; + border-radius: 12px; + font-size: 0.75rem; + margin-left: 5px; +} + +.admin-badge { + background: #e74c3c; + color: white; + padding: 3px 8px; + border-radius: 12px; + font-size: 0.75rem; + margin-left: 5px; +} + +.form-row { + display: flex; + gap: 20px; + margin-bottom: 20px; +} + +.form-row .form-group { + flex: 1; +} + +.password-fields { + background: #f8f9fa; + padding: 15px; + border-radius: 8px; + margin-bottom: 20px; + border-left: 4px solid #3498db; +} + +.modal-lg { + max-width: 800px; +} + +.search-container { + display: flex; + gap: 10px; + margin-bottom: 20px; +} + +.search-container input { + flex: 1; +} + +.stats-cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 20px; + margin-bottom: 30px; +} + +.stat-card { + background: white; + padding: 20px; + border-radius: 10px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + border-left: 4px solid #3498db; +} + +.stat-card h3 { + margin: 0 0 10px 0; + color: #495057; + font-size: 1.1rem; +} + +.stat-value { + font-size: 2rem; + font-weight: 700; + color: #2c3e50; + margin-bottom: 10px; +} + +.stat-desc { + color: #6c757d; + font-size: 0.9rem; +} + +.stat-card.admin-stat { + border-left-color: #e74c3c; +} + +.stat-card.ldap-stat { + border-left-color: #2ecc71; +} + +.stat-card.local-stat { + border-left-color: #f39c12; +} + +/* Анимация появления */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} +.error { + background: linear-gradient(135deg, #f8d7da, #f5c6cb); + color: #721c24; + padding: 15px 20px; + border-radius: 10px; + margin: 15px 0; + border: 1px solid #f5c6cb; + border-left: 5px solid #e74c3c; + text-align: center; } \ No newline at end of file diff --git a/server.js b/server.js index 270cb7a..f16b6e3 100644 --- a/server.js +++ b/server.js @@ -8,6 +8,7 @@ require('dotenv').config(); const { db, logActivity, createUserTaskFolder, saveTaskMetadata, updateTaskMetadata, checkTaskAccess } = require('./database'); const authService = require('./auth'); +const adminRouter = require('./admin-server'); const app = express(); const PORT = process.env.PORT || 3000; @@ -27,6 +28,8 @@ app.use(session({ } })); +app.use(adminRouter); + const requireAuth = (req, res, next) => { if (!req.session.user) { return res.status(401).json({ error: 'Требуется аутентификация' }); @@ -1296,7 +1299,12 @@ app.get('/api/activity-logs', requireAuth, (req, res) => { res.json(logs); }); }); - +app.get('/admin', (req, res) => { + if (!req.session.user || req.session.user.role !== 'admin') { + return res.status(403).send('Доступ запрещен'); + } + res.sendFile(path.join(__dirname, 'public/admin.html')); +}); app.listen(PORT, () => { console.log(`CRM сервер запущен на порту ${PORT}`); console.log(`Откройте http://localhost:${PORT} в браузере`);