Fix ntfy bearer token usage

This commit is contained in:
Marcus Kida 2026-08-04 14:51:34 +02:00
parent a7ab445281
commit 0c2cc6b081
4 changed files with 9 additions and 20 deletions

View file

@ -37,7 +37,7 @@ A web app that visualizes DMR repeaters on an interactive map. Filter by band an
| [BrandMeister WebSocket](https://wiki.brandmeister.network/index.php/API/Last_Heard) | Real-time Last Heard feed for heatmap via socket.io | None |
| [Nominatim](https://nominatim.openstreetmap.org) | Address geocoding and autocomplete (client-side) | None |
| [OSRM](https://project-osrm.org) | Driving route calculation (client-side) | None |
| [ntfy](https://ntfy.sh) | Push notification when a user report is submitted (optional) | Access token (bearer or basic auth) |
| [ntfy](https://ntfy.sh) | Push notification when a user report is submitted (optional) | Access token (bearer) |
The BrandMeister talk group registry is fetched once at Docker build time and bundled as `frontend/static/talkgroups.json`. It is used for talk group name resolution and autocomplete in CPS Studio.
@ -79,8 +79,7 @@ docker compose -f compose.test.yml run --rm test
| `ADMIN_TOKEN` | *(disabled)* | Set to enable the admin interface at `/admin/` |
| `NTFY_URL` | *(disabled)* | ntfy server base URL (e.g. `https://ntfy.example.com`). Together with `NTFY_TOPIC` it enables push notifications for new user reports |
| `NTFY_TOPIC` | *(disabled)* | ntfy topic that report notifications are published to |
| `NTFY_USER` | *(none)* | ntfy username. When set, the token is sent as HTTP basic auth (`user:token`) |
| `NTFY_TOKEN` | *(none)* | ntfy access token. Sent as `Authorization: Bearer` when no username is set |
| `NTFY_TOKEN` | *(none)* | ntfy access token, sent as `Authorization: Bearer`. Required for protected topics |
| `PUBLIC_BASE_URL` | *(none)* | Public base URL of the site (e.g. `https://dmrmap.de`). Used to add a click-through link to the admin view in ntfy notifications |
Both `NTFY_URL` and `NTFY_TOPIC` must be set for notifications; otherwise reports are stored but nothing is pushed. On startup the app logs either `ntfy notifications enabled: <topic URL> (auth: ...)` or `ntfy notifications disabled`. The **Test push** button in the admin Reports view publishes a test notification and shows the upstream error if it fails.

View file

@ -1,7 +1,6 @@
package main
import (
"encoding/base64"
"fmt"
"log"
"net/http"
@ -14,14 +13,13 @@ import (
// required environment variables are not set, in which case notify is a no-op.
type ntfyNotifier struct {
url string // full topic URL, e.g. https://ntfy.example.com/dmrmap
user string
token string
clickBase string
httpClient *http.Client
}
// newNtfyNotifierFromEnv builds a notifier from NTFY_URL / NTFY_TOPIC /
// NTFY_USER / NTFY_TOKEN. Returns nil when notifications are not configured.
// NTFY_TOKEN. Returns nil when notifications are not configured.
func newNtfyNotifierFromEnv() *ntfyNotifier {
base := strings.TrimSpace(os.Getenv("NTFY_URL"))
topic := strings.TrimSpace(os.Getenv("NTFY_TOPIC"))
@ -32,17 +30,14 @@ func newNtfyNotifierFromEnv() *ntfyNotifier {
n := &ntfyNotifier{
url: strings.TrimRight(base, "/") + "/" + strings.TrimLeft(topic, "/"),
user: strings.TrimSpace(os.Getenv("NTFY_USER")),
token: strings.TrimSpace(os.Getenv("NTFY_TOKEN")),
clickBase: strings.TrimRight(strings.TrimSpace(os.Getenv("PUBLIC_BASE_URL")), "/"),
httpClient: &http.Client{Timeout: 10 * time.Second},
}
auth := "none"
if n.user != "" && n.token != "" {
auth = "basic (" + n.user + ")"
} else if n.token != "" {
auth = "bearer token"
if n.token != "" {
auth = "access token"
}
log.Printf("ntfy notifications enabled: %s (auth: %s)", n.url, auth)
@ -110,14 +105,11 @@ func (n *ntfyNotifier) send(rep UserReport) error {
return nil
}
// setAuth uses basic auth when a username is configured (ntfy accepts an
// access token as the password), and bearer auth for a token on its own.
// setAuth applies the ntfy access token. It is a bearer credential: ntfy does
// NOT accept it as the password half of a username:password pair (that yields
// HTTP 401), only as a bearer token or as basic auth with an empty username.
func (n *ntfyNotifier) setAuth(req *http.Request) {
switch {
case n.user != "" && n.token != "":
creds := base64.StdEncoding.EncodeToString([]byte(n.user + ":" + n.token))
req.Header.Set("Authorization", "Basic "+creds)
case n.token != "":
if n.token != "" {
req.Header.Set("Authorization", "Bearer "+n.token)
}
}

View file

@ -33,7 +33,6 @@ services:
- MIGRATIONS_DIR=/app/backend/migrations
- NTFY_URL=${NTFY_URL:-}
- NTFY_TOPIC=${NTFY_TOPIC:-}
- NTFY_USER=${NTFY_USER:-}
- NTFY_TOKEN=${NTFY_TOKEN:-}
- PUBLIC_BASE_URL=${PUBLIC_BASE_URL:-http://localhost:8080}

View file

@ -22,7 +22,6 @@ services:
- ADMIN_TOKEN=${ADMIN_TOKEN:-}
- NTFY_URL=${NTFY_URL:-}
- NTFY_TOPIC=${NTFY_TOPIC:-}
- NTFY_USER=${NTFY_USER:-}
- NTFY_TOKEN=${NTFY_TOKEN:-}
- PUBLIC_BASE_URL=${PUBLIC_BASE_URL:-}
restart: unless-stopped