Merge pull request #65 from calcom/fix/api-hariom

pull/9078/head
Agusti Fernandez Pardo 2022-05-01 00:41:30 +02:00 committed by GitHub
commit 6124577bc2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 56 additions and 43 deletions

View File

@ -172,3 +172,9 @@ DATABASE_URL=DATABASE_URL="postgresql://postgres:@localhost:5450/calendso"
API*KEY_PREFIX=cal*# This can be changed per envirorment so cal*test* for staging for example. API*KEY_PREFIX=cal*# This can be changed per envirorment so cal*test* for staging for example.
> If you're self-hosting under our commercial license, you can use any prefix you want for api keys. either leave the default cal\_ (not providing any envirorment variable) or modify it > If you're self-hosting under our commercial license, you can use any prefix you want for api keys. either leave the default cal\_ (not providing any envirorment variable) or modify it
**Ensure that while testing swagger, API project should be run in production mode**
We make sure of this by not using next in dev, but next build && next start, if you want hot module reloading and such when developing, please use yarn run next directly on apps/api.
See <https://github.com/vercel/next.js/blob/canary/packages/next/server/dev/hot-reloader.ts#L79>. Here in dev mode OPTIONS method is hardcoded to return only GET and OPTIONS as allowed method. Running in Production mode would cause this file to be not used. This is hot-reloading logic only.
To remove this limitation, we need to ensure that on local endpoints are requested by swagger at /api/v1 and not /v1

View File

@ -4,6 +4,21 @@ import { NextMiddleware } from "next-api-middleware";
export const addRequestId: NextMiddleware = async (_req, res, next) => { export const addRequestId: NextMiddleware = async (_req, res, next) => {
// Apply header with unique ID to every request // Apply header with unique ID to every request
res.setHeader("Calcom-Response-ID", nanoid()); res.setHeader("Calcom-Response-ID", nanoid());
// Add all headers here instead of next.config.js as it is throwing error( Cannot set headers after they are sent to the client) for OPTIONS method
// It is known to happen only in Dev Mode.
res.setHeader("Access-Control-Allow-Credentials", "true");
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS, PATCH, DELETE, POST, PUT");
res.setHeader(
"Access-Control-Allow-Headers",
"X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, Content-Type, api_key, Authorization"
);
// Ensure all OPTIONS request are automatically successful. Headers are already set above.
if (_req.method === "OPTIONS") {
res.status(200).end();
return;
}
// Let remaining middleware and API route execute // Let remaining middleware and API route execute
await next(); await next();
}; };

View File

@ -15,7 +15,7 @@ export const httpMethod = (allowedHttpMethod: "GET" | "POST" | "PATCH" | "DELETE
// that checks if it's just a string or an array and apply the correct logic to both cases. // that checks if it's just a string or an array and apply the correct logic to both cases.
export const httpMethods = (allowedHttpMethod: string[]): NextMiddleware => { export const httpMethods = (allowedHttpMethod: string[]): NextMiddleware => {
return async function (req, res, next) { return async function (req, res, next) {
if (allowedHttpMethod.map((method) => method === req.method)) { if (allowedHttpMethod.some((method) => method === req.method || req.method == "OPTIONS")) {
await next(); await next();
} else { } else {
res.status(405).json({ message: `Only ${allowedHttpMethod} Method allowed` }); res.status(405).json({ message: `Only ${allowedHttpMethod} Method allowed` });

View File

@ -8,8 +8,6 @@ import prisma from "@calcom/prisma";
declare module "next" { declare module "next" {
export interface NextApiRequest extends IncomingMessage { export interface NextApiRequest extends IncomingMessage {
userId: number; userId: number;
body: any;
query: { [key: string]: string | string[] };
} }
} }
@ -22,21 +20,21 @@ export const dateNotInPast = function (date: Date) {
}; };
// This verifies the apiKey and sets the user if it is valid. // This verifies the apiKey and sets the user if it is valid.
export const verifyApiKey: NextMiddleware = async ({ query: { apiKey }, ...req }, res, next) => { export const verifyApiKey: NextMiddleware = async (req, res, next) => {
if (!apiKey) return res.status(401).json({ message: "No apiKey provided" }); if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" });
// We remove the prefix from the user provided api_key. If no env set default to "cal_" // We remove the prefix from the user provided api_key. If no env set default to "cal_"
const strippedApiKey = `${apiKey}`.replace(process.env.API_KEY_PREFIX || " cal_", ""); const strippedApiKey = `${req.query.apiKey}`.replace(process.env.API_KEY_PREFIX || "cal_", "");
// Hash the key again before matching against the database records. // Hash the key again before matching against the database records.
const hashedKey = hashAPIKey(strippedApiKey); const hashedKey = hashAPIKey(strippedApiKey);
// Check if the hashed api key exists in database. // Check if the hashed api key exists in database.
const validApiKey = await prisma.apiKey.findUnique({ where: { hashedKey } }); const apiKey = await prisma.apiKey.findUnique({ where: { hashedKey } });
// If we cannot find any api key. Throw a 401 Unauthorized. // If we cannot find any api key. Throw a 401 Unauthorized.
if (!validApiKey) return res.status(401).json({ error: "Your apiKey is not valid" }); if (!apiKey) return res.status(401).json({ error: "Your apiKey is not valid" });
if (validApiKey.expiresAt && dateNotInPast(validApiKey.expiresAt)) { if (apiKey.expiresAt && dateNotInPast(apiKey.expiresAt)) {
return res.status(401).json({ error: "This apiKey is expired" }); return res.status(401).json({ error: "This apiKey is expired" });
} }
if (!validApiKey.userId) return res.status(404).json({ error: "No user found for this apiKey" }); if (!apiKey.userId) return res.status(404).json({ error: "No user found for this apiKey" });
/* We save the user id in the request for later use */ /* We save the user id in the request for later use */
req.userId = validApiKey.userId; req.userId = apiKey.userId;
await next(); await next();
}; };

View File

@ -1,3 +1,5 @@
// https://www.npmjs.com/package/next-transpile-modules
// This makes our @calcom/prisma package from the monorepo to be transpiled and usable by API
const withTM = require("next-transpile-modules")([ const withTM = require("next-transpile-modules")([
"@calcom/app-store", "@calcom/app-store",
"@calcom/prisma", "@calcom/prisma",
@ -6,36 +8,28 @@ const withTM = require("next-transpile-modules")([
]); ]);
module.exports = withTM({ module.exports = withTM({
async headers() {
return [
{
// @note: disabling CORS matching all API routes as this will be a our Public API
source: "/api/:path*",
headers: [
{ key: "Access-Control-Allow-Credentials", value: "true" },
{ key: "Access-Control-Allow-Origin", value: "*" },
{ key: "Access-Control-Allow-Methods", value: "GET,OPTIONS,PATCH,DELETE,POST,PUT" },
{
key: "Access-Control-Allow-Headers",
value:
"X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, Content-Type, api_key, Authorization",
},
],
},
];
},
async rewrites() { async rewrites() {
return [ return {
// @note: redirects requests from: "/:rest*" the root level to the "/api/:rest*" folder by default. beforeFiles: [
{ // This redirects requests recieved at / the root to the /api/ folder.
source: "/:rest*", {
destination: "/api/:rest*", source: "/v:version/:rest*",
}, destination: "/api/v:version/:rest*",
// @note: redirects requests from api/v*/:rest to /api/:rest?version=* passing version as a query parameter. },
{ // This redirects requests to api/v*/ to /api/ passing version as a query parameter.
source: "/api/v:version/:rest*", {
destination: "/api/:rest*?version=:version", source: "/api/v:version/:rest*",
}, destination: "/api/:rest*?version=:version",
]; },
],
fallback: [
// These rewrites are checked after both pages/public files
// and dynamic routes are checked
{
source: "/:path*",
destination: `/api/:path*`,
},
],
};
}, },
}); });

View File

@ -7,8 +7,8 @@
"author": "Cal.com Inc.", "author": "Cal.com Inc.",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "PORT=3002 next", "dev": "next build && PORT=3002 next start",
"start": "next start", "start": "PORT=3002 next start",
"build": "next build", "build": "next build",
"lint": "next lint", "lint": "next lint",
"lint-fix": "next lint --fix && prettier --write .", "lint-fix": "next lint --fix && prettier --write .",