Add logging to handlers

This commit is contained in:
2024-12-17 23:28:01 +01:00
parent 54cefac4b7
commit 7ccd36f0e4
8 changed files with 739 additions and 56 deletions

View File

@@ -6,6 +6,7 @@ import (
"net/http"
"novamd/internal/context"
"novamd/internal/db"
"novamd/internal/logging"
"novamd/internal/models"
"novamd/internal/storage"
"strconv"
@@ -47,6 +48,10 @@ type SystemStats struct {
*storage.FileCountStats
}
func getAdminLogger() logging.Logger {
return getHandlersLogger().WithGroup("admin")
}
// AdminListUsers godoc
// @Summary List all users
// @Description Returns the list of all users
@@ -58,9 +63,22 @@ type SystemStats struct {
// @Failure 500 {object} ErrorResponse "Failed to list users"
// @Router /admin/users [get]
func (h *Handler) AdminListUsers() http.HandlerFunc {
return func(w http.ResponseWriter, _ *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
ctx, ok := context.GetRequestContext(w, r)
if !ok {
return
}
log := getAdminLogger().With(
"handler", "AdminListUsers",
"adminID", ctx.UserID,
"clientIP", r.RemoteAddr,
)
users, err := h.DB.GetAllUsers()
if err != nil {
log.Error("failed to fetch users from database",
"error", err.Error(),
)
respondError(w, "Failed to list users", http.StatusInternalServerError)
return
}
@@ -89,39 +107,63 @@ func (h *Handler) AdminListUsers() http.HandlerFunc {
// @Router /admin/users [post]
func (h *Handler) AdminCreateUser() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx, ok := context.GetRequestContext(w, r)
if !ok {
return
}
log := getAdminLogger().With(
"handler", "AdminCreateUser",
"adminID", ctx.UserID,
"clientIP", r.RemoteAddr,
)
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
log.Debug("failed to decode request body",
"error", err.Error(),
)
respondError(w, "Invalid request body", http.StatusBadRequest)
return
}
// Validate request
// Validation logging
if req.Email == "" || req.Password == "" || req.Role == "" {
log.Debug("missing required fields",
"hasEmail", req.Email != "",
"hasPassword", req.Password != "",
"hasRole", req.Role != "",
)
respondError(w, "Email, password, and role are required", http.StatusBadRequest)
return
}
// Check if email already exists
// Email existence check
existingUser, err := h.DB.GetUserByEmail(req.Email)
if err == nil && existingUser != nil {
log.Warn("attempted to create user with existing email",
"email", req.Email,
)
respondError(w, "Email already exists", http.StatusConflict)
return
}
// Check if password is long enough
if len(req.Password) < 8 {
log.Debug("password too short",
"passwordLength", len(req.Password),
)
respondError(w, "Password must be at least 8 characters", http.StatusBadRequest)
return
}
// Hash password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
log.Error("failed to hash password",
"error", err.Error(),
)
respondError(w, "Failed to hash password", http.StatusInternalServerError)
return
}
// Create user
user := &models.User{
Email: req.Email,
DisplayName: req.DisplayName,
@@ -131,16 +173,30 @@ func (h *Handler) AdminCreateUser() http.HandlerFunc {
insertedUser, err := h.DB.CreateUser(user)
if err != nil {
log.Error("failed to create user in database",
"error", err.Error(),
"email", req.Email,
"role", req.Role,
)
respondError(w, "Failed to create user", http.StatusInternalServerError)
return
}
// Initialize user workspace
if err := h.Storage.InitializeUserWorkspace(insertedUser.ID, insertedUser.LastWorkspaceID); err != nil {
log.Error("failed to initialize user workspace",
"error", err.Error(),
"userID", insertedUser.ID,
"workspaceID", insertedUser.LastWorkspaceID,
)
respondError(w, "Failed to initialize user workspace", http.StatusInternalServerError)
return
}
log.Info("user created",
"newUserID", insertedUser.ID,
"email", insertedUser.Email,
"role", insertedUser.Role,
)
respondJSON(w, insertedUser)
}
}
@@ -159,14 +215,32 @@ func (h *Handler) AdminCreateUser() http.HandlerFunc {
// @Router /admin/users/{userId} [get]
func (h *Handler) AdminGetUser() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx, ok := context.GetRequestContext(w, r)
if !ok {
return
}
log := getAdminLogger().With(
"handler", "AdminGetUser",
"adminID", ctx.UserID,
"clientIP", r.RemoteAddr,
)
userID, err := strconv.Atoi(chi.URLParam(r, "userId"))
if err != nil {
log.Debug("invalid user ID format",
"userIDParam", chi.URLParam(r, "userId"),
"error", err.Error(),
)
respondError(w, "Invalid user ID", http.StatusBadRequest)
return
}
user, err := h.DB.GetUserByID(userID)
if err != nil {
log.Debug("user not found",
"targetUserID", userID,
"error", err.Error(),
)
respondError(w, "User not found", http.StatusNotFound)
return
}
@@ -194,49 +268,86 @@ func (h *Handler) AdminGetUser() http.HandlerFunc {
// @Router /admin/users/{userId} [put]
func (h *Handler) AdminUpdateUser() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx, ok := context.GetRequestContext(w, r)
if !ok {
return
}
log := getAdminLogger().With(
"handler", "AdminUpdateUser",
"adminID", ctx.UserID,
"clientIP", r.RemoteAddr,
)
userID, err := strconv.Atoi(chi.URLParam(r, "userId"))
if err != nil {
log.Debug("invalid user ID format",
"userIDParam", chi.URLParam(r, "userId"),
"error", err.Error(),
)
respondError(w, "Invalid user ID", http.StatusBadRequest)
return
}
// Get existing user
user, err := h.DB.GetUserByID(userID)
if err != nil {
log.Debug("user not found",
"targetUserID", userID,
"error", err.Error(),
)
respondError(w, "User not found", http.StatusNotFound)
return
}
var req UpdateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
log.Debug("failed to decode request body",
"error", err.Error(),
)
respondError(w, "Invalid request body", http.StatusBadRequest)
return
}
// Update fields if provided
// Track what's being updated for logging
updates := make(map[string]interface{})
if req.Email != "" {
user.Email = req.Email
updates["email"] = req.Email
}
if req.DisplayName != "" {
user.DisplayName = req.DisplayName
updates["displayName"] = req.DisplayName
}
if req.Role != "" {
user.Role = req.Role
updates["role"] = req.Role
}
if req.Password != "" {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
log.Error("failed to hash password",
"error", err.Error(),
)
respondError(w, "Failed to hash password", http.StatusInternalServerError)
return
}
user.PasswordHash = string(hashedPassword)
updates["passwordUpdated"] = true
}
if err := h.DB.UpdateUser(user); err != nil {
log.Error("failed to update user in database",
"error", err.Error(),
"targetUserID", userID,
)
respondError(w, "Failed to update user", http.StatusInternalServerError)
return
}
log.Info("user updated",
"targetUserID", userID,
"updates", updates,
)
respondJSON(w, user)
}
}
@@ -261,37 +372,61 @@ func (h *Handler) AdminDeleteUser() http.HandlerFunc {
if !ok {
return
}
log := getAdminLogger().With(
"handler", "AdminDeleteUser",
"adminID", ctx.UserID,
"clientIP", r.RemoteAddr,
)
userID, err := strconv.Atoi(chi.URLParam(r, "userId"))
if err != nil {
log.Debug("invalid user ID format",
"userIDParam", chi.URLParam(r, "userId"),
"error", err.Error(),
)
respondError(w, "Invalid user ID", http.StatusBadRequest)
return
}
// Prevent admin from deleting themselves
if userID == ctx.UserID {
log.Warn("admin attempted to delete own account")
respondError(w, "Cannot delete your own account", http.StatusBadRequest)
return
}
// Get user before deletion to check role
user, err := h.DB.GetUserByID(userID)
if err != nil {
log.Debug("user not found",
"targetUserID", userID,
"error", err.Error(),
)
respondError(w, "User not found", http.StatusNotFound)
return
}
// Prevent deletion of other admin users
if user.Role == models.RoleAdmin && ctx.UserID != userID {
log.Warn("attempted to delete another admin user",
"targetUserID", userID,
"targetUserEmail", user.Email,
)
respondError(w, "Cannot delete other admin users", http.StatusForbidden)
return
}
if err := h.DB.DeleteUser(userID); err != nil {
log.Error("failed to delete user from database",
"error", err.Error(),
"targetUserID", userID,
)
respondError(w, "Failed to delete user", http.StatusInternalServerError)
return
}
log.Info("user deleted",
"targetUserID", userID,
"targetUserEmail", user.Email,
"targetUserRole", user.Role,
)
w.WriteHeader(http.StatusNoContent)
}
}
@@ -309,9 +444,22 @@ func (h *Handler) AdminDeleteUser() http.HandlerFunc {
// @Failure 500 {object} ErrorResponse "Failed to get file stats"
// @Router /admin/workspaces [get]
func (h *Handler) AdminListWorkspaces() http.HandlerFunc {
return func(w http.ResponseWriter, _ *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
ctx, ok := context.GetRequestContext(w, r)
if !ok {
return
}
log := getAdminLogger().With(
"handler", "AdminListWorkspaces",
"adminID", ctx.UserID,
"clientIP", r.RemoteAddr,
)
workspaces, err := h.DB.GetAllWorkspaces()
if err != nil {
log.Error("failed to fetch workspaces from database",
"error", err.Error(),
)
respondError(w, "Failed to list workspaces", http.StatusInternalServerError)
return
}
@@ -319,11 +467,15 @@ func (h *Handler) AdminListWorkspaces() http.HandlerFunc {
workspacesStats := make([]*WorkspaceStats, 0, len(workspaces))
for _, ws := range workspaces {
workspaceData := &WorkspaceStats{}
user, err := h.DB.GetUserByID(ws.UserID)
if err != nil {
log.Error("failed to fetch user for workspace",
"error", err.Error(),
"workspaceID", ws.ID,
"userID", ws.UserID,
)
respondError(w, "Failed to get user", http.StatusInternalServerError)
return
}
@@ -336,12 +488,16 @@ func (h *Handler) AdminListWorkspaces() http.HandlerFunc {
fileStats, err := h.Storage.GetFileStats(ws.UserID, ws.ID)
if err != nil {
log.Error("failed to fetch file stats for workspace",
"error", err.Error(),
"workspaceID", ws.ID,
"userID", ws.UserID,
)
respondError(w, "Failed to get file stats", http.StatusInternalServerError)
return
}
workspaceData.FileCountStats = fileStats
workspacesStats = append(workspacesStats, workspaceData)
}
@@ -361,15 +517,31 @@ func (h *Handler) AdminListWorkspaces() http.HandlerFunc {
// @Failure 500 {object} ErrorResponse "Failed to get file stats"
// @Router /admin/stats [get]
func (h *Handler) AdminGetSystemStats() http.HandlerFunc {
return func(w http.ResponseWriter, _ *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
ctx, ok := context.GetRequestContext(w, r)
if !ok {
return
}
log := getAdminLogger().With(
"handler", "AdminGetSystemStats",
"adminID", ctx.UserID,
"clientIP", r.RemoteAddr,
)
userStats, err := h.DB.GetSystemStats()
if err != nil {
log.Error("failed to fetch user statistics",
"error", err.Error(),
)
respondError(w, "Failed to get user stats", http.StatusInternalServerError)
return
}
fileStats, err := h.Storage.GetTotalFileStats()
if err != nil {
log.Error("failed to fetch file statistics",
"error", err.Error(),
)
respondError(w, "Failed to get file stats", http.StatusInternalServerError)
return
}

View File

@@ -7,6 +7,7 @@ import (
"net/http"
"novamd/internal/auth"
"novamd/internal/context"
"novamd/internal/logging"
"novamd/internal/models"
"time"
@@ -26,6 +27,10 @@ type LoginResponse struct {
ExpiresAt time.Time `json:"expiresAt,omitempty"`
}
func getAuthLogger() logging.Logger {
return getHandlersLogger().WithGroup("auth")
}
// Login godoc
// @Summary Login
// @Description Logs in a user and returns a session with access and refresh tokens
@@ -43,62 +48,88 @@ type LoginResponse struct {
// @Router /auth/login [post]
func (h *Handler) Login(authManager auth.SessionManager, cookieService auth.CookieManager) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log := getAuthLogger().With(
"handler", "Login",
"clientIP", r.RemoteAddr,
)
var req LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
log.Debug("failed to decode request body",
"error", err.Error(),
)
respondError(w, "Invalid request body", http.StatusBadRequest)
return
}
// Validate request
if req.Email == "" || req.Password == "" {
log.Debug("missing required fields",
"hasEmail", req.Email != "",
"hasPassword", req.Password != "",
)
respondError(w, "Email and password are required", http.StatusBadRequest)
return
}
// Get user from database
user, err := h.DB.GetUserByEmail(req.Email)
if err != nil {
log.Debug("user not found",
"email", req.Email,
"error", err.Error(),
)
respondError(w, "Invalid credentials", http.StatusUnauthorized)
return
}
// Verify password
err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password))
if err != nil {
log.Warn("invalid password attempt",
"userID", user.ID,
"email", user.Email,
)
respondError(w, "Invalid credentials", http.StatusUnauthorized)
return
}
// Create session and generate tokens
session, accessToken, err := authManager.CreateSession(user.ID, string(user.Role))
if err != nil {
log.Error("failed to create session",
"error", err.Error(),
"userID", user.ID,
)
respondError(w, "Failed to create session", http.StatusInternalServerError)
return
}
// Generate CSRF token
csrfToken := make([]byte, 32)
if _, err := rand.Read(csrfToken); err != nil {
log.Error("failed to generate CSRF token",
"error", err.Error(),
"userID", user.ID,
)
respondError(w, "Failed to generate CSRF token", http.StatusInternalServerError)
return
}
csrfTokenString := hex.EncodeToString(csrfToken)
// Set cookies
http.SetCookie(w, cookieService.GenerateAccessTokenCookie(accessToken))
http.SetCookie(w, cookieService.GenerateRefreshTokenCookie(session.RefreshToken))
http.SetCookie(w, cookieService.GenerateCSRFCookie(csrfTokenString))
// Send CSRF token in header for initial setup
w.Header().Set("X-CSRF-Token", csrfTokenString)
// Only send user info in response, not tokens
response := LoginResponse{
User: user,
SessionID: session.ID,
ExpiresAt: session.ExpiresAt,
}
log.Info("user logged in successfully",
"userID", user.ID,
"email", user.Email,
"role", user.Role,
"sessionID", session.ID,
)
respondJSON(w, response)
}
}
@@ -114,24 +145,41 @@ func (h *Handler) Login(authManager auth.SessionManager, cookieService auth.Cook
// @Router /auth/logout [post]
func (h *Handler) Logout(authManager auth.SessionManager, cookieService auth.CookieManager) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Get session ID from cookie
ctx, ok := context.GetRequestContext(w, r)
if !ok {
return
}
log := getAuthLogger().With(
"handler", "Logout",
"userID", ctx.UserID,
"clientIP", r.RemoteAddr,
)
sessionCookie, err := r.Cookie("access_token")
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
log.Debug("missing access token cookie",
"error", err.Error(),
)
respondError(w, "Access token required", http.StatusBadRequest)
return
}
// Invalidate the session in the database
if err := authManager.InvalidateSession(sessionCookie.Value); err != nil {
log.Error("failed to invalidate session",
"error", err.Error(),
"sessionID", sessionCookie.Value,
)
respondError(w, "Failed to invalidate session", http.StatusInternalServerError)
return
}
// Clear cookies
http.SetCookie(w, cookieService.InvalidateCookie("access_token"))
http.SetCookie(w, cookieService.InvalidateCookie("refresh_token"))
http.SetCookie(w, cookieService.InvalidateCookie("csrf_token"))
log.Info("user logged out successfully",
"sessionID", sessionCookie.Value,
)
w.WriteHeader(http.StatusNoContent)
}
}
@@ -151,22 +199,39 @@ func (h *Handler) Logout(authManager auth.SessionManager, cookieService auth.Coo
// @Router /auth/refresh [post]
func (h *Handler) RefreshToken(authManager auth.SessionManager, cookieService auth.CookieManager) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx, ok := context.GetRequestContext(w, r)
if !ok {
return
}
log := getAuthLogger().With(
"handler", "RefreshToken",
"userID", ctx.UserID,
"clientIP", r.RemoteAddr,
)
refreshCookie, err := r.Cookie("refresh_token")
if err != nil {
log.Debug("missing refresh token cookie",
"error", err.Error(),
)
respondError(w, "Refresh token required", http.StatusBadRequest)
return
}
// Generate new access token
accessToken, err := authManager.RefreshSession(refreshCookie.Value)
if err != nil {
log.Error("failed to refresh session",
"error", err.Error(),
)
respondError(w, "Invalid refresh token", http.StatusUnauthorized)
return
}
// Generate new CSRF token
csrfToken := make([]byte, 32)
if _, err := rand.Read(csrfToken); err != nil {
log.Error("failed to generate CSRF token",
"error", err.Error(),
)
respondError(w, "Failed to generate CSRF token", http.StatusInternalServerError)
return
}
@@ -196,10 +261,17 @@ func (h *Handler) GetCurrentUser() http.HandlerFunc {
if !ok {
return
}
log := getAuthLogger().With(
"handler", "GetCurrentUser",
"userID", ctx.UserID,
"clientIP", r.RemoteAddr,
)
// Get user from database
user, err := h.DB.GetUserByID(ctx.UserID)
if err != nil {
log.Error("failed to fetch user",
"error", err.Error(),
)
respondError(w, "User not found", http.StatusNotFound)
return
}

View File

@@ -8,6 +8,7 @@ import (
"time"
"novamd/internal/context"
"novamd/internal/logging"
"novamd/internal/storage"
"github.com/go-chi/chi/v5"
@@ -35,6 +36,10 @@ type UpdateLastOpenedFileRequest struct {
FilePath string `json:"filePath"`
}
func getFilesLogger() logging.Logger {
return getHandlersLogger().WithGroup("files")
}
// ListFiles godoc
// @Summary List files
// @Description Lists all files in the user's workspace
@@ -52,13 +57,25 @@ func (h *Handler) ListFiles() http.HandlerFunc {
if !ok {
return
}
log := getFilesLogger().With(
"handler", "ListFiles",
"userID", ctx.UserID,
"workspaceID", ctx.Workspace.ID,
"clientIP", r.RemoteAddr,
)
files, err := h.Storage.ListFilesRecursively(ctx.UserID, ctx.Workspace.ID)
if err != nil {
log.Error("failed to list files in workspace",
"error", err.Error(),
)
respondError(w, "Failed to list files", http.StatusInternalServerError)
return
}
log.Debug("files listed successfully",
"fileCount", len(files),
)
respondJSON(w, files)
}
}
@@ -82,19 +99,40 @@ func (h *Handler) LookupFileByName() http.HandlerFunc {
if !ok {
return
}
log := getFilesLogger().With(
"handler", "LookupFileByName",
"userID", ctx.UserID,
"workspaceID", ctx.Workspace.ID,
"clientIP", r.RemoteAddr,
)
filename := r.URL.Query().Get("filename")
if filename == "" {
log.Debug("missing filename parameter")
respondError(w, "Filename is required", http.StatusBadRequest)
return
}
filePaths, err := h.Storage.FindFileByName(ctx.UserID, ctx.Workspace.ID, filename)
if err != nil {
if !os.IsNotExist(err) {
log.Error("failed to lookup file",
"filename", filename,
"error", err.Error(),
)
} else {
log.Debug("file not found",
"filename", filename,
)
}
respondError(w, "File not found", http.StatusNotFound)
return
}
log.Debug("file lookup successful",
"filename", filename,
"matchCount", len(filePaths),
)
respondJSON(w, &LookupResponse{Paths: filePaths})
}
}
@@ -120,21 +158,37 @@ func (h *Handler) GetFileContent() http.HandlerFunc {
if !ok {
return
}
log := getFilesLogger().With(
"handler", "GetFileContent",
"userID", ctx.UserID,
"workspaceID", ctx.Workspace.ID,
"clientIP", r.RemoteAddr,
)
filePath := chi.URLParam(r, "*")
content, err := h.Storage.GetFileContent(ctx.UserID, ctx.Workspace.ID, filePath)
if err != nil {
if storage.IsPathValidationError(err) {
log.Error("invalid file path attempted",
"filePath", filePath,
"error", err.Error(),
)
respondError(w, "Invalid file path", http.StatusBadRequest)
return
}
if os.IsNotExist(err) {
log.Debug("file not found",
"filePath", filePath,
)
respondError(w, "File not found", http.StatusNotFound)
return
}
log.Error("failed to read file content",
"filePath", filePath,
"error", err.Error(),
)
respondError(w, "Failed to read file", http.StatusInternalServerError)
return
}
@@ -142,9 +196,18 @@ func (h *Handler) GetFileContent() http.HandlerFunc {
w.Header().Set("Content-Type", "text/plain")
_, err = w.Write(content)
if err != nil {
log.Error("failed to write response",
"filePath", filePath,
"error", err.Error(),
)
respondError(w, "Failed to write response", http.StatusInternalServerError)
return
}
log.Debug("file content retrieved",
"filePath", filePath,
"contentSize", len(content),
)
}
}
@@ -169,10 +232,20 @@ func (h *Handler) SaveFile() http.HandlerFunc {
if !ok {
return
}
log := getFilesLogger().With(
"handler", "SaveFile",
"userID", ctx.UserID,
"workspaceID", ctx.Workspace.ID,
"clientIP", r.RemoteAddr,
)
filePath := chi.URLParam(r, "*")
content, err := io.ReadAll(r.Body)
if err != nil {
log.Error("failed to read request body",
"filePath", filePath,
"error", err.Error(),
)
respondError(w, "Failed to read request body", http.StatusBadRequest)
return
}
@@ -180,10 +253,19 @@ func (h *Handler) SaveFile() http.HandlerFunc {
err = h.Storage.SaveFile(ctx.UserID, ctx.Workspace.ID, filePath, content)
if err != nil {
if storage.IsPathValidationError(err) {
log.Error("invalid file path attempted",
"filePath", filePath,
"error", err.Error(),
)
respondError(w, "Invalid file path", http.StatusBadRequest)
return
}
log.Error("failed to save file",
"filePath", filePath,
"contentSize", len(content),
"error", err.Error(),
)
respondError(w, "Failed to save file", http.StatusInternalServerError)
return
}
@@ -194,7 +276,11 @@ func (h *Handler) SaveFile() http.HandlerFunc {
UpdatedAt: time.Now().UTC(),
}
w.WriteHeader(http.StatusOK)
log.Debug("file saved",
"filePath", filePath,
"size", response.Size,
"updatedAt", response.UpdatedAt,
)
respondJSON(w, response)
}
}
@@ -211,7 +297,6 @@ func (h *Handler) SaveFile() http.HandlerFunc {
// @Failure 400 {object} ErrorResponse "Invalid file path"
// @Failure 404 {object} ErrorResponse "File not found"
// @Failure 500 {object} ErrorResponse "Failed to delete file"
// @Failure 500 {object} ErrorResponse "Failed to write response"
// @Router /workspaces/{workspace_name}/files/{file_path} [delete]
func (h *Handler) DeleteFile() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
@@ -219,24 +304,44 @@ func (h *Handler) DeleteFile() http.HandlerFunc {
if !ok {
return
}
log := getFilesLogger().With(
"handler", "DeleteFile",
"userID", ctx.UserID,
"workspaceID", ctx.Workspace.ID,
"clientIP", r.RemoteAddr,
)
filePath := chi.URLParam(r, "*")
err := h.Storage.DeleteFile(ctx.UserID, ctx.Workspace.ID, filePath)
if err != nil {
if storage.IsPathValidationError(err) {
log.Error("invalid file path attempted",
"filePath", filePath,
"error", err.Error(),
)
respondError(w, "Invalid file path", http.StatusBadRequest)
return
}
if os.IsNotExist(err) {
log.Debug("file not found",
"filePath", filePath,
)
respondError(w, "File not found", http.StatusNotFound)
return
}
log.Error("failed to delete file",
"filePath", filePath,
"error", err.Error(),
)
respondError(w, "Failed to delete file", http.StatusInternalServerError)
return
}
log.Debug("file deleted",
"filePath", filePath,
)
w.WriteHeader(http.StatusNoContent)
}
}
@@ -259,18 +364,34 @@ func (h *Handler) GetLastOpenedFile() http.HandlerFunc {
if !ok {
return
}
log := getFilesLogger().With(
"handler", "GetLastOpenedFile",
"userID", ctx.UserID,
"workspaceID", ctx.Workspace.ID,
"clientIP", r.RemoteAddr,
)
filePath, err := h.DB.GetLastOpenedFile(ctx.Workspace.ID)
if err != nil {
log.Error("failed to get last opened file from database",
"error", err.Error(),
)
respondError(w, "Failed to get last opened file", http.StatusInternalServerError)
return
}
if _, err := h.Storage.ValidatePath(ctx.UserID, ctx.Workspace.ID, filePath); err != nil {
log.Error("invalid file path stored",
"filePath", filePath,
"error", err.Error(),
)
respondError(w, "Invalid file path", http.StatusBadRequest)
return
}
log.Debug("last opened file retrieved successfully",
"filePath", filePath,
)
respondJSON(w, &LastOpenedFileResponse{LastOpenedFilePath: filePath})
}
}
@@ -297,10 +418,18 @@ func (h *Handler) UpdateLastOpenedFile() http.HandlerFunc {
if !ok {
return
}
log := getFilesLogger().With(
"handler", "UpdateLastOpenedFile",
"userID", ctx.UserID,
"workspaceID", ctx.Workspace.ID,
"clientIP", r.RemoteAddr,
)
var requestBody UpdateLastOpenedFileRequest
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
log.Error("failed to decode request body",
"error", err.Error(),
)
respondError(w, "Invalid request body", http.StatusBadRequest)
return
}
@@ -310,25 +439,43 @@ func (h *Handler) UpdateLastOpenedFile() http.HandlerFunc {
_, err := h.Storage.GetFileContent(ctx.UserID, ctx.Workspace.ID, requestBody.FilePath)
if err != nil {
if storage.IsPathValidationError(err) {
log.Error("invalid file path attempted",
"filePath", requestBody.FilePath,
"error", err.Error(),
)
respondError(w, "Invalid file path", http.StatusBadRequest)
return
}
if os.IsNotExist(err) {
log.Debug("file not found",
"filePath", requestBody.FilePath,
)
respondError(w, "File not found", http.StatusNotFound)
return
}
log.Error("failed to validate file path",
"filePath", requestBody.FilePath,
"error", err.Error(),
)
respondError(w, "Failed to update last opened file", http.StatusInternalServerError)
return
}
}
if err := h.DB.UpdateLastOpenedFile(ctx.Workspace.ID, requestBody.FilePath); err != nil {
log.Error("failed to update last opened file in database",
"filePath", requestBody.FilePath,
"error", err.Error(),
)
respondError(w, "Failed to update last opened file", http.StatusInternalServerError)
return
}
log.Debug("last opened file updated successfully",
"filePath", requestBody.FilePath,
)
w.WriteHeader(http.StatusNoContent)
}
}

View File

@@ -3,8 +3,8 @@ package handlers
import (
"encoding/json"
"net/http"
"novamd/internal/context"
"novamd/internal/logging"
)
// CommitRequest represents a request to commit changes
@@ -22,6 +22,10 @@ type PullResponse struct {
Message string `json:"message" example:"Pulled changes from remote"`
}
func getGitLogger() logging.Logger {
return getHandlersLogger().WithGroup("git")
}
// StageCommitAndPush godoc
// @Summary Stage, commit, and push changes
// @Description Stages, commits, and pushes changes to the remote repository
@@ -42,25 +46,43 @@ func (h *Handler) StageCommitAndPush() http.HandlerFunc {
if !ok {
return
}
log := getGitLogger().With(
"handler", "StageCommitAndPush",
"userID", ctx.UserID,
"workspaceID", ctx.Workspace.ID,
"clientIP", r.RemoteAddr,
)
var requestBody CommitRequest
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
log.Error("failed to decode request body",
"error", err.Error(),
)
respondError(w, "Invalid request body", http.StatusBadRequest)
return
}
if requestBody.Message == "" {
log.Debug("empty commit message provided")
respondError(w, "Commit message is required", http.StatusBadRequest)
return
}
hash, err := h.Storage.StageCommitAndPush(ctx.UserID, ctx.Workspace.ID, requestBody.Message)
if err != nil {
log.Error("failed to perform git operations",
"error", err.Error(),
"commitMessage", requestBody.Message,
)
respondError(w, "Failed to stage, commit, and push changes: "+err.Error(), http.StatusInternalServerError)
return
}
log.Debug("git operations completed successfully",
"commitHash", hash.String(),
"commitMessage", requestBody.Message,
)
respondJSON(w, CommitResponse{CommitHash: hash.String()})
}
}
@@ -82,13 +104,23 @@ func (h *Handler) PullChanges() http.HandlerFunc {
if !ok {
return
}
log := getGitLogger().With(
"handler", "PullChanges",
"userID", ctx.UserID,
"workspaceID", ctx.Workspace.ID,
"clientIP", r.RemoteAddr,
)
err := h.Storage.Pull(ctx.UserID, ctx.Workspace.ID)
if err != nil {
log.Error("failed to pull changes from remote",
"error", err.Error(),
)
respondError(w, "Failed to pull changes: "+err.Error(), http.StatusInternalServerError)
return
}
log.Debug("successfully pulled changes from remote")
respondJSON(w, PullResponse{Message: "Successfully pulled changes from remote"})
}
}

View File

@@ -4,6 +4,7 @@ import (
"encoding/json"
"net/http"
"novamd/internal/db"
"novamd/internal/logging"
"novamd/internal/storage"
)
@@ -18,6 +19,15 @@ type Handler struct {
Storage storage.Manager
}
var logger logging.Logger
func getHandlersLogger() logging.Logger {
if logger == nil {
logger = logging.WithGroup("handlers")
}
return logger
}
// NewHandler creates a new handler with the given dependencies
func NewHandler(db db.Database, s storage.Manager) *Handler {
return &Handler{

View File

@@ -2,6 +2,7 @@ package handlers
import (
"net/http"
"novamd/internal/logging"
"os"
"path/filepath"
"strings"
@@ -19,8 +20,19 @@ func NewStaticHandler(staticPath string) *StaticHandler {
}
}
func getStaticLogger() logging.Logger {
return logging.WithGroup("static")
}
// ServeHTTP serves the static files
func (h *StaticHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log := getStaticLogger().With(
"handler", "ServeHTTP",
"clientIP", r.RemoteAddr,
"method", r.Method,
"url", r.URL.Path,
)
// Get the requested path
requestedPath := r.URL.Path
fullPath := filepath.Join(h.staticPath, requestedPath)
@@ -28,6 +40,10 @@ func (h *StaticHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Security check to prevent directory traversal
if !strings.HasPrefix(cleanPath, h.staticPath) {
log.Warn("directory traversal attempt detected",
"requestedPath", requestedPath,
"cleanPath", cleanPath,
)
respondError(w, "Invalid path", http.StatusBadRequest)
return
}
@@ -35,11 +51,29 @@ func (h *StaticHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Set cache headers for assets
if strings.HasPrefix(requestedPath, "/assets/") {
w.Header().Set("Cache-Control", "public, max-age=31536000") // 1 year
log.Debug("cache headers set for asset",
"path", requestedPath,
)
}
// Check if file exists (not counting .gz files)
stat, err := os.Stat(cleanPath)
if err != nil || stat.IsDir() {
if os.IsNotExist(err) {
log.Debug("file not found, serving index.html",
"requestedPath", requestedPath,
)
} else if stat != nil && stat.IsDir() {
log.Debug("directory requested, serving index.html",
"requestedPath", requestedPath,
)
} else {
log.Error("error checking file status",
"requestedPath", requestedPath,
"error", err.Error(),
)
}
// Serve index.html for SPA routing
indexPath := filepath.Join(h.staticPath, "index.html")
http.ServeFile(w, r, indexPath)
@@ -53,20 +87,32 @@ func (h *StaticHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Encoding", "gzip")
// Set proper content type based on original file
contentType := "application/octet-stream"
switch filepath.Ext(cleanPath) {
case ".js":
w.Header().Set("Content-Type", "application/javascript")
contentType = "application/javascript"
case ".css":
w.Header().Set("Content-Type", "text/css")
contentType = "text/css"
case ".html":
w.Header().Set("Content-Type", "text/html")
contentType = "text/html"
}
w.Header().Set("Content-Type", contentType)
log.Debug("serving gzipped file",
"path", requestedPath,
"gzPath", gzPath,
"contentType", contentType,
)
http.ServeFile(w, r, gzPath)
return
}
}
// Serve original file
log.Debug("serving original file",
"path", requestedPath,
"size", stat.Size(),
"modTime", stat.ModTime(),
)
http.ServeFile(w, r, cleanPath)
}

View File

@@ -5,6 +5,7 @@ import (
"net/http"
"novamd/internal/context"
"novamd/internal/logging"
"golang.org/x/crypto/bcrypt"
)
@@ -22,6 +23,10 @@ type DeleteAccountRequest struct {
Password string `json:"password"`
}
func getProfileLogger() logging.Logger {
return getHandlersLogger().WithGroup("profile")
}
// UpdateProfile godoc
// @Summary Update profile
// @Description Updates the user's profile
@@ -48,9 +53,17 @@ func (h *Handler) UpdateProfile() http.HandlerFunc {
if !ok {
return
}
log := getProfileLogger().With(
"handler", "UpdateProfile",
"userID", ctx.UserID,
"clientIP", r.RemoteAddr,
)
var req UpdateProfileRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
log.Debug("failed to decode request body",
"error", err.Error(),
)
respondError(w, "Invalid request body", http.StatusBadRequest)
return
}
@@ -58,76 +71,97 @@ func (h *Handler) UpdateProfile() http.HandlerFunc {
// Get current user
user, err := h.DB.GetUserByID(ctx.UserID)
if err != nil {
log.Error("failed to fetch user from database",
"error", err.Error(),
)
respondError(w, "User not found", http.StatusNotFound)
return
}
// Track what's being updated for logging
updates := make(map[string]bool)
// Handle password update if requested
if req.NewPassword != "" {
// Current password must be provided to change password
if req.CurrentPassword == "" {
log.Debug("password change attempted without current password")
respondError(w, "Current password is required to change password", http.StatusBadRequest)
return
}
// Verify current password
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.CurrentPassword)); err != nil {
log.Warn("incorrect password provided for password change")
respondError(w, "Current password is incorrect", http.StatusUnauthorized)
return
}
// Validate new password
if len(req.NewPassword) < 8 {
log.Debug("password change rejected - too short",
"passwordLength", len(req.NewPassword),
)
respondError(w, "New password must be at least 8 characters long", http.StatusBadRequest)
return
}
// Hash new password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
if err != nil {
log.Error("failed to hash new password",
"error", err.Error(),
)
respondError(w, "Failed to process new password", http.StatusInternalServerError)
return
}
user.PasswordHash = string(hashedPassword)
updates["passwordChanged"] = true
}
// Handle email update if requested
if req.Email != "" && req.Email != user.Email {
// Check if email change requires password verification
if req.CurrentPassword == "" {
log.Debug("email change attempted without current password")
respondError(w, "Current password is required to change email", http.StatusBadRequest)
return
}
// Verify current password if not already verified for password change
if req.NewPassword == "" {
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.CurrentPassword)); err != nil {
log.Warn("incorrect password provided for email change")
respondError(w, "Current password is incorrect", http.StatusUnauthorized)
return
}
}
// Check if new email is already in use
existingUser, err := h.DB.GetUserByEmail(req.Email)
if err == nil && existingUser.ID != user.ID {
log.Debug("email change rejected - already in use",
"requestedEmail", req.Email,
)
respondError(w, "Email already in use", http.StatusConflict)
return
}
user.Email = req.Email
updates["emailChanged"] = true
}
// Update display name if provided (no password required)
// Update display name if provided
if req.DisplayName != "" {
user.DisplayName = req.DisplayName
updates["displayNameChanged"] = true
}
// Update user in database
if err := h.DB.UpdateUser(user); err != nil {
log.Error("failed to update user in database",
"error", err.Error(),
"updates", updates,
)
respondError(w, "Failed to update profile", http.StatusInternalServerError)
return
}
// Return updated user data
log.Debug("profile updated successfully",
"updates", updates,
)
respondJSON(w, user)
}
}
@@ -155,9 +189,17 @@ func (h *Handler) DeleteAccount() http.HandlerFunc {
if !ok {
return
}
log := getProfileLogger().With(
"handler", "DeleteAccount",
"userID", ctx.UserID,
"clientIP", r.RemoteAddr,
)
var req DeleteAccountRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
log.Debug("failed to decode request body",
"error", err.Error(),
)
respondError(w, "Invalid request body", http.StatusBadRequest)
return
}
@@ -165,25 +207,32 @@ func (h *Handler) DeleteAccount() http.HandlerFunc {
// Get current user
user, err := h.DB.GetUserByID(ctx.UserID)
if err != nil {
log.Error("failed to fetch user from database",
"error", err.Error(),
)
respondError(w, "User not found", http.StatusNotFound)
return
}
// Verify password
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil {
respondError(w, "Password is incorrect", http.StatusUnauthorized)
log.Warn("incorrect password provided for account deletion")
respondError(w, "Incorrect password", http.StatusUnauthorized)
return
}
// Prevent admin from deleting their own account if they're the last admin
if user.Role == "admin" {
// Count number of admin users
adminCount, err := h.DB.CountAdminUsers()
if err != nil {
respondError(w, "Failed to verify admin status", http.StatusInternalServerError)
log.Error("failed to count admin users",
"error", err.Error(),
)
respondError(w, "Failed to get admin count", http.StatusInternalServerError)
return
}
if adminCount <= 1 {
log.Warn("attempted to delete last admin account")
respondError(w, "Cannot delete the last admin account", http.StatusForbidden)
return
}
@@ -192,6 +241,9 @@ func (h *Handler) DeleteAccount() http.HandlerFunc {
// Get user's workspaces for cleanup
workspaces, err := h.DB.GetWorkspacesByUserID(ctx.UserID)
if err != nil {
log.Error("failed to fetch user workspaces",
"error", err.Error(),
)
respondError(w, "Failed to get user workspaces", http.StatusInternalServerError)
return
}
@@ -199,17 +251,31 @@ func (h *Handler) DeleteAccount() http.HandlerFunc {
// Delete workspace directories
for _, workspace := range workspaces {
if err := h.Storage.DeleteUserWorkspace(ctx.UserID, workspace.ID); err != nil {
log.Error("failed to delete workspace directory",
"error", err.Error(),
"workspaceID", workspace.ID,
)
respondError(w, "Failed to delete workspace files", http.StatusInternalServerError)
return
}
log.Debug("workspace deleted",
"workspaceID", workspace.ID,
)
}
// Delete user from database (this will cascade delete workspaces and sessions)
// Delete user from database
if err := h.DB.DeleteUser(ctx.UserID); err != nil {
log.Error("failed to delete user from database",
"error", err.Error(),
)
respondError(w, "Failed to delete account", http.StatusInternalServerError)
return
}
log.Info("user account deleted",
"email", user.Email,
"role", user.Role,
)
w.WriteHeader(http.StatusNoContent)
}
}

View File

@@ -6,6 +6,7 @@ import (
"net/http"
"novamd/internal/context"
"novamd/internal/logging"
"novamd/internal/models"
)
@@ -19,6 +20,10 @@ type LastWorkspaceNameResponse struct {
LastWorkspaceName string `json:"lastWorkspaceName"`
}
func getWorkspaceLogger() logging.Logger {
return getHandlersLogger().WithGroup("workspace")
}
// ListWorkspaces godoc
// @Summary List workspaces
// @Description Lists all workspaces for the current user
@@ -35,13 +40,24 @@ func (h *Handler) ListWorkspaces() http.HandlerFunc {
if !ok {
return
}
log := getWorkspaceLogger().With(
"handler", "ListWorkspaces",
"userID", ctx.UserID,
"clientIP", r.RemoteAddr,
)
workspaces, err := h.DB.GetWorkspacesByUserID(ctx.UserID)
if err != nil {
log.Error("failed to fetch workspaces from database",
"error", err.Error(),
)
respondError(w, "Failed to list workspaces", http.StatusInternalServerError)
return
}
log.Debug("workspaces retrieved successfully",
"count", len(workspaces),
)
respondJSON(w, workspaces)
}
}
@@ -68,30 +84,54 @@ func (h *Handler) CreateWorkspace() http.HandlerFunc {
if !ok {
return
}
log := getWorkspaceLogger().With(
"handler", "CreateWorkspace",
"userID", ctx.UserID,
"clientIP", r.RemoteAddr,
)
var workspace models.Workspace
if err := json.NewDecoder(r.Body).Decode(&workspace); err != nil {
log.Debug("invalid request body received",
"error", err.Error(),
)
respondError(w, "Invalid request body", http.StatusBadRequest)
return
}
if err := workspace.ValidateGitSettings(); err != nil {
log.Debug("invalid git settings provided",
"error", err.Error(),
)
respondError(w, "Invalid workspace", http.StatusBadRequest)
return
}
workspace.UserID = ctx.UserID
if err := h.DB.CreateWorkspace(&workspace); err != nil {
log.Error("failed to create workspace in database",
"error", err.Error(),
"workspaceName", workspace.Name,
)
respondError(w, "Failed to create workspace", http.StatusInternalServerError)
return
}
if err := h.Storage.InitializeUserWorkspace(workspace.UserID, workspace.ID); err != nil {
log.Error("failed to initialize workspace directory",
"error", err.Error(),
"workspaceID", workspace.ID,
)
respondError(w, "Failed to initialize workspace directory", http.StatusInternalServerError)
return
}
if workspace.GitEnabled {
log.Debug("setting up git repository",
"workspaceID", workspace.ID,
"gitURL", workspace.GitURL,
)
if err := h.Storage.SetupGitRepo(
ctx.UserID,
workspace.ID,
@@ -101,11 +141,20 @@ func (h *Handler) CreateWorkspace() http.HandlerFunc {
workspace.GitCommitName,
workspace.GitCommitEmail,
); err != nil {
log.Error("failed to setup git repository",
"error", err.Error(),
"workspaceID", workspace.ID,
)
respondError(w, "Failed to setup git repo: "+err.Error(), http.StatusInternalServerError)
return
}
}
log.Info("workspace created",
"workspaceID", workspace.ID,
"workspaceName", workspace.Name,
"gitEnabled", workspace.GitEnabled,
)
respondJSON(w, workspace)
}
}
@@ -171,9 +220,18 @@ func (h *Handler) UpdateWorkspace() http.HandlerFunc {
if !ok {
return
}
log := getWorkspaceLogger().With(
"handler", "UpdateWorkspace",
"userID", ctx.UserID,
"workspaceID", ctx.Workspace.ID,
"clientIP", r.RemoteAddr,
)
var workspace models.Workspace
if err := json.NewDecoder(r.Body).Decode(&workspace); err != nil {
log.Debug("invalid request body received",
"error", err.Error(),
)
respondError(w, "Invalid request body", http.StatusBadRequest)
return
}
@@ -184,13 +242,28 @@ func (h *Handler) UpdateWorkspace() http.HandlerFunc {
// Validate the workspace
if err := workspace.Validate(); err != nil {
log.Debug("invalid workspace configuration",
"error", err.Error(),
)
respondError(w, err.Error(), http.StatusBadRequest)
return
}
// Track what's changed for logging
changes := map[string]bool{
"gitSettings": gitSettingsChanged(&workspace, ctx.Workspace),
"name": workspace.Name != ctx.Workspace.Name,
"theme": workspace.Theme != ctx.Workspace.Theme,
"autoSave": workspace.AutoSave != ctx.Workspace.AutoSave,
}
// Handle Git repository setup/teardown if Git settings changed
if gitSettingsChanged(&workspace, ctx.Workspace) {
if changes["gitSettings"] {
if workspace.GitEnabled {
log.Debug("updating git repository configuration",
"gitURL", workspace.GitURL,
)
if err := h.Storage.SetupGitRepo(
ctx.UserID,
ctx.Workspace.ID,
@@ -200,20 +273,29 @@ func (h *Handler) UpdateWorkspace() http.HandlerFunc {
workspace.GitCommitName,
workspace.GitCommitEmail,
); err != nil {
log.Error("failed to setup git repository",
"error", err.Error(),
)
respondError(w, "Failed to setup git repo: "+err.Error(), http.StatusInternalServerError)
return
}
} else {
log.Debug("disabling git repository")
h.Storage.DisableGitRepo(ctx.UserID, ctx.Workspace.ID)
}
}
if err := h.DB.UpdateWorkspace(&workspace); err != nil {
log.Error("failed to update workspace in database",
"error", err.Error(),
)
respondError(w, "Failed to update workspace", http.StatusInternalServerError)
return
}
log.Debug("workspace updated",
"changes", changes,
)
respondJSON(w, workspace)
}
}
@@ -241,15 +323,25 @@ func (h *Handler) DeleteWorkspace() http.HandlerFunc {
if !ok {
return
}
log := getWorkspaceLogger().With(
"handler", "DeleteWorkspace",
"userID", ctx.UserID,
"workspaceID", ctx.Workspace.ID,
"clientIP", r.RemoteAddr,
)
// Check if this is the user's last workspace
workspaces, err := h.DB.GetWorkspacesByUserID(ctx.UserID)
if err != nil {
log.Error("failed to fetch workspaces from database",
"error", err.Error(),
)
respondError(w, "Failed to get workspaces", http.StatusInternalServerError)
return
}
if len(workspaces) <= 1 {
log.Debug("attempted to delete last workspace")
respondError(w, "Cannot delete the last workspace", http.StatusBadRequest)
return
}
@@ -265,14 +357,19 @@ func (h *Handler) DeleteWorkspace() http.HandlerFunc {
}
}
// Start transaction
tx, err := h.DB.Begin()
if err != nil {
log.Error("failed to start database transaction",
"error", err.Error(),
)
respondError(w, "Failed to start transaction", http.StatusInternalServerError)
return
}
defer func() {
if err := tx.Rollback(); err != nil && err != sql.ErrTxDone {
log.Error("failed to rollback transaction",
"error", err.Error(),
)
respondError(w, "Failed to rollback transaction", http.StatusInternalServerError)
}
}()
@@ -280,6 +377,10 @@ func (h *Handler) DeleteWorkspace() http.HandlerFunc {
// Update last workspace ID first
err = h.DB.UpdateLastWorkspaceTx(tx, ctx.UserID, nextWorkspaceID)
if err != nil {
log.Error("failed to update last workspace reference",
"error", err.Error(),
"nextWorkspaceID", nextWorkspaceID,
)
respondError(w, "Failed to update last workspace", http.StatusInternalServerError)
return
}
@@ -287,16 +388,27 @@ func (h *Handler) DeleteWorkspace() http.HandlerFunc {
// Delete the workspace
err = h.DB.DeleteWorkspaceTx(tx, ctx.Workspace.ID)
if err != nil {
log.Error("failed to delete workspace from database",
"error", err.Error(),
)
respondError(w, "Failed to delete workspace", http.StatusInternalServerError)
return
}
// Commit transaction
if err = tx.Commit(); err != nil {
log.Error("failed to commit transaction",
"error", err.Error(),
)
respondError(w, "Failed to commit transaction", http.StatusInternalServerError)
return
}
log.Info("workspace deleted",
"workspaceName", ctx.Workspace.Name,
"nextWorkspaceName", nextWorkspaceName,
)
// Return the next workspace ID in the response so frontend knows where to redirect
respondJSON(w, &DeleteWorkspaceResponse{NextWorkspaceName: nextWorkspaceName})
}
@@ -318,13 +430,24 @@ func (h *Handler) GetLastWorkspaceName() http.HandlerFunc {
if !ok {
return
}
log := getWorkspaceLogger().With(
"handler", "GetLastWorkspaceName",
"userID", ctx.UserID,
"clientIP", r.RemoteAddr,
)
workspaceName, err := h.DB.GetLastWorkspaceName(ctx.UserID)
if err != nil {
log.Error("failed to fetch last workspace name",
"error", err.Error(),
)
respondError(w, "Failed to get last workspace", http.StatusInternalServerError)
return
}
log.Debug("last workspace name retrieved",
"workspaceName", workspaceName,
)
respondJSON(w, &LastWorkspaceNameResponse{LastWorkspaceName: workspaceName})
}
}
@@ -347,21 +470,36 @@ func (h *Handler) UpdateLastWorkspaceName() http.HandlerFunc {
if !ok {
return
}
log := getWorkspaceLogger().With(
"handler", "UpdateLastWorkspaceName",
"userID", ctx.UserID,
"clientIP", r.RemoteAddr,
)
var requestBody struct {
WorkspaceName string `json:"workspaceName"`
}
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
log.Debug("invalid request body received",
"error", err.Error(),
)
respondError(w, "Invalid request body", http.StatusBadRequest)
return
}
if err := h.DB.UpdateLastWorkspace(ctx.UserID, requestBody.WorkspaceName); err != nil {
log.Error("failed to update last workspace",
"error", err.Error(),
"workspaceName", requestBody.WorkspaceName,
)
respondError(w, "Failed to update last workspace", http.StatusInternalServerError)
return
}
log.Debug("last workspace name updated",
"workspaceName", requestBody.WorkspaceName,
)
w.WriteHeader(http.StatusNoContent)
}
}