fix(contacts): fix CSP blocking auth, broken contact detail modal, and missing click handler

- Add fallback URLs for connect-src in CSP (localhost:3001, :3015, :3050)
- Switch ContactDetailModal from HTTP API to IndexedDB (local-first)
- Add onclick handler to alphabet view contact cards
- Add stopPropagation on phone/email action links
- Accept non-UUID contact IDs in route regex for seed data

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-31 16:29:18 +02:00
parent 750a0c77ff
commit cea0c97889
4 changed files with 40 additions and 38 deletions

View file

@ -35,9 +35,10 @@ window.__PUBLIC_GLITCHTIP_DSN__ = ${JSON.stringify(PUBLIC_GLITCHTIP_DSN)};
setSecurityHeaders(response, {
connectSrc: [
PUBLIC_MANA_CORE_AUTH_URL_CLIENT,
PUBLIC_BACKEND_URL_CLIENT,
PUBLIC_MANA_CORE_AUTH_URL_CLIENT || 'http://localhost:3001',
PUBLIC_BACKEND_URL_CLIENT || 'http://localhost:3015',
PUBLIC_TODO_BACKEND_URL,
'http://localhost:3050', // mana-sync server
],
});

View file

@ -1,7 +1,9 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
import { contactsApi, photoApi, type Contact } from '$lib/api/contacts';
import { type Contact } from '$lib/api/contacts';
import { contactCollection } from '$lib/data/local-store';
import { toContact } from '$lib/data/queries';
import { getDisplayName } from '$lib/utils/contact-display';
import ContactNotes from './ContactNotes.svelte';
import ContactTasks from './ContactTasks.svelte';
@ -134,7 +136,12 @@
loading = true;
error = null;
try {
contact = await contactsApi.get(contactId);
const local = await contactCollection.get(contactId);
if (!local) {
error = 'Kontakt nicht gefunden';
return;
}
contact = toContact(local);
populateForm();
} catch (e) {
error = getErrorMessage(e, 'Fehler beim Laden des Kontakts');
@ -147,37 +154,19 @@
saving = true;
error = null;
try {
contact = await contactsApi.update(contactId, {
firstName: firstName || null,
lastName: lastName || null,
email: email || null,
phone: phone || null,
mobile: mobile || null,
company: company || null,
jobTitle: jobTitle || null,
street: street || null,
city: city || null,
postalCode: postalCode || null,
country: country || null,
notes: notes || null,
// Dates
birthday: birthday || null,
customDates: customDates.filter((d) => d.label && d.date),
// Social Media
linkedin: linkedin || null,
twitter: twitter || null,
facebook: facebook || null,
instagram: instagram || null,
xing: xing || null,
github: github || null,
youtube: youtube || null,
tiktok: tiktok || null,
telegram: telegram || null,
whatsapp: whatsapp || null,
signal: signal || null,
discord: discord || null,
bluesky: bluesky || null,
await contactCollection.update(contactId, {
firstName: firstName || undefined,
lastName: lastName || undefined,
email: email || undefined,
phone: phone || undefined,
company: company || undefined,
jobTitle: jobTitle || undefined,
notes: notes || undefined,
birthday: birthday || undefined,
});
// Reload from local store
const local = await contactCollection.get(contactId);
if (local) contact = toContact(local);
editing = false;
} catch (e) {
error = getErrorMessage(e, 'Fehler beim Speichern');
@ -190,7 +179,7 @@
if (!confirm('Kontakt wirklich löschen?')) return;
deleting = true;
try {
await contactsApi.delete(contactId);
await contactCollection.delete(contactId);
onClose();
} catch (e) {
error = getErrorMessage(e, 'Fehler beim Löschen');
@ -202,7 +191,9 @@
if (!contact) return;
try {
contact = await contactsApi.toggleFavorite(contactId);
await contactCollection.update(contactId, { isFavorite: !contact.isFavorite });
const local = await contactCollection.get(contactId);
if (local) contact = toContact(local);
} catch (e) {
error = getErrorMessage(e, 'Fehler');
}

View file

@ -187,10 +187,13 @@
<!-- Contacts in this section -->
<div class="section-contacts">
{#each groupedContacts[letter] as contact (contact.id)}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="alphabet-contact-card {selectionMode && selectedIds.has(contact.id)
? 'selected'
: ''}"
onclick={() => onContactClick(contact.id)}
>
<!-- Selection Checkbox -->
{#if selectionMode}
@ -271,12 +274,18 @@
href="tel:{contact.mobile || contact.phone}"
class="action-chip"
title={contact.mobile || contact.phone}
onclick={(e) => e.stopPropagation()}
>
<Phone size={16} class="action-icon" />
</a>
{/if}
{#if contact.email}
<a href="mailto:{contact.email}" class="action-chip" title={contact.email}>
<a
href="mailto:{contact.email}"
class="action-chip"
title={contact.email}
onclick={(e) => e.stopPropagation()}
>
<Envelope size={16} class="action-icon" />
</a>
{/if}

View file

@ -8,6 +8,7 @@
QuickInputBar,
ImmersiveModeToggle,
TagStrip,
SyncIndicator,
} from '@manacore/shared-ui';
import {
SplitPaneContainer,
@ -77,7 +78,7 @@
setContext('contacts', allContacts);
// Check if we're on a contact detail route
const contactDetailMatch = $derived($page.url.pathname.match(/^\/contacts\/([0-9a-f-]{36})$/i));
const contactDetailMatch = $derived($page.url.pathname.match(/^\/contacts\/([a-z0-9_-]+)$/i));
const showContactModal = $derived(!!contactDetailMatch);
const modalContactId = $derived(contactDetailMatch?.[1] || null);