Compare commits
2 Commits
4451b3e45a
...
cc2bb349e5
| Author | SHA1 | Date | |
|---|---|---|---|
| cc2bb349e5 | |||
| 9ced08b88c |
@@ -62,6 +62,7 @@ class UpdateProfileRequest(BaseModel):
|
||||
full_name: Optional[str] = Field(None, max_length=100)
|
||||
email: Optional[EmailStr] = None
|
||||
ssh_public_key: Optional[str] = Field(None, max_length=10000, description="SSH public key for VM/LXC deployment")
|
||||
language: Optional[str] = Field(None, pattern='^(ru|en)$')
|
||||
|
||||
|
||||
class ChangePasswordRequest(BaseModel):
|
||||
@@ -77,6 +78,7 @@ class PanelSettingsResponse(BaseModel):
|
||||
|
||||
|
||||
class UpdatePanelSettingsRequest(BaseModel):
|
||||
panel_name: Optional[str] = Field(None, min_length=1, max_length=100)
|
||||
log_retention_days: Optional[int] = Field(None, ge=1, le=365)
|
||||
language: Optional[str] = Field(None, pattern='^(ru|en)$')
|
||||
|
||||
@@ -115,6 +117,7 @@ async def get_profile(
|
||||
"is_admin": current_user.is_admin,
|
||||
"is_active": current_user.is_active,
|
||||
"ssh_public_key": current_user.ssh_public_key,
|
||||
"language": current_user.language or 'ru',
|
||||
"created_at": current_user.created_at.isoformat() if current_user.created_at else None,
|
||||
"last_login": current_user.last_login.isoformat() if current_user.last_login else None
|
||||
}
|
||||
@@ -157,6 +160,10 @@ async def update_profile(
|
||||
)
|
||||
current_user.ssh_public_key = ssh_key if ssh_key else None
|
||||
updated_fields.append("ssh_public_key")
|
||||
|
||||
if data.language is not None:
|
||||
current_user.language = data.language
|
||||
updated_fields.append("language")
|
||||
|
||||
if updated_fields:
|
||||
db.commit()
|
||||
@@ -241,11 +248,12 @@ async def get_panel_settings(
|
||||
from ..config import settings
|
||||
|
||||
# Get settings from database or use defaults
|
||||
panel_name = get_setting(db, "panel_name", settings.PANEL_NAME)
|
||||
log_retention_days = get_setting(db, "log_retention_days", "30")
|
||||
language = get_setting(db, "language", "ru")
|
||||
|
||||
|
||||
return {
|
||||
"panel_name": settings.PANEL_NAME,
|
||||
"panel_name": panel_name,
|
||||
"log_retention_days": int(log_retention_days),
|
||||
"language": language
|
||||
}
|
||||
@@ -261,6 +269,10 @@ async def update_panel_settings(
|
||||
updated_fields = []
|
||||
|
||||
# Save settings to database
|
||||
if data.panel_name is not None:
|
||||
set_setting(db, "panel_name", data.panel_name, "Название панели")
|
||||
updated_fields.append(f"panel_name={data.panel_name}")
|
||||
|
||||
if data.log_retention_days is not None:
|
||||
set_setting(db, "log_retention_days", str(data.log_retention_days), "Срок хранения логов (дни)")
|
||||
updated_fields.append(f"log_retention_days={data.log_retention_days}")
|
||||
|
||||
@@ -53,6 +53,9 @@ class User(Base):
|
||||
|
||||
# SSH Public Key for VM/LXC deployment
|
||||
ssh_public_key = Column(Text, nullable=True)
|
||||
|
||||
# UI language preference (ru/en)
|
||||
language = Column(String(10), nullable=False, server_default='ru')
|
||||
|
||||
# Security fields
|
||||
failed_login_attempts = Column(Integer, default=0, nullable=False)
|
||||
|
||||
@@ -1366,6 +1366,14 @@ def migrate_proxmox_task_progress(conn):
|
||||
logger.info("✓ proxmox_tasks.progress already exists")
|
||||
|
||||
|
||||
def migrate_user_language(conn):
|
||||
"""Migration 26: Add 'language' column to users table (per-user UI language)."""
|
||||
if add_column_if_not_exists(conn, 'users', 'language', "VARCHAR(10) DEFAULT 'ru'"):
|
||||
logger.info("✓ Added language column to users")
|
||||
else:
|
||||
logger.info("✓ users.language already exists")
|
||||
|
||||
|
||||
def run_all_migrations(engine, db_session=None):
|
||||
"""
|
||||
Run all migrations in order.
|
||||
@@ -1580,6 +1588,14 @@ def run_all_migrations(engine, db_session=None):
|
||||
logger.warning(f"Proxmox task progress migration: {e}")
|
||||
conn.rollback()
|
||||
|
||||
# Migration 26: users.language column
|
||||
try:
|
||||
migrate_user_language(conn)
|
||||
conn.commit()
|
||||
except Exception as e:
|
||||
logger.warning(f"User language migration: {e}")
|
||||
conn.rollback()
|
||||
|
||||
logger.info("=" * 50)
|
||||
logger.info("✓ All migrations completed successfully")
|
||||
logger.info("=" * 50)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import i18n from '@/lib/i18n';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
@@ -14,7 +15,7 @@ import {
|
||||
useNotificationChannels, useUpdateSMTP, useUpdateTelegram,
|
||||
useAppVersion, useCheckUpdates,
|
||||
useUpdateRepository, useSetUpdateRepository, useUpdateStatus, usePerformUpdate, useResetUpdate,
|
||||
useTestSMTP, useTestTelegram,
|
||||
useTestSMTP,
|
||||
} from '@/hooks/use-settings';
|
||||
import {
|
||||
useNotificationPreferences, useUpdateNotificationPreferences,
|
||||
@@ -60,15 +61,16 @@ function ProfileTab() {
|
||||
const changePassword = useChangePassword();
|
||||
const [displayName, setDisplayName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [lang, setLang] = useState('ru');
|
||||
const [currentPwd, setCurrentPwd] = useState('');
|
||||
const [newPwd, setNewPwd] = useState('');
|
||||
const [confirmPwd, setConfirmPwd] = useState('');
|
||||
|
||||
// Init from profile
|
||||
useEffect(() => {
|
||||
if (profile) {
|
||||
setDisplayName(profile.full_name || '');
|
||||
setEmail(profile.email || '');
|
||||
setLang(profile.language || 'ru');
|
||||
}
|
||||
}, [profile]);
|
||||
|
||||
@@ -80,8 +82,15 @@ function ProfileTab() {
|
||||
<div><Label>{t('settings.username')}</Label><Input value={profile?.username || ''} disabled className="mt-1" /></div>
|
||||
<div><Label>{t('settings.display_name')}</Label><Input value={displayName} onChange={e => setDisplayName(e.target.value)} className="mt-1" /></div>
|
||||
<div><Label>Email</Label><Input value={email} onChange={e => setEmail(e.target.value)} className="mt-1" /></div>
|
||||
<Button size="sm" onClick={() => updateProfile.mutate({ full_name: displayName, email }, {
|
||||
onSuccess: (r) => toast.success(r.message),
|
||||
<div>
|
||||
<Label>{t('settings.language')}</Label>
|
||||
<select value={lang} onChange={e => setLang(e.target.value)} className="mt-1 block w-full rounded-md border bg-background px-3 py-2 text-sm">
|
||||
<option value="ru">Русский</option>
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
</div>
|
||||
<Button size="sm" onClick={() => updateProfile.mutate({ full_name: displayName, email, language: lang }, {
|
||||
onSuccess: (r) => { i18n.changeLanguage(lang); toast.success(r.message); },
|
||||
onError: (e: Error) => toast.error(e.message),
|
||||
})}>{t('common.save')}</Button>
|
||||
</CardContent>
|
||||
@@ -109,13 +118,11 @@ function PanelTab() {
|
||||
const { t } = useTranslation();
|
||||
const { data: settings } = usePanelSettings();
|
||||
const updatePanel = useUpdatePanelSettings();
|
||||
const [panelName, setPanelName] = useState('');
|
||||
const [lang, setLang] = useState('');
|
||||
const [logRetention, setLogRetention] = useState('30');
|
||||
|
||||
useEffect(() => {
|
||||
if (settings) {
|
||||
setPanelName(settings.panel_name || '');
|
||||
setLang(settings.language || 'ru');
|
||||
setLogRetention(String(settings.log_retention_days || 30));
|
||||
}
|
||||
}, [settings]);
|
||||
|
||||
@@ -123,15 +130,16 @@ function PanelTab() {
|
||||
<Card>
|
||||
<CardHeader><CardTitle className="text-sm">{t('settings.panel_settings')}</CardTitle></CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div><Label>{t('settings.panel_name')}</Label><Input value={panelName} onChange={e => setPanelName(e.target.value)} className="mt-1" /></div>
|
||||
<div><Label>{t('settings.panel_name')}</Label><Input value={settings?.panel_name || ''} disabled className="mt-1" /></div>
|
||||
<div>
|
||||
<Label>{t('settings.language')}</Label>
|
||||
<select value={lang} onChange={e => setLang(e.target.value)} className="mt-1 block w-full rounded-md border bg-background px-3 py-2 text-sm">
|
||||
<option value="ru">Русский</option>
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
<Label>{t('settings.log_retention_days')}</Label>
|
||||
<Input type="number" value={logRetention} onChange={e => setLogRetention(e.target.value)} className="mt-1" min={1} max={365} />
|
||||
<p className="text-xs text-muted-foreground mt-1">{t('settings.days')}</p>
|
||||
</div>
|
||||
<Button size="sm" onClick={() => updatePanel.mutate({ panel_name: panelName, language: lang })}>{t('common.save')}</Button>
|
||||
<Button size="sm" onClick={() => updatePanel.mutate({ log_retention_days: Number(logRetention) }, {
|
||||
onSuccess: (r) => toast.success(r.message),
|
||||
onError: (e: Error) => toast.error(e.message),
|
||||
})}>{t('common.save')}</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
@@ -184,7 +192,6 @@ function NotificationsTab() {
|
||||
const [smtpPass, setSmtpPass] = useState('');
|
||||
const [smtpFrom, setSmtpFrom] = useState('');
|
||||
const [tgToken, setTgToken] = useState('');
|
||||
const [tgChatId, setTgChatId] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (channels?.smtp) {
|
||||
@@ -198,12 +205,10 @@ function NotificationsTab() {
|
||||
useEffect(() => {
|
||||
if (channels?.telegram) {
|
||||
setTgToken(String(channels.telegram.telegram_bot_token ?? channels.telegram.bot_token ?? ''));
|
||||
setTgChatId(String(channels.telegram.chat_id ?? ''));
|
||||
}
|
||||
}, [channels?.telegram]);
|
||||
|
||||
const testSmtp = useTestSMTP();
|
||||
const testTg = useTestTelegram();
|
||||
const botInfo = useTelegramBotInfo();
|
||||
const [testEmail, setTestEmail] = useState('');
|
||||
|
||||
@@ -214,13 +219,6 @@ function NotificationsTab() {
|
||||
onError: (e: Error) => toast.error(e.message),
|
||||
});
|
||||
};
|
||||
const handleTestTg = () => {
|
||||
if (!tgChatId) { toast.error(t('settings.chat_id_required')); return; }
|
||||
testTg.mutate(tgChatId, {
|
||||
onSuccess: (r) => toast.success(r.message),
|
||||
onError: (e: Error) => toast.error(e.message),
|
||||
});
|
||||
};
|
||||
|
||||
const bot = botInfo.data?.bot_info as { username?: string; first_name?: string } | null | undefined;
|
||||
|
||||
@@ -255,14 +253,10 @@ function NotificationsTab() {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div><Label>Bot Token</Label><Input value={tgToken} onChange={e => setTgToken(e.target.value)} className="mt-1" /></div>
|
||||
<div><Label>Chat ID</Label><Input value={tgChatId} onChange={e => setTgChatId(e.target.value)} className="mt-1" /></div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button size="sm" onClick={() => updateTg.mutate({ telegram_bot_token: tgToken }, {
|
||||
onSuccess: (r) => toast.success(r.message),
|
||||
onError: (e: Error) => toast.error(e.message),
|
||||
})}>{t('common.save')}</Button>
|
||||
<Button size="sm" variant="outline" onClick={handleTestTg} disabled={testTg.isPending}>{t('settings.send_test')}</Button>
|
||||
</div>
|
||||
<Button size="sm" onClick={() => updateTg.mutate({ telegram_bot_token: tgToken }, {
|
||||
onSuccess: (r) => toast.success(r.message),
|
||||
onError: (e: Error) => toast.error(e.message),
|
||||
})}>{t('common.save')}</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ interface ProfileResponse {
|
||||
is_admin: boolean;
|
||||
is_active: boolean;
|
||||
ssh_public_key?: string;
|
||||
language: string;
|
||||
created_at: string;
|
||||
last_login?: string;
|
||||
}
|
||||
|
||||
@@ -681,6 +681,8 @@
|
||||
"session_timeout": "Session Timeout",
|
||||
"seconds": "seconds",
|
||||
"minutes": "minutes",
|
||||
"days": "days",
|
||||
"log_retention_days": "Log Retention",
|
||||
"security_saved": "Security settings saved",
|
||||
"version": "Version",
|
||||
"check_updates": "Check for Updates",
|
||||
|
||||
@@ -681,6 +681,8 @@
|
||||
"session_timeout": "Таймаут сессии",
|
||||
"seconds": "секунд",
|
||||
"minutes": "минут",
|
||||
"days": "дней",
|
||||
"log_retention_days": "Хранение логов",
|
||||
"security_saved": "Настройки безопасности сохранены",
|
||||
"version": "Версия",
|
||||
"check_updates": "Проверить обновления",
|
||||
|
||||
Reference in New Issue
Block a user