use caddyfile instead of api
This commit is contained in:
parent
53a5e2e45a
commit
3c79a8c778
9 changed files with 206 additions and 248 deletions
|
|
@ -13,6 +13,13 @@ FORGEJO_TOKEN=
|
|||
FORGEJO_GIT_USERNAME=oauth2
|
||||
FORGEJO_ORG=
|
||||
CADDY_ROUTE_ENABLED=false
|
||||
CADDY_AUTH_ENABLED=true
|
||||
CADDY_AUTH_UPSTREAM=localhost:9000
|
||||
CADDY_AUTH_URI=/outpost.goauthentik.io/auth/caddy
|
||||
CADDY_AUTH_IMPORT_NAME=auth
|
||||
CADDY_FRAGMENT_PATH=/etc/caddy/fragments/project-manager.caddy
|
||||
CADDY_RELOAD_COMMAND=sudo -n systemctl reload caddy
|
||||
CADDY_PERSIST_CONFIG_PATH=
|
||||
CADDY_ADMIN_URL=http://localhost:2019
|
||||
CADDY_SERVER_ID=srv0
|
||||
CADDY_DOMAIN_SUFFIX=
|
||||
|
|
|
|||
29
README.md
29
README.md
|
|
@ -6,7 +6,7 @@ Features:
|
|||
- Workspace model mapping each app-user workspace to a host Unix account
|
||||
- Create projects per authenticated user
|
||||
- Auto-create Forgejo repository or onboard an existing repository URL
|
||||
- Auto-create Caddy reverse-proxy route via Admin API (optional)
|
||||
- Auto-generate Caddyfile route fragment and reload Caddy (optional)
|
||||
- Manage project env vars
|
||||
- Auto-generate user-scoped systemd service per project
|
||||
- PostgreSQL persistence with startup migration
|
||||
|
|
@ -43,20 +43,33 @@ Important:
|
|||
- `FORGEJO_ORG` switches creation from personal repos to org repos.
|
||||
- `UNIX_SOCKET_PATH` enables unix socket listener in addition to `LISTEN_ADDR`.
|
||||
- `CADDY_ROUTE_ENABLED=true` enables route provisioning.
|
||||
- `CADDY_AUTH_ENABLED=true` enables authentik-style forward auth on provisioned app routes.
|
||||
- `CADDY_AUTH_UPSTREAM` sets auth upstream dial (default `localhost:9000`).
|
||||
- `CADDY_AUTH_URI` sets auth check URI (default `/outpost.goauthentik.io/auth/caddy`).
|
||||
- `CADDY_FRAGMENT_PATH` points to generated Caddyfile fragment path (default `/etc/caddy/fragments/project-manager.caddy`).
|
||||
- `CADDY_RELOAD_COMMAND` command run after fragment write (default `sudo -n systemctl reload caddy`).
|
||||
- `CADDY_AUTH_IMPORT_NAME` snippet import name used per app host block (default `auth`).
|
||||
- `CADDY_PERSIST_CONFIG_PATH` is only useful if you still use Caddy Admin API externally.
|
||||
- `CADDY_ADMIN_URL` points to the Caddy Admin API (default `http://localhost:2019`).
|
||||
- `CADDY_SERVER_ID` is the HTTP server object id under `apps.http.servers` (default `srv0`).
|
||||
- `CADDY_DOMAIN_SUFFIX` controls generated host as `<slug>.<suffix>` (if empty: `<user>-<slug>.local`).
|
||||
- `WEBHOOK_BASE_URL` sets absolute webhook URLs returned by API/UI (recommended behind reverse proxy).
|
||||
|
||||
## Caddy API Behavior
|
||||
## Caddyfile Fragment Behavior
|
||||
|
||||
On project creation, when Caddy routing is enabled, the app appends a route to:
|
||||
- `POST /config/apps/http/servers/<CADDY_SERVER_ID>/routes`
|
||||
On provisioning/reprovision, when `CADDY_ROUTE_ENABLED=true`, the app regenerates a managed fragment containing all project routes and then runs `CADDY_RELOAD_COMMAND`.
|
||||
|
||||
Route shape:
|
||||
- `match.host = [<project host>]`
|
||||
- `handle[0].handler = reverse_proxy`
|
||||
- `handle[0].upstreams[0].dial = unix/<unix-user-home>/projects/<app>/app.sock`
|
||||
Each route block is:
|
||||
- `<slug>.<domain> {`
|
||||
- `import auth` (name controlled by `CADDY_AUTH_IMPORT_NAME`, optional when auth enabled)
|
||||
- `log`
|
||||
- `reverse_proxy unix//<unix-user-home>/projects/<app>/app.sock`
|
||||
- `}`
|
||||
|
||||
In your static `/etc/caddy/Caddyfile`, include:
|
||||
```caddyfile
|
||||
import /etc/caddy/fragments/project-manager.caddy
|
||||
```
|
||||
|
||||
## Workspaces and Unix Users
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,13 @@ func loadConfig() (Config, error) {
|
|||
CaddyServerID: getenv("CADDY_SERVER_ID", "srv0"),
|
||||
CaddyDomainSuffix: getenv("CADDY_DOMAIN_SUFFIX", ""),
|
||||
CaddyRouteEnabled: getenvBool("CADDY_ROUTE_ENABLED", false),
|
||||
CaddyAuthEnabled: getenvBool("CADDY_AUTH_ENABLED", true),
|
||||
CaddyAuthUpstream: getenv("CADDY_AUTH_UPSTREAM", "localhost:9000"),
|
||||
CaddyAuthURI: getenv("CADDY_AUTH_URI", "/outpost.goauthentik.io/auth/caddy"),
|
||||
CaddyPersistConfigPath: strings.TrimSpace(getenv("CADDY_PERSIST_CONFIG_PATH", "")),
|
||||
CaddyFragmentPath: strings.TrimSpace(getenv("CADDY_FRAGMENT_PATH", "/etc/caddy/fragments/project-manager.caddy")),
|
||||
CaddyReloadCommand: strings.TrimSpace(getenv("CADDY_RELOAD_COMMAND", "sudo -n systemctl reload caddy")),
|
||||
CaddyAuthImportName: strings.TrimSpace(getenv("CADDY_AUTH_IMPORT_NAME", "auth")),
|
||||
WebhookBaseURL: strings.TrimRight(getenv("WEBHOOK_BASE_URL", ""), "/"),
|
||||
}
|
||||
if cfg.DatabaseURL == "" {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
|
@ -57,90 +60,109 @@ func (a *App) ensureCaddyRoute(ctx context.Context, p Project) error {
|
|||
if !a.cfg.CaddyRouteEnabled {
|
||||
return nil
|
||||
}
|
||||
projectDir, err := a.projectDirFor(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
socketPath := filepath.Join(projectDir, "app.sock")
|
||||
route := map[string]any{
|
||||
"@id": "project-route-" + p.UserID + "-" + p.Slug,
|
||||
"match": []any{map[string]any{"host": []string{p.RouteHost}}},
|
||||
"handle": []any{map[string]any{"handler": "reverse_proxy", "upstreams": []any{map[string]any{"dial": "unix/" + socketPath}}}},
|
||||
return a.syncCaddyConfigFragment(ctx)
|
||||
}
|
||||
|
||||
cfg, err := a.fetchCaddyConfig(ctx)
|
||||
func (a *App) syncCaddyConfigFragment(ctx context.Context) error {
|
||||
path := strings.TrimSpace(a.cfg.CaddyFragmentPath)
|
||||
if path == "" {
|
||||
return errors.New("CADDY_FRAGMENT_PATH is required when CADDY_ROUTE_ENABLED=true")
|
||||
}
|
||||
routes, err := a.listCaddyProjectRoutes(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newRoutes, err := mergedDedupedRoutes(cfg, a.cfg.CaddyServerID, route)
|
||||
content, err := a.renderCaddyRoutesFragment(routes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body, _ := json.Marshal(newRoutes)
|
||||
url := fmt.Sprintf("%s/config/apps/http/servers/%s/routes", a.cfg.CaddyAdminURL, a.cfg.CaddyServerID)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, strings.NewReader(string(body)))
|
||||
if err != nil {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("caddy routes rewrite failed: %w", err)
|
||||
tmp := path + ".tmp"
|
||||
if err := os.WriteFile(tmp, []byte(content), 0o640); err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode >= 300 {
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("caddy routes rewrite error: status=%d body=%s", resp.StatusCode, string(respBody))
|
||||
if err := os.Rename(tmp, path); err != nil {
|
||||
return err
|
||||
}
|
||||
return a.reloadCaddy()
|
||||
}
|
||||
|
||||
func (a *App) reloadCaddy() error {
|
||||
reloadCmd := strings.TrimSpace(a.cfg.CaddyReloadCommand)
|
||||
if reloadCmd == "" {
|
||||
return nil
|
||||
}
|
||||
cmd := exec.Command("bash", "-lc", reloadCmd)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("caddy reload failed: %w output=%s", err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mergedDedupedRoutes(cfg map[string]any, serverID string, desired map[string]any) ([]any, error) {
|
||||
apps, ok := cfg["apps"].(map[string]any)
|
||||
if !ok {
|
||||
return nil, errors.New("caddy config missing apps")
|
||||
func (a *App) listCaddyProjectRoutes(ctx context.Context) ([]Project, error) {
|
||||
rows, err := a.db.QueryContext(ctx, `
|
||||
SELECT p.id, p.user_id, p.name, p.slug, p.route_host, p.workspace_id, w.name, w.unix_user
|
||||
FROM projects p
|
||||
JOIN workspaces w ON w.id = p.workspace_id
|
||||
WHERE p.route_host <> ''
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpApp, ok := apps["http"].(map[string]any)
|
||||
if !ok {
|
||||
return nil, errors.New("caddy config missing apps.http")
|
||||
defer rows.Close()
|
||||
var out []Project
|
||||
for rows.Next() {
|
||||
var p Project
|
||||
if err := rows.Scan(&p.ID, &p.UserID, &p.Name, &p.Slug, &p.RouteHost, &p.WorkspaceID, &p.Workspace, &p.UnixUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
servers, ok := httpApp["servers"].(map[string]any)
|
||||
if !ok {
|
||||
return nil, errors.New("caddy config missing apps.http.servers")
|
||||
out = append(out, p)
|
||||
}
|
||||
server, ok := servers[serverID].(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("caddy config missing server %q", serverID)
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
routes, ok := server["routes"].([]any)
|
||||
if !ok {
|
||||
routes = []any{}
|
||||
}
|
||||
desiredID, _ := desired["@id"].(string)
|
||||
var out []any
|
||||
seenDesired := false
|
||||
for i, r := range routes {
|
||||
_ = i
|
||||
rm, ok := r.(map[string]any)
|
||||
if !ok {
|
||||
out = append(out, r)
|
||||
continue
|
||||
}
|
||||
if rid, ok := rm["@id"].(string); ok && rid == desiredID {
|
||||
if !seenDesired {
|
||||
out = append(out, desired)
|
||||
seenDesired = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
out = append(out, r)
|
||||
}
|
||||
if !seenDesired {
|
||||
out = append(out, desired)
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
if out[i].RouteHost == out[j].RouteHost {
|
||||
return out[i].ID < out[j].ID
|
||||
}
|
||||
return out[i].RouteHost < out[j].RouteHost
|
||||
})
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (a *App) renderCaddyRoutesFragment(projects []Project) (string, error) {
|
||||
var b strings.Builder
|
||||
b.WriteString("# Managed by project-manager. Do not edit manually.\n")
|
||||
for _, p := range projects {
|
||||
projectDir, err := a.projectDirFor(p)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
socketPath := filepath.Join(projectDir, "app.sock")
|
||||
b.WriteString("\n")
|
||||
b.WriteString(p.RouteHost)
|
||||
b.WriteString(" {\n")
|
||||
if a.cfg.CaddyAuthEnabled {
|
||||
importName := strings.TrimSpace(a.cfg.CaddyAuthImportName)
|
||||
if importName == "" {
|
||||
importName = "auth"
|
||||
}
|
||||
b.WriteString(" import ")
|
||||
b.WriteString(importName)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
b.WriteString(" log\n")
|
||||
b.WriteString(" reverse_proxy unix//")
|
||||
b.WriteString(socketPath)
|
||||
b.WriteString("\n")
|
||||
b.WriteString("}\n")
|
||||
}
|
||||
return b.String(), 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 {
|
||||
|
|
@ -162,151 +184,35 @@ func (a *App) fetchCaddyConfig(ctx context.Context) (map[string]any, error) {
|
|||
return out, nil
|
||||
}
|
||||
|
||||
func (a *App) ensureCaddyTLSSubject(ctx context.Context, host string) error {
|
||||
host = strings.TrimSpace(host)
|
||||
if host == "" {
|
||||
func (a *App) persistCaddyConfigToDisk(ctx context.Context) error {
|
||||
path := strings.TrimSpace(a.cfg.CaddyPersistConfigPath)
|
||||
if path == "" {
|
||||
return nil
|
||||
}
|
||||
cfg, err := a.fetchCaddyConfig(ctx)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, a.cfg.CaddyAdminURL+"/config/", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
idx, subjects, found, err := findTLSPolicyForHost(cfg, host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if found {
|
||||
return nil
|
||||
}
|
||||
if idx < 0 {
|
||||
return errors.New("no suitable Caddy TLS automation policy found")
|
||||
}
|
||||
|
||||
// Preferred: append one subject.
|
||||
appendURL := fmt.Sprintf("%s/config/apps/tls/automation/policies/%d/subjects", a.cfg.CaddyAdminURL, idx)
|
||||
body, _ := json.Marshal(host)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, appendURL, strings.NewReader(string(body)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 300 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: replace the subjects array with host appended.
|
||||
subjects = append(subjects, host)
|
||||
replaceBody, _ := json.Marshal(subjects)
|
||||
putReq, err := http.NewRequestWithContext(ctx, http.MethodPut, appendURL, strings.NewReader(string(replaceBody)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("caddy config fetch for persist failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("caddy config fetch for persist error: status=%d body=%s", resp.StatusCode, string(body))
|
||||
}
|
||||
tmp := path + ".tmp"
|
||||
if err := os.WriteFile(tmp, body, 0o640); err != nil {
|
||||
return err
|
||||
}
|
||||
putReq.Header.Set("Content-Type", "application/json")
|
||||
putResp, err := http.DefaultClient.Do(putReq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("caddy tls subjects update failed: %w", err)
|
||||
}
|
||||
defer putResp.Body.Close()
|
||||
if putResp.StatusCode >= 300 {
|
||||
b, _ := io.ReadAll(putResp.Body)
|
||||
return fmt.Errorf("caddy tls subjects update error: status=%d body=%s", putResp.StatusCode, string(b))
|
||||
return os.Rename(tmp, path)
|
||||
}
|
||||
|
||||
func (a *App) ensureCaddyTLSSubject(ctx context.Context, host string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func findTLSPolicyForHost(cfg map[string]any, host string) (policyIdx int, subjects []string, alreadyPresent bool, err error) {
|
||||
policyIdx = -1
|
||||
apps, ok := cfg["apps"].(map[string]any)
|
||||
if !ok {
|
||||
return -1, nil, false, errors.New("caddy config missing apps")
|
||||
}
|
||||
tlsObj, ok := apps["tls"].(map[string]any)
|
||||
if !ok {
|
||||
return -1, nil, false, errors.New("caddy config missing apps.tls")
|
||||
}
|
||||
automation, ok := tlsObj["automation"].(map[string]any)
|
||||
if !ok {
|
||||
return -1, nil, false, errors.New("caddy config missing apps.tls.automation")
|
||||
}
|
||||
policiesAny, ok := automation["policies"].([]any)
|
||||
if !ok || len(policiesAny) == 0 {
|
||||
return -1, nil, false, errors.New("caddy config missing tls automation policies")
|
||||
}
|
||||
|
||||
// Prefer a non-internal policy with subjects list.
|
||||
for i, pAny := range policiesAny {
|
||||
pol, ok := pAny.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
subjectsRaw, ok := pol["subjects"].([]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
cur := make([]string, 0, len(subjectsRaw))
|
||||
for _, s := range subjectsRaw {
|
||||
if sv, ok := s.(string); ok {
|
||||
cur = append(cur, sv)
|
||||
if strings.EqualFold(strings.TrimSpace(sv), host) {
|
||||
return i, cur, true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if !policyHasInternalIssuer(pol) && policyIdx < 0 {
|
||||
policyIdx = i
|
||||
subjects = cur
|
||||
}
|
||||
}
|
||||
// Fallback to first policy that has subjects.
|
||||
if policyIdx < 0 {
|
||||
for i, pAny := range policiesAny {
|
||||
pol, ok := pAny.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
subjectsRaw, ok := pol["subjects"].([]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
cur := make([]string, 0, len(subjectsRaw))
|
||||
for _, s := range subjectsRaw {
|
||||
if sv, ok := s.(string); ok {
|
||||
cur = append(cur, sv)
|
||||
if strings.EqualFold(strings.TrimSpace(sv), host) {
|
||||
return i, cur, true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
policyIdx = i
|
||||
subjects = cur
|
||||
break
|
||||
}
|
||||
}
|
||||
return policyIdx, subjects, false, nil
|
||||
}
|
||||
|
||||
func policyHasInternalIssuer(pol map[string]any) bool {
|
||||
issuersAny, ok := pol["issuers"].([]any)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
for _, iAny := range issuersAny {
|
||||
iss, ok := iAny.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if mod, ok := iss["module"].(string); ok && strings.EqualFold(strings.TrimSpace(mod), "internal") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *App) ensureForgejoDeployWebhook(ctx context.Context, p Project) error {
|
||||
if a.cfg.ForgejoBaseURL == "" || a.cfg.ForgejoToken == "" || p.RepoURL == "" {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -132,12 +132,6 @@ func (a *App) provisionProject(ctx context.Context, p *Project) error {
|
|||
p.ProvisionError = err.Error()
|
||||
return err
|
||||
}
|
||||
if err := a.ensureCaddyTLSSubject(ctx, p.RouteHost); err != nil {
|
||||
_ = a.updateProvisioningState(ctx, p.ID, "failed", err.Error())
|
||||
p.ProvisionState = "failed"
|
||||
p.ProvisionError = err.Error()
|
||||
return err
|
||||
}
|
||||
if err := a.updateProvisioningState(ctx, p.ID, "provisioned", ""); err != nil {
|
||||
p.ProvisionState = "failed"
|
||||
p.ProvisionError = err.Error()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package app
|
|||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
osuser "os/user"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
|
@ -275,10 +276,25 @@ func (a *App) handleCaddyConfig(w http.ResponseWriter, r *http.Request, _ User)
|
|||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if a.cfg.CaddyRouteEnabled && strings.TrimSpace(a.cfg.CaddyFragmentPath) != "" {
|
||||
content, err := os.ReadFile(a.cfg.CaddyFragmentPath)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"format": "caddyfile",
|
||||
"content": string(content),
|
||||
})
|
||||
return
|
||||
}
|
||||
cfg, err := a.fetchCaddyConfig(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, cfg)
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"format": "json",
|
||||
"content": mustJSONPretty(cfg),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,13 @@ type Config struct {
|
|||
CaddyServerID string
|
||||
CaddyDomainSuffix string
|
||||
CaddyRouteEnabled bool
|
||||
CaddyAuthEnabled bool
|
||||
CaddyAuthUpstream string
|
||||
CaddyAuthURI string
|
||||
CaddyPersistConfigPath string
|
||||
CaddyFragmentPath string
|
||||
CaddyReloadCommand string
|
||||
CaddyAuthImportName string
|
||||
WebhookBaseURL string
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,14 @@ func writeJSON(w http.ResponseWriter, status int, data any) {
|
|||
_ = json.NewEncoder(w).Encode(data)
|
||||
}
|
||||
|
||||
func mustJSONPretty(data any) string {
|
||||
b, err := json.MarshalIndent(data, "", " ")
|
||||
if err != nil {
|
||||
return "{}"
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func (a *App) deployWebhookURL(projectID int64, token string) string {
|
||||
if token == "" {
|
||||
return ""
|
||||
|
|
|
|||
|
|
@ -202,7 +202,7 @@ async function loadCaddyConfig() {
|
|||
out.textContent = 'Loading Caddy config...';
|
||||
try {
|
||||
const cfg = await api('/api/caddy/config');
|
||||
out.textContent = JSON.stringify(cfg, null, 2);
|
||||
out.textContent = cfg.content || JSON.stringify(cfg, null, 2);
|
||||
} catch (err) {
|
||||
out.textContent = `Error: ${err.message}`;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue