feat: comprehensive code review fixes + receivers-only architecture
Security:
- Refuse startup with default secret_key in production (was just logging)
- Settings endpoint now requires admin role
- Password validation on initial setup
- DOM-based HTML sanitizer replaces regex in template previews
- Add *.log to .gitignore
Performance & reliability:
- Token refresh deduplication prevents race condition on concurrent 401s
- Theme media query listener registered once (no leak)
- IconPicker uses $derived instead of function call per render
- Snackbar uses single-batch state update instead of while loop
- Replace 11 inline hover handlers with CSS :hover in layout
Architecture - receivers-only:
- Delivery endpoints (chat_id, email, url, room_id, topic) now stored
exclusively in TargetReceiver rows, never in target.config
- Migration extracts existing delivery fields to receiver rows
- Notifier and dispatcher remove all config fallbacks
- Frontend targets page shows receivers list per target with
add/remove/toggle/test per receiver
- Single-receiver test endpoint: POST /targets/{id}/receivers/{id}/test
Code quality:
- Extract AuthLayout.svelte from login/setup (150 lines CSS dedup)
- Split telegram-bots page (754→51 lines + 3 tab components)
- Split notification-trackers page (547→432 lines + 4 components)
- Deduplicate _send_reply into shared handler.send_reply()
- Add locale column to template models, replace name-based detection
- Fix delete_notification_tracker dead protection check
- Fix check_telegram_bot query (filter by type, remove bogus OR)
- Add graceful scheduler shutdown in lifespan
- Consistent /bots?tab=telegram URLs across all nav links
i18n:
- Error page, chat actions, target types, provider types internationalized
- All new receiver UI strings in EN + RU
This commit is contained in:
@@ -190,18 +190,41 @@
|
||||
}
|
||||
|
||||
function sanitizePreview(html: string): string {
|
||||
// Allow only Telegram-safe HTML tags, escape everything else
|
||||
return html
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
// Restore allowed tags — only http(s) URLs for <a> to prevent javascript: XSS
|
||||
.replace(/<a href="(https?:\/\/[^"]*)">/g, '<a href="$1" target="_blank" rel="noopener noreferrer">')
|
||||
.replace(/<\/a>/g, '</a>')
|
||||
.replace(/<b>/g, '<b>').replace(/<\/b>/g, '</b>')
|
||||
.replace(/<i>/g, '<i>').replace(/<\/i>/g, '</i>')
|
||||
.replace(/<code>/g, '<code>').replace(/<\/code>/g, '</code>')
|
||||
.replace(/<pre>/g, '<pre>').replace(/<\/pre>/g, '</pre>');
|
||||
// DOM-based sanitizer: parse HTML, walk tree, keep only safe elements
|
||||
const ALLOWED_TAGS = new Set(['B', 'I', 'CODE', 'PRE', 'A', 'BR']);
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
function walkNodes(parent: Node, target: Node) {
|
||||
for (const node of Array.from(parent.childNodes)) {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
target.appendChild(document.createTextNode(node.textContent || ''));
|
||||
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const el = node as Element;
|
||||
if (ALLOWED_TAGS.has(el.tagName)) {
|
||||
const safe = document.createElement(el.tagName);
|
||||
if (el.tagName === 'A') {
|
||||
const href = el.getAttribute('href') || '';
|
||||
if (/^https?:\/\//i.test(href)) {
|
||||
safe.setAttribute('href', href);
|
||||
safe.setAttribute('target', '_blank');
|
||||
safe.setAttribute('rel', 'noopener noreferrer');
|
||||
}
|
||||
}
|
||||
walkNodes(el, safe);
|
||||
target.appendChild(safe);
|
||||
} else {
|
||||
// Unwrap: keep text content of disallowed tags
|
||||
walkNodes(el, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walkNodes(doc.body, fragment);
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.appendChild(fragment);
|
||||
return wrapper.innerHTML;
|
||||
}
|
||||
|
||||
function remove(id: number) {
|
||||
|
||||
Reference in New Issue
Block a user