package proxy import ( "context" "fmt" "strconv" "github.com/alexei/tinyforge/internal/npm" ) // NpmProvider wraps the NPM client behind the Provider interface. // It handles authentication transparently before each operation. type NpmProvider struct { client *npm.Client email string password string } // NewNpmProvider creates an NPM-backed proxy provider. // The email and password are the decrypted NPM credentials. func NewNpmProvider(client *npm.Client, email, password string) *NpmProvider { return &NpmProvider{ client: client, email: email, password: password, } } // UpdateCredentials updates the stored NPM credentials (e.g., after settings change). func (p *NpmProvider) UpdateCredentials(email, password string) { p.email = email p.password = password } func (p *NpmProvider) Name() string { return "npm" } func (p *NpmProvider) ConfigureRoute(ctx context.Context, domain, targetHost string, targetPort int, opts RouteOptions) (string, error) { if err := p.auth(ctx); err != nil { return "", err } scheme := opts.ForwardScheme if scheme == "" { scheme = "http" } // Check if a proxy host already exists for this domain. existing, found, err := p.client.FindProxyHostByDomain(ctx, domain) if err != nil { return "", fmt.Errorf("find existing proxy host: %w", err) } config := npm.ProxyHostConfig{ DomainNames: []string{domain}, ForwardScheme: scheme, ForwardHost: targetHost, ForwardPort: targetPort, BlockExploits: true, AllowWebsocket: true, HTTP2Support: true, AccessListID: opts.AccessListID, Meta: npm.Meta{}, Locations: []any{}, } if opts.SSLCertificateID > 0 { config.CertificateID = opts.SSLCertificateID config.SSLForced = true config.HSTSEnabled = true } if found { host, err := p.client.UpdateProxyHost(ctx, existing.ID, config) if err != nil { return "", fmt.Errorf("update proxy host: %w", err) } return strconv.Itoa(host.ID), nil } host, err := p.client.CreateProxyHost(ctx, config) if err != nil { return "", fmt.Errorf("create proxy host: %w", err) } return strconv.Itoa(host.ID), nil } func (p *NpmProvider) DeleteRoute(ctx context.Context, routeID string) error { if routeID == "" { return nil } id, err := strconv.Atoi(routeID) if err != nil { return fmt.Errorf("invalid npm proxy host id %q: %w", routeID, err) } if err := p.auth(ctx); err != nil { return err } return p.client.DeleteProxyHost(ctx, id) } func (p *NpmProvider) RouteExists(ctx context.Context, domain string) (bool, error) { if err := p.auth(ctx); err != nil { return false, err } _, found, err := p.client.FindProxyHostByDomain(ctx, domain) if err != nil { return false, fmt.Errorf("find proxy host: %w", err) } return found, nil } func (p *NpmProvider) ContainerLabels(_ string, _ int) map[string]string { // NPM configures routing via its API, not Docker labels. return nil } func (p *NpmProvider) Ping(ctx context.Context) error { return p.client.Ping(ctx) } // Authenticate is a public wrapper over the internal auth step. It is used by // health checks that want to make authenticated list calls without going // through the full ConfigureRoute path. Returns an error if credentials are // not configured or the NPM API rejects them. func (p *NpmProvider) Authenticate(ctx context.Context) error { return p.auth(ctx) } // auth authenticates to NPM if credentials are available. func (p *NpmProvider) auth(ctx context.Context) error { if p.email == "" { return fmt.Errorf("NPM credentials not configured") } return p.client.Authenticate(ctx, p.email, p.password) }