Files
minicrm/public/admin-api-management.html
2026-02-25 22:08:57 +05:00

1787 lines
63 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Управление API ключами и пользователями</title>
<link rel="stylesheet" href="/style.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: #f5f7fa;
color: #2c3e50;
}
.app-container {
display: flex;
min-height: 100vh;
}
/* Сайдбар */
.sidebar {
width: 280px;
background: linear-gradient(180deg, #2c3e50 0%, #1a2632 100%);
color: white;
padding: 25px 0;
position: fixed;
height: 100vh;
overflow-y: auto;
box-shadow: 2px 0 10px rgba(0,0,0,0.1);
}
.logo {
font-size: 24px;
font-weight: bold;
padding: 0 25px;
margin-bottom: 30px;
color: #ecf0f1;
letter-spacing: 1px;
}
.logo span {
color: #3498db;
}
.nav-menu {
display: flex;
flex-direction: column;
}
.nav-item {
padding: 15px 25px;
color: #bdc3c7;
text-decoration: none;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 12px;
font-size: 15px;
border-left: 4px solid transparent;
}
.nav-item:hover {
background: rgba(255,255,255,0.1);
color: white;
}
.nav-item.active {
background: rgba(52, 152, 219, 0.2);
color: white;
border-left-color: #3498db;
}
.nav-item i {
width: 24px;
font-size: 18px;
}
/* Основной контент */
.main-content {
flex: 1;
margin-left: 280px;
padding: 30px;
}
.header {
background: white;
padding: 25px 30px;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
margin-bottom: 30px;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
font-size: 28px;
color: #2c3e50;
}
.header h1 small {
font-size: 14px;
color: #7f8c8d;
font-weight: normal;
margin-left: 15px;
}
.header-actions {
display: flex;
gap: 15px;
}
.btn-primary {
background: #3498db;
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
cursor: pointer;
font-size: 15px;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s;
box-shadow: 0 2px 5px rgba(52,152,219,0.3);
}
.btn-primary:hover {
background: #2980b9;
transform: translateY(-2px);
box-shadow: 0 4px 10px rgba(52,152,219,0.4);
}
.btn-secondary {
background: #95a5a6;
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
cursor: pointer;
font-size: 15px;
font-weight: 500;
transition: all 0.2s;
}
.btn-secondary:hover {
background: #7f8c8d;
}
.btn-danger {
background: #e74c3c;
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.btn-danger:hover {
background: #c0392b;
}
.btn-warning {
background: #f39c12;
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.btn-warning:hover {
background: #e67e22;
}
.btn-sm {
padding: 6px 12px;
font-size: 12px;
}
/* Статистика */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 25px;
margin-bottom: 30px;
}
.stat-card {
background: white;
padding: 25px;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
display: flex;
align-items: center;
gap: 20px;
transition: transform 0.2s;
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
}
.stat-icon {
width: 60px;
height: 60px;
background: #f0f7ff;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
color: #3498db;
}
.stat-info h3 {
font-size: 28px;
margin-bottom: 5px;
color: #2c3e50;
}
.stat-info p {
color: #7f8c8d;
font-size: 14px;
}
/* Табы */
.tabs {
background: white;
border-radius: 12px 12px 0 0;
padding: 0 20px;
display: flex;
gap: 5px;
border-bottom: 1px solid #ecf0f1;
}
.tab-btn {
padding: 18px 25px;
background: none;
border: none;
cursor: pointer;
font-size: 15px;
font-weight: 500;
color: #7f8c8d;
position: relative;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 8px;
}
.tab-btn:hover {
color: #3498db;
}
.tab-btn.active {
color: #3498db;
}
.tab-btn.active::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
right: 0;
height: 3px;
background: #3498db;
border-radius: 3px 3px 0 0;
}
.tab-content {
background: white;
border-radius: 0 0 12px 12px;
padding: 25px;
margin-bottom: 30px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
display: none;
}
.tab-content.active {
display: block;
}
/* Поиск */
.search-box {
margin-bottom: 25px;
display: flex;
gap: 15px;
}
.search-box input {
flex: 1;
padding: 12px 20px;
border: 2px solid #e1e5e9;
border-radius: 8px;
font-size: 15px;
transition: all 0.2s;
}
.search-box input:focus {
border-color: #3498db;
outline: none;
box-shadow: 0 0 0 3px rgba(52,152,219,0.1);
}
.search-box select {
width: 200px;
padding: 12px 20px;
border: 2px solid #e1e5e9;
border-radius: 8px;
font-size: 15px;
background: white;
}
/* Карточки API ключей */
.api-keys-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
gap: 20px;
}
.api-key-card {
background: white;
border: 1px solid #ecf0f1;
border-radius: 10px;
padding: 20px;
transition: all 0.2s;
position: relative;
box-shadow: 0 2px 5px rgba(0,0,0,0.02);
}
.api-key-card:hover {
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
border-color: #3498db;
}
.api-key-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.api-key-name {
font-size: 18px;
font-weight: bold;
color: #2c3e50;
display: flex;
align-items: center;
gap: 10px;
}
.api-key-badge {
padding: 4px 10px;
border-radius: 20px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
}
.badge-active {
background: #d4edda;
color: #155724;
}
.badge-inactive {
background: #f8d7da;
color: #721c24;
}
.api-key-value {
background: #f8f9fa;
padding: 12px 15px;
border-radius: 6px;
font-family: 'Courier New', monospace;
font-size: 13px;
border: 1px solid #e1e5e9;
margin: 15px 0;
display: flex;
justify-content: space-between;
align-items: center;
word-break: break-all;
}
.api-key-value span {
color: #2c3e50;
}
.api-key-meta {
font-size: 13px;
color: #7f8c8d;
margin: 10px 0;
padding: 10px 0;
border-top: 1px solid #ecf0f1;
border-bottom: 1px solid #ecf0f1;
}
.meta-row {
display: flex;
margin: 5px 0;
}
.meta-label {
width: 110px;
color: #95a5a6;
}
.meta-value {
flex: 1;
color: #2c3e50;
}
.api-key-actions {
display: flex;
gap: 10px;
margin-top: 15px;
justify-content: flex-end;
}
.copy-btn {
background: #ecf0f1;
border: none;
padding: 8px 15px;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
gap: 5px;
font-size: 13px;
transition: all 0.2s;
}
.copy-btn:hover {
background: #bdc3c7;
}
.copy-success {
background: #d4edda;
color: #155724;
}
/* Таблица пользователей */
.users-table {
width: 100%;
border-collapse: collapse;
}
.users-table th {
text-align: left;
padding: 15px 10px;
background: #f8f9fa;
color: #2c3e50;
font-weight: 600;
border-bottom: 2px solid #e1e5e9;
}
.users-table td {
padding: 15px 10px;
border-bottom: 1px solid #ecf0f1;
vertical-align: middle;
}
.users-table tr:hover td {
background: #f8f9fa;
}
.user-avatar {
width: 40px;
height: 40px;
background: #3498db;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
font-size: 16px;
}
.user-info {
display: flex;
align-items: center;
gap: 15px;
}
.user-details h4 {
font-size: 16px;
margin-bottom: 3px;
}
.user-details small {
color: #7f8c8d;
font-size: 12px;
}
.role-badge {
padding: 4px 10px;
border-radius: 20px;
font-size: 11px;
font-weight: 600;
background: #e1e5e9;
color: #2c3e50;
}
.role-admin {
background: #c0392b;
color: white;
}
.role-tasks {
background: #2980b9;
color: white;
}
.api-keys-count {
background: #3498db;
color: white;
padding: 3px 8px;
border-radius: 20px;
font-size: 11px;
margin-left: 8px;
}
/* Модальные окна */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 1000;
animation: fadeIn 0.3s;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.modal-content {
position: relative;
background: white;
margin: 50px auto;
padding: 0;
width: 90%;
max-width: 600px;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
animation: slideIn 0.3s;
}
@keyframes slideIn {
from {
transform: translateY(-50px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.modal-header {
padding: 20px 25px;
background: #f8f9fa;
border-bottom: 1px solid #e1e5e9;
border-radius: 12px 12px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
font-size: 20px;
color: #2c3e50;
display: flex;
align-items: center;
gap: 10px;
}
.close {
font-size: 28px;
cursor: pointer;
color: #95a5a6;
transition: color 0.2s;
}
.close:hover {
color: #e74c3c;
}
.modal-body {
padding: 25px;
max-height: 70vh;
overflow-y: auto;
}
.modal-footer {
padding: 20px 25px;
background: #f8f9fa;
border-top: 1px solid #e1e5e9;
border-radius: 0 0 12px 12px;
display: flex;
justify-content: flex-end;
gap: 15px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #2c3e50;
}
.form-control {
width: 100%;
padding: 12px 15px;
border: 2px solid #e1e5e9;
border-radius: 8px;
font-size: 15px;
transition: all 0.2s;
}
.form-control:focus {
border-color: #3498db;
outline: none;
box-shadow: 0 0 0 3px rgba(52,152,219,0.1);
}
textarea.form-control {
resize: vertical;
min-height: 100px;
}
.form-check {
display: flex;
align-items: center;
gap: 10px;
margin: 15px 0;
}
.form-check input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
.form-check label {
cursor: pointer;
color: #2c3e50;
}
.key-display {
background: #f0f7ff;
border: 2px dashed #3498db;
border-radius: 8px;
padding: 20px;
text-align: center;
margin: 20px 0;
}
.key-value {
font-family: 'Courier New', monospace;
font-size: 20px;
background: white;
padding: 15px;
border-radius: 6px;
margin: 15px 0;
word-break: break-all;
border: 1px solid #e1e5e9;
}
.warning-text {
color: #e74c3c;
font-size: 13px;
margin-top: 10px;
display: flex;
align-items: center;
gap: 5px;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #95a5a6;
font-size: 16px;
background: #f8f9fa;
border-radius: 8px;
}
.empty-state i {
font-size: 48px;
margin-bottom: 15px;
color: #bdc3c7;
}
.loading {
text-align: center;
padding: 40px;
color: #7f8c8d;
}
.loading::after {
content: '';
display: inline-block;
width: 20px;
height: 20px;
margin-left: 10px;
border: 3px solid #e1e5e9;
border-top-color: #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.alert {
padding: 15px 20px;
border-radius: 8px;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.alert-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-danger {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.alert-warning {
background: #fff3cd;
color: #856404;
border: 1px solid #ffeeba;
}
.tooltip {
position: relative;
display: inline-block;
}
.tooltip:hover::after {
content: attr(data-tooltip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: #2c3e50;
color: white;
padding: 5px 10px;
border-radius: 4px;
font-size: 11px;
white-space: nowrap;
z-index: 10;
}
@media (max-width: 768px) {
.sidebar {
width: 0;
display: none;
}
.main-content {
margin-left: 0;
}
.api-keys-grid {
grid-template-columns: 1fr;
}
.search-box {
flex-direction: column;
}
.search-box select {
width: 100%;
}
}
</style>
</head>
<body>
<div class="app-container">
<!-- Сайдбар -->
<div class="sidebar">
<div class="logo">CRM <span>API</span></div>
<nav class="nav-menu">
<a href="/admin" class="nav-item">
<i>📊</i> Дашборд
</a>
<a href="/admin/profiles" class="nav-item">
<i>👥</i> Профили пользователей
</a>
<a href="/admin/api-management.html" class="nav-item active">
<i>🔑</i> API управление
</a>
<a href="/admin/groups.html" class="nav-item">
<i>👥</i> Группы
</a>
<a href="/" class="nav-item">
<i>🏠</i> На главную
</a>
</nav>
</div>
<!-- Основной контент -->
<div class="main-content">
<!-- Шапка -->
<div class="header">
<div>
<h1>
Управление API ключами и пользователями
<small>интеграция с внешними сервисами</small>
</h1>
</div>
<div class="header-actions">
<button class="btn-primary" onclick="showCreateKeyModal()">
<i></i> Создать API ключ
</button>
<button class="btn-secondary" onclick="refreshData()">
<i>🔄</i> Обновить
</button>
</div>
</div>
<!-- Статистика -->
<div class="stats-grid" id="stats-container">
<div class="stat-card">
<div class="stat-icon">🔑</div>
<div class="stat-info">
<h3 id="total-keys">0</h3>
<p>Всего API ключей</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon"></div>
<div class="stat-info">
<h3 id="active-keys">0</h3>
<p>Активных ключей</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">👤</div>
<div class="stat-info">
<h3 id="total-users">0</h3>
<p>Пользователей</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">📊</div>
<div class="stat-info">
<h3 id="users-with-keys">0</h3>
<p>С API ключами</p>
</div>
</div>
</div>
<!-- Табы -->
<div class="tabs">
<button class="tab-btn active" onclick="switchTab('keys')" id="tab-keys-btn">
<i>🔑</i> API Ключи
</button>
<button class="tab-btn" onclick="switchTab('users')" id="tab-users-btn">
<i>👥</i> Пользователи
</button>
<button class="tab-btn" onclick="switchTab('docs')" id="tab-docs-btn">
<i>📚</i> Документация
</button>
</div>
<!-- Вкладка API ключей -->
<div class="tab-content active" id="keys-tab">
<div class="search-box">
<input type="text" id="key-search" placeholder="🔍 Поиск по названию, пользователю..."
oninput="filterApiKeys()">
<select id="key-status-filter" onchange="filterApiKeys()">
<option value="all">Все статусы</option>
<option value="active">Активные</option>
<option value="inactive">Неактивные</option>
</select>
</div>
<div id="api-keys-container">
<div class="loading">Загрузка API ключей...</div>
</div>
</div>
<!-- Вкладка пользователей -->
<div class="tab-content" id="users-tab">
<div class="search-box">
<input type="text" id="user-search" placeholder="🔍 Поиск по имени, логину, email..."
oninput="filterUsers()">
<select id="user-role-filter" onchange="filterUsers()">
<option value="all">Все роли</option>
<option value="admin">Администраторы</option>
<option value="tasks">Tasks</option>
<option value="teacher">Учителя</option>
</select>
</div>
<div id="users-container">
<div class="loading">Загрузка пользователей...</div>
</div>
</div>
<!-- Вкладка документации -->
<div class="tab-content" id="docs-tab">
<div style="background: white; padding: 30px; border-radius: 12px;">
<h2 style="margin-bottom: 20px; color: #2c3e50;">📚 Документация API</h2>
<div style="margin-bottom: 30px;">
<h3 style="color: #3498db; margin-bottom: 15px;">Аутентификация</h3>
<div style="background: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 20px;">
<p>Все запросы требуют заголовок <code style="background: #e1e5e9; padding: 3px 8px; border-radius: 4px;">X-API-Key</code> с вашим ключом.</p>
</div>
</div>
<div style="margin-bottom: 30px;">
<h3 style="color: #3498db; margin-bottom: 15px;">Эндпоинты</h3>
<div style="border: 1px solid #e1e5e9; border-radius: 8px; margin-bottom: 15px;">
<div style="background: #f8f9fa; padding: 15px; border-bottom: 1px solid #e1e5e9;">
<code style="font-size: 16px; font-weight: bold;">GET /api/external/tasks</code>
</div>
<div style="padding: 20px;">
<p>Получение списка задач, назначенных пользователю.</p>
<h4 style="margin: 15px 0 10px;">Параметры:</h4>
<ul style="margin-left: 20px;">
<li><code>status</code> - фильтр по статусу (assigned, in_progress, completed)</li>
<li><code>limit</code> - количество записей (по умолчанию 50)</li>
<li><code>offset</code> - смещение для пагинации</li>
</ul>
<h4 style="margin: 15px 0 10px;">Пример ответа:</h4>
<pre style="background: #2c3e50; color: #ecf0f1; padding: 15px; border-radius: 8px; overflow-x: auto;">
{
"success": true,
"tasks": [
{
"id": 123,
"title": "Название задачи",
"description": "Описание задачи",
"created_at": "2024-01-01T10:00:00Z",
"due_date": "2024-01-02T19:01:00Z",
"task_type": "document",
"creator_name": "Иванов И.И.",
"files": []
}
]
}</pre>
</div>
</div>
<div style="border: 1px solid #e1e5e9; border-radius: 8px; margin-bottom: 15px;">
<div style="background: #f8f9fa; padding: 15px; border-bottom: 1px solid #e1e5e9;">
<code style="font-size: 16px; font-weight: bold;">PUT /api/external/tasks/{taskId}/status</code>
</div>
<div style="padding: 20px;">
<p>Изменение статуса задачи.</p>
<h4 style="margin: 15px 0 10px;">Тело запроса:</h4>
<pre style="background: #2c3e50; color: #ecf0f1; padding: 15px; border-radius: 8px;">
{
"status": "in_progress", // или "completed"
"comment": "Комментарий к изменению"
}</pre>
</div>
</div>
<div style="border: 1px solid #e1e5e9; border-radius: 8px; margin-bottom: 15px;">
<div style="background: #f8f9fa; padding: 15px; border-bottom: 1px solid #e1e5e9;">
<code style="font-size: 16px; font-weight: bold;">POST /api/external/tasks/{taskId}/files</code>
</div>
<div style="padding: 20px;">
<p>Загрузка файла к задаче.</p>
<h4 style="margin: 15px 0 10px;">Формат:</h4>
<p>multipart/form-data с полем <code>file</code></p>
</div>
</div>
<div style="border: 1px solid #e1e5e9; border-radius: 8px; margin-bottom: 15px;">
<div style="background: #f8f9fa; padding: 15px; border-bottom: 1px solid #e1e5e9;">
<code style="font-size: 16px; font-weight: bold;">GET /api/external/tasks/{taskId}/files/{fileId}/download</code>
</div>
<div style="padding: 20px;">
<p>Скачивание файла.</p>
</div>
</div>
</div>
<div style="margin-bottom: 30px;">
<h3 style="color: #3498db; margin-bottom: 15px;">Примеры использования</h3>
<div style="background: #f8f9fa; padding: 20px; border-radius: 8px;">
<h4 style="margin-bottom: 10px;">cURL:</h4>
<pre style="background: #2c3e50; color: #ecf0f1; padding: 15px; border-radius: 8px; overflow-x: auto;">
# Получение задач
curl -H "X-API-Key: ваш_ключ" https://ваш-сервер/api/external/tasks
# Изменение статуса
curl -X PUT -H "X-API-Key: ваш_ключ" \
-H "Content-Type: application/json" \
-d '{"status":"in_progress","comment":"Начинаю работу"}' \
https://ваш-сервер/api/external/tasks/123/status
# Загрузка файла
curl -X POST -H "X-API-Key: ваш_ключ" \
-F "file=@document.pdf" \
https://ваш-сервер/api/external/tasks/123/files</pre>
<h4 style="margin: 20px 0 10px;">JavaScript:</h4>
<pre style="background: #2c3e50; color: #ecf0f1; padding: 15px; border-radius: 8px; overflow-x: auto;">
const API_KEY = 'ваш_ключ';
async function getTasks() {
const response = await fetch('/api/external/tasks', {
headers: { 'X-API-Key': API_KEY }
});
return response.json();
}
async function updateStatus(taskId, status) {
const response = await fetch(`/api/external/tasks/${taskId}/status`, {
method: 'PUT',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({ status, comment: 'Комментарий' })
});
return response.json();
}</pre>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Модальное окно создания API ключа -->
<div class="modal" id="create-key-modal">
<div class="modal-content">
<div class="modal-header">
<h3><i>🔑</i> Создать новый API ключ</h3>
<span class="close" onclick="closeModal('create-key-modal')">&times;</span>
</div>
<div class="modal-body">
<form id="create-key-form">
<div class="form-group">
<label for="key-name">Название ключа *</label>
<input type="text" id="key-name" class="form-control" required
placeholder="Например: Сервис документооборота">
<small style="color: #7f8c8d;">Удобное название для идентификации</small>
</div>
<div class="form-group">
<label for="key-user">Пользователь *</label>
<select id="key-user" class="form-control" required>
<option value="">Выберите пользователя...</option>
</select>
<small style="color: #7f8c8d;">Задачи будут назначаться этому пользователю</small>
</div>
<div class="form-group">
<label for="key-description">Описание</label>
<textarea id="key-description" class="form-control" rows="3"
placeholder="Для чего используется этот ключ, какие сервисы будут подключаться..."></textarea>
</div>
<div class="form-group">
<label for="key-ips">Разрешенные IP адреса</label>
<input type="text" id="key-ips" class="form-control"
placeholder="192.168.1.1, 10.0.0.1 (через запятую)">
<small style="color: #7f8c8d;">Оставьте пустым для разрешения всех IP</small>
</div>
<div class="form-check">
<input type="checkbox" id="key-active" checked>
<label for="key-active">Активировать сразу после создания</label>
</div>
<div class="modal-footer" style="padding: 0; margin-top: 30px;">
<button type="button" class="btn-secondary" onclick="closeModal('create-key-modal')">Отмена</button>
<button type="submit" class="btn-primary">Создать ключ</button>
</div>
</form>
</div>
</div>
</div>
<!-- Модальное окно показа созданного ключа -->
<div class="modal" id="show-key-modal">
<div class="modal-content">
<div class="modal-header">
<h3><i></i> API ключ создан</h3>
<span class="close" onclick="closeModal('show-key-modal')">&times;</span>
</div>
<div class="modal-body">
<div class="alert alert-warning">
<i>⚠️</i> Сохраните этот ключ в безопасном месте! Он больше не будет показан.
</div>
<div class="key-display">
<strong>Ваш API ключ:</strong>
<div class="key-value" id="new-api-key"></div>
<button class="btn-primary" onclick="copyApiKey()" style="width: 100%;">
<i>📋</i> Копировать ключ
</button>
</div>
<h4 style="margin: 20px 0 10px;">Быстрый старт:</h4>
<pre style="background: #2c3e50; color: #ecf0f1; padding: 15px; border-radius: 8px; overflow-x: auto;">
# Проверка подключения
curl -H "X-API-Key: <ваш_ключ>" https://ваш-сервер/api/external/tasks</pre>
<div class="form-group" style="margin-top: 20px;">
<label>Информация о пользователе:</label>
<div id="key-user-info" style="background: #f8f9fa; padding: 10px; border-radius: 6px;"></div>
</div>
</div>
<div class="modal-footer">
<button class="btn-primary" onclick="closeModal('show-key-modal')">Готово</button>
</div>
</div>
</div>
<!-- Модальное окно редактирования пользователя -->
<div class="modal" id="edit-user-modal">
<div class="modal-content">
<div class="modal-header">
<h3><i>👤</i> Редактирование пользователя</h3>
<span class="close" onclick="closeModal('edit-user-modal')">&times;</span>
</div>
<div class="modal-body">
<form id="edit-user-form">
<input type="hidden" id="edit-user-id">
<div class="form-group">
<label>Логин</label>
<input type="text" id="edit-user-login" class="form-control" readonly>
</div>
<div class="form-group">
<label>Имя</label>
<input type="text" id="edit-user-name" class="form-control" required>
</div>
<div class="form-group">
<label>Email</label>
<input type="email" id="edit-user-email" class="form-control">
</div>
<div class="form-group">
<label>Роль</label>
<select id="edit-user-role" class="form-control">
<option value="teacher">Учитель</option>
<option value="tasks">Tasks</option>
<option value="admin">Администратор</option>
</select>
</div>
<div class="form-group">
<label>Тип авторизации</label>
<select id="edit-user-auth-type" class="form-control">
<option value="local">Локальный</option>
<option value="ldap">LDAP</option>
</select>
</div>
<div class="form-check">
<input type="checkbox" id="edit-user-active" checked>
<label for="edit-user-active">Активен</label>
</div>
<div class="modal-footer" style="padding: 0; margin-top: 30px;">
<button type="button" class="btn-secondary" onclick="closeModal('edit-user-modal')">Отмена</button>
<button type="submit" class="btn-primary">Сохранить</button>
</div>
</form>
</div>
</div>
</div>
<!-- Модальное окно просмотра ключей пользователя -->
<div class="modal" id="user-keys-modal">
<div class="modal-content">
<div class="modal-header">
<h3><i>🔑</i> API ключи пользователя</h3>
<span class="close" onclick="closeModal('user-keys-modal')">&times;</span>
</div>
<div class="modal-body">
<div id="user-keys-list"></div>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeModal('user-keys-modal')">Закрыть</button>
</div>
</div>
</div>
<script>
let currentUser = null;
let apiKeys = [];
let users = [];
let filteredApiKeys = [];
let filteredUsers = [];
// Инициализация
document.addEventListener('DOMContentLoaded', async () => {
try {
// Проверяем авторизацию
const userResponse = await fetch('/api/user');
const userData = await userResponse.json();
if (!userData.user) {
window.location.href = '/';
return;
}
currentUser = userData.user;
if (currentUser.role !== 'admin') {
alert('Доступ запрещен');
window.location.href = '/';
return;
}
// Загружаем данные
await Promise.all([loadApiKeys(), loadUsers()]);
} catch (error) {
console.error('Ошибка инициализации:', error);
window.location.href = '/';
}
});
// Загрузка API ключей
async function loadApiKeys() {
try {
const response = await fetch('/api/api-keys');
apiKeys = await response.json();
filteredApiKeys = [...apiKeys];
renderApiKeys();
updateStats();
} catch (error) {
console.error('Ошибка загрузки ключей:', error);
showError('api-keys-container', 'Ошибка загрузки API ключей');
}
}
// Загрузка пользователей
async function loadUsers() {
try {
const response = await fetch('/api/users/all');
users = await response.json();
filteredUsers = [...users];
renderUsers();
updateStats();
// Заполняем select пользователей в модальном окне
const select = document.getElementById('key-user');
if (select) {
select.innerHTML = '<option value="">Выберите пользователя...</option>' +
users.map(user => `<option value="${user.id}">${user.name} (${user.login})</option>`).join('');
}
} catch (error) {
console.error('Ошибка загрузки пользователей:', error);
showError('users-container', 'Ошибка загрузки пользователей');
}
}
// Обновление статистики
function updateStats() {
const totalKeys = apiKeys.length;
const activeKeys = apiKeys.filter(k => k.is_active).length;
const totalUsers = users.length;
const usersWithKeys = new Set(apiKeys.map(k => k.user_id)).size;
document.getElementById('total-keys').textContent = totalKeys;
document.getElementById('active-keys').textContent = activeKeys;
document.getElementById('total-users').textContent = totalUsers;
document.getElementById('users-with-keys').textContent = usersWithKeys;
}
// Отображение API ключей
function renderApiKeys() {
const container = document.getElementById('api-keys-container');
if (filteredApiKeys.length === 0) {
container.innerHTML = `
<div class="empty-state">
<i>🔑</i>
<h3>Нет API ключей</h3>
<p>Создайте первый API ключ для интеграции с внешними сервисами</p>
<button class="btn-primary" onclick="showCreateKeyModal()" style="margin-top: 20px;">
+ Создать ключ
</button>
</div>
`;
return;
}
container.innerHTML = `
<div class="api-keys-grid">
${filteredApiKeys.map(key => renderApiKeyCard(key)).join('')}
</div>
`;
}
// Карточка API ключа
function renderApiKeyCard(key) {
const user = users.find(u => u.id === key.user_id) || {};
return `
<div class="api-key-card" data-key-id="${key.id}">
<div class="api-key-header">
<div class="api-key-name">
<i>🔑</i>
${escapeHtml(key.name)}
<span class="api-key-badge ${key.is_active ? 'badge-active' : 'badge-inactive'}">
${key.is_active ? 'Активен' : 'Неактивен'}
</span>
</div>
</div>
<div class="api-key-value">
<span>${key.key}</span>
<button class="copy-btn tooltip" data-tooltip="Копировать" onclick="copyText('${key.key}', this)">
📋
</button>
</div>
<div class="api-key-meta">
<div class="meta-row">
<span class="meta-label">Пользователь:</span>
<span class="meta-value">
<strong>${escapeHtml(user.name || 'Неизвестно')}</strong>
(${escapeHtml(user.login || '')})
</span>
</div>
${key.description ? `
<div class="meta-row">
<span class="meta-label">Описание:</span>
<span class="meta-value">${escapeHtml(key.description)}</span>
</div>
` : ''}
<div class="meta-row">
<span class="meta-label">Создан:</span>
<span class="meta-value">${formatDate(key.created_at)}</span>
</div>
${key.last_used_at ? `
<div class="meta-row">
<span class="meta-label">Последнее использование:</span>
<span class="meta-value">${formatDate(key.last_used_at)}</span>
</div>
` : ''}
${key.allowed_ips ? `
<div class="meta-row">
<span class="meta-label">Разрешенные IP:</span>
<span class="meta-value">${JSON.parse(key.allowed_ips).join(', ')}</span>
</div>
` : ''}
</div>
<div class="api-key-actions">
<button class="btn-warning btn-sm" onclick="toggleKey(${key.id}, ${!key.is_active})">
${key.is_active ? 'Деактивировать' : 'Активировать'}
</button>
<button class="btn-danger btn-sm" onclick="deleteKey(${key.id})">
Удалить
</button>
</div>
</div>
`;
}
// Отображение пользователей
function renderUsers() {
const container = document.getElementById('users-container');
if (filteredUsers.length === 0) {
container.innerHTML = `
<div class="empty-state">
<i>👥</i>
<h3>Нет пользователей</h3>
</div>
`;
return;
}
container.innerHTML = `
<table class="users-table">
<thead>
<tr>
<th>Пользователь</th>
<th>Роль</th>
<th>Тип</th>
<th>API ключи</th>
<th>Дата регистрации</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
${filteredUsers.map(user => renderUserRow(user)).join('')}
</tbody>
</table>
`;
}
// Строка пользователя
function renderUserRow(user) {
const userKeys = apiKeys.filter(k => k.user_id === user.id);
const activeKeysCount = userKeys.filter(k => k.is_active).length;
return `
<tr>
<td>
<div class="user-info">
<div class="user-avatar">${(user.name || '?').charAt(0)}</div>
<div class="user-details">
<h4>${escapeHtml(user.name || 'Без имени')}</h4>
<small>${escapeHtml(user.login || '')}${escapeHtml(user.email || '')}</small>
</div>
</div>
</td>
<td>
<span class="role-badge ${user.role === 'admin' ? 'role-admin' : user.role === 'tasks' ? 'role-tasks' : ''}">
${user.role || 'teacher'}
</span>
</td>
<td>${user.auth_type || 'local'}</td>
<td>
${userKeys.length > 0 ? `
<span class="role-badge" style="background: #3498db; color: white; cursor: pointer;"
onclick="showUserKeys(${user.id})">
${userKeys.length} ключей
${activeKeysCount > 0 ? `(${activeKeysCount} активных)` : ''}
</span>
` : '<span class="role-badge">Нет ключей</span>'}
</td>
<td>${formatDate(user.created_at)}</td>
<td>
<button class="btn-sm" style="background: #3498db; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer;"
onclick="editUser(${user.id})">
✏️ Ред.
</button>
</td>
</tr>
`;
}
// Фильтрация API ключей
function filterApiKeys() {
const search = document.getElementById('key-search').value.toLowerCase();
const statusFilter = document.getElementById('key-status-filter').value;
filteredApiKeys = apiKeys.filter(key => {
const user = users.find(u => u.id === key.user_id) || {};
const matchesSearch = key.name.toLowerCase().includes(search) ||
(user.name || '').toLowerCase().includes(search) ||
(user.login || '').toLowerCase().includes(search);
const matchesStatus = statusFilter === 'all' ||
(statusFilter === 'active' && key.is_active) ||
(statusFilter === 'inactive' && !key.is_active);
return matchesSearch && matchesStatus;
});
renderApiKeys();
}
// Фильтрация пользователей
function filterUsers() {
const search = document.getElementById('user-search').value.toLowerCase();
const roleFilter = document.getElementById('user-role-filter').value;
filteredUsers = users.filter(user => {
const matchesSearch = (user.name || '').toLowerCase().includes(search) ||
(user.login || '').toLowerCase().includes(search) ||
(user.email || '').toLowerCase().includes(search);
const matchesRole = roleFilter === 'all' || user.role === roleFilter;
return matchesSearch && matchesRole;
});
renderUsers();
}
// Переключение статуса ключа
async function toggleKey(id, isActive) {
try {
const response = await fetch(`/api/api-keys/${id}/toggle`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ is_active: isActive })
});
if (response.ok) {
await loadApiKeys();
} else {
const error = await response.json();
alert(`Ошибка: ${error.error}`);
}
} catch (error) {
console.error('Ошибка:', error);
alert('Сетевая ошибка');
}
}
// Удаление ключа
async function deleteKey(id) {
if (!confirm('Вы уверены, что хотите удалить этот ключ? Это действие нельзя отменить.')) {
return;
}
try {
const response = await fetch(`/api/api-keys/${id}`, {
method: 'DELETE'
});
if (response.ok) {
await loadApiKeys();
} else {
const error = await response.json();
alert(`Ошибка: ${error.error}`);
}
} catch (error) {
console.error('Ошибка:', error);
alert('Сетевая ошибка');
}
}
// Показать модальное окно создания ключа
function showCreateKeyModal() {
document.getElementById('create-key-modal').style.display = 'block';
}
// Создание ключа
document.getElementById('create-key-form').addEventListener('submit', async (e) => {
e.preventDefault();
const name = document.getElementById('key-name').value;
const userId = document.getElementById('key-user').value;
const description = document.getElementById('key-description').value;
const ips = document.getElementById('key-ips').value;
const isActive = document.getElementById('key-active').checked;
if (!name || !userId) {
alert('Заполните обязательные поля');
return;
}
const submitBtn = e.target.querySelector('button[type="submit"]');
submitBtn.disabled = true;
submitBtn.innerHTML = '⏳ Создание...';
try {
const response = await fetch('/api/api-keys', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name,
user_id: userId,
description,
allowed_ips: ips
})
});
const data = await response.json();
if (response.ok) {
closeModal('create-key-modal');
document.getElementById('new-api-key').textContent = data.key;
// Показываем информацию о пользователе
const user = users.find(u => u.id === parseInt(userId));
document.getElementById('key-user-info').innerHTML = `
<strong>${user.name}</strong> (${user.login})<br>
Роль: ${user.role}<br>
Email: ${user.email || 'не указан'}
`;
document.getElementById('show-key-modal').style.display = 'block';
await loadApiKeys();
} else {
alert(`Ошибка: ${data.error}`);
}
} catch (error) {
console.error('Ошибка:', error);
alert('Сетевая ошибка');
} finally {
submitBtn.disabled = false;
submitBtn.innerHTML = 'Создать ключ';
}
});
// Редактирование пользователя
async function editUser(userId) {
const user = users.find(u => u.id === userId);
if (!user) return;
document.getElementById('edit-user-id').value = user.id;
document.getElementById('edit-user-login').value = user.login || '';
document.getElementById('edit-user-name').value = user.name || '';
document.getElementById('edit-user-email').value = user.email || '';
document.getElementById('edit-user-role').value = user.role || 'teacher';
document.getElementById('edit-user-auth-type').value = user.auth_type || 'local';
document.getElementById('edit-user-active').checked = user.is_active !== false;
document.getElementById('edit-user-modal').style.display = 'block';
}
// Сохранение пользователя
document.getElementById('edit-user-form').addEventListener('submit', async (e) => {
e.preventDefault();
const userId = document.getElementById('edit-user-id').value;
const userData = {
name: document.getElementById('edit-user-name').value,
email: document.getElementById('edit-user-email').value,
role: document.getElementById('edit-user-role').value,
auth_type: document.getElementById('edit-user-auth-type').value,
is_active: document.getElementById('edit-user-active').checked
};
try {
const response = await fetch(`/api/users/${userId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData)
});
if (response.ok) {
closeModal('edit-user-modal');
await loadUsers();
} else {
const error = await response.json();
alert(`Ошибка: ${error.error}`);
}
} catch (error) {
console.error('Ошибка:', error);
alert('Сетевая ошибка');
}
});
// Показать ключи пользователя
function showUserKeys(userId) {
const userKeys = apiKeys.filter(k => k.user_id === userId);
const user = users.find(u => u.id === userId);
const container = document.getElementById('user-keys-list');
if (userKeys.length === 0) {
container.innerHTML = '<div class="empty-state">У пользователя нет API ключей</div>';
} else {
container.innerHTML = `
<h4 style="margin-bottom: 15px;">${user.name}</h4>
<div style="display: flex; flex-direction: column; gap: 10px;">
${userKeys.map(key => `
<div style="background: #f8f9fa; padding: 10px; border-radius: 6px;">
<strong>${key.name}</strong>
<span class="api-key-badge ${key.is_active ? 'badge-active' : 'badge-inactive'}"
style="margin-left: 10px;">
${key.is_active ? 'Активен' : 'Неактивен'}
</span>
<div style="font-family: monospace; margin: 5px 0;">${key.key}</div>
<small>${key.description || ''}</small>
</div>
`).join('')}
</div>
`;
}
document.getElementById('user-keys-modal').style.display = 'block';
}
// Копирование текста
function copyText(text, btn) {
navigator.clipboard.writeText(text).then(() => {
const originalText = btn.innerHTML;
btn.innerHTML = '✅';
btn.classList.add('copy-success');
setTimeout(() => {
btn.innerHTML = originalText;
btn.classList.remove('copy-success');
}, 2000);
});
}
// Копирование API ключа
function copyApiKey() {
const key = document.getElementById('new-api-key').textContent;
navigator.clipboard.writeText(key).then(() => {
alert('Ключ скопирован в буфер обмена');
});
}
// Переключение вкладок
function switchTab(tabName) {
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
document.getElementById(`tab-${tabName}-btn`).classList.add('active');
document.getElementById(`${tabName}-tab`).classList.add('active');
}
// Обновление данных
function refreshData() {
Promise.all([loadApiKeys(), loadUsers()]);
}
// Закрытие модального окна
function closeModal(modalId) {
document.getElementById(modalId).style.display = 'none';
if (modalId === 'create-key-modal') {
document.getElementById('create-key-form').reset();
}
}
// Показать ошибку
function showError(containerId, message) {
const container = document.getElementById(containerId);
if (container) {
container.innerHTML = `<div class="alert alert-danger">${message}</div>`;
}
}
// Форматирование даты
function formatDate(dateString) {
if (!dateString) return 'Никогда';
return new Date(dateString).toLocaleString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
// Экранирование HTML
function escapeHtml(text) {
if (!text) return '';
return String(text)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
// Закрытие модальных окон по клику вне их
window.onclick = function(event) {
if (event.target.classList.contains('modal')) {
event.target.style.display = 'none';
}
}
</script>
</body>
</html>