init
This commit is contained in:
commit
b38c39030b
22 changed files with 1925 additions and 0 deletions
251
internal/app/routes_handlers.go
Normal file
251
internal/app/routes_handlers.go
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
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 "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) 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 := 200
|
||||
if raw := r.URL.Query().Get("lines"); raw != "" {
|
||||
n, err := strconv.Atoi(raw)
|
||||
if err != nil || n < 1 || n > 1000 {
|
||||
http.Error(w, "lines must be 1-1000", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
lines = n
|
||||
}
|
||||
logs, err := a.getServiceLogs(p, lines)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"lines": lines, "logs": logs})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue