proxy-manager: nginx proxy manager in officer
reads hosts and certificates against a live NPM 2.15.1. one grantable permission, with /_config owner-only — reading which hosts are healthy is worth sharing, an NPM admin login is not, and NPM has no read-only role. the quirk that shapes the whole plugin: NPM answers an unauthenticated read with 404, not 401, because its router only registers a resource once a token is present. so 'not signed in' and 'no such endpoint' are the same response, and a naive client tells you a feature is missing when your password is wrong. the client mints a fresh token and retries once, then reports a persistent 404 as likelyAuth. certificates sort by expiry rather than name, which is the only ordering that answers a question. no create/edit/delete and no cert issuance: NPM's own UI does those and a wrong proxy host takes a domain off the internet. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
import { pgTable, serial, text, timestamp, integer } from 'drizzle-orm/pg-core';
|
||||
|
||||
/**
|
||||
* The connection to Nginx Proxy Manager.
|
||||
*
|
||||
* ── Why its own table rather than core `service_connections` ──
|
||||
*
|
||||
* `service_connections` fits the shape exactly — it has `url`, `username` and an encrypted `secret`. But
|
||||
* its `ServiceName` is a closed TypeScript union, so using it means editing the platform to add a name,
|
||||
* and this plugin is meant to install into a platform it did not change. The table comment there already
|
||||
* says the column is text precisely so adding a service is not a schema change; the union is the part
|
||||
* that has not caught up.
|
||||
*
|
||||
* Revisit if that union opens. Until then the cost is one small table and one encryption purpose.
|
||||
*
|
||||
* ── One row, not one per user ──
|
||||
*
|
||||
* NPM has no read-only role. Its API is admin or nothing, so a per-user credential would be a per-user
|
||||
* copy of the same total authority over every hostname this machine serves. There is one connection, the
|
||||
* owner sets it, and members granted the permission read through it without ever seeing it.
|
||||
*/
|
||||
export const proxyManagerConnection = pgTable('proxy_manager_connection', {
|
||||
id: serial('id').primaryKey(),
|
||||
|
||||
/** Where NPM's admin API is. Normalised without a trailing slash before write. */
|
||||
url: text('url').notNull(),
|
||||
|
||||
/** NPM calls it `identity`; it is an email address. */
|
||||
identity: text('identity').notNull(),
|
||||
|
||||
/**
|
||||
* Encrypted with `encryptSecret('proxy-manager', …)` — its own purpose key, so a leak of this one
|
||||
* opens this plugin's column and nothing else. Never returned across the API boundary, at any level,
|
||||
* to anybody, including the owner.
|
||||
*/
|
||||
secret: text('secret').notNull(),
|
||||
|
||||
/** From `GET /api/`, recorded on a successful probe. Tells you which NPM you are talking to. */
|
||||
version: text('version'),
|
||||
|
||||
lastProbeAt: timestamp('last_probe_at', { withTimezone: true }),
|
||||
lastProbeOk: integer('last_probe_ok'),
|
||||
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export type ProxyManagerConnection = typeof proxyManagerConnection.$inferSelect;
|
||||
Reference in New Issue
Block a user