270 lines
8.2 KiB
Go
270 lines
8.2 KiB
Go
package app
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
osuser "os/user"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
func (a *App) routes() http.Handler {
|
|
mux := http.NewServeMux()
|
|
mux.Handle("/", http.FileServer(http.Dir("./web")))
|
|
mux.HandleFunc("/api/me", a.withAuth(a.handleMe))
|
|
mux.HandleFunc("/api/workspaces", a.withAuth(a.handleWorkspaces))
|
|
mux.HandleFunc("/api/projects", a.withAuth(a.handleProjects))
|
|
mux.HandleFunc("/api/projects/", a.withAuth(a.handleProjectSubroutes))
|
|
mux.HandleFunc("/api/webhooks/deploy/", a.handleDeployWebhook)
|
|
return loggingMiddleware(mux)
|
|
}
|
|
|
|
func (a *App) handleMe(w http.ResponseWriter, _ *http.Request, user User) {
|
|
writeJSON(w, http.StatusOK, user)
|
|
}
|
|
|
|
func (a *App) handleWorkspaces(w http.ResponseWriter, r *http.Request, user User) {
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
items, err := a.listWorkspaces(r.Context(), user.Username)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, items)
|
|
case http.MethodPost:
|
|
var req struct {
|
|
Name string `json:"name"`
|
|
UnixUser string `json:"unix_user"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "invalid json", http.StatusBadRequest)
|
|
return
|
|
}
|
|
req.Name = strings.TrimSpace(req.Name)
|
|
req.UnixUser = strings.TrimSpace(req.UnixUser)
|
|
if req.Name == "" || req.UnixUser == "" {
|
|
http.Error(w, "name and unix_user are required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if _, err := osuser.Lookup(req.UnixUser); err != nil {
|
|
http.Error(w, "unix_user does not exist on host", http.StatusBadRequest)
|
|
return
|
|
}
|
|
item, err := a.createWorkspace(r.Context(), user.Username, req.Name, req.UnixUser)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, item)
|
|
default:
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
func (a *App) handleProjects(w http.ResponseWriter, r *http.Request, user User) {
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
projects, err := a.listProjects(r.Context(), user.Username)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, projects)
|
|
case http.MethodPost:
|
|
var req struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
RepoPrivate bool `json:"repo_private"`
|
|
RepoURL string `json:"repo_url"`
|
|
TargetPort int `json:"target_port"`
|
|
WorkspaceID int64 `json:"workspace_id"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "invalid json", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if strings.TrimSpace(req.Name) == "" {
|
|
http.Error(w, "name is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if req.WorkspaceID <= 0 {
|
|
http.Error(w, "workspace_id is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
req.RepoURL = strings.TrimSpace(req.RepoURL)
|
|
if req.RepoURL != "" && !isValidRepoURL(req.RepoURL) {
|
|
http.Error(w, "repo_url must be a valid repo URL (https://..., ssh://..., or git@...)", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if req.TargetPort == 0 {
|
|
req.TargetPort = 8000
|
|
}
|
|
if req.TargetPort < 1 || req.TargetPort > 65535 {
|
|
http.Error(w, "target_port must be 1-65535", http.StatusBadRequest)
|
|
return
|
|
}
|
|
project, err := a.createProject(r.Context(), user, req.WorkspaceID, req.Name, req.Description, req.RepoPrivate, req.RepoURL, req.TargetPort)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, project)
|
|
default:
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
func (a *App) handleProjectSubroutes(w http.ResponseWriter, r *http.Request, user User) {
|
|
path := strings.TrimPrefix(r.URL.Path, "/api/projects/")
|
|
parts := strings.Split(path, "/")
|
|
if len(parts) < 1 || parts[0] == "" {
|
|
http.Error(w, "bad path", http.StatusBadRequest)
|
|
return
|
|
}
|
|
projectID, err := strconv.ParseInt(parts[0], 10, 64)
|
|
if err != nil {
|
|
http.Error(w, "invalid project id", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if len(parts) == 1 {
|
|
http.Error(w, "unsupported endpoint", http.StatusNotFound)
|
|
return
|
|
}
|
|
switch parts[1] {
|
|
case "env":
|
|
a.handleProjectEnv(w, r, user, projectID)
|
|
case "service":
|
|
a.handleServiceRegenerate(w, r, user, projectID)
|
|
case "reprovision":
|
|
a.handleProjectReprovision(w, r, user, projectID)
|
|
case "status":
|
|
a.handleProjectStatus(w, r, user, projectID)
|
|
case "logs":
|
|
a.handleProjectLogs(w, r, user, projectID)
|
|
default:
|
|
http.Error(w, "unsupported endpoint", http.StatusNotFound)
|
|
}
|
|
}
|
|
|
|
func (a *App) handleProjectReprovision(w http.ResponseWriter, r *http.Request, user User, projectID int64) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
p, err := a.reprovisionProject(r.Context(), user.Username, projectID)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, p)
|
|
}
|
|
|
|
func (a *App) handleProjectEnv(w http.ResponseWriter, r *http.Request, user User, projectID int64) {
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
envVars, err := a.listEnvVars(r.Context(), user.Username, projectID)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, envVars)
|
|
case http.MethodPost:
|
|
var req struct {
|
|
Key string `json:"key"`
|
|
Value string `json:"value"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "invalid json", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := validateEnvKey(req.Key); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := a.upsertEnvVar(r.Context(), user.Username, projectID, req.Key, req.Value); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
|
case http.MethodDelete:
|
|
key := r.URL.Query().Get("key")
|
|
if err := validateEnvKey(key); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := a.deleteEnvVar(r.Context(), user.Username, projectID, key); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
|
default:
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
func (a *App) handleServiceRegenerate(w http.ResponseWriter, r *http.Request, user User, projectID int64) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
p, err := a.getProject(r.Context(), user.Username, projectID)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := a.renderSystemdForProject(r.Context(), p); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]string{"status": "service regenerated"})
|
|
}
|
|
|
|
func (a *App) handleProjectStatus(w http.ResponseWriter, r *http.Request, user User, projectID int64) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
p, err := a.getProject(r.Context(), user.Username, projectID)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
status, err := a.getServiceStatus(p)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadGateway)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, status)
|
|
}
|
|
|
|
func (a *App) handleProjectLogs(w http.ResponseWriter, r *http.Request, user User, projectID int64) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
p, err := a.getProject(r.Context(), user.Username, projectID)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
lines := 100
|
|
const maxLogBytes = 64 * 1024
|
|
if raw := r.URL.Query().Get("lines"); raw != "" {
|
|
n, err := strconv.Atoi(raw)
|
|
if err != nil || n < 1 || n > 300 {
|
|
http.Error(w, "lines must be 1-300", http.StatusBadRequest)
|
|
return
|
|
}
|
|
lines = n
|
|
}
|
|
logs, err := a.getServiceLogs(p, lines)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadGateway)
|
|
return
|
|
}
|
|
if len(logs) > maxLogBytes {
|
|
logs = logs[:maxLogBytes] + "\n... (truncated)"
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"lines": lines, "logs": logs})
|
|
}
|