diff --git a/internal/app/integrations.go b/internal/app/integrations.go index 8ddfe7c..79bd4c1 100644 --- a/internal/app/integrations.go +++ b/internal/app/integrations.go @@ -80,6 +80,27 @@ func (a *App) ensureCaddyRoute(ctx context.Context, p Project) error { return nil } +func (a *App) fetchCaddyConfig(ctx context.Context) (map[string]any, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, a.cfg.CaddyAdminURL+"/config/", nil) + if err != nil { + return nil, err + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("caddy config request failed: %w", err) + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode >= 300 { + return nil, fmt.Errorf("caddy config error: status=%d body=%s", resp.StatusCode, string(body)) + } + var out map[string]any + if err := json.Unmarshal(body, &out); err != nil { + return nil, fmt.Errorf("caddy config parse failed: %w", err) + } + return out, nil +} + func (a *App) ensureForgejoDeployWebhook(ctx context.Context, p Project) error { if a.cfg.ForgejoBaseURL == "" || a.cfg.ForgejoToken == "" || p.RepoURL == "" { return nil diff --git a/internal/app/project_service.go b/internal/app/project_service.go index f6c24c0..cf115c1 100644 --- a/internal/app/project_service.go +++ b/internal/app/project_service.go @@ -24,6 +24,7 @@ func (a *App) listProjects(ctx context.Context, userID string) ([]Project, error return nil, err } p.WebhookURL = a.deployWebhookURL(p.ID, p.DeployToken) + p.AppURL = a.appURL(p.RouteHost) out = append(out, p) } return out, rows.Err() @@ -70,6 +71,7 @@ func (a *App) createProject(ctx context.Context, user User, workspaceID int64, n p.Workspace = workspace.Name p.UnixUser = workspace.UnixUser p.WebhookURL = a.deployWebhookURL(p.ID, p.DeployToken) + p.AppURL = a.appURL(p.RouteHost) return p, a.provisionProject(ctx, &p) } @@ -152,6 +154,7 @@ func (a *App) getProject(ctx context.Context, userID string, projectID int64) (P return Project{}, err } p.WebhookURL = a.deployWebhookURL(p.ID, p.DeployToken) + p.AppURL = a.appURL(p.RouteHost) return p, nil } diff --git a/internal/app/routes_handlers.go b/internal/app/routes_handlers.go index 93c9ed2..fac0f36 100644 --- a/internal/app/routes_handlers.go +++ b/internal/app/routes_handlers.go @@ -15,6 +15,7 @@ func (a *App) routes() http.Handler { 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/caddy/config", a.withAuth(a.handleCaddyConfig)) mux.HandleFunc("/api/webhooks/deploy/", a.handleDeployWebhook) return loggingMiddleware(mux) } @@ -268,3 +269,16 @@ func (a *App) handleProjectLogs(w http.ResponseWriter, r *http.Request, user Use } writeJSON(w, http.StatusOK, map[string]any{"lines": lines, "logs": logs}) } + +func (a *App) handleCaddyConfig(w http.ResponseWriter, r *http.Request, _ User) { + if r.Method != http.MethodGet { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + cfg, err := a.fetchCaddyConfig(r.Context()) + if err != nil { + http.Error(w, err.Error(), http.StatusBadGateway) + return + } + writeJSON(w, http.StatusOK, cfg) +} diff --git a/internal/app/types.go b/internal/app/types.go index 741097e..5711a9a 100644 --- a/internal/app/types.go +++ b/internal/app/types.go @@ -47,6 +47,7 @@ type Project struct { RepoPrivate bool `json:"repo_private"` ServiceName string `json:"service_name"` RouteHost string `json:"route_host"` + AppURL string `json:"app_url"` TargetPort int `json:"target_port"` WorkspaceID int64 `json:"workspace_id"` Workspace string `json:"workspace"` diff --git a/internal/app/util.go b/internal/app/util.go index 922b5b7..bf1cb1d 100644 --- a/internal/app/util.go +++ b/internal/app/util.go @@ -50,6 +50,14 @@ func (a *App) deployWebhookURL(projectID int64, token string) string { return path } +func (a *App) appURL(routeHost string) string { + routeHost = strings.TrimSpace(routeHost) + if routeHost == "" { + return "" + } + return "https://" + routeHost +} + func newDeployToken() (string, error) { buf := make([]byte, 24) if _, err := rand.Read(buf); err != nil { diff --git a/web/app.js b/web/app.js index 48ade3b..f958d91 100644 --- a/web/app.js +++ b/web/app.js @@ -15,8 +15,10 @@ async function init() { document.getElementById('workspaceForm').addEventListener('submit', createWorkspace); document.getElementById('createForm').addEventListener('submit', createProject); + document.getElementById('refreshCaddyBtn').addEventListener('click', loadCaddyConfig); await refreshWorkspaces(); await refreshProjects(); + await loadCaddyConfig(); } async function refreshWorkspaces() { @@ -52,6 +54,7 @@ function renderProjects() {

workspace: ${escapeHtml(p.workspace)} (${escapeHtml(p.unix_user)})

slug: ${escapeHtml(p.slug)} | service: ${escapeHtml(p.service_name)}

app target: 127.0.0.1:${escapeHtml(p.target_port)} | route host: ${escapeHtml(p.route_host || 'not set')}

+

app url: ${p.app_url ? `${escapeHtml(p.app_url)}` : 'not set'}

deploy webhook: ${escapeHtml(p.webhook_url || 'n/a')}

provisioning: ${escapeHtml(p.provision_state || 'unknown')}${p.provision_error ? ` | error: ${escapeHtml(p.provision_error)}` : ''}

${escapeHtml(p.description || '')}

@@ -194,6 +197,17 @@ async function createWorkspace(e) { } } +async function loadCaddyConfig() { + const out = document.getElementById('caddyConfigOut'); + out.textContent = 'Loading Caddy config...'; + try { + const cfg = await api('/api/caddy/config'); + out.textContent = JSON.stringify(cfg, null, 2); + } catch (err) { + out.textContent = `Error: ${err.message}`; + } +} + function escapeHtml(s) { return String(s) .replaceAll('&', '&') diff --git a/web/index.html b/web/index.html index 362d778..ccbb17e 100644 --- a/web/index.html +++ b/web/index.html @@ -45,6 +45,12 @@

Your Projects

+ +
+

Caddy Config

+ +
Not loaded.
+