Implements API key endpoint (#211)
This allow us to manage our API keys directly from the API itself. User can: - Create own API keys - Edit own API keys (only the note field for now) - Delete own API keys - Get own API keys Admin can: - CRUD for any user - Get all API keyspull/9078/head
parent
26ea743af2
commit
d35f27014e
|
@ -0,0 +1,29 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { _ApiKeyModel as ApiKey } from "@calcom/prisma/zod";
|
||||
|
||||
export const apiKeyCreateBodySchema = ApiKey.pick({
|
||||
note: true,
|
||||
expiresAt: true,
|
||||
userId: true,
|
||||
})
|
||||
.partial({ userId: true })
|
||||
.merge(z.object({ neverExpires: z.boolean().optional() }))
|
||||
.strict();
|
||||
|
||||
export const apiKeyEditBodySchema = ApiKey.pick({
|
||||
note: true,
|
||||
})
|
||||
.partial()
|
||||
.strict();
|
||||
|
||||
export const apiKeyPublicSchema = ApiKey.pick({
|
||||
id: true,
|
||||
userId: true,
|
||||
note: true,
|
||||
createdAt: true,
|
||||
expiresAt: true,
|
||||
lastUsedAt: true,
|
||||
/** We might never want to expose these. Leaving this a as reminder. */
|
||||
// hashedKey: true,
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
|
||||
import { schemaQueryIdAsString } from "@lib/validations/shared/queryIdString";
|
||||
|
||||
export async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isAdmin, prisma } = req;
|
||||
const { id } = schemaQueryIdAsString.parse(req.query);
|
||||
// Admin can check any api key
|
||||
if (isAdmin) return;
|
||||
// Check if user can access the api key
|
||||
const apiKey = await prisma.apiKey.findFirst({
|
||||
where: { id, userId },
|
||||
});
|
||||
if (!apiKey) throw new HttpError({ statusCode: 404, message: "API key not found" });
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { schemaQueryIdAsString } from "@lib/validations/shared/queryIdString";
|
||||
|
||||
async function deleteHandler(req: NextApiRequest) {
|
||||
const { prisma, query } = req;
|
||||
const { id } = schemaQueryIdAsString.parse(query);
|
||||
await prisma.apiKey.delete({ where: { id } });
|
||||
return { message: `ApiKey with id: ${id} deleted` };
|
||||
}
|
||||
|
||||
export default defaultResponder(deleteHandler);
|
|
@ -0,0 +1,15 @@
|
|||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { apiKeyPublicSchema } from "@lib/validations/api-key";
|
||||
import { schemaQueryIdAsString } from "@lib/validations/shared/queryIdString";
|
||||
|
||||
async function getHandler(req: NextApiRequest) {
|
||||
const { prisma, query } = req;
|
||||
const { id } = schemaQueryIdAsString.parse(query);
|
||||
const api_key = await prisma.apiKey.findUniqueOrThrow({ where: { id } });
|
||||
return { api_key: apiKeyPublicSchema.parse(api_key) };
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
|
@ -0,0 +1,16 @@
|
|||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { apiKeyEditBodySchema, apiKeyPublicSchema } from "@lib/validations/api-key";
|
||||
import { schemaQueryIdAsString } from "@lib/validations/shared/queryIdString";
|
||||
|
||||
async function patchHandler(req: NextApiRequest) {
|
||||
const { prisma, body } = req;
|
||||
const { id } = schemaQueryIdAsString.parse(req.query);
|
||||
const data = apiKeyEditBodySchema.parse(body);
|
||||
const api_key = await prisma.apiKey.update({ where: { id }, data });
|
||||
return { api_key: apiKeyPublicSchema.parse(api_key) };
|
||||
}
|
||||
|
||||
export default defaultResponder(patchHandler);
|
|
@ -0,0 +1,18 @@
|
|||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "@lib/helpers/withMiddleware";
|
||||
|
||||
import { authMiddleware } from "./_auth-middleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await authMiddleware(req);
|
||||
return defaultHandler({
|
||||
GET: import("./_get"),
|
||||
PATCH: import("./_patch"),
|
||||
DELETE: import("./_delete"),
|
||||
})(req, res);
|
||||
})
|
||||
);
|
|
@ -0,0 +1,40 @@
|
|||
import type { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import { Ensure } from "@calcom/types/utils";
|
||||
|
||||
import { apiKeyPublicSchema } from "@lib/validations/api-key";
|
||||
import { schemaQuerySingleOrMultipleUserIds } from "@lib/validations/shared/queryUserId";
|
||||
|
||||
type CustomNextApiRequest = NextApiRequest & {
|
||||
args?: Prisma.ApiKeyFindManyArgs;
|
||||
};
|
||||
|
||||
/** Admins can query other users' API keys */
|
||||
function handleAdminRequests(req: CustomNextApiRequest) {
|
||||
// To match type safety with runtime
|
||||
if (!hasReqArgs(req)) throw Error("Missing req.args");
|
||||
const { userId, isAdmin } = req;
|
||||
if (isAdmin && req.query.userId) {
|
||||
const query = schemaQuerySingleOrMultipleUserIds.parse(req.query);
|
||||
const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
|
||||
req.args.where = { userId: { in: userIds } };
|
||||
if (Array.isArray(query.userId)) req.args.orderBy = { userId: "asc" };
|
||||
}
|
||||
}
|
||||
|
||||
function hasReqArgs(req: CustomNextApiRequest): req is Ensure<CustomNextApiRequest, "args"> {
|
||||
return "args" in req;
|
||||
}
|
||||
|
||||
async function getHandler(req: CustomNextApiRequest) {
|
||||
const { userId, isAdmin, prisma } = req;
|
||||
req.args = isAdmin ? {} : { where: { userId } };
|
||||
// Proof of concept: allowing mutation in exchange of composability
|
||||
handleAdminRequests(req);
|
||||
const data = await prisma.apiKey.findMany(req.args);
|
||||
return { api_keys: data.map((v) => apiKeyPublicSchema.parse(v)) };
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
|
@ -0,0 +1,44 @@
|
|||
import type { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
import { generateUniqueAPIKey } from "@calcom/features/ee/api-keys/lib/apiKeys";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { apiKeyCreateBodySchema, apiKeyPublicSchema } from "@lib/validations/api-key";
|
||||
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin, prisma } = req;
|
||||
const { neverExpires, userId: bodyUserId, ...input } = apiKeyCreateBodySchema.parse(req.body);
|
||||
const [hashedKey, apiKey] = generateUniqueAPIKey();
|
||||
const args: Prisma.ApiKeyCreateArgs = {
|
||||
data: {
|
||||
id: v4(),
|
||||
userId,
|
||||
...input,
|
||||
// And here we pass a null to expiresAt if never expires is true. otherwise just pass expiresAt from input
|
||||
expiresAt: neverExpires ? null : input.expiresAt,
|
||||
hashedKey,
|
||||
},
|
||||
};
|
||||
|
||||
if (!isAdmin && bodyUserId) throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` });
|
||||
|
||||
if (isAdmin && bodyUserId) {
|
||||
const where: Prisma.UserWhereInput = { id: bodyUserId };
|
||||
await prisma.user.findFirstOrThrow({ where });
|
||||
args.data.userId = bodyUserId;
|
||||
}
|
||||
|
||||
const result = await prisma.apiKey.create(args);
|
||||
return {
|
||||
api_key: {
|
||||
...apiKeyPublicSchema.parse(result),
|
||||
key: `${process.env.API_KEY_PREFIX ?? "cal_"}${apiKey}`,
|
||||
},
|
||||
message: "API key created successfully. Save the `key` value as it won't be displayed again.",
|
||||
};
|
||||
}
|
||||
|
||||
export default defaultResponder(postHandler);
|
|
@ -0,0 +1,10 @@
|
|||
import { defaultHandler } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "@lib/helpers/withMiddleware";
|
||||
|
||||
export default withMiddleware("HTTP_GET_OR_POST")(
|
||||
defaultHandler({
|
||||
GET: import("./_get"),
|
||||
POST: import("./_post"),
|
||||
})
|
||||
);
|
Loading…
Reference in New Issue