Files
downloadwf/src/auth.ts
T
2026-08-15 10:46:10 +02:00

112 lines
4.6 KiB
TypeScript

import { makeAuthToken, parseAuthToken } from "./security";
export const AUTH_COOKIE = "fh_auth";
export const AUTH_TTL_MS = 30 * 60 * 1000;
export function parseCookie(header: string | null, name: string): string | null {
if (!header) return null;
for (const part of header.split(";")) {
const idx = part.indexOf("=");
if (idx === -1) continue;
if (part.slice(0, idx).trim() === name) return part.slice(idx + 1).trim();
}
return null;
}
export function hasValidAuth(req: Request, secret: string, key: string): boolean {
const token = parseCookie(req.headers.get("cookie"), AUTH_COOKIE);
if (!token) return false;
const parsed = parseAuthToken(token, secret);
return parsed !== null && parsed.key === key;
}
export function authCookieValue(key: string, secret: string): string {
const token = makeAuthToken(key, secret, AUTH_TTL_MS);
return `${AUTH_COOKIE}=${token}`;
}
function escapeHtml(value: string): string {
return value
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
export function renderPasswordForm(
key: string,
filename: string,
error: boolean,
): Response {
const message = error
? '<p class="err">Incorrect password. Please try again.</p>'
: "";
const html = `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="referrer" content="no-referrer" />
<title>Protected file</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0; font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
background: #0f1115; color: #e6e8ee; min-height: 100vh;
display: flex; align-items: center; justify-content: center; padding: 16px;
}
.card {
width: 100%; max-width: 380px; background: #171a21; border: 1px solid #2a2f3a;
border-radius: 14px; padding: 26px; box-shadow: 0 10px 40px rgba(0,0,0,.35);
}
.brand-logo { display: block; width: 64px; height: 64px; margin: 0 auto 12px; }
h1 { margin: 0 0 4px; font-size: 19px; }
p { margin: 6px 0 16px; color: #9aa3b2; font-size: 14px; word-break: break-all; }
.err { margin: 0 0 14px; color: #e05252; background: rgba(224,82,82,.08);
border: 1px solid rgba(224,82,82,.3); border-radius: 8px; padding: 9px 12px; }
input[type="password"] {
width: 100%; background: #0f1115; border: 1px solid #2a2f3a; color: #e6e8ee;
border-radius: 8px; padding: 11px 12px; font-size: 14px; margin-bottom: 12px;
}
button { width: 100%; font: inherit; cursor: pointer; border: none; border-radius: 8px;
padding: 11px 14px; font-size: 14px; background: #4f8cff; color: #fff; }
button:hover { background: #3a74e0; }
</style>
</head>
<body>
<main class="card">
<svg class="brand-logo" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<defs>
<linearGradient id="uG" x1="200" y1="336" x2="312" y2="196" gradientUnits="userSpaceOnUse">
<stop stop-color="#4F46E5"/>
<stop offset="1" stop-color="#3B82F6"/>
</linearGradient>
</defs>
<rect x="48" y="48" width="416" height="416" rx="94" fill="#FFFFFF" stroke="#ECEFF5" stroke-width="2"/>
<path d="M184 144H306l38 38V352a16 16 0 0 1-16 16H184a16 16 0 0 1-16-16V160a16 16 0 0 1 16-16Z" fill="#fff" stroke="#E7ECF5" stroke-width="4"/>
<path d="M306 144v31c0 6 5 11 11 11h27" fill="#E8EEF9"/>
<path d="M306 144l38 38h-27c-6 0-11-5-11-11V144Z" fill="#DDE6F5"/>
<path d="M256 300V196 M222 230l34-34 34 34 M200 318v14c0 9 7 16 16 16h80c9 0 16-7 16-16v-14" stroke="url(#uG)" stroke-width="30" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<h1>Protected file</h1>
<p>Enter the password to download <strong>${escapeHtml(filename)}</strong>.</p>
${message}
<form method="post" action="/api/auth/${key}">
<input type="password" name="password" placeholder="Password" autofocus autocomplete="off" />
<button type="submit">Download</button>
</form>
</main>
</body>
</html>`;
return new Response(html, {
status: 200,
headers: {
"content-type": "text/html; charset=utf-8",
"cache-control": "private, no-store",
"x-content-type-options": "nosniff",
"referrer-policy": "no-referrer",
"content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'",
},
});
}