фикс
This commit is contained in:
parent
987ab236f8
commit
96cf4cb0db
@ -317,6 +317,99 @@ const getBlockedDeviceDetails = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Получение устройств пользователя
|
||||
// @route GET /api/admin/user/:userId/devices
|
||||
// @access Private/Admin
|
||||
const getUserDevices = async (req, res) => {
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
|
||||
// Проверяем, существует ли пользователь
|
||||
const user = await User.findById(userId);
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
message: 'Пользователь не найден'
|
||||
});
|
||||
}
|
||||
|
||||
// Получаем список устройств пользователя по его ID
|
||||
const devices = await User.findUserDevices(userId);
|
||||
|
||||
console.log(`[ADMIN] Запрос устройств пользователя ${userId}, найдено: ${devices.length}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: devices
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[ADMIN] Ошибка при получении устройств пользователя:', error);
|
||||
res.status(500).json({
|
||||
message: 'Ошибка при получении устройств пользователя',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Блокировка всех устройств пользователя
|
||||
// @route POST /api/admin/user/:userId/block-devices
|
||||
// @access Private/Admin
|
||||
const blockUserDevices = async (req, res) => {
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
const { reason = 'Блокировка администратором', duration = 'permanent' } = req.body;
|
||||
|
||||
// Проверяем, существует ли пользователь
|
||||
const user = await User.findById(userId);
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
message: 'Пользователь не найден'
|
||||
});
|
||||
}
|
||||
|
||||
// Получаем все устройства пользователя
|
||||
const devices = await User.findUserDevices(userId);
|
||||
|
||||
if (!devices.length) {
|
||||
return res.status(404).json({
|
||||
message: 'У пользователя не найдено устройств для блокировки'
|
||||
});
|
||||
}
|
||||
|
||||
// Блокируем каждое устройство
|
||||
const blockedDevices = [];
|
||||
for (const device of devices) {
|
||||
try {
|
||||
const blockedDevice = await blockUserDevice(
|
||||
userId,
|
||||
req.user._id,
|
||||
reason,
|
||||
device.fingerprint,
|
||||
duration
|
||||
);
|
||||
blockedDevices.push(blockedDevice);
|
||||
} catch (err) {
|
||||
console.error(`[ADMIN] Ошибка при блокировке устройства ${device.fingerprint}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[ADMIN] Заблокированы устройства пользователя ${userId}, количество: ${blockedDevices.length}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Заблокировано ${blockedDevices.length} из ${devices.length} устройств пользователя`,
|
||||
blockedDevices
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[ADMIN] Ошибка при блокировке устройств пользователя:', error);
|
||||
res.status(500).json({
|
||||
message: 'Ошибка при блокировке устройств пользователя',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Вычисляет уровень риска на основе подозрительной активности
|
||||
*/
|
||||
@ -357,5 +450,7 @@ module.exports = {
|
||||
blockDevice,
|
||||
unblockDeviceAdmin,
|
||||
getBlockedDevicesList,
|
||||
getBlockedDeviceDetails
|
||||
getBlockedDeviceDetails,
|
||||
getUserDevices,
|
||||
blockUserDevices
|
||||
};
|
@ -113,6 +113,66 @@ userSchema.methods.matchPassword = async function (enteredPassword) {
|
||||
return await bcrypt.compare(enteredPassword, this.password);
|
||||
};
|
||||
|
||||
// Статический метод для получения устройств пользователя
|
||||
userSchema.statics.findUserDevices = async function(userId) {
|
||||
try {
|
||||
// Получаем сессии и авторизации пользователя
|
||||
const user = await this.findById(userId).select('authHistory devices sessions');
|
||||
|
||||
if (!user) return [];
|
||||
|
||||
// Объединяем все возможные источники устройств
|
||||
let devices = [];
|
||||
|
||||
// Из истории авторизаций
|
||||
if (user.authHistory && user.authHistory.length > 0) {
|
||||
devices = devices.concat(user.authHistory.map(auth => ({
|
||||
fingerprint: auth.deviceFingerprint,
|
||||
userAgent: auth.userAgent,
|
||||
ipAddress: auth.ipAddress,
|
||||
lastUsed: auth.timestamp,
|
||||
type: 'auth'
|
||||
})));
|
||||
}
|
||||
|
||||
// Из списка устройств
|
||||
if (user.devices && user.devices.length > 0) {
|
||||
devices = devices.concat(user.devices.map(device => ({
|
||||
fingerprint: device.fingerprint,
|
||||
userAgent: device.userAgent,
|
||||
platform: device.platform,
|
||||
lastUsed: device.lastActive,
|
||||
type: 'device'
|
||||
})));
|
||||
}
|
||||
|
||||
// Из активных сессий
|
||||
if (user.sessions && user.sessions.length > 0) {
|
||||
devices = devices.concat(user.sessions.map(session => ({
|
||||
fingerprint: session.deviceFingerprint,
|
||||
userAgent: session.userAgent,
|
||||
ipAddress: session.ipAddress,
|
||||
lastUsed: session.lastActive,
|
||||
type: 'session'
|
||||
})));
|
||||
}
|
||||
|
||||
// Удаляем дубликаты по fingerprint
|
||||
const uniqueDevices = Array.from(
|
||||
devices.reduce((map, device) => {
|
||||
if (device.fingerprint && !map.has(device.fingerprint)) {
|
||||
map.set(device.fingerprint, device);
|
||||
}
|
||||
return map;
|
||||
}, new Map()).values()
|
||||
);
|
||||
|
||||
return uniqueDevices;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при поиске устройств пользователя:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const User = mongoose.model('User', userSchema);
|
||||
|
||||
|
@ -13,7 +13,9 @@ const {
|
||||
blockDevice,
|
||||
unblockDeviceAdmin,
|
||||
getBlockedDevicesList,
|
||||
getBlockedDeviceDetails
|
||||
getBlockedDeviceDetails,
|
||||
getUserDevices,
|
||||
blockUserDevices
|
||||
} = require('../controllers/deviceSecurityController');
|
||||
|
||||
// Все маршруты защищены middleware для проверки авторизации и прав администратора
|
||||
@ -44,4 +46,8 @@ router.get('/blocked-devices/:id', getBlockedDeviceDetails);
|
||||
router.post('/block-device', blockDevice);
|
||||
router.post('/unblock-device', unblockDeviceAdmin);
|
||||
|
||||
// Новые маршруты для управления устройствами пользователя
|
||||
router.get('/user/:userId/devices', getUserDevices);
|
||||
router.post('/user/:userId/block-devices', blockUserDevices);
|
||||
|
||||
module.exports = router;
|
@ -318,5 +318,13 @@ export default {
|
||||
},
|
||||
unblockDevice(deviceFingerprint, reason) {
|
||||
return apiClient.post('/admin/unblock-device', { deviceFingerprint, reason });
|
||||
},
|
||||
|
||||
// Новые методы для управления устройствами пользователей
|
||||
getUserDevices(userId) {
|
||||
return apiClient.get(`/admin/user/${userId}/devices`);
|
||||
},
|
||||
blockUserDevices(userId, data = {}) {
|
||||
return apiClient.post(`/admin/user/${userId}/block-devices`, data);
|
||||
}
|
||||
};
|
@ -5,14 +5,26 @@
|
||||
« Назад к списку пользователей
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="user"
|
||||
@click="toggleUserStatus"
|
||||
class="status-btn"
|
||||
:class="user.isActive ? 'block-btn' : 'unblock-btn'"
|
||||
>
|
||||
{{ user.isActive ? 'Заблокировать' : 'Разблокировать' }}
|
||||
</button>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
v-if="user"
|
||||
@click="blockAllDevices"
|
||||
class="action-btn device-block-btn"
|
||||
:disabled="blockingDevices"
|
||||
>
|
||||
<span v-if="!blockingDevices">Блокировать устройства</span>
|
||||
<span v-else>Блокировка...</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="user"
|
||||
@click="toggleUserStatus"
|
||||
class="status-btn"
|
||||
:class="user.isActive ? 'block-btn' : 'unblock-btn'"
|
||||
>
|
||||
{{ user.isActive ? 'Заблокировать' : 'Разблокировать' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading-indicator">
|
||||
@ -135,14 +147,80 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Диалоговое окно для блокировки устройств -->
|
||||
<div v-if="showBlockDialog" class="modal-overlay" @click="cancelBlockDialog">
|
||||
<div class="modal" @click.stop>
|
||||
<div class="modal-header">
|
||||
<h3>Подтверждение блокировки устройств</h3>
|
||||
<button @click="cancelBlockDialog" class="close-btn">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Вы собираетесь заблокировать все устройства пользователя <strong>{{ user?.name }}</strong>.</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="blockReason">Выберите причину блокировки:</label>
|
||||
<select
|
||||
id="blockReason"
|
||||
v-model="blockDeviceData.reason"
|
||||
class="form-control"
|
||||
>
|
||||
<option value="multiple_violations">Множественные нарушения</option>
|
||||
<option value="spam">Спам</option>
|
||||
<option value="harassment">Домогательства</option>
|
||||
<option value="fake_profile">Фейковый профиль</option>
|
||||
<option value="other">Другое</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if="blockDeviceData.reason === 'other'" class="form-group">
|
||||
<label for="customReason">Укажите причину:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="customReason"
|
||||
v-model="blockDeviceData.customReason"
|
||||
placeholder="Опишите причину блокировки"
|
||||
class="form-control"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="blockDuration">Срок блокировки:</label>
|
||||
<select
|
||||
id="blockDuration"
|
||||
v-model="blockDeviceData.duration"
|
||||
class="form-control"
|
||||
>
|
||||
<option value="1d">1 день</option>
|
||||
<option value="3d">3 дня</option>
|
||||
<option value="7d">7 дней</option>
|
||||
<option value="30d">30 дней</option>
|
||||
<option value="permanent">Навсегда</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button @click="cancelBlockDialog" class="btn btn-outline">Отмена</button>
|
||||
<button
|
||||
@click="confirmBlockDevices"
|
||||
class="btn btn-danger"
|
||||
:disabled="!isBlockDeviceFormValid || blockingDevices"
|
||||
>
|
||||
<span v-if="!blockingDevices">Подтвердить блокировку</span>
|
||||
<span v-else>Блокировка...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useRouter } from 'vue-router';
|
||||
import axios from 'axios';
|
||||
import toastService from '@/services/toastService';
|
||||
import api from '@/services/api';
|
||||
|
||||
export default {
|
||||
name: 'AdminUserDetail',
|
||||
@ -154,10 +232,69 @@ export default {
|
||||
},
|
||||
setup(props) {
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const user = ref(null);
|
||||
const loading = ref(false);
|
||||
const error = ref(null);
|
||||
const blockingDevices = ref(false);
|
||||
const showBlockDialog = ref(false);
|
||||
|
||||
// Данные для блокировки устройств
|
||||
const blockDeviceData = ref({
|
||||
reason: 'multiple_violations',
|
||||
customReason: '',
|
||||
duration: 'permanent'
|
||||
});
|
||||
|
||||
// Проверка валидности формы блокировки устройства
|
||||
const isBlockDeviceFormValid = computed(() => {
|
||||
return blockDeviceData.value.reason !== 'other' ||
|
||||
(blockDeviceData.value.reason === 'other' && blockDeviceData.value.customReason.trim() !== '');
|
||||
});
|
||||
|
||||
// Открыть диалог блокировки всех устройств
|
||||
const blockAllDevices = () => {
|
||||
showBlockDialog.value = true;
|
||||
};
|
||||
|
||||
// Закрыть диалог блокировки
|
||||
const cancelBlockDialog = () => {
|
||||
showBlockDialog.value = false;
|
||||
};
|
||||
|
||||
// Подтвердить и выполнить блокировку устройств
|
||||
const confirmBlockDevices = async () => {
|
||||
if (!isBlockDeviceFormValid.value) return;
|
||||
|
||||
try {
|
||||
blockingDevices.value = true;
|
||||
|
||||
// Формируем причину блокировки
|
||||
const reason = blockDeviceData.value.reason === 'other'
|
||||
? blockDeviceData.value.customReason
|
||||
: blockDeviceData.value.reason;
|
||||
|
||||
// Отправляем запрос на блокировку всех устройств
|
||||
const response = await api.blockUserDevices(props.id, {
|
||||
reason: reason,
|
||||
duration: blockDeviceData.value.duration
|
||||
});
|
||||
|
||||
// Обрабатываем успешный ответ
|
||||
if (response.data.success) {
|
||||
toastService.add(`Успешно заблокировано ${response.data.blockedDevices.length} устройств`, 'success');
|
||||
} else {
|
||||
toastService.add('Не удалось заблокировать устройства', 'error');
|
||||
}
|
||||
|
||||
// Закрываем диалог
|
||||
showBlockDialog.value = false;
|
||||
} catch (err) {
|
||||
console.error('Ошибка при блокировке устройств:', err);
|
||||
toastService.add(err.response?.data?.message || 'Ошибка при блокировке устройств', 'error');
|
||||
} finally {
|
||||
blockingDevices.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Получить основное фото пользователя
|
||||
const userProfilePhoto = computed(() => {
|
||||
@ -167,12 +304,13 @@ export default {
|
||||
return profilePhoto ? profilePhoto.url : (user.value.photos.length > 0 ? user.value.photos[0].url : null);
|
||||
});
|
||||
|
||||
// Загрузить данные пользователя
|
||||
const loadUserDetails = async () => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('userToken'); // Исправлено с 'token' на 'userToken'
|
||||
const token = localStorage.getItem('userToken');
|
||||
const response = await axios.get(
|
||||
`/api/admin/users/${props.id}`,
|
||||
{
|
||||
@ -201,7 +339,7 @@ export default {
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
const token = localStorage.getItem('userToken'); // Исправлено с 'token' на 'userToken'
|
||||
const token = localStorage.getItem('userToken');
|
||||
const response = await axios.put(
|
||||
`/api/admin/users/${props.id}/toggle-active`,
|
||||
{},
|
||||
@ -215,7 +353,7 @@ export default {
|
||||
// Обновляем статус пользователя
|
||||
user.value.isActive = response.data.isActive;
|
||||
|
||||
// Заменяем alert на уведомление через сервис
|
||||
// Показываем уведомление
|
||||
const actionType = user.value.isActive ? 'success' : 'warning';
|
||||
toastService.add(response.data.message, actionType, 3000);
|
||||
} catch (err) {
|
||||
@ -292,7 +430,16 @@ export default {
|
||||
getUserInitials,
|
||||
getGenderText,
|
||||
calculateAge,
|
||||
formatDate
|
||||
formatDate,
|
||||
|
||||
// Новые методы для блокировки устройств
|
||||
blockingDevices,
|
||||
showBlockDialog,
|
||||
blockDeviceData,
|
||||
isBlockDeviceFormValid,
|
||||
blockAllDevices,
|
||||
cancelBlockDialog,
|
||||
confirmBlockDevices
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -328,7 +475,13 @@ export default {
|
||||
transform: translateX(-2px);
|
||||
}
|
||||
|
||||
.status-btn {
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.status-btn,
|
||||
.action-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
@ -337,10 +490,25 @@ export default {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.status-btn:hover {
|
||||
.status-btn:hover,
|
||||
.action-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.device-block-btn {
|
||||
background-color: rgba(33, 150, 243, 0.1);
|
||||
color: #2196f3;
|
||||
}
|
||||
|
||||
.device-block-btn:hover {
|
||||
background-color: rgba(33, 150, 243, 0.2);
|
||||
}
|
||||
|
||||
.device-block-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.block-btn {
|
||||
background-color: rgba(211, 47, 47, 0.1);
|
||||
color: #d32f2f;
|
||||
@ -722,4 +890,156 @@ h3 {
|
||||
padding-bottom: calc(50px + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
}
|
||||
|
||||
/* Стили для модального окна */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #6c757d;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
padding: 15px 20px;
|
||||
border-top: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
font-size: 1rem;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 4px;
|
||||
transition: border-color 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #80bdff;
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
|
||||
}
|
||||
|
||||
.form-text {
|
||||
margin-top: 5px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background-color: white;
|
||||
border: 1px solid #ced4da;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #c82333;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.mb-3 {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.header-controls {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.modal {
|
||||
width: 95%;
|
||||
}
|
||||
}
|
||||
</style>
|
Loading…
x
Reference in New Issue
Block a user