feat: 补齐 v03 用户管理 Web parity
This commit is contained in:
@@ -2,6 +2,7 @@ const GET_PROXY_PREFIXES = Object.freeze(["/v1/"]);
|
||||
const GET_PROXY_ROUTES = new Set(["/v1", "/auth/session", "/auth/bootstrap", "/auth/workspace-bootstrap"]);
|
||||
const POST_PROXY_ROUTES = new Set([
|
||||
"/auth/login",
|
||||
"/auth/register",
|
||||
"/auth/logout",
|
||||
"/json-rpc",
|
||||
"/v1/agent/chat",
|
||||
@@ -18,6 +19,7 @@ const PUBLIC_PROXY_ROUTES = new Set([
|
||||
"GET /auth/bootstrap",
|
||||
"GET /auth/workspace-bootstrap",
|
||||
"POST /auth/login",
|
||||
"POST /auth/register",
|
||||
"POST /auth/logout",
|
||||
"POST /v1/agent/chat",
|
||||
"POST /v1/agent/chat/cancel",
|
||||
@@ -54,13 +56,16 @@ export function isCloudApiProxyRoute(method, pathname) {
|
||||
isAdminAccessWriteProxyRoute(normalizedMethod, normalizedPath) ||
|
||||
isProviderProfileManagementProxyRoute(normalizedMethod, normalizedPath) ||
|
||||
isApiKeyWriteProxyRoute(normalizedMethod, normalizedPath) ||
|
||||
isAdminBillingWriteProxyRoute(normalizedMethod, normalizedPath) ||
|
||||
normalizedPath.startsWith("/v1/rpc/") ||
|
||||
normalizedPath.startsWith("/v1/agent/sessions/") ||
|
||||
isWorkbenchWorkspaceWriteProxyRoute(normalizedMethod, normalizedPath);
|
||||
}
|
||||
if (normalizedMethod === "PATCH" || normalizedMethod === "PUT") {
|
||||
return isAdminAccessWriteProxyRoute(normalizedMethod, normalizedPath) ||
|
||||
isAdminBillingWriteProxyRoute(normalizedMethod, normalizedPath) ||
|
||||
isProviderProfileManagementProxyRoute(normalizedMethod, normalizedPath) ||
|
||||
isApiKeyWriteProxyRoute(normalizedMethod, normalizedPath) ||
|
||||
isWorkbenchWorkspaceWriteProxyRoute(normalizedMethod, normalizedPath) ||
|
||||
isAgentConversationWriteProxyRoute(normalizedMethod, normalizedPath);
|
||||
}
|
||||
@@ -97,11 +102,22 @@ function isProviderProfileManagementProxyRoute(method, pathname) {
|
||||
function isApiKeyWriteProxyRoute(method, pathname) {
|
||||
if (pathname === "/v1/api-keys" && method === "POST") return true;
|
||||
if (!pathname.startsWith("/v1/api-keys/")) return false;
|
||||
if (method === "PATCH") return true;
|
||||
if (method === "DELETE") return true;
|
||||
if (method !== "POST") return false;
|
||||
return pathname.endsWith("/regenerate");
|
||||
}
|
||||
|
||||
function isAdminBillingWriteProxyRoute(method, pathname) {
|
||||
const parts = pathname.split("/").filter(Boolean);
|
||||
if (parts.length < 4 || parts[0] !== "v1" || parts[1] !== "admin" || parts[2] !== "billing" || parts[3] !== "users") return false;
|
||||
if (method === "POST" && parts.length === 4) return true;
|
||||
if (method === "PATCH" && parts.length === 5) return true;
|
||||
if (method === "PATCH" && parts.length === 6 && parts[5] === "status") return true;
|
||||
if (method === "POST" && parts.length === 7 && parts[5] === "credits" && parts[6] === "adjust") return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function isWorkbenchWorkspaceWriteProxyRoute(method, pathname) {
|
||||
const parts = pathname.split("/").filter(Boolean);
|
||||
if (parts.length < 4 || parts[0] !== "v1" || parts[1] !== "workbench" || parts[2] !== "workspace") return false;
|
||||
|
||||
@@ -67,6 +67,12 @@ test("cloud web exposes auth bootstrap as a public auth route", () => {
|
||||
publicRoute: true,
|
||||
routeKey: "GET /auth/workspace-bootstrap"
|
||||
});
|
||||
assert.deepEqual(cloudWebProxyRoutePolicy("POST", "/auth/register"), {
|
||||
proxy: true,
|
||||
authRequired: false,
|
||||
publicRoute: true,
|
||||
routeKey: "POST /auth/register"
|
||||
});
|
||||
});
|
||||
|
||||
test("cloud web proxies authenticated API key management write routes", () => {
|
||||
@@ -82,6 +88,12 @@ test("cloud web proxies authenticated API key management write routes", () => {
|
||||
publicRoute: false,
|
||||
routeKey: "POST /v1/api-keys/key_1/regenerate"
|
||||
});
|
||||
assert.deepEqual(cloudWebProxyRoutePolicy("PATCH", "/v1/api-keys/key_1"), {
|
||||
proxy: true,
|
||||
authRequired: true,
|
||||
publicRoute: false,
|
||||
routeKey: "PATCH /v1/api-keys/key_1"
|
||||
});
|
||||
assert.deepEqual(cloudWebProxyRoutePolicy("DELETE", "/v1/api-keys/key_1"), {
|
||||
proxy: true,
|
||||
authRequired: true,
|
||||
@@ -90,6 +102,33 @@ test("cloud web proxies authenticated API key management write routes", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("cloud web proxies authenticated admin billing user management write routes", () => {
|
||||
assert.deepEqual(cloudWebProxyRoutePolicy("POST", "/v1/admin/billing/users"), {
|
||||
proxy: true,
|
||||
authRequired: true,
|
||||
publicRoute: false,
|
||||
routeKey: "POST /v1/admin/billing/users"
|
||||
});
|
||||
assert.deepEqual(cloudWebProxyRoutePolicy("PATCH", "/v1/admin/billing/users/usr_alice"), {
|
||||
proxy: true,
|
||||
authRequired: true,
|
||||
publicRoute: false,
|
||||
routeKey: "PATCH /v1/admin/billing/users/usr_alice"
|
||||
});
|
||||
assert.deepEqual(cloudWebProxyRoutePolicy("PATCH", "/v1/admin/billing/users/usr_alice/status"), {
|
||||
proxy: true,
|
||||
authRequired: true,
|
||||
publicRoute: false,
|
||||
routeKey: "PATCH /v1/admin/billing/users/usr_alice/status"
|
||||
});
|
||||
assert.deepEqual(cloudWebProxyRoutePolicy("POST", "/v1/admin/billing/users/usr_alice/credits/adjust"), {
|
||||
proxy: true,
|
||||
authRequired: true,
|
||||
publicRoute: false,
|
||||
routeKey: "POST /v1/admin/billing/users/usr_alice/credits/adjust"
|
||||
});
|
||||
});
|
||||
|
||||
test("cloud web proxies authenticated Admin Access write routes", () => {
|
||||
assert.deepEqual(cloudWebProxyRoutePolicy("POST", "/v1/admin/access/check"), {
|
||||
proxy: true,
|
||||
|
||||
@@ -15,8 +15,6 @@ const readOnlyRpcMethods = new Set([
|
||||
]);
|
||||
const gzipAsync = promisify(gzip);
|
||||
const GZIP_MIN_BYTES = 1024;
|
||||
const DEFAULT_AUTH_USERNAME = "admin";
|
||||
const DEFAULT_AUTH_PASSWORD = "hwlab2026";
|
||||
const DEFAULT_WORKBENCH_PROJECT_ID = "prj_hwpod_workbench";
|
||||
const CLIENT_DISCONNECT_ERROR_CODES = new Set([
|
||||
"ECONNRESET",
|
||||
@@ -193,6 +191,10 @@ export async function handleCloudWebAuth(options) {
|
||||
await proxyCloudApi(options);
|
||||
return true;
|
||||
}
|
||||
if (url.pathname === "/auth/register" && request.method === "POST") {
|
||||
await proxyCloudApi(options);
|
||||
return true;
|
||||
}
|
||||
if (url.pathname === "/auth/logout" && request.method === "POST") {
|
||||
await proxyCloudApi(options);
|
||||
return true;
|
||||
@@ -246,35 +248,8 @@ async function handleWorkspaceBootstrap(options) {
|
||||
}
|
||||
|
||||
async function resolveAuthBootstrap({ request, cloudApiBaseUrl, cloudApiProxyTimeoutMs }) {
|
||||
const config = webAuthConfig();
|
||||
if (config.mode !== "auto" || hasCookieHeader(request.headers.cookie)) {
|
||||
const session = await requestCloudApiJson({ request, method: "GET", pathname: "/auth/session", cloudApiBaseUrl, cloudApiProxyTimeoutMs });
|
||||
if (session.ok && session.body?.authenticated === true) return withCookieHeader(request, session);
|
||||
if (config.mode !== "auto") return withCookieHeader(request, session);
|
||||
}
|
||||
|
||||
const login = await requestCloudApiJson({
|
||||
request,
|
||||
method: "POST",
|
||||
pathname: "/auth/login",
|
||||
body: JSON.stringify({ username: config.username, password: config.password }),
|
||||
cloudApiBaseUrl,
|
||||
cloudApiProxyTimeoutMs
|
||||
});
|
||||
return withCookieHeader(request, login);
|
||||
}
|
||||
|
||||
function webAuthConfig() {
|
||||
return {
|
||||
mode: authConfigValue("HWLAB_CLOUD_WEB_AUTH_MODE", "auto"),
|
||||
username: authConfigValue("HWLAB_CLOUD_WEB_AUTH_USERNAME", DEFAULT_AUTH_USERNAME),
|
||||
password: authConfigValue("HWLAB_CLOUD_WEB_AUTH_PASSWORD", DEFAULT_AUTH_PASSWORD)
|
||||
};
|
||||
}
|
||||
|
||||
function authConfigValue(name, fallback) {
|
||||
const value = process.env[name];
|
||||
return typeof value === "string" && value.trim() ? value.trim() : fallback;
|
||||
const session = await requestCloudApiJson({ request, method: "GET", pathname: "/auth/session", cloudApiBaseUrl, cloudApiProxyTimeoutMs });
|
||||
return withCookieHeader(request, session);
|
||||
}
|
||||
|
||||
function authConfigValueFromSearch(value, fallback) {
|
||||
|
||||
Reference in New Issue
Block a user