1787 lines
63 KiB
HTML
1787 lines
63 KiB
HTML
<!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')">×</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')">×</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')">×</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')">×</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, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
// Закрытие модальных окон по клику вне их
|
||
window.onclick = function(event) {
|
||
if (event.target.classList.contains('modal')) {
|
||
event.target.style.display = 'none';
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html> |