149 lines
3.6 KiB
TypeScript
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
|
|
);
|
|
}
|
|
}
|