simple strings server for the wentworth coding club / my personal use
1import { Database } from "bun:sqlite";
2
3function escapeHtml(str: string): string {
4 return str
5 .replace(/&/g, "&")
6 .replace(/</g, "<")
7 .replace(/>/g, ">")
8 .replace(/"/g, """)
9 .replace(/'/g, "'");
10}
11
12function homeHtml() {
13 return `<!DOCTYPE html>
14<html lang="en">
15<head>
16 <meta charset="UTF-8">
17 <meta name="viewport" content="width=device-width, initial-scale=1.0">
18 <title>strings</title>
19 <style>
20 * { box-sizing: border-box; }
21 body {
22 font-family: system-ui, -apple-system, sans-serif;
23 background: #0d1117;
24 color: #c9d1d9;
25 margin: 0;
26 padding: 2rem;
27 min-height: 100vh;
28 }
29 .container { max-width: 800px; margin: 0 auto; }
30 h1 { color: #58a6ff; margin-bottom: 0.5rem; }
31 .subtitle { color: #8b949e; margin-bottom: 2rem; }
32 pre {
33 background: #161b22;
34 border: 1px solid #30363d;
35 border-radius: 6px;
36 padding: 1rem;
37 overflow-x: auto;
38 }
39 code { color: #79c0ff; }
40 .endpoint { color: #7ee787; }
41 .comment { color: #8b949e; }
42 a { color: #58a6ff; }
43 .btn {
44 display: inline-block;
45 background: #238636;
46 color: #fff;
47 padding: 0.75rem 1.5rem;
48 border-radius: 6px;
49 text-decoration: none;
50 margin-bottom: 2rem;
51 }
52 .btn:hover { background: #2ea043; }
53 </style>
54</head>
55<body>
56 <div class="container">
57 <h1>strings</h1>
58 <p class="subtitle">minimal pastebin</p>
59
60 <a href="/new" class="btn">+ New Paste</a>
61
62 <h2>API</h2>
63 <pre><code><span class="comment"># Create a paste (basic auth required)</span>
64curl -u user:pass -X POST <span class="endpoint">https://strings.witcc.dev/api/paste</span> \\
65 -H "Content-Type: text/plain" \\
66 -H "X-Filename: example.py" \\
67 -d 'print("hello world")'
68
69<span class="comment"># With custom slug</span>
70curl -u user:pass -X POST <span class="endpoint">https://strings.witcc.dev/api/paste</span> \\
71 -H "Content-Type: application/json" \\
72 -d '{"content": "print(1)", "filename": "test.py", "slug": "my-snippet"}'
73
74<span class="comment"># Pipe a file</span>
75cat myfile.rs | curl -u user:pass -X POST <span class="endpoint">https://strings.witcc.dev/api/paste</span> \\
76 -H "X-Filename: myfile.rs" \\
77 --data-binary @-</code></pre>
78 </div>
79</body>
80</html>`;
81}
82
83function errorPage(message: string) {
84 const escaped = escapeHtml(message);
85 return `<!DOCTYPE html>
86<html lang="en">
87<head>
88 <meta charset="UTF-8">
89 <meta name="viewport" content="width=device-width, initial-scale=1.0">
90 <title>Error - strings</title>
91 <style>
92 body {
93 font-family: system-ui, -apple-system, sans-serif;
94 background: #0d1117;
95 color: #c9d1d9;
96 display: flex;
97 align-items: center;
98 justify-content: center;
99 min-height: 100vh;
100 margin: 0;
101 }
102 .error {
103 text-align: center;
104 }
105 h1 { color: #f85149; margin-bottom: 1rem; }
106 a { color: #58a6ff; }
107 </style>
108</head>
109<body>
110 <div class="error">
111 <h1>${escaped}</h1>
112 <a href="/">← back home</a>
113 </div>
114</body>
115</html>`;
116}
117
118function renderPaste(paste: Paste) {
119 const lang = paste.language || "plaintext";
120 const filename = paste.filename ? escapeHtml(paste.filename) : paste.id;
121 const title = `${filename} - strings`;
122 const content = escapeHtml(paste.content);
123
124 return `<!DOCTYPE html>
125<html lang="en">
126<head>
127 <meta charset="UTF-8">
128 <meta name="viewport" content="width=device-width, initial-scale=1.0">
129 <title>${title}</title>
130 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
131 <style>
132 * { box-sizing: border-box; margin: 0; padding: 0; }
133 body {
134 font-family: system-ui, -apple-system, sans-serif;
135 background: #0d1117;
136 color: #c9d1d9;
137 min-height: 100vh;
138 }
139 .header {
140 background: #161b22;
141 border-bottom: 1px solid #30363d;
142 padding: 1rem 2rem;
143 display: flex;
144 justify-content: space-between;
145 align-items: center;
146 }
147 .header a { color: #58a6ff; text-decoration: none; }
148 .header a:hover { text-decoration: underline; }
149 .filename { font-weight: 600; color: #c9d1d9; }
150 .meta { color: #8b949e; font-size: 0.875rem; }
151 .actions a {
152 color: #8b949e;
153 margin-left: 1rem;
154 font-size: 0.875rem;
155 }
156 .code-wrapper {
157 margin: 1rem;
158 border: 1px solid #30363d;
159 border-radius: 6px;
160 overflow: hidden;
161 }
162 pre {
163 margin: 0;
164 padding: 1rem;
165 overflow-x: auto;
166 background: #0d1117 !important;
167 }
168 code {
169 font-family: 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
170 font-size: 0.875rem;
171 line-height: 1.5;
172 }
173 .hljs { background: #0d1117 !important; }
174 </style>
175</head>
176<body>
177 <div class="header">
178 <div>
179 <a href="/">strings</a>
180 <span class="filename"> / ${filename}</span>
181 <span class="meta"> · ${lang}</span>
182 </div>
183 <div class="actions">
184 <a href="/${paste.id}/raw">raw</a>
185 <a href="/new">+ new</a>
186 </div>
187 </div>
188 <div class="code-wrapper">
189 <pre><code class="language-${lang}">${content}</code></pre>
190 </div>
191 <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
192 <script>hljs.highlightAll();</script>
193</body>
194</html>`;
195}
196
197function newPastePage(error?: string) {
198 const errorHtml = error ? `<div class="error">${escapeHtml(error)}</div>` : "";
199
200 return `<!DOCTYPE html>
201<html lang="en">
202<head>
203 <meta charset="UTF-8">
204 <meta name="viewport" content="width=device-width, initial-scale=1.0">
205 <title>New Paste - strings</title>
206 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.css">
207 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/theme/material-darker.min.css">
208 <style>
209 * { box-sizing: border-box; }
210 body {
211 font-family: system-ui, -apple-system, sans-serif;
212 background: #0d1117;
213 color: #c9d1d9;
214 margin: 0;
215 padding: 2rem;
216 min-height: 100vh;
217 }
218 .container { max-width: 1000px; margin: 0 auto; }
219 h1 { color: #58a6ff; margin-bottom: 1.5rem; }
220 h1 a { color: inherit; text-decoration: none; }
221 .error {
222 background: #3d1f1f;
223 border: 1px solid #f85149;
224 color: #f85149;
225 padding: 0.75rem 1rem;
226 border-radius: 6px;
227 margin-bottom: 1rem;
228 }
229 form { display: flex; flex-direction: column; gap: 1rem; }
230 label {
231 display: flex;
232 flex-direction: column;
233 gap: 0.5rem;
234 color: #8b949e;
235 font-size: 0.875rem;
236 }
237 input, select {
238 background: #0d1117;
239 border: 1px solid #30363d;
240 border-radius: 6px;
241 padding: 0.75rem;
242 color: #c9d1d9;
243 font-family: inherit;
244 font-size: 1rem;
245 }
246 input:focus, select:focus {
247 outline: none;
248 border-color: #58a6ff;
249 }
250 .row { display: flex; gap: 1rem; }
251 .row > label { flex: 1; }
252 button {
253 background: #238636;
254 color: #fff;
255 border: none;
256 padding: 0.75rem 1.5rem;
257 border-radius: 6px;
258 font-size: 1rem;
259 cursor: pointer;
260 align-self: flex-start;
261 }
262 button:hover { background: #2ea043; }
263 .hint { font-size: 0.75rem; color: #6e7681; margin-top: 0.25rem; }
264 .editor-wrapper {
265 border: 1px solid #30363d;
266 border-radius: 6px;
267 overflow: hidden;
268 }
269 .CodeMirror {
270 height: 400px;
271 font-size: 14px;
272 font-family: 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
273 }
274 .CodeMirror-gutters {
275 background: #161b22;
276 border-right: 1px solid #30363d;
277 }
278 #editor {
279 display: none;
280 }
281 </style>
282</head>
283<body>
284 <div class="container">
285 <h1><a href="/">strings</a> / new</h1>
286 ${errorHtml}
287 <form method="POST" action="/new">
288 <label>
289 Content
290 <div class="editor-wrapper">
291 <textarea name="content" id="editor"></textarea>
292 </div>
293 </label>
294 <div class="row">
295 <label>
296 Filename
297 <input type="text" name="filename" id="filename" placeholder="example.py">
298 <span class="hint">Used to detect language for syntax highlighting</span>
299 </label>
300 <label>
301 Custom slug (optional)
302 <input type="text" name="slug" placeholder="my-snippet" pattern="[a-zA-Z0-9_-]{1,64}">
303 <span class="hint">Leave empty for random ID</span>
304 </label>
305 </div>
306 <label>
307 Language
308 <select name="language" id="language">
309 <option value="">Auto-detect from filename</option>
310 <option value="plaintext">Plain Text</option>
311 <option value="javascript">JavaScript</option>
312 <option value="typescript">TypeScript</option>
313 <option value="python">Python</option>
314 <option value="ruby">Ruby</option>
315 <option value="rust">Rust</option>
316 <option value="go">Go</option>
317 <option value="java">Java</option>
318 <option value="c">C</option>
319 <option value="cpp">C++</option>
320 <option value="csharp">C#</option>
321 <option value="php">PHP</option>
322 <option value="swift">Swift</option>
323 <option value="kotlin">Kotlin</option>
324 <option value="bash">Bash / Shell</option>
325 <option value="sql">SQL</option>
326 <option value="html">HTML</option>
327 <option value="css">CSS</option>
328 <option value="json">JSON</option>
329 <option value="yaml">YAML</option>
330 <option value="toml">TOML</option>
331 <option value="xml">XML</option>
332 <option value="markdown">Markdown</option>
333 <option value="nix">Nix</option>
334 <option value="dockerfile">Dockerfile</option>
335 <option value="elixir">Elixir</option>
336 <option value="haskell">Haskell</option>
337 <option value="lua">Lua</option>
338 </select>
339 </label>
340 <button type="submit">Create Paste</button>
341 </form>
342 </div>
343
344 <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.js"></script>
345 <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/javascript/javascript.min.js"></script>
346 <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/python/python.min.js"></script>
347 <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/ruby/ruby.min.js"></script>
348 <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/rust/rust.min.js"></script>
349 <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/go/go.min.js"></script>
350 <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/clike/clike.min.js"></script>
351 <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/php/php.min.js"></script>
352 <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/swift/swift.min.js"></script>
353 <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/shell/shell.min.js"></script>
354 <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/sql/sql.min.js"></script>
355 <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/htmlmixed/htmlmixed.min.js"></script>
356 <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/css/css.min.js"></script>
357 <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/xml/xml.min.js"></script>
358 <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/yaml/yaml.min.js"></script>
359 <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/toml/toml.min.js"></script>
360 <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/markdown/markdown.min.js"></script>
361 <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/nix/nix.min.js"></script>
362 <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/dockerfile/dockerfile.min.js"></script>
363 <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/haskell/haskell.min.js"></script>
364 <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/lua/lua.min.js"></script>
365 <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/mllike/mllike.min.js"></script>
366 <script>
367 const langToMode = {
368 javascript: 'javascript',
369 typescript: 'text/typescript',
370 python: 'python',
371 ruby: 'ruby',
372 rust: 'rust',
373 go: 'go',
374 java: 'text/x-java',
375 c: 'text/x-csrc',
376 cpp: 'text/x-c++src',
377 csharp: 'text/x-csharp',
378 php: 'php',
379 swift: 'swift',
380 kotlin: 'text/x-kotlin',
381 bash: 'shell',
382 sql: 'sql',
383 html: 'htmlmixed',
384 css: 'css',
385 json: 'application/json',
386 yaml: 'yaml',
387 toml: 'toml',
388 xml: 'xml',
389 markdown: 'markdown',
390 nix: 'nix',
391 dockerfile: 'dockerfile',
392 elixir: 'mllike',
393 haskell: 'haskell',
394 lua: 'lua',
395 plaintext: 'text/plain',
396 };
397
398 const extToLang = {
399 js: 'javascript', ts: 'typescript', jsx: 'javascript', tsx: 'typescript',
400 py: 'python', rb: 'ruby', rs: 'rust', go: 'go', java: 'java',
401 c: 'c', cpp: 'cpp', h: 'c', hpp: 'cpp', cs: 'csharp',
402 php: 'php', swift: 'swift', kt: 'kotlin',
403 sh: 'bash', bash: 'bash', zsh: 'bash',
404 sql: 'sql', html: 'html', css: 'css', json: 'json',
405 yaml: 'yaml', yml: 'yaml', toml: 'toml', xml: 'xml',
406 md: 'markdown', nix: 'nix', ex: 'elixir', exs: 'elixir',
407 hs: 'haskell', lua: 'lua',
408 };
409
410 const editor = CodeMirror.fromTextArea(document.getElementById('editor'), {
411 theme: 'material-darker',
412 lineNumbers: true,
413 indentUnit: 2,
414 tabSize: 2,
415 indentWithTabs: false,
416 lineWrapping: true,
417 autofocus: true,
418 });
419
420 function updateMode() {
421 const lang = document.getElementById('language').value;
422 const filename = document.getElementById('filename').value;
423
424 let mode = 'text/plain';
425
426 if (lang && langToMode[lang]) {
427 mode = langToMode[lang];
428 } else if (filename) {
429 const ext = filename.split('.').pop()?.toLowerCase();
430 if (ext && extToLang[ext]) {
431 mode = langToMode[extToLang[ext]] || 'text/plain';
432 }
433 }
434
435 editor.setOption('mode', mode);
436 }
437
438 document.getElementById('language').addEventListener('change', updateMode);
439 document.getElementById('filename').addEventListener('input', updateMode);
440 </script>
441</body>
442</html>`;
443}
444
445type Paste = {
446 id: string;
447 content: string;
448 filename: string | null;
449 language: string | null;
450 created_at: number;
451};
452
453// Config from environment
454const PORT = parseInt(process.env.PORT || "3000");
455const DB_PATH = process.env.DB_PATH || "./strings.db";
456const USERNAME = process.env.AUTH_USERNAME || "admin";
457
458// Load auth password from file or env
459async function loadPassword(): Promise<string> {
460 if (process.env.AUTH_PASSWORD_FILE) {
461 try {
462 const file = Bun.file(process.env.AUTH_PASSWORD_FILE);
463 return (await file.text()).trim();
464 } catch (e) {
465 console.error("Failed to read AUTH_PASSWORD_FILE:", e);
466 process.exit(1);
467 }
468 }
469 return process.env.AUTH_PASSWORD || "changeme";
470}
471
472const PASSWORD = await loadPassword();
473
474// Initialize database
475const db = new Database(DB_PATH);
476db.run(`
477 CREATE TABLE IF NOT EXISTS pastes (
478 id TEXT PRIMARY KEY,
479 content TEXT NOT NULL,
480 filename TEXT,
481 language TEXT,
482 created_at INTEGER DEFAULT (unixepoch())
483 )
484`);
485
486// Generate random ID
487function generateId(length = 8): string {
488 const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789";
489 let result = "";
490 for (let i = 0; i < length; i++) {
491 result += chars[Math.floor(Math.random() * chars.length)];
492 }
493 return result;
494}
495
496// Validate custom slug
497function isValidSlug(slug: string): boolean {
498 return /^[a-zA-Z0-9_-]{1,64}$/.test(slug);
499}
500
501// Check if ID exists
502function idExists(id: string): boolean {
503 const row = db.query("SELECT 1 FROM pastes WHERE id = ?").get(id);
504 return row !== null;
505}
506
507// Infer language from filename
508function inferLanguage(filename?: string): string | undefined {
509 if (!filename) return undefined;
510 const ext = filename.split(".").pop()?.toLowerCase();
511 const langMap: Record<string, string> = {
512 js: "javascript",
513 ts: "typescript",
514 jsx: "javascript",
515 tsx: "typescript",
516 py: "python",
517 rb: "ruby",
518 rs: "rust",
519 go: "go",
520 java: "java",
521 c: "c",
522 cpp: "cpp",
523 h: "c",
524 hpp: "cpp",
525 cs: "csharp",
526 php: "php",
527 swift: "swift",
528 kt: "kotlin",
529 scala: "scala",
530 sh: "bash",
531 bash: "bash",
532 zsh: "bash",
533 fish: "fish",
534 ps1: "powershell",
535 sql: "sql",
536 html: "html",
537 css: "css",
538 scss: "scss",
539 sass: "sass",
540 less: "less",
541 json: "json",
542 yaml: "yaml",
543 yml: "yaml",
544 toml: "toml",
545 xml: "xml",
546 md: "markdown",
547 markdown: "markdown",
548 nix: "nix",
549 dockerfile: "dockerfile",
550 makefile: "makefile",
551 cmake: "cmake",
552 ex: "elixir",
553 exs: "elixir",
554 erl: "erlang",
555 hs: "haskell",
556 lua: "lua",
557 r: "r",
558 jl: "julia",
559 vim: "vim",
560 tf: "hcl",
561 };
562 return ext ? langMap[ext] : undefined;
563}
564
565// Basic auth helper
566function checkAuth(req: Request): boolean {
567 const authHeader = req.headers.get("Authorization");
568 if (!authHeader || !authHeader.startsWith("Basic ")) {
569 return false;
570 }
571
572 const base64Credentials = authHeader.slice(6);
573 const credentials = atob(base64Credentials);
574 const [username, password] = credentials.split(":");
575
576 return username === USERNAME && password === PASSWORD;
577}
578
579function unauthorizedResponse(): Response {
580 return new Response("Unauthorized", {
581 status: 401,
582 headers: {
583 "WWW-Authenticate": 'Basic realm="Secure Area"',
584 },
585 });
586}
587
588function getOrCreateId(customSlug?: string): string {
589 if (customSlug) {
590 if (!isValidSlug(customSlug)) {
591 throw new Error("Invalid slug. Use 1-64 alphanumeric characters, hyphens, or underscores.");
592 }
593 if (idExists(customSlug)) {
594 throw new Error("Slug already taken");
595 }
596 return customSlug;
597 }
598
599 let id: string;
600 do {
601 id = generateId();
602 } while (idExists(id));
603 return id;
604}
605
606// Router
607async function handleRequest(req: Request): Promise<Response> {
608 const url = new URL(req.url);
609 const path = url.pathname;
610 const method = req.method;
611
612 // Create paste - API
613 if (method === "POST" && path === "/api/paste") {
614 if (!checkAuth(req)) return unauthorizedResponse();
615
616 const contentType = req.headers.get("Content-Type") || "";
617
618 let content: string;
619 let filename: string | undefined;
620 let language: string | undefined;
621 let customSlug: string | undefined;
622
623 if (contentType.includes("application/json")) {
624 const body = await req.json();
625 content = body.content;
626 filename = body.filename;
627 language = body.language;
628 customSlug = body.slug;
629 } else {
630 content = await req.text();
631 filename = req.headers.get("X-Filename") || undefined;
632 language = req.headers.get("X-Language") || undefined;
633 customSlug = req.headers.get("X-Slug") || undefined;
634 }
635
636 if (!content) {
637 return Response.json({ error: "Content is required" }, { status: 400 });
638 }
639
640 let id: string;
641 try {
642 id = getOrCreateId(customSlug);
643 } catch (e: any) {
644 const status = e.message.includes("taken") ? 409 : 400;
645 return Response.json({ error: e.message }, { status });
646 }
647
648 if (!language && filename) {
649 language = inferLanguage(filename);
650 }
651
652 db.run(
653 "INSERT INTO pastes (id, content, filename, language) VALUES (?, ?, ?, ?)",
654 [id, content, filename || null, language || null]
655 );
656
657 const baseUrl = process.env.BASE_URL || `http://localhost:${PORT}`;
658
659 return Response.json({
660 id,
661 url: `${baseUrl}/${id}`,
662 raw: `${baseUrl}/${id}/raw`,
663 });
664 }
665
666 // Create paste - Form submission
667 if (method === "POST" && path === "/new") {
668 if (!checkAuth(req)) return unauthorizedResponse();
669
670 const form = await req.formData();
671 const content = form.get("content") as string;
672 const filename = (form.get("filename") as string) || undefined;
673 const language = (form.get("language") as string) || undefined;
674 const customSlug = (form.get("slug") as string) || undefined;
675
676 if (!content) {
677 return new Response(newPastePage("Content is required"), {
678 status: 400,
679 headers: { "Content-Type": "text/html" },
680 });
681 }
682
683 let id: string;
684 try {
685 id = getOrCreateId(customSlug);
686 } catch (e: any) {
687 const status = e.message.includes("taken") ? 409 : 400;
688 return new Response(newPastePage(e.message), {
689 status,
690 headers: { "Content-Type": "text/html" },
691 });
692 }
693
694 const inferredLang = language || inferLanguage(filename || undefined);
695
696 db.run(
697 "INSERT INTO pastes (id, content, filename, language) VALUES (?, ?, ?, ?)",
698 [id, content, filename || null, inferredLang || null]
699 );
700
701 return Response.redirect(`${url.origin}/${id}`, 302);
702 }
703
704 // New paste form
705 if (method === "GET" && path === "/new") {
706 if (!checkAuth(req)) return unauthorizedResponse();
707
708 return new Response(newPastePage(), {
709 headers: { "Content-Type": "text/html" },
710 });
711 }
712
713 // Delete paste
714 if (method === "DELETE" && path.match(/^\/[^/]+$/)) {
715 if (!checkAuth(req)) return unauthorizedResponse();
716
717 const id = path.slice(1);
718 const result = db.run("DELETE FROM pastes WHERE id = ?", [id]);
719
720 if (result.changes === 0) {
721 return Response.json({ error: "Paste not found" }, { status: 404 });
722 }
723
724 return Response.json({ deleted: true });
725 }
726
727 // Get raw paste
728 if (method === "GET" && path.match(/^\/[^/]+\/raw$/)) {
729 const id = path.slice(1, -4);
730 const paste = db.query("SELECT * FROM pastes WHERE id = ?").get(id) as Paste | null;
731
732 if (!paste) {
733 return new Response("Paste not found", { status: 404 });
734 }
735
736 return new Response(paste.content, {
737 headers: { "Content-Type": "text/plain; charset=utf-8" },
738 });
739 }
740
741 // Get paste (HTML view)
742 if (method === "GET" && path.match(/^\/[^/]+$/)) {
743 const id = path.slice(1);
744
745 if (id === "new" || id === "api") {
746 return new Response(errorPage("Not found"), {
747 status: 404,
748 headers: { "Content-Type": "text/html" },
749 });
750 }
751
752 const paste = db.query("SELECT * FROM pastes WHERE id = ?").get(id) as Paste | null;
753
754 if (!paste) {
755 return new Response(errorPage("Paste not found"), {
756 status: 404,
757 headers: { "Content-Type": "text/html" },
758 });
759 }
760
761 return new Response(renderPaste(paste), {
762 headers: { "Content-Type": "text/html" },
763 });
764 }
765
766 // Home page
767 if (method === "GET" && path === "/") {
768 return new Response(homeHtml(), {
769 headers: { "Content-Type": "text/html" },
770 });
771 }
772
773 return new Response(errorPage("Not found"), {
774 status: 404,
775 headers: { "Content-Type": "text/html" },
776 });
777}
778
779console.log(`strings running on http://localhost:${PORT}`);
780
781export default {
782 port: PORT,
783 fetch: handleRequest,
784};