From 72f4bdd88a3bab50f9ae810824545d58df277ad8 Mon Sep 17 00:00:00 2001 From: prosolis <5590409+prosolis@users.noreply.github.com> Date: Sat, 20 Jun 2026 14:56:24 -0700 Subject: [PATCH] Add ntfy Basic auth, sign-out button, config-only credentials - ntfy client supports HTTP Basic auth (username/password) for servers with access control; [ntfy] config gains username/password. Scheduler + Test Ntfy use it instead of the old settings-only bearer token. - Settings page no longer has credential inputs (Apify/eBay/ntfy/Resend); they are managed in config.toml. Read-only status + Test buttons remain. - Sidebar gains a Sign out button; in forward-auth mode it routes through the Authentik outpost sign_out so the IdP session is cleared. --- config.toml.example | 6 +++ internal/config/config.go | 5 +++ internal/handlers/auth.go | 8 ++++ internal/handlers/settings.go | 69 +++++++++++++------------------- internal/ntfy/client.go | 38 +++++++++++++----- internal/scheduler/scheduler.go | 3 +- static/css/app.css | 17 ++++++++ templates/layout.templ | 10 +++-- templates/layout_templ.go | 46 +++++++++++++--------- templates/settings.templ | 12 +++--- templates/settings_templ.go | 70 ++++++++++++++++----------------- 11 files changed, 166 insertions(+), 118 deletions(-) diff --git a/config.toml.example b/config.toml.example index 803d70b..e7c7873 100644 --- a/config.toml.example +++ b/config.toml.example @@ -70,9 +70,15 @@ client_secret = "" environment = "production" daily_call_limit = 5000 +# ntfy push notifications. If the server has access control +# (auth-default-access: deny-all), set username/password to an ntfy user that +# has write access to the topics you publish to; Veola authenticates publishes +# with HTTP Basic auth. Leave both blank for an open server. [ntfy] base_url = "https://ntfy.yourdomain.com" default_topic = "veola" +username = "" +password = "" # Resend transactional email (https://resend.com) for opt-in deal alerts and # the weekly digest. api_key and from can also be set via /settings. The from diff --git a/internal/config/config.go b/internal/config/config.go index 2589c11..d6c55b2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -126,6 +126,11 @@ type ActorConfig struct { type NtfyConfig struct { BaseURL string `toml:"base_url"` DefaultTopic string `toml:"default_topic"` + // Username/Password authenticate publishes via HTTP Basic auth, for ntfy + // servers with access control (auth-default-access: deny-all). Leave both + // empty for an open server. + Username string `toml:"username"` + Password string `toml:"password"` } type SchedulerConfig struct { diff --git a/internal/handlers/auth.go b/internal/handlers/auth.go index 57004a7..8c92c37 100644 --- a/internal/handlers/auth.go +++ b/internal/handlers/auth.go @@ -49,6 +49,14 @@ func (a *App) PostLogin(w http.ResponseWriter, r *http.Request) { func (a *App) PostLogout(w http.ResponseWriter, r *http.Request) { _ = a.Auth.LogOut(r.Context()) + // In forward-auth mode, destroying only the local session is pointless: the + // next request still carries a valid Authentik proxy session and would be + // re-provisioned immediately. Send the user through the Authentik outpost + // sign-out so the IdP session is cleared too. + if a.Cfg.Auth.ForwardAuthEnabled() { + http.Redirect(w, r, "/outpost.goauthentik.io/sign_out", http.StatusSeeOther) + return + } http.Redirect(w, r, "/login", http.StatusSeeOther) } diff --git a/internal/handlers/settings.go b/internal/handlers/settings.go index edbbde0..88601fd 100644 --- a/internal/handlers/settings.go +++ b/internal/handlers/settings.go @@ -16,52 +16,38 @@ import ( "veola/templates" ) +// settingsKeys are the non-secret operational settings editable from the +// Settings page. API credentials (Apify, eBay, ntfy, Resend) are deliberately +// NOT here — they are managed only in config.toml on the server, so the UI +// never reads or writes them. credentialStatus surfaces whether each is +// configured, and the Test buttons verify them. var settingsKeys = []string{ - "apify_api_key", - "ebay_client_id", - "ebay_client_secret", "ebay_daily_call_limit", "ntfy_base_url", "ntfy_default_topic", - "ntfy_token", "global_poll_interval_minutes", "match_confidence_threshold", "apify_cost_per_call", "monthly_budget_usd", - "resend_api_key", "resend_from", } -// secretSettingsKeys are credential fields. Their values are never rendered -// back into the form, so a blank submission means "leave unchanged" rather -// than "clear" — see PostSettings. -var secretSettingsKeys = map[string]bool{ - "apify_api_key": true, - "ebay_client_id": true, - "ebay_client_secret": true, - "ntfy_token": true, - "resend_api_key": true, -} - -// credentialStatus reports, per secret key, whether a value is saved in the -// settings table, inherited from config.toml, or absent — without exposing -// the secret itself. -func (a *App) credentialStatus(values map[string]string) map[string]string { - configVals := map[string]string{ - "apify_api_key": a.Cfg.Apify.APIKey, - "ebay_client_id": a.Cfg.Ebay.ClientID, - "ebay_client_secret": a.Cfg.Ebay.ClientSecret, - "ntfy_token": "", - "resend_api_key": a.Cfg.Resend.APIKey, +// credentialStatus reports, per credential, whether it is configured in +// config.toml — without exposing the secret itself. ntfy_auth reflects the +// Basic-auth user/password pair. +func (a *App) credentialStatus() map[string]string { + configured := map[string]bool{ + "apify_api_key": strings.TrimSpace(a.Cfg.Apify.APIKey) != "", + "ebay_client_id": strings.TrimSpace(a.Cfg.Ebay.ClientID) != "", + "ebay_client_secret": strings.TrimSpace(a.Cfg.Ebay.ClientSecret) != "", + "ntfy_auth": strings.TrimSpace(a.Cfg.Ntfy.Username) != "" && a.Cfg.Ntfy.Password != "", + "resend_api_key": strings.TrimSpace(a.Cfg.Resend.APIKey) != "", } - status := make(map[string]string, len(secretSettingsKeys)) - for k := range secretSettingsKeys { - switch { - case strings.TrimSpace(values[k]) != "": - status[k] = "Saved in settings" - case strings.TrimSpace(configVals[k]) != "": + status := make(map[string]string, len(configured)) + for k, ok := range configured { + if ok { status[k] = "Set in config.toml" - default: + } else { status[k] = "Not set" } } @@ -83,7 +69,7 @@ func (a *App) settingsData(r *http.Request) (templates.SettingsData, error) { return templates.SettingsData{ Page: a.page(r, "Settings", "settings"), Values: values, - CredentialStatus: a.credentialStatus(values), + CredentialStatus: a.credentialStatus(), IsAdmin: cur != nil && cur.Role == models.RoleAdmin, Users: users, EbayUsedToday: ebayUsed, @@ -212,12 +198,6 @@ func (a *App) PostSettings(w http.ResponseWriter, r *http.Request) { } for _, k := range settingsKeys { v := strings.TrimSpace(r.PostFormValue(k)) - // Secret fields are never rendered back into the form, so a blank - // submission is the normal state and means "leave unchanged" — not - // "clear". (To clear a stored credential, edit the settings table.) - if v == "" && secretSettingsKeys[k] { - continue - } if err := a.Store.SetSetting(r.Context(), k, v); err != nil { a.serverError(w, r, err) return @@ -284,14 +264,19 @@ func (a *App) PostTestNtfy(w http.ResponseWriter, r *http.Request) { return } baseURL := strings.TrimSpace(d.Values["ntfy_base_url"]) + if baseURL == "" { + baseURL = a.Cfg.Ntfy.BaseURL + } topic := strings.TrimSpace(d.Values["ntfy_default_topic"]) - token := strings.TrimSpace(d.Values["ntfy_token"]) + if topic == "" { + topic = a.Cfg.Ntfy.DefaultTopic + } 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) + client := ntfy.NewWithBasicAuth(baseURL, a.Cfg.Ntfy.Username, a.Cfg.Ntfy.Password) if err := client.Send(r.Context(), ntfy.Notification{ Topic: topic, Title: "Veola test", diff --git a/internal/ntfy/client.go b/internal/ntfy/client.go index 3168351..5e5c7be 100644 --- a/internal/ntfy/client.go +++ b/internal/ntfy/client.go @@ -13,7 +13,12 @@ import ( type Client struct { BaseURL string Token string - HTTP *http.Client + // Username/Password take precedence over Token when set, using HTTP Basic + // auth (for ntfy servers that authenticate with user accounts rather than + // access tokens). + Username string + Password string + HTTP *http.Client } func New(baseURL string) *Client { @@ -24,13 +29,23 @@ func New(baseURL string) *Client { } // NewWithToken returns a ntfy Client with bearer-token auth set. Use this -// when the ntfy server requires authentication. +// when the ntfy server requires token authentication. func NewWithToken(baseURL, token string) *Client { c := New(baseURL) c.Token = strings.TrimSpace(token) return c } +// NewWithBasicAuth returns a ntfy Client that authenticates publishes with an +// ntfy user account (HTTP Basic auth). Use this for servers configured with +// auth-default-access: deny-all and per-user topic ACLs. +func NewWithBasicAuth(baseURL, username, password string) *Client { + c := New(baseURL) + c.Username = strings.TrimSpace(username) + c.Password = password + return c +} + type Notification struct { Topic string Title string @@ -58,7 +73,9 @@ func (c *Client) Send(ctx context.Context, n Notification) error { return err } req.Header.Set("Content-Type", "text/plain; charset=utf-8") - if c.Token != "" { + if c.Username != "" { + req.SetBasicAuth(c.Username, c.Password) + } else if c.Token != "" { req.Header.Set("Authorization", "Bearer "+c.Token) } if n.Title != "" { @@ -73,17 +90,18 @@ func (c *Client) Send(ctx context.Context, n Notification) error { if n.Click != "" { req.Header.Set("Click", n.Click) } - tokenLen := len(c.Token) - tokenPrefix := "" - if tokenLen >= 4 { - tokenPrefix = c.Token[:4] + authMode := "none" + switch { + case c.Username != "": + authMode = "basic" + case c.Token != "": + authMode = "bearer" } slog.Info("ntfy publish", "url", url, "topic", n.Topic, - "auth_header_set", c.Token != "", - "token_prefix", tokenPrefix, - "token_len", tokenLen, + "auth", authMode, + "user", c.Username, ) resp, err := c.HTTP.Do(req) if err != nil { diff --git a/internal/scheduler/scheduler.go b/internal/scheduler/scheduler.go index e4109b3..105e8b5 100644 --- a/internal/scheduler/scheduler.go +++ b/internal/scheduler/scheduler.go @@ -489,8 +489,7 @@ func (s *Scheduler) sendNotification(ctx context.Context, it models.Item, r apif if v, _ := s.store.GetSetting(ctx, "ntfy_base_url"); v != "" { baseURL = v } - token, _ := s.store.GetSetting(ctx, "ntfy_token") - client := ntfy.NewWithToken(baseURL, token) + client := ntfy.NewWithBasicAuth(baseURL, s.cfg.Ntfy.Username, s.cfg.Ntfy.Password) return client.Send(ctx, ntfy.Notification{ Topic: topic, Title: fmt.Sprintf("Veola Alert: %s", it.Name), diff --git a/static/css/app.css b/static/css/app.css index c6d9134..8c76e1e 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -304,6 +304,23 @@ a.v-feed-name:hover { color: var(--accent); text-decoration: none; } } .v-side-nav a:hover { color: white; } +/* Sign-out button, styled to read like a nav link (it lives in a
). */ +.v-side-signout { + display: flex; + align-items: center; + gap: 0.6rem; + width: 100%; + padding: 0.7rem 1rem; + color: var(--text-2); + background: transparent; + border: 0; + border-left: 3px solid transparent; + text-align: left; + cursor: pointer; + font: inherit; +} +.v-side-signout:hover { color: white; } + /* The brand wordmark at the top of the sidebar is also an anchor (→ /), but shouldn't pick up the active-item border-left / padding treatment that the nav links get. Higher specificity overrides .v-side-nav a defaults. */ diff --git a/templates/layout.templ b/templates/layout.templ index df554eb..bd5a363 100644 --- a/templates/layout.templ +++ b/templates/layout.templ @@ -31,7 +31,7 @@ templ head(title string) { } -templ Sidebar(active string) { +templ Sidebar(active, csrf string) {
@@ -59,7 +63,7 @@ templ Layout(p Page, body templ.Component) { @head(p.Title) - @Sidebar(p.Active) + @Sidebar(p.Active, p.CSRFToken)
if p.Flash != "" {
{ p.Flash }
diff --git a/templates/layout_templ.go b/templates/layout_templ.go index 898afa5..aaf292c 100644 --- a/templates/layout_templ.go +++ b/templates/layout_templ.go @@ -63,7 +63,7 @@ func head(title string) templ.Component { }) } -func Sidebar(active string) templ.Component { +func Sidebar(active, csrf string) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -172,7 +172,15 @@ func Sidebar(active string) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\">Settings
Track. Watch. Notice.
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\">Settings
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = CSRFInput(csrf).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
Track. Watch. Notice.
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -208,7 +216,7 @@ func Layout(p Page, body templ.Component) templ.Component { templ_7745c5c3_Var12 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -216,52 +224,52 @@ func Layout(p Page, body templ.Component) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = Sidebar(p.Active).Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = Sidebar(p.Active, p.CSRFToken).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if p.Flash != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var13 string templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(p.Flash) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/layout.templ`, Line: 65, Col: 35} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/layout.templ`, Line: 69, Col: 35} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if p.FlashError != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var14 string templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(p.FlashError) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/layout.templ`, Line: 68, Col: 46} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/layout.templ`, Line: 72, Col: 46} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -270,7 +278,7 @@ func Layout(p Page, body templ.Component) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -300,7 +308,7 @@ func Bare(p Page, body templ.Component) templ.Component { templ_7745c5c3_Var15 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -308,7 +316,7 @@ func Bare(p Page, body templ.Component) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -316,7 +324,7 @@ func Bare(p Page, body templ.Component) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -346,20 +354,20 @@ func CSRFInput(token string) templ.Component { templ_7745c5c3_Var16 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/templates/settings.templ b/templates/settings.templ index f09da84..6ecd213 100644 --- a/templates/settings.templ +++ b/templates/settings.templ @@ -67,21 +67,21 @@ templ settingsBody(d SettingsData) {

Apify, eBay and Ntfy

+
+ API credentials (Apify, eBay, ntfy, Resend) are managed in config.toml on the server, not here. Use the Test buttons to verify them. +
@CSRFInput(d.CSRFToken)
- @credStatus(d, "apify_api_key")
- @credStatus(d, "ebay_client_id")
- @credStatus(d, "ebay_client_secret")
@@ -108,9 +108,8 @@ templ settingsBody(d SettingsData) {
- - - @credStatus(d, "ntfy_token") + + @credStatus(d, "ntfy_auth")
@@ -122,7 +121,6 @@ templ settingsBody(d SettingsData) {
- @credStatus(d, "resend_api_key")
diff --git a/templates/settings_templ.go b/templates/settings_templ.go index 6b09d05..9daf32a 100644 --- a/templates/settings_templ.go +++ b/templates/settings_templ.go @@ -123,7 +123,7 @@ func settingsBody(d SettingsData) templ.Component { templ_7745c5c3_Var3 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "

Settings

Credentials, budget, notifications, and users

Apify, eBay and Ntfy

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "

Settings

Credentials, budget, notifications, and users

Apify, eBay and Ntfy

API credentials (Apify, eBay, ntfy, Resend) are managed in config.toml on the server, not here. Use the Test buttons to verify them.
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -131,7 +131,7 @@ func settingsBody(d SettingsData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -139,7 +139,7 @@ func settingsBody(d SettingsData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -147,7 +147,7 @@ func settingsBody(d SettingsData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -241,11 +241,11 @@ func settingsBody(d SettingsData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\">
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = credStatus(d, "ntfy_token").Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = credStatus(d, "ntfy_auth").Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -256,7 +256,7 @@ func settingsBody(d SettingsData) templ.Component { var templ_7745c5c3_Var9 string templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.Values["global_poll_interval_minutes"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 117, Col: 136} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 116, Col: 136} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9) if templ_7745c5c3_Err != nil { @@ -269,13 +269,13 @@ func settingsBody(d SettingsData) templ.Component { var templ_7745c5c3_Var10 string templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.Values["match_confidence_threshold"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 121, Col: 160} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 120, Col: 160} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\">
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -290,7 +290,7 @@ func settingsBody(d SettingsData) templ.Component { var templ_7745c5c3_Var11 string templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.Values["resend_from"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 130, Col: 78} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 128, Col: 78} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11) if templ_7745c5c3_Err != nil { @@ -303,7 +303,7 @@ func settingsBody(d SettingsData) templ.Component { var templ_7745c5c3_Var12 string templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.Values["apify_cost_per_call"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 135, Col: 139} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 133, Col: 139} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12) if templ_7745c5c3_Err != nil { @@ -316,7 +316,7 @@ func settingsBody(d SettingsData) templ.Component { var templ_7745c5c3_Var13 string templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.Values["monthly_budget_usd"]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 140, Col: 133} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 138, Col: 133} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13) if templ_7745c5c3_Err != nil { @@ -349,7 +349,7 @@ func settingsBody(d SettingsData) templ.Component { var templ_7745c5c3_Var14 string templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(d.TestNtfyOK) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 156, Col: 44} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 154, Col: 44} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) if templ_7745c5c3_Err != nil { @@ -368,7 +368,7 @@ func settingsBody(d SettingsData) templ.Component { var templ_7745c5c3_Var15 string templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(d.TestApifyOK) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 159, Col: 45} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 157, Col: 45} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) if templ_7745c5c3_Err != nil { @@ -387,7 +387,7 @@ func settingsBody(d SettingsData) templ.Component { var templ_7745c5c3_Var16 string templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(d.TestEbayOK) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 162, Col: 44} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 160, Col: 44} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) if templ_7745c5c3_Err != nil { @@ -406,7 +406,7 @@ func settingsBody(d SettingsData) templ.Component { var templ_7745c5c3_Var17 string templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(d.TestResendOK) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 165, Col: 46} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 163, Col: 46} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) if templ_7745c5c3_Err != nil { @@ -424,7 +424,7 @@ func settingsBody(d SettingsData) templ.Component { var templ_7745c5c3_Var18 string templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", d.ApifyMonth)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 169, Col: 61} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 167, Col: 61} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) if templ_7745c5c3_Err != nil { @@ -442,7 +442,7 @@ func settingsBody(d SettingsData) templ.Component { var templ_7745c5c3_Var19 string templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("(~$%.2f est.)", d.ApifyMonthCost())) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 171, Col: 82} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 169, Col: 82} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) if templ_7745c5c3_Err != nil { @@ -460,7 +460,7 @@ func settingsBody(d SettingsData) templ.Component { var templ_7745c5c3_Var20 string templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", d.ApifyToday)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 174, Col: 61} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 172, Col: 61} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) if templ_7745c5c3_Err != nil { @@ -478,7 +478,7 @@ func settingsBody(d SettingsData) templ.Component { var templ_7745c5c3_Var21 string templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(d.PasswordError) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 181, Col: 48} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 179, Col: 48} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) if templ_7745c5c3_Err != nil { @@ -497,7 +497,7 @@ func settingsBody(d SettingsData) templ.Component { var templ_7745c5c3_Var22 string templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(d.PasswordMsg) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 184, Col: 40} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 182, Col: 40} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) if templ_7745c5c3_Err != nil { @@ -528,7 +528,7 @@ func settingsBody(d SettingsData) templ.Component { var templ_7745c5c3_Var23 string templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(d.EmailError) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 207, Col: 45} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 205, Col: 45} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) if templ_7745c5c3_Err != nil { @@ -547,7 +547,7 @@ func settingsBody(d SettingsData) templ.Component { var templ_7745c5c3_Var24 string templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(d.EmailMsg) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 210, Col: 37} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 208, Col: 37} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) if templ_7745c5c3_Err != nil { @@ -578,7 +578,7 @@ func settingsBody(d SettingsData) templ.Component { var templ_7745c5c3_Var25 string templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.ResolveAttributeValue(currentEmail(d)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 217, Col: 65} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 215, Col: 65} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var25) if templ_7745c5c3_Err != nil { @@ -596,7 +596,7 @@ func settingsBody(d SettingsData) templ.Component { var templ_7745c5c3_Var26 string templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.ResolveAttributeValue(currentEmail(d)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 220, Col: 78} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 218, Col: 78} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var26) if templ_7745c5c3_Err != nil { @@ -644,7 +644,7 @@ func settingsBody(d SettingsData) templ.Component { var templ_7745c5c3_Var27 string templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(d.UserError) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 240, Col: 45} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 238, Col: 45} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) if templ_7745c5c3_Err != nil { @@ -663,7 +663,7 @@ func settingsBody(d SettingsData) templ.Component { var templ_7745c5c3_Var28 string templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(d.UserMsg) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 243, Col: 37} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 241, Col: 37} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) if templ_7745c5c3_Err != nil { @@ -686,7 +686,7 @@ func settingsBody(d SettingsData) templ.Component { var templ_7745c5c3_Var29 string templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(u.Username) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 250, Col: 24} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 248, Col: 24} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) if templ_7745c5c3_Err != nil { @@ -699,7 +699,7 @@ func settingsBody(d SettingsData) templ.Component { var templ_7745c5c3_Var30 string templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(string(u.Role)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 251, Col: 44} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 249, Col: 44} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) if templ_7745c5c3_Err != nil { @@ -712,7 +712,7 @@ func settingsBody(d SettingsData) templ.Component { var templ_7745c5c3_Var31 string templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(u.CreatedAt.Format("2006-01-02")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 252, Col: 70} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 250, Col: 70} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) if templ_7745c5c3_Err != nil { @@ -725,7 +725,7 @@ func settingsBody(d SettingsData) templ.Component { var templ_7745c5c3_Var32 templ.SafeURL templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/users/%d/reset-password", u.ID))) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 254, Col: 113} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 252, Col: 113} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) if templ_7745c5c3_Err != nil { @@ -738,7 +738,7 @@ func settingsBody(d SettingsData) templ.Component { var templ_7745c5c3_Var33 string templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.CSRFToken) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 255, Col: 68} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 253, Col: 68} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var33) if templ_7745c5c3_Err != nil { @@ -751,7 +751,7 @@ func settingsBody(d SettingsData) templ.Component { var templ_7745c5c3_Var34 templ.SafeURL templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/users/%d/delete", u.ID))) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 259, Col: 105} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 257, Col: 105} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) if templ_7745c5c3_Err != nil { @@ -764,7 +764,7 @@ func settingsBody(d SettingsData) templ.Component { var templ_7745c5c3_Var35 string templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.CSRFToken) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 260, Col: 68} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 258, Col: 68} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var35) if templ_7745c5c3_Err != nil { @@ -782,7 +782,7 @@ func settingsBody(d SettingsData) templ.Component { var templ_7745c5c3_Var36 string templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.CSRFToken) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 269, Col: 63} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 267, Col: 63} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var36) if templ_7745c5c3_Err != nil {