fix(settings): align SMTP/security settings fields with API
Rename SMTP request/response fields (host/port/username/password/from_email/tls) and point security settings hooks at /admin/api/security/settings with a typed SecuritySettings shape. Add minutes / security_saved i18n keys. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -406,12 +406,12 @@ async def update_security_settings(
|
||||
# ==================== Notification Channel Settings API ====================
|
||||
|
||||
class SMTPSettingsRequest(BaseModel):
|
||||
smtp_host: Optional[str] = Field(None, max_length=255)
|
||||
smtp_port: Optional[int] = Field(None, ge=1, le=65535)
|
||||
smtp_user: Optional[str] = Field(None, max_length=255)
|
||||
smtp_password: Optional[str] = Field(None, max_length=255)
|
||||
smtp_from: Optional[str] = Field(None, max_length=255)
|
||||
smtp_tls: Optional[bool] = None
|
||||
host: Optional[str] = Field(None, max_length=255)
|
||||
port: Optional[int] = Field(None, ge=1, le=65535)
|
||||
username: Optional[str] = Field(None, max_length=255)
|
||||
password: Optional[str] = Field(None, max_length=255)
|
||||
from_email: Optional[str] = Field(None, max_length=255)
|
||||
tls: Optional[bool] = None
|
||||
|
||||
|
||||
class TelegramSettingsRequest(BaseModel):
|
||||
@@ -435,9 +435,9 @@ async def get_notification_channel_settings(
|
||||
"smtp": {
|
||||
"host": get_setting(db, "smtp_host", ""),
|
||||
"port": int(get_setting(db, "smtp_port", "587") or "587"),
|
||||
"user": get_setting(db, "smtp_user", ""),
|
||||
"username": get_setting(db, "smtp_user", ""),
|
||||
"password": "***" if get_setting(db, "smtp_password") else "",
|
||||
"from": get_setting(db, "smtp_from", ""),
|
||||
"from_email": get_setting(db, "smtp_from", ""),
|
||||
"tls": get_setting(db, "smtp_tls", "true").lower() == "true"
|
||||
},
|
||||
"telegram": {
|
||||
@@ -462,29 +462,29 @@ async def update_smtp_settings(
|
||||
)
|
||||
|
||||
updated_fields = []
|
||||
|
||||
if data.smtp_host is not None:
|
||||
set_setting(db, "smtp_host", data.smtp_host, "SMTP сервер")
|
||||
|
||||
if data.host is not None:
|
||||
set_setting(db, "smtp_host", data.host, "SMTP сервер")
|
||||
updated_fields.append("smtp_host")
|
||||
|
||||
if data.smtp_port is not None:
|
||||
set_setting(db, "smtp_port", str(data.smtp_port), "SMTP порт")
|
||||
|
||||
if data.port is not None:
|
||||
set_setting(db, "smtp_port", str(data.port), "SMTP порт")
|
||||
updated_fields.append("smtp_port")
|
||||
|
||||
if data.smtp_user is not None:
|
||||
set_setting(db, "smtp_user", data.smtp_user, "SMTP пользователь")
|
||||
|
||||
if data.username is not None:
|
||||
set_setting(db, "smtp_user", data.username, "SMTP пользователь")
|
||||
updated_fields.append("smtp_user")
|
||||
|
||||
if data.smtp_password is not None:
|
||||
set_setting(db, "smtp_password", data.smtp_password, "SMTP пароль")
|
||||
|
||||
if data.password is not None:
|
||||
set_setting(db, "smtp_password", data.password, "SMTP пароль")
|
||||
updated_fields.append("smtp_password")
|
||||
|
||||
if data.smtp_from is not None:
|
||||
set_setting(db, "smtp_from", data.smtp_from, "Email отправителя")
|
||||
|
||||
if data.from_email is not None:
|
||||
set_setting(db, "smtp_from", data.from_email, "Email отправителя")
|
||||
updated_fields.append("smtp_from")
|
||||
|
||||
if data.smtp_tls is not None:
|
||||
set_setting(db, "smtp_tls", str(data.smtp_tls).lower(), "Использовать TLS")
|
||||
|
||||
if data.tls is not None:
|
||||
set_setting(db, "smtp_tls", str(data.tls).lower(), "Использовать TLS")
|
||||
updated_fields.append("smtp_tls")
|
||||
|
||||
if updated_fields:
|
||||
|
||||
@@ -80,7 +80,10 @@ 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 })}>{t('common.save')}</Button>
|
||||
<Button size="sm" onClick={() => updateProfile.mutate({ full_name: displayName, email }, {
|
||||
onSuccess: (r) => toast.success(r.message),
|
||||
onError: (e: Error) => toast.error(e.message),
|
||||
})}>{t('common.save')}</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
@@ -91,8 +94,10 @@ function ProfileTab() {
|
||||
<div><Label>{t('settings.confirm_password')}</Label><Input type="password" value={confirmPwd} onChange={e => setConfirmPwd(e.target.value)} className="mt-1" /></div>
|
||||
<Button size="sm" onClick={() => {
|
||||
if (newPwd !== confirmPwd) { toast.error(t('settings.password_mismatch')); return; }
|
||||
changePassword.mutate({ current_password: currentPwd, new_password: newPwd, confirm_password: confirmPwd });
|
||||
setCurrentPwd(''); setNewPwd(''); setConfirmPwd('');
|
||||
changePassword.mutate({ current_password: currentPwd, new_password: newPwd, confirm_password: confirmPwd }, {
|
||||
onSuccess: (r) => { toast.success(r.message); setCurrentPwd(''); setNewPwd(''); setConfirmPwd(''); },
|
||||
onError: (e: Error) => toast.error(e.message),
|
||||
});
|
||||
}}>{t('settings.change_password')}</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -143,8 +148,8 @@ function SecurityTab() {
|
||||
useEffect(() => {
|
||||
if (security) {
|
||||
setMaxAttempts(String(security.max_login_attempts ?? 5));
|
||||
setLockoutDuration(String(security.lockout_duration ?? 300));
|
||||
setSessionTimeout(String(security.session_timeout ?? 3600));
|
||||
setLockoutDuration(String(security.lockout_duration_minutes ?? 30));
|
||||
setSessionTimeout(String(security.session_timeout_minutes ?? 60));
|
||||
}
|
||||
}, [security]);
|
||||
|
||||
@@ -153,12 +158,15 @@ function SecurityTab() {
|
||||
<CardHeader><CardTitle className="text-sm">{t('settings.security_settings')}</CardTitle></CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div><Label>{t('settings.max_attempts')}</Label><Input type="number" value={maxAttempts} onChange={e => setMaxAttempts(e.target.value)} className="mt-1" /></div>
|
||||
<div><Label>{t('settings.lockout_duration')}</Label><Input type="number" value={lockoutDuration} onChange={e => setLockoutDuration(e.target.value)} className="mt-1" /><p className="text-xs text-muted-foreground mt-1">{t('settings.seconds')}</p></div>
|
||||
<div><Label>{t('settings.session_timeout')}</Label><Input type="number" value={sessionTimeout} onChange={e => setSessionTimeout(e.target.value)} className="mt-1" /><p className="text-xs text-muted-foreground mt-1">{t('settings.seconds')}</p></div>
|
||||
<div><Label>{t('settings.lockout_duration')}</Label><Input type="number" value={lockoutDuration} onChange={e => setLockoutDuration(e.target.value)} className="mt-1" /><p className="text-xs text-muted-foreground mt-1">{t('settings.minutes')}</p></div>
|
||||
<div><Label>{t('settings.session_timeout')}</Label><Input type="number" value={sessionTimeout} onChange={e => setSessionTimeout(e.target.value)} className="mt-1" /><p className="text-xs text-muted-foreground mt-1">{t('settings.minutes')}</p></div>
|
||||
<Button size="sm" onClick={() => updateSecurity.mutate({
|
||||
max_login_attempts: Number(maxAttempts),
|
||||
lockout_duration: Number(lockoutDuration),
|
||||
session_timeout: Number(sessionTimeout),
|
||||
lockout_duration_minutes: Number(lockoutDuration),
|
||||
session_timeout_minutes: Number(sessionTimeout),
|
||||
}, {
|
||||
onSuccess: () => toast.success(t('settings.security_saved')),
|
||||
onError: (e: Error) => toast.error(e.message),
|
||||
})}>{t('common.save')}</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -228,7 +236,10 @@ function NotificationsTab() {
|
||||
<div><Label>Password</Label><Input type="password" value={smtpPass} onChange={e => setSmtpPass(e.target.value)} className="mt-1" /></div>
|
||||
<div><Label>From Email</Label><Input value={smtpFrom} onChange={e => setSmtpFrom(e.target.value)} className="mt-1" /></div>
|
||||
</div>
|
||||
<Button size="sm" onClick={() => updateSmtp.mutate({ host: smtpHost, port: Number(smtpPort), username: smtpUser, password: smtpPass || undefined, from_email: smtpFrom })}>{t('common.save')}</Button>
|
||||
<Button size="sm" onClick={() => updateSmtp.mutate({ host: smtpHost, port: Number(smtpPort), username: smtpUser, password: smtpPass || undefined, from_email: smtpFrom }, {
|
||||
onSuccess: (r) => toast.success(r.message),
|
||||
onError: (e: Error) => toast.error(e.message),
|
||||
})}>{t('common.save')}</Button>
|
||||
<div className="flex items-center gap-2 border-t pt-3">
|
||||
<Input value={testEmail} onChange={e => setTestEmail(e.target.value)} className="max-w-xs" placeholder={t('settings.test_email_placeholder')} />
|
||||
<Button size="sm" variant="outline" onClick={handleTestSmtp} disabled={testSmtp.isPending}>{t('settings.send_test')}</Button>
|
||||
@@ -246,7 +257,10 @@ function NotificationsTab() {
|
||||
<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 })}>{t('common.save')}</Button>
|
||||
<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>
|
||||
</CardContent>
|
||||
|
||||
@@ -90,18 +90,31 @@ export function useUpdatePanelSettings() {
|
||||
});
|
||||
}
|
||||
|
||||
export interface SecuritySettings {
|
||||
max_login_attempts: number;
|
||||
lockout_duration_minutes: number;
|
||||
session_timeout_minutes: number;
|
||||
ip_block_threshold: number;
|
||||
ip_block_duration_minutes: number;
|
||||
password_min_length: number;
|
||||
password_require_uppercase: boolean;
|
||||
password_require_lowercase: boolean;
|
||||
password_require_numbers: boolean;
|
||||
password_require_special: boolean;
|
||||
}
|
||||
|
||||
export function useSecuritySettings() {
|
||||
return useQuery({
|
||||
queryKey: settingsKeys.security,
|
||||
queryFn: () => apiClient.get<{ single_session_enabled: boolean; max_login_attempts?: number; lockout_duration?: number; session_timeout?: number }>('/settings/api/security-settings'),
|
||||
queryFn: () => apiClient.get<SecuritySettings>('/admin/api/security/settings'),
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateSecuritySettings() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: { single_session_enabled?: boolean; max_login_attempts?: number; lockout_duration?: number; session_timeout?: number }) =>
|
||||
apiClient.put<{ message: string }>('/settings/api/security-settings', data),
|
||||
mutationFn: (data: Partial<SecuritySettings>) =>
|
||||
apiClient.put<{ message: string }>('/admin/api/security/settings', data),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: settingsKeys.security }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -680,6 +680,8 @@
|
||||
"lockout_duration": "Lockout Duration",
|
||||
"session_timeout": "Session Timeout",
|
||||
"seconds": "seconds",
|
||||
"minutes": "minutes",
|
||||
"security_saved": "Security settings saved",
|
||||
"version": "Version",
|
||||
"check_updates": "Check for Updates",
|
||||
"update_available": "Update available",
|
||||
|
||||
@@ -680,6 +680,8 @@
|
||||
"lockout_duration": "Время блокировки",
|
||||
"session_timeout": "Таймаут сессии",
|
||||
"seconds": "секунд",
|
||||
"minutes": "минут",
|
||||
"security_saved": "Настройки безопасности сохранены",
|
||||
"version": "Версия",
|
||||
"check_updates": "Проверить обновления",
|
||||
"update_available": "Доступно обновление",
|
||||
|
||||
Reference in New Issue
Block a user