🐛 fix(auth): skip body parser for Stripe webhooks

The JSON body parser was consuming the request body before NestJS
could access the rawBody needed for Stripe webhook signature
verification. Now webhooks to /api/v1/webhooks/stripe skip the
body parser middleware.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2026-02-16 12:01:24 +01:00
parent bfc2737ce5
commit d86e9031bb
15 changed files with 238 additions and 191 deletions

View file

@ -65,18 +65,17 @@
background: rgba(255, 255, 255, 0.25);
}
/* Glassmorphic utilities */
/* Elevation utilities - semantic surface styles */
.glass {
@apply bg-white/80 dark:bg-white/10 backdrop-blur-xl border border-black/10 dark:border-white/20;
@apply bg-surface-elevated border border-border;
}
.glass-card {
@apply bg-white/60 dark:bg-white/5 backdrop-blur-sm border border-black/10 dark:border-white/10;
@apply bg-surface border border-border;
}
.glass-button {
@apply bg-white/90 dark:bg-white/20 backdrop-blur-sm border border-black/10 dark:border-white/20
hover:bg-white dark:hover:bg-white/30 hover:shadow-lg transition-all duration-200;
@apply bg-surface border border-border hover:bg-surface-hover hover:shadow-lg transition-all duration-200;
}
/* iOS Safe Area Insets for PWA */

View file

@ -64,10 +64,10 @@
let IconComponent = $derived(iconMap[bot.icon] || Robot);
</script>
<div class="glass-card rounded-xl overflow-hidden border border-white/10">
<div class="glass-card rounded-xl overflow-hidden border border-border">
<!-- Header (always visible) -->
<button
class="w-full p-4 text-left hover:bg-white/5 transition-colors cursor-pointer"
class="w-full p-4 text-left hover:bg-surface-hover transition-colors cursor-pointer"
onclick={() => (expanded = !expanded)}
>
<div class="flex items-start gap-3">
@ -128,7 +128,7 @@
<h4 class="text-sm font-medium text-foreground mb-2">{$t('bots.commands')}</h4>
<div class="space-y-1.5 max-h-48 overflow-y-auto">
{#each bot.commands as cmd}
<div class="text-xs bg-black/20 rounded px-2 py-1.5">
<div class="text-xs bg-muted rounded px-2 py-1.5">
<code class="text-primary font-mono">{cmd.command}</code>
{#if cmd.aliases?.length}
<span class="text-muted-foreground"> ({cmd.aliases.join(', ')})</span>

View file

@ -103,20 +103,23 @@
{#if open}
<!-- Backdrop -->
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"
onclick={handleClose}
>
<!-- Dialog -->
<div
class="w-full max-w-md rounded-xl bg-base-100 shadow-xl max-h-[90vh] overflow-y-auto"
class="w-full max-w-md rounded-xl bg-surface-elevated shadow-xl max-h-[90vh] overflow-y-auto"
onclick={(e) => e.stopPropagation()}
role="dialog"
aria-modal="true"
>
<!-- Header -->
<div class="flex items-center justify-between border-b border-base-300 px-6 py-4">
<h2 class="text-xl font-semibold">Neuer Chat</h2>
<button class="btn btn-ghost btn-sm btn-circle" onclick={handleClose}>
<div class="flex items-center justify-between border-b border-border px-6 py-4">
<h2 class="text-xl font-semibold text-foreground">Neuer Chat</h2>
<button
class="p-2 rounded-lg hover:bg-surface-hover transition-colors"
onclick={handleClose}
>
<X class="h-5 w-5" />
</button>
</div>
@ -126,18 +129,20 @@
<!-- Type Selection -->
<div class="flex gap-2">
<button
class="btn flex-1"
class:btn-primary={isDirect}
class:btn-ghost={!isDirect}
class="flex-1 flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg font-medium transition-colors
{isDirect
? 'bg-primary text-primary-foreground'
: 'bg-surface hover:bg-surface-hover text-foreground border border-border'}"
onclick={() => (isDirect = true)}
>
<ChatCircle class="h-4 w-4" />
Direktnachricht
</button>
<button
class="btn flex-1"
class:btn-primary={!isDirect}
class:btn-ghost={isDirect}
class="flex-1 flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg font-medium transition-colors
{!isDirect
? 'bg-primary text-primary-foreground'
: 'bg-surface hover:bg-surface-hover text-foreground border border-border'}"
onclick={() => (isDirect = false)}
>
<Users class="h-4 w-4" />
@ -147,60 +152,66 @@
<!-- Room Name (only for groups) -->
{#if !isDirect}
<div class="form-control">
<label class="label" for="room-name">
<span class="label-text">Raumname</span>
</label>
<div class="space-y-1.5">
<label class="text-sm font-medium text-foreground" for="room-name">Raumname</label>
<input
id="room-name"
type="text"
bind:value={name}
class="input input-bordered"
class="w-full px-4 py-2.5 rounded-lg bg-surface border border-border text-foreground
focus:outline-none focus:ring-2 focus:ring-primary placeholder:text-muted-foreground"
placeholder="z.B. Team Chat"
/>
</div>
<div class="form-control">
<label class="label" for="room-topic">
<span class="label-text">Beschreibung (optional)</span>
</label>
<div class="space-y-1.5">
<label class="text-sm font-medium text-foreground" for="room-topic"
>Beschreibung (optional)</label
>
<input
id="room-topic"
type="text"
bind:value={topic}
class="input input-bordered"
class="w-full px-4 py-2.5 rounded-lg bg-surface border border-border text-foreground
focus:outline-none focus:ring-2 focus:ring-primary placeholder:text-muted-foreground"
placeholder="Worum geht es in diesem Raum?"
/>
</div>
<!-- Privacy -->
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text flex items-center gap-2">
{#if isPrivate}
<Lock class="h-4 w-4" />
Privater Raum
{:else}
<Globe class="h-4 w-4" />
Öffentlicher Raum
{/if}
</span>
<input type="checkbox" class="toggle" bind:checked={isPrivate} />
</label>
<p class="text-xs text-base-content/60 ml-1">
{isPrivate
? 'Nur eingeladene Benutzer können beitreten'
: 'Jeder kann diesen Raum finden und beitreten'}
</p>
<div class="flex items-center justify-between p-3 rounded-lg bg-muted">
<span class="flex items-center gap-2 text-sm text-foreground">
{#if isPrivate}
<Lock class="h-4 w-4" />
Privater Raum
{:else}
<Globe class="h-4 w-4" />
Öffentlicher Raum
{/if}
</span>
<button
class="relative w-11 h-6 rounded-full transition-colors {isPrivate
? 'bg-primary'
: 'bg-muted-foreground/30'}"
onclick={() => (isPrivate = !isPrivate)}
>
<span
class="absolute top-0.5 left-0.5 w-5 h-5 rounded-full bg-white shadow transition-transform
{isPrivate ? 'translate-x-5' : 'translate-x-0'}"
></span>
</button>
</div>
<p class="text-xs text-muted-foreground">
{isPrivate
? 'Nur eingeladene Benutzer können beitreten'
: 'Jeder kann diesen Raum finden und beitreten'}
</p>
{/if}
<!-- User Search -->
<div class="form-control">
<label class="label" for="user-search">
<span class="label-text">
{isDirect ? 'Mit wem möchtest du chatten?' : 'Benutzer einladen (optional)'}
</span>
<div class="space-y-1.5">
<label class="text-sm font-medium text-foreground" for="user-search">
{isDirect ? 'Mit wem möchtest du chatten?' : 'Benutzer einladen (optional)'}
</label>
<div class="relative">
<input
@ -208,7 +219,8 @@
type="text"
bind:value={searchQuery}
oninput={handleSearchInput}
class="input input-bordered w-full"
class="w-full px-4 py-2.5 rounded-lg bg-surface border border-border text-foreground
focus:outline-none focus:ring-2 focus:ring-primary placeholder:text-muted-foreground"
placeholder="@benutzer:server.de oder Name"
/>
{#if searching}
@ -218,29 +230,34 @@
<!-- Search Results -->
{#if searchResults.length > 0}
<ul class="menu mt-2 max-h-40 overflow-y-auto rounded-lg bg-base-200 p-2">
<div
class="mt-2 rounded-lg bg-surface border border-border overflow-hidden max-h-40 overflow-y-auto"
>
{#each searchResults as user}
<li>
<button class="flex items-center gap-2" onclick={() => selectUser(user)}>
<div class="avatar placeholder">
<div class="w-8 rounded-full bg-neutral text-neutral-content">
{#if user.avatarUrl}
<img src={user.avatarUrl} alt="" />
{:else}
<span class="text-xs">{user.displayName?.[0] || user.userId[1]}</span>
{/if}
</div>
</div>
<div class="flex-1 text-left">
<p class="font-medium">{user.displayName || user.userId}</p>
{#if user.displayName}
<p class="text-xs text-base-content/60">{user.userId}</p>
{/if}
</div>
</button>
</li>
<button
class="flex items-center gap-3 w-full px-3 py-2 hover:bg-surface-hover transition-colors"
onclick={() => selectUser(user)}
>
<div
class="w-8 h-8 rounded-full bg-gradient-to-br from-violet-500 to-purple-600 flex items-center justify-center text-white text-sm"
>
{#if user.avatarUrl}
<img src={user.avatarUrl} alt="" class="w-8 h-8 rounded-full object-cover" />
{:else}
{user.displayName?.[0] || user.userId[1]}
{/if}
</div>
<div class="flex-1 text-left min-w-0">
<p class="font-medium text-foreground truncate">
{user.displayName || user.userId}
</p>
{#if user.displayName}
<p class="text-xs text-muted-foreground truncate">{user.userId}</p>
{/if}
</div>
</button>
{/each}
</ul>
</div>
{/if}
</div>
@ -248,9 +265,14 @@
{#if selectedUsers.length > 0}
<div class="flex flex-wrap gap-2">
{#each selectedUsers as user}
<span class="badge badge-lg gap-1">
<span
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-primary/10 text-primary text-sm"
>
{user.displayName || user.userId}
<button onclick={() => removeUser(user.userId)}>
<button
class="hover:bg-primary/20 rounded-full p-0.5 transition-colors"
onclick={() => removeUser(user.userId)}
>
<X class="h-3 w-3" />
</button>
</span>
@ -260,16 +282,26 @@
<!-- Error -->
{#if error}
<div class="alert alert-error">
<span>{error}</span>
<div class="px-4 py-3 rounded-lg bg-error/10 text-error text-sm">
{error}
</div>
{/if}
</div>
<!-- Footer -->
<div class="flex justify-end gap-2 border-t border-base-300 px-6 py-4">
<button class="btn btn-ghost" onclick={handleClose}>Abbrechen</button>
<button class="btn btn-primary" onclick={handleCreate} disabled={loading}>
<div class="flex justify-end gap-2 border-t border-border px-6 py-4">
<button
class="px-4 py-2.5 rounded-lg hover:bg-surface-hover text-foreground font-medium transition-colors"
onclick={handleClose}
>
Abbrechen
</button>
<button
class="px-4 py-2.5 rounded-lg bg-primary hover:bg-primary/90 text-primary-foreground font-medium transition-colors
disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
onclick={handleCreate}
disabled={loading}
>
{#if loading}
<CircleNotch class="h-4 w-4 animate-spin" />
{/if}

View file

@ -13,7 +13,7 @@
class="fixed inset-0 z-50 bg-primary/10 backdrop-blur-sm flex items-center justify-center pointer-events-none"
>
<div
class="bg-white dark:bg-zinc-800 rounded-2xl p-8 shadow-2xl border-2 border-dashed border-primary flex flex-col items-center gap-4"
class="bg-surface-elevated rounded-2xl p-8 shadow-2xl border-2 border-dashed border-primary flex flex-col items-center gap-4"
>
<div class="p-4 rounded-full bg-primary/10">
<UploadSimple class="h-12 w-12 text-primary" />

View file

@ -70,14 +70,14 @@
>
<!-- Dialog -->
<div
class="w-full max-w-md rounded-2xl bg-white dark:bg-zinc-900 shadow-2xl overflow-hidden"
class="w-full max-w-md rounded-2xl bg-surface-elevated shadow-2xl overflow-hidden"
onclick={(e) => e.stopPropagation()}
>
<!-- Header -->
<div class="flex items-center justify-between border-b border-black/10 dark:border-white/10 px-4 py-3">
<div class="flex items-center justify-between border-b border-border px-4 py-3">
<h2 class="text-lg font-semibold">Nachricht weiterleiten</h2>
<button
class="p-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
class="p-1.5 rounded-lg hover:bg-surface-hover transition-colors"
onclick={handleClose}
>
<X class="h-5 w-5" />
@ -85,20 +85,22 @@
</div>
<!-- Message Preview -->
<div class="px-4 py-3 bg-black/5 dark:bg-white/5 border-b border-black/5 dark:border-white/5">
<div class="px-4 py-3 bg-muted border-b border-border">
<p class="text-xs text-muted-foreground mb-1">Von {message.senderName}</p>
<p class="text-sm line-clamp-3">{message.body}</p>
</div>
<!-- Search -->
<div class="p-4 border-b border-black/5 dark:border-white/5">
<div class="p-4 border-b border-border">
<div class="relative">
<MagnifyingGlass class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<MagnifyingGlass
class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground"
/>
<input
type="text"
bind:value={search}
placeholder="Chat suchen..."
class="w-full pl-10 pr-4 py-2.5 rounded-xl bg-black/5 dark:bg-white/10 border border-black/10 dark:border-white/10
class="w-full pl-10 pr-4 py-2.5 rounded-xl bg-surface border border-border
text-sm focus:outline-none focus:ring-2 focus:ring-violet-500"
/>
</div>
@ -112,17 +114,29 @@
{#each filteredRooms as room (room.id)}
<button
class="flex items-center gap-3 w-full px-4 py-3 transition-colors text-left
{selectedRooms.has(room.id) ? 'bg-violet-500/10' : 'hover:bg-black/5 dark:hover:bg-white/5'}"
{selectedRooms.has(room.id) ? 'bg-violet-500/10' : 'hover:bg-surface-hover'}"
onclick={() => toggleRoom(room.id)}
>
<!-- Checkbox -->
<div
class="w-5 h-5 rounded-md border-2 flex items-center justify-center transition-colors
{selectedRooms.has(room.id) ? 'bg-violet-500 border-violet-500' : 'border-black/20 dark:border-white/20'}"
{selectedRooms.has(room.id)
? 'bg-violet-500 border-violet-500'
: 'border-black/20 dark:border-white/20'}"
>
{#if selectedRooms.has(room.id)}
<svg class="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" />
<svg
class="w-3 h-3 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="3"
d="M5 13l4 4L19 7"
/>
</svg>
{/if}
</div>
@ -133,7 +147,11 @@
bg-gradient-to-br from-violet-500 to-purple-600 text-white"
>
{#if room.avatar}
<img src={room.avatar} alt={room.name} class="w-10 h-10 rounded-full object-cover" />
<img
src={room.avatar}
alt={room.name}
class="w-10 h-10 rounded-full object-cover"
/>
{:else if room.isDirect}
<User class="w-5 h-5" />
{:else}
@ -154,7 +172,7 @@
</div>
<!-- Footer -->
<div class="flex items-center justify-between border-t border-black/10 dark:border-white/10 px-4 py-3">
<div class="flex items-center justify-between border-t border-border px-4 py-3">
<p class="text-sm text-muted-foreground">
{selectedRooms.size} ausgewählt
</p>

View file

@ -123,7 +123,7 @@
// Apply markdown formatting (bold, italic, code, strikethrough)
function applyMarkdown(text: string, isOwn: boolean): string {
const codeColor = isOwn ? 'bg-white/20 text-white' : 'bg-black/5 dark:bg-white/10';
const codeColor = isOwn ? 'bg-white/20 text-white' : 'bg-muted';
// Inline code (backticks) - process first to avoid conflicts
text = text.replace(
@ -331,7 +331,7 @@
class="relative px-4 py-3 shadow-md
{message.isOwn
? 'bg-gradient-to-br from-blue-500 to-indigo-600 text-white rounded-2xl rounded-tr-md'
: 'bg-white dark:bg-white/10 text-foreground border border-black/5 dark:border-white/10 rounded-2xl rounded-tl-md'}"
: 'bg-surface text-foreground border border-border rounded-2xl rounded-tl-md'}"
>
{#if message.redacted}
<p class="italic text-white/70">Nachricht wurde gelöscht</p>
@ -345,12 +345,12 @@
<!-- Image message -->
<div class="relative">
{#if imageLoading}
<div class="flex h-48 w-full items-center justify-center rounded-lg bg-black/10">
<div class="flex h-48 w-full items-center justify-center rounded-lg bg-muted">
<ImageIcon class="h-8 w-8 animate-pulse text-white/50" />
</div>
{/if}
{#if imageError}
<div class="flex h-32 w-full items-center justify-center rounded-lg bg-black/10">
<div class="flex h-32 w-full items-center justify-center rounded-lg bg-muted">
<p class="text-sm text-white/70">Bild konnte nicht geladen werden</p>
</div>
{:else}
@ -394,7 +394,7 @@
<div
class="flex items-center gap-3 rounded-lg {message.isOwn
? 'bg-white/20'
: 'bg-black/5 dark:bg-white/5'} p-3 min-w-[220px]"
: 'bg-muted'} p-3 min-w-[220px]"
>
<!-- Hidden audio element -->
{#if mediaUrl}
@ -432,7 +432,7 @@
<button
class="relative h-1.5 w-full rounded-full {message.isOwn
? 'bg-white/20'
: 'bg-black/10 dark:bg-white/10'} overflow-hidden cursor-pointer"
: 'bg-muted dark:bg-white/10'} overflow-hidden cursor-pointer"
onclick={seekAudio}
>
<div
@ -461,7 +461,7 @@
rel="noopener noreferrer"
class="flex items-center gap-3 rounded-lg {message.isOwn
? 'bg-white/20 hover:bg-white/30'
: 'bg-black/5 dark:bg-white/5 hover:bg-black/10 dark:hover:bg-white/10'} p-3 transition-colors"
: 'bg-muted hover:bg-muted dark:hover:bg-white/10'} p-3 transition-colors"
>
<div class="rounded-lg {message.isOwn ? 'bg-white/20' : 'bg-primary/10'} p-2">
<FileIcon class="h-5 w-5 {message.isOwn ? 'text-white' : 'text-primary'}" />
@ -504,7 +504,7 @@
rel="noopener noreferrer"
class="mt-2 flex items-center gap-2 rounded-lg {message.isOwn
? 'bg-white/10 hover:bg-white/20'
: 'bg-black/5 dark:bg-white/5 hover:bg-black/10 dark:hover:bg-white/10'} p-2 transition-colors"
: 'bg-muted hover:bg-muted dark:hover:bg-white/10'} p-2 transition-colors"
>
<img
src="https://www.google.com/s2/favicons?domain={getDomain(firstUrl() || '')}&sz=32"
@ -540,7 +540,7 @@
class="flex items-center gap-1 px-2 py-0.5 rounded-full text-xs transition-colors
{reaction.includesMe
? 'bg-primary/20 border border-primary/40 text-primary'
: 'bg-black/5 dark:bg-white/10 border border-black/10 dark:border-white/10 hover:bg-black/10 dark:hover:bg-white/20'}"
: 'bg-muted border border-border hover:bg-surface-hover'}"
title={reaction.users.join(', ')}
onclick={() => handleReaction(reaction.key)}
>
@ -581,7 +581,7 @@
<!-- Emoji reaction button -->
<div class="relative">
<button
class="p-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
class="p-1.5 rounded-lg hover:bg-surface-hover transition-colors"
title="Reaktion"
onclick={() => (showEmojiPicker = !showEmojiPicker)}
>
@ -599,7 +599,7 @@
></button>
<!-- Emoji picker dropdown -->
<div
class="absolute z-50 rounded-xl bg-white dark:bg-zinc-800 border border-black/10 dark:border-white/10 shadow-xl
class="absolute z-50 rounded-xl bg-surface-elevated border border-border shadow-xl
left-0 top-full mt-2 lg:bottom-full lg:top-auto lg:mt-0 lg:mb-2
{message.isOwn ? 'lg:right-0 lg:left-auto' : ''}
{showFullPicker ? 'w-72' : ''}"
@ -608,15 +608,13 @@
<!-- Full emoji picker with categories -->
<div class="p-2">
<!-- Category tabs -->
<div
class="flex gap-1 mb-2 border-b border-black/10 dark:border-white/10 pb-2 overflow-x-auto"
>
<div class="flex gap-1 mb-2 border-b border-border pb-2 overflow-x-auto">
{#each emojiCategories as category, i}
<button
class="px-2 py-1 text-xs rounded-md whitespace-nowrap transition-colors
{selectedCategory === i
? 'bg-violet-500 text-white'
: 'hover:bg-black/5 dark:hover:bg-white/10 text-muted-foreground'}"
: 'hover:bg-surface-hover text-muted-foreground'}"
onclick={() => (selectedCategory = i)}
>
{category.name}
@ -627,7 +625,7 @@
<div class="grid grid-cols-8 gap-1 max-h-40 overflow-y-auto">
{#each emojiCategories[selectedCategory].emojis as emoji}
<button
class="text-xl hover:scale-110 hover:bg-black/5 dark:hover:bg-white/10 rounded p-1 transition-all"
class="text-xl hover:scale-110 hover:bg-surface-hover rounded p-1 transition-all"
onclick={() => handleReaction(emoji)}
>
{emoji}
@ -648,7 +646,7 @@
{/each}
<!-- Expand button -->
<button
class="ml-1 p-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
class="ml-1 p-1.5 rounded-lg hover:bg-surface-hover transition-colors"
onclick={() => (showFullPicker = true)}
title="Mehr Emojis"
>
@ -660,14 +658,14 @@
{/if}
</div>
<button
class="p-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
class="p-1.5 rounded-lg hover:bg-surface-hover transition-colors"
title="Antworten"
onclick={() => onReply?.(message)}
>
<ArrowBendUpLeft class="h-4 w-4 text-muted-foreground" />
</button>
<button
class="p-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
class="p-1.5 rounded-lg hover:bg-surface-hover transition-colors"
title="Weiterleiten"
onclick={() => onForward?.(message)}
>
@ -675,7 +673,7 @@
</button>
{#if message.isOwn && message.type === 'm.text'}
<button
class="p-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
class="p-1.5 rounded-lg hover:bg-surface-hover transition-colors"
title="Bearbeiten"
onclick={() => onEdit?.(message)}
>

View file

@ -508,9 +508,7 @@
<div class="p-3 pb-4 lg:pb-20 safe-area-bottom">
<!-- Reply/Edit Preview -->
{#if replyTo || editMessage}
<div
class="mb-2 flex items-center gap-2 rounded-xl bg-white/60 dark:bg-white/5 border border-black/5 dark:border-white/10 px-3 py-2"
>
<div class="mb-2 flex items-center gap-2 rounded-xl bg-surface border border-border px-3 py-2">
<div class="flex-1">
{#if editMessage}
<p class="text-xs text-muted-foreground">Nachricht bearbeiten</p>
@ -523,7 +521,7 @@
{/if}
</div>
<button
class="p-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
class="p-1.5 rounded-lg hover:bg-surface-hover transition-colors"
onclick={() => {
if (editMessage) {
onCancelEdit?.();
@ -540,12 +538,10 @@
<!-- Upload Progress -->
{#if uploading}
<div
class="mb-2 flex items-center gap-3 rounded-xl bg-white/60 dark:bg-white/5 border border-black/5 dark:border-white/10 px-3 py-2"
>
<div class="mb-2 flex items-center gap-3 rounded-xl bg-surface border border-border px-3 py-2">
<CircleNotch class="h-4 w-4 animate-spin text-primary" />
<div class="flex-1">
<div class="h-1.5 overflow-hidden rounded-full bg-black/10 dark:bg-white/10">
<div class="h-1.5 overflow-hidden rounded-full bg-muted">
<div
class="h-full bg-primary transition-all duration-300"
style="width: {uploadProgress}%"
@ -578,12 +574,8 @@
<!-- @Mention Picker -->
{#if showMentionPicker && mentionResults.length > 0}
<div
class="mb-2 rounded-xl bg-white dark:bg-zinc-800 border border-black/10 dark:border-white/10 shadow-xl overflow-hidden"
>
<div
class="px-3 py-1.5 text-xs text-muted-foreground border-b border-black/5 dark:border-white/5"
>
<div class="mb-2 rounded-xl bg-surface-elevated border border-border shadow-xl overflow-hidden">
<div class="px-3 py-1.5 text-xs text-muted-foreground border-b border-border">
Erwähne jemanden
</div>
{#each mentionResults as member, i}
@ -591,7 +583,7 @@
class="flex items-center gap-3 w-full px-3 py-2 transition-colors text-left
{i === selectedMentionIndex
? 'bg-violet-500/10 dark:bg-violet-500/20'
: 'hover:bg-black/5 dark:hover:bg-white/5'}"
: 'hover:bg-surface-hover'}"
onclick={() => insertMention(member)}
>
<!-- Avatar -->
@ -623,7 +615,7 @@
<!-- Attachment button (left, outside input) -->
<div class="relative flex-shrink-0">
<button
class="p-2.5 rounded-full hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
class="p-2.5 rounded-full hover:bg-surface-hover transition-colors"
title="Datei anhängen"
disabled={uploading}
onclick={() => (showAttachMenu = !showAttachMenu)}
@ -640,14 +632,14 @@
></button>
<!-- Dropdown menu -->
<div
class="absolute bottom-full left-0 mb-2 z-50 w-44 rounded-xl bg-white dark:bg-zinc-800 border border-black/10 dark:border-white/10 p-1.5 shadow-xl"
class="absolute bottom-full left-0 mb-2 z-50 w-44 rounded-xl bg-surface-elevated border border-border p-1.5 shadow-xl"
>
<button
onclick={() => {
openFilePicker();
showAttachMenu = false;
}}
class="flex items-center gap-2 w-full px-3 py-2 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors text-sm"
class="flex items-center gap-2 w-full px-3 py-2 rounded-lg hover:bg-surface-hover transition-colors text-sm"
>
<Image class="h-4 w-4" />
Bild oder Video
@ -657,7 +649,7 @@
openFilePicker();
showAttachMenu = false;
}}
class="flex items-center gap-2 w-full px-3 py-2 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors text-sm"
class="flex items-center gap-2 w-full px-3 py-2 rounded-lg hover:bg-surface-hover transition-colors text-sm"
>
<FileIcon class="h-4 w-4" />
Datei
@ -677,7 +669,7 @@
<!-- Text input with emoji button inside -->
<div
class="relative flex-1 flex items-end rounded-full bg-white/80 dark:bg-white/10 backdrop-blur-xl border border-black/10 dark:border-white/20 px-4 py-1"
class="relative flex-1 flex items-end rounded-full bg-surface border border-border px-4 py-1"
>
<textarea
bind:this={textarea}
@ -698,7 +690,7 @@
></textarea>
<!-- Emoji button inside input -->
<button
class="flex-shrink-0 p-1.5 rounded-full hover:bg-black/5 dark:hover:bg-white/10 transition-colors mb-1"
class="flex-shrink-0 p-1.5 rounded-full hover:bg-surface-hover transition-colors mb-1"
title="Emoji"
onclick={handleEmojiClick}
>
@ -715,7 +707,7 @@
></button>
<!-- Picker -->
<div
class="absolute bottom-full right-0 mb-2 z-50 w-72 max-h-80 overflow-y-auto rounded-xl bg-white dark:bg-zinc-800 border border-black/10 dark:border-white/10 p-2 shadow-xl"
class="absolute bottom-full right-0 mb-2 z-50 w-72 max-h-80 overflow-y-auto rounded-xl bg-surface-elevated border border-border p-2 shadow-xl"
>
<!-- Recent/Frequently used emojis -->
{#if recentEmojis.length > 0}
@ -726,7 +718,7 @@
<div class="grid grid-cols-8 gap-1">
{#each recentEmojis as emoji}
<button
class="p-1.5 text-xl hover:bg-black/5 dark:hover:bg-white/10 rounded-lg transition-colors"
class="p-1.5 text-xl hover:bg-surface-hover rounded-lg transition-colors"
onclick={() => insertEmoji(emoji)}
>
{emoji}
@ -734,13 +726,13 @@
{/each}
</div>
</div>
<div class="border-t border-black/5 dark:border-white/10 my-2"></div>
<div class="border-t border-border my-2"></div>
{/if}
<!-- All emojis -->
<div class="grid grid-cols-8 gap-1">
{#each commonEmojis as emoji}
<button
class="p-1.5 text-xl hover:bg-black/5 dark:hover:bg-white/10 rounded-lg transition-colors"
class="p-1.5 text-xl hover:bg-surface-hover rounded-lg transition-colors"
onclick={() => insertEmoji(emoji)}
>
{emoji}
@ -772,7 +764,7 @@
</button>
{:else}
<button
class="flex-shrink-0 p-2.5 rounded-full hover:bg-black/5 dark:hover:bg-white/10 text-muted-foreground hover:text-primary transition-colors
class="flex-shrink-0 p-2.5 rounded-full hover:bg-surface-hover text-muted-foreground hover:text-primary transition-colors
disabled:opacity-50 disabled:cursor-not-allowed"
onclick={startRecording}
disabled={uploading}

View file

@ -76,13 +76,11 @@
</script>
{#if room}
<header
class="flex items-center gap-3 border-b border-black/10 dark:border-white/10 bg-white/50 dark:bg-white/5 backdrop-blur-sm px-4 py-3"
>
<header class="flex items-center gap-3 border-b border-border bg-surface-elevated px-4 py-3">
<!-- Mobile back button or menu button -->
{#if showBackButton}
<button
class="p-2 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
class="p-2 rounded-lg hover:bg-surface-hover transition-colors"
onclick={onBackClick}
aria-label="Zurück"
>
@ -90,7 +88,7 @@
</button>
{:else}
<button
class="p-2 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors lg:hidden"
class="p-2 rounded-lg hover:bg-surface-hover transition-colors lg:hidden"
onclick={onMenuClick}
>
<List class="h-5 w-5" />
@ -112,8 +110,8 @@
<!-- Online indicator for DMs -->
{#if room.isDirect}
<div
class="absolute -bottom-0.5 -right-0.5 w-3.5 h-3.5 rounded-full border-2 border-white dark:border-zinc-900
{isOnline ? 'bg-green-500' : 'bg-zinc-400 dark:bg-zinc-600'}"
class="absolute -bottom-0.5 -right-0.5 w-3.5 h-3.5 rounded-full border-2 border-background
{isOnline ? 'bg-green-500' : 'bg-muted-foreground'}"
title={presenceText()}
></div>
{/if}
@ -151,7 +149,7 @@
<span class="w-2 h-2 rounded-full bg-green-500"></span>
<span class="text-green-600 dark:text-green-400">Online</span>
{:else}
<span class="w-2 h-2 rounded-full bg-zinc-400"></span>
<span class="w-2 h-2 rounded-full bg-muted-foreground"></span>
<span>{presenceText() || 'Offline'}</span>
{/if}
</span>

View file

@ -48,8 +48,8 @@
<button
class="flex w-full items-center gap-3 px-3 py-2.5 mb-1 rounded-xl transition-all duration-200
{selected
? 'bg-white dark:bg-white/15 shadow-md border border-black/5 dark:border-white/10'
: 'hover:bg-white/60 dark:hover:bg-white/5 hover:-translate-y-0.5'}"
? 'bg-surface-elevated shadow-md border border-border'
: 'hover:bg-surface-hover hover:-translate-y-0.5'}"
{onclick}
>
<!-- Avatar with online indicator -->
@ -69,8 +69,8 @@
<!-- Online indicator dot -->
{#if room.isDirect}
<div
class="absolute -bottom-0.5 -right-0.5 w-3.5 h-3.5 rounded-full border-2 border-white dark:border-zinc-900
{isOnline ? 'bg-green-500' : 'bg-zinc-400 dark:bg-zinc-600'}"
class="absolute -bottom-0.5 -right-0.5 w-3.5 h-3.5 rounded-full border-2 border-background
{isOnline ? 'bg-green-500' : 'bg-muted-foreground'}"
title={lastActiveText()}
></div>
{/if}

View file

@ -63,8 +63,7 @@
type="text"
bind:value={search}
placeholder="Chats durchsuchen..."
class="w-full rounded-xl bg-white/70 dark:bg-white/10 backdrop-blur-xl
border border-black/10 dark:border-white/20 px-4 py-2.5 pl-10
class="w-full rounded-xl bg-surface border border-border px-4 py-2.5 pl-10
text-sm font-medium text-foreground focus:ring-2 focus:ring-primary focus:outline-none
placeholder:text-muted-foreground shadow-sm"
/>
@ -143,7 +142,7 @@
>
<ChatCircle class="h-3.5 w-3.5" />
Direktnachrichten
<span class="px-1.5 py-0.5 rounded-full bg-black/10 dark:bg-white/10 text-[10px]">
<span class="px-1.5 py-0.5 rounded-full bg-muted text-[10px]">
{matrixStore.directRooms.length}
</span>
</div>
@ -165,7 +164,7 @@
>
<Users class="h-3.5 w-3.5" />
Räume
<span class="px-1.5 py-0.5 rounded-full bg-black/10 dark:bg-white/10 text-[10px]">
<span class="px-1.5 py-0.5 rounded-full bg-muted text-[10px]">
{matrixStore.groupRooms.length}
</span>
</div>
@ -189,7 +188,7 @@
</div>
<!-- New Room Button -->
<div class="border-t border-black/10 dark:border-white/10 p-3 pb-4 lg:pb-20">
<div class="border-t border-border p-3 pb-4 lg:pb-20">
<button
class="w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl
bg-gradient-to-r from-violet-500 to-purple-600 text-white font-medium

View file

@ -79,7 +79,10 @@
function highlightMatch(text: string, searchTerm: string): string {
if (!searchTerm.trim()) return text;
const regex = new RegExp(`(${searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
return text.replace(regex, '<mark class="bg-yellow-300/50 dark:bg-yellow-500/30 rounded px-0.5">$1</mark>');
return text.replace(
regex,
'<mark class="bg-yellow-300/50 dark:bg-yellow-500/30 rounded px-0.5">$1</mark>'
);
}
</script>
@ -95,12 +98,12 @@
>
<!-- Dialog -->
<div
class="w-full max-w-2xl rounded-2xl bg-white dark:bg-zinc-900 shadow-2xl overflow-hidden"
class="w-full max-w-2xl rounded-2xl bg-surface-elevated shadow-2xl overflow-hidden"
onclick={(e) => e.stopPropagation()}
role="document"
>
<!-- Search Header -->
<div class="flex items-center gap-3 p-4 border-b border-black/10 dark:border-white/10">
<div class="flex items-center gap-3 p-4 border-b border-border">
<MagnifyingGlass class="h-5 w-5 text-muted-foreground flex-shrink-0" />
<input
bind:this={inputRef}
@ -113,26 +116,23 @@
{#if searching}
<CircleNotch class="h-5 w-5 animate-spin text-muted-foreground" />
{/if}
<button
class="p-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
onclick={onClose}
>
<button class="p-1.5 rounded-lg hover:bg-surface-hover transition-colors" onclick={onClose}>
<X class="h-5 w-5" />
</button>
</div>
<!-- Scope Toggle -->
<div class="flex gap-2 px-4 py-2 border-b border-black/5 dark:border-white/5 bg-muted/30">
<div class="flex gap-2 px-4 py-2 border-b border-border bg-muted/30">
<button
class="px-3 py-1.5 rounded-lg text-sm font-medium transition-colors
{searchScope === 'room' ? 'bg-primary text-primary-foreground' : 'hover:bg-black/5 dark:hover:bg-white/10'}"
{searchScope === 'room' ? 'bg-primary text-primary-foreground' : 'hover:bg-surface-hover'}"
onclick={() => (searchScope = 'room')}
>
Aktueller Raum
</button>
<button
class="px-3 py-1.5 rounded-lg text-sm font-medium transition-colors
{searchScope === 'all' ? 'bg-primary text-primary-foreground' : 'hover:bg-black/5 dark:hover:bg-white/10'}"
{searchScope === 'all' ? 'bg-primary text-primary-foreground' : 'hover:bg-surface-hover'}"
onclick={() => (searchScope = 'all')}
>
Alle Räume
@ -147,10 +147,10 @@
<span>Suche läuft...</span>
</div>
{:else if searchResults.length > 0}
<div class="divide-y divide-black/5 dark:divide-white/5">
<div class="divide-y divide-border">
{#each searchResults as result}
<button
class="w-full text-left px-4 py-3 hover:bg-black/5 dark:hover:bg-white/5 transition-colors"
class="w-full text-left px-4 py-3 hover:bg-surface-hover transition-colors"
onclick={() => handleSelectResult(result)}
>
<div class="flex items-center gap-2 mb-1">
@ -158,7 +158,9 @@
{#if searchScope === 'all'}
<span class="text-xs text-muted-foreground">in {result.roomName}</span>
{/if}
<span class="text-xs text-muted-foreground ml-auto">{formatTime(result.timestamp)}</span>
<span class="text-xs text-muted-foreground ml-auto"
>{formatTime(result.timestamp)}</span
>
</div>
<p class="text-sm text-muted-foreground line-clamp-2">
{@html highlightMatch(result.body, query)}

View file

@ -37,12 +37,12 @@
<img
src={user.avatarUrl}
alt={user.name}
class="w-6 h-6 rounded-full border-2 border-white dark:border-zinc-900 object-cover"
class="w-6 h-6 rounded-full border-2 border-background object-cover"
style="z-index: {3 - i}"
/>
{:else}
<div
class="w-6 h-6 rounded-full border-2 border-white dark:border-zinc-900 bg-gradient-to-br from-violet-500 to-purple-600 flex items-center justify-center"
class="w-6 h-6 rounded-full border-2 border-background bg-gradient-to-br from-violet-500 to-purple-600 flex items-center justify-center"
style="z-index: {3 - i}"
>
<User class="w-3 h-3 text-white" />
@ -52,10 +52,16 @@
</div>
<!-- Animated dots -->
<div class="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-black/5 dark:bg-white/10">
<span class="h-1.5 w-1.5 animate-bounce rounded-full bg-muted-foreground [animation-delay:0ms]"></span>
<span class="h-1.5 w-1.5 animate-bounce rounded-full bg-muted-foreground [animation-delay:150ms]"></span>
<span class="h-1.5 w-1.5 animate-bounce rounded-full bg-muted-foreground [animation-delay:300ms]"></span>
<div class="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-muted">
<span
class="h-1.5 w-1.5 animate-bounce rounded-full bg-muted-foreground [animation-delay:0ms]"
></span>
<span
class="h-1.5 w-1.5 animate-bounce rounded-full bg-muted-foreground [animation-delay:150ms]"
></span>
<span
class="h-1.5 w-1.5 animate-bounce rounded-full bg-muted-foreground [animation-delay:300ms]"
></span>
</div>
<!-- Text -->

View file

@ -91,7 +91,7 @@
type="text"
bind:value={search}
placeholder={$t('bots.search')}
class="w-full pl-10 pr-4 py-2.5 rounded-xl bg-white/5 border border-white/10 text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary/50 transition-all"
class="w-full pl-10 pr-4 py-2.5 rounded-xl bg-surface border border-border text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary/50 transition-all"
/>
</div>
</div>
@ -104,7 +104,7 @@
class="px-4 py-2 rounded-full text-sm font-medium transition-all whitespace-nowrap cursor-pointer
{selectedCategory === category.id
? 'bg-gradient-to-r from-violet-500 to-purple-600 text-white shadow-lg'
: 'bg-white/5 text-muted-foreground hover:bg-white/10 hover:text-foreground border border-white/10'}"
: 'bg-surface text-muted-foreground hover:bg-surface-hover hover:text-foreground border border-border'}"
onclick={() => (selectedCategory = category.id as BotCategory)}
>
{category.label}

View file

@ -183,9 +183,7 @@
<!-- Mobile: Full-screen room list -->
<div class="flex flex-col h-full bg-background safe-area-bottom">
<!-- User Info / Status Bar -->
<div
class="border-b border-black/10 dark:border-white/10 px-4 py-3 bg-white/95 dark:bg-zinc-900/95 backdrop-blur-xl safe-area-top"
>
<div class="border-b border-border px-4 py-3 bg-surface-elevated safe-area-top">
<div class="flex items-center justify-between">
<div>
<h1 class="text-xl font-bold text-foreground">Manalink</h1>
@ -204,13 +202,13 @@
<div class="flex items-center gap-1">
<a
href="/settings"
class="p-2 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
class="p-2 rounded-lg hover:bg-surface-hover transition-colors"
title="Einstellungen"
>
<Gear class="h-5 w-5" />
</a>
<button
class="p-2 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
class="p-2 rounded-lg hover:bg-surface-hover transition-colors"
title="Neuer Chat"
onclick={() => (showCreateRoom = true)}
>
@ -229,23 +227,21 @@
<!-- Desktop: Side-by-side layout -->
<div class="chat-layout flex h-full min-h-0 overflow-hidden bg-background">
<!-- Sidebar -->
<aside
class="flex flex-col border-r border-black/10 dark:border-white/10 bg-white/95 dark:bg-zinc-900/95 backdrop-blur-xl w-80"
>
<aside class="flex flex-col border-r border-border bg-surface-elevated w-80">
<!-- User Info / Status Bar -->
<div class="border-b border-black/10 dark:border-white/10 px-4 py-3">
<div class="border-b border-border px-4 py-3">
<div class="flex items-center justify-between">
<p class="truncate text-sm font-medium">{matrixStore.userId}</p>
<div class="flex items-center gap-1">
<a
href="/settings"
class="p-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
class="p-1.5 rounded-lg hover:bg-surface-hover transition-colors"
title="Einstellungen"
>
<Gear class="h-4 w-4" />
</a>
<button
class="p-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
class="p-1.5 rounded-lg hover:bg-surface-hover transition-colors"
title="Neuer Chat"
onclick={() => (showCreateRoom = true)}
>

View file

@ -77,7 +77,14 @@ async function bootstrap() {
app.use(cookieParser());
// Explicit body parsers for form-urlencoded (needed for OAuth2 token endpoint)
app.use(bodyParser.json());
// IMPORTANT: Skip JSON body parsing for Stripe webhooks to preserve rawBody for signature verification
app.use((req: Request, res: Response, next: NextFunction) => {
if (req.path === '/api/v1/webhooks/stripe') {
// Skip body parsing for Stripe webhooks - NestJS rawBody will be used instead
return next();
}
bodyParser.json()(req, res, next);
});
app.use(bodyParser.urlencoded({ extended: true }));
// CORS configuration