- 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 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 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
The gorilla/sessions MaxAge field expects seconds, not nanoseconds.
Previously MaxAge was set to -1000000000 (-1 * time.Second in nanoseconds),
which worked by accident since any negative value deletes the cookie.
Changed to the conventional value of -1.
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)
realIP() now parses RemoteAddr and checks if the source IP is in
RFC1918 (10/8, 172.16/12, 192.168/16), loopback (127/8), or IPv6
ULA/loopback ranges before trusting X-Real-IP or X-Forwarded-For
headers. Public source IPs have headers ignored (fail closed).
This prevents attackers from spoofing X-Forwarded-For to bypass
the login rate limiter.
- appDetail: poll every 1s during active deployments, 10s when idle
- deploymentsPage: same adaptive polling for status checks
- Skip fetching container/build logs when panes are not visible
- Use setTimeout chains instead of setInterval for dynamic intervals
- 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
Remove the sync.Mutex and CreateUserAtomic (INSERT ON CONFLICT) in favor
of a single DB transaction in CreateFirstUser that atomically checks for
existing users and inserts. SQLite serializes write transactions, so this
is sufficient to prevent the race condition without application-level locking.
Add mutex and INSERT ON CONFLICT to CreateUser to prevent TOCTOU race
where concurrent requests could create multiple admin users.
Changes:
- Add sync.Mutex to auth.Service to serialize CreateUser calls
- Add models.CreateUserAtomic using INSERT ... ON CONFLICT(username) DO NOTHING
- Check RowsAffected to detect conflicts at the DB level (defense-in-depth)
- Add concurrent race condition test (10 goroutines, only 1 succeeds)
The existing UNIQUE constraint on users.username was already in place.
This fix adds the application-level protection (items 1 & 2 from #26).
- Validate branch names against ^[a-zA-Z0-9._/\-]+$
- Validate commit SHAs against ^[0-9a-f]{40}$
- Pass repo URL, branch, and SHA via environment variables instead of
interpolating into shell script string
- Add comprehensive tests for validation and injection rejection
Behind a reverse proxy like Traefik, RemoteAddr always contains the
proxy's IP. Add realIP() helper that checks X-Real-IP first, then the
first entry of X-Forwarded-For, falling back to RemoteAddr.
Update both LoginRateLimit and Logging middleware to use realIP().
Add comprehensive tests for the new function.
Fixes#12
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
- Store lastSeen timestamp per IP limiter entry
- Lazy sweep removes entries older than 10 minutes on each request
- Add Retry-After header to 429 responses
- Add test for stale entry eviction
Fixes memory leak under sustained attack from many IPs.
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.