english version update
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, createContext, useContext } from 'react';
|
||||
import axios from 'axios';
|
||||
import { API_URL } from '../../config/api';
|
||||
import apiClient from '../../utils/apiClient';
|
||||
import { useTranslation } from '../../i18n';
|
||||
import {
|
||||
getProfile,
|
||||
updateProfile,
|
||||
@@ -30,10 +31,16 @@ import {
|
||||
|
||||
type TabType = 'profile' | 'security' | 'notifications' | 'api' | 'ssh' | 'delete';
|
||||
|
||||
// Context for sharing isEn across settings tabs
|
||||
const SettingsLangContext = createContext<boolean>(false);
|
||||
const useSettingsLang = () => useContext(SettingsLangContext);
|
||||
|
||||
const SettingsPage = () => {
|
||||
const [activeTab, setActiveTab] = useState<TabType>('profile');
|
||||
const [profile, setProfile] = useState<UserProfile | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { locale } = useTranslation();
|
||||
const isEn = locale === 'en';
|
||||
|
||||
useEffect(() => {
|
||||
loadProfile();
|
||||
@@ -60,21 +67,22 @@ const SettingsPage = () => {
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
{ id: 'profile' as TabType, label: 'Профиль' },
|
||||
{ id: 'security' as TabType, label: 'Безопасность' },
|
||||
{ id: 'notifications' as TabType, label: 'Уведомления' },
|
||||
{ id: 'api' as TabType, label: 'API ключи' },
|
||||
{ id: 'ssh' as TabType, label: 'SSH ключи' },
|
||||
{ id: 'delete' as TabType, label: 'Удаление' },
|
||||
{ id: 'profile' as TabType, label: isEn ? 'Profile' : 'Профиль' },
|
||||
{ id: 'security' as TabType, label: isEn ? 'Security' : 'Безопасность' },
|
||||
{ id: 'notifications' as TabType, label: isEn ? 'Notifications' : 'Уведомления' },
|
||||
{ id: 'api' as TabType, label: isEn ? 'API Keys' : 'API ключи' },
|
||||
{ id: 'ssh' as TabType, label: isEn ? 'SSH Keys' : 'SSH ключи' },
|
||||
{ id: 'delete' as TabType, label: isEn ? 'Delete' : 'Удаление' },
|
||||
];
|
||||
|
||||
return (
|
||||
<SettingsLangContext.Provider value={isEn}>
|
||||
<div className="p-4 lg:p-8">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Заголовок */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900">Настройки аккаунта</h1>
|
||||
<p className="text-gray-600 mt-2">Управление профилем, безопасностью и интеграциями</p>
|
||||
<h1 className="text-3xl font-bold text-gray-900">{isEn ? 'Account Settings' : 'Настройки аккаунта'}</h1>
|
||||
<p className="text-gray-600 mt-2">{isEn ? 'Manage profile, security and integrations' : 'Управление профилем, безопасностью и интеграциями'}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||
@@ -113,11 +121,13 @@ const SettingsPage = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsLangContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
// ============ ПРОФИЛЬ ============
|
||||
const ProfileTab = ({ profile, onUpdate }: { profile: UserProfile | null; onUpdate: () => void }) => {
|
||||
const isEn = useSettingsLang();
|
||||
const [username, setUsername] = useState(profile?.username || '');
|
||||
const [email, setEmail] = useState(profile?.email || '');
|
||||
const [phoneNumber, setPhoneNumber] = useState(profile?.profile?.phoneNumber || '');
|
||||
@@ -145,29 +155,29 @@ const ProfileTab = ({ profile, onUpdate }: { profile: UserProfile | null; onUpda
|
||||
try {
|
||||
setSaving(true);
|
||||
await uploadAvatar(avatarFile);
|
||||
alert('Аватар загружен!');
|
||||
alert(isEn ? 'Avatar uploaded!' : 'Аватар загружен!');
|
||||
onUpdate();
|
||||
setAvatarFile(null);
|
||||
setAvatarPreview(null);
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки аватара:', error);
|
||||
alert('Ошибка загрузки аватара');
|
||||
alert(isEn ? 'Error uploading avatar' : 'Ошибка загрузки аватара');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteAvatar = async () => {
|
||||
if (!confirm('Удалить аватар?')) return;
|
||||
if (!confirm(isEn ? 'Delete avatar?' : 'Удалить аватар?')) return;
|
||||
|
||||
try {
|
||||
setSaving(true);
|
||||
await deleteAvatar();
|
||||
alert('Аватар удалён');
|
||||
alert(isEn ? 'Avatar deleted' : 'Аватар удалён');
|
||||
onUpdate();
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления аватара:', error);
|
||||
alert('Ошибка удаления аватара');
|
||||
alert(isEn ? 'Error deleting avatar' : 'Ошибка удаления аватара');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -177,11 +187,11 @@ const ProfileTab = ({ profile, onUpdate }: { profile: UserProfile | null; onUpda
|
||||
try {
|
||||
setSaving(true);
|
||||
await updateProfile({ username, email, phoneNumber, timezone, language });
|
||||
alert('Профиль обновлён!');
|
||||
alert(isEn ? 'Profile updated!' : 'Профиль обновлён!');
|
||||
onUpdate();
|
||||
} catch (error) {
|
||||
console.error('Ошибка обновления профиля:', error);
|
||||
alert('Ошибка обновления профиля');
|
||||
alert(isEn ? 'Error updating profile' : 'Ошибка обновления профиля');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -190,13 +200,13 @@ const ProfileTab = ({ profile, onUpdate }: { profile: UserProfile | null; onUpda
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">Профиль</h2>
|
||||
<p className="text-gray-600">Обновите информацию о своём профиле</p>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">{isEn ? 'Profile' : 'Профиль'}</h2>
|
||||
<p className="text-gray-600">{isEn ? 'Update your profile information' : 'Обновите информацию о своём профиле'}</p>
|
||||
</div>
|
||||
|
||||
{/* Аватар */}
|
||||
<div className="border-t border-gray-200 pt-6">
|
||||
<h3 className="text-lg font-semibold mb-4">Аватар</h3>
|
||||
<h3 className="text-lg font-semibold mb-4">{isEn ? 'Avatar' : 'Аватар'}</h3>
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="w-24 h-24 rounded-full bg-gray-200 flex items-center justify-center overflow-hidden">
|
||||
{avatarPreview || profile?.profile?.avatarUrl ? (
|
||||
@@ -213,7 +223,7 @@ const ProfileTab = ({ profile, onUpdate }: { profile: UserProfile | null; onUpda
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="px-4 py-2 bg-ospab-primary text-white rounded-lg cursor-pointer hover:bg-ospab-accent transition">
|
||||
Выбрать файл
|
||||
{isEn ? 'Choose file' : 'Выбрать файл'}
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
@@ -227,7 +237,7 @@ const ProfileTab = ({ profile, onUpdate }: { profile: UserProfile | null; onUpda
|
||||
disabled={saving}
|
||||
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50"
|
||||
>
|
||||
Загрузить
|
||||
{isEn ? 'Upload' : 'Загрузить'}
|
||||
</button>
|
||||
)}
|
||||
{profile?.profile?.avatarUrl && (
|
||||
@@ -236,7 +246,7 @@ const ProfileTab = ({ profile, onUpdate }: { profile: UserProfile | null; onUpda
|
||||
disabled={saving}
|
||||
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 disabled:opacity-50"
|
||||
>
|
||||
Удалить
|
||||
{isEn ? 'Delete' : 'Удалить'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -245,10 +255,10 @@ const ProfileTab = ({ profile, onUpdate }: { profile: UserProfile | null; onUpda
|
||||
|
||||
{/* Основная информация */}
|
||||
<div className="border-t border-gray-200 pt-6 space-y-4">
|
||||
<h3 className="text-lg font-semibold mb-4">Основная информация</h3>
|
||||
<h3 className="text-lg font-semibold mb-4">{isEn ? 'Basic Information' : 'Основная информация'}</h3>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Имя пользователя</label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">{isEn ? 'Username' : 'Имя пользователя'}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
@@ -268,7 +278,7 @@ const ProfileTab = ({ profile, onUpdate }: { profile: UserProfile | null; onUpda
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Телефон (опционально)</label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">{isEn ? 'Phone (optional)' : 'Телефон (опционально)'}</label>
|
||||
<input
|
||||
type="tel"
|
||||
value={phoneNumber}
|
||||
@@ -280,7 +290,7 @@ const ProfileTab = ({ profile, onUpdate }: { profile: UserProfile | null; onUpda
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Часовой пояс</label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">{isEn ? 'Timezone' : 'Часовой пояс'}</label>
|
||||
<select
|
||||
value={timezone}
|
||||
onChange={(e) => setTimezone(e.target.value)}
|
||||
@@ -294,7 +304,7 @@ const ProfileTab = ({ profile, onUpdate }: { profile: UserProfile | null; onUpda
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Язык</label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">{isEn ? 'Language' : 'Язык'}</label>
|
||||
<select
|
||||
value={language}
|
||||
onChange={(e) => setLanguage(e.target.value)}
|
||||
@@ -311,7 +321,7 @@ const ProfileTab = ({ profile, onUpdate }: { profile: UserProfile | null; onUpda
|
||||
disabled={saving}
|
||||
className="px-6 py-3 bg-ospab-primary text-white rounded-lg font-medium hover:bg-ospab-accent disabled:opacity-50 transition"
|
||||
>
|
||||
{saving ? 'Сохранение...' : 'Сохранить изменения'}
|
||||
{saving ? (isEn ? 'Saving...' : 'Сохранение...') : (isEn ? 'Save changes' : 'Сохранить изменения')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -320,13 +330,14 @@ const ProfileTab = ({ profile, onUpdate }: { profile: UserProfile | null; onUpda
|
||||
|
||||
// ============ БЕЗОПАСНОСТЬ ============
|
||||
const SecurityTab = () => {
|
||||
const isEn = useSettingsLang();
|
||||
const [view, setView] = useState<'password' | 'sessions'>('password');
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">Безопасность</h2>
|
||||
<p className="text-gray-600">Управление паролем и активными сеансами</p>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">{isEn ? 'Security' : 'Безопасность'}</h2>
|
||||
<p className="text-gray-600">{isEn ? 'Manage password and active sessions' : 'Управление паролем и активными сеансами'}</p>
|
||||
</div>
|
||||
|
||||
{/* Sub-tabs */}
|
||||
@@ -339,7 +350,7 @@ const SecurityTab = () => {
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
Смена пароля
|
||||
{isEn ? 'Change Password' : 'Смена пароля'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setView('sessions')}
|
||||
@@ -349,7 +360,7 @@ const SecurityTab = () => {
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
Активные сеансы
|
||||
{isEn ? 'Active Sessions' : 'Активные сеансы'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -360,6 +371,7 @@ const SecurityTab = () => {
|
||||
};
|
||||
|
||||
const PasswordChange = () => {
|
||||
const isEn = useSettingsLang();
|
||||
const [currentPassword, setCurrentPassword] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
@@ -367,11 +379,11 @@ const PasswordChange = () => {
|
||||
|
||||
const getPasswordStrength = (password: string) => {
|
||||
if (password.length === 0) return { strength: 0, label: '' };
|
||||
if (password.length < 6) return { strength: 1, label: 'Слабый', color: 'bg-red-500' };
|
||||
if (password.length < 10) return { strength: 2, label: 'Средний', color: 'bg-yellow-500' };
|
||||
if (password.length < 6) return { strength: 1, label: isEn ? 'Weak' : 'Слабый', color: 'bg-red-500' };
|
||||
if (password.length < 10) return { strength: 2, label: isEn ? 'Medium' : 'Средний', color: 'bg-yellow-500' };
|
||||
if (!/[A-Z]/.test(password) || !/[0-9]/.test(password))
|
||||
return { strength: 2, label: 'Средний', color: 'bg-yellow-500' };
|
||||
return { strength: 3, label: 'Сильный', color: 'bg-green-500' };
|
||||
return { strength: 2, label: isEn ? 'Medium' : 'Средний', color: 'bg-yellow-500' };
|
||||
return { strength: 3, label: isEn ? 'Strong' : 'Сильный', color: 'bg-green-500' };
|
||||
};
|
||||
|
||||
const strength = getPasswordStrength(newPassword);
|
||||
@@ -380,20 +392,20 @@ const PasswordChange = () => {
|
||||
e.preventDefault();
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
alert('Пароли не совпадают');
|
||||
alert(isEn ? 'Passwords do not match' : 'Пароли не совпадают');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
await changePassword({ currentPassword, newPassword });
|
||||
alert('Пароль успешно изменён!');
|
||||
alert(isEn ? 'Password changed successfully!' : 'Пароль успешно изменён!');
|
||||
setCurrentPassword('');
|
||||
setNewPassword('');
|
||||
setConfirmPassword('');
|
||||
} catch (error) {
|
||||
console.error('Ошибка смены пароля:', error);
|
||||
alert('Ошибка смены пароля. Проверьте текущий пароль.');
|
||||
alert(isEn ? 'Password change error. Check current password.' : 'Ошибка смены пароля. Проверьте текущий пароль.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -402,7 +414,7 @@ const PasswordChange = () => {
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4 pt-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Текущий пароль</label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">{isEn ? 'Current password' : 'Текущий пароль'}</label>
|
||||
<input
|
||||
type="password"
|
||||
value={currentPassword}
|
||||
@@ -413,7 +425,7 @@ const PasswordChange = () => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Новый пароль</label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">{isEn ? 'New password' : 'Новый пароль'}</label>
|
||||
<input
|
||||
type="password"
|
||||
value={newPassword}
|
||||
@@ -434,13 +446,13 @@ const PasswordChange = () => {
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 mt-1">Сила пароля: {strength.label}</p>
|
||||
<p className="text-sm text-gray-600 mt-1">{isEn ? 'Password strength:' : 'Сила пароля:'} {strength.label}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Подтвердите новый пароль</label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">{isEn ? 'Confirm new password' : 'Подтвердите новый пароль'}</label>
|
||||
<input
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
@@ -455,13 +467,14 @@ const PasswordChange = () => {
|
||||
disabled={loading}
|
||||
className="px-6 py-3 bg-ospab-primary text-white rounded-lg font-medium hover:bg-ospab-accent disabled:opacity-50 transition"
|
||||
>
|
||||
{loading ? 'Изменение...' : 'Изменить пароль'}
|
||||
{loading ? (isEn ? 'Changing...' : 'Изменение...') : (isEn ? 'Change password' : 'Изменить пароль')}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
const ActiveSessions = () => {
|
||||
const isEn = useSettingsLang();
|
||||
const [sessions, setSessions] = useState<Session[]>([]);
|
||||
const [loginHistory, setLoginHistory] = useState<LoginHistoryEntry[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -494,29 +507,29 @@ const ActiveSessions = () => {
|
||||
};
|
||||
|
||||
const handleTerminate = async (id: number) => {
|
||||
if (!confirm('Вы уверены, что хотите завершить эту сессию?')) return;
|
||||
if (!confirm(isEn ? 'Are you sure you want to terminate this session?' : 'Вы уверены, что хотите завершить эту сессию?')) return;
|
||||
|
||||
try {
|
||||
await terminateSession(id);
|
||||
alert('Сеанс завершён');
|
||||
alert(isEn ? 'Session terminated' : 'Сеанс завершён');
|
||||
loadSessions();
|
||||
} catch (error) {
|
||||
console.error('Ошибка завершения сеанса:', error);
|
||||
alert('Не удалось завершить сессию');
|
||||
alert(isEn ? 'Failed to terminate session' : 'Не удалось завершить сессию');
|
||||
}
|
||||
};
|
||||
|
||||
const handleTerminateAllOthers = async () => {
|
||||
if (!confirm('Вы уверены, что хотите завершить все остальные сессии?')) return;
|
||||
if (!confirm(isEn ? 'Are you sure you want to terminate all other sessions?' : 'Вы уверены, что хотите завершить все остальные сессии?')) return;
|
||||
|
||||
try {
|
||||
// Используем API для завершения всех остальных сессий
|
||||
await apiClient.delete('/api/sessions/others/all');
|
||||
alert('Все остальные сессии завершены');
|
||||
alert(isEn ? 'All other sessions terminated' : 'Все остальные сессии завершены');
|
||||
loadSessions();
|
||||
} catch (error) {
|
||||
console.error('Ошибка завершения сессий:', error);
|
||||
alert('Не удалось завершить сессии');
|
||||
alert(isEn ? 'Failed to terminate sessions' : 'Не удалось завершить сессии');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -535,11 +548,11 @@ const ActiveSessions = () => {
|
||||
const diffHours = Math.floor(diffMs / 3600000);
|
||||
const diffDays = Math.floor(diffMs / 86400000);
|
||||
|
||||
if (diffMins < 1) return 'только что';
|
||||
if (diffMins < 60) return `${diffMins} мин. назад`;
|
||||
if (diffHours < 24) return `${diffHours} ч. назад`;
|
||||
if (diffDays < 7) return `${diffDays} дн. назад`;
|
||||
return date.toLocaleDateString('ru-RU');
|
||||
if (diffMins < 1) return isEn ? 'just now' : 'только что';
|
||||
if (diffMins < 60) return isEn ? `${diffMins} min ago` : `${diffMins} мин. назад`;
|
||||
if (diffHours < 24) return isEn ? `${diffHours}h ago` : `${diffHours} ч. назад`;
|
||||
if (diffDays < 7) return isEn ? `${diffDays}d ago` : `${diffDays} дн. назад`;
|
||||
return date.toLocaleDateString(isEn ? 'en-US' : 'ru-RU');
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
@@ -559,7 +572,7 @@ const ActiveSessions = () => {
|
||||
className="bg-orange-500 hover:bg-orange-600 text-white px-6 py-3 rounded-lg font-medium transition-colors duration-200 flex items-center gap-2"
|
||||
>
|
||||
<span>🚫</span>
|
||||
Завершить все остальные сессии
|
||||
{isEn ? 'Terminate all other sessions' : 'Завершить все остальные сессии'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -567,7 +580,7 @@ const ActiveSessions = () => {
|
||||
{/* Сессии в виде карточек */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{sessions.length === 0 ? (
|
||||
<p className="text-gray-600 text-center py-8 col-span-2">Нет активных сеансов</p>
|
||||
<p className="text-gray-600 text-center py-8 col-span-2">{isEn ? 'No active sessions' : 'Нет активных сеансов'}</p>
|
||||
) : (
|
||||
sessions.map((session) => {
|
||||
const isCurrent = session.isCurrent || session.device?.includes('Current');
|
||||
@@ -583,7 +596,7 @@ const ActiveSessions = () => {
|
||||
{isCurrent && (
|
||||
<div className="mb-3">
|
||||
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800">
|
||||
✓ Текущая сессия
|
||||
✓ {isEn ? 'Current Session' : 'Текущая сессия'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -593,12 +606,12 @@ const ActiveSessions = () => {
|
||||
<div className="text-4xl">{getDeviceIcon(session.device || 'desktop')}</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-1">
|
||||
{session.browser || 'Неизвестный браузер'} · {session.device || 'Desktop'}
|
||||
{session.browser || (isEn ? 'Unknown browser' : 'Неизвестный браузер')} · {session.device || 'Desktop'}
|
||||
</h3>
|
||||
<div className="space-y-1 text-sm text-gray-600">
|
||||
<p className="flex items-center gap-2">
|
||||
<span>🌐</span>
|
||||
<span>{session.ipAddress || 'Неизвестно'}</span>
|
||||
<span>{session.ipAddress || (isEn ? 'Unknown' : 'Неизвестно')}</span>
|
||||
</p>
|
||||
{session.location && (
|
||||
<p className="flex items-center gap-2">
|
||||
@@ -608,11 +621,11 @@ const ActiveSessions = () => {
|
||||
)}
|
||||
<p className="flex items-center gap-2">
|
||||
<span>⏱️</span>
|
||||
<span>Активность: {formatRelativeTime(session.lastActivity)}</span>
|
||||
<span>{isEn ? 'Activity' : 'Активность'}: {formatRelativeTime(session.lastActivity)}</span>
|
||||
</p>
|
||||
<p className="flex items-center gap-2 text-gray-500">
|
||||
<span>🔐</span>
|
||||
<span>Вход: {new Date(session.createdAt || session.lastActivity).toLocaleString('ru-RU')}</span>
|
||||
<span>{isEn ? 'Login' : 'Вход'}: {new Date(session.createdAt || session.lastActivity).toLocaleString(isEn ? 'en-US' : 'ru-RU')}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -625,7 +638,7 @@ const ActiveSessions = () => {
|
||||
onClick={() => handleTerminate(session.id)}
|
||||
className="w-full bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-lg font-medium transition-colors duration-200"
|
||||
>
|
||||
Завершить сессию
|
||||
{isEn ? 'Terminate Session' : 'Завершить сессию'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -644,8 +657,8 @@ const ActiveSessions = () => {
|
||||
className="w-full flex items-center justify-between text-left"
|
||||
>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-gray-900">История входов</h2>
|
||||
<p className="text-sm text-gray-600 mt-1">Последние 20 попыток входа в аккаунт</p>
|
||||
<h2 className="text-xl font-bold text-gray-900">{isEn ? 'Login History' : 'История входов'}</h2>
|
||||
<p className="text-sm text-gray-600 mt-1">{isEn ? 'Last 20 login attempts' : 'Последние 20 попыток входа в аккаунт'}</p>
|
||||
</div>
|
||||
<span className="text-2xl">{showHistory ? '▼' : '▶'}</span>
|
||||
</button>
|
||||
@@ -657,16 +670,16 @@ const ActiveSessions = () => {
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Статус
|
||||
{isEn ? 'Status' : 'Статус'}
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
IP адрес
|
||||
{isEn ? 'IP Address' : 'IP адрес'}
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Устройство
|
||||
{isEn ? 'Device' : 'Устройство'}
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Дата и время
|
||||
{isEn ? 'Date and Time' : 'Дата и время'}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -681,17 +694,17 @@ const ActiveSessions = () => {
|
||||
: 'bg-red-100 text-red-800'
|
||||
}`}
|
||||
>
|
||||
{entry.success ? '✓ Успешно' : '✗ Ошибка'}
|
||||
{entry.success ? (isEn ? '✓ Success' : '✓ Успешно') : (isEn ? '✗ Error' : '✗ Ошибка')}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{entry.ipAddress}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-600">
|
||||
{entry.userAgent ? entry.userAgent.substring(0, 60) + '...' : entry.device || 'Неизвестно'}
|
||||
{entry.userAgent ? entry.userAgent.substring(0, 60) + '...' : entry.device || (isEn ? 'Unknown' : 'Неизвестно')}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
|
||||
{new Date(entry.createdAt).toLocaleString('ru-RU')}
|
||||
{new Date(entry.createdAt).toLocaleString(isEn ? 'en-US' : 'ru-RU')}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -703,12 +716,12 @@ const ActiveSessions = () => {
|
||||
|
||||
{/* Советы по безопасности */}
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-blue-900 mb-3">💡 Советы по безопасности</h3>
|
||||
<h3 className="text-lg font-semibold text-blue-900 mb-3">💡 {isEn ? 'Security Tips' : 'Советы по безопасности'}</h3>
|
||||
<ul className="space-y-2 text-sm text-blue-800">
|
||||
<li>• Регулярно проверяйте список активных сессий</li>
|
||||
<li>• Завершайте сессии на устройствах, которыми больше не пользуетесь</li>
|
||||
<li>• Если вы видите подозрительную активность, немедленно завершите все сессии и смените пароль</li>
|
||||
<li>• Используйте надёжные пароли и двухфакторную аутентификацию</li>
|
||||
<li>• {isEn ? 'Regularly check the list of active sessions' : 'Регулярно проверяйте список активных сессий'}</li>
|
||||
<li>• {isEn ? 'Terminate sessions on devices you no longer use' : 'Завершайте сессии на устройствах, которыми больше не пользуетесь'}</li>
|
||||
<li>• {isEn ? 'If you see suspicious activity, immediately terminate all sessions and change password' : 'Если вы видите подозрительную активность, немедленно завершите все сессии и смените пароль'}</li>
|
||||
<li>• {isEn ? 'Use strong passwords and two-factor authentication' : 'Используйте надёжные пароли и двухфакторную аутентификацию'}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -717,6 +730,7 @@ const ActiveSessions = () => {
|
||||
|
||||
// ============ УВЕДОМЛЕНИЯ ============
|
||||
const NotificationsTab = () => {
|
||||
const isEn = useSettingsLang();
|
||||
const [settings, setSettings] = useState<NotificationSettings | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
@@ -748,7 +762,7 @@ const NotificationsTab = () => {
|
||||
await updateNotificationSettings({ [field]: value });
|
||||
} catch (error) {
|
||||
console.error('Ошибка обновления настроек:', error);
|
||||
alert('Ошибка сохранения настроек');
|
||||
alert(isEn ? 'Error saving settings' : 'Ошибка сохранения настроек');
|
||||
loadSettings(); // Revert
|
||||
} finally {
|
||||
setSaving(false);
|
||||
@@ -760,7 +774,7 @@ const NotificationsTab = () => {
|
||||
}
|
||||
|
||||
if (!settings) {
|
||||
return <div className="text-center py-8 text-gray-600">Ошибка загрузки настроек</div>;
|
||||
return <div className="text-center py-8 text-gray-600">{isEn ? 'Error loading settings' : 'Ошибка загрузки настроек'}</div>;
|
||||
}
|
||||
|
||||
const emailSettings = [
|
||||
@@ -779,13 +793,13 @@ const NotificationsTab = () => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">Уведомления</h2>
|
||||
<p className="text-gray-600">Настройте способы получения уведомлений</p>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">{isEn ? 'Notifications' : 'Уведомления'}</h2>
|
||||
<p className="text-gray-600">{isEn ? 'Configure notification methods' : 'Настройте способы получения уведомлений'}</p>
|
||||
</div>
|
||||
|
||||
{/* Email уведомления */}
|
||||
<div className="border-t border-gray-200 pt-6">
|
||||
<h3 className="text-lg font-semibold mb-4">Email уведомления</h3>
|
||||
<h3 className="text-lg font-semibold mb-4">{isEn ? 'Email Notifications' : 'Email уведомления'}</h3>
|
||||
<div className="space-y-3">
|
||||
{emailSettings.map((setting) => (
|
||||
<label key={setting.key} className="flex items-center justify-between p-3 hover:bg-gray-50 rounded-lg cursor-pointer">
|
||||
@@ -804,7 +818,7 @@ const NotificationsTab = () => {
|
||||
|
||||
{/* Push уведомления */}
|
||||
<div className="border-t border-gray-200 pt-6">
|
||||
<h3 className="text-lg font-semibold mb-4">Push уведомления</h3>
|
||||
<h3 className="text-lg font-semibold mb-4">{isEn ? 'Push Notifications' : 'Push уведомления'}</h3>
|
||||
<div className="space-y-3">
|
||||
{pushSettings.map((setting) => (
|
||||
<label key={setting.key} className="flex items-center justify-between p-3 hover:bg-gray-50 rounded-lg cursor-pointer">
|
||||
@@ -824,7 +838,7 @@ const NotificationsTab = () => {
|
||||
{saving && (
|
||||
<div className="text-sm text-gray-600 flex items-center gap-2">
|
||||
<div className="w-4 h-4 border-2 border-ospab-primary border-t-transparent rounded-full animate-spin"></div>
|
||||
Сохранение...
|
||||
{isEn ? 'Saving...' : 'Сохранение...'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -833,6 +847,7 @@ const NotificationsTab = () => {
|
||||
|
||||
// ============ API КЛЮЧИ ============
|
||||
const APIKeysTab = () => {
|
||||
const isEn = useSettingsLang();
|
||||
const [keys, setKeys] = useState<APIKey[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
@@ -861,20 +876,20 @@ const APIKeysTab = () => {
|
||||
loadKeys();
|
||||
} catch (error) {
|
||||
console.error('Ошибка создания ключа:', error);
|
||||
alert('Ошибка создания ключа');
|
||||
alert(isEn ? 'Error creating key' : 'Ошибка создания ключа');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (!confirm('Удалить этот API ключ? Приложения, использующие его, перестанут работать.')) return;
|
||||
if (!confirm(isEn ? 'Delete this API key? Applications using it will stop working.' : 'Удалить этот API ключ? Приложения, использующие его, перестанут работать.')) return;
|
||||
|
||||
try {
|
||||
await deleteAPIKey(id);
|
||||
alert('Ключ удалён');
|
||||
alert(isEn ? 'Key deleted' : 'Ключ удалён');
|
||||
loadKeys();
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления ключа:', error);
|
||||
alert('Ошибка удаления ключа');
|
||||
alert(isEn ? 'Error deleting key' : 'Ошибка удаления ключа');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -886,29 +901,29 @@ const APIKeysTab = () => {
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">API ключи</h2>
|
||||
<p className="text-gray-600">Управление ключами для интеграций</p>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">{isEn ? 'API Keys' : 'API ключи'}</h2>
|
||||
<p className="text-gray-600">{isEn ? 'Manage integration keys' : 'Управление ключами для интеграций'}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowModal(true)}
|
||||
className="px-4 py-2 bg-ospab-primary text-white rounded-lg hover:bg-ospab-accent transition"
|
||||
>
|
||||
Создать ключ
|
||||
{isEn ? 'Create Key' : 'Создать ключ'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{keys.length === 0 ? (
|
||||
<p className="text-gray-600 text-center py-8">Нет созданных ключей</p>
|
||||
<p className="text-gray-600 text-center py-8">{isEn ? 'No keys created' : 'Нет созданных ключей'}</p>
|
||||
) : (
|
||||
<div className="border border-gray-200 rounded-lg overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="text-left py-3 px-4 font-medium text-gray-700">Название</th>
|
||||
<th className="text-left py-3 px-4 font-medium text-gray-700">Префикс</th>
|
||||
<th className="text-left py-3 px-4 font-medium text-gray-700">Создан</th>
|
||||
<th className="text-left py-3 px-4 font-medium text-gray-700">Последнее использование</th>
|
||||
<th className="text-right py-3 px-4 font-medium text-gray-700">Действия</th>
|
||||
<th className="text-left py-3 px-4 font-medium text-gray-700">{isEn ? 'Name' : 'Название'}</th>
|
||||
<th className="text-left py-3 px-4 font-medium text-gray-700">{isEn ? 'Prefix' : 'Префикс'}</th>
|
||||
<th className="text-left py-3 px-4 font-medium text-gray-700">{isEn ? 'Created' : 'Создан'}</th>
|
||||
<th className="text-left py-3 px-4 font-medium text-gray-700">{isEn ? 'Last Used' : 'Последнее использование'}</th>
|
||||
<th className="text-right py-3 px-4 font-medium text-gray-700">{isEn ? 'Actions' : 'Действия'}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -917,17 +932,17 @@ const APIKeysTab = () => {
|
||||
<td className="py-3 px-4">{key.name}</td>
|
||||
<td className="py-3 px-4 font-mono text-sm">{key.prefix}...</td>
|
||||
<td className="py-3 px-4 text-sm text-gray-600">
|
||||
{new Date(key.createdAt).toLocaleDateString('ru-RU')}
|
||||
{new Date(key.createdAt).toLocaleDateString(isEn ? 'en-US' : 'ru-RU')}
|
||||
</td>
|
||||
<td className="py-3 px-4 text-sm text-gray-600">
|
||||
{key.lastUsed ? new Date(key.lastUsed).toLocaleDateString('ru-RU') : 'Никогда'}
|
||||
{key.lastUsed ? new Date(key.lastUsed).toLocaleDateString(isEn ? 'en-US' : 'ru-RU') : (isEn ? 'Never' : 'Никогда')}
|
||||
</td>
|
||||
<td className="py-3 px-4 text-right">
|
||||
<button
|
||||
onClick={() => handleDelete(key.id)}
|
||||
className="text-red-600 hover:text-red-700 text-sm"
|
||||
>
|
||||
Удалить
|
||||
{isEn ? 'Delete' : 'Удалить'}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -971,16 +986,16 @@ const CreateAPIKeyModal = ({ onClose, onCreate }: { onClose: () => void; onCreat
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-6 max-w-md w-full">
|
||||
<h3 className="text-xl font-bold mb-4">Создать API ключ</h3>
|
||||
<h3 className="text-xl font-bold mb-4">Create API Key</h3>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Название</label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
required
|
||||
placeholder="Мой проект"
|
||||
placeholder="My project"
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-ospab-primary focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
@@ -990,14 +1005,14 @@ const CreateAPIKeyModal = ({ onClose, onCreate }: { onClose: () => void; onCreat
|
||||
onClick={onClose}
|
||||
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
|
||||
>
|
||||
Отмена
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="flex-1 px-4 py-2 bg-ospab-primary text-white rounded-lg hover:bg-ospab-accent disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Создание...' : 'Создать'}
|
||||
{loading ? 'Creating...' : 'Create'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1018,10 +1033,10 @@ const ShowNewKeyModal = ({ keyData, onClose }: { keyData: { fullKey: string }; o
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-6 max-w-2xl w-full">
|
||||
<h3 className="text-xl font-bold mb-4 text-green-600">Ключ успешно создан!</h3>
|
||||
<h3 className="text-xl font-bold mb-4 text-green-600">Key created successfully!</h3>
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-4">
|
||||
<p className="text-sm text-yellow-800 font-medium">
|
||||
Сохраните этот ключ сейчас! Он больше не будет показан.
|
||||
Save this key now! It will not be shown again.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gray-100 rounded-lg p-4 mb-4 font-mono text-sm break-all">
|
||||
@@ -1032,13 +1047,13 @@ const ShowNewKeyModal = ({ keyData, onClose }: { keyData: { fullKey: string }; o
|
||||
onClick={handleCopy}
|
||||
className="flex-1 px-4 py-2 bg-ospab-primary text-white rounded-lg hover:bg-ospab-accent"
|
||||
>
|
||||
{copied ? 'Скопировано!' : 'Копировать'}
|
||||
{copied ? 'Copied!' : 'Copy'}
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
|
||||
>
|
||||
Закрыть
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1048,6 +1063,7 @@ const ShowNewKeyModal = ({ keyData, onClose }: { keyData: { fullKey: string }; o
|
||||
|
||||
// ============ SSH КЛЮЧИ ============
|
||||
const SSHKeysTab = () => {
|
||||
const isEn = useSettingsLang();
|
||||
const [keys, setKeys] = useState<SSHKey[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
@@ -1071,25 +1087,25 @@ const SSHKeysTab = () => {
|
||||
const handleAdd = async (name: string, publicKey: string) => {
|
||||
try {
|
||||
await addSSHKey({ name, publicKey });
|
||||
alert('SSH ключ добавлен');
|
||||
alert(isEn ? 'SSH key added' : 'SSH ключ добавлен');
|
||||
loadKeys();
|
||||
setShowModal(false);
|
||||
} catch (error) {
|
||||
console.error('Ошибка добавления ключа:', error);
|
||||
alert('Ошибка добавления ключа. Проверьте формат.');
|
||||
alert(isEn ? 'Error adding key. Check the format.' : 'Ошибка добавления ключа. Проверьте формат.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (!confirm('Удалить этот SSH ключ?')) return;
|
||||
if (!confirm(isEn ? 'Delete this SSH key?' : 'Удалить этот SSH ключ?')) return;
|
||||
|
||||
try {
|
||||
await deleteSSHKey(id);
|
||||
alert('Ключ удалён');
|
||||
alert(isEn ? 'Key deleted' : 'Ключ удалён');
|
||||
loadKeys();
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления ключа:', error);
|
||||
alert('Ошибка удаления ключа');
|
||||
alert(isEn ? 'Error deleting key' : 'Ошибка удаления ключа');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1101,19 +1117,19 @@ const SSHKeysTab = () => {
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">SSH ключи</h2>
|
||||
<p className="text-gray-600">Управление SSH ключами для доступа к серверам</p>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">{isEn ? 'SSH Keys' : 'SSH ключи'}</h2>
|
||||
<p className="text-gray-600">{isEn ? 'Manage SSH keys for server access' : 'Управление SSH ключами для доступа к серверам'}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowModal(true)}
|
||||
className="px-4 py-2 bg-ospab-primary text-white rounded-lg hover:bg-ospab-accent transition"
|
||||
>
|
||||
Добавить ключ
|
||||
{isEn ? 'Add Key' : 'Добавить ключ'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{keys.length === 0 ? (
|
||||
<p className="text-gray-600 text-center py-8">Нет добавленных ключей</p>
|
||||
<p className="text-gray-600 text-center py-8">{isEn ? 'No keys added' : 'Нет добавленных ключей'}</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{keys.map((key) => (
|
||||
@@ -1124,15 +1140,15 @@ const SSHKeysTab = () => {
|
||||
onClick={() => handleDelete(key.id)}
|
||||
className="text-red-600 hover:text-red-700 text-sm"
|
||||
>
|
||||
Удалить
|
||||
{isEn ? 'Delete' : 'Удалить'}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 mb-1">
|
||||
Отпечаток: <span className="font-mono">{key.fingerprint}</span>
|
||||
{isEn ? 'Fingerprint' : 'Отпечаток'}: <span className="font-mono">{key.fingerprint}</span>
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
Добавлен: {new Date(key.createdAt).toLocaleDateString('ru-RU')}
|
||||
{key.lastUsed && ` • Использован: ${new Date(key.lastUsed).toLocaleDateString('ru-RU')}`}
|
||||
{isEn ? 'Added' : 'Добавлен'}: {new Date(key.createdAt).toLocaleDateString(isEn ? 'en-US' : 'ru-RU')}
|
||||
{key.lastUsed && ` • ${isEn ? 'Used' : 'Использован'}: ${new Date(key.lastUsed).toLocaleDateString(isEn ? 'en-US' : 'ru-RU')}`}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
@@ -1164,21 +1180,21 @@ const AddSSHKeyModal = ({ onClose, onAdd }: { onClose: () => void; onAdd: (name:
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-6 max-w-2xl w-full">
|
||||
<h3 className="text-xl font-bold mb-4">Добавить SSH ключ</h3>
|
||||
<h3 className="text-xl font-bold mb-4">Add SSH Key</h3>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Название</label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
required
|
||||
placeholder="Мой ноутбук"
|
||||
placeholder="My laptop"
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-ospab-primary focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Публичный ключ</label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Public Key</label>
|
||||
<textarea
|
||||
value={publicKey}
|
||||
onChange={(e) => setPublicKey(e.target.value)}
|
||||
@@ -1188,7 +1204,7 @@ const AddSSHKeyModal = ({ onClose, onAdd }: { onClose: () => void; onAdd: (name:
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg font-mono text-sm focus:ring-2 focus:ring-ospab-primary focus:border-transparent"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Скопируйте содержимое файла ~/.ssh/id_rsa.pub или ~/.ssh/id_ed25519.pub
|
||||
Copy the contents of ~/.ssh/id_rsa.pub or ~/.ssh/id_ed25519.pub
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
@@ -1197,14 +1213,14 @@ const AddSSHKeyModal = ({ onClose, onAdd }: { onClose: () => void; onAdd: (name:
|
||||
onClick={onClose}
|
||||
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
|
||||
>
|
||||
Отмена
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="flex-1 px-4 py-2 bg-ospab-primary text-white rounded-lg hover:bg-ospab-accent disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Добавление...' : 'Добавить'}
|
||||
{loading ? 'Adding...' : 'Add'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1215,6 +1231,7 @@ const AddSSHKeyModal = ({ onClose, onAdd }: { onClose: () => void; onAdd: (name:
|
||||
|
||||
// ============ УДАЛЕНИЕ АККАУНТА ============
|
||||
const DeleteAccountTab = () => {
|
||||
const isEn = useSettingsLang();
|
||||
const [showConfirm, setShowConfirm] = useState(false);
|
||||
|
||||
const handleExport = async () => {
|
||||
@@ -1231,44 +1248,44 @@ const DeleteAccountTab = () => {
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Ошибка экспорта данных:', error);
|
||||
alert('Ошибка экспорта данных');
|
||||
alert(isEn ? 'Error exporting data' : 'Ошибка экспорта данных');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">Удаление аккаунта</h2>
|
||||
<p className="text-gray-600">Экспорт данных и безвозвратное удаление аккаунта</p>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">{isEn ? 'Delete Account' : 'Удаление аккаунта'}</h2>
|
||||
<p className="text-gray-600">{isEn ? 'Export data and permanently delete account' : 'Экспорт данных и безвозвратное удаление аккаунта'}</p>
|
||||
</div>
|
||||
|
||||
{/* Экспорт данных */}
|
||||
<div className="border-t border-gray-200 pt-6">
|
||||
<h3 className="text-lg font-semibold mb-4">Экспорт данных</h3>
|
||||
<h3 className="text-lg font-semibold mb-4">{isEn ? 'Export Data' : 'Экспорт данных'}</h3>
|
||||
<p className="text-gray-600 mb-4">
|
||||
Скачайте копию всех ваших данных включая профиль, серверы, тикеты и транзакции в формате JSON.
|
||||
{isEn ? 'Download a copy of all your data including profile, servers, tickets and transactions in JSON format.' : 'Скачайте копию всех ваших данных включая профиль, серверы, тикеты и транзакции в формате JSON.'}
|
||||
</p>
|
||||
<button
|
||||
onClick={handleExport}
|
||||
className="px-6 py-3 bg-ospab-primary text-white rounded-lg font-medium hover:bg-ospab-accent transition"
|
||||
>
|
||||
Скачать мои данные
|
||||
{isEn ? 'Download My Data' : 'Скачать мои данные'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Удаление аккаунта */}
|
||||
<div className="border-t border-gray-200 pt-6">
|
||||
<div className="bg-red-50 border-2 border-red-200 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-red-900 mb-4">Опасная зона</h3>
|
||||
<h3 className="text-lg font-semibold text-red-900 mb-4">{isEn ? 'Danger Zone' : 'Опасная зона'}</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<svg className="w-6 h-6 text-red-600 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd"/>
|
||||
</svg>
|
||||
<div>
|
||||
<p className="text-red-900 font-medium">Это действие необратимо</p>
|
||||
<p className="text-red-900 font-medium">{isEn ? 'This action is irreversible' : 'Это действие необратимо'}</p>
|
||||
<p className="text-red-700 text-sm mt-1">
|
||||
Все ваши серверы будут остановлены и удалены. История платежей, тикеты и другие данные будут безвозвратно удалены.
|
||||
{isEn ? 'All your servers will be stopped and deleted. Payment history, tickets and other data will be permanently deleted.' : 'Все ваши серверы будут остановлены и удалены. История платежей, тикеты и другие данные будут безвозвратно удалены.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1276,7 +1293,7 @@ const DeleteAccountTab = () => {
|
||||
onClick={() => setShowConfirm(true)}
|
||||
className="px-6 py-3 bg-red-600 text-white rounded-lg font-medium hover:bg-red-700 transition"
|
||||
>
|
||||
Удалить мой аккаунт
|
||||
{isEn ? 'Delete My Account' : 'Удалить мой аккаунт'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user