Skip to content

Commit d8fcdde

Browse files
committed
refactor: native h3 routing params
1 parent d43f803 commit d8fcdde

12 files changed

Lines changed: 649 additions & 809 deletions

File tree

nitro.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export default defineNitroConfig({
8181
compatibility_date: "2024-12-18",
8282
vars: {
8383
XYRA_STORAGE_DRIVER: "${XYRA_STORAGE_DRIVER}",
84+
XYRA_CORS_ORIGINS: "${XYRA_CORS_ORIGINS}",
8485
},
8586
kv_namespaces: kvNamespaces,
8687
},

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
"prepare": "nitro prepare"
99
},
1010
"devDependencies": {
11-
"@types/node": "^25.0.10",
11+
"@types/node": "^25.3.2",
1212
"nitropack": "^2.13.1",
1313
"typescript": "^5.9.3",
14-
"wrangler": "^4.60.0"
14+
"wrangler": "^4.69.0"
1515
},
1616
"dependencies": {
1717
"zod": "^4.3.6"

pnpm-lock.yaml

Lines changed: 505 additions & 733 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/middleware/05.cors.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
const ALLOWED_ORIGINS = (process.env.XYRA_CORS_ORIGINS ?? "*")
2+
.split(",")
3+
.map((o) => o.trim())
4+
.filter(Boolean);
5+
6+
const CORS_HEADERS = {
7+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
8+
"Access-Control-Allow-Headers": "Content-Type",
9+
"Access-Control-Max-Age": "86400",
10+
};
11+
12+
export default defineEventHandler((event) => {
13+
const origin = getRequestHeader(event, "origin");
14+
15+
let allowedOrigin: string | undefined;
16+
17+
if (ALLOWED_ORIGINS.includes("*")) {
18+
allowedOrigin = "*";
19+
} else if (origin && ALLOWED_ORIGINS.includes(origin)) {
20+
allowedOrigin = origin;
21+
}
22+
23+
if (!allowedOrigin) return;
24+
25+
setResponseHeader(event, "Access-Control-Allow-Origin", allowedOrigin);
26+
27+
for (const [key, value] of Object.entries(CORS_HEADERS)) {
28+
setResponseHeader(event, key, value);
29+
}
30+
31+
if (event.method?.toUpperCase() === "OPTIONS") {
32+
setResponseStatus(event, 204);
33+
return "";
34+
}
35+
});

server/middleware/10.rate-limit.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
type RateLimitRecord = { count: number; resetAt: number };
22

33
export default defineEventHandler(async (event) => {
4-
if (event.node.req.method?.toUpperCase() !== "POST") {
5-
return;
6-
}
4+
if (event.method !== "POST") return;
75

8-
if (!getRequestURL(event).pathname.startsWith("/api/pastes")) {
9-
return;
10-
}
6+
const url = getRequestURL(event);
7+
if (!url.pathname.startsWith("/api/pastes")) return;
118

129
const { windowMs, maxRequests } = useRuntimeConfig(event).paste.rateLimit;
10+
1311
const storage = useStorage("ratelimits");
14-
const ip = getRequestIP(event, { xForwardedFor: true });
12+
13+
const ip = getRequestIP(event, { xForwardedFor: true }) ?? "unknown";
1514
const key = `pastes:${ip}`;
15+
1616
const now = Date.now();
1717

18-
const current: RateLimitRecord = (await storage.getItem<RateLimitRecord>(key)) ?? {
18+
const current: RateLimitRecord = (await storage.getItem<RateLimitRecord>(
19+
key,
20+
)) ?? {
1921
count: 0,
2022
resetAt: now + windowMs,
2123
};
@@ -27,12 +29,16 @@ export default defineEventHandler(async (event) => {
2729

2830
if (current.count >= maxRequests) {
2931
const retryAfter = Math.ceil((current.resetAt - now) / 1000);
32+
33+
setResponseHeader(event, "Retry-After", retryAfter);
34+
3035
throw createError({
3136
statusCode: 429,
3237
statusMessage: `Rate limit exceeded. Try again in ${retryAfter}s.`,
3338
});
3439
}
3540

3641
current.count += 1;
42+
3743
await storage.setItem(key, current);
3844
});
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { getPasteOrThrow } from "~/services/paste-service";
22

3-
export default eventHandler((event) =>
4-
getPasteOrThrow(event.context.params?.id),
3+
export default defineEventHandler((event) =>
4+
getPasteOrThrow(getRouterParam(event, "id")!),
55
);
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { getPasteOrThrow } from "~/services/paste-service";
22

3-
export default eventHandler(async (event) => {
4-
const paste = await getPasteOrThrow(event.context.params?.id);
3+
export default defineEventHandler(async (event) => {
4+
const id = getRouterParam(event, "id");
5+
const paste = await getPasteOrThrow(id);
56
setResponseHeader(event, "Content-Type", "text/plain; charset=utf-8");
67
setResponseHeader(
78
event,
89
"Content-Disposition",
910
`inline; filename="xyra-paste-${paste.id}.log"`,
1011
);
12+
1113
return paste.content;
1214
});

server/routes/api/pastes/index.post.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { createPaste } from "~/services/paste-service";
22
import { createPasteInputSchema, MAX_CONTENT_BYTES } from "~/schemas/paste";
3-
export default eventHandler(async (event) => {
3+
4+
export default defineEventHandler(async (event) => {
45
const body = await readBody(event);
6+
57
const parsed = createPasteInputSchema.safeParse(body);
68
if (!parsed.success) {
79
throw createError({
@@ -12,6 +14,7 @@ export default eventHandler(async (event) => {
1214
}
1315

1416
const byteSize = new TextEncoder().encode(parsed.data.content).length;
17+
1518
if (byteSize > MAX_CONTENT_BYTES) {
1619
throw createError({
1720
statusCode: 413,
@@ -21,14 +24,17 @@ export default eventHandler(async (event) => {
2124

2225
const paste = await createPaste(parsed.data, {
2326
ip: getRequestIP(event, { xForwardedFor: true }),
24-
userAgent: event.node.req.headers["user-agent"],
27+
userAgent: getRequestHeader(event, "user-agent"),
2528
});
2629

30+
const url = getRequestURL(event);
31+
2732
setResponseStatus(event, 201, "Created");
33+
2834
return {
2935
id: paste.id,
30-
url: `${getRequestURL(event).origin}/p/${paste.id}`,
31-
rawUrl: `${getRequestURL(event).origin}/api/pastes/${paste.id}/raw`,
36+
url: `${url.origin}/p/${paste.id}`,
37+
rawUrl: `${url.origin}/api/pastes/${paste.id}/raw`,
3238
expiresAt: paste.expiresAt,
3339
createdAt: paste.createdAt,
3440
};

server/routes/index.get.ts

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,6 @@ export default eventHandler(async () => {
108108
(function () {
109109
var form = document.getElementById("paste-form");
110110
var statusEl = document.getElementById("status");
111-
var keyInput = document.getElementById("api-key");
112111
var recentList = document.getElementById("recent-list");
113112
114113
if (!form || !statusEl) {
@@ -142,12 +141,6 @@ export default eventHandler(async () => {
142141
statusEl.className = "small mt-1 text-" + type;
143142
};
144143
145-
var requireKey = form.dataset && form.dataset.requireKey === "true";
146-
var defaultKey = (form.dataset && form.dataset.defaultKey) || "";
147-
if (defaultKey && keyInput && !keyInput.value) {
148-
keyInput.value = defaultKey;
149-
}
150-
151144
form.addEventListener("submit", async function (event) {
152145
event.preventDefault();
153146
setStatus("");
@@ -182,15 +175,6 @@ export default eventHandler(async () => {
182175
"Content-Type": "application/json",
183176
};
184177
185-
if (requireKey) {
186-
var providedKey = keyInput && keyInput.value ? keyInput.value.trim() : "";
187-
if (!providedKey) {
188-
setStatus("API key is required for this server.", "warning");
189-
return;
190-
}
191-
headers["X-API-Key"] = providedKey;
192-
}
193-
194178
var response = await fetch("/api/pastes", {
195179
method: "POST",
196180
headers: headers,

server/routes/p/[id].get.ts

Lines changed: 74 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,29 @@
11
import { getPasteOrThrow } from "~/services/paste-service";
22
import { escapeHtml, renderLayout } from "~/templates/layout";
33

4-
export default eventHandler(async (event) => {
5-
const paste = await getPasteOrThrow(event.context.params?.id);
4+
export default defineEventHandler(async (event) => {
5+
const id = getRouterParam(event, "id");
6+
7+
if (!id) {
8+
throw createError({
9+
statusCode: 400,
10+
statusMessage: "Missing paste id",
11+
});
12+
}
13+
14+
const paste = await getPasteOrThrow(id);
615
const host = getRequestHost(event, { xForwardedHost: true });
716

817
const metadataList = `
918
<ul class="list-group list-group-flush">
1019
<li class="list-group-item bg-transparent text-light border-secondary">
1120
<strong>ID:</strong> ${paste.id}
1221
</li>
13-
${paste.source ? `<li class="list-group-item bg-transparent text-light border-secondary"><strong>Source:</strong> ${escapeHtml(paste.source)}</li>` : ""}
22+
${
23+
paste.source
24+
? `<li class="list-group-item bg-transparent text-light border-secondary"><strong>Source:</strong> ${escapeHtml(paste.source)}</li>`
25+
: ""
26+
}
1427
<li class="list-group-item bg-transparent text-light border-secondary">
1528
<strong>Created:</strong> ${formatDate(paste.createdAt)}
1629
</li>
@@ -24,34 +37,47 @@ export default eventHandler(async (event) => {
2437
<div class="mb-3">
2538
<a class="btn btn-link text-muted text-decoration-none" href="/">← Back to home</a>
2639
</div>
40+
2741
<div class="card shadow-sm p-4 mb-4">
2842
<div class="d-flex justify-content-between flex-wrap gap-2 align-items-start">
2943
<div>
3044
<h1 class="h4 mb-2">${escapeHtml(paste.title)}</h1>
3145
<p class="text-muted mb-0">Share this link with support to review the logs.</p>
3246
</div>
47+
3348
<div class="d-flex gap-2">
3449
<a class="btn btn-outline-light btn-sm" href="/api/pastes/${paste.id}/raw" target="_blank" rel="noopener">Raw</a>
35-
<button class="btn btn-outline-info btn-sm" id="copy-link" data-url="${escapeHtml(`https://${host}/p/${paste.id}`)}">Copy link</button>
50+
<button
51+
class="btn btn-outline-info btn-sm"
52+
id="copy-link"
53+
data-url="${escapeHtml(`https://${host}/p/${paste.id}`)}"
54+
>
55+
Copy link
56+
</button>
3657
</div>
3758
</div>
3859
</div>
60+
3961
<div class="row g-4">
4062
<div class="col-12 col-lg-4">
4163
<div class="card shadow-sm p-4">
4264
<h2 class="h6">Details</h2>
4365
${metadataList}
4466
</div>
4567
</div>
68+
4669
<div class="col-12 col-lg-8">
4770
<div class="card shadow-sm p-4">
4871
<div class="d-flex justify-content-between align-items-center mb-3">
4972
<h2 class="h6 mb-0">Log contents</h2>
5073
<button class="btn btn-sm btn-secondary" id="copy-log">Copy log</button>
5174
</div>
52-
<div class="log-viewer" id="log" data-raw="${escapeHtml(
53-
paste.content,
54-
)}">
75+
76+
<div
77+
class="log-viewer"
78+
id="log"
79+
data-raw="${escapeHtml(paste.content)}"
80+
>
5581
${renderLogLines(paste.content)}
5682
</div>
5783
</div>
@@ -60,37 +86,40 @@ export default eventHandler(async (event) => {
6086
`;
6187

6288
const inlineScript = String.raw`
63-
(() => {
64-
const copyLinkBtn = document.getElementById("copy-link");
65-
const copyLogBtn = document.getElementById("copy-log");
66-
67-
copyLinkBtn?.addEventListener("click", async () => {
68-
const url = copyLinkBtn.dataset.url;
69-
if (!url) return;
70-
try {
71-
await navigator.clipboard.writeText(url);
72-
copyLinkBtn.textContent = "Copied!";
73-
setTimeout(() => (copyLinkBtn.textContent = "Copy link"), 2000);
74-
} catch {
75-
copyLinkBtn.textContent = "Failed";
76-
setTimeout(() => (copyLinkBtn.textContent = "Copy link"), 2000);
77-
}
78-
});
79-
80-
copyLogBtn?.addEventListener("click", async () => {
81-
const logEl = document.getElementById("log");
82-
const rawLog = logEl?.getAttribute("data-raw") ?? logEl?.textContent ?? "";
83-
try {
84-
await navigator.clipboard.writeText(rawLog);
85-
copyLogBtn.textContent = "Copied!";
86-
setTimeout(() => (copyLogBtn.textContent = "Copy log"), 2000);
87-
} catch {
88-
copyLogBtn.textContent = "Failed";
89-
setTimeout(() => (copyLogBtn.textContent = "Copy log"), 2000);
90-
}
91-
});
92-
})();
93-
`;
89+
(() => {
90+
const copyLinkBtn = document.getElementById("copy-link");
91+
const copyLogBtn = document.getElementById("copy-log");
92+
93+
copyLinkBtn?.addEventListener("click", async () => {
94+
const url = copyLinkBtn.dataset.url;
95+
if (!url) return;
96+
97+
try {
98+
await navigator.clipboard.writeText(url);
99+
copyLinkBtn.textContent = "Copied!";
100+
setTimeout(() => (copyLinkBtn.textContent = "Copy link"), 2000);
101+
} catch {
102+
copyLinkBtn.textContent = "Failed";
103+
setTimeout(() => (copyLinkBtn.textContent = "Copy link"), 2000);
104+
}
105+
});
106+
107+
copyLogBtn?.addEventListener("click", async () => {
108+
const logEl = document.getElementById("log");
109+
const rawLog =
110+
logEl?.getAttribute("data-raw") ?? logEl?.textContent ?? "";
111+
112+
try {
113+
await navigator.clipboard.writeText(rawLog);
114+
copyLogBtn.textContent = "Copied!";
115+
setTimeout(() => (copyLogBtn.textContent = "Copy log"), 2000);
116+
} catch {
117+
copyLogBtn.textContent = "Failed";
118+
setTimeout(() => (copyLogBtn.textContent = "Copy log"), 2000);
119+
}
120+
});
121+
})();
122+
`;
94123

95124
return renderLayout({
96125
title: `${paste.title} · Xyra Paste`,
@@ -109,19 +138,23 @@ const formatDate = (timestamp: number) =>
109138

110139
const formatExpiry = (expiresAt: number | null) => {
111140
if (!expiresAt) return "Never";
141+
112142
const diffMs = expiresAt - Date.now();
113143
if (diffMs <= 0) return "Expired";
144+
114145
return new Date(expiresAt).toLocaleString();
115146
};
116147

117148
const renderLogLines = (content: string) => {
118149
const normalized = content.replace(/\r\n/g, "\n").split("\n");
150+
119151
return normalized
120152
.map((line, index) => {
121153
const safeLine = escapeHtml(line) || "&nbsp;";
122-
return `<div class="log-line"><span class="log-line-number">${
123-
index + 1
124-
}</span><span class="log-line-text">${safeLine}</span></div>`;
154+
return `<div class="log-line">
155+
<span class="log-line-number">${index + 1}</span>
156+
<span class="log-line-text">${safeLine}</span>
157+
</div>`;
125158
})
126159
.join("");
127160
};

0 commit comments

Comments
 (0)