149 lines
3.6 KiB
TypeScript

import { and, eq, isNotNull } from "drizzle-orm";
import { z } from "zod";
import {
createUserAccessKey,
listUserAccessKeys,
} from "@/lib/access-keys/service";
import {
MIN_PROVISIONING_SECRET_LENGTH,
parseProvisioningBearerSecret,
verifyProvisioningSecret,
} from "@/lib/access-keys/provisioning-secret";
import { db } from "@/lib/db";
import { user } from "@/lib/db/schema";
const provisionAccessKeySchema = z.object({
dingTalkUsername: z.string().trim().min(1).max(100),
dingTalkUserId: z.string().trim().min(1).max(128).optional(),
keyName: z.string().trim().min(1).max(100).optional(),
scopes: z
.array(z.enum(["canvas:read", "canvas:write"]))
.min(1)
.optional(),
expiresInDays: z.number().int().min(1).max(365).optional(),
});
function jsonError(error: string, code: string, status: number) {
return Response.json(
{ success: false, error, code },
{ status, headers: { "Cache-Control": "no-store" } }
);
}
export async function POST(request: Request) {
const expectedSecret = String(
process.env.MCP_KEY_PROVISIONING_SECRET || ""
).trim();
if (expectedSecret.length < MIN_PROVISIONING_SECRET_LENGTH) {
return jsonError(
"Access Key provisioning is not configured",
"PROVISIONING_NOT_CONFIGURED",
503
);
}
const providedSecret = parseProvisioningBearerSecret(
request.headers.get("authorization")
);
if (!verifyProvisioningSecret(providedSecret, expectedSecret)) {
return jsonError(
"Missing or invalid provisioning credential",
"PROVISIONING_UNAUTHORIZED",
401
);
}
const parsedBody = provisionAccessKeySchema.safeParse(
await request.json().catch(() => null)
);
if (!parsedBody.success) {
return jsonError(
"Invalid provisioning request",
"PROVISIONING_REQUEST_INVALID",
400
);
}
const body = parsedBody.data;
const conditions = [
eq(user.name, body.dingTalkUsername),
eq(user.isActive, true),
isNotNull(user.dingtalkUserId),
];
if (body.dingTalkUserId) {
conditions.push(eq(user.dingtalkUserId, body.dingTalkUserId));
}
const matches = await db
.select({
id: user.id,
name: user.name,
dingtalkUserId: user.dingtalkUserId,
})
.from(user)
.where(and(...conditions))
.limit(2);
if (matches.length === 0) {
return jsonError(
"No active DingTalk user matched the supplied identity",
"DINGTALK_USER_NOT_FOUND",
404
);
}
if (matches.length > 1) {
return jsonError(
"DingTalk username is not unique; provide dingTalkUserId",
"DINGTALK_USERNAME_AMBIGUOUS",
409
);
}
const matchedUser = matches[0];
const existingKeys = await listUserAccessKeys(matchedUser.id);
const activeKeyCount = existingKeys.filter(
(item) =>
!item.revokedAt &&
(!item.expiresAt || new Date(item.expiresAt).getTime() > Date.now())
).length;
if (activeKeyCount >= 10) {
return jsonError(
"The user already has 10 active Access Keys",
"ACCESS_KEY_LIMIT_REACHED",
409
);
}
try {
const accessKey = await createUserAccessKey({
userId: matchedUser.id,
name: body.keyName || "第三方 OpenClaw",
scopes: body.scopes,
expiresInDays: body.expiresInDays,
});
return Response.json(
{
success: true,
data: {
user: matchedUser,
accessKey,
},
},
{
status: 201,
headers: {
"Cache-Control": "no-store",
Pragma: "no-cache",
},
}
);
} catch {
return jsonError(
"Failed to provision Access Key",
"ACCESS_KEY_PROVISION_FAILED",
500
);
}
}