CLAUDE.md
Rama main · réplica de lectura del repositorio original.
# wgroster — project guide for Claude Code
Self-service WireGuard configuration portal: LDAP (or local-admin) login,
admin-managed **endpoints** (concentrators) and **machines**, and a live status
dashboard fed by `wg show dump` uploads. The portal is the **source of truth**;
it never connects to or configures concentrators — agents on each concentrator
push status and optionally pull the expected peer list.
Global preferences (git, language, etc.) live in the user's `~/.claude/CLAUDE.md`
and apply here too. This file only adds project-specific guidance.
## Commands
```sh
# Build
go build ./...
# Checks (run all three before proposing a commit)
gofmt -l . # must print nothing
go vet ./...
go test ./...
# Run locally (dev): uses ./config.yaml + ./data/wg-portal.db, listens on :8484.
# Dev config has local_admin admin/admin and points ldap at the real server.
go run ./cmd/wgroster -config config.yaml
# Generate a bcrypt hash for local_admin.password_hash
go run ./cmd/wgroster -hash-password
```
Tailwind CSS (**v4**) is **prebuilt** and committed at
`internal/web/static/app.css`. After changing any template's CSS classes,
regenerate it (otherwise new classes have no styles):
```sh
# One-off: grab the standalone CLI (no node/npm needed, nothing to commit).
curl -fsSL -o /tmp/tailwindcss \
https://github.com/tailwindlabs/tailwindcss/releases/download/v4.3.3/tailwindcss-macos-arm64
chmod +x /tmp/tailwindcss
/tmp/tailwindcss -i tailwind.input.css -o internal/web/static/app.css --minify
```
Swap `macos-arm64` for your platform (`linux-x64`, …). `npx @tailwindcss/cli`
does **not** work here: v4 resolves `@import "tailwindcss"` from the input CSS
file's directory, so it needs a real `node_modules` at the repo root.
Configuration is CSS-first in `tailwind.input.css` — there is no
`tailwind.config.js`. `@custom-variant dark` replaces v3's `darkMode: "class"`,
and `source(none)` plus an explicit `@source` keeps content scanning limited to
the templates.
## Architecture
Everything is a single Go binary; the web layer is server-rendered HTML +
htmx (no SPA, no build step for JS). Packages under `internal/`:
- `config` — YAML config loading and validation (`config.go`).
- `store` — SQLite persistence (pure-Go `modernc.org/sqlite`). One file per
concern (`machines.go`, `endpoints.go`, `status.go`, `profiles.go`, …).
- `ldap` — OpenLDAP bind, admin-group membership, profile (name/photo) lookup.
- `ipam` — global client address pool + next-free suggestion.
- `wg` — generates client/concentrator configs (`config.go`) and parses
`wg show dump` (`dump.go`).
- `geoip` — optional offline MaxMind lookups for the peer drawer.
- `auth` — server-side sessions (HMAC-signed cookie holds only the sid) + CSRF.
- `web` — HTTP handlers, middleware, templates, embedded static assets. Routes
are all wired in `server.go`'s `Handler()`.
Templates live in `internal/web/templates/`; full pages and partials are
registered in `render.go` (`fullPages` / `partials`). Template funcs (e.g.
`initial`, `ago`, `since`, `humanBytes`) are defined there too.
## Domain model
- **Endpoint** = a WireGuard concentrator (hub). Has a public key, `host:port`,
`AllowedIPs`, and a rotatable **upload token** used by its agent for the
machine-to-machine API (`/api/endpoints/{id}/status`, `/expected-peers`,
`/config`). Token auth returns 404 (not 403) on mismatch so ids can't be
enumerated.
- **Machine** = a user device with one global VPN IP (from the pool) linked to
one or more endpoints. Users add machines (pending); admins approve/assign
IP+endpoints. `owner_name` is a cached LDAP `cn`; `user_profile` caches the
name+photo (see LDAP section).
- Config generation is in `internal/wg/config.go`: `ClientConfig` (per machine)
and `ConcentratorConfig` (per endpoint). The portal never knows any private
key, so generated configs emit a `<...PRIVATE_KEY>` placeholder.
## Conventions & gotchas
- **SQLite**: schema is a single `CREATE TABLE IF NOT EXISTS` block in
`store.go`; add new columns as idempotent `ALTER TABLE` in the migration loop
right after it (guarded against "duplicate column name"). `SetMaxOpenConns(1)`
— writes are serialized; keep queries simple.
- **Security posture** (do not regress): strict CSP with self-hosted assets and
**no inline script/style** (`img-src 'self'`: avatars come from
`/avatar/{uid}`, QR codes are inline SVG); all mutating routes require a CSRF
token; login rejects cross-origin POSTs; secrets
(tokens, hashes, photos) live only in the 0600 DB. Fields written line-by-line
into wg configs are rejected if they contain control chars (config injection).
- **LDAP profiles**: name (`name_attr`, default `cn`) and photo (`photo_attr`,
default `jpegPhoto`) are cached in `user_profile` and served at
`/avatar/{uid}`. The logged-in user's own profile is read at login via their
bind; other users are resolved lazily in the background from the admin
machines list — with `search_bind_dn` if set, otherwise **anonymously**. All
of it degrades gracefully to the login + initial badge when unavailable.
- **Handlers**: return generic 500s via `s.serverError` (never leak SQL/paths);
redirect with `redirectMsg(w, r, path, "ok"|"err", msg)`; record admin actions
with `s.audit`.
- **Tests**: `internal/web/integration_test.go` drives the real handler stack
against a temp SQLite DB (`testServer` helper, local-admin session). Add
behavior tests there; store-level tests use `newTestStore`. LDAP tests run
against `internal/ldap/fakedir_test.go`, a minimal in-process LDAPv3 directory
(simple bind, base search, StartTLS) that records the DNs bound, filters sent
and attributes requested — assert on those rather than mocking the client.
## Verifying UI changes
`go run ./cmd/wgroster -config config.yaml` then hit `http://localhost:8484`
(login `admin`/`admin`). The dev DB is seeded and points at the real LDAP, so
directory names/photos populate on the machines page. Before killing a dev
server you started, confirm the PID is the wgroster binary you launched.