Implement secure client-side favorites loading to fix nginx 502 issues
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				CI / update (push) Successful in 16s
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	CI / update (push) Successful in 16s
				
			- Create client-side favorites store with secure authentication - Remove server-side favorites fetching that caused nginx routing issues - Update FavoriteButton to properly handle short_name/ObjectId relationship - Use existing /api/rezepte/favorites/check endpoint for status checking - Maintain security by requiring authentication for all favorites operations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
		@@ -1,30 +1,28 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
  export let recipeId: string;
 | 
			
		||||
  export let isFavorite: boolean = false;
 | 
			
		||||
  import { favorites } from '$lib/stores/favorites';
 | 
			
		||||
  import { onMount } from 'svelte';
 | 
			
		||||
  
 | 
			
		||||
  export let recipeId: string; // This will be short_name from the component usage
 | 
			
		||||
  export let isFavorite: boolean = false; // Initial state from server
 | 
			
		||||
  export let isLoggedIn: boolean = false;
 | 
			
		||||
  
 | 
			
		||||
  let currentIsFavorite = isFavorite;
 | 
			
		||||
  let isLoading = false;
 | 
			
		||||
 | 
			
		||||
  // Load current favorite status when component mounts
 | 
			
		||||
  onMount(async () => {
 | 
			
		||||
    if (isLoggedIn) {
 | 
			
		||||
      currentIsFavorite = await favorites.isFavoriteByShortName(recipeId);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  async function toggleFavorite() {
 | 
			
		||||
    if (!isLoggedIn || isLoading) return;
 | 
			
		||||
    
 | 
			
		||||
    isLoading = true;
 | 
			
		||||
    
 | 
			
		||||
    try {
 | 
			
		||||
      const method = isFavorite ? 'DELETE' : 'POST';
 | 
			
		||||
      const response = await fetch('/api/rezepte/favorites', {
 | 
			
		||||
        method,
 | 
			
		||||
        headers: {
 | 
			
		||||
          'Content-Type': 'application/json',
 | 
			
		||||
        },
 | 
			
		||||
        body: JSON.stringify({ recipeId }),
 | 
			
		||||
      });
 | 
			
		||||
      
 | 
			
		||||
      if (response.ok) {
 | 
			
		||||
        isFavorite = !isFavorite;
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('Failed to toggle favorite:', error);
 | 
			
		||||
      await favorites.toggle(recipeId);
 | 
			
		||||
      currentIsFavorite = !currentIsFavorite;
 | 
			
		||||
    } finally {
 | 
			
		||||
      isLoading = false;
 | 
			
		||||
    }
 | 
			
		||||
@@ -59,8 +57,8 @@
 | 
			
		||||
    class="favorite-button"
 | 
			
		||||
    disabled={isLoading}
 | 
			
		||||
    on:click={toggleFavorite}
 | 
			
		||||
    title={isFavorite ? 'Favorit entfernen' : 'Als Favorit speichern'}
 | 
			
		||||
    title={currentIsFavorite ? 'Favorit entfernen' : 'Als Favorit speichern'}
 | 
			
		||||
  >
 | 
			
		||||
    {isFavorite ? '❤️' : '🖤'}
 | 
			
		||||
    {currentIsFavorite ? '❤️' : '🖤'}
 | 
			
		||||
  </button>
 | 
			
		||||
{/if}
 | 
			
		||||
							
								
								
									
										156
									
								
								src/lib/stores/favorites.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								src/lib/stores/favorites.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,156 @@
 | 
			
		||||
import { writable, derived } from 'svelte/store';
 | 
			
		||||
import { page } from '$app/stores';
 | 
			
		||||
 | 
			
		||||
export interface FavoritesState {
 | 
			
		||||
    favorites: string[]; // Array of ObjectIds
 | 
			
		||||
    shortNameToObjectId: Map<string, string>; // Mapping from short_name to ObjectId
 | 
			
		||||
    loading: boolean;
 | 
			
		||||
    error: string | null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Create the favorites store
 | 
			
		||||
function createFavoritesStore() {
 | 
			
		||||
    const { subscribe, set, update } = writable<FavoritesState>({
 | 
			
		||||
        favorites: [],
 | 
			
		||||
        shortNameToObjectId: new Map(),
 | 
			
		||||
        loading: false,
 | 
			
		||||
        error: null
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        subscribe,
 | 
			
		||||
        
 | 
			
		||||
        // Load user's favorites from API
 | 
			
		||||
        async load() {
 | 
			
		||||
            update(state => ({ ...state, loading: true, error: null }));
 | 
			
		||||
            
 | 
			
		||||
            try {
 | 
			
		||||
                const response = await fetch('/api/rezepte/favorites', {
 | 
			
		||||
                    credentials: 'include' // Ensure cookies are sent for authentication
 | 
			
		||||
                });
 | 
			
		||||
                
 | 
			
		||||
                if (response.ok) {
 | 
			
		||||
                    const data = await response.json();
 | 
			
		||||
                    update(state => ({
 | 
			
		||||
                        ...state,
 | 
			
		||||
                        favorites: data.favorites || [],
 | 
			
		||||
                        loading: false
 | 
			
		||||
                    }));
 | 
			
		||||
                } else if (response.status === 401) {
 | 
			
		||||
                    // User not authenticated, clear favorites
 | 
			
		||||
                    update(state => ({
 | 
			
		||||
                        ...state,
 | 
			
		||||
                        favorites: [],
 | 
			
		||||
                        loading: false
 | 
			
		||||
                    }));
 | 
			
		||||
                } else {
 | 
			
		||||
                    throw new Error(`Failed to load favorites: ${response.status}`);
 | 
			
		||||
                }
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                console.error('Error loading favorites:', error);
 | 
			
		||||
                update(state => ({
 | 
			
		||||
                    ...state,
 | 
			
		||||
                    loading: false,
 | 
			
		||||
                    error: error instanceof Error ? error.message : 'Failed to load favorites'
 | 
			
		||||
                }));
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // Add a recipe to favorites
 | 
			
		||||
        async add(recipeShortName: string) {
 | 
			
		||||
            try {
 | 
			
		||||
                const response = await fetch('/api/rezepte/favorites', {
 | 
			
		||||
                    method: 'POST',
 | 
			
		||||
                    headers: {
 | 
			
		||||
                        'Content-Type': 'application/json'
 | 
			
		||||
                    },
 | 
			
		||||
                    body: JSON.stringify({ recipeId: recipeShortName }),
 | 
			
		||||
                    credentials: 'include'
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                if (response.ok) {
 | 
			
		||||
                    // Reload favorites to get the updated list with ObjectIds
 | 
			
		||||
                    await this.load();
 | 
			
		||||
                } else {
 | 
			
		||||
                    throw new Error(`Failed to add favorite: ${response.status}`);
 | 
			
		||||
                }
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                console.error('Error adding favorite:', error);
 | 
			
		||||
                update(state => ({
 | 
			
		||||
                    ...state,
 | 
			
		||||
                    error: error instanceof Error ? error.message : 'Failed to add favorite'
 | 
			
		||||
                }));
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // Remove a recipe from favorites
 | 
			
		||||
        async remove(recipeShortName: string) {
 | 
			
		||||
            try {
 | 
			
		||||
                const response = await fetch('/api/rezepte/favorites', {
 | 
			
		||||
                    method: 'DELETE',
 | 
			
		||||
                    headers: {
 | 
			
		||||
                        'Content-Type': 'application/json'
 | 
			
		||||
                    },
 | 
			
		||||
                    body: JSON.stringify({ recipeId: recipeShortName }),
 | 
			
		||||
                    credentials: 'include'
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                if (response.ok) {
 | 
			
		||||
                    // Reload favorites to get the updated list
 | 
			
		||||
                    await this.load();
 | 
			
		||||
                } else {
 | 
			
		||||
                    throw new Error(`Failed to remove favorite: ${response.status}`);
 | 
			
		||||
                }
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                console.error('Error removing favorite:', error);
 | 
			
		||||
                update(state => ({
 | 
			
		||||
                    ...state,
 | 
			
		||||
                    error: error instanceof Error ? error.message : 'Failed to remove favorite'
 | 
			
		||||
                }));
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // Toggle favorite status by short_name
 | 
			
		||||
        async toggle(recipeShortName: string) {
 | 
			
		||||
            // Check if favorited by checking the current recipe list for this short_name
 | 
			
		||||
            const response = await fetch(`/api/rezepte/favorites/check/${recipeShortName}`, {
 | 
			
		||||
                credentials: 'include'
 | 
			
		||||
            });
 | 
			
		||||
            
 | 
			
		||||
            if (response.ok) {
 | 
			
		||||
                const { isFavorite } = await response.json();
 | 
			
		||||
                if (isFavorite) {
 | 
			
		||||
                    await this.remove(recipeShortName);
 | 
			
		||||
                } else {
 | 
			
		||||
                    await this.add(recipeShortName);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // Check if a recipe is favorited by short_name
 | 
			
		||||
        async isFavoriteByShortName(shortName: string): Promise<boolean> {
 | 
			
		||||
            try {
 | 
			
		||||
                const response = await fetch(`/api/rezepte/favorites/check/${shortName}`, {
 | 
			
		||||
                    credentials: 'include'
 | 
			
		||||
                });
 | 
			
		||||
                if (response.ok) {
 | 
			
		||||
                    const { isFavorite } = await response.json();
 | 
			
		||||
                    return isFavorite;
 | 
			
		||||
                }
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                console.error('Error checking favorite status:', error);
 | 
			
		||||
            }
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const favorites = createFavoritesStore();
 | 
			
		||||
 | 
			
		||||
// Helper function to add favorite status to recipes on client-side
 | 
			
		||||
export function addFavoriteStatusToRecipes(recipes: any[], userFavorites: string[]): any[] {
 | 
			
		||||
    return recipes.map(recipe => ({
 | 
			
		||||
        ...recipe,
 | 
			
		||||
        isFavorite: userFavorites.includes(recipe._id?.toString() || recipe.short_name)
 | 
			
		||||
    }));
 | 
			
		||||
}
 | 
			
		||||
@@ -1,22 +1,14 @@
 | 
			
		||||
import type { PageServerLoad } from "./$types";
 | 
			
		||||
import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites";
 | 
			
		||||
 | 
			
		||||
export async function load({ fetch, locals }) {
 | 
			
		||||
export async function load({ fetch }) {
 | 
			
		||||
    let current_month = new Date().getMonth() + 1
 | 
			
		||||
    const res_season = await fetch(`/api/rezepte/items/in_season/` + current_month);
 | 
			
		||||
    const res_all_brief = await fetch(`/api/rezepte/items/all_brief`);
 | 
			
		||||
    const item_season = await res_season.json();
 | 
			
		||||
    const item_all_brief = await res_all_brief.json();
 | 
			
		||||
    
 | 
			
		||||
    // Get user favorites and session
 | 
			
		||||
    const [userFavorites, session] = await Promise.all([
 | 
			
		||||
        getUserFavorites(fetch, locals),
 | 
			
		||||
        locals.auth()
 | 
			
		||||
    ]);
 | 
			
		||||
    
 | 
			
		||||
    return {
 | 
			
		||||
        season: addFavoriteStatusToRecipes(item_season, userFavorites),
 | 
			
		||||
        all_brief: addFavoriteStatusToRecipes(item_all_brief, userFavorites),
 | 
			
		||||
        session
 | 
			
		||||
        season: item_season,
 | 
			
		||||
        all_brief: item_all_brief
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,26 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import type { PageData } from './$types';
 | 
			
		||||
	import { onMount } from 'svelte';
 | 
			
		||||
	import { favorites, addFavoriteStatusToRecipes } from '$lib/stores/favorites';
 | 
			
		||||
	import { page } from '$app/stores';
 | 
			
		||||
    	import MediaScroller from '$lib/components/MediaScroller.svelte';
 | 
			
		||||
    	import AddButton from '$lib/components/AddButton.svelte';
 | 
			
		||||
    	import Card from '$lib/components/Card.svelte';
 | 
			
		||||
    	import Search from '$lib/components/Search.svelte';
 | 
			
		||||
    	export let data: PageData;
 | 
			
		||||
    	export let current_month = new Date().getMonth() + 1
 | 
			
		||||
	const categories = ["Hauptspeise", "Nudel", "Brot", "Dessert", "Suppe", "Beilage", "Salat", "Kuchen", "Frühstück", "Sauce", "Zutat", "Getränk", "Aufstrich", "Guetzli", "Snack"]
 | 
			
		||||
	const categories = ["Hauptspeise", "Nudel", "Brot", "Dessert", "Suppe", "Beilage", "Salat", "Kuchen", "Frühstück", "Sauce", "Zutat", "Getränk", "Aufstrich", "Guetzli", "Snack"];
 | 
			
		||||
 | 
			
		||||
	// Load favorites when component mounts and user is authenticated
 | 
			
		||||
	onMount(() => {
 | 
			
		||||
		if ($page.data.session?.user) {
 | 
			
		||||
			favorites.load();
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	// Reactively add favorite status to recipes
 | 
			
		||||
	$: seasonWithFavorites = addFavoriteStatusToRecipes(data.season, $favorites.favorites);
 | 
			
		||||
	$: allBriefWithFavorites = addFavoriteStatusToRecipes(data.all_brief, $favorites.favorites);
 | 
			
		||||
</script>
 | 
			
		||||
<style>
 | 
			
		||||
h1{
 | 
			
		||||
@@ -35,15 +49,15 @@ h1{
 | 
			
		||||
<Search></Search>
 | 
			
		||||
 | 
			
		||||
<MediaScroller title="In Saison">
 | 
			
		||||
{#each data.season as recipe}
 | 
			
		||||
	<Card {recipe} {current_month} loading_strat={"eager"} do_margin_right={true} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user}></Card>
 | 
			
		||||
{#each seasonWithFavorites as recipe}
 | 
			
		||||
	<Card {recipe} {current_month} loading_strat={"eager"} do_margin_right={true} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!$page.data.session?.user}></Card>
 | 
			
		||||
{/each}
 | 
			
		||||
</MediaScroller>
 | 
			
		||||
 | 
			
		||||
{#each categories as category}
 | 
			
		||||
	<MediaScroller title={category}>
 | 
			
		||||
	{#each data.all_brief.filter(recipe => recipe.category == category) as recipe}
 | 
			
		||||
		<Card {recipe} {current_month} do_margin_right={true} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user}></Card>
 | 
			
		||||
	{#each allBriefWithFavorites.filter(recipe => recipe.category == category) as recipe}
 | 
			
		||||
		<Card {recipe} {current_month} do_margin_right={true} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!$page.data.session?.user}></Card>
 | 
			
		||||
	{/each}
 | 
			
		||||
	</MediaScroller>
 | 
			
		||||
{/each}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user