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.
This commit is contained in:
prosolis
2026-06-20 14:56:24 -07:00
parent 4fb1a4553e
commit 72f4bdd88a
11 changed files with 166 additions and 118 deletions

View File

@@ -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)
}

View File

@@ -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",