feat: comprehensive code review fixes — security, performance, quality

Backend security:
- Reject Gitea webhooks when webhook_secret is empty (was silently skipping)
- Add slowapi rate limiting on login (5/min) and setup (3/min) endpoints
- Add CORS middleware with configurable origins
- Mask telegram_webhook_secret in settings API response
- Protect system-owned command template configs from regular user modification
- Increase minimum password length to 8 characters

Backend performance:
- Batch queries in _resolve_command_context (3 queries instead of 3N)
- Concurrent album fetching with asyncio.gather in immich commands
- Singleton Jinja2 SandboxedEnvironment (reuse instead of per-render creation)
- TTLCache for rate limits (bounded memory, auto-eviction)
- Optional aiohttp session reuse in send_reply/send_media_group

Backend code quality:
- Extract dispatch_helpers.py (shared link_data loading + event filtering)
- Extract database/seeds.py from main.py (490 lines → dedicated module)
- Split immich_handler.py (415 lines) into commands/immich/ subpackage
- Replace bare except blocks with logged warnings
- Add per-provider config validation (Pydantic models)
- Truncate command input to 512 chars
- Expose usage_* and desc_* slots in capabilities and variables API

Frontend security:
- CSS.escape() for user-controlled querySelector in highlight.ts
- Client-side password min 8 chars validation on setup and password change

Frontend code quality:
- Replace any types with proper interfaces across top files
- Decompose targets/+page.svelte into TargetForm + ReceiverSection
- Fix $derived.by usage, $state mutation patterns
- Add console.warn to empty catch blocks

Frontend UX:
- Auth redirect via goto() with "Redirecting..." state
- Platform-aware Ctrl/Cmd K keyboard hint
- Remove stat-card hover transform

Frontend accessibility:
- Modal: role=dialog, aria-modal, focus trap, restore focus
- EntitySelect/IconGridSelect: listbox/option roles, aria-selected/disabled
This commit is contained in:
2026-03-23 01:59:51 +03:00
parent 31584c5d31
commit e0bae394ee
78 changed files with 2855 additions and 1658 deletions
+3 -2
View File
@@ -47,7 +47,7 @@
"createAccount": "Create account",
"creatingAccount": "Creating account...",
"passwordMismatch": "Passwords do not match",
"passwordTooShort": "Password must be at least 6 characters",
"passwordTooShort": "Password must be at least 8 characters",
"or": "or"
},
"dashboard": {
@@ -753,7 +753,8 @@
"filterByName": "Filter by name...",
"allTypes": "All types",
"allProviders": "All providers",
"noFilterResults": "No items match the current filter."
"noFilterResults": "No items match the current filter.",
"redirecting": "Redirecting..."
},
"gridDesc": {
"sortNone": "No sorting applied",
+63
View File
@@ -0,0 +1,63 @@
/**
* Reactive i18n module using Svelte 5 $state rune.
* Locale changes automatically propagate to all components using t().
*/
import en from './en.json';
import ru from './ru.json';
export type Locale = 'en' | 'ru';
const translations: Record<Locale, Record<string, any>> = { en, ru };
function detectLocale(): Locale {
if (typeof localStorage !== 'undefined') {
const saved = localStorage.getItem('locale') as Locale | null;
if (saved && saved in translations) return saved;
}
if (typeof navigator !== 'undefined') {
const lang = navigator.language.slice(0, 2);
if (lang in translations) return lang as Locale;
}
return 'en';
}
let currentLocale = $state<Locale>(detectLocale());
export function getLocale(): Locale {
return currentLocale;
}
export function setLocale(locale: Locale) {
currentLocale = locale;
if (typeof localStorage !== 'undefined') {
localStorage.setItem('locale', locale);
}
}
export function initLocale() {
// No-op: locale is auto-detected at module load via $state.
// Kept for backward compatibility with existing onMount calls.
}
/**
* Get a translated string by dot-separated key.
* Falls back to English if key not found in current locale.
* Reactive: re-evaluates when currentLocale changes.
*/
export function t(key: string, fallback?: string): string {
return resolve(translations[currentLocale], key)
?? resolve(translations.en, key)
?? fallback
?? key;
}
function resolve(obj: any, path: string): string | undefined {
const parts = path.split('.');
let current = obj;
for (const part of parts) {
if (current == null || typeof current !== 'object') return undefined;
current = current[part];
}
return typeof current === 'string' ? current : undefined;
}
+2
View File
@@ -0,0 +1,2 @@
// Re-export from the .svelte.ts module which supports $state runes
export { t, getLocale, setLocale, initLocale, type Locale } from './index.svelte';
+3 -2
View File
@@ -47,7 +47,7 @@
"createAccount": "Создать аккаунт",
"creatingAccount": "Создание...",
"passwordMismatch": "Пароли не совпадают",
"passwordTooShort": "Пароль должен быть не менее 6 символов",
"passwordTooShort": "Пароль должен быть не менее 8 символов",
"or": "или"
},
"dashboard": {
@@ -753,7 +753,8 @@
"filterByName": "Фильтр по имени...",
"allTypes": "Все типы",
"allProviders": "Все провайдеры",
"noFilterResults": "Нет элементов, соответствующих фильтру."
"noFilterResults": "Нет элементов, соответствующих фильтру.",
"redirecting": "Перенаправление..."
},
"gridDesc": {
"sortNone": "Без сортировки",