Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: resolve build stuff #139

Merged
merged 1 commit into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading