- services/gateway/api: net/http router with JWT auth, stub login, 6 tests - services/gateway/ws: WebSocket hub with echo support - services/voice: STT/TTS provider chain with fallback, 6 tests - cmd/cyrene/main.go: single binary startup, 108 tests total - Smoke test: health check + login verified on :8080
168 lines
4.9 KiB
Go
168 lines
4.9 KiB
Go
// 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)
|
|
}
|
|
}
|