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"
|
"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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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})
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user