Răsfoiți Sursa

Version API endpoint

binwiederhier 6 zile în urmă
părinte
comite
cfdc364e3f
5 a modificat fișierele cu 54 adăugiri și 0 ștergeri
  1. 1 0
      docs/releases.md
  2. 3 0
      server/server.go
  3. 8 0
      server/server_admin.go
  4. 36 0
      server/server_admin_test.go
  5. 6 0
      server/types.go

+ 1 - 0
docs/releases.md

@@ -1686,6 +1686,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
 **Features:**
 
 * Server: Support templating in the priority field ([#1426](https://github.com/binwiederhier/ntfy/issues/1426), thanks to [@seantomburke](https://github.com/seantomburke) for reporting)
+* Server: Add admin-only `GET /v1/version` endpoint returning server version, build commit, and date ([#1599](https://github.com/binwiederhier/ntfy/issues/1599), thanks to [@crivchri](https://github.com/crivchri) for reporting)
 * Web: Show red notification dot on favicon when there are unread messages ([#1017](https://github.com/binwiederhier/ntfy/issues/1017), thanks to [@ad-si](https://github.com/ad-si) for reporting)
 
 **Bug fixes + maintenance:**

+ 3 - 0
server/server.go

@@ -90,6 +90,7 @@ var (
 	matrixPushPath                                       = "/_matrix/push/v1/notify"
 	metricsPath                                          = "/metrics"
 	apiHealthPath                                        = "/v1/health"
+	apiVersionPath                                       = "/v1/version"
 	apiConfigPath                                        = "/v1/config"
 	apiStatsPath                                         = "/v1/stats"
 	apiWebPushPath                                       = "/v1/webpush"
@@ -467,6 +468,8 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
 		return s.ensureWebEnabled(s.handleEmpty)(w, r, v)
 	} else if r.Method == http.MethodGet && r.URL.Path == apiHealthPath {
 		return s.handleHealth(w, r, v)
+	} else if r.Method == http.MethodGet && r.URL.Path == apiVersionPath {
+		return s.ensureAdmin(s.handleVersion)(w, r, v)
 	} else if r.Method == http.MethodGet && r.URL.Path == apiConfigPath {
 		return s.handleConfig(w, r, v)
 	} else if r.Method == http.MethodGet && r.URL.Path == webConfigPath {

+ 8 - 0
server/server_admin.go

@@ -6,6 +6,14 @@ import (
 	"net/http"
 )
 
+func (s *Server) handleVersion(w http.ResponseWriter, r *http.Request, v *visitor) error {
+	return s.writeJSON(w, &apiVersionResponse{
+		Version: s.config.BuildVersion,
+		Commit:  s.config.BuildCommit,
+		Date:    s.config.BuildDate,
+	})
+}
+
 func (s *Server) handleUsersGet(w http.ResponseWriter, r *http.Request, v *visitor) error {
 	users, err := s.userManager.Users()
 	if err != nil {

+ 36 - 0
server/server_admin_test.go

@@ -1,6 +1,7 @@
 package server
 
 import (
+	"encoding/json"
 	"github.com/stretchr/testify/require"
 	"heckel.io/ntfy/v2/user"
 	"heckel.io/ntfy/v2/util"
@@ -9,6 +10,41 @@ import (
 	"time"
 )
 
+func TestVersion_Admin(t *testing.T) {
+	c := newTestConfigWithAuthFile(t)
+	c.BuildVersion = "1.2.3"
+	c.BuildCommit = "abcdef0"
+	c.BuildDate = "2026-02-08T00:00:00Z"
+	s := newTestServer(t, c)
+	defer s.closeDatabases()
+
+	// Create admin and regular user
+	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
+	require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false))
+
+	// Admin can access /v1/version
+	rr := request(t, s, "GET", "/v1/version", "", map[string]string{
+		"Authorization": util.BasicAuth("phil", "phil"),
+	})
+	require.Equal(t, 200, rr.Code)
+
+	var versionResponse apiVersionResponse
+	require.Nil(t, json.NewDecoder(rr.Body).Decode(&versionResponse))
+	require.Equal(t, "1.2.3", versionResponse.Version)
+	require.Equal(t, "abcdef0", versionResponse.Commit)
+	require.Equal(t, "2026-02-08T00:00:00Z", versionResponse.Date)
+
+	// Non-admin user cannot access /v1/version
+	rr = request(t, s, "GET", "/v1/version", "", map[string]string{
+		"Authorization": util.BasicAuth("ben", "ben"),
+	})
+	require.Equal(t, 401, rr.Code)
+
+	// Unauthenticated user cannot access /v1/version
+	rr = request(t, s, "GET", "/v1/version", "", nil)
+	require.Equal(t, 401, rr.Code)
+}
+
 func TestUser_AddRemove(t *testing.T) {
 	s := newTestServer(t, newTestConfigWithAuthFile(t))
 	defer s.closeDatabases()

+ 6 - 0
server/types.go

@@ -319,6 +319,12 @@ type apiHealthResponse struct {
 	Healthy bool `json:"healthy"`
 }
 
+type apiVersionResponse struct {
+	Version string `json:"version"`
+	Commit  string `json:"commit"`
+	Date    string `json:"date"`
+}
+
 type apiStatsResponse struct {
 	Messages     int64   `json:"messages"`
 	MessagesRate float64 `json:"messages_rate"` // Average number of messages per second