Added Role Owner and new UI for Owner
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Server, Play, Square, Terminal, FolderOpen, HardDrive, Settings, Plus, Users as UsersIcon, LogOut, Menu, X, MessageSquare, UserCircle } from 'lucide-react';
|
||||
import { Server, Play, Square, Terminal, FolderOpen, HardDrive, Settings, Plus, Users as UsersIcon, LogOut, Menu, X, MessageSquare, UserCircle, Shield } from 'lucide-react';
|
||||
import Console from './components/Console';
|
||||
import FileManager from './components/FileManager';
|
||||
import Stats from './components/Stats';
|
||||
import ServerSettings from './components/ServerSettings';
|
||||
import CreateServerModal from './components/CreateServerModal';
|
||||
import Users from './components/Users';
|
||||
import UserManagement from './components/UserManagement';
|
||||
import Tickets from './components/Tickets';
|
||||
import Profile from './components/Profile';
|
||||
import Auth from './components/Auth';
|
||||
@@ -24,6 +25,7 @@ function App() {
|
||||
const [activeTab, setActiveTab] = useState('console');
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [showUsers, setShowUsers] = useState(false);
|
||||
const [showUserManagement, setShowUserManagement] = useState(false);
|
||||
const [showTickets, setShowTickets] = useState(false);
|
||||
const [showProfile, setShowProfile] = useState(false);
|
||||
const [viewingUsername, setViewingUsername] = useState(null);
|
||||
@@ -393,6 +395,16 @@ function App() {
|
||||
<MessageSquare className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">Тикеты</span>
|
||||
</button>
|
||||
{user?.role === 'owner' && (
|
||||
<button
|
||||
onClick={() => setShowUserManagement(true)}
|
||||
className="bg-yellow-500 hover:bg-yellow-600 px-4 py-2 rounded-lg transition flex items-center gap-2 text-white"
|
||||
title="Управление пользователями (только для владельца)"
|
||||
>
|
||||
<Shield className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">Управление</span>
|
||||
</button>
|
||||
)}
|
||||
{user?.role === 'admin' && (
|
||||
<button
|
||||
onClick={() => setShowUsers(true)}
|
||||
@@ -593,6 +605,31 @@ function App() {
|
||||
onCreated={loadServers}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showUserManagement && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className={`${currentTheme.card} rounded-2xl ${currentTheme.border} border w-full max-w-6xl max-h-[90vh] overflow-hidden flex flex-col`}>
|
||||
<div className={`${currentTheme.secondary} px-6 py-4 ${currentTheme.border} border-b flex items-center justify-between`}>
|
||||
<h2 className="text-2xl font-bold flex items-center gap-2">
|
||||
<Shield className="w-6 h-6 text-yellow-400" />
|
||||
Управление пользователями
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setShowUserManagement(false)}
|
||||
className={`${currentTheme.hover} p-2 rounded-lg transition`}
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<UserManagement
|
||||
currentUser={user}
|
||||
addNotification={notify}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
327
frontend/src/components/UserManagement.jsx
Normal file
327
frontend/src/components/UserManagement.jsx
Normal file
@@ -0,0 +1,327 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Users, Shield, Ban, Trash2, UserCheck, Server, AlertCircle, CheckCircle } from 'lucide-react';
|
||||
import axios from 'axios';
|
||||
|
||||
const UserManagement = ({ currentUser, addNotification }) => {
|
||||
const [users, setUsers] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedUser, setSelectedUser] = useState(null);
|
||||
const [showRoleModal, setShowRoleModal] = useState(false);
|
||||
const [showAccessModal, setShowAccessModal] = useState(false);
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000';
|
||||
|
||||
// Загрузка пользователей
|
||||
const loadUsers = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const response = await axios.get(`${API_URL}/api/users`, {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
|
||||
// Преобразуем объект в массив если нужно
|
||||
const usersData = Array.isArray(response.data)
|
||||
? response.data
|
||||
: Object.values(response.data);
|
||||
|
||||
setUsers(usersData);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки пользователей:', error);
|
||||
addNotification('error', 'Ошибка загрузки пользователей');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadUsers();
|
||||
}, []);
|
||||
|
||||
// Изменить роль
|
||||
const changeRole = async (username, newRole) => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
await axios.put(
|
||||
`${API_URL}/api/users/${username}/role`,
|
||||
{ role: newRole },
|
||||
{ headers: { Authorization: `Bearer ${token}` } }
|
||||
);
|
||||
|
||||
addNotification('success', `Роль пользователя ${username} изменена на ${newRole}`);
|
||||
loadUsers();
|
||||
setShowRoleModal(false);
|
||||
} catch (error) {
|
||||
console.error('Ошибка изменения роли:', error);
|
||||
addNotification('error', error.response?.data?.detail || 'Ошибка изменения роли');
|
||||
}
|
||||
};
|
||||
|
||||
// Заблокировать пользователя
|
||||
const banUser = async (username) => {
|
||||
if (!confirm(`Заблокировать пользователя ${username}?`)) return;
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
await axios.post(
|
||||
`${API_URL}/api/users/${username}/ban`,
|
||||
{ reason: 'Заблокирован администратором' },
|
||||
{ headers: { Authorization: `Bearer ${token}` } }
|
||||
);
|
||||
|
||||
addNotification('success', `Пользователь ${username} заблокирован`);
|
||||
loadUsers();
|
||||
} catch (error) {
|
||||
console.error('Ошибка блокировки:', error);
|
||||
addNotification('error', error.response?.data?.detail || 'Ошибка блокировки');
|
||||
}
|
||||
};
|
||||
|
||||
// Разблокировать пользователя
|
||||
const unbanUser = async (username) => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
await axios.post(
|
||||
`${API_URL}/api/users/${username}/unban`,
|
||||
{},
|
||||
{ headers: { Authorization: `Bearer ${token}` } }
|
||||
);
|
||||
|
||||
addNotification('success', `Пользователь ${username} разблокирован`);
|
||||
loadUsers();
|
||||
} catch (error) {
|
||||
console.error('Ошибка разблокировки:', error);
|
||||
addNotification('error', error.response?.data?.detail || 'Ошибка разблокировки');
|
||||
}
|
||||
};
|
||||
|
||||
// Удалить пользователя
|
||||
const deleteUser = async (username) => {
|
||||
if (!confirm(`Удалить пользователя ${username}? Это действие необратимо!`)) return;
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
await axios.delete(
|
||||
`${API_URL}/api/users/${username}`,
|
||||
{ headers: { Authorization: `Bearer ${token}` } }
|
||||
);
|
||||
|
||||
addNotification('success', `Пользователь ${username} удалён`);
|
||||
loadUsers();
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления:', error);
|
||||
addNotification('error', error.response?.data?.detail || 'Ошибка удаления');
|
||||
}
|
||||
};
|
||||
|
||||
// Цвета ролей
|
||||
const getRoleColor = (role) => {
|
||||
switch (role) {
|
||||
case 'owner': return 'text-yellow-400';
|
||||
case 'admin': return 'text-red-400';
|
||||
case 'support': return 'text-blue-400';
|
||||
case 'user': return 'text-green-400';
|
||||
case 'banned': return 'text-gray-400';
|
||||
default: return 'text-gray-400';
|
||||
}
|
||||
};
|
||||
|
||||
const getRoleName = (role) => {
|
||||
switch (role) {
|
||||
case 'owner': return 'Владелец';
|
||||
case 'admin': return 'Администратор';
|
||||
case 'support': return 'Поддержка';
|
||||
case 'user': return 'Пользователь';
|
||||
case 'banned': return 'Заблокирован';
|
||||
default: return role;
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-gray-400">Загрузка пользователей...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<Users className="w-8 h-8 text-blue-400" />
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white">Управление пользователями</h2>
|
||||
<p className="text-gray-400">Всего пользователей: {users.length}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Список пользователей */}
|
||||
<div className="grid gap-4">
|
||||
{users.map((user) => (
|
||||
<div
|
||||
key={user.username}
|
||||
className="bg-gray-800 rounded-lg p-4 border border-gray-700 hover:border-gray-600 transition-colors"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Информация о пользователе */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 bg-gray-700 rounded-full flex items-center justify-center">
|
||||
<Users className="w-6 h-6 text-gray-400" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-lg font-semibold text-white">{user.username}</h3>
|
||||
{user.role === 'owner' && (
|
||||
<span className="px-2 py-1 bg-yellow-500/20 text-yellow-400 text-xs rounded">
|
||||
👑 Владелец
|
||||
</span>
|
||||
)}
|
||||
{user.role === 'admin' && (
|
||||
<span className="px-2 py-1 bg-red-500/20 text-red-400 text-xs rounded">
|
||||
🛡️ Админ
|
||||
</span>
|
||||
)}
|
||||
{user.role === 'support' && (
|
||||
<span className="px-2 py-1 bg-blue-500/20 text-blue-400 text-xs rounded">
|
||||
💬 Поддержка
|
||||
</span>
|
||||
)}
|
||||
{user.role === 'banned' && (
|
||||
<span className="px-2 py-1 bg-gray-500/20 text-gray-400 text-xs rounded">
|
||||
🚫 Заблокирован
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 mt-1 text-sm text-gray-400">
|
||||
<span className={getRoleColor(user.role)}>
|
||||
{getRoleName(user.role)}
|
||||
</span>
|
||||
{user.resource_access?.servers && user.resource_access.servers.length > 0 && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Server className="w-4 h-4" />
|
||||
{user.resource_access.servers.length} серверов
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Права */}
|
||||
{user.permissions && (
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{user.permissions.manage_users && (
|
||||
<span className="px-2 py-0.5 bg-green-500/20 text-green-400 text-xs rounded">
|
||||
Управление пользователями
|
||||
</span>
|
||||
)}
|
||||
{user.permissions.manage_servers && (
|
||||
<span className="px-2 py-0.5 bg-blue-500/20 text-blue-400 text-xs rounded">
|
||||
Управление серверами
|
||||
</span>
|
||||
)}
|
||||
{user.permissions.view_all_resources && (
|
||||
<span className="px-2 py-0.5 bg-purple-500/20 text-purple-400 text-xs rounded">
|
||||
Просмотр всех ресурсов
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Действия */}
|
||||
{currentUser.role === 'owner' && user.username !== currentUser.username && (
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Изменить роль */}
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedUser(user);
|
||||
setShowRoleModal(true);
|
||||
}}
|
||||
className="px-3 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded transition-colors flex items-center gap-2"
|
||||
title="Изменить роль"
|
||||
>
|
||||
<Shield className="w-4 h-4" />
|
||||
Роль
|
||||
</button>
|
||||
|
||||
{/* Заблокировать/Разблокировать */}
|
||||
{user.role !== 'banned' ? (
|
||||
<button
|
||||
onClick={() => banUser(user.username)}
|
||||
className="px-3 py-2 bg-orange-500 hover:bg-orange-600 text-white rounded transition-colors flex items-center gap-2"
|
||||
title="Заблокировать"
|
||||
>
|
||||
<Ban className="w-4 h-4" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => unbanUser(user.username)}
|
||||
className="px-3 py-2 bg-green-500 hover:bg-green-600 text-white rounded transition-colors flex items-center gap-2"
|
||||
title="Разблокировать"
|
||||
>
|
||||
<UserCheck className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Удалить */}
|
||||
<button
|
||||
onClick={() => deleteUser(user.username)}
|
||||
className="px-3 py-2 bg-red-500 hover:bg-red-600 text-white rounded transition-colors flex items-center gap-2"
|
||||
title="Удалить"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Модальное окно изменения роли */}
|
||||
{showRoleModal && selectedUser && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-gray-800 rounded-lg p-6 w-96 border border-gray-700">
|
||||
<h3 className="text-xl font-bold text-white mb-4">
|
||||
Изменить роль: {selectedUser.username}
|
||||
</h3>
|
||||
|
||||
<div className="space-y-2">
|
||||
{['owner', 'admin', 'support', 'user', 'banned'].map((role) => (
|
||||
<button
|
||||
key={role}
|
||||
onClick={() => changeRole(selectedUser.username, role)}
|
||||
className={`w-full px-4 py-3 rounded text-left transition-colors ${
|
||||
selectedUser.role === role
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
<div className="font-semibold">{getRoleName(role)}</div>
|
||||
<div className="text-sm opacity-75">
|
||||
{role === 'owner' && 'Полный контроль над панелью'}
|
||||
{role === 'admin' && 'Управление панелью без изменения ролей'}
|
||||
{role === 'support' && 'Работа с тикетами поддержки'}
|
||||
{role === 'user' && 'Базовые возможности'}
|
||||
{role === 'banned' && 'Доступ заблокирован'}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setShowRoleModal(false)}
|
||||
className="w-full mt-4 px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded transition-colors"
|
||||
>
|
||||
Отмена
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserManagement;
|
||||
Reference in New Issue
Block a user