From c8cc854fd649d190569d56ea66fb9d3b385fe8de Mon Sep 17 00:00:00 2001 From: LordMathis Date: Sun, 3 Nov 2024 19:17:10 +0100 Subject: [PATCH] Rework request context handler --- backend/cmd/server/main.go | 3 +- backend/internal/api/context.go | 85 ------------------- backend/internal/api/handler_utils.go | 27 ------ backend/internal/api/routes.go | 48 ++++++----- backend/internal/auth/middleware.go | 7 +- .../{api => handlers}/auth_handlers.go | 18 ++-- .../{api => handlers}/file_handlers.go | 51 ++++++----- .../{api => handlers}/git_handlers.go | 16 ++-- backend/internal/handlers/handlers.go | 30 +++++++ .../{api => handlers}/static_handler.go | 2 +- .../{api => handlers}/user_handlers.go | 10 +-- .../{api => handlers}/workspace_handlers.go | 57 ++++++------- backend/internal/httpcontext/context.go | 31 +++++++ backend/internal/middleware/context.go | 49 +++++++++++ 14 files changed, 217 insertions(+), 217 deletions(-) delete mode 100644 backend/internal/api/context.go delete mode 100644 backend/internal/api/handler_utils.go rename backend/internal/{api => handlers}/auth_handlers.go (86%) rename backend/internal/{api => handlers}/file_handlers.go (63%) rename backend/internal/{api => handlers}/git_handlers.go (71%) create mode 100644 backend/internal/handlers/handlers.go rename backend/internal/{api => handlers}/static_handler.go (99%) rename backend/internal/{api => handlers}/user_handlers.go (55%) rename backend/internal/{api => handlers}/workspace_handlers.go (74%) create mode 100644 backend/internal/httpcontext/context.go create mode 100644 backend/internal/middleware/context.go diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index cc21fb7..671fc54 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -14,6 +14,7 @@ import ( "novamd/internal/config" "novamd/internal/db" "novamd/internal/filesystem" + "novamd/internal/handlers" ) func main() { @@ -74,7 +75,7 @@ func main() { }) // Handle all other routes with static file server - r.Get("/*", api.NewStaticHandler(cfg.StaticPath).ServeHTTP) + r.Get("/*", handlers.NewStaticHandler(cfg.StaticPath).ServeHTTP) // Start server port := os.Getenv("PORT") diff --git a/backend/internal/api/context.go b/backend/internal/api/context.go deleted file mode 100644 index 92089e2..0000000 --- a/backend/internal/api/context.go +++ /dev/null @@ -1,85 +0,0 @@ -// api/context.go -package api - -import ( - "context" - "net/http" - "novamd/internal/auth" - "novamd/internal/db" - "novamd/internal/filesystem" - "novamd/internal/models" - - "github.com/go-chi/chi/v5" -) - -type HandlerContext struct { - UserID int - UserRole string - Workspace *models.Workspace -} - -type contextKey string - -const handlerContextKey contextKey = "handlerContext" - -// Middleware to populate handler context -func WithHandlerContext(db *db.DB) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Get user claims from auth context - claims, err := auth.GetUserFromContext(r.Context()) - if err != nil { - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - - // Get workspace name from URL if it exists - workspaceName := chi.URLParam(r, "workspaceName") - - var workspace *models.Workspace - // Only look up workspace if name is provided - if workspaceName != "" { - workspace, err = db.GetWorkspaceByName(claims.UserID, workspaceName) - if err != nil { - http.Error(w, "Workspace not found", http.StatusNotFound) - return - } - } - - // Create handler context - ctx := &HandlerContext{ - UserID: claims.UserID, - UserRole: claims.Role, - Workspace: workspace, - } - - // Add to request context - reqCtx := context.WithValue(r.Context(), handlerContextKey, ctx) - next.ServeHTTP(w, r.WithContext(reqCtx)) - }) - } -} - -// Helper function to get handler context -func GetHandlerContext(r *http.Request) *HandlerContext { - ctx := r.Context().Value(handlerContextKey) - if ctx == nil { - return nil - } - return ctx.(*HandlerContext) -} - -type BaseHandler struct { - DB *db.DB - FS *filesystem.FileSystem -} - -// Helper method to get context and handle errors -func (h *BaseHandler) getContext(w http.ResponseWriter, r *http.Request) (*HandlerContext, bool) { - ctx := GetHandlerContext(r) - if ctx == nil { - http.Error(w, "Internal server error", http.StatusInternalServerError) - return nil, false - } - return ctx, true -} diff --git a/backend/internal/api/handler_utils.go b/backend/internal/api/handler_utils.go deleted file mode 100644 index bc98d88..0000000 --- a/backend/internal/api/handler_utils.go +++ /dev/null @@ -1,27 +0,0 @@ -package api - -import ( - "encoding/json" - "errors" - "net/http" - "strconv" - - "github.com/go-chi/chi/v5" -) - -func getWorkspaceID(r *http.Request) (int, error) { - workspaceIDStr := chi.URLParam(r, "workspaceId") - workspaceID, err := strconv.Atoi(workspaceIDStr) - if err != nil { - return 0, errors.New("invalid workspaceId") - } - - return workspaceID, nil -} - -func respondJSON(w http.ResponseWriter, data interface{}) { - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(data); err != nil { - http.Error(w, "Failed to encode response", http.StatusInternalServerError) - } -} diff --git a/backend/internal/api/routes.go b/backend/internal/api/routes.go index 1c600fa..1487d05 100644 --- a/backend/internal/api/routes.go +++ b/backend/internal/api/routes.go @@ -4,32 +4,34 @@ import ( "novamd/internal/auth" "novamd/internal/db" "novamd/internal/filesystem" + "novamd/internal/handlers" + "novamd/internal/middleware" "github.com/go-chi/chi/v5" ) func SetupRoutes(r chi.Router, db *db.DB, fs *filesystem.FileSystem, authMiddleware *auth.Middleware, sessionService *auth.SessionService) { - handler := &BaseHandler{ + handler := &handlers.Handler{ DB: db, FS: fs, } // Public routes (no authentication required) r.Group(func(r chi.Router) { - r.Post("/auth/login", Login(sessionService, db)) - r.Post("/auth/refresh", RefreshToken(sessionService)) + r.Post("/auth/login", handler.Login(sessionService)) + r.Post("/auth/refresh", handler.RefreshToken(sessionService)) }) // Protected routes (authentication required) r.Group(func(r chi.Router) { // Apply authentication middleware to all routes in this group r.Use(authMiddleware.Authenticate) - r.Use(WithHandlerContext(db)) + r.Use(middleware.WithHandlerContext(db)) // Auth routes - r.Post("/auth/logout", Logout(sessionService)) - r.Get("/auth/me", handler.GetCurrentUser(db)) + r.Post("/auth/logout", handler.Logout(sessionService)) + r.Get("/auth/me", handler.GetCurrentUser()) // Admin-only routes r.Group(func(r chi.Router) { @@ -41,35 +43,35 @@ func SetupRoutes(r chi.Router, db *db.DB, fs *filesystem.FileSystem, authMiddlew // Workspace routes r.Route("/workspaces", func(r chi.Router) { - r.Get("/", handler.ListWorkspaces(db)) - r.Post("/", handler.CreateWorkspace(db, fs)) - r.Get("/last", handler.GetLastWorkspace(db)) - r.Put("/last", handler.UpdateLastWorkspace(db)) + r.Get("/", handler.ListWorkspaces()) + r.Post("/", handler.CreateWorkspace()) + r.Get("/last", handler.GetLastWorkspace()) + r.Put("/last", handler.UpdateLastWorkspace()) // Single workspace routes r.Route("/{workspaceId}", func(r chi.Router) { - r.Use(authMiddleware.RequireWorkspaceOwnership(db)) + r.Use(authMiddleware.RequireWorkspaceAccess) - r.Get("/", handler.GetWorkspace(db)) - r.Put("/", handler.UpdateWorkspace(db, fs)) - r.Delete("/", handler.DeleteWorkspace(db)) + r.Get("/", handler.GetWorkspace()) + r.Put("/", handler.UpdateWorkspace()) + r.Delete("/", handler.DeleteWorkspace()) // File routes r.Route("/files", func(r chi.Router) { - r.Get("/", handler.ListFiles(fs)) - r.Get("/last", handler.GetLastOpenedFile(db, fs)) - r.Put("/last", handler.UpdateLastOpenedFile(db, fs)) - r.Get("/lookup", handler.LookupFileByName(fs)) + r.Get("/", handler.ListFiles()) + r.Get("/last", handler.GetLastOpenedFile()) + r.Put("/last", handler.UpdateLastOpenedFile()) + r.Get("/lookup", handler.LookupFileByName()) - r.Post("/*", handler.SaveFile(fs)) - r.Get("/*", handler.GetFileContent(fs)) - r.Delete("/*", handler.DeleteFile(fs)) + r.Post("/*", handler.SaveFile()) + r.Get("/*", handler.GetFileContent()) + r.Delete("/*", handler.DeleteFile()) }) // Git routes r.Route("/git", func(r chi.Router) { - r.Post("/commit", handler.StageCommitAndPush(fs)) - r.Post("/pull", handler.PullChanges(fs)) + r.Post("/commit", handler.StageCommitAndPush()) + r.Post("/pull", handler.PullChanges()) }) }) }) diff --git a/backend/internal/auth/middleware.go b/backend/internal/auth/middleware.go index ff41353..da2713d 100644 --- a/backend/internal/auth/middleware.go +++ b/backend/internal/auth/middleware.go @@ -5,6 +5,8 @@ import ( "fmt" "net/http" "strings" + + "novamd/internal/httpcontext" ) type contextKey string @@ -95,9 +97,8 @@ func (m *Middleware) RequireRole(role string) func(http.Handler) http.Handler { func (m *Middleware) RequireWorkspaceAccess(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Get our handler context - ctx := context.GetHandlerContext(r) - if ctx == nil { - http.Error(w, "Internal server error", http.StatusInternalServerError) + ctx, ok := httpcontext.GetRequestContext(w, r) + if !ok { return } diff --git a/backend/internal/api/auth_handlers.go b/backend/internal/handlers/auth_handlers.go similarity index 86% rename from backend/internal/api/auth_handlers.go rename to backend/internal/handlers/auth_handlers.go index 7187955..319b6d9 100644 --- a/backend/internal/api/auth_handlers.go +++ b/backend/internal/handlers/auth_handlers.go @@ -1,10 +1,10 @@ -package api +package handlers import ( "encoding/json" "net/http" "novamd/internal/auth" - "novamd/internal/db" + "novamd/internal/httpcontext" "novamd/internal/models" "golang.org/x/crypto/bcrypt" @@ -31,7 +31,7 @@ type RefreshResponse struct { } // Login handles user authentication and returns JWT tokens -func Login(authService *auth.SessionService, db *db.DB) http.HandlerFunc { +func (h *Handler) Login(authService *auth.SessionService) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req LoginRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -46,7 +46,7 @@ func Login(authService *auth.SessionService, db *db.DB) http.HandlerFunc { } // Get user from database - user, err := db.GetUserByEmail(req.Email) + user, err := h.DB.GetUserByEmail(req.Email) if err != nil { http.Error(w, "Invalid credentials", http.StatusUnauthorized) return @@ -79,7 +79,7 @@ func Login(authService *auth.SessionService, db *db.DB) http.HandlerFunc { } // Logout invalidates the user's session -func Logout(authService *auth.SessionService) http.HandlerFunc { +func (h *Handler) Logout(authService *auth.SessionService) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { sessionID := r.Header.Get("X-Session-ID") if sessionID == "" { @@ -98,7 +98,7 @@ func Logout(authService *auth.SessionService) http.HandlerFunc { } // RefreshToken generates a new access token using a refresh token -func RefreshToken(authService *auth.SessionService) http.HandlerFunc { +func (h *Handler) RefreshToken(authService *auth.SessionService) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req RefreshRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -127,15 +127,15 @@ func RefreshToken(authService *auth.SessionService) http.HandlerFunc { } // GetCurrentUser returns the currently authenticated user -func (h *BaseHandler) GetCurrentUser(db *db.DB) http.HandlerFunc { +func (h *Handler) GetCurrentUser() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - ctx, ok := h.getContext(w, r) + ctx, ok := httpcontext.GetRequestContext(w, r) if !ok { return } // Get user from database - user, err := db.GetUserByID(ctx.UserID) + user, err := h.DB.GetUserByID(ctx.UserID) if err != nil { http.Error(w, "User not found", http.StatusNotFound) return diff --git a/backend/internal/api/file_handlers.go b/backend/internal/handlers/file_handlers.go similarity index 63% rename from backend/internal/api/file_handlers.go rename to backend/internal/handlers/file_handlers.go index 7644fc0..4af815a 100644 --- a/backend/internal/api/file_handlers.go +++ b/backend/internal/handlers/file_handlers.go @@ -1,24 +1,23 @@ -package api +package handlers import ( "encoding/json" "io" "net/http" - "novamd/internal/db" - "novamd/internal/filesystem" + "novamd/internal/httpcontext" "github.com/go-chi/chi/v5" ) -func (h *BaseHandler) ListFiles(fs *filesystem.FileSystem) http.HandlerFunc { +func (h *Handler) ListFiles() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - ctx, ok := h.getContext(w, r) + ctx, ok := httpcontext.GetRequestContext(w, r) if !ok { return } - files, err := fs.ListFilesRecursively(ctx.UserID, ctx.Workspace.ID) + files, err := h.FS.ListFilesRecursively(ctx.UserID, ctx.Workspace.ID) if err != nil { http.Error(w, "Failed to list files", http.StatusInternalServerError) return @@ -28,9 +27,9 @@ func (h *BaseHandler) ListFiles(fs *filesystem.FileSystem) http.HandlerFunc { } } -func (h *BaseHandler) LookupFileByName(fs *filesystem.FileSystem) http.HandlerFunc { +func (h *Handler) LookupFileByName() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - ctx, ok := h.getContext(w, r) + ctx, ok := httpcontext.GetRequestContext(w, r) if !ok { return } @@ -41,7 +40,7 @@ func (h *BaseHandler) LookupFileByName(fs *filesystem.FileSystem) http.HandlerFu return } - filePaths, err := fs.FindFileByName(ctx.UserID, ctx.Workspace.ID, filename) + filePaths, err := h.FS.FindFileByName(ctx.UserID, ctx.Workspace.ID, filename) if err != nil { http.Error(w, "File not found", http.StatusNotFound) return @@ -51,15 +50,15 @@ func (h *BaseHandler) LookupFileByName(fs *filesystem.FileSystem) http.HandlerFu } } -func (h *BaseHandler) GetFileContent(fs *filesystem.FileSystem) http.HandlerFunc { +func (h *Handler) GetFileContent() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - ctx, ok := h.getContext(w, r) + ctx, ok := httpcontext.GetRequestContext(w, r) if !ok { return } filePath := chi.URLParam(r, "*") - content, err := fs.GetFileContent(ctx.UserID, ctx.Workspace.ID, filePath) + content, err := h.FS.GetFileContent(ctx.UserID, ctx.Workspace.ID, filePath) if err != nil { http.Error(w, "Failed to read file", http.StatusNotFound) return @@ -70,9 +69,9 @@ func (h *BaseHandler) GetFileContent(fs *filesystem.FileSystem) http.HandlerFunc } } -func (h *BaseHandler) SaveFile(fs *filesystem.FileSystem) http.HandlerFunc { +func (h *Handler) SaveFile() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - ctx, ok := h.getContext(w, r) + ctx, ok := httpcontext.GetRequestContext(w, r) if !ok { return } @@ -84,7 +83,7 @@ func (h *BaseHandler) SaveFile(fs *filesystem.FileSystem) http.HandlerFunc { return } - err = fs.SaveFile(ctx.UserID, ctx.Workspace.ID, filePath, content) + err = h.FS.SaveFile(ctx.UserID, ctx.Workspace.ID, filePath, content) if err != nil { http.Error(w, "Failed to save file", http.StatusInternalServerError) return @@ -94,15 +93,15 @@ func (h *BaseHandler) SaveFile(fs *filesystem.FileSystem) http.HandlerFunc { } } -func (h *BaseHandler) DeleteFile(fs *filesystem.FileSystem) http.HandlerFunc { +func (h *Handler) DeleteFile() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - ctx, ok := h.getContext(w, r) + ctx, ok := httpcontext.GetRequestContext(w, r) if !ok { return } filePath := chi.URLParam(r, "*") - err := fs.DeleteFile(ctx.UserID, ctx.Workspace.ID, filePath) + err := h.FS.DeleteFile(ctx.UserID, ctx.Workspace.ID, filePath) if err != nil { http.Error(w, "Failed to delete file", http.StatusInternalServerError) return @@ -113,20 +112,20 @@ func (h *BaseHandler) DeleteFile(fs *filesystem.FileSystem) http.HandlerFunc { } } -func (h *BaseHandler) GetLastOpenedFile(db *db.DB, fs *filesystem.FileSystem) http.HandlerFunc { +func (h *Handler) GetLastOpenedFile() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - ctx, ok := h.getContext(w, r) + ctx, ok := httpcontext.GetRequestContext(w, r) if !ok { return } - filePath, err := db.GetLastOpenedFile(ctx.Workspace.ID) + filePath, err := h.DB.GetLastOpenedFile(ctx.Workspace.ID) if err != nil { http.Error(w, "Failed to get last opened file", http.StatusInternalServerError) return } - if _, err := fs.ValidatePath(ctx.UserID, ctx.Workspace.ID, filePath); err != nil { + if _, err := h.FS.ValidatePath(ctx.UserID, ctx.Workspace.ID, filePath); err != nil { http.Error(w, "Invalid file path", http.StatusBadRequest) return } @@ -135,9 +134,9 @@ func (h *BaseHandler) GetLastOpenedFile(db *db.DB, fs *filesystem.FileSystem) ht } } -func (h *BaseHandler) UpdateLastOpenedFile(db *db.DB, fs *filesystem.FileSystem) http.HandlerFunc { +func (h *Handler) UpdateLastOpenedFile() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - ctx, ok := h.getContext(w, r) + ctx, ok := httpcontext.GetRequestContext(w, r) if !ok { return } @@ -153,13 +152,13 @@ func (h *BaseHandler) UpdateLastOpenedFile(db *db.DB, fs *filesystem.FileSystem) // Validate the file path exists in the workspace if requestBody.FilePath != "" { - if _, err := fs.ValidatePath(ctx.UserID, ctx.Workspace.ID, requestBody.FilePath); err != nil { + if _, err := h.FS.ValidatePath(ctx.UserID, ctx.Workspace.ID, requestBody.FilePath); err != nil { http.Error(w, "Invalid file path", http.StatusBadRequest) return } } - if err := db.UpdateLastOpenedFile(ctx.Workspace.ID, requestBody.FilePath); err != nil { + if err := h.DB.UpdateLastOpenedFile(ctx.Workspace.ID, requestBody.FilePath); err != nil { http.Error(w, "Failed to update last opened file", http.StatusInternalServerError) return } diff --git a/backend/internal/api/git_handlers.go b/backend/internal/handlers/git_handlers.go similarity index 71% rename from backend/internal/api/git_handlers.go rename to backend/internal/handlers/git_handlers.go index 662c811..61f7ba4 100644 --- a/backend/internal/api/git_handlers.go +++ b/backend/internal/handlers/git_handlers.go @@ -1,15 +1,15 @@ -package api +package handlers import ( "encoding/json" "net/http" - "novamd/internal/filesystem" + "novamd/internal/httpcontext" ) -func (h *BaseHandler) StageCommitAndPush(fs *filesystem.FileSystem) http.HandlerFunc { +func (h *Handler) StageCommitAndPush() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - ctx, ok := h.getContext(w, r) + ctx, ok := httpcontext.GetRequestContext(w, r) if !ok { return } @@ -28,7 +28,7 @@ func (h *BaseHandler) StageCommitAndPush(fs *filesystem.FileSystem) http.Handler return } - err := fs.StageCommitAndPush(ctx.UserID, ctx.Workspace.ID, requestBody.Message) + err := h.FS.StageCommitAndPush(ctx.UserID, ctx.Workspace.ID, requestBody.Message) if err != nil { http.Error(w, "Failed to stage, commit, and push changes: "+err.Error(), http.StatusInternalServerError) return @@ -38,14 +38,14 @@ func (h *BaseHandler) StageCommitAndPush(fs *filesystem.FileSystem) http.Handler } } -func (h *BaseHandler) PullChanges(fs *filesystem.FileSystem) http.HandlerFunc { +func (h *Handler) PullChanges() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - ctx, ok := h.getContext(w, r) + ctx, ok := httpcontext.GetRequestContext(w, r) if !ok { return } - err := fs.Pull(ctx.UserID, ctx.Workspace.ID) + err := h.FS.Pull(ctx.UserID, ctx.Workspace.ID) if err != nil { http.Error(w, "Failed to pull changes: "+err.Error(), http.StatusInternalServerError) return diff --git a/backend/internal/handlers/handlers.go b/backend/internal/handlers/handlers.go new file mode 100644 index 0000000..a9d4e75 --- /dev/null +++ b/backend/internal/handlers/handlers.go @@ -0,0 +1,30 @@ +package handlers + +import ( + "encoding/json" + "net/http" + "novamd/internal/db" + "novamd/internal/filesystem" +) + +// Handler provides common functionality for all handlers +type Handler struct { + DB *db.DB + FS *filesystem.FileSystem +} + +// NewHandler creates a new handler with the given dependencies +func NewHandler(db *db.DB, fs *filesystem.FileSystem) *Handler { + return &Handler{ + DB: db, + FS: fs, + } +} + +// respondJSON is a helper to send JSON responses +func respondJSON(w http.ResponseWriter, data interface{}) { + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(data); err != nil { + http.Error(w, "Failed to encode response", http.StatusInternalServerError) + } +} diff --git a/backend/internal/api/static_handler.go b/backend/internal/handlers/static_handler.go similarity index 99% rename from backend/internal/api/static_handler.go rename to backend/internal/handlers/static_handler.go index 971b35d..8dfb710 100644 --- a/backend/internal/api/static_handler.go +++ b/backend/internal/handlers/static_handler.go @@ -1,4 +1,4 @@ -package api +package handlers import ( "net/http" diff --git a/backend/internal/api/user_handlers.go b/backend/internal/handlers/user_handlers.go similarity index 55% rename from backend/internal/api/user_handlers.go rename to backend/internal/handlers/user_handlers.go index cdcff9f..8773f19 100644 --- a/backend/internal/api/user_handlers.go +++ b/backend/internal/handlers/user_handlers.go @@ -1,19 +1,19 @@ -package api +package handlers import ( "net/http" - "novamd/internal/db" + "novamd/internal/httpcontext" ) -func (h *BaseHandler) GetUser(db *db.DB) http.HandlerFunc { +func (h *Handler) GetUser() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - ctx, ok := h.getContext(w, r) + ctx, ok := httpcontext.GetRequestContext(w, r) if !ok { return } - user, err := db.GetUserByID(ctx.UserID) + user, err := h.DB.GetUserByID(ctx.UserID) if err != nil { http.Error(w, "Failed to get user", http.StatusInternalServerError) return diff --git a/backend/internal/api/workspace_handlers.go b/backend/internal/handlers/workspace_handlers.go similarity index 74% rename from backend/internal/api/workspace_handlers.go rename to backend/internal/handlers/workspace_handlers.go index 714516a..c734575 100644 --- a/backend/internal/api/workspace_handlers.go +++ b/backend/internal/handlers/workspace_handlers.go @@ -1,22 +1,21 @@ -package api +package handlers import ( "encoding/json" "net/http" - "novamd/internal/db" - "novamd/internal/filesystem" + "novamd/internal/httpcontext" "novamd/internal/models" ) -func (h *BaseHandler) ListWorkspaces(db *db.DB) http.HandlerFunc { +func (h *Handler) ListWorkspaces() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - ctx, ok := h.getContext(w, r) + ctx, ok := httpcontext.GetRequestContext(w, r) if !ok { return } - workspaces, err := db.GetWorkspacesByUserID(ctx.UserID) + workspaces, err := h.DB.GetWorkspacesByUserID(ctx.UserID) if err != nil { http.Error(w, "Failed to list workspaces", http.StatusInternalServerError) return @@ -26,9 +25,9 @@ func (h *BaseHandler) ListWorkspaces(db *db.DB) http.HandlerFunc { } } -func (h *BaseHandler) CreateWorkspace(db *db.DB, fs *filesystem.FileSystem) http.HandlerFunc { +func (h *Handler) CreateWorkspace() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - ctx, ok := h.getContext(w, r) + ctx, ok := httpcontext.GetRequestContext(w, r) if !ok { return } @@ -40,12 +39,12 @@ func (h *BaseHandler) CreateWorkspace(db *db.DB, fs *filesystem.FileSystem) http } workspace.UserID = ctx.UserID - if err := db.CreateWorkspace(&workspace); err != nil { + if err := h.DB.CreateWorkspace(&workspace); err != nil { http.Error(w, "Failed to create workspace", http.StatusInternalServerError) return } - if err := fs.InitializeUserWorkspace(workspace.UserID, workspace.ID); err != nil { + if err := h.FS.InitializeUserWorkspace(workspace.UserID, workspace.ID); err != nil { http.Error(w, "Failed to initialize workspace directory", http.StatusInternalServerError) return } @@ -54,9 +53,9 @@ func (h *BaseHandler) CreateWorkspace(db *db.DB, fs *filesystem.FileSystem) http } } -func (h *BaseHandler) GetWorkspace(db *db.DB) http.HandlerFunc { +func (h *Handler) GetWorkspace() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - ctx, ok := h.getContext(w, r) + ctx, ok := httpcontext.GetRequestContext(w, r) if !ok { return } @@ -81,9 +80,9 @@ func gitSettingsChanged(new, old *models.Workspace) bool { return false } -func (h *BaseHandler) UpdateWorkspace(db *db.DB, fs *filesystem.FileSystem) http.HandlerFunc { +func (h *Handler) UpdateWorkspace() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - ctx, ok := h.getContext(w, r) + ctx, ok := httpcontext.GetRequestContext(w, r) if !ok { return } @@ -107,7 +106,7 @@ func (h *BaseHandler) UpdateWorkspace(db *db.DB, fs *filesystem.FileSystem) http // Handle Git repository setup/teardown if Git settings changed if gitSettingsChanged(&workspace, ctx.Workspace) { if workspace.GitEnabled { - if err := fs.SetupGitRepo( + if err := h.FS.SetupGitRepo( ctx.UserID, ctx.Workspace.ID, workspace.GitURL, @@ -119,11 +118,11 @@ func (h *BaseHandler) UpdateWorkspace(db *db.DB, fs *filesystem.FileSystem) http } } else { - fs.DisableGitRepo(ctx.UserID, ctx.Workspace.ID) + h.FS.DisableGitRepo(ctx.UserID, ctx.Workspace.ID) } } - if err := db.UpdateWorkspace(&workspace); err != nil { + if err := h.DB.UpdateWorkspace(&workspace); err != nil { http.Error(w, "Failed to update workspace", http.StatusInternalServerError) return } @@ -132,15 +131,15 @@ func (h *BaseHandler) UpdateWorkspace(db *db.DB, fs *filesystem.FileSystem) http } } -func (h *BaseHandler) DeleteWorkspace(db *db.DB) http.HandlerFunc { +func (h *Handler) DeleteWorkspace() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - ctx, ok := h.getContext(w, r) + ctx, ok := httpcontext.GetRequestContext(w, r) if !ok { return } // Check if this is the user's last workspace - workspaces, err := db.GetWorkspacesByUserID(ctx.UserID) + workspaces, err := h.DB.GetWorkspacesByUserID(ctx.UserID) if err != nil { http.Error(w, "Failed to get workspaces", http.StatusInternalServerError) return @@ -161,7 +160,7 @@ func (h *BaseHandler) DeleteWorkspace(db *db.DB) http.HandlerFunc { } // Start transaction - tx, err := db.Begin() + tx, err := h.DB.Begin() if err != nil { http.Error(w, "Failed to start transaction", http.StatusInternalServerError) return @@ -169,14 +168,14 @@ func (h *BaseHandler) DeleteWorkspace(db *db.DB) http.HandlerFunc { defer tx.Rollback() // Update last workspace ID first - err = db.UpdateLastWorkspaceTx(tx, ctx.UserID, nextWorkspaceID) + err = h.DB.UpdateLastWorkspaceTx(tx, ctx.UserID, nextWorkspaceID) if err != nil { http.Error(w, "Failed to update last workspace", http.StatusInternalServerError) return } // Delete the workspace - err = db.DeleteWorkspaceTx(tx, ctx.Workspace.ID) + err = h.DB.DeleteWorkspaceTx(tx, ctx.Workspace.ID) if err != nil { http.Error(w, "Failed to delete workspace", http.StatusInternalServerError) return @@ -193,14 +192,14 @@ func (h *BaseHandler) DeleteWorkspace(db *db.DB) http.HandlerFunc { } } -func (h *BaseHandler) GetLastWorkspace(db *db.DB) http.HandlerFunc { +func (h *Handler) GetLastWorkspace() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - ctx, ok := h.getContext(w, r) + ctx, ok := httpcontext.GetRequestContext(w, r) if !ok { return } - workspaceID, err := db.GetLastWorkspaceID(ctx.UserID) + workspaceID, err := h.DB.GetLastWorkspaceID(ctx.UserID) if err != nil { http.Error(w, "Failed to get last workspace", http.StatusInternalServerError) return @@ -210,9 +209,9 @@ func (h *BaseHandler) GetLastWorkspace(db *db.DB) http.HandlerFunc { } } -func (h *BaseHandler) UpdateLastWorkspace(db *db.DB) http.HandlerFunc { +func (h *Handler) UpdateLastWorkspace() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - ctx, ok := h.getContext(w, r) + ctx, ok := httpcontext.GetRequestContext(w, r) if !ok { return } @@ -226,7 +225,7 @@ func (h *BaseHandler) UpdateLastWorkspace(db *db.DB) http.HandlerFunc { return } - if err := db.UpdateLastWorkspace(ctx.UserID, requestBody.WorkspaceID); err != nil { + if err := h.DB.UpdateLastWorkspace(ctx.UserID, requestBody.WorkspaceID); err != nil { http.Error(w, "Failed to update last workspace", http.StatusInternalServerError) return } diff --git a/backend/internal/httpcontext/context.go b/backend/internal/httpcontext/context.go new file mode 100644 index 0000000..9e41519 --- /dev/null +++ b/backend/internal/httpcontext/context.go @@ -0,0 +1,31 @@ +package httpcontext + +import ( + "context" + "net/http" + "novamd/internal/models" +) + +// HandlerContext holds the request-specific data available to all handlers +type HandlerContext struct { + UserID int + UserRole string + Workspace *models.Workspace // Will be nil for non-workspace endpoints +} + +type contextKey string + +const HandlerContextKey contextKey = "handlerContext" + +func GetRequestContext(w http.ResponseWriter, r *http.Request) (*HandlerContext, bool) { + ctx := r.Context().Value(HandlerContextKey) + if ctx == nil { + http.Error(w, "Internal server error", http.StatusInternalServerError) + return nil, false + } + return ctx.(*HandlerContext), true +} + +func WithHandlerContext(r *http.Request, hctx *HandlerContext) *http.Request { + return r.WithContext(context.WithValue(r.Context(), HandlerContextKey, hctx)) +} diff --git a/backend/internal/middleware/context.go b/backend/internal/middleware/context.go new file mode 100644 index 0000000..7965918 --- /dev/null +++ b/backend/internal/middleware/context.go @@ -0,0 +1,49 @@ +package middleware + +import ( + "net/http" + "novamd/internal/auth" + "novamd/internal/db" + "novamd/internal/httpcontext" + "novamd/internal/models" + + "github.com/go-chi/chi/v5" +) + +// WithHandlerContext middleware populates the HandlerContext for the request +// This should be placed after authentication middleware +func WithHandlerContext(db *db.DB) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Get user claims from auth middleware + claims, err := auth.GetUserFromContext(r.Context()) + if err != nil { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + // Try to get workspace from URL if it exists + workspaceName := chi.URLParam(r, "workspaceName") + + var workspace *models.Workspace + if workspaceName != "" { + workspace, err = db.GetWorkspaceByName(claims.UserID, workspaceName) + if err != nil { + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + } + + // Create handler context with user and workspace info + hctx := &httpcontext.HandlerContext{ + UserID: claims.UserID, + UserRole: claims.Role, + Workspace: workspace, + } + + // Add context to request + r = httpcontext.WithHandlerContext(r, hctx) + next.ServeHTTP(w, r) + }) + } +}