feat(contracts): site center schemas, endpoints and migration 0031
Site health/proxy, SiteAccount login state (challenge, lastLoginAt, lastError, userData), SiteUserData history table, SiteCredentialsBody by kind, refresh-login/health-check/user-data/delete endpoints, SitesSettings defaults. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
381791cd64
commit
e2d1e21968
7 files changed
+458
-31
No files matched your search
@@ -0,0 +1,88 @@
|
||||
import type { Kysely } from "kysely";
|
||||
import { entityTable, execDdl, oneOf } from "../src/migration-helpers.ts";
|
||||
|
||||
// W9-A 站点中心(docs/design/site-center.md §5):
|
||||
// - site_account:登录状态加 challenge;账密登录名、TOTP / 会话 Cookie / passkey 的 secret 引用、UA、
|
||||
// 上次登录、最近错误、最近一次用户数据;user_info.username 迁到 login_name。
|
||||
// - site_user_data:用户数据按天一行(同日 upsert 取最新),随账号删除。
|
||||
// - site:最近健康检测(结果 + 时间)、使用代理;deleted_at 软删除(source_release 外键 restrict,资源保留)。
|
||||
|
||||
const OLD_STATUS = ["unknown", "active", "invalid", "expired"];
|
||||
const NEW_STATUS = [...OLD_STATUS, "challenge"];
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await execDdl(
|
||||
db,
|
||||
`
|
||||
alter table site_account drop constraint site_account_status_check;
|
||||
alter table site_account add constraint site_account_status_check ${oneOf("status", NEW_STATUS)};
|
||||
alter table site_account
|
||||
add column login_name text,
|
||||
add column totp_secret_ref uuid references secret (id) on delete set null,
|
||||
add column session_secret_ref uuid references secret (id) on delete set null,
|
||||
add column passkey_secret_ref uuid references secret (id) on delete set null,
|
||||
add column user_agent text,
|
||||
add column last_login_at timestamptz,
|
||||
add column last_error jsonb,
|
||||
add column user_data jsonb,
|
||||
add column user_data_at timestamptz;
|
||||
update site_account set login_name = user_info ->> 'username' where user_info ? 'username';
|
||||
|
||||
alter table site
|
||||
add column last_health jsonb,
|
||||
add column last_health_at timestamptz,
|
||||
add column proxy boolean not null default false,
|
||||
add column deleted_at timestamptz;
|
||||
|
||||
${entityTable(
|
||||
"site_user_data",
|
||||
`
|
||||
site_account_id uuid not null references site_account (id) on delete cascade,
|
||||
day date not null,
|
||||
upload bigint not null default 0,
|
||||
download bigint not null default 0,
|
||||
ratio double precision,
|
||||
bonus double precision,
|
||||
seeding integer not null default 0,
|
||||
seeding_size bigint not null default 0,
|
||||
leeching integer not null default 0,
|
||||
user_level text,
|
||||
vip boolean,
|
||||
message_unread integer not null default 0,
|
||||
join_at text,
|
||||
raw jsonb not null default '{}'::jsonb,
|
||||
captured_at timestamptz not null default now(),
|
||||
constraint site_user_data_day_key unique (site_account_id, day)`,
|
||||
)}
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 同上
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await execDdl(
|
||||
db,
|
||||
`
|
||||
drop table if exists site_user_data;
|
||||
alter table site
|
||||
drop column last_health,
|
||||
drop column last_health_at,
|
||||
drop column proxy,
|
||||
drop column deleted_at;
|
||||
alter table site_account
|
||||
drop column login_name,
|
||||
drop column totp_secret_ref,
|
||||
drop column session_secret_ref,
|
||||
drop column passkey_secret_ref,
|
||||
drop column user_agent,
|
||||
drop column last_login_at,
|
||||
drop column last_error,
|
||||
drop column user_data,
|
||||
drop column user_data_at;
|
||||
update site_account set status = 'expired' where status = 'challenge';
|
||||
alter table site_account drop constraint site_account_status_check;
|
||||
alter table site_account add constraint site_account_status_check ${oneOf("status", OLD_STATUS)};
|
||||
`,
|
||||
);
|
||||
}
|
||||
@@ -228,7 +228,7 @@ export type SiteAdapter = "nexusphp" | "mteam_api" | "api" | "generic_rss";
|
||||
export type SiteHealthStatus = "unknown" | "healthy" | "degraded" | "failing" | "disabled";
|
||||
export type SiteCircuitState = "closed" | "open" | "half_open";
|
||||
export type SiteAccountAuthKind = "cookie" | "passkey" | "api_key" | "password";
|
||||
export type SiteAccountStatus = "unknown" | "active" | "invalid" | "expired";
|
||||
export type SiteAccountStatus = "unknown" | "active" | "invalid" | "expired" | "challenge";
|
||||
export type SiteSnapshotKind = "periodic" | "backfill" | "live_search" | "manual";
|
||||
export type SiteSnapshotStatus = "running" | "ok" | "partial" | "failed";
|
||||
export type SourceReleaseResolutionStatus = "unresolved" | "resolved" | "ambiguous" | "unidentifiable" | "rejected";
|
||||
@@ -1208,6 +1208,10 @@ export interface SiteTable {
|
||||
snapshot_cursor: Generated<Json>;
|
||||
created_at: GeneratedTimestamptz;
|
||||
updated_at: GeneratedTimestamptz;
|
||||
last_health: Json | null;
|
||||
last_health_at: Timestamptz | null;
|
||||
proxy: Generated<boolean>;
|
||||
deleted_at: Timestamptz | null;
|
||||
}
|
||||
|
||||
export interface SiteAccountTable {
|
||||
@@ -1222,6 +1226,15 @@ export interface SiteAccountTable {
|
||||
user_info: Generated<Json>;
|
||||
created_at: GeneratedTimestamptz;
|
||||
updated_at: GeneratedTimestamptz;
|
||||
login_name: string | null;
|
||||
totp_secret_ref: string | null;
|
||||
session_secret_ref: string | null;
|
||||
passkey_secret_ref: string | null;
|
||||
user_agent: string | null;
|
||||
last_login_at: Timestamptz | null;
|
||||
last_error: Json | null;
|
||||
user_data: Json | null;
|
||||
user_data_at: Timestamptz | null;
|
||||
}
|
||||
|
||||
export interface SiteSnapshotTable {
|
||||
@@ -1242,6 +1255,27 @@ export interface SiteSnapshotTable {
|
||||
updated_at: GeneratedTimestamptz;
|
||||
}
|
||||
|
||||
export interface SiteUserDataTable {
|
||||
id: Generated<string>;
|
||||
site_account_id: string;
|
||||
day: string;
|
||||
upload: Generated<number>;
|
||||
download: Generated<number>;
|
||||
ratio: number | null;
|
||||
bonus: number | null;
|
||||
seeding: Generated<number>;
|
||||
seeding_size: Generated<number>;
|
||||
leeching: Generated<number>;
|
||||
user_level: string | null;
|
||||
vip: boolean | null;
|
||||
message_unread: Generated<number>;
|
||||
join_at: string | null;
|
||||
raw: Generated<Json>;
|
||||
captured_at: GeneratedTimestamptz;
|
||||
created_at: GeneratedTimestamptz;
|
||||
updated_at: GeneratedTimestamptz;
|
||||
}
|
||||
|
||||
export interface SlotDeferralTable {
|
||||
id: Generated<string>;
|
||||
slot_id: string;
|
||||
@@ -1489,6 +1523,7 @@ export interface DB {
|
||||
site: SiteTable;
|
||||
site_account: SiteAccountTable;
|
||||
site_snapshot: SiteSnapshotTable;
|
||||
site_user_data: SiteUserDataTable;
|
||||
slot_deferral: SlotDeferralTable;
|
||||
slot_unit: SlotUnitTable;
|
||||
source_release: SourceReleaseTable;
|
||||
@@ -1671,6 +1706,9 @@ export type SiteAccountUpdate = Updateable<SiteAccountTable>;
|
||||
export type SiteSnapshotRow = Selectable<SiteSnapshotTable>;
|
||||
export type NewSiteSnapshot = Insertable<SiteSnapshotTable>;
|
||||
export type SiteSnapshotUpdate = Updateable<SiteSnapshotTable>;
|
||||
export type SiteUserDataRow = Selectable<SiteUserDataTable>;
|
||||
export type NewSiteUserData = Insertable<SiteUserDataTable>;
|
||||
export type SiteUserDataUpdate = Updateable<SiteUserDataTable>;
|
||||
export type SlotDeferralRow = Selectable<SlotDeferralTable>;
|
||||
export type NewSlotDeferral = Insertable<SlotDeferralTable>;
|
||||
export type SlotDeferralUpdate = Updateable<SlotDeferralTable>;
|
||||
|
||||
Reference in new issue
Block a user