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:
@@ -11,9 +11,14 @@
|
||||
import ConfirmModal from '$lib/components/ConfirmModal.svelte';
|
||||
import IconButton from '$lib/components/IconButton.svelte';
|
||||
import { snackSuccess, snackError, snackInfo } from '$lib/stores/snackbar.svelte';
|
||||
import type { TelegramChat } from '$lib/types';
|
||||
import type { TelegramBot, TelegramChat } from '$lib/types';
|
||||
|
||||
let { settings, onreload }: { settings: any; onreload: () => Promise<void> } = $props();
|
||||
interface CommandTrackerSummary { id: number; name: string; icon?: string; enabled: boolean }
|
||||
interface ListenerEntry { listener_type: string; listener_id: number }
|
||||
interface WebhookStatusInfo { url?: string; pending_update_count?: number; last_error_message?: string }
|
||||
interface ApiResult { success: boolean; error?: string; verified?: boolean }
|
||||
|
||||
let { settings, onreload }: { settings: Record<string, string>; onreload: () => Promise<void> } = $props();
|
||||
|
||||
let bots = $derived(telegramBotsCache.items);
|
||||
let showForm = $state(false);
|
||||
@@ -21,7 +26,7 @@
|
||||
let form = $state({ name: '', icon: '', token: '' });
|
||||
let error = $state('');
|
||||
let submitting = $state(false);
|
||||
let confirmDelete = $state<any>(null);
|
||||
let confirmDelete = $state<{ id: number; onconfirm: () => Promise<void> } | null>(null);
|
||||
|
||||
// Per-bot expandable sections
|
||||
let chats = $state<Record<number, TelegramChat[]>>({});
|
||||
@@ -29,17 +34,17 @@
|
||||
let expandedSection = $state<Record<number, string>>({});
|
||||
|
||||
// Webhook status per bot
|
||||
let webhookStatus = $state<Record<number, any>>({});
|
||||
let webhookStatus = $state<Record<number, WebhookStatusInfo>>({});
|
||||
|
||||
let chatTesting = $state<Record<string, boolean>>({});
|
||||
let modeChanging = $state<Record<number, boolean>>({});
|
||||
|
||||
// Listener status: command trackers using this bot
|
||||
let botListenerStatus = $state<Record<number, any[]>>({});
|
||||
let botListenerStatus = $state<Record<number, CommandTrackerSummary[]>>({});
|
||||
let botListenerLoading = $state<Record<number, boolean>>({});
|
||||
|
||||
function openNew() { form = { name: '', icon: '', token: '' }; editing = null; showForm = true; }
|
||||
function editBot(bot: any) { form = { name: bot.name, icon: bot.icon || '', token: '' }; editing = bot.id; showForm = true; }
|
||||
function editBot(bot: TelegramBot) { form = { name: bot.name, icon: bot.icon || '', token: '' }; editing = bot.id; showForm = true; }
|
||||
|
||||
async function saveBot(e: SubmitEvent) {
|
||||
e.preventDefault(); error = ''; submitting = true;
|
||||
@@ -78,14 +83,14 @@
|
||||
|
||||
async function loadChats(botId: number) {
|
||||
chatsLoading = { ...chatsLoading, [botId]: true };
|
||||
try { chats = { ...chats, [botId]: await api(`/telegram-bots/${botId}/chats`) }; } catch { chats = { ...chats, [botId]: [] }; }
|
||||
try { chats = { ...chats, [botId]: await api<TelegramChat[]>(`/telegram-bots/${botId}/chats`) }; } catch (e) { console.warn('Failed to load chats:', e); chats = { ...chats, [botId]: [] }; }
|
||||
chatsLoading = { ...chatsLoading, [botId]: false };
|
||||
}
|
||||
|
||||
async function discoverChats(botId: number) {
|
||||
chatsLoading = { ...chatsLoading, [botId]: true };
|
||||
try {
|
||||
chats = { ...chats, [botId]: await api(`/telegram-bots/${botId}/chats/discover`, { method: 'POST' }) };
|
||||
chats = { ...chats, [botId]: await api<TelegramChat[]>(`/telegram-bots/${botId}/chats/discover`, { method: 'POST' }) };
|
||||
snackSuccess(t('telegramBot.chatsDiscovered'));
|
||||
} catch (err: any) { snackError(err.message); }
|
||||
chatsLoading = { ...chatsLoading, [botId]: false };
|
||||
@@ -94,7 +99,7 @@
|
||||
async function deleteChat(botId: number, chatDbId: number) {
|
||||
try {
|
||||
await api(`/telegram-bots/${botId}/chats/${chatDbId}`, { method: 'DELETE' });
|
||||
chats[botId] = (chats[botId] || []).filter((c: any) => c.id !== chatDbId);
|
||||
chats[botId] = (chats[botId] || []).filter((c) => c.id !== chatDbId);
|
||||
snackSuccess(t('telegramBot.chatDeleted'));
|
||||
} catch (err: any) { snackError(err.message); }
|
||||
}
|
||||
@@ -102,24 +107,24 @@
|
||||
async function loadListenerStatus(botId: number) {
|
||||
botListenerLoading = { ...botListenerLoading, [botId]: true };
|
||||
try {
|
||||
const trackers = await api('/command-trackers');
|
||||
const matched: any[] = [];
|
||||
const trackers = await api<CommandTrackerSummary[]>('/command-trackers');
|
||||
const matched: CommandTrackerSummary[] = [];
|
||||
for (const trk of trackers) {
|
||||
try {
|
||||
const listeners = await api(`/command-trackers/${trk.id}/listeners`);
|
||||
const hasBot = listeners.some((l: any) => l.listener_type === 'telegram_bot' && l.listener_id === botId);
|
||||
const listeners = await api<ListenerEntry[]>(`/command-trackers/${trk.id}/listeners`);
|
||||
const hasBot = listeners.some((l) => l.listener_type === 'telegram_bot' && l.listener_id === botId);
|
||||
if (hasBot) matched.push(trk);
|
||||
} catch { /* ignore */ }
|
||||
} catch (e) { console.warn('Failed to load listeners for tracker:', e); }
|
||||
}
|
||||
botListenerStatus = { ...botListenerStatus, [botId]: matched };
|
||||
} catch { botListenerStatus = { ...botListenerStatus, [botId]: [] }; }
|
||||
} catch (e) { console.warn('Failed to load listener status:', e); botListenerStatus = { ...botListenerStatus, [botId]: [] }; }
|
||||
botListenerLoading = { ...botListenerLoading, [botId]: false };
|
||||
}
|
||||
|
||||
async function syncCommands(botId: number) {
|
||||
modeChanging = { ...modeChanging, [botId]: true };
|
||||
try {
|
||||
const res = await api(`/telegram-bots/${botId}/sync-commands`, { method: 'POST' });
|
||||
const res = await api<ApiResult>(`/telegram-bots/${botId}/sync-commands`, { method: 'POST' });
|
||||
if (res.success) snackSuccess(t('telegramBot.commandsSynced'));
|
||||
else snackError(res.error || 'Failed');
|
||||
} catch (err: any) { snackError(err.message); }
|
||||
@@ -141,14 +146,14 @@
|
||||
|
||||
async function loadWebhookStatus(botId: number) {
|
||||
try {
|
||||
webhookStatus = { ...webhookStatus, [botId]: await api(`/telegram-bots/${botId}/webhook/status`) };
|
||||
} catch { webhookStatus = { ...webhookStatus, [botId]: null }; }
|
||||
webhookStatus = { ...webhookStatus, [botId]: await api<WebhookStatusInfo>(`/telegram-bots/${botId}/webhook/status`) };
|
||||
} catch (e) { console.warn('Failed to load webhook status:', e); webhookStatus = { ...webhookStatus, [botId]: {} }; }
|
||||
}
|
||||
|
||||
async function registerWebhook(botId: number) {
|
||||
modeChanging = { ...modeChanging, [botId]: true };
|
||||
try {
|
||||
const res = await api(`/telegram-bots/${botId}/webhook/register`, { method: 'POST' });
|
||||
const res = await api<ApiResult>(`/telegram-bots/${botId}/webhook/register`, { method: 'POST' });
|
||||
if (res.success) {
|
||||
snackSuccess(res.verified ? t('telegramBot.webhookVerified') : t('telegramBot.webhookRegistered'));
|
||||
await loadWebhookStatus(botId);
|
||||
@@ -162,7 +167,7 @@
|
||||
async function unregisterWebhook(botId: number) {
|
||||
modeChanging = { ...modeChanging, [botId]: true };
|
||||
try {
|
||||
const res = await api(`/telegram-bots/${botId}/webhook/unregister`, { method: 'POST' });
|
||||
const res = await api<ApiResult>(`/telegram-bots/${botId}/webhook/unregister`, { method: 'POST' });
|
||||
if (res.success) { snackSuccess(t('telegramBot.webhookUnregistered')); await loadWebhookStatus(botId); }
|
||||
else snackError(res.error || 'Failed');
|
||||
} catch (err: any) { snackError(err.message); }
|
||||
@@ -193,7 +198,7 @@
|
||||
if (chatTesting[key]) return;
|
||||
chatTesting = { ...chatTesting, [key]: true };
|
||||
try {
|
||||
const res = await api(`/telegram-bots/${botId}/chats/${chatId}/test?locale=${getLocale()}`, { method: 'POST' });
|
||||
const res = await api<ApiResult>(`/telegram-bots/${botId}/chats/${chatId}/test?locale=${getLocale()}`, { method: 'POST' });
|
||||
if (res.success) snackSuccess(t('snack.targetTestSent'));
|
||||
else snackError(res.error || 'Failed');
|
||||
} catch (err: any) { snackError(err.message); }
|
||||
@@ -398,7 +403,7 @@
|
||||
{@const ws = webhookStatus[bot.id]}
|
||||
<span class="text-xs font-mono {ws.url ? 'text-blue-500' : 'text-[var(--color-muted-foreground)]'}">
|
||||
{ws.url ? t('telegramBot.webhookActive') : t('telegramBot.webhookNotSet')}
|
||||
{#if ws.pending_update_count > 0}
|
||||
{#if (ws.pending_update_count ?? 0) > 0}
|
||||
({ws.pending_update_count} {t('telegramBot.pendingUpdates')})
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user