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
218 lines
5.3 KiB
Go
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)
|
|
}
|