dev-pod-api-build/internal/api/users.go
2026-04-16 04:16:36 +00:00

225 lines
6.7 KiB
Go

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)
}