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:
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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": "Без сортировки",
|
||||
|
||||
Reference in New Issue
Block a user