feat(sources): site center backend (credentials, refresh login, user data, health check, delete)
PUT /sites/{id}/credentials by kind (moved from auth-settings, legacy body
accepted), POST refresh-login (async task or wait), POST health-check,
GET user-data history, DELETE /sites/{id} (soft delete, releases kept),
DELETE /site-accounts/{id}, GET /sites?include=accounts; refreshSiteAccount
task with login-failure decisions and auto re-login on auth_expired;
scheduled jobs sites.user_data (6h) and sites.health_check (1h);
settings.sites defaults for rate limit, snapshot interval, UA and proxy.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
d6fca25679
commit
05a7706dde
15 files changed
+1602
-179
No files matched your search
@@ -18,10 +18,7 @@ import {
|
||||
PathMappingCreate,
|
||||
PathMappingUpdate,
|
||||
type SecretPurpose,
|
||||
SiteCredentialsPut,
|
||||
SiteCredentialsResult,
|
||||
} from "@umt/contracts";
|
||||
import type { DB } from "@umt/db";
|
||||
import { z } from "zod";
|
||||
import { ApiException } from "../../http/errors.ts";
|
||||
import { redact, redactString } from "../../logger.ts";
|
||||
@@ -41,7 +38,7 @@ import {
|
||||
} from "./endpoint.ts";
|
||||
|
||||
// 实例与路径:library_root / downloader_instance / media_server_instance / path_mapping 的 CRUD + 连通性测试,
|
||||
// 以及站点账号凭证录入 PUT /sites/{id}/credentials。凭证明文只在请求里出现,存为 secret 后只回 secretRef。
|
||||
// 站点账号凭证录入已迁到 sources-identity(PUT /sites/{id}/credentials)。
|
||||
|
||||
const OkResult = z.object({ ok: z.literal(true) });
|
||||
const del = (path: string, summary: string, tag: string) => ({
|
||||
@@ -470,97 +467,5 @@ export function registerInstanceRoutes(app: ModuleApp, ctx: ServerContext) {
|
||||
await audit(ctx, actor, "path_mapping.verify", { type: "path_mapping", id: row.id }, { ok });
|
||||
return toApi(updated);
|
||||
});
|
||||
|
||||
// ── 站点账号凭证 ──
|
||||
const PURPOSE: Record<SiteCredentialsPut["authKind"], SecretPurpose> = {
|
||||
cookie: "site_cookie",
|
||||
passkey: "site_passkey",
|
||||
api_key: "site_api_key",
|
||||
password: "site_password",
|
||||
};
|
||||
endpoint(
|
||||
app,
|
||||
{
|
||||
method: "put",
|
||||
path: "/sites/{id}/credentials",
|
||||
scope: "admin",
|
||||
summary: "录入站点账号凭证(cookie/passkey/apiKey/密码 → secret_ref,不回显)",
|
||||
params: IdParam,
|
||||
body: SiteCredentialsPut,
|
||||
response: SiteCredentialsResult,
|
||||
tags: ["sites"],
|
||||
},
|
||||
async ({ params, body, actor }) => {
|
||||
const site = await db().selectFrom("site").select("id").where("id", "=", params.id).executeTakeFirst();
|
||||
if (!site) throw new ApiException("not_found", "站点不存在");
|
||||
const newRef = await ctx.secrets.create({ purpose: PURPOSE[body.authKind], plaintext: body.credential });
|
||||
let oldRef: string | null = null;
|
||||
let created = false;
|
||||
let accountId: string;
|
||||
try {
|
||||
accountId = await db()
|
||||
.transaction()
|
||||
.execute(async (trx) => {
|
||||
const cur = await trx
|
||||
.selectFrom("site_account")
|
||||
.select(["id", "secret_ref", "user_info"])
|
||||
.where("site_id", "=", params.id)
|
||||
.where("label", "=", body.label)
|
||||
.forUpdate()
|
||||
.executeTakeFirst();
|
||||
const userInfo = {
|
||||
...((cur?.user_info as Record<string, unknown> | undefined) ?? {}),
|
||||
...(body.username ? { username: body.username } : {}),
|
||||
};
|
||||
const common: Partial<Record<keyof DB["site_account"], unknown>> = {
|
||||
auth_kind: body.authKind,
|
||||
secret_ref: newRef,
|
||||
status: "unknown",
|
||||
user_info: JSON.stringify(userInfo),
|
||||
...(body.enabled !== undefined ? { enabled: body.enabled } : {}),
|
||||
};
|
||||
if (cur) {
|
||||
oldRef = cur.secret_ref;
|
||||
await trx
|
||||
.updateTable("site_account")
|
||||
.set(common as never)
|
||||
.where("id", "=", cur.id)
|
||||
.execute();
|
||||
return cur.id;
|
||||
}
|
||||
created = true;
|
||||
const r = await trx
|
||||
.insertInto("site_account")
|
||||
.values({ site_id: params.id, label: body.label, ...common } as never)
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow();
|
||||
return r.id;
|
||||
});
|
||||
} catch (e) {
|
||||
await ctx.secrets.remove(newRef).catch(() => {});
|
||||
throw e;
|
||||
}
|
||||
if (oldRef) await ctx.secrets.remove(oldRef).catch(() => {});
|
||||
await audit(
|
||||
ctx,
|
||||
actor,
|
||||
"site_account.credentials_set",
|
||||
{ type: "site_account", id: accountId },
|
||||
{
|
||||
siteId: params.id,
|
||||
label: body.label,
|
||||
authKind: body.authKind,
|
||||
created,
|
||||
},
|
||||
);
|
||||
return {
|
||||
siteAccountId: accountId,
|
||||
siteId: params.id,
|
||||
label: body.label,
|
||||
authKind: body.authKind,
|
||||
secretRef: newRef,
|
||||
created,
|
||||
};
|
||||
},
|
||||
);
|
||||
// 站点账号凭证 PUT /sites/{id}/credentials 已迁到 sources-identity(W9-A 站点中心,按 kind 判别)
|
||||
}
|
||||
Reference in new issue
Block a user