feat(ntfy): support HTTP Basic auth alongside access tokens
Self-hosted ntfy that doesn't expose a token-mint endpoint can still authenticate over HTTP Basic. Add Username + Password fields to NtfyConfig; the channel sends 'Authorization: Basic …' when token is empty and username is set. Token wins when both are configured. Form-side: two new optional fields next to the access token, with the same write-only placeholder treatment as smtp_password (blank on edit means 'keep stored value'). Username is round-tripped on edit; password is masked.
This commit is contained in:
@@ -3,6 +3,7 @@ package notification
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -11,11 +12,15 @@ import (
|
||||
)
|
||||
|
||||
// NtfyConfig is the per-channel JSON shape stored AEAD-encrypted in
|
||||
// notification_channels.config.
|
||||
// notification_channels.config. AccessToken takes precedence over
|
||||
// (Username, Password) when both are set; supply one or the other for
|
||||
// self-hosted ntfy that requires auth.
|
||||
type NtfyConfig struct {
|
||||
ServerURL string `json:"server_url"`
|
||||
Topic string `json:"topic"`
|
||||
AccessToken string `json:"access_token,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
}
|
||||
|
||||
// NtfyChannel delivers alerts to an ntfy server using POST with
|
||||
@@ -65,8 +70,12 @@ func (c *NtfyChannel) Send(ctx context.Context, p Payload) (int, time.Duration,
|
||||
if p.Link != "" {
|
||||
req.Header.Set("Click", p.Link)
|
||||
}
|
||||
if c.cfg.AccessToken != "" {
|
||||
switch {
|
||||
case c.cfg.AccessToken != "":
|
||||
req.Header.Set("Authorization", "Bearer "+c.cfg.AccessToken)
|
||||
case c.cfg.Username != "":
|
||||
creds := c.cfg.Username + ":" + c.cfg.Password
|
||||
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(creds)))
|
||||
}
|
||||
|
||||
t0 := time.Now()
|
||||
|
||||
@@ -79,6 +79,8 @@ type notificationForm struct {
|
||||
NtfyServerURL string
|
||||
NtfyTopic string
|
||||
NtfyAccessToken string
|
||||
NtfyUsername string
|
||||
NtfyPassword string
|
||||
|
||||
// SMTP sub-fields.
|
||||
SMTPHost string
|
||||
@@ -188,6 +190,8 @@ func formFromRequest(r *stdhttp.Request) *notificationForm {
|
||||
NtfyServerURL: strings.TrimSpace(r.PostForm.Get("ntfy_server_url")),
|
||||
NtfyTopic: strings.TrimSpace(r.PostForm.Get("ntfy_topic")),
|
||||
NtfyAccessToken: r.PostForm.Get("ntfy_access_token"),
|
||||
NtfyUsername: strings.TrimSpace(r.PostForm.Get("ntfy_username")),
|
||||
NtfyPassword: r.PostForm.Get("ntfy_password"),
|
||||
|
||||
SMTPHost: strings.TrimSpace(r.PostForm.Get("smtp_host")),
|
||||
SMTPPort: strings.TrimSpace(r.PostForm.Get("smtp_port")),
|
||||
@@ -288,12 +292,20 @@ func buildConfig(f *notificationForm, existing any) (any, error) {
|
||||
ServerURL: f.NtfyServerURL,
|
||||
Topic: f.NtfyTopic,
|
||||
AccessToken: f.NtfyAccessToken,
|
||||
Username: f.NtfyUsername,
|
||||
Password: f.NtfyPassword,
|
||||
}
|
||||
if existing != nil {
|
||||
ex, ok := existing.(*notification.NtfyConfig)
|
||||
if ok && cfg.AccessToken == "" {
|
||||
if ex, ok := existing.(*notification.NtfyConfig); ok {
|
||||
// Blank password on edit means "keep stored value"
|
||||
// — same write-only treatment as smtp_password.
|
||||
if cfg.AccessToken == "" {
|
||||
cfg.AccessToken = ex.AccessToken
|
||||
}
|
||||
if cfg.Password == "" {
|
||||
cfg.Password = ex.Password
|
||||
}
|
||||
}
|
||||
}
|
||||
return cfg, nil
|
||||
|
||||
@@ -479,7 +491,8 @@ func (s *Server) handleUINotificationEditGet(w stdhttp.ResponseWriter, r *stdhtt
|
||||
if err := s.decryptChannelConfig(*ch, &cfg); err == nil {
|
||||
f.NtfyServerURL = cfg.ServerURL
|
||||
f.NtfyTopic = cfg.Topic
|
||||
// AccessToken is write-only.
|
||||
f.NtfyUsername = cfg.Username
|
||||
// AccessToken and Password are write-only.
|
||||
}
|
||||
case "smtp":
|
||||
var cfg notification.SMTPConfig
|
||||
|
||||
@@ -271,7 +271,20 @@
|
||||
<label class="field-label" for="nt-token">Access token <span class="text-ink-fade font-normal">· optional</span></label>
|
||||
<input id="nt-token" name="ntfy_access_token" type="password" class="field mono"
|
||||
placeholder="{{if and $isEdit (eq $f.Kind "ntfy")}}tk_… · stored, leave blank to keep{{else}}tk_… · required for protected topics{{end}}" />
|
||||
<div class="field-help">Required for protected topics on self-hosted ntfy.</div>
|
||||
<div class="field-help">Use this OR username+password below. Token wins when both are set.</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3.5">
|
||||
<div>
|
||||
<label class="field-label" for="nt-user">Username <span class="text-ink-fade font-normal">· optional</span></label>
|
||||
<input id="nt-user" name="ntfy_username" type="text" class="field mono"
|
||||
value="{{$f.NtfyUsername}}" placeholder="ntfy basic-auth user" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="field-label" for="nt-pass">Password <span class="text-ink-fade font-normal">· optional</span></label>
|
||||
<input id="nt-pass" name="ntfy_password" type="password" class="field mono"
|
||||
placeholder="{{if and $isEdit (eq $f.Kind "ntfy")}}stored, leave blank to keep{{else}}ntfy basic-auth password{{end}}" />
|
||||
<div class="field-help">Sent as HTTP Basic auth when no token is set.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="field-label" for="nt-priority">Default priority</label>
|
||||
|
||||
Reference in New Issue
Block a user