feat(server): W2-A login gate, secrets, settings, instances, audit, engine panel API and PG-backed SSE bus
- Admin login gate (docs/design/auth-reference.md): admin_user/admin_session/login_attempt
(migration 0015), one-time setup token logged once + conditional UPDATE, argon2id via
hash-wasm (WASM; compiled binary has no --allow-ffi), sha256-stored sessions in HttpOnly
SameSite=Lax cookie umt_session (fixed 12h TTL, soft revoke), HMAC-derived double-submit
CSRF, IP (5) + account (15) failure limits per 15 min, password change revokes other
sessions, optional TOTP with replay counter, AuditAction enum.
- Unified auth middleware: cookie session or Bearer API key -> c.var.actor
{type: admin|api_key, id, name, scopes}; requireScope works for both; requireAdminSession.
- ctx.secrets: AES-256-GCM, per-secret HKDF-SHA256 key from SECRET_MASTER_KEY, AAD bound
to (id, purpose); ctx.settings: 30s cache invalidated by settings.changed events.
- Endpoints: /auth/*, /settings, /api-keys, /secrets, /audit-events, library-roots,
downloader/media-server instances (+test), path-mappings (+verify),
PUT /sites/{id}/credentials, /engine/runs|stats|cancel (read-only openworkflow schema).
- SSE: PgEventBus (sse_event table, migration 0016, LISTEN/NOTIFY umt_events, catch-up by
seq, pruning, resync); EventBusPort is now async.
- Tests: login/rate limit/revocation/CSRF/TOTP, secrets round trip and no log leakage,
admin APIs against mock qB/Emby, SSE across two Deno subprocesses.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
07db0dac5b
commit
ff230e5da7
34 files changed
+4363
-78
No files matched your search
@@ -159,3 +159,37 @@ export const PathMappingUpdate = PathMappingInput.partial().refine(pathMappingOw
|
||||
message: "viewKind 与实例 ID 不匹配",
|
||||
});
|
||||
export type PathMappingUpdate = z.infer<typeof PathMappingUpdate>;
|
||||
|
||||
/** POST /{downloader,media-server}-instances/{id}/test 与 /library-roots/{id}/test 结果 */
|
||||
export const InstanceTestResult = z.object({
|
||||
ok: z.boolean(),
|
||||
version: z.string().nullable(),
|
||||
message: z.string().nullable(),
|
||||
healthStatus: InstanceHealth,
|
||||
});
|
||||
export type InstanceTestResult = z.infer<typeof InstanceTestResult>;
|
||||
|
||||
/**
|
||||
* PUT /sites/{id}/credentials:站点账号凭证录入(cookie / passkey / apiKey / 密码)。
|
||||
* 按 (siteId, label) 建立或更新 site_account;明文加密为 secret 后丢弃,响应不回显。
|
||||
*/
|
||||
export const SiteCredentialsPut = z.object({
|
||||
label: z.string().min(1).max(80).default("default"),
|
||||
authKind: z.enum(["cookie", "passkey", "api_key", "password"]),
|
||||
/** 只写明文 */
|
||||
credential: Credential,
|
||||
/** authKind=password 时的登录名(存入 user_info.username) */
|
||||
username: z.string().max(200).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
});
|
||||
export type SiteCredentialsPut = z.infer<typeof SiteCredentialsPut>;
|
||||
export const SiteCredentialsResult = z.object({
|
||||
siteAccountId: Id,
|
||||
siteId: Id,
|
||||
label: z.string(),
|
||||
authKind: z.string(),
|
||||
secretRef: Id,
|
||||
/** true:新建账号;false:替换已有凭证 */
|
||||
created: z.boolean(),
|
||||
});
|
||||
export type SiteCredentialsResult = z.infer<typeof SiteCredentialsResult>;
|
||||
Reference in new issue
Block a user