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
@@ -6,6 +6,8 @@
label: string;
icon?: string;
desc?: string;
disabled?: boolean;
disabledHint?: string;
}
let {
@@ -62,6 +64,7 @@
}
function selectItem(item: EntityItem) {
if (item.disabled) return;
value = item.value || null;
onselect?.(value);
closePalette();
@@ -103,6 +106,8 @@
<!-- Trigger button -->
<button type="button" class="es-trigger" class:es-sm={size === 'sm'} onclick={openPalette}
aria-expanded={open}
aria-haspopup="listbox"
style="opacity: {disabled ? 0.5 : 1}; cursor: {disabled ? 'default' : 'pointer'};">
{#if selected}
{#if selected.icon}
@@ -135,15 +140,19 @@
<kbd class="ep-kbd">ESC</kbd>
</div>
<div class="ep-list" bind:this={listEl}>
<div class="ep-list" bind:this={listEl} role="listbox">
{#if filtered.length === 0}
<div class="ep-empty">No matches</div>
{:else}
{#each filtered as item, i}
<button
class="ep-item"
class:ep-highlight={i === highlightIdx}
class:ep-highlight={i === highlightIdx && !item.disabled}
class:ep-current={String(item.value) === String(value)}
class:ep-disabled={item.disabled}
role="option"
aria-selected={String(item.value) === String(value)}
aria-disabled={item.disabled || undefined}
onclick={() => selectItem(item)}
onmouseenter={() => highlightIdx = i}
type="button"
@@ -152,7 +161,9 @@
<span class="ep-item-icon"><MdiIcon name={item.icon} size={18} /></span>
{/if}
<span class="ep-item-label">{item.label}</span>
{#if item.desc}
{#if item.disabled && item.disabledHint}
<span class="ep-item-hint">{item.disabledHint}</span>
{:else if item.desc}
<span class="ep-item-desc">{item.desc}</span>
{/if}
</button>
@@ -292,6 +303,13 @@
.ep-item:hover, .ep-item.ep-highlight {
background: var(--color-muted);
}
.ep-item.ep-disabled {
opacity: 0.4;
cursor: default;
}
.ep-item.ep-disabled:hover {
background: transparent;
}
.ep-item.ep-current {
border-left-color: var(--color-primary);
}
@@ -319,4 +337,11 @@
text-overflow: ellipsis;
max-width: 40%;
}
.ep-item-hint {
font-size: 0.7rem;
font-style: italic;
color: var(--color-muted-foreground);
white-space: nowrap;
margin-left: auto;
}
</style>