Files
agent-evals/backend/internal/api/server.go
Evan Reichard d2d8370c95 feat: add CORS middleware to backend server
Add cross-origin resource sharing support to the backend API to fix
frontend console errors when accessing resources from different origins.
The middleware handles preflight requests and includes necessary CORS
headers for API endpoints.

Fixes: Cross-Origin Request Blocked error
2026-02-05 18:21:47 -05:00

218 lines
5.3 KiB
Go

package api
import (
"encoding/json"
"fmt"
"net/http"
"path/filepath"
"strings"
"github.com/markdown-editor/internal/storage"
"github.com/markdown-editor/pkg/logger"
)
type ErrorResponse struct {
Error string `json:"error"`
}
// CORS middleware for allowing cross-origin requests
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Allow requests from any origin during development
// In production, you would specify allowed origins
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", "Content-Type, Authorization")
// Handle preflight requests
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
type Server struct {
storage *storage.Storage
host string
port int
}
func NewServer(dataDir, host string, port int) (*Server, error) {
return &Server{
storage: storage.NewStorage(dataDir),
host: host,
port: port,
}, nil
}
func (s *Server) Start() error {
mux := http.NewServeMux()
// API routes
mux.HandleFunc("/api/files", s.handleFiles)
mux.HandleFunc("/api/files/", s.handleFiles)
// Static files
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))
// Frontend SPA fallback
mux.HandleFunc("/", s.handleFrontend)
// Wrap with CORS middleware
handler := corsMiddleware(mux)
addr := fmt.Sprintf("%s:%d", s.host, s.port)
logger.Infof("Starting server on %s", addr)
return http.ListenAndServe(addr, handler)
}
func (s *Server) handleFiles(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
s.handleGetFiles(w, r)
case http.MethodPost:
s.handleCreateFile(w, r)
case http.MethodPut:
s.handleUpdateFile(w, r)
case http.MethodDelete:
s.handleDeleteFile(w, r)
default:
s.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
}
}
func (s *Server) handleGetFiles(w http.ResponseWriter, r *http.Request) {
// Check if URL includes a filename (e.g., /api/files/test.md)
filename := filepath.Base(r.URL.Path)
if filename != "" && r.URL.Path != "/api/files" {
// Get specific file
content, err := s.storage.GetFile(filename)
if err != nil {
s.sendError(w, http.StatusNotFound, err.Error())
return
}
w.Header().Set("Content-Type", "text/markdown")
w.Write([]byte(content))
return
}
// List all files
files, err := s.storage.ListFiles()
if err != nil {
s.sendError(w, http.StatusInternalServerError, err.Error())
return
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte{'['})
for i, file := range files {
if i > 0 {
w.Write([]byte{','})
}
w.Write([]byte{'"'})
w.Write([]byte(file))
w.Write([]byte{'"'})
}
w.Write([]byte{']'})
}
func (s *Server) handleCreateFile(w http.ResponseWriter, r *http.Request) {
var req struct {
Content string `json:"content"`
Name string `json:"name"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.sendError(w, http.StatusBadRequest, "invalid request body")
return
}
if req.Name == "" {
s.sendError(w, http.StatusBadRequest, "filename is required")
return
}
if !strings.HasSuffix(req.Name, ".md") {
s.sendError(w, http.StatusBadRequest, "filename must end with .md")
return
}
if err := s.storage.SaveFile(req.Name, req.Content); err != nil {
s.sendError(w, http.StatusInternalServerError, err.Error())
return
}
logger.Infof("Created file: %s", req.Name)
w.WriteHeader(http.StatusCreated)
}
func (s *Server) handleUpdateFile(w http.ResponseWriter, r *http.Request) {
filename := filepath.Base(r.URL.Path)
if filename == "" {
s.sendError(w, http.StatusBadRequest, "filename is required")
return
}
if !strings.HasSuffix(filename, ".md") {
s.sendError(w, http.StatusBadRequest, "filename must end with .md")
return
}
var req struct {
Content string `json:"content"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.sendError(w, http.StatusBadRequest, "invalid request body")
return
}
if err := s.storage.SaveFile(filename, req.Content); err != nil {
s.sendError(w, http.StatusInternalServerError, err.Error())
return
}
logger.Infof("Updated file: %s", filename)
}
func (s *Server) handleDeleteFile(w http.ResponseWriter, r *http.Request) {
filename := filepath.Base(r.URL.Path)
if filename == "" {
s.sendError(w, http.StatusBadRequest, "filename is required")
return
}
if !strings.HasSuffix(filename, ".md") {
s.sendError(w, http.StatusBadRequest, "filename must end with .md")
return
}
if err := s.storage.DeleteFile(filename); err != nil {
s.sendError(w, http.StatusInternalServerError, err.Error())
return
}
logger.Infof("Deleted file: %s", filename)
}
func (s *Server) handleFrontend(w http.ResponseWriter, r *http.Request) {
// Serve the index.html for SPA
http.ServeFile(w, r, "./static/index.html")
}
func (s *Server) sendError(w http.ResponseWriter, statusCode int, message string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(ErrorResponse{Error: message})
}
func (s *Server) ServeStaticFiles(w http.ResponseWriter, r *http.Request) {
http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))).ServeHTTP(w, r)
}