From 9b119302d315b00cf1b6b7831fbf5b07660ec908 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 26 Jan 2026 17:26:17 +0100 Subject: [PATCH] Fix admin settings 502 and improve notifications UX - Use supabaseAdmin for admin settings operations (bypasses RLS completely) - Add proper error handling to updateSettings action - Update notifications to be expandable with full message display - Clicking notifications in dropdown now goes to /notifications?id=X - Auto-scroll and expand notification when opening from dropdown link Co-Authored-By: Claude Opus 4.5 --- .../layout/NotificationCenter.svelte | 10 +- .../(app)/admin/settings/+page.server.ts | 185 ++++++++++-------- .../(app)/notifications/+page.server.ts | 10 +- src/routes/(app)/notifications/+page.svelte | 133 +++++++++---- 4 files changed, 210 insertions(+), 128 deletions(-) diff --git a/src/lib/components/layout/NotificationCenter.svelte b/src/lib/components/layout/NotificationCenter.svelte index 466c4fc..35f8410 100644 --- a/src/lib/components/layout/NotificationCenter.svelte +++ b/src/lib/components/layout/NotificationCenter.svelte @@ -90,14 +90,8 @@ markAsRead(notification.id); } closeDropdown(); - if (notification.link) { - // Use goto for internal links, window.location for external - if (notification.link.startsWith('/')) { - await goto(notification.link); - } else if (notification.link.startsWith('http')) { - window.location.href = notification.link; - } - } + // Always go to notifications page with the notification ID to expand it + await goto(`/notifications?id=${notification.id}`); } function getTypeIcon(type: Notification['type']) { diff --git a/src/routes/(app)/admin/settings/+page.server.ts b/src/routes/(app)/admin/settings/+page.server.ts index 47105ab..e49b5d0 100644 --- a/src/routes/(app)/admin/settings/+page.server.ts +++ b/src/routes/(app)/admin/settings/+page.server.ts @@ -2,6 +2,7 @@ import { fail } from '@sveltejs/kit'; import type { Actions, PageServerLoad } from './$types'; import { testSmtpConnection, sendTemplatedEmail } from '$lib/server/email'; import { testS3Connection, clearS3ClientCache } from '$lib/server/storage'; +import { supabaseAdmin } from '$lib/server/supabase'; import * as poste from '$lib/server/poste'; export const load: PageServerLoad = async ({ locals }) => { @@ -205,94 +206,122 @@ export const actions: Actions = { // Update app settings updateSettings: async ({ request, locals }) => { const { member } = await locals.safeGetSession(); + + if (!member || member.role !== 'admin') { + return fail(403, { error: 'Unauthorized' }); + } + const formData = await request.formData(); const category = formData.get('category') as string; - // Get existing boolean settings for this category to handle unchecked checkboxes - const { data: existingSettings } = await locals.supabase - .from('app_settings') - .select('setting_key, setting_type') - .eq('category', category); - - const existingBooleanKeys = new Set( - (existingSettings || []) - .filter(s => s.setting_type === 'boolean') - .map(s => s.setting_key) - ); - - // Get all settings from form data - const settingsToUpdate: Array<{ key: string; value: any; type: string }> = []; - const processedKeys = new Set(); - - for (const [key, value] of formData.entries()) { - if (key !== 'category' && key.startsWith('setting_')) { - const settingKey = key.replace('setting_', ''); - processedKeys.add(settingKey); - // Handle checkbox values - they come as 'on' when checked - const isCheckbox = value === 'on' || existingBooleanKeys.has(settingKey); - settingsToUpdate.push({ - key: settingKey, - value: isCheckbox ? (value === 'on' || value === 'true') : value, - type: isCheckbox ? 'boolean' : 'text' - }); - } + if (!category) { + return fail(400, { error: 'Category is required' }); } - // Handle unchecked checkboxes - they don't send any value - // For any existing boolean setting NOT in the form data, set to false - for (const booleanKey of existingBooleanKeys) { - if (!processedKeys.has(booleanKey)) { - settingsToUpdate.push({ - key: booleanKey, - value: false, - type: 'boolean' - }); - } - } - - // Update or insert each setting - for (const setting of settingsToUpdate) { - // Try to update first - const { data: existing } = await locals.supabase + try { + // Get existing boolean settings for this category to handle unchecked checkboxes + // Use supabaseAdmin to bypass RLS + const { data: existingSettings, error: fetchError } = await supabaseAdmin .from('app_settings') - .select('id') - .eq('category', category) - .eq('setting_key', setting.key) - .single(); + .select('setting_key, setting_type') + .eq('category', category); - if (existing) { - // Update existing - await locals.supabase - .from('app_settings') - .update({ - setting_value: setting.type === 'boolean' ? setting.value : JSON.stringify(setting.value), - updated_at: new Date().toISOString(), - updated_by: member?.id - }) - .eq('category', category) - .eq('setting_key', setting.key); - } else { - // Insert new setting - await locals.supabase - .from('app_settings') - .insert({ - category, - setting_key: setting.key, - setting_value: setting.type === 'boolean' ? setting.value : JSON.stringify(setting.value), - setting_type: setting.type, - display_name: setting.key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()), - updated_at: new Date().toISOString(), - updated_by: member?.id - }); + if (fetchError) { + console.error('Error fetching existing settings:', fetchError); + return fail(500, { error: 'Failed to load existing settings' }); } - } - // Clear caches if storage settings were updated - if (category === 'storage') { - clearS3ClientCache(); - } + const existingBooleanKeys = new Set( + (existingSettings || []) + .filter(s => s.setting_type === 'boolean') + .map(s => s.setting_key) + ); - return { success: 'Settings updated successfully!' }; + // Get all settings from form data + const settingsToUpdate: Array<{ key: string; value: any; type: string }> = []; + const processedKeys = new Set(); + + for (const [key, value] of formData.entries()) { + if (key !== 'category' && key.startsWith('setting_')) { + const settingKey = key.replace('setting_', ''); + processedKeys.add(settingKey); + // Handle checkbox values - they come as 'on' when checked + const isCheckbox = value === 'on' || existingBooleanKeys.has(settingKey); + settingsToUpdate.push({ + key: settingKey, + value: isCheckbox ? (value === 'on' || value === 'true') : value, + type: isCheckbox ? 'boolean' : 'text' + }); + } + } + + // Handle unchecked checkboxes - they don't send any value + // For any existing boolean setting NOT in the form data, set to false + for (const booleanKey of existingBooleanKeys) { + if (!processedKeys.has(booleanKey)) { + settingsToUpdate.push({ + key: booleanKey, + value: false, + type: 'boolean' + }); + } + } + + // Update or insert each setting using supabaseAdmin (bypasses RLS) + for (const setting of settingsToUpdate) { + // Try to update first + const { data: existing } = await supabaseAdmin + .from('app_settings') + .select('id') + .eq('category', category) + .eq('setting_key', setting.key) + .single(); + + if (existing) { + // Update existing + const { error: updateError } = await supabaseAdmin + .from('app_settings') + .update({ + setting_value: setting.type === 'boolean' ? setting.value : JSON.stringify(setting.value), + updated_at: new Date().toISOString(), + updated_by: member.id + }) + .eq('category', category) + .eq('setting_key', setting.key); + + if (updateError) { + console.error('Error updating setting:', setting.key, updateError); + } + } else { + // Insert new setting + const { error: insertError } = await supabaseAdmin + .from('app_settings') + .insert({ + category, + setting_key: setting.key, + setting_value: setting.type === 'boolean' ? setting.value : JSON.stringify(setting.value), + setting_type: setting.type, + display_name: setting.key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()), + updated_at: new Date().toISOString(), + updated_by: member.id + }); + + if (insertError) { + console.error('Error inserting setting:', setting.key, insertError); + } + } + } + + // Clear caches if storage settings were updated + if (category === 'storage') { + clearS3ClientCache(); + } + + return { success: 'Settings updated successfully!' }; + } catch (err) { + console.error('Unexpected error updating settings:', err); + return fail(500, { error: 'An unexpected error occurred while saving settings' }); + } }, // Test SMTP connection diff --git a/src/routes/(app)/notifications/+page.server.ts b/src/routes/(app)/notifications/+page.server.ts index 111cc44..880d165 100644 --- a/src/routes/(app)/notifications/+page.server.ts +++ b/src/routes/(app)/notifications/+page.server.ts @@ -1,10 +1,10 @@ import type { PageServerLoad } from './$types'; -export const load: PageServerLoad = async ({ locals }) => { +export const load: PageServerLoad = async ({ locals, url }) => { const { member } = await locals.safeGetSession(); if (!member) { - return { notifications: [] }; + return { notifications: [], expandedId: null }; } const { data: notifications } = await locals.supabase @@ -13,7 +13,11 @@ export const load: PageServerLoad = async ({ locals }) => { .eq('member_id', member.id) .order('created_at', { ascending: false }); + // Get the ID from query param to auto-expand that notification + const expandedId = url.searchParams.get('id'); + return { - notifications: notifications || [] + notifications: notifications || [], + expandedId }; }; diff --git a/src/routes/(app)/notifications/+page.svelte b/src/routes/(app)/notifications/+page.svelte index 55c1215..141f8eb 100644 --- a/src/routes/(app)/notifications/+page.svelte +++ b/src/routes/(app)/notifications/+page.svelte @@ -1,6 +1,7 @@ @@ -179,42 +202,74 @@ {#each filteredNotifications as notification} {@const Icon = getTypeIcon(notification.type)} {@const badge = getTypeBadge(notification.type)} - + + + + {#if isExpanded} +
+
+

{notification.message}

+
+ {#if notification.link} + + {/if} +
+ {/if} + {/each} {/if}