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 keys
pull/9078/head
Omar López 2022-11-29 15:06:23 -07:00 committed by GitHub
parent 26ea743af2
commit d35f27014e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 203 additions and 0 deletions

View File

@ -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,
});

View File

@ -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" });
}

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);
})
);

View File

@ -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);

View File

@ -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);

View File

@ -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"),
})
);