mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-21 11:28:43 +00:00
feat(infisical-pg): added all previous simple server middlewares, swagger support
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
import dotenv from "dotenv";
|
||||
import fasitfy from "fastify";
|
||||
import { z } from "zod";
|
||||
import type { FastifyCookieOptions } from "@fastify/cookie";
|
||||
import cookie from "@fastify/cookie";
|
||||
import type { FastifyCorsOptions } from "@fastify/cors";
|
||||
import cors from "@fastify/cors";
|
||||
import helmet from "@fastify/helmet";
|
||||
import type { FastifyRateLimitOptions } from "@fastify/rate-limit";
|
||||
import ratelimiter from "@fastify/rate-limit";
|
||||
|
||||
import { initEnvConfig } from "@lib/config/env";
|
||||
import { initLogger } from "@lib/logger";
|
||||
|
||||
import { globalRateLimiterCfg } from "./config/rateLimiter";
|
||||
import { serializerCompiler, validatorCompiler, ZodTypeProvider } from "./plugins/fastify-zod";
|
||||
import { fastifyIp } from "./plugins/ip";
|
||||
import { fastifySwagger } from "./plugins/swagger";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
// Run the server!
|
||||
const main = async () => {
|
||||
const logger = await initLogger();
|
||||
initEnvConfig(logger);
|
||||
|
||||
const server = fasitfy({
|
||||
logger,
|
||||
trustProxy: true
|
||||
}).withTypeProvider<ZodTypeProvider>();
|
||||
|
||||
server.setValidatorCompiler(validatorCompiler);
|
||||
server.setSerializerCompiler(serializerCompiler);
|
||||
|
||||
try {
|
||||
// TODO(akhilmhdh:pg): change this to environment variable with default
|
||||
await server.register<FastifyCookieOptions>(cookie, {
|
||||
secret: "infisical-cookie-secret"
|
||||
});
|
||||
|
||||
await server.register<FastifyCorsOptions>(cors, {
|
||||
credentials: true,
|
||||
origin: "http://localhost:3000"
|
||||
});
|
||||
// pull ip based on various proxy headers
|
||||
await server.register(fastifyIp);
|
||||
|
||||
// Rate limiters and security headers
|
||||
await server.register<FastifyRateLimitOptions>(ratelimiter, globalRateLimiterCfg);
|
||||
await server.register(helmet);
|
||||
|
||||
await server.register(fastifySwagger);
|
||||
|
||||
// Declare a route
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({ hello: z.string() })
|
||||
}
|
||||
},
|
||||
handler: () => ({
|
||||
hello: "world"
|
||||
})
|
||||
});
|
||||
|
||||
await server.ready();
|
||||
server.swagger();
|
||||
await server.listen({ port: 8000 });
|
||||
} catch (err) {
|
||||
server.log.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { RateLimitOptions } from "@fastify/rate-limit";
|
||||
|
||||
export const globalRateLimiterCfg: RateLimitOptions = {
|
||||
timeWindow: 60 * 1000,
|
||||
max: 100,
|
||||
keyGenerator: (req) => req.realIp
|
||||
};
|
||||
@@ -0,0 +1,142 @@
|
||||
/* eslint-disable */
|
||||
// Code taken from https://www.npmjs.com/package/fastify-type-provider-zod
|
||||
// Full credits goes to https://github.com/turkerdev
|
||||
// Code taken to keep in in house
|
||||
import type { FastifySchema, FastifySchemaCompiler, FastifyTypeProvider } from "fastify";
|
||||
import type { FastifySerializerCompiler } from "fastify/types/schema";
|
||||
import type { z, ZodAny, ZodTypeAny } from "zod";
|
||||
import { zodToJsonSchema } from "zod-to-json-schema";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type FreeformRecord = Record<string, any>;
|
||||
|
||||
const defaultSkipList = [
|
||||
"/documentation/",
|
||||
"/documentation/initOAuth",
|
||||
"/documentation/json",
|
||||
"/documentation/uiConfig",
|
||||
"/documentation/yaml",
|
||||
"/documentation/*",
|
||||
"/documentation/static/*"
|
||||
];
|
||||
|
||||
export interface ZodTypeProvider extends FastifyTypeProvider {
|
||||
output: this["input"] extends ZodTypeAny ? z.infer<this["input"]> : never;
|
||||
}
|
||||
|
||||
interface Schema extends FastifySchema {
|
||||
hide?: boolean;
|
||||
}
|
||||
|
||||
const zodToJsonSchemaOptions = {
|
||||
target: "openApi3",
|
||||
$refStrategy: "none"
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function hasOwnProperty<T, K extends PropertyKey>(obj: T, prop: K): obj is T & Record<K, any> {
|
||||
return Object.prototype.hasOwnProperty.call(obj, prop);
|
||||
}
|
||||
|
||||
function resolveSchema(maybeSchema: ZodAny | { properties: ZodAny }): Pick<ZodAny, "safeParse"> {
|
||||
if (hasOwnProperty(maybeSchema, "safeParse")) {
|
||||
return maybeSchema;
|
||||
}
|
||||
if (hasOwnProperty(maybeSchema, "properties")) {
|
||||
return maybeSchema.properties;
|
||||
}
|
||||
throw new Error(`Invalid schema passed: ${JSON.stringify(maybeSchema)}`);
|
||||
}
|
||||
|
||||
export const createJsonSchemaTransform = ({ skipList }: { skipList: readonly string[] }) => {
|
||||
return ({ schema, url }: { schema: Schema; url: string }) => {
|
||||
if (!schema) {
|
||||
return {
|
||||
schema,
|
||||
url
|
||||
};
|
||||
}
|
||||
|
||||
const { response, headers, querystring, body, params, hide, ...rest } = schema;
|
||||
|
||||
const transformed: FreeformRecord = {};
|
||||
|
||||
if (skipList.includes(url) || hide) {
|
||||
transformed.hide = true;
|
||||
return { schema: transformed, url };
|
||||
}
|
||||
|
||||
const zodSchemas: FreeformRecord = { headers, querystring, body, params };
|
||||
|
||||
for (const prop in zodSchemas) {
|
||||
const zodSchema = zodSchemas[prop];
|
||||
if (zodSchema) {
|
||||
transformed[prop] = zodToJsonSchema(zodSchema, zodToJsonSchemaOptions);
|
||||
}
|
||||
}
|
||||
|
||||
if (response) {
|
||||
transformed.response = {};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
for (const prop in response as any) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const schema = resolveSchema((response as any)[prop]);
|
||||
|
||||
const transformedResponse = zodToJsonSchema(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
schema as any,
|
||||
zodToJsonSchemaOptions
|
||||
);
|
||||
transformed.response[prop] = transformedResponse;
|
||||
}
|
||||
}
|
||||
|
||||
for (const prop in rest) {
|
||||
const meta = rest[prop as keyof typeof rest];
|
||||
if (meta) {
|
||||
transformed[prop] = meta;
|
||||
}
|
||||
}
|
||||
|
||||
return { schema: transformed, url };
|
||||
};
|
||||
};
|
||||
|
||||
export const jsonSchemaTransform = createJsonSchemaTransform({
|
||||
skipList: defaultSkipList
|
||||
});
|
||||
|
||||
export const validatorCompiler: FastifySchemaCompiler<ZodAny> =
|
||||
({ schema }) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(data): any => {
|
||||
try {
|
||||
return { value: schema.parse(data) };
|
||||
} catch (error) {
|
||||
return { error };
|
||||
}
|
||||
};
|
||||
|
||||
export class ResponseValidationError extends Error {
|
||||
public details: FreeformRecord;
|
||||
|
||||
constructor(validationResult: FreeformRecord) {
|
||||
super("Response doesn't match the schema");
|
||||
this.name = "ResponseValidationError";
|
||||
this.details = validationResult.error;
|
||||
}
|
||||
}
|
||||
|
||||
export const serializerCompiler: FastifySerializerCompiler<ZodAny | { properties: ZodAny }> =
|
||||
({ schema: maybeSchema }) =>
|
||||
(data) => {
|
||||
const schema: Pick<ZodAny, "safeParse"> = resolveSchema(maybeSchema);
|
||||
|
||||
const result = schema.safeParse(data);
|
||||
if (result.success) {
|
||||
return JSON.stringify(result.data);
|
||||
}
|
||||
|
||||
throw new ResponseValidationError(result);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import fp from "fastify-plugin";
|
||||
|
||||
/*! https://github.com/pbojinov/request-ip/blob/9501cdf6e73059cc70fc6890adb086348d7cca46/src/index.js.
|
||||
MIT License. 2022 Petar Bojinov - [email protected] */
|
||||
const headersOrder = [
|
||||
"x-client-ip", // Most common
|
||||
"x-forwarded-for", // Mostly used by proxies
|
||||
"cf-connecting-ip", // Cloudflare
|
||||
"Cf-Pseudo-IPv4", // Cloudflare
|
||||
"fastly-client-ip",
|
||||
"true-client-ip", // Akamai and Cloudflare
|
||||
"x-real-ip", // Nginx
|
||||
"x-cluser-client-ip", // Rackspace LB
|
||||
"forwarded-for",
|
||||
"x-forwarded",
|
||||
"forwarded",
|
||||
"x-appengine-user-ip" // GCP App Engine
|
||||
];
|
||||
|
||||
export const fastifyIp = fp(async (fastify) => {
|
||||
fastify.decorateRequest("realIp", null);
|
||||
fastify.addHook("onRequest", async (req) => {
|
||||
const forwardedIpHeader = headersOrder.find((header) => Boolean(req.headers[header]));
|
||||
const forwardedIp = forwardedIpHeader ? req.headers[forwardedIpHeader] : undefined;
|
||||
if (forwardedIp) {
|
||||
req.realIp = Array.isArray(forwardedIp) ? forwardedIp[0] : forwardedIp;
|
||||
} else {
|
||||
req.realIp = req.ip;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import fp from "fastify-plugin";
|
||||
import swagger from "@fastify/swagger";
|
||||
import swaggerUI from "@fastify/swagger-ui";
|
||||
|
||||
import { jsonSchemaTransform } from "./fastify-zod";
|
||||
|
||||
export const fastifySwagger = fp(async (fastify) => {
|
||||
await fastify.register(swagger, {
|
||||
transform: jsonSchemaTransform,
|
||||
openapi: {
|
||||
info: {
|
||||
title: "Infisical API",
|
||||
description: "List of all available APIs that can be consumed",
|
||||
version: "0.0.1"
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: "https://app.infisical.com",
|
||||
description: "Production server"
|
||||
},
|
||||
{
|
||||
url: "http://localhost:8000",
|
||||
description: "Local server"
|
||||
}
|
||||
],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
bearer: {
|
||||
type: "http",
|
||||
scheme: "bearer",
|
||||
bearerFormat: "JWT",
|
||||
description: "A service token in Infisical"
|
||||
},
|
||||
apiKey: {
|
||||
type: "apiKey",
|
||||
in: "header",
|
||||
name: "X-API-Key",
|
||||
description: "An API Key in Infisical"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await fastify.register(swaggerUI, {
|
||||
routePrefix: "/docs"
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user