ai cooking
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #194 from paulgmiller/pmiller/refreshonsesionexpire

Pmiller/refreshonsesionexpire

authored by

Paul Miller and committed by
GitHub
f30296de ee94fea8

+73 -39
+9 -7
cmd/careme/web.go
··· 52 52 return fmt.Errorf("failed to create recipe generator: %w", err) 53 53 } 54 54 55 - tailwindETag := fmt.Sprintf(`"%x"`, sha256.Sum256(tailwindCSS)) 55 + tailwindETag := fmt.Sprintf(`"%x"`, sha256.Sum256(tailwindCSS)) 56 56 mux.HandleFunc("/static/tailwind.css", func(w http.ResponseWriter, r *http.Request) { 57 57 if r.Header.Get("If-None-Match") == tailwindETag { 58 58 w.WriteHeader(http.StatusNotModified) ··· 108 108 } 109 109 } 110 110 data := struct { 111 - ClarityScript template.HTML 112 - User *users.User 113 - Style seasons.Style 111 + ClarityScript template.HTML 112 + User *users.User 113 + Style seasons.Style 114 + ServerSignedIn bool 114 115 }{ 115 - ClarityScript: templates.ClarityScript(), 116 - User: currentUser, 117 - Style: seasons.GetCurrentStyle(), 116 + ClarityScript: templates.ClarityScript(), 117 + User: currentUser, 118 + Style: seasons.GetCurrentStyle(), 119 + ServerSignedIn: currentUser != nil, 118 120 } 119 121 if err := templates.Home.Execute(w, data); err != nil { 120 122 slog.ErrorContext(ctx, "home template execute error", "error", err)
+1 -1
internal/recipes/buttons_test.go
··· 40 40 loc := locations.Location{ID: "L1", Name: "Store", Address: "1 Main St"} 41 41 p := DefaultParams(&loc, time.Now()) 42 42 w := httptest.NewRecorder() 43 - FormatShoppingListHTML(p, multiRecipeList, w) 43 + FormatShoppingListHTML(p, multiRecipeList, true, w) 44 44 html := w.Body.String() 45 45 46 46 // Verify HTML is valid
+18 -14
internal/recipes/html.go
··· 12 12 ) 13 13 14 14 // FormatShoppingListHTML renders the multi-recipe shopping list view. 15 - func FormatShoppingListHTML(p *generatorParams, l ai.ShoppingList, writer http.ResponseWriter) { 15 + func FormatShoppingListHTML(p *generatorParams, l ai.ShoppingList, signedIn bool, writer http.ResponseWriter) { 16 16 // TODO just put params into shopping list and pass that up? 17 17 data := struct { 18 18 Location locations.Location ··· 24 24 ShoppingList []ai.Ingredient 25 25 ConversationID string 26 26 Style seasons.Style 27 + ServerSignedIn bool 27 28 }{ 28 29 Location: *p.Location, 29 30 Date: p.Date.Format("2006-01-02"), ··· 34 35 ShoppingList: shoppingListForDisplay(l.Recipes), 35 36 ConversationID: l.ConversationID, 36 37 Style: seasons.GetCurrentStyle(), 38 + ServerSignedIn: signedIn, 37 39 } 38 40 39 41 if err := templates.ShoppingList.Execute(writer, data); err != nil { ··· 42 44 } 43 45 44 46 // FormatRecipeHTML renders a single recipe view. 45 - func FormatRecipeHTML(p *generatorParams, recipe ai.Recipe, writer http.ResponseWriter) { 47 + func FormatRecipeHTML(p *generatorParams, recipe ai.Recipe, signedIn bool, writer http.ResponseWriter) { 46 48 data := struct { 47 - Location locations.Location 48 - Date string 49 - ClarityScript template.HTML 50 - Recipe ai.Recipe 51 - OriginHash string 52 - Style seasons.Style 49 + Location locations.Location 50 + Date string 51 + ClarityScript template.HTML 52 + Recipe ai.Recipe 53 + OriginHash string 54 + Style seasons.Style 55 + ServerSignedIn bool 53 56 }{ 54 - Location: *p.Location, 55 - Date: p.Date.Format("2006-01-02"), 56 - ClarityScript: templates.ClarityScript(), 57 - Recipe: recipe, 58 - OriginHash: recipe.OriginHash, 59 - Style: seasons.GetCurrentStyle(), 57 + Location: *p.Location, 58 + Date: p.Date.Format("2006-01-02"), 59 + ClarityScript: templates.ClarityScript(), 60 + Recipe: recipe, 61 + OriginHash: recipe.OriginHash, 62 + Style: seasons.GetCurrentStyle(), 63 + ServerSignedIn: signedIn, 60 64 } 61 65 62 66 if err := templates.Recipe.Execute(writer, data); err != nil {
+6 -6
internal/recipes/html_test.go
··· 47 47 loc := locations.Location{ID: "L1", Name: "Store", Address: "1 Main St"} 48 48 p := DefaultParams(&loc, time.Now()) 49 49 w := httptest.NewRecorder() 50 - FormatShoppingListHTML(p, list, w) 50 + FormatShoppingListHTML(p, list, true, w) 51 51 html := w.Body.String() 52 52 if w.Code != http.StatusOK { 53 53 t.Error("Want ok statuscode") ··· 59 59 loc := locations.Location{ID: "L1", Name: "Store", Address: "1 Main St"} 60 60 p := DefaultParams(&loc, time.Now()) 61 61 w := httptest.NewRecorder() 62 - FormatShoppingListHTML(p, list, w) 62 + FormatShoppingListHTML(p, list, true, w) 63 63 html := w.Body.String() 64 64 65 65 isValidHTML(t, html) ··· 73 73 p := DefaultParams(&loc, time.Now()) 74 74 templates.SetClarity("test456") 75 75 w := httptest.NewRecorder() 76 - FormatShoppingListHTML(p, list, w) 76 + FormatShoppingListHTML(p, list, true, w) 77 77 if !bytes.Contains(w.Body.Bytes(), []byte("www.clarity.ms/tag/")) { 78 78 t.Error("HTML should contain Clarity script URL") 79 79 } ··· 88 88 p := DefaultParams(&loc, time.Now()) 89 89 templates.SetClarity("") 90 90 w := httptest.NewRecorder() 91 - FormatShoppingListHTML(p, list, w) 91 + FormatShoppingListHTML(p, list, true, w) 92 92 if bytes.Contains(w.Body.Bytes(), []byte("clarity.ms")) { 93 93 t.Error("HTML should not contain Clarity script when project ID is empty") 94 94 } ··· 98 98 loc := locations.Location{ID: "L1", Name: "Store", Address: "1 Main St"} 99 99 p := DefaultParams(&loc, time.Now()) 100 100 w := httptest.NewRecorder() 101 - FormatShoppingListHTML(p, list, w) 101 + FormatShoppingListHTML(p, list, true, w) 102 102 html := w.Body.String() 103 103 104 104 // Verify "Careme Recipes" is a link to home page ··· 115 115 p := DefaultParams(&loc, time.Now()) 116 116 p.ConversationID = "convo123" 117 117 w := httptest.NewRecorder() 118 - FormatRecipeHTML(p, list.Recipes[0], w) 118 + FormatRecipeHTML(p, list.Recipes[0], true, w) 119 119 html := w.Body.String() 120 120 121 121 isValidHTML(t, html)
+8 -4
internal/recipes/server.go
··· 79 79 http.Error(w, "recipe not found", http.StatusNotFound) 80 80 return 81 81 } 82 + _, err = s.clerk.GetUserIDFromRequest(r) 83 + signedIn := !errors.Is(err, auth.ErrNoSession) 82 84 83 85 if recipe.OriginHash == "" { 84 86 slog.WarnContext(ctx, "recipe missing origin hash Probably and old recipe", "hash", hash) ··· 86 88 ID: "", 87 89 Name: "Unknown Location", 88 90 }, time.Now()) 89 - FormatRecipeHTML(p, *recipe, w) 91 + FormatRecipeHTML(p, *recipe, signedIn, w) 90 92 return 91 93 } 92 94 ··· 105 107 106 108 // TODO: Add questions or regneration to signle recipes 107 109 108 - slog.InfoContext(ctx, "serving shared recipe by hash", "hash", hash) 109 - FormatRecipeHTML(p, *recipe, w) 110 + slog.InfoContext(ctx, "serving shared recipe by hash", "hash", hash, "signedIn", signedIn) 111 + FormatRecipeHTML(p, *recipe, signedIn, w) 110 112 } 111 113 112 114 const ( ··· 180 182 } 181 183 return 182 184 } 183 - FormatShoppingListHTML(p, *slist, w) 185 + _, err = s.clerk.GetUserIDFromRequest(r) 186 + signedIn := !errors.Is(err, auth.ErrNoSession) 187 + FormatShoppingListHTML(p, *slist, signedIn, w) 184 188 return 185 189 } 186 190
+26 -7
internal/templates/clerk_refresh.html
··· 7 7 </script> 8 8 9 9 <script> 10 - // Minimal: initialize Clerk so it can keep session tokens refreshed 11 10 (async () => { 12 - // wait until Clerk is present 11 + const serverSignedIn = {{.ServerSignedIn}}; 12 + {{/* https://chatgpt.com/share/698a36fe-0834-8009-8f5e-d7dfcccae7c0 13 + // Without this, you can get stuck in: 14 + // 1) SSR renders logged-out (expired/missing __session) 15 + // 2) Clerk loads and reports signed-in (it can rehydrate from client state) 16 + // 3) You reload to let SSR see the fresh session 17 + // 4) SSR *still* renders logged-out (cookie timing/scope, caching, stricter validation, dev quirks) 18 + // 5) Clerk still reports signed-in -> reload again -> forever 19 + // 20 + // sessionStorage is per-tab and clears when the tab closes, which is ideal here. 21 + */}} 22 + const key = `clerk-ssr-sync-reloaded:${location.pathname}${location.search}`; 23 + 24 + // Wait for the Clerk global to exist, then initialize it. 13 25 while (!window.Clerk?.load) await new Promise(r => setTimeout(r, 10)); 14 26 await Clerk.load(); 15 27 16 - // Optional: if you want to be extra defensive, force-refresh occasionally 17 - // (Clerk docs describe forcing refresh via skipCache on getToken) 18 - // setInterval(async () => { 19 - // if (Clerk.session) await Clerk.session.getToken({ skipCache: true }); 20 - // }, 45_000); 28 + const clerkSignedIn = !!Clerk.isSignedIn; 29 + 30 + {{/* The core fix: 31 + // If SSR thought we're signed-out, but Clerk says we're signed-in, 32 + // then Clerk likely just re-established a valid browser session *after* SSR ran. 33 + // Do ONE reload so the *next* SSR request includes the refreshed __session cookie. 34 + */}} 35 + if (!serverSignedIn && clerkSignedIn && !sessionStorage.getItem(key)) { 36 + sessionStorage.setItem(key, "1"); 37 + location.reload(); 38 + return; 39 + } 21 40 })(); 22 41 </script> 23 42 {{end}}
+5
internal/users/server.go
··· 106 106 http.Error(w, "unable to load account", http.StatusInternalServerError) 107 107 return 108 108 } 109 + // if session expires this is less than optimal. We want to give them just the 110 + // clerk_refresh and seee if they are then logged in. But we only want to do that once? 111 + // TODO stick just show a sign in button on user page if no session 109 112 http.Redirect(w, r, "/", http.StatusSeeOther) 110 113 return 111 114 } ··· 163 166 Success bool 164 167 FavoriteStoreName string 165 168 Style seasons.Style 169 + ServerSignedIn bool 166 170 }{ 167 171 ClarityScript: templates.ClarityScript(), 168 172 User: currentUser, 169 173 Success: success, 170 174 FavoriteStoreName: favoriteStoreName, 171 175 Style: seasons.GetCurrentStyle(), 176 + ServerSignedIn: true, 172 177 } 173 178 if err := s.userTmpl.Execute(w, data); err != nil { 174 179 slog.ErrorContext(ctx, "user template execute error", "error", err)