cv[i]
+ }
+ }
+ return false
+}
+
+// parseVersion extracts the major.minor.patch core from a tag like "v0.9.4" or
+// "0.9.4-evup.1". Returns ok=false when there is no numeric core to compare.
+func parseVersion(v string) ([3]int, bool) {
+ var out [3]int
+ v = strings.TrimPrefix(strings.TrimSpace(v), "v")
+ if i := strings.IndexAny(v, "-+"); i >= 0 {
+ v = v[:i]
+ }
+ if v == "" {
+ return out, false
+ }
+ for i, part := range strings.Split(v, ".") {
+ if i >= 3 {
+ break
+ }
+ n, err := strconv.Atoi(part)
+ if err != nil {
+ return out, false
+ }
+ out[i] = n
+ }
+ return out, true
+}
diff --git a/services/api/internal/transport/http/router/router.go b/services/api/internal/transport/http/router/router.go
index 6bea341c..a7f4b7e6 100644
--- a/services/api/internal/transport/http/router/router.go
+++ b/services/api/internal/transport/http/router/router.go
@@ -23,6 +23,7 @@ type Deps struct {
Authorizer *authz.Authorizer
ProjectVisibilitySvc httpmw.ProjectVisibilityChecker
Health *handler.HealthHandler
+ Version *handler.VersionHandler
Auth *handler.AuthHandler
User *handler.UserHandler
GlobalRole *handler.GlobalRoleHandler
@@ -62,6 +63,12 @@ func New(deps Deps) http.Handler {
r.Get("/healthz", deps.Health.Check)
r.Route("/v1", func(r chi.Router) {
+ // Version / update check — public, no auth required.
+ if deps.Version != nil {
+ r.Get("/version", deps.Version.Check)
+ r.Get("/releases", deps.Version.ListReleases)
+ }
+
// Auth
r.Route("/auth", func(r chi.Router) {
r.Post("/login", deps.Auth.Login)