// Package handlers provides HTTP handlers for the rdev API. package handlers import ( "context" "encoding/json" "log/slog" "net/http" "strings" "time" "github.com/go-chi/chi/v5" "github.com/orchard9/rdev/internal/service" "github.com/orchard9/rdev/pkg/api" ) // ProjectManagementHandler handles project lifecycle operations. type ProjectManagementHandler struct { infraService *service.ProjectInfraService } // NewProjectManagementHandler creates a new project management handler. func NewProjectManagementHandler(infraService *service.ProjectInfraService) *ProjectManagementHandler { return &ProjectManagementHandler{ infraService: infraService, } } // Mount registers the project management routes. func (h *ProjectManagementHandler) Mount(r api.Router) { r.Route("/project", func(r chi.Router) { r.Post("/", h.Create) // POST /project - Create new project r.Get("/", h.List) // GET /project - List all projects r.Get("/{name}", h.Status) // GET /project/{name} - Get project status r.Delete("/{name}", h.Delete) // DELETE /project/{name} - Delete project }) } // CreateRequest is the request body for POST /project. type CreateRequest struct { Name string `json:"name"` Description string `json:"description,omitempty"` Private bool `json:"private,omitempty"` } // Create creates a new project with git repo and DNS. // POST /project func (h *ProjectManagementHandler) Create(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second) defer cancel() if h.infraService == nil { api.WriteInternalError(w, r, "project infrastructure service not configured") return } var req CreateRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { api.WriteBadRequest(w, r, "invalid request body") return } if req.Name == "" { api.WriteBadRequest(w, r, "name is required") return } result, err := h.infraService.CreateProject(ctx, service.CreateProjectRequest{ Name: req.Name, Description: req.Description, Private: req.Private, }) if err != nil { // Check for validation errors (user input) vs internal errors if strings.Contains(err.Error(), "invalid project name") { api.WriteBadRequest(w, r, err.Error()) return } // Log internal errors but return generic message to client slog.Error("project creation failed", "error", err, "name", req.Name) api.WriteInternalError(w, r, "failed to create project") return } api.WriteCreated(w, r, map[string]any{ "project_id": result.ProjectID, "name": result.Name, "description": result.Description, "git": map[string]string{ "owner": result.GitRepoOwner, "name": result.GitRepoName, "clone_ssh": result.CloneSSH, "clone_http": result.CloneHTTP, "html_url": result.HTMLURL, }, "domain": result.Domain, "url": result.URL, "next_steps": result.NextSteps, }) } // List returns all projects. // GET /project func (h *ProjectManagementHandler) List(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) defer cancel() if h.infraService == nil { api.WriteInternalError(w, r, "project infrastructure service not configured") return } projects, err := h.infraService.ListProjects(ctx) if err != nil { slog.Error("failed to list projects", "error", err) api.WriteInternalError(w, r, "failed to list projects") return } // Convert to response format response := make([]map[string]any, len(projects)) for i, p := range projects { response[i] = map[string]any{ "project_id": p.ProjectID, "name": p.Name, "description": p.Description, "git": map[string]string{ "clone_ssh": p.CloneSSH, "clone_http": p.CloneHTTP, "html_url": p.HTMLURL, }, "domain": p.Domain, "url": p.URL, "deployment": map[string]any{ "status": p.DeploymentStatus, "image": p.DeploymentImage, "replicas": p.DeploymentReplicas, "ready_replicas": p.ReadyReplicas, }, } } api.WriteSuccess(w, r, response) } // Status returns the status of a specific project. // GET /project/{name} func (h *ProjectManagementHandler) Status(w http.ResponseWriter, r *http.Request) { name := chi.URLParam(r, "name") ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) defer cancel() if h.infraService == nil { api.WriteInternalError(w, r, "project infrastructure service not configured") return } status, err := h.infraService.GetStatus(ctx, name) if err != nil { // Check if it's a "not found" error if strings.Contains(err.Error(), "not found") { api.WriteNotFound(w, r, "project not found") return } slog.Error("failed to get project status", "error", err, "name", name) api.WriteInternalError(w, r, "failed to get project status") return } api.WriteSuccess(w, r, map[string]any{ "project_id": status.ProjectID, "name": status.Name, "description": status.Description, "git": map[string]string{ "owner": status.GitRepoOwner, "name": status.GitRepoName, "clone_ssh": status.CloneSSH, "clone_http": status.CloneHTTP, "html_url": status.HTMLURL, }, "domain": status.Domain, "custom_domain": status.CustomDomain, "url": status.URL, "deployment": map[string]any{ "status": status.DeploymentStatus, "image": status.DeploymentImage, "replicas": status.DeploymentReplicas, "ready_replicas": status.ReadyReplicas, }, }) } // Delete removes a project and its associated resources. // DELETE /project/{name} func (h *ProjectManagementHandler) Delete(w http.ResponseWriter, r *http.Request) { name := chi.URLParam(r, "name") ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second) defer cancel() if h.infraService == nil { api.WriteInternalError(w, r, "project infrastructure service not configured") return } err := h.infraService.DeleteProject(ctx, name) if err != nil { // Check if it's a "not found" error if strings.Contains(err.Error(), "not found") { api.WriteNotFound(w, r, "project not found") return } slog.Error("failed to delete project", "error", err, "name", name) api.WriteInternalError(w, r, "failed to delete project") return } api.WriteSuccess(w, r, map[string]string{ "status": "deleted", "project": name, }) }