package api import ( "context" "encoding/json" "errors" "net/http" "github.com/go-chi/chi/v5" "github.com/iliaivanov/spec-kit-remote/cmd/dev-pod-api/internal/model" "github.com/iliaivanov/spec-kit-remote/cmd/dev-pod-api/internal/store" ) // createUserResponse is returned when a new user is created. type createUserResponse struct { User *model.User `json:"user"` APIKey string `json:"api_key"` } func (s *Server) handleCreateUser(w http.ResponseWriter, r *http.Request) { if RoleFromContext(r.Context()) != model.RoleAdmin { writeError(w, http.StatusForbidden, "admin access required") return } var req model.CreateUserRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } if err := req.Validate(); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } quota := model.DefaultQuota() if req.Quotas != nil { if err := req.Quotas.Validate(); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } quota.ApplyOverrides(*req.Quotas) } user, err := s.Users.CreateUser(r.Context(), req.User, quota) if err != nil { if errors.Is(err, store.ErrDuplicate) { writeError(w, http.StatusConflict, "user already exists") return } writeError(w, http.StatusInternalServerError, "failed to create user") return } plainKey, keyHash, err := s.GenerateKey() if err != nil { cleanupCtx := context.WithoutCancel(r.Context()) if delErr := s.Users.DeleteUser(cleanupCtx, req.User); delErr != nil { s.Logger.Error("rollback: failed to delete orphaned user", "user", req.User, "error", delErr) } writeError(w, http.StatusInternalServerError, "failed to generate api key") return } if err := s.Users.CreateAPIKey(r.Context(), req.User, model.RoleUser, keyHash); err != nil { cleanupCtx := context.WithoutCancel(r.Context()) if delErr := s.Users.DeleteUser(cleanupCtx, req.User); delErr != nil { s.Logger.Error("rollback: failed to delete orphaned user", "user", req.User, "error", delErr) } writeError(w, http.StatusInternalServerError, "failed to store api key") return } if s.Forgejo != nil { forgejoToken, forgejoErr := s.Forgejo.CreateForgejoUser(r.Context(), req.User) if forgejoErr != nil { s.Logger.Error("failed to create forgejo user", "user", req.User, "error", forgejoErr) cleanupCtx := context.WithoutCancel(r.Context()) if delErr := s.Users.DeleteUser(cleanupCtx, req.User); delErr != nil { s.Logger.Error("rollback: failed to delete orphaned user", "user", req.User, "error", delErr) } writeError(w, http.StatusInternalServerError, "failed to create forgejo user") return } if err := s.Users.SaveForgejoToken(r.Context(), req.User, forgejoToken); err != nil { s.Logger.Error("failed to store forgejo token", "user", req.User, "error", err) cleanupCtx := context.WithoutCancel(r.Context()) _ = s.Forgejo.DeleteForgejoUser(cleanupCtx, req.User) if delErr := s.Users.DeleteUser(cleanupCtx, req.User); delErr != nil { s.Logger.Error("rollback: failed to delete orphaned user", "user", req.User, "error", delErr) } writeError(w, http.StatusInternalServerError, "failed to store forgejo token") return } } writeJSON(w, http.StatusCreated, createUserResponse{ User: user, APIKey: plainKey, }) } func (s *Server) handleListUsers(w http.ResponseWriter, r *http.Request) { role := RoleFromContext(r.Context()) authUser := UserFromContext(r.Context()) if role == model.RoleAdmin { users, err := s.Users.ListUsers(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, "failed to list users") return } if users == nil { users = []model.User{} } writeJSON(w, http.StatusOK, users) return } // Regular user: return only self user, err := s.Users.GetUser(r.Context(), authUser.ID) if err != nil { writeError(w, http.StatusInternalServerError, "failed to get user") return } writeJSON(w, http.StatusOK, []model.User{*user}) } func (s *Server) handleGetUser(w http.ResponseWriter, r *http.Request) { targetUser := chi.URLParam(r, "user") if !canAccess(r, targetUser) { writeError(w, http.StatusForbidden, "access denied") return } user, err := s.Users.GetUser(r.Context(), targetUser) if err != nil { if errors.Is(err, store.ErrNotFound) { writeError(w, http.StatusNotFound, "user not found") return } writeError(w, http.StatusInternalServerError, "failed to get user") return } writeJSON(w, http.StatusOK, user) } func (s *Server) handleDeleteUser(w http.ResponseWriter, r *http.Request) { if RoleFromContext(r.Context()) != model.RoleAdmin { writeError(w, http.StatusForbidden, "admin access required") return } targetUser := chi.URLParam(r, "user") // Verify user exists before deleting resources if _, err := s.Users.GetUser(r.Context(), targetUser); err != nil { if errors.Is(err, store.ErrNotFound) { writeError(w, http.StatusNotFound, "user not found") return } writeError(w, http.StatusInternalServerError, "failed to look up user") return } // Delete all pods first — fail if this doesn't succeed to avoid orphaned pods if err := s.K8s.DeleteAllPods(r.Context(), targetUser); err != nil { s.Logger.Error("failed to delete user pods", "user", targetUser, "error", err) writeError(w, http.StatusInternalServerError, "failed to delete user pods") return } if s.Forgejo != nil { if forgejoErr := s.Forgejo.DeleteForgejoUser(r.Context(), targetUser); forgejoErr != nil { s.Logger.Error("failed to delete forgejo user", "user", targetUser, "error", forgejoErr) } } if err := s.Users.DeleteUser(r.Context(), targetUser); err != nil { if errors.Is(err, store.ErrNotFound) { writeError(w, http.StatusNotFound, "user not found") return } writeError(w, http.StatusInternalServerError, "failed to delete user") return } w.WriteHeader(http.StatusNoContent) } func (s *Server) handleUpdateQuotas(w http.ResponseWriter, r *http.Request) { if RoleFromContext(r.Context()) != model.RoleAdmin { writeError(w, http.StatusForbidden, "admin access required") return } targetUser := chi.URLParam(r, "user") var req model.UpdateQuotasRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } if err := req.Validate(); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } user, err := s.Users.UpdateQuotas(r.Context(), targetUser, req) if err != nil { if errors.Is(err, store.ErrNotFound) { writeError(w, http.StatusNotFound, "user not found") return } writeError(w, http.StatusInternalServerError, "failed to update quotas") return } writeJSON(w, http.StatusOK, user) }