mirror of
https://github.com/lordmathis/lemma.git
synced 2025-11-05 15:44:21 +00:00
Update api docs
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"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/transport/http"
|
||||
)
|
||||
@@ -26,11 +27,19 @@ type Config struct {
|
||||
type Client interface {
|
||||
Clone() error
|
||||
Pull() error
|
||||
Commit(message string) error
|
||||
Commit(message string) (CommitHash, error)
|
||||
Push() 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
|
||||
type client struct {
|
||||
Config
|
||||
@@ -101,22 +110,22 @@ func (c *client) Pull() error {
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return fmt.Errorf("repository not initialized")
|
||||
return CommitHash(plumbing.ZeroHash), fmt.Errorf("repository not initialized")
|
||||
}
|
||||
|
||||
w, err := c.repo.Worktree()
|
||||
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(".")
|
||||
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{
|
||||
Name: c.CommitName,
|
||||
Email: c.CommitEmail,
|
||||
@@ -124,10 +133,10 @@ func (c *client) Commit(message string) error {
|
||||
},
|
||||
})
|
||||
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
|
||||
|
||||
@@ -55,13 +55,13 @@ type SystemStats struct {
|
||||
// @ID adminListUsers
|
||||
// @Produce json
|
||||
// @Success 200 {array} models.User
|
||||
// @Failure 500 {string} "Failed to list users"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to list users"
|
||||
// @Router /admin/users [get]
|
||||
func (h *Handler) AdminListUsers() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, _ *http.Request) {
|
||||
users, err := h.DB.GetAllUsers()
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to list users", http.StatusInternalServerError)
|
||||
respondError(w, "Failed to list users", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -79,45 +79,45 @@ func (h *Handler) AdminListUsers() http.HandlerFunc {
|
||||
// @Produce json
|
||||
// @Param user body CreateUserRequest true "User details"
|
||||
// @Success 200 {object} models.User
|
||||
// @Failure 400 {string} "Invalid request body"
|
||||
// @Failure 400 {string} "Email, password, and role are required"
|
||||
// @Failure 400 {string} "Password must be at least 8 characters"
|
||||
// @Failure 409 {string} "Email already exists"
|
||||
// @Failure 500 {string} "Failed to hash password"
|
||||
// @Failure 500 {string} "Failed to create user"
|
||||
// @Failure 500 {string} "Failed to initialize user workspace"
|
||||
// @Failure 400 {object} ErrorResponse "Invalid request body"
|
||||
// @Failure 400 {object} ErrorResponse "Email, password, and role are required"
|
||||
// @Failure 400 {object} ErrorResponse "Password must be at least 8 characters"
|
||||
// @Failure 409 {object} ErrorResponse "Email already exists"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to hash password"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to create user"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to initialize user workspace"
|
||||
// @Router /admin/users [post]
|
||||
func (h *Handler) AdminCreateUser() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req CreateUserRequest
|
||||
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
|
||||
}
|
||||
|
||||
// Validate request
|
||||
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
|
||||
}
|
||||
|
||||
// Check if email already exists
|
||||
existingUser, err := h.DB.GetUserByEmail(req.Email)
|
||||
if err == nil && existingUser != nil {
|
||||
http.Error(w, "Email already exists", http.StatusConflict)
|
||||
respondError(w, "Email already exists", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if password is long enough
|
||||
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
|
||||
}
|
||||
|
||||
// Hash password
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to hash password", http.StatusInternalServerError)
|
||||
respondError(w, "Failed to hash password", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -131,13 +131,13 @@ func (h *Handler) AdminCreateUser() http.HandlerFunc {
|
||||
|
||||
insertedUser, err := h.DB.CreateUser(user)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to create user", http.StatusInternalServerError)
|
||||
respondError(w, "Failed to create user", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Initialize user workspace
|
||||
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
|
||||
}
|
||||
|
||||
@@ -154,20 +154,20 @@ func (h *Handler) AdminCreateUser() http.HandlerFunc {
|
||||
// @Produce json
|
||||
// @Param userId path int true "User ID"
|
||||
// @Success 200 {object} models.User
|
||||
// @Failure 400 {string} "Invalid user ID"
|
||||
// @Failure 404 {string} "User not found"
|
||||
// @Failure 400 {object} ErrorResponse "Invalid user ID"
|
||||
// @Failure 404 {object} ErrorResponse "User not found"
|
||||
// @Router /admin/users/{userId} [get]
|
||||
func (h *Handler) AdminGetUser() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
userID, err := strconv.Atoi(chi.URLParam(r, "userId"))
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid user ID", http.StatusBadRequest)
|
||||
respondError(w, "Invalid user ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.DB.GetUserByID(userID)
|
||||
if err != nil {
|
||||
http.Error(w, "User not found", http.StatusNotFound)
|
||||
respondError(w, "User not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -186,30 +186,30 @@ func (h *Handler) AdminGetUser() http.HandlerFunc {
|
||||
// @Param userId path int true "User ID"
|
||||
// @Param user body UpdateUserRequest true "User details"
|
||||
// @Success 200 {object} models.User
|
||||
// @Failure 400 {string} "Invalid user ID"
|
||||
// @Failure 400 {string} "Invalid request body"
|
||||
// @Failure 404 {string} "User not found"
|
||||
// @Failure 500 {string} "Failed to hash password"
|
||||
// @Failure 500 {string} "Failed to update user"
|
||||
// @Failure 400 {object} ErrorResponse "Invalid user ID"
|
||||
// @Failure 400 {object} ErrorResponse "Invalid request body"
|
||||
// @Failure 404 {object} ErrorResponse "User not found"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to hash password"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to update user"
|
||||
// @Router /admin/users/{userId} [put]
|
||||
func (h *Handler) AdminUpdateUser() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
userID, err := strconv.Atoi(chi.URLParam(r, "userId"))
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid user ID", http.StatusBadRequest)
|
||||
respondError(w, "Invalid user ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Get existing user
|
||||
user, err := h.DB.GetUserByID(userID)
|
||||
if err != nil {
|
||||
http.Error(w, "User not found", http.StatusNotFound)
|
||||
respondError(w, "User not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdateUserRequest
|
||||
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
|
||||
}
|
||||
|
||||
@@ -226,14 +226,14 @@ func (h *Handler) AdminUpdateUser() http.HandlerFunc {
|
||||
if req.Password != "" {
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to hash password", http.StatusInternalServerError)
|
||||
respondError(w, "Failed to hash password", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
user.PasswordHash = string(hashedPassword)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -249,11 +249,11 @@ func (h *Handler) AdminUpdateUser() http.HandlerFunc {
|
||||
// @ID adminDeleteUser
|
||||
// @Param userId path int true "User ID"
|
||||
// @Success 204 "No Content"
|
||||
// @Failure 400 {string} "Invalid user ID"
|
||||
// @Failure 400 {string} "Cannot delete your own account"
|
||||
// @Failure 403 {string} "Cannot delete other admin users"
|
||||
// @Failure 404 {string} "User not found"
|
||||
// @Failure 500 {string} "Failed to delete user"
|
||||
// @Failure 400 {object} ErrorResponse "Invalid user ID"
|
||||
// @Failure 400 {object} ErrorResponse "Cannot delete your own account"
|
||||
// @Failure 403 {object} ErrorResponse "Cannot delete other admin users"
|
||||
// @Failure 404 {object} ErrorResponse "User not found"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to delete user"
|
||||
// @Router /admin/users/{userId} [delete]
|
||||
func (h *Handler) AdminDeleteUser() http.HandlerFunc {
|
||||
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"))
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid user ID", http.StatusBadRequest)
|
||||
respondError(w, "Invalid user ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Prevent admin from deleting themselves
|
||||
if userID == ctx.UserID {
|
||||
http.Error(w, "Cannot delete your own account", http.StatusBadRequest)
|
||||
respondError(w, "Cannot delete your own account", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Get user before deletion to check role
|
||||
user, err := h.DB.GetUserByID(userID)
|
||||
if err != nil {
|
||||
http.Error(w, "User not found", http.StatusNotFound)
|
||||
respondError(w, "User not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Prevent deletion of other admin users
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -304,15 +304,15 @@ func (h *Handler) AdminDeleteUser() http.HandlerFunc {
|
||||
// @ID adminListWorkspaces
|
||||
// @Produce json
|
||||
// @Success 200 {array} WorkspaceStats
|
||||
// @Failure 500 {string} "Failed to list workspaces"
|
||||
// @Failure 500 {string} "Failed to get user"
|
||||
// @Failure 500 {string} "Failed to get file stats"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to list workspaces"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to get user"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to get file stats"
|
||||
// @Router /admin/workspaces [get]
|
||||
func (h *Handler) AdminListWorkspaces() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, _ *http.Request) {
|
||||
workspaces, err := h.DB.GetAllWorkspaces()
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to list workspaces", http.StatusInternalServerError)
|
||||
respondError(w, "Failed to list workspaces", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -324,7 +324,7 @@ func (h *Handler) AdminListWorkspaces() http.HandlerFunc {
|
||||
|
||||
user, err := h.DB.GetUserByID(ws.UserID)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get user", http.StatusInternalServerError)
|
||||
respondError(w, "Failed to get user", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -336,7 +336,7 @@ func (h *Handler) AdminListWorkspaces() http.HandlerFunc {
|
||||
|
||||
fileStats, err := h.Storage.GetFileStats(ws.UserID, ws.ID)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get file stats", http.StatusInternalServerError)
|
||||
respondError(w, "Failed to get file stats", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -357,20 +357,20 @@ func (h *Handler) AdminListWorkspaces() http.HandlerFunc {
|
||||
// @ID adminGetSystemStats
|
||||
// @Produce json
|
||||
// @Success 200 {object} SystemStats
|
||||
// @Failure 500 {string} "Failed to get user stats"
|
||||
// @Failure 500 {string} "Failed to get file stats"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to get user stats"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to get file stats"
|
||||
// @Router /admin/stats [get]
|
||||
func (h *Handler) AdminGetSystemStats() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, _ *http.Request) {
|
||||
userStats, err := h.DB.GetSystemStats()
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get user stats", http.StatusInternalServerError)
|
||||
respondError(w, "Failed to get user stats", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
fileStats, err := h.Storage.GetTotalFileStats()
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get file stats", http.StatusInternalServerError)
|
||||
respondError(w, "Failed to get file stats", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -43,43 +43,43 @@ type RefreshResponse struct {
|
||||
// @Produce json
|
||||
// @Param body body LoginRequest true "Login request"
|
||||
// @Success 200 {object} LoginResponse
|
||||
// @Failure 400 {string} string "Invalid request body"
|
||||
// @Failure 400 {string} string "Email and password are required"
|
||||
// @Failure 401 {string} string "Invalid credentials"
|
||||
// @Failure 500 {string} string "Failed to create session"
|
||||
// @Failure 400 {object} ErrorResponse "Invalid request body"
|
||||
// @Failure 400 {object} ErrorResponse "Email and password are required"
|
||||
// @Failure 401 {object} ErrorResponse "Invalid credentials"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to create session"
|
||||
// @Router /auth/login [post]
|
||||
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 {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
respondError(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate request
|
||||
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
|
||||
}
|
||||
|
||||
// Get user from database
|
||||
user, err := h.DB.GetUserByEmail(req.Email)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
|
||||
respondError(w, "Invalid credentials", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify password
|
||||
err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password))
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
|
||||
respondError(w, "Invalid credentials", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Create session and generate tokens
|
||||
session, accessToken, err := authService.CreateSession(user.ID, string(user.Role))
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to create session", http.StatusInternalServerError)
|
||||
respondError(w, "Failed to create session", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -101,25 +101,25 @@ func (h *Handler) Login(authService *auth.SessionService) http.HandlerFunc {
|
||||
// @Tags auth
|
||||
// @ID logout
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {string} string "OK"
|
||||
// @Failure 400 {string} string "Session ID required"
|
||||
// @Failure 500 {string} string "Failed to logout"
|
||||
// @Success 204 "No Content"
|
||||
// @Failure 400 {object} ErrorResponse "Session ID required"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to logout"
|
||||
// @Router /auth/logout [post]
|
||||
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 == "" {
|
||||
http.Error(w, "Session ID required", http.StatusBadRequest)
|
||||
respondError(w, "Session ID required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err := authService.InvalidateSession(sessionID)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to logout", http.StatusInternalServerError)
|
||||
respondError(w, "Failed to logout", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,27 +132,27 @@ func (h *Handler) Logout(authService *auth.SessionService) http.HandlerFunc {
|
||||
// @Produce json
|
||||
// @Param body body RefreshRequest true "Refresh request"
|
||||
// @Success 200 {object} RefreshResponse
|
||||
// @Failure 400 {string} string "Invalid request body"
|
||||
// @Failure 400 {string} string "Refresh token required"
|
||||
// @Failure 401 {string} string "Invalid refresh token"
|
||||
// @Failure 400 {object} ErrorResponse "Invalid request body"
|
||||
// @Failure 400 {object} ErrorResponse "Refresh token required"
|
||||
// @Failure 401 {object} ErrorResponse "Invalid refresh token"
|
||||
// @Router /auth/refresh [post]
|
||||
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 {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
respondError(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.RefreshToken == "" {
|
||||
http.Error(w, "Refresh token required", http.StatusBadRequest)
|
||||
respondError(w, "Refresh token required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate new access token
|
||||
accessToken, err := authService.RefreshSession(req.RefreshToken)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid refresh token", http.StatusUnauthorized)
|
||||
respondError(w, "Invalid refresh token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ func (h *Handler) RefreshToken(authService *auth.SessionService) http.HandlerFun
|
||||
// @Security BearerAuth
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.User
|
||||
// @Failure 404 {string} string "User not found"
|
||||
// @Failure 404 {object} ErrorResponse "User not found"
|
||||
// @Router /auth/me [get]
|
||||
func (h *Handler) GetCurrentUser() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -184,7 +184,7 @@ func (h *Handler) GetCurrentUser() http.HandlerFunc {
|
||||
// Get user from database
|
||||
user, err := h.DB.GetUserByID(ctx.UserID)
|
||||
if err != nil {
|
||||
http.Error(w, "User not found", http.StatusNotFound)
|
||||
respondError(w, "User not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -188,7 +188,7 @@ func TestAuthHandlers_Integration(t *testing.T) {
|
||||
"X-Session-ID": loginResp.Session.ID,
|
||||
}
|
||||
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
|
||||
refreshReq := handlers.RefreshRequest{
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"novamd/internal/context"
|
||||
"novamd/internal/storage"
|
||||
@@ -12,6 +13,28 @@ import (
|
||||
"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
|
||||
// @Summary List files
|
||||
// @Description Lists all files in the user's workspace
|
||||
@@ -20,8 +43,8 @@ import (
|
||||
// @Security BearerAuth
|
||||
// @Produce json
|
||||
// @Param workspace_name path string true "Workspace name"
|
||||
// @Success 200 {array} string
|
||||
// @Failure 500 {string} string "Failed to list files"
|
||||
// @Success 200 {array} storage.FileNode
|
||||
// @Failure 500 {object} ErrorResponse "Failed to list files"
|
||||
// @Router /workspaces/{workspace_name}/files [get]
|
||||
func (h *Handler) ListFiles() http.HandlerFunc {
|
||||
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)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to list files", http.StatusInternalServerError)
|
||||
respondError(w, "Failed to list files", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -49,9 +72,9 @@ func (h *Handler) ListFiles() http.HandlerFunc {
|
||||
// @Produce json
|
||||
// @Param workspace_name path string true "Workspace name"
|
||||
// @Param filename query string true "File name"
|
||||
// @Success 200 {object} map[string][]string
|
||||
// @Failure 400 {string} string "Filename is required"
|
||||
// @Failure 404 {string} string "File not found"
|
||||
// @Success 200 {object} LookupResponse
|
||||
// @Failure 400 {object} ErrorResponse "Filename is required"
|
||||
// @Failure 404 {object} ErrorResponse "File not found"
|
||||
// @Router /workspaces/{workspace_name}/files/lookup [get]
|
||||
func (h *Handler) LookupFileByName() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -62,17 +85,17 @@ func (h *Handler) LookupFileByName() http.HandlerFunc {
|
||||
|
||||
filename := r.URL.Query().Get("filename")
|
||||
if filename == "" {
|
||||
http.Error(w, "Filename is required", http.StatusBadRequest)
|
||||
respondError(w, "Filename is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
filePaths, err := h.Storage.FindFileByName(ctx.UserID, ctx.Workspace.ID, filename)
|
||||
if err != nil {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
respondError(w, "File not found", http.StatusNotFound)
|
||||
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 file_path path string true "File path"
|
||||
// @Success 200 {string} "File content"
|
||||
// @Failure 400 {string} string "Invalid file path"
|
||||
// @Failure 404 {string} string "File not found"
|
||||
// @Failure 500 {string} string "Failed to read file"
|
||||
// @Failure 500 {string} string "Failed to write response"
|
||||
// @Failure 400 {object} ErrorResponse "Invalid file path"
|
||||
// @Failure 404 {object} ErrorResponse "File not found"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to read file"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to write response"
|
||||
// @Router /workspaces/{workspace_name}/files/* [get]
|
||||
func (h *Handler) GetFileContent() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -103,23 +126,23 @@ func (h *Handler) GetFileContent() http.HandlerFunc {
|
||||
if err != nil {
|
||||
|
||||
if storage.IsPathValidationError(err) {
|
||||
http.Error(w, "Invalid file path", http.StatusBadRequest)
|
||||
respondError(w, "Invalid file path", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
respondError(w, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, "Failed to read file", http.StatusInternalServerError)
|
||||
respondError(w, "Failed to read file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
_, err = w.Write(content)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to write response", http.StatusInternalServerError)
|
||||
respondError(w, "Failed to write response", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -135,10 +158,10 @@ func (h *Handler) GetFileContent() http.HandlerFunc {
|
||||
// @Produce json
|
||||
// @Param workspace_name path string true "Workspace name"
|
||||
// @Param file_path path string true "File path"
|
||||
// @Success 200 {string} "File saved successfully"
|
||||
// @Failure 400 {string} string "Failed to read request body"
|
||||
// @Failure 400 {string} string "Invalid file path"
|
||||
// @Failure 500 {string} string "Failed to save file"
|
||||
// @Success 200 {object} SaveFileResponse
|
||||
// @Failure 400 {object} ErrorResponse "Failed to read request body"
|
||||
// @Failure 400 {object} ErrorResponse "Invalid file path"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to save file"
|
||||
// @Router /workspaces/{workspace_name}/files/* [post]
|
||||
func (h *Handler) SaveFile() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -150,22 +173,29 @@ func (h *Handler) SaveFile() http.HandlerFunc {
|
||||
filePath := chi.URLParam(r, "*")
|
||||
content, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to read request body", http.StatusBadRequest)
|
||||
respondError(w, "Failed to read request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = h.Storage.SaveFile(ctx.UserID, ctx.Workspace.ID, filePath, content)
|
||||
if err != nil {
|
||||
if storage.IsPathValidationError(err) {
|
||||
http.Error(w, "Invalid file path", http.StatusBadRequest)
|
||||
respondError(w, "Invalid file path", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, "Failed to save file", http.StatusInternalServerError)
|
||||
respondError(w, "Failed to save file", http.StatusInternalServerError)
|
||||
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
|
||||
// @Param workspace_name path string true "Workspace name"
|
||||
// @Param file_path path string true "File path"
|
||||
// @Success 200 {string} "File deleted successfully"
|
||||
// @Failure 400 {string} string "Invalid file path"
|
||||
// @Failure 404 {string} string "File not found"
|
||||
// @Failure 500 {string} string "Failed to delete file"
|
||||
// @Failure 500 {string} string "Failed to write response"
|
||||
// @Success 204 "No Content - File deleted successfully"
|
||||
// @Failure 400 {object} ErrorResponse "Invalid file path"
|
||||
// @Failure 404 {object} ErrorResponse "File not found"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to delete file"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to write response"
|
||||
// @Router /workspaces/{workspace_name}/files/* [delete]
|
||||
func (h *Handler) DeleteFile() http.HandlerFunc {
|
||||
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)
|
||||
if err != nil {
|
||||
if storage.IsPathValidationError(err) {
|
||||
http.Error(w, "Invalid file path", http.StatusBadRequest)
|
||||
respondError(w, "Invalid file path", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
respondError(w, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, "Failed to delete file", http.StatusInternalServerError)
|
||||
respondError(w, "Failed to delete file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, err = w.Write([]byte("File deleted successfully"))
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to write response", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,9 +250,9 @@ func (h *Handler) DeleteFile() http.HandlerFunc {
|
||||
// @Security BearerAuth
|
||||
// @Produce json
|
||||
// @Param workspace_name path string true "Workspace name"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 400 {string} string "Invalid file path"
|
||||
// @Failure 500 {string} string "Failed to get last opened file"
|
||||
// @Success 200 {object} LastOpenedFileResponse
|
||||
// @Failure 400 {object} ErrorResponse "Invalid file path"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to get last opened file"
|
||||
// @Router /workspaces/{workspace_name}/files/last [get]
|
||||
func (h *Handler) GetLastOpenedFile() http.HandlerFunc {
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
respondJSON(w, map[string]string{"lastOpenedFilePath": filePath})
|
||||
respondJSON(w, &LastOpenedFileResponse{LastOpenedFilePath: filePath})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,11 +285,12 @@ func (h *Handler) GetLastOpenedFile() http.HandlerFunc {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param workspace_name path string true "Workspace name"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 400 {string} string "Invalid request body"
|
||||
// @Failure 400 {string} string "Invalid file path"
|
||||
// @Failure 404 {string} string "File not found"
|
||||
// @Failure 500 {string} string "Failed to update file"
|
||||
// @Param body body UpdateLastOpenedFileRequest true "Update last opened file request"
|
||||
// @Success 204 "No Content - Last opened file updated successfully"
|
||||
// @Failure 400 {object} ErrorResponse "Invalid request body"
|
||||
// @Failure 400 {object} ErrorResponse "Invalid file path"
|
||||
// @Failure 404 {object} ErrorResponse "File not found"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to update file"
|
||||
// @Router /workspaces/{workspace_name}/files/last [put]
|
||||
func (h *Handler) UpdateLastOpenedFile() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -273,12 +299,10 @@ func (h *Handler) UpdateLastOpenedFile() http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
var requestBody struct {
|
||||
FilePath string `json:"filePath"`
|
||||
}
|
||||
var requestBody UpdateLastOpenedFileRequest
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -287,25 +311,25 @@ func (h *Handler) UpdateLastOpenedFile() http.HandlerFunc {
|
||||
_, err := h.Storage.GetFileContent(ctx.UserID, ctx.Workspace.ID, requestBody.FilePath)
|
||||
if err != nil {
|
||||
if storage.IsPathValidationError(err) {
|
||||
http.Error(w, "Invalid file path", http.StatusBadRequest)
|
||||
respondError(w, "Invalid file path", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
respondError(w, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, "Failed to update last opened file", http.StatusInternalServerError)
|
||||
respondError(w, "Failed to update last opened file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
respondJSON(w, map[string]string{"message": "Last opened file updated successfully"})
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ func TestFileHandlers_Integration(t *testing.T) {
|
||||
|
||||
// Delete file
|
||||
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
|
||||
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",
|
||||
}
|
||||
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
|
||||
rr = h.makeRequest(t, http.MethodGet, baseURL+"/last", nil, h.RegularToken, nil)
|
||||
|
||||
@@ -7,6 +7,21 @@ import (
|
||||
"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
|
||||
// @Summary Stage, commit, and push changes
|
||||
// @Description Stages, commits, and pushes changes to the remote repository
|
||||
@@ -15,11 +30,11 @@ import (
|
||||
// @Security BearerAuth
|
||||
// @Produce json
|
||||
// @Param workspace_name path string true "Workspace name"
|
||||
// @Param body body string true "Commit message"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 400 {string} string "Invalid request body"
|
||||
// @Failure 400 {string} string "Commit message is required"
|
||||
// @Failure 500 {string} string "Failed to stage, commit, and push changes"
|
||||
// @Param body body CommitRequest true "Commit request"
|
||||
// @Success 200 {object} CommitResponse
|
||||
// @Failure 400 {object} ErrorResponse "Invalid request body"
|
||||
// @Failure 400 {object} ErrorResponse "Commit message is required"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to stage, commit, and push changes"
|
||||
// @Router /workspaces/{workspace_name}/git/commit [post]
|
||||
func (h *Handler) StageCommitAndPush() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -28,27 +43,25 @@ func (h *Handler) StageCommitAndPush() http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
var requestBody struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
var requestBody CommitRequest
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if requestBody.Message == "" {
|
||||
http.Error(w, "Commit message is required", http.StatusBadRequest)
|
||||
respondError(w, "Commit message is required", http.StatusBadRequest)
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
// @Produce json
|
||||
// @Param workspace_name path string true "Workspace name"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 500 {string} string "Failed to pull changes"
|
||||
// @Success 200 {object} PullResponse
|
||||
// @Failure 500 {object} ErrorResponse "Failed to pull changes"
|
||||
// @Router /workspaces/{workspace_name}/git/pull [post]
|
||||
func (h *Handler) PullChanges() http.HandlerFunc {
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
respondJSON(w, map[string]string{"message": "Pulled changes from remote"})
|
||||
respondJSON(w, PullResponse{Message: "Successfully pulled changes from remote"})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ func TestGitHandlers_Integration(t *testing.T) {
|
||||
var response map[string]string
|
||||
err := json.NewDecoder(rr.Body).Decode(&response)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, response["message"], "successfully")
|
||||
require.Contains(t, response, "commitHash")
|
||||
|
||||
// Verify mock was called correctly
|
||||
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
|
||||
err := json.NewDecoder(rr.Body).Decode(&response)
|
||||
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")
|
||||
})
|
||||
|
||||
@@ -7,6 +7,11 @@ import (
|
||||
"novamd/internal/storage"
|
||||
)
|
||||
|
||||
// ErrorResponse is a generic error response
|
||||
type ErrorResponse struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// Handler provides common functionality for all handlers
|
||||
type Handler struct {
|
||||
DB db.Database
|
||||
@@ -25,6 +30,12 @@ func NewHandler(db db.Database, s storage.Manager) *Handler {
|
||||
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)
|
||||
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})
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ package handlers_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"novamd/internal/git"
|
||||
)
|
||||
|
||||
// MockGitClient implements the git.Client interface for testing
|
||||
@@ -51,13 +52,13 @@ func (m *MockGitClient) Pull() error {
|
||||
}
|
||||
|
||||
// Commit implements git.Client
|
||||
func (m *MockGitClient) Commit(message string) error {
|
||||
func (m *MockGitClient) Commit(message string) (git.CommitHash, error) {
|
||||
if m.error != nil {
|
||||
return m.error
|
||||
return git.CommitHash{}, m.error
|
||||
}
|
||||
m.commitCount++
|
||||
m.lastCommitMsg = message
|
||||
return nil
|
||||
return git.CommitHash{}, nil
|
||||
}
|
||||
|
||||
// Push implements git.Client
|
||||
|
||||
@@ -28,7 +28,7 @@ func (h *StaticHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Security check to prevent directory traversal
|
||||
if !strings.HasPrefix(cleanPath, h.staticPath) {
|
||||
http.Error(w, "Invalid path", http.StatusBadRequest)
|
||||
respondError(w, "Invalid path", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -32,15 +32,15 @@ type DeleteAccountRequest struct {
|
||||
// @Produce json
|
||||
// @Param body body UpdateProfileRequest true "Profile update request"
|
||||
// @Success 200 {object} models.User
|
||||
// @Failure 400 {string} string "Invalid request body"
|
||||
// @Failure 400 {string} string "Current password is required to change password"
|
||||
// @Failure 400 {string} string "New password must be at least 8 characters long"
|
||||
// @Failure 400 {string} string "Current password is required to change email"
|
||||
// @Failure 401 {string} string "Current password is incorrect"
|
||||
// @Failure 404 {string} string "User not found"
|
||||
// @Failure 409 {string} string "Email already in use"
|
||||
// @Failure 500 {string} string "Failed to process new password"
|
||||
// @Failure 500 {string} string "Failed to update profile"
|
||||
// @Failure 400 {object} ErrorResponse "Invalid request body"
|
||||
// @Failure 400 {object} ErrorResponse "Current password is required to change password"
|
||||
// @Failure 400 {object} ErrorResponse "New password must be at least 8 characters long"
|
||||
// @Failure 400 {object} ErrorResponse "Current password is required to change email"
|
||||
// @Failure 401 {object} ErrorResponse "Current password is incorrect"
|
||||
// @Failure 404 {object} ErrorResponse "User not found"
|
||||
// @Failure 409 {object} ErrorResponse "Email already in use"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to process new password"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to update profile"
|
||||
// @Router /profile [put]
|
||||
func (h *Handler) UpdateProfile() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -51,14 +51,14 @@ func (h *Handler) UpdateProfile() http.HandlerFunc {
|
||||
|
||||
var req UpdateProfileRequest
|
||||
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
|
||||
}
|
||||
|
||||
// Get current user
|
||||
user, err := h.DB.GetUserByID(ctx.UserID)
|
||||
if err != nil {
|
||||
http.Error(w, "User not found", http.StatusNotFound)
|
||||
respondError(w, "User not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -66,26 +66,26 @@ func (h *Handler) UpdateProfile() http.HandlerFunc {
|
||||
if req.NewPassword != "" {
|
||||
// Current password must be provided to change password
|
||||
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
|
||||
}
|
||||
|
||||
// Verify current password
|
||||
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
|
||||
}
|
||||
|
||||
// Validate new password
|
||||
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
|
||||
}
|
||||
|
||||
// Hash new password
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to process new password", http.StatusInternalServerError)
|
||||
respondError(w, "Failed to process new password", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
user.PasswordHash = string(hashedPassword)
|
||||
@@ -95,14 +95,14 @@ func (h *Handler) UpdateProfile() http.HandlerFunc {
|
||||
if req.Email != "" && req.Email != user.Email {
|
||||
// Check if email change requires password verification
|
||||
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
|
||||
}
|
||||
|
||||
// Verify current password if not already verified for password change
|
||||
if req.NewPassword == "" {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -110,7 +110,7 @@ func (h *Handler) UpdateProfile() http.HandlerFunc {
|
||||
// Check if new email is already in use
|
||||
existingUser, err := h.DB.GetUserByEmail(req.Email)
|
||||
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
|
||||
}
|
||||
user.Email = req.Email
|
||||
@@ -123,7 +123,7 @@ func (h *Handler) UpdateProfile() http.HandlerFunc {
|
||||
|
||||
// Update user in database
|
||||
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
|
||||
}
|
||||
|
||||
@@ -141,13 +141,13 @@ func (h *Handler) UpdateProfile() http.HandlerFunc {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body DeleteAccountRequest true "Account deletion request"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 400 {string} string "Invalid request body"
|
||||
// @Failure 401 {string} string "Password is incorrect"
|
||||
// @Failure 403 {string} string "Cannot delete the last admin account"
|
||||
// @Failure 404 {string} string "User not found"
|
||||
// @Failure 500 {string} string "Failed to verify admin status"
|
||||
// @Failure 500 {string} string "Failed to delete account"
|
||||
// @Success 204 "No Content - Account deleted successfully"
|
||||
// @Failure 400 {object} ErrorResponse "Invalid request body"
|
||||
// @Failure 401 {object} ErrorResponse "Password is incorrect"
|
||||
// @Failure 403 {object} ErrorResponse "Cannot delete the last admin account"
|
||||
// @Failure 404 {object} ErrorResponse "User not found"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to verify admin status"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to delete account"
|
||||
// @Router /profile [delete]
|
||||
func (h *Handler) DeleteAccount() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -158,20 +158,20 @@ func (h *Handler) DeleteAccount() http.HandlerFunc {
|
||||
|
||||
var req DeleteAccountRequest
|
||||
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
|
||||
}
|
||||
|
||||
// Get current user
|
||||
user, err := h.DB.GetUserByID(ctx.UserID)
|
||||
if err != nil {
|
||||
http.Error(w, "User not found", http.StatusNotFound)
|
||||
respondError(w, "User not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify password
|
||||
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
|
||||
}
|
||||
|
||||
@@ -180,11 +180,11 @@ func (h *Handler) DeleteAccount() http.HandlerFunc {
|
||||
// Count number of admin users
|
||||
adminCount, err := h.DB.CountAdminUsers()
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to verify admin status", http.StatusInternalServerError)
|
||||
respondError(w, "Failed to verify admin status", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -192,24 +192,24 @@ func (h *Handler) DeleteAccount() http.HandlerFunc {
|
||||
// Get user's workspaces for cleanup
|
||||
workspaces, err := h.DB.GetWorkspacesByUserID(ctx.UserID)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get user workspaces", http.StatusInternalServerError)
|
||||
respondError(w, "Failed to get user workspaces", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete workspace directories
|
||||
for _, workspace := range workspaces {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Delete user from database (this will cascade delete workspaces and sessions)
|
||||
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
|
||||
}
|
||||
|
||||
respondJSON(w, map[string]string{"message": "Account deleted successfully"})
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ func TestUserHandlers_Integration(t *testing.T) {
|
||||
}
|
||||
|
||||
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
|
||||
rr = h.makeRequest(t, http.MethodPost, "/api/v1/auth/login", loginReq, "", nil)
|
||||
|
||||
@@ -9,6 +9,16 @@ import (
|
||||
"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
|
||||
// @Summary List workspaces
|
||||
// @Description Lists all workspaces for the current user
|
||||
@@ -17,7 +27,7 @@ import (
|
||||
// @Security BearerAuth
|
||||
// @Produce json
|
||||
// @Success 200 {array} models.Workspace
|
||||
// @Failure 500 {string} string "Failed to list workspaces"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to list workspaces"
|
||||
// @Router /workspaces [get]
|
||||
func (h *Handler) ListWorkspaces() http.HandlerFunc {
|
||||
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)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to list workspaces", http.StatusInternalServerError)
|
||||
respondError(w, "Failed to list workspaces", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -46,11 +56,11 @@ func (h *Handler) ListWorkspaces() http.HandlerFunc {
|
||||
// @Produce json
|
||||
// @Param body body models.Workspace true "Workspace"
|
||||
// @Success 200 {object} models.Workspace
|
||||
// @Failure 400 {string} string "Invalid request body"
|
||||
// @Failure 400 {string} string "Invalid workspace"
|
||||
// @Failure 500 {string} string "Failed to create workspace"
|
||||
// @Failure 500 {string} string "Failed to initialize workspace directory"
|
||||
// @Failure 500 {string} string "Failed to setup git repo"
|
||||
// @Failure 400 {object} ErrorResponse "Invalid request body"
|
||||
// @Failure 400 {object} ErrorResponse "Invalid workspace"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to create workspace"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to initialize workspace directory"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to setup git repo"
|
||||
// @Router /workspaces [post]
|
||||
func (h *Handler) CreateWorkspace() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -61,23 +71,23 @@ func (h *Handler) CreateWorkspace() http.HandlerFunc {
|
||||
|
||||
var workspace models.Workspace
|
||||
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
|
||||
}
|
||||
|
||||
if err := workspace.ValidateGitSettings(); err != nil {
|
||||
http.Error(w, "Invalid workspace", http.StatusBadRequest)
|
||||
respondError(w, "Invalid workspace", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
workspace.UserID = ctx.UserID
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -91,7 +101,7 @@ func (h *Handler) CreateWorkspace() http.HandlerFunc {
|
||||
workspace.GitCommitName,
|
||||
workspace.GitCommitEmail,
|
||||
); 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
|
||||
}
|
||||
}
|
||||
@@ -109,7 +119,7 @@ func (h *Handler) CreateWorkspace() http.HandlerFunc {
|
||||
// @Produce json
|
||||
// @Param workspace_name path string true "Workspace name"
|
||||
// @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]
|
||||
func (h *Handler) GetWorkspace() http.HandlerFunc {
|
||||
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 body body models.Workspace true "Workspace"
|
||||
// @Success 200 {object} models.Workspace
|
||||
// @Failure 400 {string} string "Invalid request body"
|
||||
// @Failure 500 {string} string "Failed to update workspace"
|
||||
// @Failure 500 {string} string "Failed to setup git repo"
|
||||
// @Failure 400 {object} ErrorResponse "Invalid request body"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to update workspace"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to setup git repo"
|
||||
// @Router /workspaces/{workspace_name} [put]
|
||||
func (h *Handler) UpdateWorkspace() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -164,7 +174,7 @@ func (h *Handler) UpdateWorkspace() http.HandlerFunc {
|
||||
|
||||
var workspace models.Workspace
|
||||
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
|
||||
}
|
||||
|
||||
@@ -174,7 +184,7 @@ func (h *Handler) UpdateWorkspace() http.HandlerFunc {
|
||||
|
||||
// Validate the workspace
|
||||
if err := workspace.Validate(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
respondError(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -190,7 +200,7 @@ func (h *Handler) UpdateWorkspace() http.HandlerFunc {
|
||||
workspace.GitCommitName,
|
||||
workspace.GitCommitEmail,
|
||||
); 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
|
||||
}
|
||||
|
||||
@@ -200,7 +210,7 @@ func (h *Handler) UpdateWorkspace() http.HandlerFunc {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -216,14 +226,14 @@ func (h *Handler) UpdateWorkspace() http.HandlerFunc {
|
||||
// @Security BearerAuth
|
||||
// @Produce json
|
||||
// @Param workspace_name path string true "Workspace name"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 400 {string} string "Cannot delete the last workspace"
|
||||
// @Failure 500 {string} string "Failed to get workspaces"
|
||||
// @Failure 500 {string} string "Failed to start transaction"
|
||||
// @Failure 500 {string} string "Failed to update last workspace"
|
||||
// @Failure 500 {string} string "Failed to delete workspace"
|
||||
// @Failure 500 {string} string "Failed to rollback transaction"
|
||||
// @Failure 500 {string} string "Failed to commit transaction"
|
||||
// @Success 200 {object} DeleteWorkspaceResponse
|
||||
// @Failure 400 {object} ErrorResponse "Cannot delete the last workspace"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to get workspaces"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to start transaction"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to update last workspace"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to delete workspace"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to rollback transaction"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to commit transaction"
|
||||
// @Router /workspaces/{workspace_name} [delete]
|
||||
func (h *Handler) DeleteWorkspace() http.HandlerFunc {
|
||||
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
|
||||
workspaces, err := h.DB.GetWorkspacesByUserID(ctx.UserID)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get workspaces", http.StatusInternalServerError)
|
||||
respondError(w, "Failed to get workspaces", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if len(workspaces) <= 1 {
|
||||
http.Error(w, "Cannot delete the last workspace", http.StatusBadRequest)
|
||||
respondError(w, "Cannot delete the last workspace", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -258,37 +268,37 @@ func (h *Handler) DeleteWorkspace() http.HandlerFunc {
|
||||
// Start transaction
|
||||
tx, err := h.DB.Begin()
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to start transaction", http.StatusInternalServerError)
|
||||
respondError(w, "Failed to start transaction", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
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
|
||||
err = h.DB.UpdateLastWorkspaceTx(tx, ctx.UserID, nextWorkspaceID)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to update last workspace", http.StatusInternalServerError)
|
||||
respondError(w, "Failed to update last workspace", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the workspace
|
||||
err = h.DB.DeleteWorkspaceTx(tx, ctx.Workspace.ID)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to delete workspace", http.StatusInternalServerError)
|
||||
respondError(w, "Failed to delete workspace", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Commit transaction
|
||||
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 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
|
||||
// @Security BearerAuth
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 500 {string} string "Failed to get last workspace"
|
||||
// @Success 200 {object} LastWorkspaceNameResponse
|
||||
// @Failure 500 {object} ErrorResponse "Failed to get last workspace"
|
||||
// @Router /workspaces/last [get]
|
||||
func (h *Handler) GetLastWorkspaceName() http.HandlerFunc {
|
||||
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)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get last workspace", http.StatusInternalServerError)
|
||||
respondError(w, "Failed to get last workspace", http.StatusInternalServerError)
|
||||
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
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 400 {string} string "Invalid request body"
|
||||
// @Failure 500 {string} string "Failed to update last workspace"
|
||||
// @Success 204 "No Content - Last workspace updated successfully"
|
||||
// @Failure 400 {object} ErrorResponse "Invalid request body"
|
||||
// @Failure 500 {object} ErrorResponse "Failed to update last workspace"
|
||||
// @Router /workspaces/last [put]
|
||||
func (h *Handler) UpdateLastWorkspaceName() http.HandlerFunc {
|
||||
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 {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
respondError(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
respondJSON(w, map[string]string{"message": "Last workspace updated successfully"})
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,7 +226,7 @@ func TestWorkspaceHandlers_Integration(t *testing.T) {
|
||||
}
|
||||
|
||||
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
|
||||
rr = h.makeRequest(t, http.MethodGet, "/api/v1/workspaces/last", nil, h.RegularToken, nil)
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
type RepositoryManager interface {
|
||||
SetupGitRepo(userID, workspaceID int, gitURL, gitUser, gitToken, commitName, commitEmail string) error
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
// 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)
|
||||
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 {
|
||||
return err
|
||||
hash, err := repo.Commit(message)
|
||||
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.
|
||||
|
||||
@@ -29,10 +29,10 @@ func (m *MockGitClient) Pull() error {
|
||||
return m.ReturnError
|
||||
}
|
||||
|
||||
func (m *MockGitClient) Commit(message string) error {
|
||||
func (m *MockGitClient) Commit(message string) (git.CommitHash, error) {
|
||||
m.CommitCalled = true
|
||||
m.CommitMessage = message
|
||||
return m.ReturnError
|
||||
return git.CommitHash{}, m.ReturnError
|
||||
}
|
||||
|
||||
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) {
|
||||
err := s.StageCommitAndPush(1, 1, "test commit")
|
||||
_, err := s.StageCommitAndPush(1, 1, "test commit")
|
||||
if err == 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
|
||||
|
||||
// Test commit and push
|
||||
err := s.StageCommitAndPush(1, 1, "test commit")
|
||||
_, err := s.StageCommitAndPush(1, 1, "test commit")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
@@ -189,7 +189,7 @@ func TestGitOperations(t *testing.T) {
|
||||
s.GitRepos[1][1] = mockClient
|
||||
|
||||
// Test commit error
|
||||
err := s.StageCommitAndPush(1, 1, "test commit")
|
||||
_, err := s.StageCommitAndPush(1, 1, "test commit")
|
||||
if err == nil {
|
||||
t.Error("expected error for commit, got nil")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user