feat: security hardening — SSRF guard, template sandbox timeout, webhook log prune, auth & backup polish
- Add outbound URL validation (SSRF) for webhook/Discord/Slack/ntfy/Matrix dispatch - Template renderer: input/output caps and thread-based render timeout - Webhook log filter: strip Authorization/signature/token-like headers; atomic prune - Auth/JWT/backup/config tightening; misc frontend UX fixes
This commit is contained in:
@@ -21,7 +21,7 @@
|
||||
let editingEmail = $state<number | null>(null);
|
||||
let emailSubmitting = $state(false);
|
||||
let emailTesting = $state<Record<number, boolean>>({});
|
||||
let confirmDeleteEmail = $state<EmailBot | null>(null);
|
||||
let confirmDeleteEmail = $state<{ id: number; onconfirm: () => Promise<void> } | null>(null);
|
||||
let error = $state('');
|
||||
|
||||
const defaultEmailForm = () => ({
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
let editingMatrix = $state<number | null>(null);
|
||||
let matrixSubmitting = $state(false);
|
||||
let matrixTesting = $state<Record<number, boolean>>({});
|
||||
let confirmDeleteMatrix = $state<MatrixBot | null>(null);
|
||||
let confirmDeleteMatrix = $state<{ id: number; onconfirm: () => Promise<void> } | null>(null);
|
||||
let error = $state('');
|
||||
|
||||
const defaultMatrixForm = () => ({
|
||||
|
||||
@@ -375,12 +375,14 @@
|
||||
<div style={gridStyle}
|
||||
class="text-sm px-2 py-1.5 rounded hover:bg-[var(--color-muted)] cursor-pointer"
|
||||
onclick={(e: MouseEvent) => copyChatId(e, chat.chat_id)}
|
||||
onkeydown={(e: KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); copyChatId(e as unknown as MouseEvent, chat.chat_id); } }}
|
||||
title={t('telegramBot.clickToCopy')}
|
||||
aria-label={t('telegramBot.clickToCopy')}
|
||||
role="button" tabindex="0">
|
||||
<span class="font-medium truncate">{chat.title || chat.username || t('common.unknown')}</span>
|
||||
<span style="text-align:center" class="text-xs px-1.5 py-0.5 rounded bg-[var(--color-muted)] text-[var(--color-muted-foreground)]">{chatTypeLabel(chat.type)}</span>
|
||||
<span style="text-align:center" class="text-xs text-[var(--color-muted-foreground)]">{(chat.language_code || '—').toUpperCase()}</span>
|
||||
<div style="justify-self:center" onclick={(e: MouseEvent) => e.stopPropagation()}>
|
||||
<div style="justify-self:center" role="presentation" onclick={(e: MouseEvent) => e.stopPropagation()} onkeydown={(e: KeyboardEvent) => e.stopPropagation()}>
|
||||
<EntitySelect
|
||||
items={LANG_ITEMS}
|
||||
value={chat.language_override || ''}
|
||||
@@ -388,7 +390,7 @@
|
||||
onselect={(val) => updateChatLanguage(bot.id, chat, String(val ?? ''))}
|
||||
/>
|
||||
</div>
|
||||
<div style="justify-self:center" onclick={(e: MouseEvent) => e.stopPropagation()}>
|
||||
<div style="justify-self:center" role="presentation" onclick={(e: MouseEvent) => e.stopPropagation()} onkeydown={(e: KeyboardEvent) => e.stopPropagation()}>
|
||||
<button
|
||||
style="width:28px; height:16px; border-radius:8px; position:relative; transition:background-color 0.2s; background-color:{chat.commands_enabled ? 'var(--color-primary)' : 'var(--color-border)'};"
|
||||
title={t('telegramBot.commandsToggle')}
|
||||
|
||||
@@ -57,15 +57,6 @@
|
||||
favorites: 'mdiStar', people: 'mdiAccountGroup',
|
||||
};
|
||||
|
||||
let allCapabilities = $derived(capabilitiesCache.items);
|
||||
let providerCommands = $derived<{key: string, icon: string}[]>(
|
||||
(allCapabilities[form.provider_type]?.commands || []).map((c: { name: string }) => ({
|
||||
key: c.name,
|
||||
icon: commandIcons[c.name] || 'mdiConsole',
|
||||
}))
|
||||
);
|
||||
let hasCommands = $derived(providerCommands.length > 0);
|
||||
|
||||
const defaultForm = () => ({
|
||||
name: '',
|
||||
icon: '',
|
||||
@@ -78,6 +69,15 @@
|
||||
});
|
||||
let form = $state(defaultForm());
|
||||
|
||||
let allCapabilities = $derived(capabilitiesCache.items);
|
||||
let providerCommands = $derived<{key: string, icon: string}[]>(
|
||||
(allCapabilities[form.provider_type]?.commands || []).map((c: { name: string }) => ({
|
||||
key: c.name,
|
||||
icon: commandIcons[c.name] || 'mdiConsole',
|
||||
}))
|
||||
);
|
||||
let hasCommands = $derived(providerCommands.length > 0);
|
||||
|
||||
onMount(load);
|
||||
async function load() {
|
||||
try {
|
||||
|
||||
@@ -90,6 +90,15 @@
|
||||
return 'empty';
|
||||
}
|
||||
|
||||
const defaultForm = () => ({
|
||||
provider_type: '',
|
||||
name: '',
|
||||
description: '',
|
||||
icon: '',
|
||||
slots: {} as Record<string, Record<string, string>>,
|
||||
});
|
||||
let form = $state(defaultForm());
|
||||
|
||||
// Provider capabilities
|
||||
let allCapabilities = $state<Record<string, any>>({});
|
||||
let providerTypes = $derived(Object.keys(allCapabilities));
|
||||
@@ -102,15 +111,6 @@
|
||||
: commandSlots
|
||||
);
|
||||
|
||||
const defaultForm = () => ({
|
||||
provider_type: '',
|
||||
name: '',
|
||||
description: '',
|
||||
icon: '',
|
||||
slots: {} as Record<string, Record<string, string>>,
|
||||
});
|
||||
let form = $state(defaultForm());
|
||||
|
||||
/** Get slot template for current locale, with fallback. */
|
||||
function getSlotValue(slotName: string): string {
|
||||
return form.slots[slotName]?.[activeLocale] || '';
|
||||
|
||||
@@ -35,9 +35,6 @@
|
||||
const providerItems = $derived(providers
|
||||
.filter(p => !globalProviderFilter.providerType || p.type === globalProviderFilter.providerType)
|
||||
.map(p => ({ value: p.id, label: p.name, icon: providerDefaultIcon(p), desc: p.type })));
|
||||
const configItems = $derived(filteredConfigs()
|
||||
.filter((c: any) => !globalProviderFilter.providerType || c.provider_type === globalProviderFilter.providerType)
|
||||
.map((c: any) => ({ value: c.id, label: c.name, icon: c.icon || 'mdiCog', desc: c.provider_type })));
|
||||
const botItems = $derived(telegramBots.map(b => ({ value: b.id, label: b.name, icon: b.icon || 'mdiRobot', desc: b.bot_username ? `@${b.bot_username}` : '' })));
|
||||
let loaded = $state(false);
|
||||
let showForm = $state(false);
|
||||
@@ -64,12 +61,15 @@
|
||||
let form = $state(defaultForm());
|
||||
|
||||
// Filter command configs by selected provider's type
|
||||
let filteredConfigs = $derived(() => {
|
||||
let filteredConfigs = $derived.by(() => {
|
||||
if (!form.provider_id) return commandConfigs;
|
||||
const provider = providers.find(p => p.id === form.provider_id);
|
||||
if (!provider) return commandConfigs;
|
||||
return commandConfigs.filter(c => c.provider_type === provider.type);
|
||||
});
|
||||
const configItems = $derived(filteredConfigs
|
||||
.filter((c: any) => !globalProviderFilter.providerType || c.provider_type === globalProviderFilter.providerType)
|
||||
.map((c: any) => ({ value: c.id, label: c.name, icon: c.icon || 'mdiCog', desc: c.provider_type })));
|
||||
|
||||
onMount(load);
|
||||
async function load() {
|
||||
|
||||
@@ -352,7 +352,7 @@
|
||||
<div class="mb-4 p-3 rounded-md text-xs border" style="border-color: var(--color-border);">
|
||||
<div class="flex items-center gap-2 mb-2 font-medium">
|
||||
{#if validationResult.valid}
|
||||
<MdiIcon name="mdiCheckCircle" size={14} class="text-green-600" />
|
||||
<span style="color: var(--color-success-fg, green);"><MdiIcon name="mdiCheckCircle" size={14} /></span>
|
||||
<span style="color: var(--color-success-fg, green);">{t('backup.validationPassed')}</span>
|
||||
{:else}
|
||||
<MdiIcon name="mdiCloseCircle" size={14} />
|
||||
|
||||
Reference in New Issue
Block a user