package handlers import ( "fmt" "net/http" "strings" "veola/internal/apify" "veola/internal/auth" "veola/internal/models" "veola/internal/ntfy" "veola/templates" ) var settingsKeys = []string{ "apify_api_key", "ntfy_base_url", "ntfy_default_topic", "ntfy_token", "global_poll_interval_minutes", "match_confidence_threshold", } func (a *App) settingsData(r *http.Request) (templates.SettingsData, error) { values, err := a.Store.GetAllSettings(r.Context()) if err != nil { return templates.SettingsData{}, err } if values == nil { values = map[string]string{} } users, _ := a.Store.ListUsers(r.Context()) cur := auth.CurrentUserFromRequest(r) return templates.SettingsData{ Page: a.page(r, "Settings", "settings"), Values: values, IsAdmin: cur != nil && cur.Role == models.RoleAdmin, Users: users, }, nil } func (a *App) GetSettings(w http.ResponseWriter, r *http.Request) { d, err := a.settingsData(r) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } render(w, r, templates.Settings(d)) } func (a *App) PostSettings(w http.ResponseWriter, r *http.Request) { cur := auth.CurrentUserFromRequest(r) if cur == nil || cur.Role != models.RoleAdmin { http.Error(w, "forbidden", http.StatusForbidden) return } if err := r.ParseForm(); err != nil { http.Error(w, "bad form", http.StatusBadRequest) return } for _, k := range settingsKeys { v := strings.TrimSpace(r.PostFormValue(k)) if err := a.Store.SetSetting(r.Context(), k, v); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } http.Redirect(w, r, "/settings", http.StatusSeeOther) } func (a *App) PostPasswordChange(w http.ResponseWriter, r *http.Request) { cur := auth.CurrentUserFromRequest(r) if cur == nil { http.Redirect(w, r, "/login", http.StatusSeeOther) return } if err := r.ParseForm(); err != nil { http.Error(w, "bad form", http.StatusBadRequest) return } current := r.PostFormValue("current_password") next := r.PostFormValue("new_password") confirm := r.PostFormValue("new_password_confirm") d, err := a.settingsData(r) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } switch { case !auth.CheckPassword(cur.PasswordHash, current): d.PasswordError = "Current password is incorrect" case len(next) < auth.MinPasswordLen: d.PasswordError = fmt.Sprintf("New password must be at least %d characters", auth.MinPasswordLen) case next != confirm: d.PasswordError = "New passwords do not match" } if d.PasswordError != "" { render(w, r, templates.Settings(d)) return } hash, err := auth.HashPassword(next) if err != nil { http.Error(w, "hash error", http.StatusInternalServerError) return } if err := a.Store.UpdateUserPassword(r.Context(), cur.ID, hash); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } d.PasswordMsg = "Password updated" render(w, r, templates.Settings(d)) } func (a *App) PostTestNtfy(w http.ResponseWriter, r *http.Request) { cur := auth.CurrentUserFromRequest(r) if cur == nil || cur.Role != models.RoleAdmin { http.Error(w, "forbidden", http.StatusForbidden) return } d, err := a.settingsData(r) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } baseURL := strings.TrimSpace(d.Values["ntfy_base_url"]) topic := strings.TrimSpace(d.Values["ntfy_default_topic"]) token := strings.TrimSpace(d.Values["ntfy_token"]) if baseURL == "" || topic == "" { d.TestNtfyOK = "Set ntfy base URL and default topic first." render(w, r, templates.Settings(d)) return } client := ntfy.NewWithToken(baseURL, token) if err := client.Send(r.Context(), ntfy.Notification{ Topic: topic, Title: "Veola test", Message: "Test notification from Veola settings.", Priority: "default", Tags: []string{"white_check_mark"}, }); err != nil { d.TestNtfyOK = "Ntfy test failed: " + err.Error() } else { d.TestNtfyOK = fmt.Sprintf("Sent test notification to %s/%s", baseURL, topic) } render(w, r, templates.Settings(d)) } func (a *App) PostTestApify(w http.ResponseWriter, r *http.Request) { cur := auth.CurrentUserFromRequest(r) if cur == nil || cur.Role != models.RoleAdmin { http.Error(w, "forbidden", http.StatusForbidden) return } d, err := a.settingsData(r) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } apiKey := strings.TrimSpace(d.Values["apify_api_key"]) actorID := a.Cfg.Apify.Actors.ActiveListings if apiKey == "" { apiKey = a.Cfg.Apify.APIKey } if apiKey == "" || actorID == "" { d.TestApifyOK = "Apify API key or active_listings actor is not configured." render(w, r, templates.Settings(d)) return } client := apify.New(apiKey) var proxy *apify.ProxyConfiguration p := a.Cfg.Apify.Proxy if p.UseApifyProxy { proxy = &apify.ProxyConfiguration{ UseApifyProxy: true, ApifyProxyGroups: p.Groups, ApifyProxyCountry: p.Country, } } raw, err := client.Run(r.Context(), actorID, apify.ActiveListingInput{ SearchQueries: []string{"test"}, MaxProductsPerSearch: 1, MaxSearchPages: 1, ListingType: "all", ProxyConfiguration: proxy, }) if err != nil { d.TestApifyOK = "Apify test failed: " + err.Error() } else { d.TestApifyOK = fmt.Sprintf("Apify returned %d item(s).", len(raw)) } render(w, r, templates.Settings(d)) }