// Package api provides HTTP routing, middleware, and handlers for the Cyrene Gateway. // Uses only the standard library net/http to minimize dependencies. package api import ( "encoding/json" "net/http" "strings" ) // Router handles HTTP routing with middleware support. type Router struct { mux *http.ServeMux authMgr AuthManager } // AuthManager is the interface for JWT authentication. type AuthManager interface { ValidateToken(token string) (userID, username, role string, err error) } // New creates a new Router with all routes configured. func New(authMgr AuthManager) *Router { r := &Router{ mux: http.NewServeMux(), authMgr: authMgr, } r.setupRoutes() return r } func (r *Router) setupRoutes() { // Public endpoints r.mux.HandleFunc("/api/v1/health", r.healthHandler) r.mux.HandleFunc("/api/v1/auth/login", r.loginHandler) r.mux.HandleFunc("/api/v1/auth/register", r.registerHandler) // Protected endpoints r.mux.HandleFunc("/api/v1/sessions", r.authMW(r.sessionsHandler)) r.mux.HandleFunc("/api/v1/sessions/", r.authMW(r.sessionByIDHandler)) r.mux.HandleFunc("/api/v1/admin/dashboard", r.authMW(r.adminMW(r.adminDashboardHandler))) } // ServeHTTP implements http.Handler. func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { // CORS w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type") if req.Method == "OPTIONS" { w.WriteHeader(http.StatusNoContent) return } r.mux.ServeHTTP(w, req) } // --- Middleware --- func (r *Router) authMW(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { header := req.Header.Get("Authorization") if header == "" || !strings.HasPrefix(strings.ToLower(header), "bearer ") { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "missing or invalid authorization"}) return } token := header[7:] userID, username, role, err := r.authMgr.ValidateToken(token) if err != nil { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid token"}) return } req.Header.Set("X-User-ID", userID) req.Header.Set("X-Username", username) req.Header.Set("X-Role", role) next(w, req) } } func (r *Router) adminMW(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { if req.Header.Get("X-Role") != "admin" { writeJSON(w, http.StatusForbidden, map[string]string{"error": "admin access required"}) return } next(w, req) } } // --- Public handlers --- func (r *Router) healthHandler(w http.ResponseWriter, _ *http.Request) { writeJSON(w, http.StatusOK, map[string]string{"status": "ok", "service": "cyrene-gateway"}) } func (r *Router) loginHandler(w http.ResponseWriter, req *http.Request) { if req.Method != "POST" { writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "POST required"}) return } var body struct { Username string `json:"username"` Password string `json:"password"` } if err := json.NewDecoder(req.Body).Decode(&body); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"}) return } mgr, ok := r.authMgr.(*StubAuthManager) if !ok { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "auth not configured"}) return } token, err := mgr.Login(body.Username, body.Password) if err != nil { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid credentials"}) return } writeJSON(w, http.StatusOK, map[string]string{"token": token}) } func (r *Router) registerHandler(w http.ResponseWriter, _ *http.Request) { writeJSON(w, http.StatusOK, map[string]string{"message": "registration not implemented"}) } // --- Protected handlers --- func (r *Router) sessionsHandler(w http.ResponseWriter, req *http.Request) { switch req.Method { case "GET": writeJSON(w, http.StatusOK, map[string]any{"sessions": []any{}}) case "POST": writeJSON(w, http.StatusCreated, map[string]string{"session_id": "stub-session"}) default: writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"}) } } func (r *Router) sessionByIDHandler(w http.ResponseWriter, req *http.Request) { id := strings.TrimPrefix(req.URL.Path, "/api/v1/sessions/") if req.Method == "DELETE" { writeJSON(w, http.StatusNoContent, nil) return } writeJSON(w, http.StatusOK, map[string]string{"session_id": id}) } func (r *Router) adminDashboardHandler(w http.ResponseWriter, _ *http.Request) { writeJSON(w, http.StatusOK, map[string]any{ "dashboard": map[string]any{ "status": "running", "version": "2.0.0-dev", }, }) } // --- Helpers --- func writeJSON(w http.ResponseWriter, status int, data any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) if data != nil { json.NewEncoder(w).Encode(data) } }