# Phase 4: NPM Client **Status:** ✅ Complete **Parent plan:** [PLAN.md](./PLAN.md) **Domain:** backend ## Objective Implement the Nginx Proxy Manager API client — JWT authentication, CRUD for proxy hosts, and host lookup. ## Tasks - [x] Task 1: Create NPM client struct with base URL, cached JWT token, and auto-refresh - [x] Task 2: Implement `Authenticate(ctx, email, password)` — POST /api/tokens, store JWT - [x] Task 3: Implement `CreateProxyHost(ctx, config)` — POST /api/nginx/proxy-hosts - [x] Task 4: Implement `UpdateProxyHost(ctx, id, config)` — PUT /api/nginx/proxy-hosts/{id} - [x] Task 5: Implement `DeleteProxyHost(ctx, id)` — DELETE /api/nginx/proxy-hosts/{id} - [x] Task 6: Implement `ListProxyHosts(ctx)` — GET /api/nginx/proxy-hosts - [x] Task 7: Implement `FindProxyHostByDomain(ctx, domain)` — search existing hosts by domain name - [x] Task 8: Define proxy host config struct (domain, forward host/port, SSL settings, etc.) - [x] Task 9: Handle JWT token expiry — re-authenticate automatically on 401 ## Files to Modify/Create - `internal/npm/client.go` — NPM API client, auth, HTTP helpers - `internal/npm/types.go` — request/response types for proxy hosts ## Acceptance Criteria - Client authenticates and caches JWT - CRUD operations work for proxy hosts - Token refresh happens transparently on expiry - Proxy host config supports: domain, forward host, forward port, SSL (Let's Encrypt optional) - FindByDomain enables checking if a proxy already exists before creating ## Notes - NPM API base: typically `http://npm:81/api` - Forward host for containers: use container name on the shared Docker network - Forward port: the container's internal port (from EXPOSE) - SSL: for staging, can be disabled; production may want Let's Encrypt - NPM credentials come from settings (encrypted in SQLite, decrypted at runtime) ## Review Checklist - [ ] All tasks completed - [ ] JWT caching and refresh work correctly - [ ] HTTP errors are properly handled (not just status code, but response body) - [ ] No credentials logged or leaked in errors - [ ] Struct types match NPM API contract ## Handoff to Next Phase ### What was built - `internal/npm/types.go` — `ProxyHostConfig` (create/update input), `ProxyHost` (API response), `Meta`, auth types, and `boolInt` custom JSON type for NPM's 0/1 boolean fields. - `internal/npm/client.go` — Full NPM API client with JWT auth, auto-refresh, and CRUD. ### Public API surface ```go npm.New(baseURL string) *Client (*Client).Authenticate(ctx, email, password string) error (*Client).CreateProxyHost(ctx, config ProxyHostConfig) (ProxyHost, error) (*Client).UpdateProxyHost(ctx, id int, config ProxyHostConfig) (ProxyHost, error) (*Client).DeleteProxyHost(ctx, id int) error (*Client).ListProxyHosts(ctx) ([]ProxyHost, error) (*Client).FindProxyHostByDomain(ctx, domain string) (ProxyHost, bool, error) ``` ### Key design decisions - JWT token is cached with expiry; auto-refreshed 5 minutes before expiry or on 401. - Credentials are stored in memory after `Authenticate` to enable transparent re-auth. - All HTTP errors include the response body text for debugging. - Credentials are never included in error messages. - `boolInt` type handles NPM API's inconsistent 0/1 vs true/false for boolean fields. - `FindProxyHostByDomain` does case-insensitive matching against all domain names. ### Dependencies for next phase - Caller must provide decrypted NPM credentials (email + password from settings via `crypto.Decrypt`). - `ProxyHost.ID` (int) maps to `Instance.NpmProxyID` in the store for tracking.