Skip to content

Commit

Permalink
chore: resolve build stuff (#139)
Browse files Browse the repository at this point in the history
  • Loading branch information
aldy505 authored May 23, 2024
1 parent 0212bbe commit 1958536
Show file tree
Hide file tree
Showing 15 changed files with 4,198 additions and 3,032 deletions.
9 changes: 2 additions & 7 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,10 @@ COPY fonts/ /usr/local/share/fonts/
# Files required by pnpm install
COPY pnpm-lock.yaml ./

# cache into the global store
RUN pnpm fetch

ADD . ./

RUN pnpm install -r --offline

# build the frontend code
RUN pnpm --filter "{packages/frontend}" build
RUN pnpm install && \
pnpm -r run build

WORKDIR /home/app/packages/backend

Expand Down
31 changes: 16 additions & 15 deletions packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"main": "index.js",
"scripts": {
"start": "esno -r dotenv/config src/index.ts",
"build": "tspc; tsc-alias",
"dev": "esno watch -r dotenv/config src/index.ts",
"fmt:write": "prettier --write --ignore-path ../../.gitignore .",
"fmt:check": "prettier --check --ignore-path ../../.gitignore .",
Expand All @@ -20,40 +21,40 @@
},
"dependencies": {
"@logtail/node": "0.4.0",
"@sentry/node": "^7.91.0",
"@tinyhttp/proxy-addr": "2.1.0",
"@sentry/node": "^8.3.0",
"@tinyhttp/proxy-addr": "2.1.3",
"dotenv": "16.0.3",
"flourite": "1.2.3",
"flourite": "1.2.4",
"gura": "1.4.4",
"helmet": "5.1.1",
"polka": "1.0.0-next.22",
"rate-limiter-flexible": "2.4.1",
"shared": "link:../shared",
"sharp": "0.32.1",
"shikiji": "^0.9.14",
"sirv": "2.0.3",
"toml": "3.0.0",
"yaml": "2.3.1",
"zod": "^3.22.3"
"sharp": "^0.33.4",
"shikiji": "^0.10.2",
"sirv": "^2.0.4",
"toml": "^3.0.0",
"yaml": "^2.4.2",
"zod": "^3.23.8"
},
"devDependencies": {
"@logtail/types": "^0.4.14",
"@types/node": "18.16.16",
"@types/node": "^20.12.12",
"@types/sharp": "0.31.1",
"@types/supertest": "2.0.12",
"@typescript-eslint/eslint-plugin": "5.59.7",
"@typescript-eslint/parser": "5.59.7",
"@vitest/coverage-v8": "^1.1.0",
"c8": "^8.0.1",
"esbuild": "0.17.19",
"eslint": "8.41.0",
"eslint-config-prettier": "8.8.0",
"eslint-plugin-prettier": "4.2.1",
"esno": "^4.0.0",
"prettier": "2.8.8",
"supertest": "6.3.3",
"tslib": "2.5.2",
"typescript": "4.9.5",
"ts-patch": "^3.1.2",
"tsc-alias": "^1.8.10",
"tslib": "^2.6.2",
"typescript": "^5.3.3",
"typescript-transform-paths": "^3.4.7",
"vitest": "^1.1.0"
},
"engineStrict": true,
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export * from "./env";
export * from "./env.js";
139 changes: 73 additions & 66 deletions packages/backend/src/handler/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,79 +2,86 @@ import type { Middleware } from "polka";
import { ZodError } from "zod";
import * as Sentry from "@sentry/node";
import { generateImage } from "~/logic/generate-image";
import { logger, sentryTraceFromHeader } from "~/utils";
import { convertOpenTelemetryHeaders, getUserIP, sentryTraceFromHeader } from "~/utils";
import { optionSchema, OptionSchema } from "~/schema/options";

export const coreHandler: Middleware = async (req, res) => {
/* c8 ignore start */
const abortController = new AbortController();
const sentrySpan = Sentry.continueTrace(
{ sentryTrace: sentryTraceFromHeader(req.headers), baggage: req.headers["baggage"] },
(ctx) =>
Sentry.startTransaction(
{
name: `${req.method.toUpperCase()} ${req.path}`,
op: "http.server",
origin: "manual.http.node.tracingHandler",
...ctx,
metadata: {
...ctx.metadata,
request: req,
source: "url"
}
},
{ request: Sentry.extractRequestData(req) }
)
);

req.once("close", () => {
if (req.destroyed) {
abortController.abort("Request closed");
}
sentrySpan.setHttpStatus(res.statusCode);
sentrySpan.finish();
});
return Sentry.withIsolationScope(() =>
Sentry.continueTrace(
{
sentryTrace: sentryTraceFromHeader(req.headers),
baggage: req.headers["baggage"]
},
() => {
const userIPAddress = getUserIP(req.headers);
if (userIPAddress != null) {
Sentry.getIsolationScope().setUser({ ip_address: userIPAddress });
}
Sentry.getIsolationScope().setExtra("Headers", req.headers);

Sentry.getCurrentHub().getScope().setSpan(sentrySpan);
return Sentry.startSpan(
{
name: `${req.method.toUpperCase()} ${req.path}`,
op: "http.server",
attributes: {
source: "url",
"http.request.method": req.method,
"http.method": req.method,
"http.url": req.url,
"http.user_agent": req.headers["User-Agent"] || "unknown",
"http.host": req.headers["Host"],
"http.client_ip": userIPAddress,
...convertOpenTelemetryHeaders(req.headers, "request")
}
},
async (sentrySpan) => {
req.once("close", () => {
if (req.destroyed) {
abortController.abort("Request closed");
}
sentrySpan?.setStatus(Sentry.getSpanStatusFromHttpCode(res.statusCode));
});
/* c8 ignore end */

await logger.info("Incoming POST request", {
body: req.body ?? "",
headers: {
accept: req.headers.accept ?? "",
"content-type": req.headers["content-type"] ?? "",
origin: req.headers.origin ?? "",
referer: req.headers.referer ?? "",
"user-agent": req.headers["user-agent"] ?? ""
},
port: req.socket.remotePort ?? 0,
ipv: req.socket.remoteFamily ?? ""
});
/* c8 ignore end */
if (req.body === "" || !Object.keys(req.body).length) {
res
.writeHead(400, { "Content-Type": "application/json" })
.end(JSON.stringify({ message: "Body can't be empty!" }));
return;
}

if (req.body === "" || !Object.keys(req.body).length) {
res.writeHead(400, { "Content-Type": "application/json" }).end(JSON.stringify({ message: "Body can't be empty!" }));
return;
}
try {
const options = (await optionSchema.parseAsync(req.body)) as OptionSchema;
const { image, format, length } = await generateImage(options);

try {
const options = (await optionSchema.parseAsync(req.body)) as OptionSchema;
const { image, format, length } = await generateImage(options);

res
.writeHead(200, {
"Content-Type": `image/${format === "svg" ? "svg+xml" : format}`,
"Content-Length": length
})
.end(image);
} catch (err) {
if (err instanceof ZodError) {
res
.writeHead(400, { "Content-Type": "application/json" })
.end(JSON.stringify({ message: "Validation error", issues: err.issues.map((issue) => issue.message) }));
} else if (err instanceof Error) {
res.writeHead(500, { "Content-Type": "application/json" }).end(JSON.stringify({ message: err.message }));
} else {
res.writeHead(500, { "Content-Type": "application/json" }).end(JSON.stringify({ message: "Unknown error" }));
}
}
res
.writeHead(200, {
"Content-Type": `image/${format === "svg" ? "svg+xml" : format}`,
"Content-Length": length
})
.end(image);
} catch (err) {
if (err instanceof ZodError) {
res
.writeHead(400, { "Content-Type": "application/json" })
.end(
JSON.stringify({ message: "Validation error", issues: err.issues.map((issue) => issue.message) })
);
} else if (err instanceof Error) {
res
.writeHead(500, { "Content-Type": "application/json" })
.end(JSON.stringify({ message: err.message }));
} else {
res
.writeHead(500, { "Content-Type": "application/json" })
.end(JSON.stringify({ message: "Unknown error" }));
}
}
}
);
}
)
);
};
15 changes: 11 additions & 4 deletions packages/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,29 @@ import helmet from "helmet";
import { cors, bodyParser, errorHandler, notFoundHandler, rateLimiter } from "~/middleware/index.js";
import { logger } from "~/utils/index.js";
import { coreHandler } from "~/handler/core.js";
import { IS_PRODUCTION, IS_TEST, PORT } from "~/constants";
import { IS_PRODUCTION, IS_TEST, PORT } from "~/constants/index.js";

const MAX_AGE = 24 * 60; // 1 day
const CWD = dirname(fileURLToPath(import.meta.url));
const STATIC_PATH = resolve(CWD, "./views");

Sentry.init({
dsn: "",
integrations: [new Sentry.Integrations.Http({ tracing: true }), new Sentry.Integrations.Undici()],
dsn: process.env.SENTRY_DSN ?? "",
sampleRate: 1.0,
tracesSampleRate: 0.5
});

const app = polka({ onError: errorHandler, onNoMatch: notFoundHandler })
.use(
helmet() as Middleware,
helmet({
contentSecurityPolicy: {
directives: {
"script-src": ["'self'", "https:"],
"connect-src": ["'self'", "https:"]
}
},
crossOriginResourcePolicy: { policy: "same-site" }
}) as Middleware,
sirv(STATIC_PATH, {
dev: !IS_PRODUCTION,
etag: true,
Expand Down
7 changes: 4 additions & 3 deletions packages/backend/src/logic/generate-image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import flourite from "flourite";
import sharp from "sharp";
import * as shikiji from "shikiji";
import * as Sentry from "@sentry/node";
import { SvgRenderer } from "~/logic/svg-renderer";
import type { OptionSchema } from "~/schema/options";
import { SvgRenderer } from "~/logic/svg-renderer.js";
import type { OptionSchema } from "~/schema/options.js";
import { FONT_MAPPING } from "shared";

function guessLanguage(code: string, language: string): string {
Expand All @@ -26,7 +26,8 @@ export function generateImage({
return Sentry.startSpan({ name: "Generate Image", op: "logic.generate_image.generate_image" }, async () => {
const highlighter = await shikiji.getHighlighter({ themes: [theme] });
const resolvedTheme = highlighter.getTheme(theme);
const fontConfig = FONT_MAPPING[font];
const fontConfig: { fontFamily: string; lineHeightToFontSizeRatio: number; fontSize: number; fontWidth: number } =
FONT_MAPPING[font];

const svgRenderer = new SvgRenderer({
...fontConfig,
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/middleware/error-handler.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import console from "node:console";
import type { ErrorHandler } from "polka";
import * as Sentry from "@sentry/node";
import { IS_PRODUCTION } from "~/constants";
import { IS_PRODUCTION } from "~/constants/index.js";
import { logger } from "~/utils/index.js";

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/middleware/rate-limiter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Middleware } from "polka";
import { RateLimiterMemory } from "rate-limiter-flexible";
import { IS_TEST } from "~/constants";
import { IS_TEST } from "~/constants/index.js";
import { getIP } from "../utils/get-ip";

const rateLimiterMemory = new RateLimiterMemory({
Expand Down
12 changes: 6 additions & 6 deletions packages/backend/src/schema/options.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import z from "zod";
import { themeSchema } from "./theme";
import { languageSchema } from "./language";
import { imageFormatSchema } from "./image-format";
import { fontSchema } from "./font";
import { borderSchema } from "./border";
import { upscaleSchema } from "./upscale";
import { themeSchema } from "./theme.js";
import { languageSchema } from "./language.js";
import { imageFormatSchema } from "./image-format.js";
import { fontSchema } from "./font.js";
import { borderSchema } from "./border.js";
import { upscaleSchema } from "./upscale.js";

export const optionSchema = z.object({
code: z
Expand Down
48 changes: 48 additions & 0 deletions packages/backend/src/utils/sentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,51 @@ export function sentryTraceFromHeader(headers: Record<string, string | string[]

return undefined;
}

export const getUserIP = (headers: Record<string, string | string[] | undefined>): string | undefined => {
let ipAddress: string | undefined;
const possibleHeaders = [
"Forwarded",
"Forwarded-For",
"Client-IP",
"X-Forwarded",
"X-Forwarded-For",
"X-Client-IP",
"X-Real-IP",
"True-Client-IP"
];
for (const possibleHeader of possibleHeaders) {
const value = headers[possibleHeader];
if (value !== undefined && value !== "") {
if (Array.isArray(value)) {
ipAddress = value[0];
continue;
}

ipAddress = value;
}
}

return ipAddress;
};

export const convertOpenTelemetryHeaders = (
headers: Record<string, string | string[] | undefined>,
precedence: "request" | "response"
): Record<string, string[]> => {
const openTelemetryHeadersCollection: Record<string, string[]> = {};
for (const [key, value] of Object.entries(headers)) {
if (value == null) {
continue;
}

if (Array.isArray(value)) {
openTelemetryHeadersCollection[`http.${precedence}.header.${key.toLowerCase()}`] = value;
continue;
}

openTelemetryHeadersCollection[`http.${precedence}.header.${key.toLowerCase()}`] = [value];
}

return openTelemetryHeadersCollection;
};
Loading

0 comments on commit 1958536

Please sign in to comment.