package rest import ( "encoding/json" "errors" "net/http" "github.com/explorer/backend/auth" ) // handleAuthRefresh implements POST /api/v1/auth/refresh. // // Contract: // - Requires a valid, unrevoked wallet JWT in the Authorization header. // - Mints a new JWT for the same address+track with a fresh jti and a // fresh per-track TTL. // - Revokes the presented token so it cannot be reused. // // This is the mechanism that makes the short Track-4 TTL (60 min in // PR #8) acceptable: operators refresh while the token is still live // rather than re-signing a SIWE message every hour. func (s *Server) handleAuthRefresh(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed") return } if s.walletAuth == nil { writeError(w, http.StatusServiceUnavailable, "service_unavailable", "wallet auth not configured") return } token := extractBearerToken(r) if token == "" { writeError(w, http.StatusUnauthorized, "unauthorized", "missing or malformed Authorization header") return } resp, err := s.walletAuth.RefreshJWT(r.Context(), token) if err != nil { switch { case errors.Is(err, auth.ErrJWTRevoked): writeError(w, http.StatusUnauthorized, "token_revoked", err.Error()) case errors.Is(err, auth.ErrWalletAuthStorageNotInitialized): writeError(w, http.StatusServiceUnavailable, "service_unavailable", err.Error()) default: writeError(w, http.StatusUnauthorized, "unauthorized", err.Error()) } return } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(resp) } // handleAuthLogout implements POST /api/v1/auth/logout. // // Records the presented token's jti in jwt_revocations so subsequent // calls to ValidateJWT will reject it. Idempotent: logging out twice // with the same token succeeds. func (s *Server) handleAuthLogout(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed") return } if s.walletAuth == nil { writeError(w, http.StatusServiceUnavailable, "service_unavailable", "wallet auth not configured") return } token := extractBearerToken(r) if token == "" { writeError(w, http.StatusUnauthorized, "unauthorized", "missing or malformed Authorization header") return } if err := s.walletAuth.RevokeJWT(r.Context(), token, "logout"); err != nil { switch { case errors.Is(err, auth.ErrJWTRevocationStorageMissing): // Surface 503 so ops know migration 0016 hasn't run; the // client should treat the token as logged out locally. writeError(w, http.StatusServiceUnavailable, "service_unavailable", err.Error()) default: writeError(w, http.StatusUnauthorized, "unauthorized", err.Error()) } return } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]any{ "status": "ok", }) }