english version update

This commit is contained in:
Georgiy Syralev
2025-12-31 19:59:43 +03:00
parent b799f278a4
commit a2809a705f
57 changed files with 4263 additions and 1333 deletions

View File

@@ -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>