225 lines
6.7 KiB
Go
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)
|
|
}
|