ai cooking
0
fork

Configure Feed

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

admin middle ware codex resume 019c913c-4d41-7802-b1fc-0f26b3c11bc8 (#261)

* admin middle ware codex resume 019c913c-4d41-7802-b1fc-0f26b3c11bc8

* forgot new toys

* register on admin mux

* whoops

* naming fixes and hide internals

* hide handler

* be specfic

* spell origin right

* don't waste my chars codex

---------

Co-authored-by: paul miller <paul.miller>

authored by

Paul Miller
paul miller
and committed by
GitHub
3d5e3742 9f85e210

+268 -28
+5 -1
cmd/careme/web.go
··· 1 1 package main 2 2 3 3 import ( 4 + "careme/internal/admin" 4 5 "careme/internal/auth" 5 6 "careme/internal/cache" 6 7 "careme/internal/config" ··· 65 66 recipeHandler := recipes.NewHandler(cfg, userStorage, generator, locationStorage, cache, authClient) 66 67 recipeHandler.Register(mux) 67 68 69 + adminMux := http.NewServeMux() 70 + mux.Handle("/admin/", admin.New(cfg, authClient).Enforce(http.StripPrefix("/admin", adminMux))) 71 + 68 72 if logsinkCfg.Enabled() { 69 73 logsHandler, err := logs.NewHandler(logsinkCfg) 70 74 if err != nil { 71 75 return fmt.Errorf("failed to create logs handler: %w", err) 72 76 } 73 - logsHandler.Register(mux) 77 + logsHandler.Register(adminMux) 74 78 } 75 79 76 80 mux.HandleFunc("/about", func(w http.ResponseWriter, r *http.Request) {
+2
deploy/deploy.yaml
··· 32 32 env: 33 33 - name: CLARITY_PROJECT_ID 34 34 value: "td2gxd3sq9" 35 + - name: ADMIN_EMAILS 36 + value: "paul.miller@gmail.com" 35 37 volumeMounts: 36 38 - name: recipes 37 39 mountPath: /recipes
+70
internal/admin/middleware.go
··· 1 + package admin 2 + 3 + import ( 4 + "careme/internal/auth" 5 + "careme/internal/config" 6 + "errors" 7 + "log/slog" 8 + "net/http" 9 + "strings" 10 + ) 11 + 12 + type middleware struct { 13 + auth auth.AuthClient 14 + admins map[string]struct{} 15 + } 16 + 17 + func New(cfg *config.Config, authClient auth.AuthClient) *middleware { 18 + admins := make(map[string]struct{}, len(cfg.Admin.Emails)) 19 + for _, email := range cfg.Admin.Emails { 20 + normalized := normalizeEmail(email) 21 + if normalized == "" { 22 + continue 23 + } 24 + admins[normalized] = struct{}{} 25 + } 26 + 27 + return &middleware{ 28 + auth: authClient, 29 + admins: admins, 30 + } 31 + } 32 + 33 + func (m *middleware) Enforce(next http.Handler) http.Handler { 34 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 35 + userID, err := m.auth.GetUserIDFromRequest(r) 36 + if err != nil { 37 + if !errors.Is(err, auth.ErrNoSession) { 38 + slog.WarnContext(r.Context(), "admin auth failed", "error", err) 39 + } 40 + http.NotFound(w, r) 41 + return 42 + } 43 + 44 + email, err := m.auth.GetUserEmail(r.Context(), userID) 45 + if err != nil { 46 + slog.WarnContext(r.Context(), "admin email lookup failed", "user_id", userID, "error", err) 47 + http.NotFound(w, r) 48 + return 49 + } 50 + 51 + if !m.isAdmin(email) { 52 + http.NotFound(w, r) 53 + return 54 + } 55 + 56 + next.ServeHTTP(w, r) 57 + }) 58 + } 59 + 60 + func (m *middleware) isAdmin(email string) bool { 61 + if len(m.admins) == 0 { 62 + return true 63 + } 64 + _, ok := m.admins[normalizeEmail(email)] 65 + return ok 66 + } 67 + 68 + func normalizeEmail(email string) string { 69 + return strings.ToLower(strings.TrimSpace(email)) 70 + }
+156
internal/admin/middleware_test.go
··· 1 + package admin 2 + 3 + import ( 4 + "careme/internal/auth" 5 + "careme/internal/config" 6 + "context" 7 + "errors" 8 + "net/http" 9 + "net/http/httptest" 10 + "testing" 11 + ) 12 + 13 + type stubAuthClient struct { 14 + userID string 15 + userIDErr error 16 + email string 17 + emailErr error 18 + } 19 + 20 + func (s stubAuthClient) GetUserEmail(_ context.Context, _ string) (string, error) { 21 + if s.emailErr != nil { 22 + return "", s.emailErr 23 + } 24 + return s.email, nil 25 + } 26 + 27 + func (s stubAuthClient) GetUserIDFromRequest(_ *http.Request) (string, error) { 28 + if s.userIDErr != nil { 29 + return "", s.userIDErr 30 + } 31 + return s.userID, nil 32 + } 33 + 34 + func (s stubAuthClient) WithAuthHTTP(handler http.Handler) http.Handler { 35 + return handler 36 + } 37 + 38 + func (s stubAuthClient) Register(_ *http.ServeMux) {} 39 + 40 + func TestMiddlewareWrapRejectsNoSession(t *testing.T) { 41 + t.Parallel() 42 + 43 + m := New(&config.Config{ 44 + Admin: config.AdminConfig{ 45 + Emails: []string{"admin@example.com"}, 46 + }, 47 + }, stubAuthClient{userIDErr: auth.ErrNoSession}) 48 + 49 + req := httptest.NewRequest(http.MethodGet, "/logs", nil) 50 + rr := httptest.NewRecorder() 51 + 52 + next := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 53 + w.WriteHeader(http.StatusNoContent) 54 + }) 55 + m.Enforce(next).ServeHTTP(rr, req) 56 + 57 + if rr.Code != http.StatusNotFound { 58 + t.Fatalf("expected status %d, got %d", http.StatusNotFound, rr.Code) 59 + } 60 + } 61 + 62 + func TestMiddlewareWrapRejectsNonAdmin(t *testing.T) { 63 + t.Parallel() 64 + 65 + m := New(&config.Config{ 66 + Admin: config.AdminConfig{ 67 + Emails: []string{"admin@example.com"}, 68 + }, 69 + }, stubAuthClient{ 70 + userID: "user_123", 71 + email: "user@example.com", 72 + }) 73 + 74 + req := httptest.NewRequest(http.MethodGet, "/logs", nil) 75 + rr := httptest.NewRecorder() 76 + 77 + next := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 78 + w.WriteHeader(http.StatusNoContent) 79 + }) 80 + m.Enforce(next).ServeHTTP(rr, req) 81 + 82 + if rr.Code != http.StatusNotFound { 83 + t.Fatalf("expected status %d, got %d", http.StatusNotFound, rr.Code) 84 + } 85 + } 86 + 87 + func TestMiddlewareWrapAllowsAdmin(t *testing.T) { 88 + t.Parallel() 89 + 90 + m := New(&config.Config{ 91 + Admin: config.AdminConfig{ 92 + Emails: []string{"admin@example.com"}, 93 + }, 94 + }, stubAuthClient{ 95 + userID: "user_123", 96 + email: "ADMIN@example.com", 97 + }) 98 + 99 + req := httptest.NewRequest(http.MethodGet, "/logs", nil) 100 + rr := httptest.NewRecorder() 101 + 102 + next := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 103 + w.WriteHeader(http.StatusNoContent) 104 + }) 105 + m.Enforce(next).ServeHTTP(rr, req) 106 + 107 + if rr.Code != http.StatusNoContent { 108 + t.Fatalf("expected status %d, got %d", http.StatusNoContent, rr.Code) 109 + } 110 + } 111 + 112 + func TestMiddlewareWrapRejectsEmailLookupFailure(t *testing.T) { 113 + t.Parallel() 114 + 115 + m := New(&config.Config{ 116 + Admin: config.AdminConfig{ 117 + Emails: []string{"admin@example.com"}, 118 + }, 119 + }, stubAuthClient{ 120 + userID: "user_123", 121 + emailErr: errors.New("lookup failed"), 122 + }) 123 + 124 + req := httptest.NewRequest(http.MethodGet, "/logs", nil) 125 + rr := httptest.NewRecorder() 126 + 127 + next := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 128 + w.WriteHeader(http.StatusNoContent) 129 + }) 130 + m.Enforce(next).ServeHTTP(rr, req) 131 + 132 + if rr.Code != http.StatusNotFound { 133 + t.Fatalf("expected status %d, got %d", http.StatusNotFound, rr.Code) 134 + } 135 + } 136 + 137 + func TestMiddlewareWrapAllowsAnyLoggedInUserWhenNoAdminsConfigured(t *testing.T) { 138 + t.Parallel() 139 + 140 + m := New(&config.Config{}, stubAuthClient{ 141 + userID: "user_123", 142 + email: "user@example.com", 143 + }) 144 + 145 + req := httptest.NewRequest(http.MethodGet, "/logs", nil) 146 + rr := httptest.NewRecorder() 147 + 148 + next := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 149 + w.WriteHeader(http.StatusNoContent) 150 + }) 151 + m.Enforce(next).ServeHTTP(rr, req) 152 + 153 + if rr.Code != http.StatusNoContent { 154 + t.Fatalf("expected status %d, got %d", http.StatusNoContent, rr.Code) 155 + } 156 + }
+31
internal/config/config.go
··· 11 11 Kroger KrogerConfig `json:"kroger"` 12 12 Mocks MockConfig `json:"mocks"` 13 13 Clerk ClerkConfig `json:"clerk"` 14 + Admin AdminConfig `json:"admin"` 14 15 } 15 16 16 17 type AIConfig struct { ··· 32 33 PublishableKey string 33 34 Domain string 34 35 Prod bool 36 + } 37 + 38 + type AdminConfig struct { 39 + Emails []string `json:"emails"` 35 40 } 36 41 37 42 func (c *ClerkConfig) IsEnabled() bool { ··· 74 79 SecretKey: os.Getenv("CLERK_SECRET_KEY"), 75 80 PublishableKey: os.Getenv("CLERK_PUBLISHABLE_KEY"), 76 81 Domain: os.Getenv("CLERK_DOMAIN"), 82 + }, 83 + Admin: AdminConfig{ 84 + Emails: parseAdminEmails(os.Getenv("ADMIN_EMAILS")), 77 85 }, 78 86 } 79 87 if strings.HasSuffix(config.Clerk.Domain, "careme.cooking") { ··· 99 107 } 100 108 return nil 101 109 } 110 + 111 + func parseAdminEmails(value string) []string { 112 + if value == "" { 113 + return nil 114 + } 115 + 116 + parts := strings.Split(value, ",") 117 + emails := make([]string, 0, len(parts)) 118 + seen := make(map[string]struct{}, len(parts)) 119 + for _, part := range parts { 120 + email := strings.ToLower(strings.TrimSpace(part)) 121 + if email == "" { 122 + continue 123 + } 124 + if _, ok := seen[email]; ok { 125 + continue 126 + } 127 + seen[email] = struct{}{} 128 + emails = append(emails, email) 129 + } 130 + 131 + return emails 132 + }
+4 -27
internal/logs/handler.go
··· 1 - // TODO merge with log sink 2 1 package logs 3 2 4 3 import ( 5 - "careme/internal/auth" 6 4 "careme/internal/logsink" 7 - "errors" 8 5 "fmt" 9 6 "log/slog" 10 7 "net/http" 11 8 "strconv" 12 9 ) 13 10 14 - // Handler handles HTTP requests for log viewing 15 11 type handler struct { 16 12 reader *Reader 17 - auth auth.AuthClient 18 13 } 19 14 20 - // NewHandler creates a new logs HTTP handler 21 15 func NewHandler(cfg logsink.Config) (*handler, error) { 22 - // Only create reader if Azure credentials are available 23 16 reader, err := NewReader(&cfg) 24 17 if err != nil { 25 18 return nil, fmt.Errorf("failed to create log reader: %w", err) ··· 37 30 } 38 31 39 32 func (h *handler) handleLogsPage(w http.ResponseWriter, r *http.Request) { 40 - if _, err := h.auth.GetUserIDFromRequest(r); errors.Is(err, auth.ErrNoSession) { 41 - w.WriteHeader(http.StatusNotFound) 42 - return 43 - } 44 - 45 33 w.Header().Set("Content-Type", "text/html; charset=utf-8") 46 34 w.Header().Set("X-Content-Type-Options", "nosniff") 47 35 w.Header().Set("Cache-Control", "no-store") ··· 57 45 <meta charset="utf-8" /> 58 46 <title>Logs</title> 59 47 <script> 60 - const api = new URL("/api/logs", location.origin); 48 + const api = new URL("/admin/api/logs", location.origin); 61 49 const qs = new URLSearchParams(location.search); 62 50 for (const k of ["hours"]) if (qs.has(k)) api.searchParams.set(k, qs.get(k)); 63 51 ··· 73 61 } 74 62 } 75 63 76 - // handleLogsAPI serves the logs as JSON 77 64 func (h *handler) handleLogsAPI(w http.ResponseWriter, r *http.Request) { 78 - 79 - if _, err := h.auth.GetUserIDFromRequest(r); errors.Is(err, auth.ErrNoSession) { 80 - w.WriteHeader(http.StatusNotFound) 81 - return 82 - } 83 - 84 - // Parse hours parameter 85 65 hoursStr := r.URL.Query().Get("hours") 86 - hours := 24 // default 66 + hours := 24 87 67 if hoursStr != "" { 88 - if h, err := strconv.Atoi(hoursStr); err == nil && h > 0 { 89 - hours = h 68 + if parsedHours, err := strconv.Atoi(hoursStr); err == nil && parsedHours > 0 { 69 + hours = parsedHours 90 70 } 91 71 } 92 72 93 - // Get logs 94 - // Return as JSON // kinda? 95 73 w.Header().Set("Access-Control-Allow-Origin", "*") 96 74 w.Header().Set("Content-Type", "application/json") 97 75 err := h.reader.GetLogs(r.Context(), hours, w) ··· 100 78 http.Error(w, fmt.Sprintf("Failed to retrieve logs: %v", err), http.StatusInternalServerError) 101 79 return 102 80 } 103 - 104 81 }