Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 16 additions & 8 deletions internal/handlers/stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -1265,30 +1265,38 @@ func (h *StackHandler) UpdateEnv(c *fiber.Ctx) error {
"keys_deleted": deletes,
"total_after": len(merged),
})
go func(teamID uuid.UUID, stackID uuid.UUID, slug string, meta []byte) {
// safego.Go (not a bare `go func`) so a panic in the audit insert is
// recovered instead of crashing the worker goroutine / process — matches
// the runStackDeploy/runStackRedeploy launches in this file.
safego.Go("stack.env.audit", func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if aErr := models.InsertAuditEvent(ctx, h.db, models.AuditEvent{
TeamID: teamID,
TeamID: team.ID,
Actor: auditActorSystem,
Kind: "stack.env.updated",
ResourceType: "stack",
ResourceID: uuid.NullUUID{UUID: stackID, Valid: true},
ResourceID: uuid.NullUUID{UUID: stack.ID, Valid: true},
Summary: "updated env vars on stack <code>" + slug + "</code>",
Metadata: meta,
Metadata: auditMeta,
}); aErr != nil {
slog.Warn("stack.env.audit_failed",
"error", aErr, "team_id", teamID, "stack_id", stackID, "slug", slug)
"error", aErr, "team_id", team.ID, "stack_id", stack.ID, "slug", slug)
}
}(team.ID, stack.ID, slug, auditMeta)
})

slog.Info("stack.env.updated",
"slug", slug, "team_id", team.ID, "stack_id", stack.ID,
"keys_set", len(body.Env)-deletes, "keys_deleted", deletes, "total_after", len(merged))

return c.JSON(fiber.Map{
"ok": true,
"env": merged,
"ok": true,
// Redact outbound env vars — mirrors DeployHandler.UpdateEnv and
// GET /deploy/:id. The stored value (persisted above) is the
// unredacted merged map; only the response JSON is masked so
// secrets carried over from earlier PATCHes never echo in cleartext
// into proxy logs / agent transcripts.
"env": redactEnvVars(merged),
"message": "Env vars persisted. Call POST /stacks/" + slug + "/redeploy to apply.",
})
}
Expand Down
8 changes: 6 additions & 2 deletions internal/handlers/stack_env_persist_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,14 @@ func TestStack_PatchEnv_PersistsAndReturns(t *testing.T) {
}
require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
assert.True(t, body.OK)
// The RESPONSE redacts secret-bearing values (bug bash #1): DATABASE_URL
// (key contains "URL") is masked to "***", while a non-secret key like
// NODE_ENV passes through. The STORED value stays unredacted — asserted
// against the DB below. This mirrors DeployHandler.UpdateEnv.
assert.Equal(t, map[string]string{
"DATABASE_URL": "postgres://example",
"DATABASE_URL": "***",
"NODE_ENV": "production",
}, body.Env, "response carries the merged env")
}, body.Env, "response masks secret values but keeps non-secret keys")

// DB round-trip — pre-fix this would still be `{}` because the
// handler dropped the payload. With migration 062 + UpdateStackEnvVars
Expand Down
Loading