Empties gitea.tf and act-runner.tf so terraform apply removes all Gitea
and runner resources. Drops the gitea namespace from the managed list.
Full config preserved in git history.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
auth/gateway, auth/users, and test/example-service were referencing
images without a registry prefix, causing k8s to fall back to Docker Hub
(which doesn't have these images).
Also generalises the gitea-registry imagePullSecret to all app namespaces
(auth, finance, home, test) via a for_each in Terraform.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The endpoint GET /api/v1/admin/runners/registration-token returns the
token — POST returns 405. Bootstrapper was silently failing, leaving
the secret empty and the act-runner unable to register.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace var.gitea_admin_password with random_password (like Grafana)
- Replace var.gitea_runner_token with terraform_data bootstrapper that
calls the Gitea admin API after first deploy and patches the secret
- Empty variables.tf — no manual secrets needed on terraform apply
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes 6 pods (3x postgresql-ha, 1x pgpool, 2x valkey-cluster) in favour
of SQLite (database) and leveldb queue, memory cache/session. Appropriate
for a single-user homelab instance with no HA requirements.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Chart 10.x pinned bitnami/redis-cluster:7.2.3-debian-11 and
bitnami/postgresql-repmgr:16.1.0-debian-11 — both removed from
Docker Hub by Bitnami. Chart 12.x replaces Redis with Valkey and
uses bitnamilegacy/ images that are still available.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In Terraform quoted strings $var is literal — only ${var} triggers
interpolation. The $$ was passing through as literal $$kube_* to
Fluent Bit, causing a record accessor syntax error on startup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds two targeted observability improvements across all homelab services.
pkg/logger/access.go (new)
HTTP access log middleware that logs one structured line per request:
method, path, status, ms, trace_id
The trace_id comes from the OTel span already in context (created by
trace.Middleware which runs outside this one), so each log entry in
Loki has a clickable link into Jaeger. Health/metrics endpoints are
excluded to avoid noise. Level is ERROR for 5xx, WARN for 4xx, INFO
otherwise.
pkg/setup/setup.go
Wire the new middleware between trace.Middleware (which creates the
span) and metrics.Middleware:
trace → AccessMiddleware → metrics → mux
Order matters: span must exist before AccessMiddleware reads it.
infrastructure/terraform/monitoring.tf
Fluent Bit was shipping all container logs to Loki with a single
static label (job=fluent-bit), making it impossible to filter logs
by service. Added a `nest/lift` filter that flattens the kubernetes
metadata block to top-level fields (kube_namespace_name,
kube_container_name, …), then promoted those as Loki label_keys.
After this change you can query:
{kube_namespace_name="finance"} |= "trace_id"
and LogQL will only return finance-api logs.
Co-authored-by: Gonçalo Rodrigues <guga@Goncalos-MacBook-Pro.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* infra(terraform): manage finance session secret via random_password
Replace the hand-rolled variable (with insecure hardcoded default) with a
random_password resource so Terraform auto-generates a 48-char secret and
owns the finance-api-secrets k8s Secret lifecycle.
To rotate: terraform taint random_password.finance_session_secret && terraform apply
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(finance): active sessions panel + account deletion with full data purge
Sessions panel (/account):
- AuthSession now stores IPAddress and Device (browser + OS hint)
populated from X-Forwarded-For / User-Agent on every login
- Lists all active sessions with device icon, IP, sign-in time
- Current session badge ("This device") — cannot be self-revoked
- DELETE /sessions/:id revokes any other session (user-scoped)
Account deletion (POST /account/delete):
- Password accounts require password confirmation
- OAuth accounts require typing email address to confirm
- deleteAllUserData purges all 12 finance collections + user record
in a single call: accounts, categories, transactions, trades,
ticker_mappings, goals, import_schedules, properties, loans,
permissions, households, sessions → then the user itself
- Clears session cookie and redirects to login with success message
Infrastructure:
- findAuthUserByID added to store + storeIface
- getSessionsByUserID, deleteSessionForUser added to store + storeIface
- contains() added to template FuncMap
- accountTmpl registered; GET /account, POST /account/delete,
DELETE /sessions/:id routes wired
- 🔐 nav icon links to /account page
- Full EN + PT i18n coverage for all new strings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(finance): expand unit test coverage from ~55% to 64.7%
- Add handler_coverage_test.go (~3300 lines) covering auth flows,
org request lifecycle, CSV bank import, property/loan views,
fiscal year operations, session management, and cross-handler
consistency (values shown on one page match actions on others)
- Add handler_org_test.go (~1800 lines) covering the full org
handler surface: teams, members, invites, events, budget lines,
tx requests (all status transitions), ledger, analysis, and reports
- Extend handler_test.go mockStore with: properties/loans slice fields,
authUsers map with session-aware lookup, household field, org maps,
and updateFiscalYearStatusErr for error-path testing
- Fix nav bar: Business and Account links now show active state and
use i18n keys (removes hardcoded emoji); add account key to en/pt locales
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Gonçalo Rodrigues <guga@Goncalos-MacBook-Pro.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(k8s): expose / without auth so homepage is publicly reachable
Adds a second Ingress (api-public) for the exact path / with no
forward-auth middleware. Traefik prefers the Exact match for the root,
while the Prefix ingress (with auth) still protects all other routes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: homepage renders correctly at / for unauthenticated visitors
Two fixes:
1. Added parseStandalone() helper — parseTmpl() roots on "" but ParseFS()
stores standalone (no {{define}}) files under their base filename, so
Execute() ran the empty root and returned Content-Length: 0.
2. Added router.priority: 100 annotation to api-public ingress so Traefik
picks the Exact / rule over the Prefix / rule (Traefik ranks by rule
string length by default, which made PathPrefix beat Path).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(k8s): remove forward-auth middleware from finance ingress
The app now handles its own auth at /auth/login — Traefik no longer
needs to forward-auth requests, which was causing redirects to
auth.homelab.local instead of finance.homelab.local.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(auth): harden authentication for cloud deployment
1. Secure cookie flag — set when BASE_URL starts with https://
2. SameSite=Strict on session cookie (was Lax)
3. Rate limiter — per-IP, 10 failures → 15-min lockout, auto-cleanup goroutine
4. Session rotation on login — old session deleted before issuing new one
(prevents session fixation attacks)
5. bcrypt cost 12 (was DefaultCost/10, OWASP minimum for cloud)
6. Security headers middleware on all responses:
X-Content-Type-Options, X-Frame-Options, Referrer-Policy,
Permissions-Policy, Content-Security-Policy, HSTS (when HTTPS)
7. Structured audit logging — login success/failure/lockout with IP + email
8. Google OAuth state cookie gets Secure flag too
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(infra): Gitea self-hosted CI/CD + MongoDB PVC + registry pipeline
- Add Gitea Helm deployment (git hosting, container registry, Gitea Actions)
- Add act runner with DinD sidecar for Docker builds in-cluster
- Add RBAC so act runner can kubectl-deploy to finance namespace
- Fix MongoDB StatefulSet: add volumeClaimTemplates (data was lost on restart)
- Configure k3d containerd to mirror git.homelab.local → Gitea NodePort 30002
- Add .gitea/workflows/finance-api.yml: test → build/push → rolling deploy
- Update finance-api deployment: Gitea registry image, imagePullPolicy Always
- Extract finance-api secrets (SESSION_SECRET, Google OAuth) into Terraform
- Add variables.tf for Gitea admin password and runner token
All changes testable on local k3d before the VPS exists.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Gonçalo Rodrigues <guga@Goncalos-MacBook-Pro.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes all ghcr.io and registry dependencies. Workflows now build
images locally, import them into k3d, and deploy with kubectl set image
— all on the self-hosted runner which already has Docker and kubectl.
Also removes the github Terraform provider and ci.tf since no registry
pull secrets or GitHub Actions secrets are needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds github provider + ci.tf which provisions:
- KUBECONFIG GitHub Actions secret (from local kubeconfig)
- ghcr-credentials k8s pull secret in finance and auth namespaces
Run `terraform apply -var github_token=<PAT>` once after cluster setup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>