feat: establish NekoNest Cloud control and relay
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import { getCloudViewer } from "@/app/cloud-auth";
|
||||
import {
|
||||
cancelAccountDeletion,
|
||||
DomainError,
|
||||
getOrCreateAccount,
|
||||
requestAccountDeletion,
|
||||
} from "@/db/repository";
|
||||
import { apiError, readJsonMutation } from "../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const viewer = await getCloudViewer();
|
||||
if (!viewer) {
|
||||
return Response.json(
|
||||
{ error: "authentication_required", message: "请先登录" },
|
||||
{ status: 401, headers: { "cache-control": "no-store" } },
|
||||
);
|
||||
}
|
||||
const payload = await readJsonMutation<{
|
||||
action?: string;
|
||||
requestId?: string;
|
||||
confirmed?: boolean;
|
||||
reason?: string;
|
||||
idempotencyKey?: string;
|
||||
}>(request);
|
||||
const account = await getOrCreateAccount(viewer);
|
||||
|
||||
if (payload.action === "request") {
|
||||
const deletionRequest = await requestAccountDeletion({
|
||||
accountId: account.id,
|
||||
actorId: viewer.userId,
|
||||
confirmed: payload.confirmed === true,
|
||||
reason: payload.reason ?? "",
|
||||
idempotencyKey: payload.idempotencyKey ?? "",
|
||||
});
|
||||
return Response.json(
|
||||
{ deletionRequest },
|
||||
{ status: 201, headers: { "cache-control": "no-store" } },
|
||||
);
|
||||
}
|
||||
if (payload.action === "cancel") {
|
||||
const deletionRequest = await cancelAccountDeletion({
|
||||
accountId: account.id,
|
||||
actorId: viewer.userId,
|
||||
requestId: payload.requestId ?? "",
|
||||
});
|
||||
return Response.json(
|
||||
{ deletionRequest },
|
||||
{ headers: { "cache-control": "no-store" } },
|
||||
);
|
||||
}
|
||||
throw new DomainError(
|
||||
"invalid_deletion_action",
|
||||
"请选择有效的注销申请操作",
|
||||
);
|
||||
} catch (error) {
|
||||
const response = apiError(error);
|
||||
response.headers.set("cache-control", "no-store");
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { getCloudViewer } from "@/app/cloud-auth";
|
||||
import {
|
||||
getAccountControlPlaneExport,
|
||||
getOrCreateAccount,
|
||||
} from "@/db/repository";
|
||||
import { apiError } from "../../respond";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const viewer = await getCloudViewer();
|
||||
if (!viewer) {
|
||||
return Response.json(
|
||||
{ error: "authentication_required", message: "请先登录" },
|
||||
{ status: 401, headers: { "cache-control": "no-store" } },
|
||||
);
|
||||
}
|
||||
const account = await getOrCreateAccount(viewer);
|
||||
const exportData = await getAccountControlPlaneExport(account);
|
||||
const date = exportData.exported_at.slice(0, 10);
|
||||
return new Response(`${JSON.stringify(exportData, null, 2)}\n`, {
|
||||
headers: {
|
||||
"cache-control": "no-store",
|
||||
"content-disposition": `attachment; filename="nekonest-cloud-${date}.json"`,
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"x-content-type-options": "nosniff",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const response = apiError(error);
|
||||
response.headers.set("cache-control", "no-store");
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { getCloudViewer } from "@/app/cloud-auth";
|
||||
import { resolveBetaAccessRequest } from "@/db/repository";
|
||||
import { apiError, readJsonMutation } from "../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const viewer = await getCloudViewer();
|
||||
if (!viewer) {
|
||||
return Response.json(
|
||||
{ error: "authentication_required", message: "请先登录" },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
if (!viewer.isAdmin) {
|
||||
return Response.json(
|
||||
{ error: "forbidden", message: "没有商业后台权限" },
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
const payload = await readJsonMutation<{
|
||||
requestId?: string;
|
||||
action?: "approve" | "decline";
|
||||
capacitySlots?: number;
|
||||
endsAt?: string;
|
||||
response?: string;
|
||||
reason?: string;
|
||||
idempotencyKey?: string;
|
||||
}>(request);
|
||||
const result = await resolveBetaAccessRequest({
|
||||
actorId: viewer.userId,
|
||||
requestId: payload.requestId ?? "",
|
||||
action: payload.action ?? "decline",
|
||||
capacitySlots: payload.capacitySlots ?? 0,
|
||||
endsAt: payload.endsAt ?? "",
|
||||
response: payload.response ?? "",
|
||||
reason: payload.reason ?? "",
|
||||
idempotencyKey: payload.idempotencyKey ?? "",
|
||||
});
|
||||
return Response.json(result);
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { getCloudViewer } from "@/app/cloud-auth";
|
||||
import { setPublicBeta } from "@/db/repository";
|
||||
import { apiError, readJsonMutation } from "../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const viewer = await getCloudViewer();
|
||||
if (!viewer) return Response.json({ error: "authentication_required", message: "请先登录" }, { status: 401 });
|
||||
if (!viewer.isAdmin) return Response.json({ error: "forbidden", message: "没有商业后台权限" }, { status: 403 });
|
||||
const payload = await readJsonMutation<{ enabled?: boolean; capacitySlots?: number | null; reason?: string; idempotencyKey?: string }>(request);
|
||||
const beta = await setPublicBeta({ actorId: viewer.userId, enabled: payload.enabled ?? false, capacitySlots: payload.capacitySlots ?? null, reason: payload.reason ?? "", idempotencyKey: payload.idempotencyKey ?? "" });
|
||||
return Response.json({ beta });
|
||||
} catch (error) { return apiError(error); }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { getCloudViewer } from "@/app/cloud-auth";
|
||||
import { createExemption, revokeExemption } from "@/db/repository";
|
||||
import { apiError, readJsonMutation } from "../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const viewer = await getCloudViewer();
|
||||
if (!viewer) return Response.json({ error: "authentication_required", message: "请先登录" }, { status: 401 });
|
||||
if (!viewer.isAdmin) return Response.json({ error: "forbidden", message: "没有商业后台权限" }, { status: 403 });
|
||||
const payload = await readJsonMutation<{ action?: string; grantId?: string; accountId?: string; capacitySlots?: number | null; endsAt?: string; reason?: string; idempotencyKey?: string }>(request);
|
||||
if (payload.action === "revoke") {
|
||||
const grant = await revokeExemption({ actorId: viewer.userId, grantId: payload.grantId ?? "", reason: payload.reason ?? "", idempotencyKey: payload.idempotencyKey ?? "" });
|
||||
return Response.json({ grant });
|
||||
}
|
||||
const grant = await createExemption({ actorId: viewer.userId, accountId: payload.accountId ?? "", capacitySlots: payload.capacitySlots ?? null, endsAt: payload.endsAt ?? "", reason: payload.reason ?? "", idempotencyKey: payload.idempotencyKey ?? "" });
|
||||
return Response.json({ grant }, { status: 201 });
|
||||
} catch (error) { return apiError(error); }
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { getCloudViewer } from "@/app/cloud-auth";
|
||||
import { resolveFeedback } from "@/db/repository";
|
||||
import { apiError, readJsonMutation } from "../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const viewer = await getCloudViewer();
|
||||
if (!viewer) {
|
||||
return Response.json(
|
||||
{ error: "authentication_required", message: "请先登录" },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
if (!viewer.isAdmin) {
|
||||
return Response.json(
|
||||
{ error: "forbidden", message: "没有公测后台权限" },
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
const payload = await readJsonMutation<{
|
||||
feedbackId?: string;
|
||||
response?: string;
|
||||
reason?: string;
|
||||
idempotencyKey?: string;
|
||||
}>(request);
|
||||
const feedback = await resolveFeedback({
|
||||
feedbackId: payload.feedbackId ?? "",
|
||||
actorId: viewer.userId,
|
||||
response: payload.response ?? "",
|
||||
reason: payload.reason ?? "",
|
||||
idempotencyKey: payload.idempotencyKey ?? "",
|
||||
});
|
||||
return Response.json({ feedback });
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { getCloudViewer } from "@/app/cloud-auth";
|
||||
import {
|
||||
createServiceIncident,
|
||||
resolveServiceIncident,
|
||||
} from "@/db/repository";
|
||||
import { apiError, readJsonMutation } from "../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const viewer = await getCloudViewer();
|
||||
if (!viewer) {
|
||||
return Response.json(
|
||||
{ error: "authentication_required", message: "请先登录" },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
if (!viewer.isAdmin) {
|
||||
return Response.json(
|
||||
{ error: "forbidden", message: "没有公测后台权限" },
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
const payload = await readJsonMutation<{
|
||||
action?: string;
|
||||
incidentId?: string;
|
||||
severity?: string;
|
||||
title?: string;
|
||||
message?: string;
|
||||
resolution?: string;
|
||||
reason?: string;
|
||||
idempotencyKey?: string;
|
||||
}>(request);
|
||||
|
||||
if (payload.action === "create") {
|
||||
const incident = await createServiceIncident({
|
||||
actorId: viewer.userId,
|
||||
severity: payload.severity ?? "",
|
||||
title: payload.title ?? "",
|
||||
message: payload.message ?? "",
|
||||
reason: payload.reason ?? "",
|
||||
idempotencyKey: payload.idempotencyKey ?? "",
|
||||
});
|
||||
return Response.json({ incident }, { status: 201 });
|
||||
}
|
||||
if (payload.action === "resolve") {
|
||||
const incident = await resolveServiceIncident({
|
||||
incidentId: payload.incidentId ?? "",
|
||||
actorId: viewer.userId,
|
||||
resolution: payload.resolution ?? "",
|
||||
reason: payload.reason ?? "",
|
||||
});
|
||||
return Response.json({ incident });
|
||||
}
|
||||
return Response.json(
|
||||
{ error: "invalid_incident_action", message: "请选择有效的故障公告操作" },
|
||||
{ status: 400 },
|
||||
);
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { getCloudViewer } from "@/app/cloud-auth";
|
||||
import { updateLaunchGate, type LaunchGateRecord } from "@/db/repository";
|
||||
import { apiError, readJsonMutation } from "../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const viewer = await getCloudViewer();
|
||||
if (!viewer) return Response.json({ error: "authentication_required", message: "请先登录" }, { status: 401 });
|
||||
if (!viewer.isAdmin) return Response.json({ error: "forbidden", message: "没有商业后台权限" }, { status: 403 });
|
||||
const payload = await readJsonMutation<{ key?: string; status?: LaunchGateRecord["status"]; owner?: string; evidenceUrl?: string; notes?: string; reason?: string; idempotencyKey?: string }>(request);
|
||||
const gate = await updateLaunchGate({ actorId: viewer.userId, key: payload.key ?? "", status: payload.status ?? "blocked", owner: payload.owner ?? "", evidenceUrl: payload.evidenceUrl ?? "", notes: payload.notes ?? "", reason: payload.reason ?? "", idempotencyKey: payload.idempotencyKey ?? "" });
|
||||
return Response.json({ gate });
|
||||
} catch (error) { return apiError(error); }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { getCloudViewer } from "@/app/cloud-auth";
|
||||
import { DomainError } from "@/db/repository";
|
||||
import { apiError, readJsonMutation } from "../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const viewer = await getCloudViewer();
|
||||
if (!viewer) return Response.json({ error: "authentication_required", message: "请先登录" }, { status: 401 });
|
||||
if (!viewer.isAdmin) return Response.json({ error: "forbidden", message: "没有商业后台权限" }, { status: 403 });
|
||||
await readJsonMutation<{ period?: "month" | "year"; amountMinor?: number; reason?: string; idempotencyKey?: string }>(request);
|
||||
throw new DomainError("paid_features_deferred", "免费公测阶段不发布价格版本", 409);
|
||||
} catch (error) { return apiError(error); }
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { getCloudViewer } from "@/app/cloud-auth";
|
||||
import { beginRelayMigration } from "@/db/relay-migrations";
|
||||
import { apiError, readJsonMutation } from "../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const viewer = await getCloudViewer();
|
||||
if (!viewer) {
|
||||
return Response.json(
|
||||
{ error_code: "authentication_required", message: "请先登录", retryable: false },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
if (!viewer.isAdmin) {
|
||||
return Response.json(
|
||||
{ error_code: "forbidden", message: "没有 Relay 迁移权限", retryable: false },
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
const payload = await readJsonMutation<{
|
||||
tenant_id?: string;
|
||||
target_node_id?: string;
|
||||
reason?: string;
|
||||
idempotency_key?: string;
|
||||
}>(request);
|
||||
const migration = await beginRelayMigration({
|
||||
tenantId: payload.tenant_id ?? "",
|
||||
targetNodeId: payload.target_node_id ?? "",
|
||||
actorId: viewer.userId,
|
||||
reason: payload.reason ?? "",
|
||||
idempotencyKey: payload.idempotency_key ?? "",
|
||||
});
|
||||
return Response.json(
|
||||
{ migration_id: migration.id, state: migration.state, started_at: migration.started_at },
|
||||
{ status: 202, headers: { "cache-control": "no-store" } },
|
||||
);
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { getCloudViewer } from "@/app/cloud-auth";
|
||||
import { beginRelayPurge } from "@/db/relay-purges";
|
||||
import { apiError, readJsonMutation } from "../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const viewer = await getCloudViewer();
|
||||
if (!viewer) {
|
||||
return Response.json(
|
||||
{ error_code: "authentication_required", message: "请先登录", retryable: false },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
if (!viewer.isAdmin) {
|
||||
return Response.json(
|
||||
{ error_code: "forbidden", message: "没有永久删除租户数据的权限", retryable: false },
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
const payload = await readJsonMutation<{
|
||||
deletion_request_id?: string;
|
||||
reason?: string;
|
||||
confirmation?: string;
|
||||
}>(request);
|
||||
const purge = await beginRelayPurge({
|
||||
deletionRequestId: payload.deletion_request_id ?? "",
|
||||
actorId: viewer.userId,
|
||||
reason: payload.reason ?? "",
|
||||
confirmation: payload.confirmation ?? "",
|
||||
});
|
||||
return Response.json(
|
||||
{ purge_id: purge.id, state: purge.state, started_at: purge.started_at },
|
||||
{ status: 202, headers: { "cache-control": "no-store" } },
|
||||
);
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { getCloudViewer } from "@/app/cloud-auth";
|
||||
import { runRetentionMaintenance } from "@/db/repository";
|
||||
import { apiError, readJsonMutation } from "../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const viewer = await getCloudViewer();
|
||||
if (!viewer) {
|
||||
return Response.json(
|
||||
{ error: "authentication_required", message: "请先登录" },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
if (!viewer.isAdmin) {
|
||||
return Response.json(
|
||||
{ error: "forbidden", message: "没有公测后台权限" },
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
|
||||
const payload = await readJsonMutation<{
|
||||
confirmed?: boolean;
|
||||
reason?: string;
|
||||
}>(request);
|
||||
const retention = await runRetentionMaintenance({
|
||||
actorId: viewer.userId,
|
||||
confirmed: payload.confirmed === true,
|
||||
reason: payload.reason ?? "",
|
||||
});
|
||||
return Response.json({ retention });
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { getCloudViewer } from "@/app/cloud-auth";
|
||||
import {
|
||||
cancelBetaAccessRequest,
|
||||
createBetaAccessRequest,
|
||||
getOrCreateAccount,
|
||||
} from "@/db/repository";
|
||||
import { apiError, readJsonMutation } from "../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const viewer = await getCloudViewer();
|
||||
if (!viewer) {
|
||||
return Response.json(
|
||||
{ error: "authentication_required", message: "请先登录" },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
const payload = await readJsonMutation<{
|
||||
action?: string;
|
||||
requestId?: string;
|
||||
preferredOs?: string;
|
||||
requestedSlots?: number;
|
||||
useCase?: string;
|
||||
idempotencyKey?: string;
|
||||
}>(request);
|
||||
const account = await getOrCreateAccount(viewer);
|
||||
if (payload.action === "cancel") {
|
||||
const accessRequest = await cancelBetaAccessRequest({
|
||||
accountId: account.id,
|
||||
actorId: viewer.userId,
|
||||
requestId: payload.requestId ?? "",
|
||||
idempotencyKey: payload.idempotencyKey ?? "",
|
||||
});
|
||||
return Response.json({ accessRequest });
|
||||
}
|
||||
if (payload.action !== "request") {
|
||||
return Response.json(
|
||||
{ error: "invalid_action", message: "请选择提交或撤回闭测申请" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
const accessRequest = await createBetaAccessRequest({
|
||||
accountId: account.id,
|
||||
actorId: viewer.userId,
|
||||
preferredOs: payload.preferredOs ?? "",
|
||||
requestedSlots: payload.requestedSlots ?? 0,
|
||||
useCase: payload.useCase ?? "",
|
||||
idempotencyKey: payload.idempotencyKey ?? "",
|
||||
});
|
||||
return Response.json({ accessRequest }, { status: 201 });
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { getCloudViewer } from "@/app/cloud-auth";
|
||||
import { DomainError } from "@/db/repository";
|
||||
import { apiError, readJsonMutation } from "../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const viewer = await getCloudViewer();
|
||||
if (!viewer) {
|
||||
return Response.json(
|
||||
{ error: "authentication_required", message: "请先登录" },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
await readJsonMutation<{
|
||||
period?: "month" | "year";
|
||||
quantity?: number;
|
||||
idempotencyKey?: string;
|
||||
}>(request);
|
||||
throw new DomainError(
|
||||
"paid_features_deferred",
|
||||
"免费公测阶段不提供报价或订单;未来收费方案确定后会另行通知",
|
||||
409,
|
||||
);
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { getCloudViewer } from "@/app/cloud-auth";
|
||||
import { createFeedback, getOrCreateAccount } from "@/db/repository";
|
||||
import { apiError, readJsonMutation } from "../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const viewer = await getCloudViewer();
|
||||
if (!viewer) {
|
||||
return Response.json(
|
||||
{ error: "authentication_required", message: "请先登录" },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
const payload = await readJsonMutation<{
|
||||
category?: string;
|
||||
message?: string;
|
||||
idempotencyKey?: string;
|
||||
}>(request);
|
||||
const account = await getOrCreateAccount(viewer);
|
||||
const feedback = await createFeedback({
|
||||
accountId: account.id,
|
||||
actorId: viewer.userId,
|
||||
category: payload.category ?? "",
|
||||
message: payload.message ?? "",
|
||||
idempotencyKey: payload.idempotencyKey ?? "",
|
||||
});
|
||||
return Response.json({ feedback }, { status: 201 });
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { getCloudViewer } from "@/app/cloud-auth";
|
||||
import {
|
||||
cancelPairingRequest,
|
||||
getOrCreateAccount,
|
||||
} from "@/db/repository";
|
||||
import { apiError, readJsonMutation } from "../../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const viewer = await getCloudViewer();
|
||||
if (!viewer) {
|
||||
return Response.json(
|
||||
{ error: "authentication_required", message: "请先登录" },
|
||||
{ status: 401, headers: { "cache-control": "no-store" } },
|
||||
);
|
||||
}
|
||||
const payload = await readJsonMutation<{ pairingId?: string }>(request);
|
||||
const account = await getOrCreateAccount(viewer);
|
||||
const result = await cancelPairingRequest({
|
||||
accountId: account.id,
|
||||
pairingId: payload.pairingId ?? "",
|
||||
actorId: viewer.userId,
|
||||
});
|
||||
return Response.json(result, {
|
||||
status: 200,
|
||||
headers: { "cache-control": "no-store" },
|
||||
});
|
||||
} catch (error) {
|
||||
const response = apiError(error);
|
||||
response.headers.set("cache-control", "no-store");
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { getCloudViewer } from "@/app/cloud-auth";
|
||||
import {
|
||||
createPairingRequest,
|
||||
getOrCreateAccount,
|
||||
getOwnedPairingProgress,
|
||||
} from "@/db/repository";
|
||||
import { apiError, readJsonMutation } from "../../respond";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const viewer = await getCloudViewer();
|
||||
if (!viewer) {
|
||||
return Response.json(
|
||||
{ error: "authentication_required", message: "请先登录" },
|
||||
{ status: 401, headers: { "cache-control": "no-store" } },
|
||||
);
|
||||
}
|
||||
const pairingId = new URL(request.url).searchParams.get("pairing_id") ?? "";
|
||||
const account = await getOrCreateAccount(viewer);
|
||||
const pairing = await getOwnedPairingProgress({
|
||||
accountId: account.id,
|
||||
pairingId,
|
||||
});
|
||||
return Response.json(
|
||||
{ pairing },
|
||||
{ status: 200, headers: { "cache-control": "no-store" } },
|
||||
);
|
||||
} catch (error) {
|
||||
const response = apiError(error);
|
||||
response.headers.set("cache-control", "no-store");
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const viewer = await getCloudViewer();
|
||||
if (!viewer) {
|
||||
return Response.json(
|
||||
{ error: "authentication_required", message: "请先登录" },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
const payload = await readJsonMutation<{
|
||||
requestedName?: string;
|
||||
os?: "windows" | "linux";
|
||||
}>(request);
|
||||
const account = await getOrCreateAccount(viewer);
|
||||
const pairing = await createPairingRequest({
|
||||
accountId: account.id,
|
||||
requestedName: payload.requestedName ?? "",
|
||||
os: payload.os ?? "windows",
|
||||
actorId: viewer.userId,
|
||||
});
|
||||
return Response.json(
|
||||
{ pairing },
|
||||
{ status: 201, headers: { "cache-control": "no-store" } },
|
||||
);
|
||||
} catch (error) {
|
||||
const response = apiError(error);
|
||||
response.headers.set("cache-control", "no-store");
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { getCloudViewer } from "@/app/cloud-auth";
|
||||
import { getOrCreateAccount, revokeHost } from "@/db/repository";
|
||||
import { apiError, readJsonMutation } from "../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const viewer = await getCloudViewer();
|
||||
if (!viewer) {
|
||||
return Response.json(
|
||||
{ error: "authentication_required", message: "请先登录" },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
const payload = await readJsonMutation<{ hostId?: string; reason?: string }>(
|
||||
request,
|
||||
);
|
||||
const account = await getOrCreateAccount(viewer);
|
||||
const result = await revokeHost({
|
||||
accountId: account.id,
|
||||
hostId: payload.hostId ?? "",
|
||||
actorId: viewer.userId,
|
||||
reason: payload.reason ?? "用户从控制台撤销主机",
|
||||
});
|
||||
return Response.json(result);
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
authenticateRelayNode,
|
||||
authorizationRevisionDelta,
|
||||
} from "@/db/relay-control-plane";
|
||||
import { apiError, readJsonMutation } from "../../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const principal = await authenticateRelayNode(request);
|
||||
const payload = await readJsonMutation<{
|
||||
tenant_id?: string;
|
||||
after_revision?: number;
|
||||
}>(request);
|
||||
return Response.json(
|
||||
await authorizationRevisionDelta({
|
||||
principal,
|
||||
tenantId: payload.tenant_id ?? "",
|
||||
afterRevision: Number(payload.after_revision ?? -1),
|
||||
}),
|
||||
{ headers: { "cache-control": "no-store" } },
|
||||
);
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import {
|
||||
authenticateRelayNode,
|
||||
fullAuthorizationSnapshot,
|
||||
} from "@/db/relay-control-plane";
|
||||
import { apiError, readJsonMutation } from "../../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const principal = await authenticateRelayNode(request);
|
||||
const payload = await readJsonMutation<{
|
||||
tenant_id?: string;
|
||||
placement_generation?: number;
|
||||
}>(request);
|
||||
return Response.json(await fullAuthorizationSnapshot({
|
||||
principal,
|
||||
tenantId: payload.tenant_id ?? "",
|
||||
placementGeneration: Number(payload.placement_generation ?? -1),
|
||||
}), { headers: { "cache-control": "no-store" } });
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import {
|
||||
authenticateRelayNode,
|
||||
authorizeDeviceForRelay,
|
||||
} from "@/db/relay-control-plane";
|
||||
import { apiError, readJsonMutation } from "../../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const principal = await authenticateRelayNode(request);
|
||||
const payload = await readJsonMutation<{
|
||||
device_id?: string;
|
||||
token_hash?: string;
|
||||
}>(request);
|
||||
const result = await authorizeDeviceForRelay({
|
||||
principal,
|
||||
deviceId: payload.device_id ?? "",
|
||||
tokenHash: payload.token_hash?.trim().toLowerCase() ?? "",
|
||||
});
|
||||
return Response.json(result, { headers: { "cache-control": "no-store" } });
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
authenticateRelayNode,
|
||||
authorizePhoneRoute,
|
||||
} from "@/db/relay-control-plane";
|
||||
import { apiError, readJsonMutation } from "../../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const principal = await authenticateRelayNode(request);
|
||||
const payload = await readJsonMutation<{
|
||||
route_handle?: string;
|
||||
phone_token_hash?: string;
|
||||
}>(request);
|
||||
return Response.json(
|
||||
await authorizePhoneRoute({
|
||||
principal,
|
||||
routeHandle: payload.route_handle ?? "",
|
||||
phoneTokenHash: payload.phone_token_hash ?? "",
|
||||
}),
|
||||
{ headers: { "cache-control": "no-store" } },
|
||||
);
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import {
|
||||
authenticateRelayNode,
|
||||
completePhoneHandoffForRelay,
|
||||
} from "@/db/relay-control-plane";
|
||||
import { apiError, readJsonMutation } from "../../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const principal = await authenticateRelayNode(request);
|
||||
const payload = await readJsonMutation<{
|
||||
handoff_id?: string;
|
||||
phone_id?: string;
|
||||
phone_token_hash?: string;
|
||||
route_handle_hash?: string;
|
||||
}>(request);
|
||||
return Response.json(await completePhoneHandoffForRelay({
|
||||
principal,
|
||||
handoffId: payload.handoff_id ?? "",
|
||||
phoneId: payload.phone_id ?? "",
|
||||
phoneTokenHash: payload.phone_token_hash ?? "",
|
||||
routeHandleHash: payload.route_handle_hash ?? "",
|
||||
}), { headers: { "cache-control": "no-store" } });
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
authenticateRelayNode,
|
||||
consumePhoneHandoffForRelay,
|
||||
} from "@/db/relay-control-plane";
|
||||
import { apiError, readJsonMutation } from "../../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const principal = await authenticateRelayNode(request);
|
||||
const payload = await readJsonMutation<{
|
||||
ticket?: string;
|
||||
pwa_origin?: string;
|
||||
name?: string;
|
||||
phone_ed25519_public?: string;
|
||||
phone_x25519_public?: string;
|
||||
identity_fingerprint?: string;
|
||||
}>(request);
|
||||
return Response.json(await consumePhoneHandoffForRelay({
|
||||
principal,
|
||||
ticket: payload.ticket ?? "",
|
||||
pwaOrigin: payload.pwa_origin ?? "",
|
||||
name: payload.name ?? "",
|
||||
phoneEd25519Public: payload.phone_ed25519_public ?? "",
|
||||
phoneX25519Public: payload.phone_x25519_public ?? "",
|
||||
identityFingerprint: payload.identity_fingerprint ?? "",
|
||||
}), { headers: { "cache-control": "no-store" } });
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
authenticateRelayNode,
|
||||
heartbeatRelayNode,
|
||||
} from "@/db/relay-control-plane";
|
||||
import { relayMigrationAssignments } from "@/db/relay-migrations";
|
||||
import { relayPurgeAssignments } from "@/db/relay-purges";
|
||||
import { apiError, readJsonMutation } from "../../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const principal = await authenticateRelayNode(request);
|
||||
const payload = await readJsonMutation<{
|
||||
generation?: number;
|
||||
capacity_tenants?: number;
|
||||
}>(request);
|
||||
const heartbeat = await heartbeatRelayNode({
|
||||
principal,
|
||||
generation: Number(payload.generation ?? -1),
|
||||
capacityTenants: Number(payload.capacity_tenants ?? -1),
|
||||
});
|
||||
const [migrations, purges] = await Promise.all([
|
||||
relayMigrationAssignments(principal),
|
||||
relayPurgeAssignments(principal),
|
||||
]);
|
||||
return Response.json(
|
||||
{ ...heartbeat, migrations, purges },
|
||||
{ headers: { "cache-control": "no-store" } },
|
||||
);
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { authenticateRelayNode } from "@/db/relay-control-plane";
|
||||
import { advanceRelayMigration } from "@/db/relay-migrations";
|
||||
import { apiError, readJsonMutation } from "../../../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const principal = await authenticateRelayNode(request);
|
||||
const payload = await readJsonMutation<{
|
||||
migration_id?: string;
|
||||
action?: "quiesced" | "copied" | "switched" | "finalized" | "failed";
|
||||
backup_ref?: string;
|
||||
manifest_sha256?: string;
|
||||
error_code?: string;
|
||||
}>(request);
|
||||
if (!payload.action) {
|
||||
return Response.json(
|
||||
{ error_code: "invalid_relay_migration", message: "迁移动作无效", retryable: false },
|
||||
{ status: 400, headers: { "cache-control": "no-store" } },
|
||||
);
|
||||
}
|
||||
const migration = await advanceRelayMigration({
|
||||
principal,
|
||||
migrationId: payload.migration_id ?? "",
|
||||
action: payload.action,
|
||||
backupRef: payload.backup_ref,
|
||||
manifestSha256: payload.manifest_sha256,
|
||||
errorCode: payload.error_code,
|
||||
});
|
||||
return Response.json(
|
||||
{ migration_id: migration.id, state: migration.state, updated_at: migration.updated_at },
|
||||
{ headers: { "cache-control": "no-store" } },
|
||||
);
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { authenticateRelayNode } from "@/db/relay-control-plane";
|
||||
import { advanceRelayPurge } from "@/db/relay-purges";
|
||||
import { apiError, readJsonMutation } from "../../../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const principal = await authenticateRelayNode(request);
|
||||
const payload = await readJsonMutation<{
|
||||
purge_id?: string;
|
||||
action?: "completed" | "failed";
|
||||
evidence_sha256?: string;
|
||||
error_code?: string;
|
||||
}>(request);
|
||||
if (!payload.action) {
|
||||
return Response.json(
|
||||
{ error_code: "invalid_relay_purge", message: "租户删除动作无效", retryable: false },
|
||||
{ status: 400, headers: { "cache-control": "no-store" } },
|
||||
);
|
||||
}
|
||||
const purge = await advanceRelayPurge({
|
||||
principal,
|
||||
purgeId: payload.purge_id ?? "",
|
||||
action: payload.action,
|
||||
evidenceSha256: payload.evidence_sha256,
|
||||
errorCode: payload.error_code,
|
||||
});
|
||||
return Response.json(
|
||||
{ purge_id: purge.id, state: purge.state, updated_at: purge.updated_at },
|
||||
{ headers: { "cache-control": "no-store" } },
|
||||
);
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { authenticateRelayNode } from "@/db/relay-control-plane";
|
||||
import { claimDevice, DomainError } from "@/db/repository";
|
||||
import { apiError, readJsonMutation } from "../../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
await authenticateRelayNode(request);
|
||||
const payload = await readJsonMutation<{
|
||||
bootstrap_token?: string;
|
||||
source_hash?: string;
|
||||
os?: string;
|
||||
ed25519_public?: string;
|
||||
x25519_public?: string;
|
||||
identity_fingerprint?: string;
|
||||
transport_mode?: string;
|
||||
registration_proof?: string;
|
||||
daemon_version?: string;
|
||||
registration_retry_key?: string;
|
||||
}>(request);
|
||||
const sourceHash = payload.source_hash?.trim().toLowerCase() ?? "";
|
||||
if (!/^[0-9a-f]{64}$/u.test(sourceHash)) {
|
||||
throw new DomainError("registration_rate_limited", "注册来源摘要无效", 429, true, 60);
|
||||
}
|
||||
const result = await claimDevice({
|
||||
bootstrapToken: payload.bootstrap_token ?? "",
|
||||
trustedSourceHash: sourceHash,
|
||||
os: payload.os ?? "",
|
||||
ed25519Public: payload.ed25519_public ?? "",
|
||||
x25519Public: payload.x25519_public ?? "",
|
||||
identityFingerprint: payload.identity_fingerprint ?? "",
|
||||
transportMode: payload.transport_mode ?? "",
|
||||
registrationProof: payload.registration_proof ?? "",
|
||||
daemonVersion: payload.daemon_version ?? "",
|
||||
registrationRetryKey: payload.registration_retry_key ?? "",
|
||||
});
|
||||
return Response.json(result, {
|
||||
status: 200,
|
||||
headers: { "cache-control": "no-store" },
|
||||
});
|
||||
} catch (error) {
|
||||
const response = apiError(error);
|
||||
response.headers.set("cache-control", "no-store");
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import {
|
||||
authenticateRelayNode,
|
||||
resolveDeviceRouteForRelay,
|
||||
} from "@/db/relay-control-plane";
|
||||
import { apiError, readJsonMutation } from "../../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const principal = await authenticateRelayNode(request);
|
||||
const payload = await readJsonMutation<{ device_id?: string; token_hash?: string }>(request);
|
||||
return Response.json(
|
||||
await resolveDeviceRouteForRelay({
|
||||
principal,
|
||||
deviceId: payload.device_id ?? "",
|
||||
tokenHash: payload.token_hash ?? "",
|
||||
}),
|
||||
{ headers: { "cache-control": "no-store" } },
|
||||
);
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import {
|
||||
authenticateRelayNode,
|
||||
resolveHandoffRouteForRelay,
|
||||
} from "@/db/relay-control-plane";
|
||||
import { apiError, readJsonMutation } from "../../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const principal = await authenticateRelayNode(request);
|
||||
const payload = await readJsonMutation<{ ticket?: string; pwa_origin?: string }>(request);
|
||||
return Response.json(
|
||||
await resolveHandoffRouteForRelay({
|
||||
principal,
|
||||
ticket: payload.ticket ?? "",
|
||||
pwaOrigin: payload.pwa_origin ?? "",
|
||||
}),
|
||||
{ headers: { "cache-control": "no-store" } },
|
||||
);
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
authenticateRelayNode,
|
||||
resolvePhoneRouteForRelay,
|
||||
} from "@/db/relay-control-plane";
|
||||
import { apiError, readJsonMutation } from "../../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const principal = await authenticateRelayNode(request);
|
||||
const payload = await readJsonMutation<{
|
||||
route_handle?: string;
|
||||
phone_token_hash?: string;
|
||||
}>(request);
|
||||
return Response.json(
|
||||
await resolvePhoneRouteForRelay({
|
||||
principal,
|
||||
routeHandle: payload.route_handle ?? "",
|
||||
phoneTokenHash: payload.phone_token_hash ?? "",
|
||||
}),
|
||||
{ headers: { "cache-control": "no-store" } },
|
||||
);
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
authenticateRelayNode,
|
||||
resolveTenantRouteForRelay,
|
||||
} from "@/db/relay-control-plane";
|
||||
import { apiError, readJsonMutation } from "../../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const principal = await authenticateRelayNode(request);
|
||||
const payload = await readJsonMutation<{
|
||||
tenant_id?: string;
|
||||
placement_generation?: number;
|
||||
}>(request);
|
||||
return Response.json(
|
||||
await resolveTenantRouteForRelay({
|
||||
principal,
|
||||
tenantId: payload.tenant_id ?? "",
|
||||
placementGeneration: Number(payload.placement_generation ?? -1),
|
||||
}),
|
||||
{ headers: { "cache-control": "no-store" } },
|
||||
);
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
authenticateRelayNode,
|
||||
revokePhoneForRelay,
|
||||
} from "@/db/relay-control-plane";
|
||||
import { apiError, readJsonMutation } from "../../../respond";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const principal = await authenticateRelayNode(request);
|
||||
const payload = await readJsonMutation<{
|
||||
tenant_id?: string;
|
||||
phone_id?: string;
|
||||
reason?: string;
|
||||
}>(request);
|
||||
return Response.json(await revokePhoneForRelay({
|
||||
principal,
|
||||
tenantId: payload.tenant_id ?? "",
|
||||
phoneId: payload.phone_id ?? "",
|
||||
reason: payload.reason ?? "",
|
||||
}), { headers: { "cache-control": "no-store" } });
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { env } from "cloudflare:workers";
|
||||
import { getCloudViewer } from "@/app/cloud-auth";
|
||||
import { createPhoneHandoffTicket } from "@/db/relay-control-plane";
|
||||
import { DomainError, getOrCreateAccount } from "@/db/repository";
|
||||
import { apiError } from "../../respond";
|
||||
|
||||
export async function POST() {
|
||||
try {
|
||||
const viewer = await getCloudViewer();
|
||||
if (!viewer) throw new DomainError("authentication_required", "请先登录", 401);
|
||||
const pwaOrigin = env.NEKONEST_CLOUD_PWA_ORIGIN?.trim() ?? "";
|
||||
if (!pwaOrigin) {
|
||||
throw new DomainError("pwa_handoff_unavailable", "Cloud PWA 地址尚未配置", 503, true, 30);
|
||||
}
|
||||
const account = await getOrCreateAccount(viewer);
|
||||
return Response.json(
|
||||
await createPhoneHandoffTicket({ accountId: account.id, pwaOrigin }),
|
||||
{ status: 201, headers: { "cache-control": "no-store" } },
|
||||
);
|
||||
} catch (error) {
|
||||
return apiError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { DomainError } from "@/db/repository";
|
||||
|
||||
function requireJsonMutation(request: Request): void {
|
||||
const contentType = request.headers
|
||||
.get("content-type")
|
||||
?.split(";", 1)[0]
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (contentType !== "application/json") {
|
||||
throw new DomainError(
|
||||
"json_required",
|
||||
"写操作只接受 application/json",
|
||||
415,
|
||||
);
|
||||
}
|
||||
|
||||
const contentLength = Number(request.headers.get("content-length") ?? "0");
|
||||
if (Number.isFinite(contentLength) && contentLength > 32_768) {
|
||||
throw new DomainError("request_too_large", "请求内容过大", 413);
|
||||
}
|
||||
|
||||
const origin = request.headers.get("origin");
|
||||
if (origin && origin !== new URL(request.url).origin) {
|
||||
throw new DomainError("invalid_origin", "拒绝跨站写操作", 403);
|
||||
}
|
||||
const fetchSite = request.headers.get("sec-fetch-site");
|
||||
if (fetchSite && fetchSite !== "same-origin") {
|
||||
throw new DomainError("invalid_fetch_site", "拒绝跨站写操作", 403);
|
||||
}
|
||||
}
|
||||
|
||||
export async function readJsonMutation<T>(request: Request): Promise<T> {
|
||||
requireJsonMutation(request);
|
||||
const body = await request.text();
|
||||
if (new TextEncoder().encode(body).byteLength > 32_768) {
|
||||
throw new DomainError("request_too_large", "请求内容过大", 413);
|
||||
}
|
||||
try {
|
||||
return JSON.parse(body) as T;
|
||||
} catch {
|
||||
throw new DomainError("invalid_json", "请求不是有效 JSON", 400);
|
||||
}
|
||||
}
|
||||
|
||||
export function apiError(error: unknown) {
|
||||
if (error instanceof DomainError) {
|
||||
const headers = new Headers({ "cache-control": "no-store" });
|
||||
if (error.retryAfterSeconds !== undefined) {
|
||||
headers.set("retry-after", String(error.retryAfterSeconds));
|
||||
}
|
||||
return Response.json({
|
||||
error_code: error.code,
|
||||
error: error.code,
|
||||
message: error.message,
|
||||
retryable: error.retryable,
|
||||
...(error.retryAfterSeconds === undefined
|
||||
? {}
|
||||
: { retry_after_seconds: error.retryAfterSeconds }),
|
||||
...(error.actionUrl === undefined ? {} : { action_url: error.actionUrl }),
|
||||
}, { status: error.status, headers });
|
||||
}
|
||||
console.error("Unhandled NekoNest Cloud API error", error);
|
||||
return Response.json(
|
||||
{
|
||||
error_code: "internal_error",
|
||||
error: "internal_error",
|
||||
message: "服务暂时不可用,请稍后重试",
|
||||
retryable: true,
|
||||
},
|
||||
{ status: 500, headers: { "cache-control": "no-store" } },
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { getServiceStatusSnapshot } from "@/db/repository";
|
||||
import { apiError } from "../respond";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const snapshot = await getServiceStatusSnapshot();
|
||||
return Response.json(snapshot, {
|
||||
headers: { "cache-control": "no-store" },
|
||||
});
|
||||
} catch (error) {
|
||||
const response = apiError(error);
|
||||
response.headers.set("cache-control", "no-store");
|
||||
return response;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user