Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • dev
  • main
2 results

Target

Select target project
  • creations/profilePage
1 result
Select Git revision
  • dev
  • main
2 results
Show changes

Commits on Source 2

......@@ -3,3 +3,4 @@ bun.lock
.env
logs/
.vscode/
robots.txt
export const environment: Environment = {
import { resolve } from "node:path";
import { logger } from "@creations.works/logger";
const environment: Environment = {
port: Number.parseInt(process.env.PORT || "8080", 10),
host: process.env.HOST || "0.0.0.0",
development:
process.env.NODE_ENV === "development" || process.argv.includes("--dev"),
};
export const redisTtl: number = process.env.REDIS_TTL
const redisTtl: number = process.env.REDIS_TTL
? Number.parseInt(process.env.REDIS_TTL, 10)
: 60 * 60 * 1; // 1 hour
export const lanyardConfig: LanyardConfig = {
const lanyardConfig: LanyardConfig = {
userId: process.env.LANYARD_USER_ID || "",
instance: process.env.LANYARD_INSTANCE || "https://api.lanyard.rest",
instance: process.env.LANYARD_INSTANCE || "",
};
export const reviewDb = {
const reviewDb = {
enabled: process.env.REVIEW_DB === "true" || process.env.REVIEW_DB === "1",
url: "https://manti.vendicated.dev/api/reviewdb",
};
export const badgeApi: string | null = process.env.BADGE_API_URL || null;
export const steamGridDbKey: string | undefined =
process.env.STEAMGRIDDB_API_KEY;
const badgeApi: string | null = process.env.BADGE_API_URL || null;
const steamGridDbKey: string | undefined = process.env.STEAMGRIDDB_API_KEY;
export const plausibleScript: string | null =
const plausibleScript: string | null =
process.env.PLAUSIBLE_SCRIPT_HTML?.trim() || null;
const robotstxtPath: string | null = process.env.ROBOTS_FILE
? resolve(process.env.ROBOTS_FILE)
: null;
function verifyRequiredVariables(): void {
const requiredVariables = [
"HOST",
"PORT",
"LANYARD_USER_ID",
"LANYARD_INSTANCE",
];
let hasError = false;
for (const key of requiredVariables) {
const value = process.env[key];
if (value === undefined || value.trim() === "") {
logger.error(`Missing or empty environment variable: ${key}`);
hasError = true;
}
}
if (hasError) {
process.exit(1);
}
}
export {
environment,
lanyardConfig,
redisTtl,
reviewDb,
badgeApi,
steamGridDbKey,
plausibleScript,
robotstxtPath,
verifyRequiredVariables,
};
......@@ -884,6 +884,13 @@ ul {
flex-wrap: wrap;
}
.review-header-inner {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5rem;
}
.review-username {
font-weight: 600;
color: var(--text-color);
......@@ -907,6 +914,23 @@ ul {
flex-wrap: wrap;
}
.emoji {
width: 20px;
height: 20px;
vertical-align: middle;
margin: 0 2px;
display: inline-block;
transition: transform 0.3s ease;
}
.emoji:hover {
transform: scale(1.2);
}
.review-content img.emoji {
vertical-align: middle;
}
@media (max-width: 600px) {
.reviews {
max-width: 100%;
......@@ -965,4 +989,9 @@ ul {
.review-badges {
justify-content: center;
}
.emoji {
width: 16px;
height: 16px;
}
}
......@@ -142,7 +142,7 @@ async function populateReviews(userId) {
const data = await res.json();
if (!data.success || !Array.isArray(data.reviews)) {
if (page === 1) reviewSection.classList.add("hidden");
if (currentReviewOffset === 0) reviewSection.classList.add("hidden");
isLoadingReviews = false;
return;
}
......@@ -152,7 +152,14 @@ async function populateReviews(userId) {
const sender = review.sender;
const username = sender.username;
const avatar = sender.profilePhoto;
const comment = review.comment;
let comment = review.comment;
comment = comment.replace(
/<(a?):\w+:(\d+)>/g,
(_, animated, id) =>
`<img src="https://cdn.discordapp.com/emojis/${id}.${animated ? "gif" : "webp"}" class="emoji" alt="emoji" />`,
);
const timestamp = review.timestamp
? new Date(review.timestamp * 1000).toLocaleString(undefined, {
hour12: false,
......@@ -176,10 +183,12 @@ async function populateReviews(userId) {
<img class="review-avatar" src="${avatar}" alt="${username}'s avatar"/>
<div class="review-body">
<div class="review-header">
<div class="review-header-inner">
<span class="review-username">${username}</span>
<span class="review-badges">${badges}</span>
</div>
<span class="review-timestamp">${timestamp}</span>
</div>
<div class="review-badges">${badges}</div>
<div class="review-content">${comment}</div>
</div>
</li>
......@@ -193,7 +202,6 @@ async function populateReviews(userId) {
reviewSection.classList.remove("hidden");
hasMoreReviews = data.hasNextPage;
currentReviewPage = page;
isLoadingReviews = false;
} catch (err) {
console.error("Failed to fetch reviews", err);
......
import { serverHandler } from "@/server";
import { verifyRequiredVariables } from "@config/environment";
import { logger } from "@creations.works/logger";
async function main(): Promise<void> {
verifyRequiredVariables();
serverHandler.initialize();
}
......
import { resolve } from "node:path";
import { environment } from "@config/environment";
import { environment, robotstxtPath } from "@config/environment";
import { logger } from "@creations.works/logger";
import {
type BunFile,
......@@ -65,10 +65,15 @@ class ServerHandler {
}
}
private async serveStaticFile(pathname: string): Promise<Response> {
try {
private async serveStaticFile(
request: ExtendedRequest,
pathname: string,
ip: string,
): Promise<Response> {
let filePath: string;
let response: Response;
try {
if (pathname === "/favicon.ico") {
filePath = resolve("public", "assets", "favicon.ico");
} else {
......@@ -81,16 +86,37 @@ class ServerHandler {
const fileContent: ArrayBuffer = await file.arrayBuffer();
const contentType: string = file.type || "application/octet-stream";
return new Response(fileContent, {
response = new Response(fileContent, {
headers: { "Content-Type": contentType },
});
}
} else {
logger.warn(`File not found: ${filePath}`);
return new Response("Not Found", { status: 404 });
response = new Response("Not Found", { status: 404 });
}
} catch (error) {
logger.error([`Error serving static file: ${pathname}`, error as Error]);
return new Response("Internal Server Error", { status: 500 });
response = new Response("Internal Server Error", { status: 500 });
}
this.logRequest(request, response, ip);
return response;
}
private logRequest(
request: ExtendedRequest,
response: Response,
ip: string | undefined,
): void {
logger.custom(
`[${request.method}]`,
`(${response.status})`,
[
request.url,
`${(performance.now() - request.startPerf).toFixed(2)}ms`,
ip || "unknown",
],
"90",
);
}
private async handleRequest(
......@@ -100,16 +126,52 @@ class ServerHandler {
const extendedRequest: ExtendedRequest = request as ExtendedRequest;
extendedRequest.startPerf = performance.now();
const headers = request.headers;
let ip = server.requestIP(request)?.address;
let response: Response;
if (!ip || ip.startsWith("172.") || ip === "127.0.0.1") {
ip =
headers.get("CF-Connecting-IP")?.trim() ||
headers.get("X-Real-IP")?.trim() ||
headers.get("X-Forwarded-For")?.split(",")[0].trim() ||
"unknown";
}
const pathname: string = new URL(request.url).pathname;
if (pathname === "/robots.txt" && robotstxtPath) {
try {
const file: BunFile = Bun.file(robotstxtPath);
if (await file.exists()) {
const fileContent: ArrayBuffer = await file.arrayBuffer();
const contentType: string = file.type || "text/plain";
response = new Response(fileContent, {
headers: { "Content-Type": contentType },
});
} else {
logger.warn(`File not found: ${robotstxtPath}`);
response = new Response("Not Found", { status: 404 });
}
} catch (error) {
logger.error([
`Error serving robots.txt: ${robotstxtPath}`,
error as Error,
]);
response = new Response("Internal Server Error", { status: 500 });
}
this.logRequest(extendedRequest, response, ip);
return response;
}
if (pathname.startsWith("/public") || pathname === "/favicon.ico") {
return await this.serveStaticFile(pathname);
return await this.serveStaticFile(extendedRequest, pathname, ip);
}
const match: MatchedRoute | null = this.router.match(request);
let requestBody: unknown = {};
let response: Response;
let logRequest = true;
if (match) {
const { filePath, params, query } = match;
......@@ -121,8 +183,6 @@ class ServerHandler {
? contentType.split(";")[0].trim()
: null;
logRequest = routeModule.routeDef.log !== false;
if (
routeModule.routeDef.needsBody === "json" &&
actualContentType === "application/json"
......@@ -231,30 +291,6 @@ class ServerHandler {
);
}
if (logRequest) {
const headers = request.headers;
let ip = server.requestIP(request)?.address;
if (!ip || ip.startsWith("172.") || ip === "127.0.0.1") {
ip =
headers.get("CF-Connecting-IP")?.trim() ||
headers.get("X-Real-IP")?.trim() ||
headers.get("X-Forwarded-For")?.split(",")[0].trim() ||
"unknown";
}
logger.custom(
`[${request.method}]`,
`(${response.status})`,
[
request.url,
`${(performance.now() - extendedRequest.startPerf).toFixed(2)}ms`,
ip || "unknown",
],
"90",
);
}
return response;
}
}
......