Added user account overview for admins
This commit is contained in:
187
VIEW_USER_PROFILES.md
Normal file
187
VIEW_USER_PROFILES.md
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
# 👁️ Просмотр профилей пользователей
|
||||||
|
|
||||||
|
## Что добавлено
|
||||||
|
|
||||||
|
### Возможность просмотра профилей для админов и тех. поддержки
|
||||||
|
Администраторы и сотрудники технической поддержки теперь могут просматривать личные кабинеты других пользователей, нажав на их логин в списке пользователей.
|
||||||
|
|
||||||
|
## 🎯 Как использовать
|
||||||
|
|
||||||
|
### Просмотр профиля пользователя
|
||||||
|
1. Войдите как администратор или тех. поддержка
|
||||||
|
2. Нажмите кнопку "Пользователи" в header
|
||||||
|
3. Найдите нужного пользователя в списке
|
||||||
|
4. **Нажмите на логин пользователя** (он теперь кликабельный и подсвечивается при наведении)
|
||||||
|
5. Откроется личный кабинет этого пользователя
|
||||||
|
|
||||||
|
### Что можно увидеть
|
||||||
|
- ✅ Имя пользователя
|
||||||
|
- ✅ Роль пользователя
|
||||||
|
- ✅ Статистику по серверам (всего, мои, доступные)
|
||||||
|
- ✅ Список серверов пользователя
|
||||||
|
- ✅ Статистику по тикетам (всего, по статусам)
|
||||||
|
|
||||||
|
### Что нельзя сделать
|
||||||
|
- ❌ Изменить имя пользователя (вкладка скрыта)
|
||||||
|
- ❌ Изменить пароль пользователя (вкладка скрыта)
|
||||||
|
- ❌ Редактировать профиль другого пользователя
|
||||||
|
|
||||||
|
## 🎨 Визуальные изменения
|
||||||
|
|
||||||
|
### В списке пользователей (Users.jsx)
|
||||||
|
- **Логин пользователя** теперь кликабельный
|
||||||
|
- При наведении логин подсвечивается синим цветом
|
||||||
|
- Курсор меняется на pointer (указатель)
|
||||||
|
- Подсказка "Просмотреть профиль" при наведении
|
||||||
|
|
||||||
|
### В личном кабинете (Profile.jsx)
|
||||||
|
- **Заголовок**: "Профиль пользователя: [username]" (вместо "Личный кабинет")
|
||||||
|
- **Подзаголовок**: "Просмотр профиля другого пользователя"
|
||||||
|
- **Вкладки**: скрыты вкладки "Имя пользователя" и "Пароль"
|
||||||
|
- **Только вкладка "Обзор"**: показывается статистика пользователя
|
||||||
|
|
||||||
|
## 📋 Технические детали
|
||||||
|
|
||||||
|
### Backend (main.py)
|
||||||
|
|
||||||
|
#### Новый endpoint
|
||||||
|
```python
|
||||||
|
@app.get("/api/profile/stats/{username}")
|
||||||
|
async def get_user_profile_stats(username: str, user: dict = Depends(get_current_user)):
|
||||||
|
"""Получить статистику профиля другого пользователя"""
|
||||||
|
# Проверка прав доступа
|
||||||
|
if user["role"] not in ["admin", "support"]:
|
||||||
|
raise HTTPException(403, "Недостаточно прав")
|
||||||
|
|
||||||
|
# Возвращает статистику указанного пользователя
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Проверка прав
|
||||||
|
- Только администраторы и тех. поддержка могут просматривать чужие профили
|
||||||
|
- Обычные пользователи получат ошибку 403
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
#### App.jsx
|
||||||
|
```javascript
|
||||||
|
const [viewingUsername, setViewingUsername] = useState(null);
|
||||||
|
|
||||||
|
const handleViewProfile = (username) => {
|
||||||
|
setViewingUsername(username);
|
||||||
|
setShowProfile(true);
|
||||||
|
setShowUsers(false);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Users.jsx
|
||||||
|
```javascript
|
||||||
|
<button
|
||||||
|
onClick={() => onViewProfile && onViewProfile(user.username)}
|
||||||
|
className="text-lg font-semibold hover:text-blue-400 transition cursor-pointer"
|
||||||
|
title="Просмотреть профиль"
|
||||||
|
>
|
||||||
|
{user.username}
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Profile.jsx
|
||||||
|
```javascript
|
||||||
|
const isViewingOther = viewingUsername && viewingUsername !== user?.username;
|
||||||
|
|
||||||
|
const loadStats = async () => {
|
||||||
|
const endpoint = isViewingOther
|
||||||
|
? `${API_URL}/api/profile/stats/${viewingUsername}`
|
||||||
|
: `${API_URL}/api/profile/stats`;
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔐 Безопасность
|
||||||
|
|
||||||
|
### Проверка прав на уровне API
|
||||||
|
- Endpoint `/api/profile/stats/{username}` проверяет роль пользователя
|
||||||
|
- Только `admin` и `support` могут получить доступ
|
||||||
|
- Обычные пользователи получат ошибку 403
|
||||||
|
|
||||||
|
### Защита на уровне UI
|
||||||
|
- Вкладки изменения имени и пароля скрыты при просмотре чужого профиля
|
||||||
|
- Невозможно редактировать данные другого пользователя
|
||||||
|
- Только просмотр статистики
|
||||||
|
|
||||||
|
## 📊 Доступные роли
|
||||||
|
|
||||||
|
### Кто может просматривать чужие профили
|
||||||
|
1. **Администратор** (admin) - ✅ Может просматривать все профили
|
||||||
|
2. **Тех. поддержка** (support) - ✅ Может просматривать все профили
|
||||||
|
3. **Пользователь** (user) - ❌ Не может просматривать чужие профили
|
||||||
|
4. **Забанен** (banned) - ❌ Не имеет доступа к панели
|
||||||
|
|
||||||
|
## ✅ Примеры использования
|
||||||
|
|
||||||
|
### Сценарий 1: Проверка активности пользователя
|
||||||
|
1. Админ хочет проверить, сколько серверов у пользователя
|
||||||
|
2. Открывает "Пользователи"
|
||||||
|
3. Нажимает на логин пользователя
|
||||||
|
4. Видит статистику: 3 сервера, 5 тикетов
|
||||||
|
|
||||||
|
### Сценарий 2: Помощь пользователю
|
||||||
|
1. Тех. поддержка получила тикет от пользователя
|
||||||
|
2. Хочет посмотреть его серверы для диагностики
|
||||||
|
3. Открывает "Пользователи"
|
||||||
|
4. Нажимает на логин пользователя
|
||||||
|
5. Видит список серверов и их названия
|
||||||
|
|
||||||
|
### Сценарий 3: Модерация
|
||||||
|
1. Админ хочет проверить активность пользователя перед баном
|
||||||
|
2. Открывает профиль пользователя
|
||||||
|
3. Видит статистику по тикетам и серверам
|
||||||
|
4. Принимает решение о блокировке
|
||||||
|
|
||||||
|
## 🎯 Возврат к списку пользователей
|
||||||
|
|
||||||
|
### Из профиля пользователя
|
||||||
|
1. Нажмите кнопку "Серверы" в header
|
||||||
|
2. Вы вернётесь к главной странице
|
||||||
|
3. Снова откройте "Пользователи" для просмотра других профилей
|
||||||
|
|
||||||
|
### Или откройте свой профиль
|
||||||
|
1. Нажмите кнопку "Личный кабинет" в header
|
||||||
|
2. Откроется ваш собственный профиль
|
||||||
|
3. Будут доступны все вкладки (Обзор, Имя пользователя, Пароль)
|
||||||
|
|
||||||
|
## ⚠️ Важные замечания
|
||||||
|
|
||||||
|
### Ограничения
|
||||||
|
- Нельзя редактировать чужие профили
|
||||||
|
- Нельзя изменить имя или пароль другого пользователя
|
||||||
|
- Только просмотр статистики
|
||||||
|
|
||||||
|
### Рекомендации
|
||||||
|
- Используйте эту функцию для помощи пользователям
|
||||||
|
- Не злоупотребляйте просмотром чужих профилей
|
||||||
|
- Соблюдайте конфиденциальность данных пользователей
|
||||||
|
|
||||||
|
## ✅ Готово!
|
||||||
|
|
||||||
|
Функция просмотра профилей пользователей полностью интегрирована в MC Panel. Администраторы и тех. поддержка могут легко просматривать информацию о пользователях для помощи и модерации.
|
||||||
|
|
||||||
|
### Тестирование
|
||||||
|
|
||||||
|
1. **Войдите как администратор**
|
||||||
|
- Логин: none
|
||||||
|
- Пароль: none
|
||||||
|
|
||||||
|
2. **Создайте тестового пользователя**
|
||||||
|
- Зарегистрируйте нового пользователя
|
||||||
|
- Создайте несколько серверов от его имени
|
||||||
|
|
||||||
|
3. **Просмотрите его профиль**
|
||||||
|
- Откройте "Пользователи"
|
||||||
|
- Нажмите на логин тестового пользователя
|
||||||
|
- Увидите его статистику
|
||||||
|
|
||||||
|
4. **Вернитесь к своему профилю**
|
||||||
|
- Нажмите "Личный кабинет"
|
||||||
|
- Откроется ваш профиль со всеми вкладками
|
||||||
|
|
||||||
|
**Удобного использования! 👁️**
|
||||||
@@ -404,6 +404,60 @@ async def get_profile_stats(user: dict = Depends(get_current_user)):
|
|||||||
"total_servers": len(owned_servers) + len(accessible_servers)
|
"total_servers": len(owned_servers) + len(accessible_servers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@app.get("/api/profile/stats/{username}")
|
||||||
|
async def get_user_profile_stats(username: str, user: dict = Depends(get_current_user)):
|
||||||
|
"""Получить статистику профиля другого пользователя (только для админов и тех. поддержки)"""
|
||||||
|
# Проверка прав доступа
|
||||||
|
if user["role"] not in ["admin", "support"]:
|
||||||
|
raise HTTPException(403, "Недостаточно прав для просмотра профилей других пользователей")
|
||||||
|
|
||||||
|
users = load_users()
|
||||||
|
|
||||||
|
# Проверка существования пользователя
|
||||||
|
if username not in users:
|
||||||
|
raise HTTPException(404, "Пользователь не найден")
|
||||||
|
|
||||||
|
target_user = users[username]
|
||||||
|
|
||||||
|
# Подсчитываем серверы пользователя
|
||||||
|
owned_servers = []
|
||||||
|
accessible_servers = []
|
||||||
|
|
||||||
|
for server_dir in SERVERS_DIR.iterdir():
|
||||||
|
if server_dir.is_dir():
|
||||||
|
config = load_server_config(server_dir.name)
|
||||||
|
if config.get("owner") == username:
|
||||||
|
owned_servers.append({
|
||||||
|
"name": server_dir.name,
|
||||||
|
"displayName": config.get("displayName", server_dir.name)
|
||||||
|
})
|
||||||
|
elif username in target_user.get("servers", []) or target_user["role"] == "admin":
|
||||||
|
accessible_servers.append({
|
||||||
|
"name": server_dir.name,
|
||||||
|
"displayName": config.get("displayName", server_dir.name)
|
||||||
|
})
|
||||||
|
|
||||||
|
# Подсчитываем тикеты
|
||||||
|
tickets = load_tickets()
|
||||||
|
user_tickets = [t for t in tickets.values() if t["author"] == username]
|
||||||
|
|
||||||
|
tickets_stats = {
|
||||||
|
"total": len(user_tickets),
|
||||||
|
"pending": len([t for t in user_tickets if t["status"] == "pending"]),
|
||||||
|
"in_progress": len([t for t in user_tickets if t["status"] == "in_progress"]),
|
||||||
|
"closed": len([t for t in user_tickets if t["status"] == "closed"])
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"username": username,
|
||||||
|
"role": target_user["role"],
|
||||||
|
"owned_servers": owned_servers,
|
||||||
|
"accessible_servers": accessible_servers,
|
||||||
|
"tickets": tickets_stats,
|
||||||
|
"total_servers": len(owned_servers) + len(accessible_servers),
|
||||||
|
"is_viewing_other": True # Флаг что это чужой профиль
|
||||||
|
}
|
||||||
|
|
||||||
# API для серверов
|
# API для серверов
|
||||||
@app.get("/api/servers")
|
@app.get("/api/servers")
|
||||||
async def get_servers(user: dict = Depends(get_current_user)):
|
async def get_servers(user: dict = Depends(get_current_user)):
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ function App() {
|
|||||||
const [showUsers, setShowUsers] = useState(false);
|
const [showUsers, setShowUsers] = useState(false);
|
||||||
const [showTickets, setShowTickets] = useState(false);
|
const [showTickets, setShowTickets] = useState(false);
|
||||||
const [showProfile, setShowProfile] = useState(false);
|
const [showProfile, setShowProfile] = useState(false);
|
||||||
|
const [viewingUsername, setViewingUsername] = useState(null);
|
||||||
const [connectionError, setConnectionError] = useState(false);
|
const [connectionError, setConnectionError] = useState(false);
|
||||||
const [theme, setTheme] = useState(localStorage.getItem('theme') || 'dark');
|
const [theme, setTheme] = useState(localStorage.getItem('theme') || 'dark');
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||||
@@ -118,6 +119,12 @@ function App() {
|
|||||||
loadServers();
|
loadServers();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleViewProfile = (username) => {
|
||||||
|
setViewingUsername(username);
|
||||||
|
setShowProfile(true);
|
||||||
|
setShowUsers(false);
|
||||||
|
};
|
||||||
|
|
||||||
const startServer = async (serverName) => {
|
const startServer = async (serverName) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
@@ -201,7 +208,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<Users token={token} theme={currentTheme} />
|
<Users token={token} theme={currentTheme} onViewProfile={handleViewProfile} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -284,7 +291,10 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
<ThemeSelector currentTheme={theme} onThemeChange={handleThemeChange} />
|
<ThemeSelector currentTheme={theme} onThemeChange={handleThemeChange} />
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowProfile(false)}
|
onClick={() => {
|
||||||
|
setShowProfile(false);
|
||||||
|
setViewingUsername(null);
|
||||||
|
}}
|
||||||
className={`${currentTheme.card} ${currentTheme.hover} px-4 py-2 rounded-lg transition flex items-center gap-2`}
|
className={`${currentTheme.card} ${currentTheme.hover} px-4 py-2 rounded-lg transition flex items-center gap-2`}
|
||||||
>
|
>
|
||||||
<Server className="w-4 h-4" />
|
<Server className="w-4 h-4" />
|
||||||
@@ -301,7 +311,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<Profile token={token} user={user} theme={currentTheme} onUsernameChange={handleUsernameChange} />
|
<Profile token={token} user={user} theme={currentTheme} onUsernameChange={handleUsernameChange} viewingUsername={viewingUsername} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -345,7 +355,10 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
<ThemeSelector currentTheme={theme} onThemeChange={handleThemeChange} />
|
<ThemeSelector currentTheme={theme} onThemeChange={handleThemeChange} />
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowProfile(true)}
|
onClick={() => {
|
||||||
|
setShowProfile(true);
|
||||||
|
setViewingUsername(null);
|
||||||
|
}}
|
||||||
className={`${currentTheme.accent} ${currentTheme.accentHover} px-4 py-2 rounded-lg transition flex items-center gap-2 text-white`}
|
className={`${currentTheme.accent} ${currentTheme.accentHover} px-4 py-2 rounded-lg transition flex items-center gap-2 text-white`}
|
||||||
>
|
>
|
||||||
<UserCircle className="w-4 h-4" />
|
<UserCircle className="w-4 h-4" />
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ import { User, Lock, Server, MessageSquare, Shield, TrendingUp, Eye, EyeOff } fr
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { API_URL } from '../config';
|
import { API_URL } from '../config';
|
||||||
|
|
||||||
export default function Profile({ token, user, theme, onUsernameChange }) {
|
export default function Profile({ token, user, theme, onUsernameChange, viewingUsername }) {
|
||||||
const [stats, setStats] = useState(null);
|
const [stats, setStats] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [activeTab, setActiveTab] = useState('overview');
|
const [activeTab, setActiveTab] = useState('overview');
|
||||||
|
const isViewingOther = viewingUsername && viewingUsername !== user?.username;
|
||||||
|
|
||||||
// Форма смены имени
|
// Форма смены имени
|
||||||
const [usernameForm, setUsernameForm] = useState({
|
const [usernameForm, setUsernameForm] = useState({
|
||||||
@@ -28,11 +29,15 @@ export default function Profile({ token, user, theme, onUsernameChange }) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadStats();
|
loadStats();
|
||||||
}, []);
|
}, [viewingUsername]);
|
||||||
|
|
||||||
const loadStats = async () => {
|
const loadStats = async () => {
|
||||||
try {
|
try {
|
||||||
const { data } = await axios.get(`${API_URL}/api/profile/stats`, {
|
const endpoint = isViewingOther
|
||||||
|
? `${API_URL}/api/profile/stats/${viewingUsername}`
|
||||||
|
: `${API_URL}/api/profile/stats`;
|
||||||
|
|
||||||
|
const { data } = await axios.get(endpoint, {
|
||||||
headers: { Authorization: `Bearer ${token}` }
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
});
|
});
|
||||||
setStats(data);
|
setStats(data);
|
||||||
@@ -157,11 +162,16 @@ export default function Profile({ token, user, theme, onUsernameChange }) {
|
|||||||
<div className="max-w-6xl mx-auto">
|
<div className="max-w-6xl mx-auto">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h1 className="text-2xl font-bold mb-2">Личный кабинет</h1>
|
<h1 className="text-2xl font-bold mb-2">
|
||||||
<p className={theme.textSecondary}>Управление профилем и настройками</p>
|
{isViewingOther ? `Профиль пользователя: ${viewingUsername}` : 'Личный кабинет'}
|
||||||
|
</h1>
|
||||||
|
<p className={theme.textSecondary}>
|
||||||
|
{isViewingOther ? 'Просмотр профиля другого пользователя' : 'Управление профилем и настройками'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
|
{!isViewingOther && (
|
||||||
<div className={`${theme.secondary} ${theme.border} border rounded-2xl mb-6 p-2 flex gap-2`}>
|
<div className={`${theme.secondary} ${theme.border} border rounded-2xl mb-6 p-2 flex gap-2`}>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('overview')}
|
onClick={() => setActiveTab('overview')}
|
||||||
@@ -197,9 +207,10 @@ export default function Profile({ token, user, theme, onUsernameChange }) {
|
|||||||
Пароль
|
Пароль
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Overview Tab */}
|
{/* Overview Tab */}
|
||||||
{activeTab === 'overview' && (
|
{(activeTab === 'overview' || isViewingOther) && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* User Info Card */}
|
{/* User Info Card */}
|
||||||
<div className={`${theme.card} ${theme.border} border rounded-2xl p-6`}>
|
<div className={`${theme.card} ${theme.border} border rounded-2xl p-6`}>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Users as UsersIcon, Trash2, Shield, User } from 'lucide-react';
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { API_URL } from '../config';
|
import { API_URL } from '../config';
|
||||||
|
|
||||||
export default function Users({ token }) {
|
export default function Users({ token, onViewProfile }) {
|
||||||
const [users, setUsers] = useState([]);
|
const [users, setUsers] = useState([]);
|
||||||
const [servers, setServers] = useState([]);
|
const [servers, setServers] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -111,7 +111,13 @@ export default function Users({ token }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold">{user.username}</h3>
|
<button
|
||||||
|
onClick={() => onViewProfile && onViewProfile(user.username)}
|
||||||
|
className="text-lg font-semibold hover:text-blue-400 transition cursor-pointer text-left"
|
||||||
|
title="Просмотреть профиль"
|
||||||
|
>
|
||||||
|
{user.username}
|
||||||
|
</button>
|
||||||
<p className="text-sm text-gray-400">
|
<p className="text-sm text-gray-400">
|
||||||
{user.role === 'admin' ? 'Администратор' : user.role === 'support' ? 'Тех. поддержка' : user.role === 'banned' ? 'Забанен' : 'Пользователь'}
|
{user.role === 'admin' ? 'Администратор' : user.role === 'support' ? 'Тех. поддержка' : user.role === 'banned' ? 'Забанен' : 'Пользователь'}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user