simple strings server for the wentworth coding club / my personal use
at main 784 lines 24 kB view raw
1import { Database } from "bun:sqlite"; 2 3function escapeHtml(str: string): string { 4 return str 5 .replace(/&/g, "&amp;") 6 .replace(/</g, "&lt;") 7 .replace(/>/g, "&gt;") 8 .replace(/"/g, "&quot;") 9 .replace(/'/g, "&#039;"); 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};