Update api docs

This commit is contained in:
2024-12-03 21:50:16 +01:00
parent c400d81c87
commit e413e955c5
17 changed files with 331 additions and 261 deletions

View File

@@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/plumbing/transport/http" "github.com/go-git/go-git/v5/plumbing/transport/http"
) )
@@ -26,11 +27,19 @@ type Config struct {
type Client interface { type Client interface {
Clone() error Clone() error
Pull() error Pull() error
Commit(message string) error Commit(message string) (CommitHash, error)
Push() error Push() error
EnsureRepo() error EnsureRepo() error
} }
// CommitHash represents a Git commit hash
type CommitHash plumbing.Hash
// String returns the string representation of the CommitHash
func (h CommitHash) String() string {
return plumbing.Hash(h).String()
}
// client implements the Client interface // client implements the Client interface
type client struct { type client struct {
Config Config
@@ -101,22 +110,22 @@ func (c *client) Pull() error {
} }
// Commit commits the changes in the repository with the given message // Commit commits the changes in the repository with the given message
func (c *client) Commit(message string) error { func (c *client) Commit(message string) (CommitHash, error) {
if c.repo == nil { if c.repo == nil {
return fmt.Errorf("repository not initialized") return CommitHash(plumbing.ZeroHash), fmt.Errorf("repository not initialized")
} }
w, err := c.repo.Worktree() w, err := c.repo.Worktree()
if err != nil { if err != nil {
return fmt.Errorf("failed to get worktree: %w", err) return CommitHash(plumbing.ZeroHash), fmt.Errorf("failed to get worktree: %w", err)
} }
_, err = w.Add(".") _, err = w.Add(".")
if err != nil { if err != nil {
return fmt.Errorf("failed to add changes: %w", err) return CommitHash(plumbing.ZeroHash), fmt.Errorf("failed to add changes: %w", err)
} }
_, err = w.Commit(message, &git.CommitOptions{ hash, err := w.Commit(message, &git.CommitOptions{
Author: &object.Signature{ Author: &object.Signature{
Name: c.CommitName, Name: c.CommitName,
Email: c.CommitEmail, Email: c.CommitEmail,
@@ -124,10 +133,10 @@ func (c *client) Commit(message string) error {
}, },
}) })
if err != nil { if err != nil {
return fmt.Errorf("failed to commit changes: %w", err) return CommitHash(plumbing.ZeroHash), fmt.Errorf("failed to commit changes: %w", err)
} }
return nil return CommitHash(hash), nil
} }
// Push pushes the changes to the remote repository // Push pushes the changes to the remote repository

View File

@@ -55,13 +55,13 @@ type SystemStats struct {
// @ID adminListUsers // @ID adminListUsers
// @Produce json // @Produce json
// @Success 200 {array} models.User // @Success 200 {array} models.User
// @Failure 500 {string} "Failed to list users" // @Failure 500 {object} ErrorResponse "Failed to list users"
// @Router /admin/users [get] // @Router /admin/users [get]
func (h *Handler) AdminListUsers() http.HandlerFunc { func (h *Handler) AdminListUsers() http.HandlerFunc {
return func(w http.ResponseWriter, _ *http.Request) { return func(w http.ResponseWriter, _ *http.Request) {
users, err := h.DB.GetAllUsers() users, err := h.DB.GetAllUsers()
if err != nil { if err != nil {
http.Error(w, "Failed to list users", http.StatusInternalServerError) respondError(w, "Failed to list users", http.StatusInternalServerError)
return return
} }
@@ -79,45 +79,45 @@ func (h *Handler) AdminListUsers() http.HandlerFunc {
// @Produce json // @Produce json
// @Param user body CreateUserRequest true "User details" // @Param user body CreateUserRequest true "User details"
// @Success 200 {object} models.User // @Success 200 {object} models.User
// @Failure 400 {string} "Invalid request body" // @Failure 400 {object} ErrorResponse "Invalid request body"
// @Failure 400 {string} "Email, password, and role are required" // @Failure 400 {object} ErrorResponse "Email, password, and role are required"
// @Failure 400 {string} "Password must be at least 8 characters" // @Failure 400 {object} ErrorResponse "Password must be at least 8 characters"
// @Failure 409 {string} "Email already exists" // @Failure 409 {object} ErrorResponse "Email already exists"
// @Failure 500 {string} "Failed to hash password" // @Failure 500 {object} ErrorResponse "Failed to hash password"
// @Failure 500 {string} "Failed to create user" // @Failure 500 {object} ErrorResponse "Failed to create user"
// @Failure 500 {string} "Failed to initialize user workspace" // @Failure 500 {object} ErrorResponse "Failed to initialize user workspace"
// @Router /admin/users [post] // @Router /admin/users [post]
func (h *Handler) AdminCreateUser() http.HandlerFunc { func (h *Handler) AdminCreateUser() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest) respondError(w, "Invalid request body", http.StatusBadRequest)
return return
} }
// Validate request // Validate request
if req.Email == "" || req.Password == "" || req.Role == "" { if req.Email == "" || req.Password == "" || req.Role == "" {
http.Error(w, "Email, password, and role are required", http.StatusBadRequest) respondError(w, "Email, password, and role are required", http.StatusBadRequest)
return return
} }
// Check if email already exists // Check if email already exists
existingUser, err := h.DB.GetUserByEmail(req.Email) existingUser, err := h.DB.GetUserByEmail(req.Email)
if err == nil && existingUser != nil { if err == nil && existingUser != nil {
http.Error(w, "Email already exists", http.StatusConflict) respondError(w, "Email already exists", http.StatusConflict)
return return
} }
// Check if password is long enough // Check if password is long enough
if len(req.Password) < 8 { if len(req.Password) < 8 {
http.Error(w, "Password must be at least 8 characters", http.StatusBadRequest) respondError(w, "Password must be at least 8 characters", http.StatusBadRequest)
return return
} }
// Hash password // Hash password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil { if err != nil {
http.Error(w, "Failed to hash password", http.StatusInternalServerError) respondError(w, "Failed to hash password", http.StatusInternalServerError)
return return
} }
@@ -131,13 +131,13 @@ func (h *Handler) AdminCreateUser() http.HandlerFunc {
insertedUser, err := h.DB.CreateUser(user) insertedUser, err := h.DB.CreateUser(user)
if err != nil { if err != nil {
http.Error(w, "Failed to create user", http.StatusInternalServerError) respondError(w, "Failed to create user", http.StatusInternalServerError)
return return
} }
// Initialize user workspace // Initialize user workspace
if err := h.Storage.InitializeUserWorkspace(insertedUser.ID, insertedUser.LastWorkspaceID); err != nil { if err := h.Storage.InitializeUserWorkspace(insertedUser.ID, insertedUser.LastWorkspaceID); err != nil {
http.Error(w, "Failed to initialize user workspace", http.StatusInternalServerError) respondError(w, "Failed to initialize user workspace", http.StatusInternalServerError)
return return
} }
@@ -154,20 +154,20 @@ func (h *Handler) AdminCreateUser() http.HandlerFunc {
// @Produce json // @Produce json
// @Param userId path int true "User ID" // @Param userId path int true "User ID"
// @Success 200 {object} models.User // @Success 200 {object} models.User
// @Failure 400 {string} "Invalid user ID" // @Failure 400 {object} ErrorResponse "Invalid user ID"
// @Failure 404 {string} "User not found" // @Failure 404 {object} ErrorResponse "User not found"
// @Router /admin/users/{userId} [get] // @Router /admin/users/{userId} [get]
func (h *Handler) AdminGetUser() http.HandlerFunc { func (h *Handler) AdminGetUser() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
userID, err := strconv.Atoi(chi.URLParam(r, "userId")) userID, err := strconv.Atoi(chi.URLParam(r, "userId"))
if err != nil { if err != nil {
http.Error(w, "Invalid user ID", http.StatusBadRequest) respondError(w, "Invalid user ID", http.StatusBadRequest)
return return
} }
user, err := h.DB.GetUserByID(userID) user, err := h.DB.GetUserByID(userID)
if err != nil { if err != nil {
http.Error(w, "User not found", http.StatusNotFound) respondError(w, "User not found", http.StatusNotFound)
return return
} }
@@ -186,30 +186,30 @@ func (h *Handler) AdminGetUser() http.HandlerFunc {
// @Param userId path int true "User ID" // @Param userId path int true "User ID"
// @Param user body UpdateUserRequest true "User details" // @Param user body UpdateUserRequest true "User details"
// @Success 200 {object} models.User // @Success 200 {object} models.User
// @Failure 400 {string} "Invalid user ID" // @Failure 400 {object} ErrorResponse "Invalid user ID"
// @Failure 400 {string} "Invalid request body" // @Failure 400 {object} ErrorResponse "Invalid request body"
// @Failure 404 {string} "User not found" // @Failure 404 {object} ErrorResponse "User not found"
// @Failure 500 {string} "Failed to hash password" // @Failure 500 {object} ErrorResponse "Failed to hash password"
// @Failure 500 {string} "Failed to update user" // @Failure 500 {object} ErrorResponse "Failed to update user"
// @Router /admin/users/{userId} [put] // @Router /admin/users/{userId} [put]
func (h *Handler) AdminUpdateUser() http.HandlerFunc { func (h *Handler) AdminUpdateUser() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
userID, err := strconv.Atoi(chi.URLParam(r, "userId")) userID, err := strconv.Atoi(chi.URLParam(r, "userId"))
if err != nil { if err != nil {
http.Error(w, "Invalid user ID", http.StatusBadRequest) respondError(w, "Invalid user ID", http.StatusBadRequest)
return return
} }
// Get existing user // Get existing user
user, err := h.DB.GetUserByID(userID) user, err := h.DB.GetUserByID(userID)
if err != nil { if err != nil {
http.Error(w, "User not found", http.StatusNotFound) respondError(w, "User not found", http.StatusNotFound)
return return
} }
var req UpdateUserRequest var req UpdateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest) respondError(w, "Invalid request body", http.StatusBadRequest)
return return
} }
@@ -226,14 +226,14 @@ func (h *Handler) AdminUpdateUser() http.HandlerFunc {
if req.Password != "" { if req.Password != "" {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil { if err != nil {
http.Error(w, "Failed to hash password", http.StatusInternalServerError) respondError(w, "Failed to hash password", http.StatusInternalServerError)
return return
} }
user.PasswordHash = string(hashedPassword) user.PasswordHash = string(hashedPassword)
} }
if err := h.DB.UpdateUser(user); err != nil { if err := h.DB.UpdateUser(user); err != nil {
http.Error(w, "Failed to update user", http.StatusInternalServerError) respondError(w, "Failed to update user", http.StatusInternalServerError)
return return
} }
@@ -249,11 +249,11 @@ func (h *Handler) AdminUpdateUser() http.HandlerFunc {
// @ID adminDeleteUser // @ID adminDeleteUser
// @Param userId path int true "User ID" // @Param userId path int true "User ID"
// @Success 204 "No Content" // @Success 204 "No Content"
// @Failure 400 {string} "Invalid user ID" // @Failure 400 {object} ErrorResponse "Invalid user ID"
// @Failure 400 {string} "Cannot delete your own account" // @Failure 400 {object} ErrorResponse "Cannot delete your own account"
// @Failure 403 {string} "Cannot delete other admin users" // @Failure 403 {object} ErrorResponse "Cannot delete other admin users"
// @Failure 404 {string} "User not found" // @Failure 404 {object} ErrorResponse "User not found"
// @Failure 500 {string} "Failed to delete user" // @Failure 500 {object} ErrorResponse "Failed to delete user"
// @Router /admin/users/{userId} [delete] // @Router /admin/users/{userId} [delete]
func (h *Handler) AdminDeleteUser() http.HandlerFunc { func (h *Handler) AdminDeleteUser() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
@@ -264,31 +264,31 @@ func (h *Handler) AdminDeleteUser() http.HandlerFunc {
userID, err := strconv.Atoi(chi.URLParam(r, "userId")) userID, err := strconv.Atoi(chi.URLParam(r, "userId"))
if err != nil { if err != nil {
http.Error(w, "Invalid user ID", http.StatusBadRequest) respondError(w, "Invalid user ID", http.StatusBadRequest)
return return
} }
// Prevent admin from deleting themselves // Prevent admin from deleting themselves
if userID == ctx.UserID { if userID == ctx.UserID {
http.Error(w, "Cannot delete your own account", http.StatusBadRequest) respondError(w, "Cannot delete your own account", http.StatusBadRequest)
return return
} }
// Get user before deletion to check role // Get user before deletion to check role
user, err := h.DB.GetUserByID(userID) user, err := h.DB.GetUserByID(userID)
if err != nil { if err != nil {
http.Error(w, "User not found", http.StatusNotFound) respondError(w, "User not found", http.StatusNotFound)
return return
} }
// Prevent deletion of other admin users // Prevent deletion of other admin users
if user.Role == models.RoleAdmin && ctx.UserID != userID { if user.Role == models.RoleAdmin && ctx.UserID != userID {
http.Error(w, "Cannot delete other admin users", http.StatusForbidden) respondError(w, "Cannot delete other admin users", http.StatusForbidden)
return return
} }
if err := h.DB.DeleteUser(userID); err != nil { if err := h.DB.DeleteUser(userID); err != nil {
http.Error(w, "Failed to delete user", http.StatusInternalServerError) respondError(w, "Failed to delete user", http.StatusInternalServerError)
return return
} }
@@ -304,15 +304,15 @@ func (h *Handler) AdminDeleteUser() http.HandlerFunc {
// @ID adminListWorkspaces // @ID adminListWorkspaces
// @Produce json // @Produce json
// @Success 200 {array} WorkspaceStats // @Success 200 {array} WorkspaceStats
// @Failure 500 {string} "Failed to list workspaces" // @Failure 500 {object} ErrorResponse "Failed to list workspaces"
// @Failure 500 {string} "Failed to get user" // @Failure 500 {object} ErrorResponse "Failed to get user"
// @Failure 500 {string} "Failed to get file stats" // @Failure 500 {object} ErrorResponse "Failed to get file stats"
// @Router /admin/workspaces [get] // @Router /admin/workspaces [get]
func (h *Handler) AdminListWorkspaces() http.HandlerFunc { func (h *Handler) AdminListWorkspaces() http.HandlerFunc {
return func(w http.ResponseWriter, _ *http.Request) { return func(w http.ResponseWriter, _ *http.Request) {
workspaces, err := h.DB.GetAllWorkspaces() workspaces, err := h.DB.GetAllWorkspaces()
if err != nil { if err != nil {
http.Error(w, "Failed to list workspaces", http.StatusInternalServerError) respondError(w, "Failed to list workspaces", http.StatusInternalServerError)
return return
} }
@@ -324,7 +324,7 @@ func (h *Handler) AdminListWorkspaces() http.HandlerFunc {
user, err := h.DB.GetUserByID(ws.UserID) user, err := h.DB.GetUserByID(ws.UserID)
if err != nil { if err != nil {
http.Error(w, "Failed to get user", http.StatusInternalServerError) respondError(w, "Failed to get user", http.StatusInternalServerError)
return return
} }
@@ -336,7 +336,7 @@ func (h *Handler) AdminListWorkspaces() http.HandlerFunc {
fileStats, err := h.Storage.GetFileStats(ws.UserID, ws.ID) fileStats, err := h.Storage.GetFileStats(ws.UserID, ws.ID)
if err != nil { if err != nil {
http.Error(w, "Failed to get file stats", http.StatusInternalServerError) respondError(w, "Failed to get file stats", http.StatusInternalServerError)
return return
} }
@@ -357,20 +357,20 @@ func (h *Handler) AdminListWorkspaces() http.HandlerFunc {
// @ID adminGetSystemStats // @ID adminGetSystemStats
// @Produce json // @Produce json
// @Success 200 {object} SystemStats // @Success 200 {object} SystemStats
// @Failure 500 {string} "Failed to get user stats" // @Failure 500 {object} ErrorResponse "Failed to get user stats"
// @Failure 500 {string} "Failed to get file stats" // @Failure 500 {object} ErrorResponse "Failed to get file stats"
// @Router /admin/stats [get] // @Router /admin/stats [get]
func (h *Handler) AdminGetSystemStats() http.HandlerFunc { func (h *Handler) AdminGetSystemStats() http.HandlerFunc {
return func(w http.ResponseWriter, _ *http.Request) { return func(w http.ResponseWriter, _ *http.Request) {
userStats, err := h.DB.GetSystemStats() userStats, err := h.DB.GetSystemStats()
if err != nil { if err != nil {
http.Error(w, "Failed to get user stats", http.StatusInternalServerError) respondError(w, "Failed to get user stats", http.StatusInternalServerError)
return return
} }
fileStats, err := h.Storage.GetTotalFileStats() fileStats, err := h.Storage.GetTotalFileStats()
if err != nil { if err != nil {
http.Error(w, "Failed to get file stats", http.StatusInternalServerError) respondError(w, "Failed to get file stats", http.StatusInternalServerError)
return return
} }

View File

@@ -43,43 +43,43 @@ type RefreshResponse struct {
// @Produce json // @Produce json
// @Param body body LoginRequest true "Login request" // @Param body body LoginRequest true "Login request"
// @Success 200 {object} LoginResponse // @Success 200 {object} LoginResponse
// @Failure 400 {string} string "Invalid request body" // @Failure 400 {object} ErrorResponse "Invalid request body"
// @Failure 400 {string} string "Email and password are required" // @Failure 400 {object} ErrorResponse "Email and password are required"
// @Failure 401 {string} string "Invalid credentials" // @Failure 401 {object} ErrorResponse "Invalid credentials"
// @Failure 500 {string} string "Failed to create session" // @Failure 500 {object} ErrorResponse "Failed to create session"
// @Router /auth/login [post] // @Router /auth/login [post]
func (h *Handler) Login(authService *auth.SessionService) http.HandlerFunc { func (h *Handler) Login(authService *auth.SessionService) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
var req LoginRequest var req LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest) respondError(w, "Invalid request body", http.StatusBadRequest)
return return
} }
// Validate request // Validate request
if req.Email == "" || req.Password == "" { if req.Email == "" || req.Password == "" {
http.Error(w, "Email and password are required", http.StatusBadRequest) respondError(w, "Email and password are required", http.StatusBadRequest)
return return
} }
// Get user from database // Get user from database
user, err := h.DB.GetUserByEmail(req.Email) user, err := h.DB.GetUserByEmail(req.Email)
if err != nil { if err != nil {
http.Error(w, "Invalid credentials", http.StatusUnauthorized) respondError(w, "Invalid credentials", http.StatusUnauthorized)
return return
} }
// Verify password // Verify password
err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)) err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password))
if err != nil { if err != nil {
http.Error(w, "Invalid credentials", http.StatusUnauthorized) respondError(w, "Invalid credentials", http.StatusUnauthorized)
return return
} }
// Create session and generate tokens // Create session and generate tokens
session, accessToken, err := authService.CreateSession(user.ID, string(user.Role)) session, accessToken, err := authService.CreateSession(user.ID, string(user.Role))
if err != nil { if err != nil {
http.Error(w, "Failed to create session", http.StatusInternalServerError) respondError(w, "Failed to create session", http.StatusInternalServerError)
return return
} }
@@ -101,25 +101,25 @@ func (h *Handler) Login(authService *auth.SessionService) http.HandlerFunc {
// @Tags auth // @Tags auth
// @ID logout // @ID logout
// @Security BearerAuth // @Security BearerAuth
// @Success 200 {string} string "OK" // @Success 204 "No Content"
// @Failure 400 {string} string "Session ID required" // @Failure 400 {object} ErrorResponse "Session ID required"
// @Failure 500 {string} string "Failed to logout" // @Failure 500 {object} ErrorResponse "Failed to logout"
// @Router /auth/logout [post] // @Router /auth/logout [post]
func (h *Handler) Logout(authService *auth.SessionService) http.HandlerFunc { func (h *Handler) Logout(authService *auth.SessionService) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
sessionID := r.Header.Get("X-Session-ID") sessionID := r.Header.Get("X-Session-ID")
if sessionID == "" { if sessionID == "" {
http.Error(w, "Session ID required", http.StatusBadRequest) respondError(w, "Session ID required", http.StatusBadRequest)
return return
} }
err := authService.InvalidateSession(sessionID) err := authService.InvalidateSession(sessionID)
if err != nil { if err != nil {
http.Error(w, "Failed to logout", http.StatusInternalServerError) respondError(w, "Failed to logout", http.StatusInternalServerError)
return return
} }
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusNoContent)
} }
} }
@@ -132,27 +132,27 @@ func (h *Handler) Logout(authService *auth.SessionService) http.HandlerFunc {
// @Produce json // @Produce json
// @Param body body RefreshRequest true "Refresh request" // @Param body body RefreshRequest true "Refresh request"
// @Success 200 {object} RefreshResponse // @Success 200 {object} RefreshResponse
// @Failure 400 {string} string "Invalid request body" // @Failure 400 {object} ErrorResponse "Invalid request body"
// @Failure 400 {string} string "Refresh token required" // @Failure 400 {object} ErrorResponse "Refresh token required"
// @Failure 401 {string} string "Invalid refresh token" // @Failure 401 {object} ErrorResponse "Invalid refresh token"
// @Router /auth/refresh [post] // @Router /auth/refresh [post]
func (h *Handler) RefreshToken(authService *auth.SessionService) http.HandlerFunc { func (h *Handler) RefreshToken(authService *auth.SessionService) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
var req RefreshRequest var req RefreshRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest) respondError(w, "Invalid request body", http.StatusBadRequest)
return return
} }
if req.RefreshToken == "" { if req.RefreshToken == "" {
http.Error(w, "Refresh token required", http.StatusBadRequest) respondError(w, "Refresh token required", http.StatusBadRequest)
return return
} }
// Generate new access token // Generate new access token
accessToken, err := authService.RefreshSession(req.RefreshToken) accessToken, err := authService.RefreshSession(req.RefreshToken)
if err != nil { if err != nil {
http.Error(w, "Invalid refresh token", http.StatusUnauthorized) respondError(w, "Invalid refresh token", http.StatusUnauthorized)
return return
} }
@@ -172,7 +172,7 @@ func (h *Handler) RefreshToken(authService *auth.SessionService) http.HandlerFun
// @Security BearerAuth // @Security BearerAuth
// @Produce json // @Produce json
// @Success 200 {object} models.User // @Success 200 {object} models.User
// @Failure 404 {string} string "User not found" // @Failure 404 {object} ErrorResponse "User not found"
// @Router /auth/me [get] // @Router /auth/me [get]
func (h *Handler) GetCurrentUser() http.HandlerFunc { func (h *Handler) GetCurrentUser() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
@@ -184,7 +184,7 @@ func (h *Handler) GetCurrentUser() http.HandlerFunc {
// Get user from database // Get user from database
user, err := h.DB.GetUserByID(ctx.UserID) user, err := h.DB.GetUserByID(ctx.UserID)
if err != nil { if err != nil {
http.Error(w, "User not found", http.StatusNotFound) respondError(w, "User not found", http.StatusNotFound)
return return
} }

View File

@@ -188,7 +188,7 @@ func TestAuthHandlers_Integration(t *testing.T) {
"X-Session-ID": loginResp.Session.ID, "X-Session-ID": loginResp.Session.ID,
} }
rr = h.makeRequest(t, http.MethodPost, "/api/v1/auth/logout", nil, loginResp.AccessToken, headers) rr = h.makeRequest(t, http.MethodPost, "/api/v1/auth/logout", nil, loginResp.AccessToken, headers)
require.Equal(t, http.StatusOK, rr.Code) require.Equal(t, http.StatusNoContent, rr.Code)
// Try to use the refresh token - should fail // Try to use the refresh token - should fail
refreshReq := handlers.RefreshRequest{ refreshReq := handlers.RefreshRequest{

View File

@@ -5,6 +5,7 @@ import (
"io" "io"
"net/http" "net/http"
"os" "os"
"time"
"novamd/internal/context" "novamd/internal/context"
"novamd/internal/storage" "novamd/internal/storage"
@@ -12,6 +13,28 @@ import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
) )
// LookupResponse represents a response to a file lookup request
type LookupResponse struct {
Paths []string `json:"paths"`
}
// SaveFileResponse represents a response to a save file request
type SaveFileResponse struct {
FilePath string `json:"filePath"`
Size int64 `json:"size"`
UpdatedAt time.Time `json:"updatedAt"`
}
// LastOpenedFileResponse represents a response to a last opened file request
type LastOpenedFileResponse struct {
LastOpenedFilePath string `json:"lastOpenedFilePath"`
}
// UpdateLastOpenedFileRequest represents a request to update the last opened file
type UpdateLastOpenedFileRequest struct {
FilePath string `json:"filePath"`
}
// ListFiles godoc // ListFiles godoc
// @Summary List files // @Summary List files
// @Description Lists all files in the user's workspace // @Description Lists all files in the user's workspace
@@ -20,8 +43,8 @@ import (
// @Security BearerAuth // @Security BearerAuth
// @Produce json // @Produce json
// @Param workspace_name path string true "Workspace name" // @Param workspace_name path string true "Workspace name"
// @Success 200 {array} string // @Success 200 {array} storage.FileNode
// @Failure 500 {string} string "Failed to list files" // @Failure 500 {object} ErrorResponse "Failed to list files"
// @Router /workspaces/{workspace_name}/files [get] // @Router /workspaces/{workspace_name}/files [get]
func (h *Handler) ListFiles() http.HandlerFunc { func (h *Handler) ListFiles() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
@@ -32,7 +55,7 @@ func (h *Handler) ListFiles() http.HandlerFunc {
files, err := h.Storage.ListFilesRecursively(ctx.UserID, ctx.Workspace.ID) files, err := h.Storage.ListFilesRecursively(ctx.UserID, ctx.Workspace.ID)
if err != nil { if err != nil {
http.Error(w, "Failed to list files", http.StatusInternalServerError) respondError(w, "Failed to list files", http.StatusInternalServerError)
return return
} }
@@ -49,9 +72,9 @@ func (h *Handler) ListFiles() http.HandlerFunc {
// @Produce json // @Produce json
// @Param workspace_name path string true "Workspace name" // @Param workspace_name path string true "Workspace name"
// @Param filename query string true "File name" // @Param filename query string true "File name"
// @Success 200 {object} map[string][]string // @Success 200 {object} LookupResponse
// @Failure 400 {string} string "Filename is required" // @Failure 400 {object} ErrorResponse "Filename is required"
// @Failure 404 {string} string "File not found" // @Failure 404 {object} ErrorResponse "File not found"
// @Router /workspaces/{workspace_name}/files/lookup [get] // @Router /workspaces/{workspace_name}/files/lookup [get]
func (h *Handler) LookupFileByName() http.HandlerFunc { func (h *Handler) LookupFileByName() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
@@ -62,17 +85,17 @@ func (h *Handler) LookupFileByName() http.HandlerFunc {
filename := r.URL.Query().Get("filename") filename := r.URL.Query().Get("filename")
if filename == "" { if filename == "" {
http.Error(w, "Filename is required", http.StatusBadRequest) respondError(w, "Filename is required", http.StatusBadRequest)
return return
} }
filePaths, err := h.Storage.FindFileByName(ctx.UserID, ctx.Workspace.ID, filename) filePaths, err := h.Storage.FindFileByName(ctx.UserID, ctx.Workspace.ID, filename)
if err != nil { if err != nil {
http.Error(w, "File not found", http.StatusNotFound) respondError(w, "File not found", http.StatusNotFound)
return return
} }
respondJSON(w, map[string][]string{"paths": filePaths}) respondJSON(w, &LookupResponse{Paths: filePaths})
} }
} }
@@ -86,10 +109,10 @@ func (h *Handler) LookupFileByName() http.HandlerFunc {
// @Param workspace_name path string true "Workspace name" // @Param workspace_name path string true "Workspace name"
// @Param file_path path string true "File path" // @Param file_path path string true "File path"
// @Success 200 {string} "File content" // @Success 200 {string} "File content"
// @Failure 400 {string} string "Invalid file path" // @Failure 400 {object} ErrorResponse "Invalid file path"
// @Failure 404 {string} string "File not found" // @Failure 404 {object} ErrorResponse "File not found"
// @Failure 500 {string} string "Failed to read file" // @Failure 500 {object} ErrorResponse "Failed to read file"
// @Failure 500 {string} string "Failed to write response" // @Failure 500 {object} ErrorResponse "Failed to write response"
// @Router /workspaces/{workspace_name}/files/* [get] // @Router /workspaces/{workspace_name}/files/* [get]
func (h *Handler) GetFileContent() http.HandlerFunc { func (h *Handler) GetFileContent() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
@@ -103,23 +126,23 @@ func (h *Handler) GetFileContent() http.HandlerFunc {
if err != nil { if err != nil {
if storage.IsPathValidationError(err) { if storage.IsPathValidationError(err) {
http.Error(w, "Invalid file path", http.StatusBadRequest) respondError(w, "Invalid file path", http.StatusBadRequest)
return return
} }
if os.IsNotExist(err) { if os.IsNotExist(err) {
http.Error(w, "File not found", http.StatusNotFound) respondError(w, "File not found", http.StatusNotFound)
return return
} }
http.Error(w, "Failed to read file", http.StatusInternalServerError) respondError(w, "Failed to read file", http.StatusInternalServerError)
return return
} }
w.Header().Set("Content-Type", "text/plain") w.Header().Set("Content-Type", "text/plain")
_, err = w.Write(content) _, err = w.Write(content)
if err != nil { if err != nil {
http.Error(w, "Failed to write response", http.StatusInternalServerError) respondError(w, "Failed to write response", http.StatusInternalServerError)
return return
} }
} }
@@ -135,10 +158,10 @@ func (h *Handler) GetFileContent() http.HandlerFunc {
// @Produce json // @Produce json
// @Param workspace_name path string true "Workspace name" // @Param workspace_name path string true "Workspace name"
// @Param file_path path string true "File path" // @Param file_path path string true "File path"
// @Success 200 {string} "File saved successfully" // @Success 200 {object} SaveFileResponse
// @Failure 400 {string} string "Failed to read request body" // @Failure 400 {object} ErrorResponse "Failed to read request body"
// @Failure 400 {string} string "Invalid file path" // @Failure 400 {object} ErrorResponse "Invalid file path"
// @Failure 500 {string} string "Failed to save file" // @Failure 500 {object} ErrorResponse "Failed to save file"
// @Router /workspaces/{workspace_name}/files/* [post] // @Router /workspaces/{workspace_name}/files/* [post]
func (h *Handler) SaveFile() http.HandlerFunc { func (h *Handler) SaveFile() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
@@ -150,22 +173,29 @@ func (h *Handler) SaveFile() http.HandlerFunc {
filePath := chi.URLParam(r, "*") filePath := chi.URLParam(r, "*")
content, err := io.ReadAll(r.Body) content, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
http.Error(w, "Failed to read request body", http.StatusBadRequest) respondError(w, "Failed to read request body", http.StatusBadRequest)
return return
} }
err = h.Storage.SaveFile(ctx.UserID, ctx.Workspace.ID, filePath, content) err = h.Storage.SaveFile(ctx.UserID, ctx.Workspace.ID, filePath, content)
if err != nil { if err != nil {
if storage.IsPathValidationError(err) { if storage.IsPathValidationError(err) {
http.Error(w, "Invalid file path", http.StatusBadRequest) respondError(w, "Invalid file path", http.StatusBadRequest)
return return
} }
http.Error(w, "Failed to save file", http.StatusInternalServerError) respondError(w, "Failed to save file", http.StatusInternalServerError)
return return
} }
respondJSON(w, map[string]string{"message": "File saved successfully"}) response := SaveFileResponse{
FilePath: filePath,
Size: int64(len(content)),
UpdatedAt: time.Now().UTC(),
}
w.WriteHeader(http.StatusOK)
respondJSON(w, response)
} }
} }
@@ -178,11 +208,11 @@ func (h *Handler) SaveFile() http.HandlerFunc {
// @Produce string // @Produce string
// @Param workspace_name path string true "Workspace name" // @Param workspace_name path string true "Workspace name"
// @Param file_path path string true "File path" // @Param file_path path string true "File path"
// @Success 200 {string} "File deleted successfully" // @Success 204 "No Content - File deleted successfully"
// @Failure 400 {string} string "Invalid file path" // @Failure 400 {object} ErrorResponse "Invalid file path"
// @Failure 404 {string} string "File not found" // @Failure 404 {object} ErrorResponse "File not found"
// @Failure 500 {string} string "Failed to delete file" // @Failure 500 {object} ErrorResponse "Failed to delete file"
// @Failure 500 {string} string "Failed to write response" // @Failure 500 {object} ErrorResponse "Failed to write response"
// @Router /workspaces/{workspace_name}/files/* [delete] // @Router /workspaces/{workspace_name}/files/* [delete]
func (h *Handler) DeleteFile() http.HandlerFunc { func (h *Handler) DeleteFile() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
@@ -195,25 +225,20 @@ func (h *Handler) DeleteFile() http.HandlerFunc {
err := h.Storage.DeleteFile(ctx.UserID, ctx.Workspace.ID, filePath) err := h.Storage.DeleteFile(ctx.UserID, ctx.Workspace.ID, filePath)
if err != nil { if err != nil {
if storage.IsPathValidationError(err) { if storage.IsPathValidationError(err) {
http.Error(w, "Invalid file path", http.StatusBadRequest) respondError(w, "Invalid file path", http.StatusBadRequest)
return return
} }
if os.IsNotExist(err) { if os.IsNotExist(err) {
http.Error(w, "File not found", http.StatusNotFound) respondError(w, "File not found", http.StatusNotFound)
return return
} }
http.Error(w, "Failed to delete file", http.StatusInternalServerError) respondError(w, "Failed to delete file", http.StatusInternalServerError)
return return
} }
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusNoContent)
_, err = w.Write([]byte("File deleted successfully"))
if err != nil {
http.Error(w, "Failed to write response", http.StatusInternalServerError)
return
}
} }
} }
@@ -225,9 +250,9 @@ func (h *Handler) DeleteFile() http.HandlerFunc {
// @Security BearerAuth // @Security BearerAuth
// @Produce json // @Produce json
// @Param workspace_name path string true "Workspace name" // @Param workspace_name path string true "Workspace name"
// @Success 200 {object} map[string]string // @Success 200 {object} LastOpenedFileResponse
// @Failure 400 {string} string "Invalid file path" // @Failure 400 {object} ErrorResponse "Invalid file path"
// @Failure 500 {string} string "Failed to get last opened file" // @Failure 500 {object} ErrorResponse "Failed to get last opened file"
// @Router /workspaces/{workspace_name}/files/last [get] // @Router /workspaces/{workspace_name}/files/last [get]
func (h *Handler) GetLastOpenedFile() http.HandlerFunc { func (h *Handler) GetLastOpenedFile() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
@@ -238,16 +263,16 @@ func (h *Handler) GetLastOpenedFile() http.HandlerFunc {
filePath, err := h.DB.GetLastOpenedFile(ctx.Workspace.ID) filePath, err := h.DB.GetLastOpenedFile(ctx.Workspace.ID)
if err != nil { if err != nil {
http.Error(w, "Failed to get last opened file", http.StatusInternalServerError) respondError(w, "Failed to get last opened file", http.StatusInternalServerError)
return return
} }
if _, err := h.Storage.ValidatePath(ctx.UserID, ctx.Workspace.ID, filePath); err != nil { if _, err := h.Storage.ValidatePath(ctx.UserID, ctx.Workspace.ID, filePath); err != nil {
http.Error(w, "Invalid file path", http.StatusBadRequest) respondError(w, "Invalid file path", http.StatusBadRequest)
return return
} }
respondJSON(w, map[string]string{"lastOpenedFilePath": filePath}) respondJSON(w, &LastOpenedFileResponse{LastOpenedFilePath: filePath})
} }
} }
@@ -260,11 +285,12 @@ func (h *Handler) GetLastOpenedFile() http.HandlerFunc {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param workspace_name path string true "Workspace name" // @Param workspace_name path string true "Workspace name"
// @Success 200 {object} map[string]string // @Param body body UpdateLastOpenedFileRequest true "Update last opened file request"
// @Failure 400 {string} string "Invalid request body" // @Success 204 "No Content - Last opened file updated successfully"
// @Failure 400 {string} string "Invalid file path" // @Failure 400 {object} ErrorResponse "Invalid request body"
// @Failure 404 {string} string "File not found" // @Failure 400 {object} ErrorResponse "Invalid file path"
// @Failure 500 {string} string "Failed to update file" // @Failure 404 {object} ErrorResponse "File not found"
// @Failure 500 {object} ErrorResponse "Failed to update file"
// @Router /workspaces/{workspace_name}/files/last [put] // @Router /workspaces/{workspace_name}/files/last [put]
func (h *Handler) UpdateLastOpenedFile() http.HandlerFunc { func (h *Handler) UpdateLastOpenedFile() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
@@ -273,12 +299,10 @@ func (h *Handler) UpdateLastOpenedFile() http.HandlerFunc {
return return
} }
var requestBody struct { var requestBody UpdateLastOpenedFileRequest
FilePath string `json:"filePath"`
}
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil { if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest) respondError(w, "Invalid request body", http.StatusBadRequest)
return return
} }
@@ -287,25 +311,25 @@ func (h *Handler) UpdateLastOpenedFile() http.HandlerFunc {
_, err := h.Storage.GetFileContent(ctx.UserID, ctx.Workspace.ID, requestBody.FilePath) _, err := h.Storage.GetFileContent(ctx.UserID, ctx.Workspace.ID, requestBody.FilePath)
if err != nil { if err != nil {
if storage.IsPathValidationError(err) { if storage.IsPathValidationError(err) {
http.Error(w, "Invalid file path", http.StatusBadRequest) respondError(w, "Invalid file path", http.StatusBadRequest)
return return
} }
if os.IsNotExist(err) { if os.IsNotExist(err) {
http.Error(w, "File not found", http.StatusNotFound) respondError(w, "File not found", http.StatusNotFound)
return return
} }
http.Error(w, "Failed to update last opened file", http.StatusInternalServerError) respondError(w, "Failed to update last opened file", http.StatusInternalServerError)
return return
} }
} }
if err := h.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) respondError(w, "Failed to update last opened file", http.StatusInternalServerError)
return return
} }
respondJSON(w, map[string]string{"message": "Last opened file updated successfully"}) w.WriteHeader(http.StatusNoContent)
} }
} }

View File

@@ -145,7 +145,7 @@ func TestFileHandlers_Integration(t *testing.T) {
// Delete file // Delete file
rr = h.makeRequest(t, http.MethodDelete, baseURL+"/"+filePath, nil, h.RegularToken, nil) rr = h.makeRequest(t, http.MethodDelete, baseURL+"/"+filePath, nil, h.RegularToken, nil)
require.Equal(t, http.StatusOK, rr.Code) require.Equal(t, http.StatusNoContent, rr.Code)
// Verify file is gone // Verify file is gone
rr = h.makeRequest(t, http.MethodGet, baseURL+"/"+filePath, nil, h.RegularToken, nil) rr = h.makeRequest(t, http.MethodGet, baseURL+"/"+filePath, nil, h.RegularToken, nil)
@@ -171,7 +171,7 @@ func TestFileHandlers_Integration(t *testing.T) {
FilePath: "docs/readme.md", FilePath: "docs/readme.md",
} }
rr = h.makeRequest(t, http.MethodPut, baseURL+"/last", updateReq, h.RegularToken, nil) rr = h.makeRequest(t, http.MethodPut, baseURL+"/last", updateReq, h.RegularToken, nil)
require.Equal(t, http.StatusOK, rr.Code) require.Equal(t, http.StatusNoContent, rr.Code)
// Verify update // Verify update
rr = h.makeRequest(t, http.MethodGet, baseURL+"/last", nil, h.RegularToken, nil) rr = h.makeRequest(t, http.MethodGet, baseURL+"/last", nil, h.RegularToken, nil)

View File

@@ -7,6 +7,21 @@ import (
"novamd/internal/context" "novamd/internal/context"
) )
// CommitRequest represents a request to commit changes
type CommitRequest struct {
Message string `json:"message" example:"Initial commit"`
}
// CommitResponse represents a response to a commit request
type CommitResponse struct {
CommitHash string `json:"commitHash" example:"a1b2c3d4"`
}
// PullResponse represents a response to a pull http request
type PullResponse struct {
Message string `json:"message" example:"Pulled changes from remote"`
}
// StageCommitAndPush godoc // StageCommitAndPush godoc
// @Summary Stage, commit, and push changes // @Summary Stage, commit, and push changes
// @Description Stages, commits, and pushes changes to the remote repository // @Description Stages, commits, and pushes changes to the remote repository
@@ -15,11 +30,11 @@ import (
// @Security BearerAuth // @Security BearerAuth
// @Produce json // @Produce json
// @Param workspace_name path string true "Workspace name" // @Param workspace_name path string true "Workspace name"
// @Param body body string true "Commit message" // @Param body body CommitRequest true "Commit request"
// @Success 200 {object} map[string]string // @Success 200 {object} CommitResponse
// @Failure 400 {string} string "Invalid request body" // @Failure 400 {object} ErrorResponse "Invalid request body"
// @Failure 400 {string} string "Commit message is required" // @Failure 400 {object} ErrorResponse "Commit message is required"
// @Failure 500 {string} string "Failed to stage, commit, and push changes" // @Failure 500 {object} ErrorResponse "Failed to stage, commit, and push changes"
// @Router /workspaces/{workspace_name}/git/commit [post] // @Router /workspaces/{workspace_name}/git/commit [post]
func (h *Handler) StageCommitAndPush() http.HandlerFunc { func (h *Handler) StageCommitAndPush() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
@@ -28,27 +43,25 @@ func (h *Handler) StageCommitAndPush() http.HandlerFunc {
return return
} }
var requestBody struct { var requestBody CommitRequest
Message string `json:"message"`
}
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil { if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest) respondError(w, "Invalid request body", http.StatusBadRequest)
return return
} }
if requestBody.Message == "" { if requestBody.Message == "" {
http.Error(w, "Commit message is required", http.StatusBadRequest) respondError(w, "Commit message is required", http.StatusBadRequest)
return return
} }
err := h.Storage.StageCommitAndPush(ctx.UserID, ctx.Workspace.ID, requestBody.Message) hash, err := h.Storage.StageCommitAndPush(ctx.UserID, ctx.Workspace.ID, requestBody.Message)
if err != nil { if err != nil {
http.Error(w, "Failed to stage, commit, and push changes: "+err.Error(), http.StatusInternalServerError) respondError(w, "Failed to stage, commit, and push changes: "+err.Error(), http.StatusInternalServerError)
return return
} }
respondJSON(w, map[string]string{"message": "Changes staged, committed, and pushed successfully"}) respondJSON(w, CommitResponse{CommitHash: hash.String()})
} }
} }
@@ -60,8 +73,8 @@ func (h *Handler) StageCommitAndPush() http.HandlerFunc {
// @Security BearerAuth // @Security BearerAuth
// @Produce json // @Produce json
// @Param workspace_name path string true "Workspace name" // @Param workspace_name path string true "Workspace name"
// @Success 200 {object} map[string]string // @Success 200 {object} PullResponse
// @Failure 500 {string} string "Failed to pull changes" // @Failure 500 {object} ErrorResponse "Failed to pull changes"
// @Router /workspaces/{workspace_name}/git/pull [post] // @Router /workspaces/{workspace_name}/git/pull [post]
func (h *Handler) PullChanges() http.HandlerFunc { func (h *Handler) PullChanges() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
@@ -72,10 +85,10 @@ func (h *Handler) PullChanges() http.HandlerFunc {
err := h.Storage.Pull(ctx.UserID, ctx.Workspace.ID) err := h.Storage.Pull(ctx.UserID, ctx.Workspace.ID)
if err != nil { if err != nil {
http.Error(w, "Failed to pull changes: "+err.Error(), http.StatusInternalServerError) respondError(w, "Failed to pull changes: "+err.Error(), http.StatusInternalServerError)
return return
} }
respondJSON(w, map[string]string{"message": "Pulled changes from remote"}) respondJSON(w, PullResponse{Message: "Successfully pulled changes from remote"})
} }
} }

View File

@@ -56,7 +56,7 @@ func TestGitHandlers_Integration(t *testing.T) {
var response map[string]string var response map[string]string
err := json.NewDecoder(rr.Body).Decode(&response) err := json.NewDecoder(rr.Body).Decode(&response)
require.NoError(t, err) require.NoError(t, err)
assert.Contains(t, response["message"], "successfully") require.Contains(t, response, "commitHash")
// Verify mock was called correctly // Verify mock was called correctly
assert.Equal(t, 1, h.MockGit.GetCommitCount(), "Commit should be called once") assert.Equal(t, 1, h.MockGit.GetCommitCount(), "Commit should be called once")
@@ -100,7 +100,7 @@ func TestGitHandlers_Integration(t *testing.T) {
var response map[string]string var response map[string]string
err := json.NewDecoder(rr.Body).Decode(&response) err := json.NewDecoder(rr.Body).Decode(&response)
require.NoError(t, err) require.NoError(t, err)
assert.Contains(t, response["message"], "Pulled changes") assert.Contains(t, response["message"], "Successfully pulled changes")
assert.Equal(t, 1, h.MockGit.GetPullCount(), "Pull should be called once") assert.Equal(t, 1, h.MockGit.GetPullCount(), "Pull should be called once")
}) })

View File

@@ -7,6 +7,11 @@ import (
"novamd/internal/storage" "novamd/internal/storage"
) )
// ErrorResponse is a generic error response
type ErrorResponse struct {
Message string `json:"message"`
}
// Handler provides common functionality for all handlers // Handler provides common functionality for all handlers
type Handler struct { type Handler struct {
DB db.Database DB db.Database
@@ -25,6 +30,12 @@ func NewHandler(db db.Database, s storage.Manager) *Handler {
func respondJSON(w http.ResponseWriter, data interface{}) { func respondJSON(w http.ResponseWriter, data interface{}) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(data); err != nil { if err := json.NewEncoder(w).Encode(data); err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError) respondError(w, "Failed to encode response", http.StatusInternalServerError)
} }
} }
// respondError is a helper to send error responses
func respondError(w http.ResponseWriter, message string, code int) {
w.WriteHeader(code)
respondJSON(w, ErrorResponse{Message: message})
}

View File

@@ -4,6 +4,7 @@ package handlers_test
import ( import (
"fmt" "fmt"
"novamd/internal/git"
) )
// MockGitClient implements the git.Client interface for testing // MockGitClient implements the git.Client interface for testing
@@ -51,13 +52,13 @@ func (m *MockGitClient) Pull() error {
} }
// Commit implements git.Client // Commit implements git.Client
func (m *MockGitClient) Commit(message string) error { func (m *MockGitClient) Commit(message string) (git.CommitHash, error) {
if m.error != nil { if m.error != nil {
return m.error return git.CommitHash{}, m.error
} }
m.commitCount++ m.commitCount++
m.lastCommitMsg = message m.lastCommitMsg = message
return nil return git.CommitHash{}, nil
} }
// Push implements git.Client // Push implements git.Client

View File

@@ -28,7 +28,7 @@ func (h *StaticHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Security check to prevent directory traversal // Security check to prevent directory traversal
if !strings.HasPrefix(cleanPath, h.staticPath) { if !strings.HasPrefix(cleanPath, h.staticPath) {
http.Error(w, "Invalid path", http.StatusBadRequest) respondError(w, "Invalid path", http.StatusBadRequest)
return return
} }

View File

@@ -32,15 +32,15 @@ type DeleteAccountRequest struct {
// @Produce json // @Produce json
// @Param body body UpdateProfileRequest true "Profile update request" // @Param body body UpdateProfileRequest true "Profile update request"
// @Success 200 {object} models.User // @Success 200 {object} models.User
// @Failure 400 {string} string "Invalid request body" // @Failure 400 {object} ErrorResponse "Invalid request body"
// @Failure 400 {string} string "Current password is required to change password" // @Failure 400 {object} ErrorResponse "Current password is required to change password"
// @Failure 400 {string} string "New password must be at least 8 characters long" // @Failure 400 {object} ErrorResponse "New password must be at least 8 characters long"
// @Failure 400 {string} string "Current password is required to change email" // @Failure 400 {object} ErrorResponse "Current password is required to change email"
// @Failure 401 {string} string "Current password is incorrect" // @Failure 401 {object} ErrorResponse "Current password is incorrect"
// @Failure 404 {string} string "User not found" // @Failure 404 {object} ErrorResponse "User not found"
// @Failure 409 {string} string "Email already in use" // @Failure 409 {object} ErrorResponse "Email already in use"
// @Failure 500 {string} string "Failed to process new password" // @Failure 500 {object} ErrorResponse "Failed to process new password"
// @Failure 500 {string} string "Failed to update profile" // @Failure 500 {object} ErrorResponse "Failed to update profile"
// @Router /profile [put] // @Router /profile [put]
func (h *Handler) UpdateProfile() http.HandlerFunc { func (h *Handler) UpdateProfile() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
@@ -51,14 +51,14 @@ func (h *Handler) UpdateProfile() http.HandlerFunc {
var req UpdateProfileRequest var req UpdateProfileRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest) respondError(w, "Invalid request body", http.StatusBadRequest)
return return
} }
// Get current user // Get current user
user, err := h.DB.GetUserByID(ctx.UserID) user, err := h.DB.GetUserByID(ctx.UserID)
if err != nil { if err != nil {
http.Error(w, "User not found", http.StatusNotFound) respondError(w, "User not found", http.StatusNotFound)
return return
} }
@@ -66,26 +66,26 @@ func (h *Handler) UpdateProfile() http.HandlerFunc {
if req.NewPassword != "" { if req.NewPassword != "" {
// Current password must be provided to change password // Current password must be provided to change password
if req.CurrentPassword == "" { if req.CurrentPassword == "" {
http.Error(w, "Current password is required to change password", http.StatusBadRequest) respondError(w, "Current password is required to change password", http.StatusBadRequest)
return return
} }
// Verify current password // Verify current password
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.CurrentPassword)); err != nil { if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.CurrentPassword)); err != nil {
http.Error(w, "Current password is incorrect", http.StatusUnauthorized) respondError(w, "Current password is incorrect", http.StatusUnauthorized)
return return
} }
// Validate new password // Validate new password
if len(req.NewPassword) < 8 { if len(req.NewPassword) < 8 {
http.Error(w, "New password must be at least 8 characters long", http.StatusBadRequest) respondError(w, "New password must be at least 8 characters long", http.StatusBadRequest)
return return
} }
// Hash new password // Hash new password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost) hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
if err != nil { if err != nil {
http.Error(w, "Failed to process new password", http.StatusInternalServerError) respondError(w, "Failed to process new password", http.StatusInternalServerError)
return return
} }
user.PasswordHash = string(hashedPassword) user.PasswordHash = string(hashedPassword)
@@ -95,14 +95,14 @@ func (h *Handler) UpdateProfile() http.HandlerFunc {
if req.Email != "" && req.Email != user.Email { if req.Email != "" && req.Email != user.Email {
// Check if email change requires password verification // Check if email change requires password verification
if req.CurrentPassword == "" { if req.CurrentPassword == "" {
http.Error(w, "Current password is required to change email", http.StatusBadRequest) respondError(w, "Current password is required to change email", http.StatusBadRequest)
return return
} }
// Verify current password if not already verified for password change // Verify current password if not already verified for password change
if req.NewPassword == "" { if req.NewPassword == "" {
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.CurrentPassword)); err != nil { if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.CurrentPassword)); err != nil {
http.Error(w, "Current password is incorrect", http.StatusUnauthorized) respondError(w, "Current password is incorrect", http.StatusUnauthorized)
return return
} }
} }
@@ -110,7 +110,7 @@ func (h *Handler) UpdateProfile() http.HandlerFunc {
// Check if new email is already in use // Check if new email is already in use
existingUser, err := h.DB.GetUserByEmail(req.Email) existingUser, err := h.DB.GetUserByEmail(req.Email)
if err == nil && existingUser.ID != user.ID { if err == nil && existingUser.ID != user.ID {
http.Error(w, "Email already in use", http.StatusConflict) respondError(w, "Email already in use", http.StatusConflict)
return return
} }
user.Email = req.Email user.Email = req.Email
@@ -123,7 +123,7 @@ func (h *Handler) UpdateProfile() http.HandlerFunc {
// Update user in database // Update user in database
if err := h.DB.UpdateUser(user); err != nil { if err := h.DB.UpdateUser(user); err != nil {
http.Error(w, "Failed to update profile", http.StatusInternalServerError) respondError(w, "Failed to update profile", http.StatusInternalServerError)
return return
} }
@@ -141,13 +141,13 @@ func (h *Handler) UpdateProfile() http.HandlerFunc {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param body body DeleteAccountRequest true "Account deletion request" // @Param body body DeleteAccountRequest true "Account deletion request"
// @Success 200 {object} map[string]string // @Success 204 "No Content - Account deleted successfully"
// @Failure 400 {string} string "Invalid request body" // @Failure 400 {object} ErrorResponse "Invalid request body"
// @Failure 401 {string} string "Password is incorrect" // @Failure 401 {object} ErrorResponse "Password is incorrect"
// @Failure 403 {string} string "Cannot delete the last admin account" // @Failure 403 {object} ErrorResponse "Cannot delete the last admin account"
// @Failure 404 {string} string "User not found" // @Failure 404 {object} ErrorResponse "User not found"
// @Failure 500 {string} string "Failed to verify admin status" // @Failure 500 {object} ErrorResponse "Failed to verify admin status"
// @Failure 500 {string} string "Failed to delete account" // @Failure 500 {object} ErrorResponse "Failed to delete account"
// @Router /profile [delete] // @Router /profile [delete]
func (h *Handler) DeleteAccount() http.HandlerFunc { func (h *Handler) DeleteAccount() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
@@ -158,20 +158,20 @@ func (h *Handler) DeleteAccount() http.HandlerFunc {
var req DeleteAccountRequest var req DeleteAccountRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest) respondError(w, "Invalid request body", http.StatusBadRequest)
return return
} }
// Get current user // Get current user
user, err := h.DB.GetUserByID(ctx.UserID) user, err := h.DB.GetUserByID(ctx.UserID)
if err != nil { if err != nil {
http.Error(w, "User not found", http.StatusNotFound) respondError(w, "User not found", http.StatusNotFound)
return return
} }
// Verify password // Verify password
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil { if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil {
http.Error(w, "Password is incorrect", http.StatusUnauthorized) respondError(w, "Password is incorrect", http.StatusUnauthorized)
return return
} }
@@ -180,11 +180,11 @@ func (h *Handler) DeleteAccount() http.HandlerFunc {
// Count number of admin users // Count number of admin users
adminCount, err := h.DB.CountAdminUsers() adminCount, err := h.DB.CountAdminUsers()
if err != nil { if err != nil {
http.Error(w, "Failed to verify admin status", http.StatusInternalServerError) respondError(w, "Failed to verify admin status", http.StatusInternalServerError)
return return
} }
if adminCount <= 1 { if adminCount <= 1 {
http.Error(w, "Cannot delete the last admin account", http.StatusForbidden) respondError(w, "Cannot delete the last admin account", http.StatusForbidden)
return return
} }
} }
@@ -192,24 +192,24 @@ func (h *Handler) DeleteAccount() http.HandlerFunc {
// Get user's workspaces for cleanup // Get user's workspaces for cleanup
workspaces, err := h.DB.GetWorkspacesByUserID(ctx.UserID) workspaces, err := h.DB.GetWorkspacesByUserID(ctx.UserID)
if err != nil { if err != nil {
http.Error(w, "Failed to get user workspaces", http.StatusInternalServerError) respondError(w, "Failed to get user workspaces", http.StatusInternalServerError)
return return
} }
// Delete workspace directories // Delete workspace directories
for _, workspace := range workspaces { for _, workspace := range workspaces {
if err := h.Storage.DeleteUserWorkspace(ctx.UserID, workspace.ID); err != nil { if err := h.Storage.DeleteUserWorkspace(ctx.UserID, workspace.ID); err != nil {
http.Error(w, "Failed to delete workspace files", http.StatusInternalServerError) respondError(w, "Failed to delete workspace files", http.StatusInternalServerError)
return return
} }
} }
// Delete user from database (this will cascade delete workspaces and sessions) // Delete user from database (this will cascade delete workspaces and sessions)
if err := h.DB.DeleteUser(ctx.UserID); err != nil { if err := h.DB.DeleteUser(ctx.UserID); err != nil {
http.Error(w, "Failed to delete account", http.StatusInternalServerError) respondError(w, "Failed to delete account", http.StatusInternalServerError)
return return
} }
respondJSON(w, map[string]string{"message": "Account deleted successfully"}) w.WriteHeader(http.StatusNoContent)
} }
} }

View File

@@ -191,7 +191,7 @@ func TestUserHandlers_Integration(t *testing.T) {
} }
rr := h.makeRequest(t, http.MethodDelete, "/api/v1/profile", deleteReq, userToken, nil) rr := h.makeRequest(t, http.MethodDelete, "/api/v1/profile", deleteReq, userToken, nil)
require.Equal(t, http.StatusOK, rr.Code) require.Equal(t, http.StatusNoContent, rr.Code)
// Verify user is deleted // Verify user is deleted
rr = h.makeRequest(t, http.MethodPost, "/api/v1/auth/login", loginReq, "", nil) rr = h.makeRequest(t, http.MethodPost, "/api/v1/auth/login", loginReq, "", nil)

View File

@@ -9,6 +9,16 @@ import (
"novamd/internal/models" "novamd/internal/models"
) )
// DeleteWorkspaceResponse contains the name of the next workspace after deleting the current one
type DeleteWorkspaceResponse struct {
NextWorkspaceName string `json:"nextWorkspaceName"`
}
// GetLastWorkspaceNameResponse contains the name of the last opened workspace
type GetLastWorkspaceNameResponse struct {
LastWorkspaceName string `json:"lastWorkspaceName"`
}
// ListWorkspaces godoc // ListWorkspaces godoc
// @Summary List workspaces // @Summary List workspaces
// @Description Lists all workspaces for the current user // @Description Lists all workspaces for the current user
@@ -17,7 +27,7 @@ import (
// @Security BearerAuth // @Security BearerAuth
// @Produce json // @Produce json
// @Success 200 {array} models.Workspace // @Success 200 {array} models.Workspace
// @Failure 500 {string} string "Failed to list workspaces" // @Failure 500 {object} ErrorResponse "Failed to list workspaces"
// @Router /workspaces [get] // @Router /workspaces [get]
func (h *Handler) ListWorkspaces() http.HandlerFunc { func (h *Handler) ListWorkspaces() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
@@ -28,7 +38,7 @@ func (h *Handler) ListWorkspaces() http.HandlerFunc {
workspaces, err := h.DB.GetWorkspacesByUserID(ctx.UserID) workspaces, err := h.DB.GetWorkspacesByUserID(ctx.UserID)
if err != nil { if err != nil {
http.Error(w, "Failed to list workspaces", http.StatusInternalServerError) respondError(w, "Failed to list workspaces", http.StatusInternalServerError)
return return
} }
@@ -46,11 +56,11 @@ func (h *Handler) ListWorkspaces() http.HandlerFunc {
// @Produce json // @Produce json
// @Param body body models.Workspace true "Workspace" // @Param body body models.Workspace true "Workspace"
// @Success 200 {object} models.Workspace // @Success 200 {object} models.Workspace
// @Failure 400 {string} string "Invalid request body" // @Failure 400 {object} ErrorResponse "Invalid request body"
// @Failure 400 {string} string "Invalid workspace" // @Failure 400 {object} ErrorResponse "Invalid workspace"
// @Failure 500 {string} string "Failed to create workspace" // @Failure 500 {object} ErrorResponse "Failed to create workspace"
// @Failure 500 {string} string "Failed to initialize workspace directory" // @Failure 500 {object} ErrorResponse "Failed to initialize workspace directory"
// @Failure 500 {string} string "Failed to setup git repo" // @Failure 500 {object} ErrorResponse "Failed to setup git repo"
// @Router /workspaces [post] // @Router /workspaces [post]
func (h *Handler) CreateWorkspace() http.HandlerFunc { func (h *Handler) CreateWorkspace() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
@@ -61,23 +71,23 @@ func (h *Handler) CreateWorkspace() http.HandlerFunc {
var workspace models.Workspace var workspace models.Workspace
if err := json.NewDecoder(r.Body).Decode(&workspace); err != nil { if err := json.NewDecoder(r.Body).Decode(&workspace); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest) respondError(w, "Invalid request body", http.StatusBadRequest)
return return
} }
if err := workspace.ValidateGitSettings(); err != nil { if err := workspace.ValidateGitSettings(); err != nil {
http.Error(w, "Invalid workspace", http.StatusBadRequest) respondError(w, "Invalid workspace", http.StatusBadRequest)
return return
} }
workspace.UserID = ctx.UserID workspace.UserID = ctx.UserID
if err := h.DB.CreateWorkspace(&workspace); err != nil { if err := h.DB.CreateWorkspace(&workspace); err != nil {
http.Error(w, "Failed to create workspace", http.StatusInternalServerError) respondError(w, "Failed to create workspace", http.StatusInternalServerError)
return return
} }
if err := h.Storage.InitializeUserWorkspace(workspace.UserID, workspace.ID); err != nil { if err := h.Storage.InitializeUserWorkspace(workspace.UserID, workspace.ID); err != nil {
http.Error(w, "Failed to initialize workspace directory", http.StatusInternalServerError) respondError(w, "Failed to initialize workspace directory", http.StatusInternalServerError)
return return
} }
@@ -91,7 +101,7 @@ func (h *Handler) CreateWorkspace() http.HandlerFunc {
workspace.GitCommitName, workspace.GitCommitName,
workspace.GitCommitEmail, workspace.GitCommitEmail,
); err != nil { ); err != nil {
http.Error(w, "Failed to setup git repo: "+err.Error(), http.StatusInternalServerError) respondError(w, "Failed to setup git repo: "+err.Error(), http.StatusInternalServerError)
return return
} }
} }
@@ -109,7 +119,7 @@ func (h *Handler) CreateWorkspace() http.HandlerFunc {
// @Produce json // @Produce json
// @Param workspace_name path string true "Workspace name" // @Param workspace_name path string true "Workspace name"
// @Success 200 {object} models.Workspace // @Success 200 {object} models.Workspace
// @Failure 500 {string} string "Failed to get workspace" // @Failure 500 {object} ErrorResponse "Internal server error"
// @Router /workspaces/{workspace_name} [get] // @Router /workspaces/{workspace_name} [get]
func (h *Handler) GetWorkspace() http.HandlerFunc { func (h *Handler) GetWorkspace() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
@@ -151,9 +161,9 @@ func gitSettingsChanged(new, old *models.Workspace) bool {
// @Param workspace_name path string true "Workspace name" // @Param workspace_name path string true "Workspace name"
// @Param body body models.Workspace true "Workspace" // @Param body body models.Workspace true "Workspace"
// @Success 200 {object} models.Workspace // @Success 200 {object} models.Workspace
// @Failure 400 {string} string "Invalid request body" // @Failure 400 {object} ErrorResponse "Invalid request body"
// @Failure 500 {string} string "Failed to update workspace" // @Failure 500 {object} ErrorResponse "Failed to update workspace"
// @Failure 500 {string} string "Failed to setup git repo" // @Failure 500 {object} ErrorResponse "Failed to setup git repo"
// @Router /workspaces/{workspace_name} [put] // @Router /workspaces/{workspace_name} [put]
func (h *Handler) UpdateWorkspace() http.HandlerFunc { func (h *Handler) UpdateWorkspace() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
@@ -164,7 +174,7 @@ func (h *Handler) UpdateWorkspace() http.HandlerFunc {
var workspace models.Workspace var workspace models.Workspace
if err := json.NewDecoder(r.Body).Decode(&workspace); err != nil { if err := json.NewDecoder(r.Body).Decode(&workspace); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest) respondError(w, "Invalid request body", http.StatusBadRequest)
return return
} }
@@ -174,7 +184,7 @@ func (h *Handler) UpdateWorkspace() http.HandlerFunc {
// Validate the workspace // Validate the workspace
if err := workspace.Validate(); err != nil { if err := workspace.Validate(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) respondError(w, err.Error(), http.StatusBadRequest)
return return
} }
@@ -190,7 +200,7 @@ func (h *Handler) UpdateWorkspace() http.HandlerFunc {
workspace.GitCommitName, workspace.GitCommitName,
workspace.GitCommitEmail, workspace.GitCommitEmail,
); err != nil { ); err != nil {
http.Error(w, "Failed to setup git repo: "+err.Error(), http.StatusInternalServerError) respondError(w, "Failed to setup git repo: "+err.Error(), http.StatusInternalServerError)
return return
} }
@@ -200,7 +210,7 @@ func (h *Handler) UpdateWorkspace() http.HandlerFunc {
} }
if err := h.DB.UpdateWorkspace(&workspace); err != nil { if err := h.DB.UpdateWorkspace(&workspace); err != nil {
http.Error(w, "Failed to update workspace", http.StatusInternalServerError) respondError(w, "Failed to update workspace", http.StatusInternalServerError)
return return
} }
@@ -216,14 +226,14 @@ func (h *Handler) UpdateWorkspace() http.HandlerFunc {
// @Security BearerAuth // @Security BearerAuth
// @Produce json // @Produce json
// @Param workspace_name path string true "Workspace name" // @Param workspace_name path string true "Workspace name"
// @Success 200 {object} map[string]string // @Success 200 {object} DeleteWorkspaceResponse
// @Failure 400 {string} string "Cannot delete the last workspace" // @Failure 400 {object} ErrorResponse "Cannot delete the last workspace"
// @Failure 500 {string} string "Failed to get workspaces" // @Failure 500 {object} ErrorResponse "Failed to get workspaces"
// @Failure 500 {string} string "Failed to start transaction" // @Failure 500 {object} ErrorResponse "Failed to start transaction"
// @Failure 500 {string} string "Failed to update last workspace" // @Failure 500 {object} ErrorResponse "Failed to update last workspace"
// @Failure 500 {string} string "Failed to delete workspace" // @Failure 500 {object} ErrorResponse "Failed to delete workspace"
// @Failure 500 {string} string "Failed to rollback transaction" // @Failure 500 {object} ErrorResponse "Failed to rollback transaction"
// @Failure 500 {string} string "Failed to commit transaction" // @Failure 500 {object} ErrorResponse "Failed to commit transaction"
// @Router /workspaces/{workspace_name} [delete] // @Router /workspaces/{workspace_name} [delete]
func (h *Handler) DeleteWorkspace() http.HandlerFunc { func (h *Handler) DeleteWorkspace() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
@@ -235,12 +245,12 @@ func (h *Handler) DeleteWorkspace() http.HandlerFunc {
// Check if this is the user's last workspace // Check if this is the user's last workspace
workspaces, err := h.DB.GetWorkspacesByUserID(ctx.UserID) workspaces, err := h.DB.GetWorkspacesByUserID(ctx.UserID)
if err != nil { if err != nil {
http.Error(w, "Failed to get workspaces", http.StatusInternalServerError) respondError(w, "Failed to get workspaces", http.StatusInternalServerError)
return return
} }
if len(workspaces) <= 1 { if len(workspaces) <= 1 {
http.Error(w, "Cannot delete the last workspace", http.StatusBadRequest) respondError(w, "Cannot delete the last workspace", http.StatusBadRequest)
return return
} }
@@ -258,37 +268,37 @@ func (h *Handler) DeleteWorkspace() http.HandlerFunc {
// Start transaction // Start transaction
tx, err := h.DB.Begin() tx, err := h.DB.Begin()
if err != nil { if err != nil {
http.Error(w, "Failed to start transaction", http.StatusInternalServerError) respondError(w, "Failed to start transaction", http.StatusInternalServerError)
return return
} }
defer func() { defer func() {
if err := tx.Rollback(); err != nil && err != sql.ErrTxDone { if err := tx.Rollback(); err != nil && err != sql.ErrTxDone {
http.Error(w, "Failed to rollback transaction", http.StatusInternalServerError) respondError(w, "Failed to rollback transaction", http.StatusInternalServerError)
} }
}() }()
// Update last workspace ID first // Update last workspace ID first
err = h.DB.UpdateLastWorkspaceTx(tx, ctx.UserID, nextWorkspaceID) err = h.DB.UpdateLastWorkspaceTx(tx, ctx.UserID, nextWorkspaceID)
if err != nil { if err != nil {
http.Error(w, "Failed to update last workspace", http.StatusInternalServerError) respondError(w, "Failed to update last workspace", http.StatusInternalServerError)
return return
} }
// Delete the workspace // Delete the workspace
err = h.DB.DeleteWorkspaceTx(tx, ctx.Workspace.ID) err = h.DB.DeleteWorkspaceTx(tx, ctx.Workspace.ID)
if err != nil { if err != nil {
http.Error(w, "Failed to delete workspace", http.StatusInternalServerError) respondError(w, "Failed to delete workspace", http.StatusInternalServerError)
return return
} }
// Commit transaction // Commit transaction
if err = tx.Commit(); err != nil { if err = tx.Commit(); err != nil {
http.Error(w, "Failed to commit transaction", http.StatusInternalServerError) respondError(w, "Failed to commit transaction", http.StatusInternalServerError)
return return
} }
// Return the next workspace ID in the response so frontend knows where to redirect // Return the next workspace ID in the response so frontend knows where to redirect
respondJSON(w, map[string]string{"nextWorkspaceName": nextWorkspaceName}) respondJSON(w, &DeleteWorkspaceResponse{NextWorkspaceName: nextWorkspaceName})
} }
} }
@@ -299,8 +309,8 @@ func (h *Handler) DeleteWorkspace() http.HandlerFunc {
// @ID getLastWorkspaceName // @ID getLastWorkspaceName
// @Security BearerAuth // @Security BearerAuth
// @Produce json // @Produce json
// @Success 200 {object} map[string]string // @Success 200 {object} LastWorkspaceNameResponse
// @Failure 500 {string} string "Failed to get last workspace" // @Failure 500 {object} ErrorResponse "Failed to get last workspace"
// @Router /workspaces/last [get] // @Router /workspaces/last [get]
func (h *Handler) GetLastWorkspaceName() http.HandlerFunc { func (h *Handler) GetLastWorkspaceName() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
@@ -311,11 +321,11 @@ func (h *Handler) GetLastWorkspaceName() http.HandlerFunc {
workspaceName, err := h.DB.GetLastWorkspaceName(ctx.UserID) workspaceName, err := h.DB.GetLastWorkspaceName(ctx.UserID)
if err != nil { if err != nil {
http.Error(w, "Failed to get last workspace", http.StatusInternalServerError) respondError(w, "Failed to get last workspace", http.StatusInternalServerError)
return return
} }
respondJSON(w, map[string]string{"lastWorkspaceName": workspaceName}) respondJSON(w, &GetLastWorkspaceNameResponse{LastWorkspaceName: workspaceName})
} }
} }
@@ -327,9 +337,9 @@ func (h *Handler) GetLastWorkspaceName() http.HandlerFunc {
// @Security BearerAuth // @Security BearerAuth
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {object} map[string]string // @Success 204 "No Content - Last workspace updated successfully"
// @Failure 400 {string} string "Invalid request body" // @Failure 400 {object} ErrorResponse "Invalid request body"
// @Failure 500 {string} string "Failed to update last workspace" // @Failure 500 {object} ErrorResponse "Failed to update last workspace"
// @Router /workspaces/last [put] // @Router /workspaces/last [put]
func (h *Handler) UpdateLastWorkspaceName() http.HandlerFunc { func (h *Handler) UpdateLastWorkspaceName() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
@@ -343,15 +353,15 @@ func (h *Handler) UpdateLastWorkspaceName() http.HandlerFunc {
} }
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil { if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest) respondError(w, "Invalid request body", http.StatusBadRequest)
return return
} }
if err := h.DB.UpdateLastWorkspace(ctx.UserID, requestBody.WorkspaceName); err != nil { if err := h.DB.UpdateLastWorkspace(ctx.UserID, requestBody.WorkspaceName); err != nil {
http.Error(w, "Failed to update last workspace", http.StatusInternalServerError) respondError(w, "Failed to update last workspace", http.StatusInternalServerError)
return return
} }
respondJSON(w, map[string]string{"message": "Last workspace updated successfully"}) w.WriteHeader(http.StatusNoContent)
} }
} }

View File

@@ -226,7 +226,7 @@ func TestWorkspaceHandlers_Integration(t *testing.T) {
} }
rr := h.makeRequest(t, http.MethodPut, "/api/v1/workspaces/last", req, h.RegularToken, nil) rr := h.makeRequest(t, http.MethodPut, "/api/v1/workspaces/last", req, h.RegularToken, nil)
require.Equal(t, http.StatusOK, rr.Code) require.Equal(t, http.StatusNoContent, rr.Code)
// Verify the update // Verify the update
rr = h.makeRequest(t, http.MethodGet, "/api/v1/workspaces/last", nil, h.RegularToken, nil) rr = h.makeRequest(t, http.MethodGet, "/api/v1/workspaces/last", nil, h.RegularToken, nil)

View File

@@ -9,7 +9,7 @@ import (
type RepositoryManager interface { type RepositoryManager interface {
SetupGitRepo(userID, workspaceID int, gitURL, gitUser, gitToken, commitName, commitEmail string) error SetupGitRepo(userID, workspaceID int, gitURL, gitUser, gitToken, commitName, commitEmail string) error
DisableGitRepo(userID, workspaceID int) DisableGitRepo(userID, workspaceID int)
StageCommitAndPush(userID, workspaceID int, message string) error StageCommitAndPush(userID, workspaceID int, message string) (git.CommitHash, error)
Pull(userID, workspaceID int) error Pull(userID, workspaceID int) error
} }
@@ -36,17 +36,19 @@ func (s *Service) DisableGitRepo(userID, workspaceID int) {
// StageCommitAndPush stages, commit with the message, and pushes the changes to the Git repository. // StageCommitAndPush stages, commit with the message, and pushes the changes to the Git repository.
// The git repository belongs to the given userID and is associated with the given workspaceID. // The git repository belongs to the given userID and is associated with the given workspaceID.
func (s *Service) StageCommitAndPush(userID, workspaceID int, message string) error { func (s *Service) StageCommitAndPush(userID, workspaceID int, message string) (git.CommitHash, error) {
repo, ok := s.getGitRepo(userID, workspaceID) repo, ok := s.getGitRepo(userID, workspaceID)
if !ok { if !ok {
return fmt.Errorf("git settings not configured for this workspace") return git.CommitHash{}, fmt.Errorf("git settings not configured for this workspace")
} }
if err := repo.Commit(message); err != nil { hash, err := repo.Commit(message)
return err if err != nil {
return git.CommitHash{}, err
} }
return repo.Push() err = repo.Push()
return hash, err
} }
// Pull pulls the changes from the remote Git repository. // Pull pulls the changes from the remote Git repository.

View File

@@ -29,10 +29,10 @@ func (m *MockGitClient) Pull() error {
return m.ReturnError return m.ReturnError
} }
func (m *MockGitClient) Commit(message string) error { func (m *MockGitClient) Commit(message string) (git.CommitHash, error) {
m.CommitCalled = true m.CommitCalled = true
m.CommitMessage = message m.CommitMessage = message
return m.ReturnError return git.CommitHash{}, m.ReturnError
} }
func (m *MockGitClient) Push() error { func (m *MockGitClient) Push() error {
@@ -138,7 +138,7 @@ func TestGitOperations(t *testing.T) {
}) })
t.Run("operations on non-configured workspace", func(t *testing.T) { t.Run("operations on non-configured workspace", func(t *testing.T) {
err := s.StageCommitAndPush(1, 1, "test commit") _, err := s.StageCommitAndPush(1, 1, "test commit")
if err == nil { if err == nil {
t.Error("expected error for non-configured workspace, got nil") t.Error("expected error for non-configured workspace, got nil")
} }
@@ -157,7 +157,7 @@ func TestGitOperations(t *testing.T) {
s.GitRepos[1][1] = mockClient s.GitRepos[1][1] = mockClient
// Test commit and push // Test commit and push
err := s.StageCommitAndPush(1, 1, "test commit") _, err := s.StageCommitAndPush(1, 1, "test commit")
if err != nil { if err != nil {
t.Errorf("unexpected error: %v", err) t.Errorf("unexpected error: %v", err)
} }
@@ -189,7 +189,7 @@ func TestGitOperations(t *testing.T) {
s.GitRepos[1][1] = mockClient s.GitRepos[1][1] = mockClient
// Test commit error // Test commit error
err := s.StageCommitAndPush(1, 1, "test commit") _, err := s.StageCommitAndPush(1, 1, "test commit")
if err == nil { if err == nil {
t.Error("expected error for commit, got nil") t.Error("expected error for commit, got nil")
} }