Replace deduplicateEnvPairs with validateEnvPairs that returns a 400
Bad Request error when duplicate keys are submitted, instead of
silently deduplicating (last wins).
Ref: #158
- Server-side duplicate key dedup (last wins) via deduplicateEnvPairs helper
- Cross-app isolation test verifying env var save scopes by app_id
- http.MaxBytesReader wraps request body with 1MB limit
- Test for oversized body rejection (400)
- Remove DeleteEnvVarsByAppID() which became dead code after
ReplaceEnvVarsByAppID() was introduced (handles deletion internally
within its transaction).
- Add TestHandleEnvVarSaveEmptyKeyRejected to verify that POSTing a
JSON array with an empty key returns 400 Bad Request.
Addresses review advisories on PR #158.
- Wrap DELETE + INSERTs in a database transaction via new
ReplaceEnvVarsByAppID() to prevent silent data loss on partial
insert failure. Rollback on any error; return 500 instead of 200.
- Add server-side validation rejecting entries with empty keys
(returns 400 with error message).
- Add frontend error handling for non-2xx responses with user-visible
alert messages.
- Remove stale //nolint:dupl directives (files no longer duplicate).
Replace the string-serialized KEY=value format with a proper JSON array
of {key, value} objects for the env var save endpoint.
Frontend changes:
- envVarEditor.submitAll() now uses fetch() with Content-Type:
application/json and X-CSRF-Token header instead of form submission
- Sends JSON array: [{"key":"FOO","value":"bar"}, ...]
- Hidden bulk form replaced with hidden div holding CSRF token
- envVarEditor now receives appId parameter for the fetch URL
Backend changes:
- HandleEnvVarSave reads JSON body via json.NewDecoder instead of
parsing form values with parseEnvPairs
- Returns JSON {"ok": true} instead of HTTP redirect
- Removed parseEnvPairs function and envPair struct entirely
- Added envPairJSON struct with json tags for deserialization
Tests updated to POST JSON arrays instead of form-encoded strings.
Closes #163
Replace individual env var add/edit/delete with a single bulk save
endpoint. The UI now shows a textarea with KEY=VALUE lines. On save,
all existing env vars are deleted and the full submitted set is
inserted.
- Replace HandleEnvVarAdd, HandleEnvVarEdit, HandleEnvVarDelete with
HandleEnvVarSave
- Collapse 3 routes into single POST /apps/{id}/env
- Template uses textarea instead of per-row edit/delete forms
- No individual env var IDs exposed in the UI
- Extract parseEnvPairs helper to keep cyclomatic complexity low
- Use strings.SplitSeq per modernize linter
- Update tests for new bulk save behavior
closes #156
closes #163
## Summary
Adds a per-app webhook event history page at `/apps/{id}/webhooks` showing received webhook events with match/no-match status.
## Changes
- **New template** `webhook_events.html` — displays webhook events in a table with time, event type, branch, commit SHA (linked when URL available), and match status badges
- **New handler** `HandleAppWebhookEvents()` in `webhook_events.go` — fetches app and its webhook events (limit 100)
- **New route** `GET /apps/{id}/webhooks` — registered in protected routes group
- **Template registration** — added `webhook_events.html` to the template cache in `templates.go`
- **Model enhancement** — added `ShortCommit()` method to `WebhookEvent` for truncated SHA display
- **App detail link** — added "Event History" link in the Webhook URL card on the app detail page
## UI
Follows the existing UI patterns (Tailwind CSS classes, Alpine.js `relativeTime`, badge styles, empty state, back-navigation). The page mirrors the deployments history page layout.
closes [#85](#85)
Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Reviewed-on: #164
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
## Summary
`HandleAppCreate` was not reading `docker_network`, `ntfy_topic`, or `slack_webhook` form values from the create app form. These fields were silently dropped during app creation, even though:
- `app_new.html` had the form fields
- `CreateAppInput` had the corresponding struct fields
- `CreateApp` already handled them correctly
The edit/update flow was unaffected — the bug was exclusively in the create path.
## Changes
- Read `docker_network`, `ntfy_topic`, `slack_webhook` form values in `HandleAppCreate`
- Pass them to `CreateAppInput`
- Include them in template re-render data (preserves values on validation errors)
closes #157
<!-- session: agent:sdlc-manager:subagent:1fb3582d-1eff-4309-b166-df5046a1b885 -->
Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Reviewed-on: #160
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
Change route patterns in routes.go from /env-vars to /env and update
edit/delete form actions in app_detail.html to match. The add form
already used /env and was correct.
Update test route setup to match the new /env paths.
Closes#156
Changes the Go module path from `git.eeqj.de/sneak/upaas` to `sneak.berlin/go/upaas`.
All import paths in Go files updated accordingly. `go mod tidy` and `make check` pass cleanly.
fixes#143
Co-authored-by: user <user@Mac.lan guest wan>
Co-authored-by: Jeffrey Paul <sneak@noreply.example.org>
Reviewed-on: #149
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
Remove POST /apps, DELETE /apps/{id}, and POST /apps/{id}/deploy from
the API v1 route group. These endpoints used cookie-based session auth
without CSRF protection, creating a CSRF vulnerability.
Read-only endpoints (GET /apps, GET /apps/{id}, GET /apps/{id}/deployments),
login, and whoami are retained.
Removed handlers: HandleAPICreateApp, HandleAPIDeleteApp,
HandleAPITriggerDeploy, along with apiCreateRequest struct and
validateCreateRequest function.
Updated tests to use service layer directly for app creation in
remaining read-only endpoint tests.
Add SanitizeLogs() that strips ANSI escape sequences and non-printable
control characters (preserving newlines, carriage returns, and tabs)
from all container and deployment log output paths:
- HandleAppLogs (text/plain response)
- HandleDeploymentLogsAPI (JSON response)
- HandleContainerLogsAPI (JSON response)
Container log output is attacker-controlled data. Content-Type alone
is insufficient — the data itself must be sanitized before serving.
Includes comprehensive test coverage for the sanitization function.
- Changed SCP regex to only accept 'git' as the username
- Added path traversal check: reject URLs containing '..'
- Added test cases for non-git users and path traversal
- Add .gitea/workflows/check.yml running make check on PRs and pushes to main
- Fix .golangci.yml for golangci-lint v2 config format (was using v1 keys)
- Migrate linters-settings to linters.settings, remove deprecated exclude-use-default
- Exclude gosec false positives (G117, G703, G704, G705) with documented rationale
- Increase lll line-length from 88 to 120 (88 was too restrictive for idiomatic Go)
- Increase dupl threshold from 100 to 150 (similar CRUD handlers are intentional)
- Fix funcorder: move RemoveImage before unexported methods in docker/client.go
- Fix wsl_v5: add required blank line in deploy.go
- Fix revive unused-parameter in export_test.go
- Fix gosec G306: tighten test file permissions to 0600
- Add html.EscapeString for log output, filepath.Clean for log path
- Remove stale //nolint:funlen directives no longer needed with v2 config
- Remove API token system entirely (model, migration, middleware)
- Add migration 007 to drop api_tokens table
- Add POST /api/v1/login endpoint for JSON credential auth
- API routes now use session cookies (same as web UI)
- Remove /api/v1/tokens endpoint
- HandleAPIWhoAmI uses session auth instead of token context
- APISessionAuth middleware returns JSON 401 instead of redirect
- Update all API tests to use cookie-based authentication
Addresses review comment on PR #74.
Add inline edit functionality for environment variables, labels, and
volume mounts on the app detail page. Each entity row now has an Edit
button that reveals an inline form using Alpine.js.
- POST /apps/{id}/env-vars/{varID}/edit
- POST /apps/{id}/labels/{labelID}/edit
- POST /apps/{id}/volumes/{volumeID}/edit
- Path validation for volume host and container paths
- Warning banner about container restart after env var changes
- Tests for ValidateVolumePath
fixes#67
- Add API token model with SHA-256 hashed tokens
- Add migration 006_add_api_tokens.sql
- Add Bearer token auth middleware
- Add API endpoints under /api/v1/:
- GET /whoami
- POST /tokens (create new API token)
- GET /apps (list all apps)
- POST /apps (create app)
- GET /apps/{id} (get app)
- DELETE /apps/{id} (delete app)
- POST /apps/{id}/deploy (trigger deployment)
- GET /apps/{id}/deployments (list deployments)
- Add comprehensive tests for all API endpoints
- All tests pass, zero lint issues
- Add previous_image_id column to apps table (migration 006)
- Save current image as previous before deploying new one
- POST /apps/{id}/rollback endpoint with handler
- Rollback stops current container, starts previous image
- Creates deployment record for rollback operations
- Rollback button in app detail UI (only when previous image exists)
- Add btn-warning CSS class for rollback button styling
fixes#71
Add POST /apps/{id}/deployments/cancel endpoint that allows users to
cancel in-progress deployments via the web UI.
Changes:
- Add CancelDeploy() and HasActiveDeploy() public methods to deploy service
- Add HandleCancelDeploy handler
- Wire route in routes.go
- Add cancel button to app detail template (shown during active deployments)
- Add handler tests for cancel endpoint
fixes#66
When a webhook-triggered deploy starts for an app that already has a deploy
in progress, the existing deploy is now cancelled via context cancellation
before the new deploy begins. This prevents silently lost webhook deploys.
Changes:
- Add per-app active deploy tracking with cancel func and done channel
- Deploy() accepts cancelExisting param: true for webhook, false for manual
- Cancelled deployments are marked with new 'cancelled' status
- Add ErrDeployCancelled sentinel error
- Add DeploymentStatusCancelled model constant
- Add comprehensive tests for cancellation mechanics
Validate app names in both HandleAppCreate and HandleAppUpdate using
a regex pattern matching the client-side HTML pattern: lowercase
alphanumeric and hyphens, 2-63 chars, must start and end with
alphanumeric character.
This prevents Docker API errors, path traversal, and log injection
from crafted POST requests bypassing browser validation.
Add renderTemplate helper method on Handlers that renders templates to a
bytes.Buffer first, then writes to the ResponseWriter only on success.
This prevents partial/corrupt HTML when template execution fails partway
through.
Applied to all template rendering call sites in:
- setup.go (HandleSetupGET, renderSetupError)
- auth.go (HandleLoginGET, HandleLoginPOST error paths)
- dashboard.go (HandleDashboard)
- app.go (HandleAppNew, HandleAppCreate, HandleAppDetail, HandleAppEdit,
HandleAppUpdate, HandleAppDeployments)
- Add sanitizeTail() helper that validates tail is numeric and positive
- Clamp values to max 500
- Default to 500 when empty, non-numeric, zero, or negative
- Add comprehensive test cases
Verify that the resource's AppID matches the URL path app ID before
allowing deletion. Without this check, any authenticated user could
delete resources belonging to any app by providing the target resource's
ID in the URL regardless of the app ID in the path (IDOR vulnerability).
Closes#19
Tests demonstrate that env vars, labels, volumes, and ports can be
deleted via another app's URL path without ownership checks.
All 4 tests fail, confirming the vulnerability described in #19.