Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/just-cameron/microstructure.exchange. A seminar series for market microstructure.
Something went wrong. Try again.
23 kB · 778 lines
JavaScript
at master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779import { upcomingPreview } from "./talk-sharing.js";
const VOTE_ROUTE_PREFIX = "/tme-best-paper-vote-2026";const ASSESSMENT_ROUTE_PREFIX = "/tme-paper-assessment-2026";const SUBMISSION_ROUTE_PREFIX = "/tme-paper-submission";const SUBMISSION_CALL_ID = "winter2027";const SUBMISSION_R2_PREFIX = "submissions/winter2027";const MAX_SUBMISSION_BYTES = 10 * 1024 * 1024;
const JSON_HEADERS = { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store"};
export default { async fetch(request, env) { const url = new URL(request.url); const path = normalizePath(url.pathname);
try { if ((url.pathname === "/" || url.pathname === "/index.html") && ["GET", "HEAD"].includes(request.method)) { return await upcomingPreview(request, env); } if (url.pathname === VOTE_ROUTE_PREFIX || url.pathname === ASSESSMENT_ROUTE_PREFIX || url.pathname === SUBMISSION_ROUTE_PREFIX) { url.pathname = `${url.pathname}/`; return Response.redirect(url.toString(), 302); }
if (path === "/api/vote/status" && request.method === "GET") { return handleStatus(request, env); }
if (path === "/api/vote" && request.method === "POST") { return handleVote(request, env); }
if (path === "/api/admin/export.csv" && request.method === "GET") { return handleExport(request, env); }
if (path === "/api/assessment/status" && request.method === "GET") { return handleAssessmentStatus(request, env); }
if (path === "/api/assessment" && request.method === "POST") { return handleAssessmentSubmit(request, env); }
if (path === "/api/admin/assessment-export.csv" && request.method === "GET") { return handleAssessmentExport(request, env); }
if (path === "/api/submission" && request.method === "POST") { return handlePaperSubmission(request, env); }
if (path === "/api/admin/submissions-export.csv" && request.method === "GET") { return handleSubmissionsExport(request, env); }
if (url.pathname.startsWith(`${ASSESSMENT_ROUTE_PREFIX}/papers/`) && request.method === "GET") { return handleAssessmentPaper(request, env, path); }
if (path.startsWith("/api/")) { return json({ error: "Not found" }, 404); }
if (url.pathname.startsWith(`${VOTE_ROUTE_PREFIX}/`)) { return fetchPrefixedAsset(request, env, path); }
if (url.pathname.startsWith(`${ASSESSMENT_ROUTE_PREFIX}/`)) { return fetchAssessmentAsset(request, env, path); }
if (url.pathname.startsWith(`${SUBMISSION_ROUTE_PREFIX}/`)) { return fetchSubmissionAsset(request, env, path); }
if (url.pathname === "/best-paper-vote" || url.pathname === "/best-paper-vote/") { const assetUrl = new URL(request.url); assetUrl.pathname = "/"; return env.ASSETS.fetch(new Request(assetUrl.toString(), request)); }
return env.ASSETS.fetch(request); } catch (error) { return json({ error: "Unexpected server error" }, 500); } }};
function normalizePath(pathname) { for (const prefix of [VOTE_ROUTE_PREFIX, ASSESSMENT_ROUTE_PREFIX, SUBMISSION_ROUTE_PREFIX]) { if (pathname === prefix) { return "/"; }
if (pathname.startsWith(`${prefix}/`)) { return pathname.slice(prefix.length) || "/"; } }
return pathname;}
function fetchPrefixedAsset(request, env, path) { const assetUrl = new URL(request.url); assetUrl.pathname = path === "/" ? "/" : path; return env.ASSETS.fetch(new Request(assetUrl.toString(), request));}
function fetchAssessmentAsset(request, env, path) { const assetUrl = new URL(request.url); const assetMap = new Map([ ["/", "/assessment"], ["/assessment", "/assessment"], ["/assessment.html", "/assessment"], ["/assessment.css", "/assessment.css"], ["/assessment.js", "/assessment.js"] ]); assetUrl.pathname = assetMap.get(path) || path; return env.ASSETS.fetch(new Request(assetUrl.toString(), request));}
function fetchSubmissionAsset(request, env, path) { const assetUrl = new URL(request.url); const assetMap = new Map([ ["/", "/submission"], ["/submission", "/submission"], ["/submission.html", "/submission"], ["/submission.css", "/submission.css"], ["/submission.js", "/submission.js"] ]); assetUrl.pathname = assetMap.get(path) || path; return env.ASSETS.fetch(new Request(assetUrl.toString(), request));}
async function handleStatus(request, env) { const token = new URL(request.url).searchParams.get("token") || ""; const voter = await findVoter(env.DB, token);
if (!voter) { return json({ error: "Invalid voting link" }, 401); }
const papers = await listPapers(env.DB);
return json({ hasVoted: Boolean(voter.voted_at), voter: { name: voter.name, reviewer: Boolean(voter.reviewer), attendee: Boolean(voter.attendee) }, papers });}
async function handleVote(request, env) { const body = await readJson(request); const voter = await findVoter(env.DB, body.token || "");
if (!voter) { return json({ error: "Invalid voting link" }, 401); }
if (voter.voted_at) { return json({ error: "This voting link has already been used" }, 409); }
const choices = [ body.firstChoice, body.secondChoice || null, body.thirdChoice || null ].filter(Boolean);
if (!body.firstChoice) { return json({ error: "A first choice is required" }, 400); }
if (new Set(choices).size !== choices.length) { return json({ error: "Ranked choices must be distinct" }, 400); }
const validPaperIds = new Set((await listPapers(env.DB)).map((paper) => paper.id)); for (const choice of choices) { if (!validPaperIds.has(choice)) { return json({ error: "One or more selected papers are invalid" }, 400); } }
const comments = String(body.comments || "").slice(0, 1000); const userAgent = request.headers.get("User-Agent") || ""; const ipHash = await hashIp(request.headers.get("CF-Connecting-IP") || "", env.IP_HASH_SALT || "");
const submittedAt = new Date().toISOString();
await env.DB.batch([ env.DB.prepare(` INSERT INTO votes ( voter_id, first_choice_paper_id, second_choice_paper_id, third_choice_paper_id, comments, submitted_at, user_agent, ip_hash ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `).bind( voter.id, body.firstChoice, body.secondChoice || null, body.thirdChoice || null, comments, submittedAt, userAgent.slice(0, 300), ipHash ), env.DB.prepare("UPDATE voters SET voted_at = ? WHERE id = ? AND voted_at IS NULL").bind(submittedAt, voter.id) ]);
return json({ ok: true });}
async function handleExport(request, env) { if (!authorizeAdmin(request, env)) { return new Response("Unauthorized\n", { status: 401 }); }
const { results } = await env.DB.prepare(` SELECT voters.name, voters.email, voters.reviewer, voters.attendee, voters.sessions_attended, voters.session_dates, votes.first_choice_paper_id, first_paper.presenter AS first_choice_presenter, first_paper.title AS first_choice_title, votes.second_choice_paper_id, second_paper.presenter AS second_choice_presenter, second_paper.title AS second_choice_title, votes.third_choice_paper_id, third_paper.presenter AS third_choice_presenter, third_paper.title AS third_choice_title, votes.comments, votes.submitted_at FROM votes JOIN voters ON voters.id = votes.voter_id JOIN papers AS first_paper ON first_paper.id = votes.first_choice_paper_id LEFT JOIN papers AS second_paper ON second_paper.id = votes.second_choice_paper_id LEFT JOIN papers AS third_paper ON third_paper.id = votes.third_choice_paper_id ORDER BY votes.submitted_at ASC `).all();
return csvResponse(results, "tme-best-paper-votes.csv", [ "name", "email", "reviewer", "attendee", "sessions_attended", "session_dates", "first_choice_paper_id", "first_choice_presenter", "first_choice_title", "second_choice_paper_id", "second_choice_presenter", "second_choice_title", "third_choice_paper_id", "third_choice_presenter", "third_choice_title", "comments", "submitted_at" ]);}
async function handleAssessmentStatus(request, env) { const token = new URL(request.url).searchParams.get("token") || ""; const reviewer = await findAssessmentReviewer(env.DB, token);
if (!reviewer) { return json({ error: "Invalid review link" }, 401); }
const papers = await listAssessmentPapers(env.DB, reviewer.id);
return json({ submitted: Boolean(reviewer.submitted_at), reviewer: { name: reviewer.name, firstName: reviewer.first_name, lastName: reviewer.last_name, email: reviewer.email }, papers });}
async function handleAssessmentPaper(request, env, path) { const token = new URL(request.url).searchParams.get("token") || ""; const reviewer = await findAssessmentReviewer(env.DB, token);
if (!reviewer) { return json({ error: "Invalid review link" }, 401); }
const paperNumber = Number(path.split("/").filter(Boolean).at(1)); if (!Number.isInteger(paperNumber)) { return json({ error: "Invalid paper link" }, 400); }
const paper = await env.DB.prepare(` SELECT assessment_papers.* FROM assessment_assignments JOIN assessment_papers ON assessment_papers.id = assessment_assignments.paper_id WHERE assessment_assignments.reviewer_id = ? AND assessment_papers.paper_number = ? AND assessment_papers.active = 1 `).bind(reviewer.id, paperNumber).first();
if (!paper) { return json({ error: "Paper is not assigned to this reviewer" }, 403); }
if (!env.PAPERS) { return json({ error: "Paper storage is not configured" }, 500); }
const object = await env.PAPERS.get(paper.r2_key); if (!object) { return json({ error: "Paper file was not found" }, 404); }
const headers = new Headers(); object.writeHttpMetadata(headers); headers.set("Content-Type", paper.content_type || headers.get("Content-Type") || "application/octet-stream"); headers.set("Content-Disposition", `inline; filename="${paper.filename.replaceAll('"', "")}"`); headers.set("Cache-Control", "private, no-store"); headers.set("X-Content-Type-Options", "nosniff");
return new Response(object.body, { headers });}
async function handleAssessmentSubmit(request, env) { const body = await readJson(request); const reviewer = await findAssessmentReviewer(env.DB, body.token || "");
if (!reviewer) { return json({ error: "Invalid review link" }, 401); }
if (reviewer.submitted_at) { return json({ error: "This review link has already been submitted" }, 409); }
const assignedPapers = await listAssessmentPapers(env.DB, reviewer.id); const assignedIds = new Set(assignedPapers.map((paper) => paper.id)); const ratings = Array.isArray(body.ratings) ? body.ratings : [];
if (ratings.length !== assignedIds.size) { return json({ error: "Please rate every assigned paper before submitting" }, 400); }
const seen = new Set(); for (const rating of ratings) { if (!assignedIds.has(rating.paperId) || seen.has(rating.paperId)) { return json({ error: "Ratings include an invalid paper" }, 400); } seen.add(rating.paperId);
if (!Number.isInteger(rating.rating) || rating.rating < 1 || rating.rating > 5) { return json({ error: "Ratings must be between 1 and 5" }, 400); } }
const submittedAt = new Date().toISOString(); const userAgent = request.headers.get("User-Agent") || ""; const ipHash = await hashIp(request.headers.get("CF-Connecting-IP") || "", env.IP_HASH_SALT || ""); const overallComments = String(body.comments || "").slice(0, 1000); const statements = ratings.map((rating) => env.DB.prepare(` INSERT INTO assessment_ratings ( reviewer_id, paper_id, rating, comments, submitted_at, user_agent, ip_hash ) VALUES (?, ?, ?, ?, ?, ?, ?) `).bind( reviewer.id, rating.paperId, rating.rating, "", submittedAt, userAgent.slice(0, 300), ipHash ));
statements.push( env.DB.prepare("UPDATE assessment_reviewers SET submitted_at = ?, overall_comments = ? WHERE id = ? AND submitted_at IS NULL").bind(submittedAt, overallComments, reviewer.id) );
await env.DB.batch(statements);
return json({ ok: true });}
async function handleAssessmentExport(request, env) { if (!authorizeAdmin(request, env)) { return new Response("Unauthorized\n", { status: 401 }); }
const headers = [ "paper_number", "paper_title", "rating", "reviewer_name", "reviewer_email", "reviewer_code", "overall_comments", "rating_submitted_at" ]; const { results } = await env.DB.prepare(` SELECT assessment_papers.paper_number, assessment_papers.title AS paper_title, assessment_ratings.rating, assessment_reviewers.name AS reviewer_name, assessment_reviewers.email AS reviewer_email, assessment_reviewers.reviewer_code, assessment_reviewers.overall_comments, assessment_ratings.submitted_at AS rating_submitted_at FROM assessment_ratings JOIN assessment_reviewers ON assessment_reviewers.id = assessment_ratings.reviewer_id JOIN assessment_papers ON assessment_papers.id = assessment_ratings.paper_id ORDER BY assessment_papers.paper_number ASC, assessment_reviewers.name ASC `).all();
return csvResponse(results, "tme-paper-assessment-results.csv", headers);}
async function handlePaperSubmission(request, env) { if (!env.PAPERS) { return json({ error: "Paper storage is not configured" }, 500); }
const form = await request.formData(); const title = cleanText(form.get("title"), 300); const abstract = cleanText(form.get("abstract"), 3500); const submitterName = cleanText(form.get("submitterName"), 160); const submitterEmail = cleanEmail(form.get("submitterEmail")); const file = form.get("paper"); const coauthors = parseCoauthors(form.get("coauthorsJson"));
if (!title) { return json({ error: "Paper title is required" }, 400); }
if (!abstract) { return json({ error: "Paper abstract is required" }, 400); }
if (!submitterName || !submitterEmail) { return json({ error: "Submitting author name and email are required" }, 400); }
if (!(file instanceof File) || !file.name || file.size === 0) { return json({ error: "A paper file is required" }, 400); }
if (file.size > MAX_SUBMISSION_BYTES) { return json({ error: "The uploaded paper is larger than 10MB" }, 400); }
const extension = extensionFor(file.name, file.type); if (!extension) { return json({ error: "Please upload an anonymous PDF file" }, 400); }
for (const coauthor of coauthors) { if (!coauthor.name || !coauthor.email) { return json({ error: "Each co-author needs both a name and an email address" }, 400); } }
const submittedAt = new Date().toISOString(); const confirmationToken = crypto.randomUUID(); const allAuthors = [ { name: submitterName, email: submitterEmail, role: "submitter" }, ...coauthors.map((author) => ({ ...author, role: "coauthor" })) ]; const authorNames = allAuthors.map((author) => author.name).join("; "); const authorEmails = allAuthors.map((author) => author.email).join("; "); const userAgent = request.headers.get("User-Agent") || ""; const ipHash = await hashIp(request.headers.get("CF-Connecting-IP") || "", env.IP_HASH_SALT || "");
const inserted = await env.DB.prepare(` INSERT INTO paper_submissions ( call_id, submission_number, title, abstract, submitter_name, submitter_email, coauthors_json, author_names, author_emails, original_filename, content_type, size_bytes, confirmation_token, submitted_at, user_agent, ip_hash ) VALUES ( ?, (SELECT COALESCE(MAX(submission_number), 0) + 1 FROM paper_submissions WHERE call_id = ?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) RETURNING id, submission_number `).bind( SUBMISSION_CALL_ID, SUBMISSION_CALL_ID, title, abstract, submitterName, submitterEmail, JSON.stringify(coauthors), authorNames, authorEmails, file.name.slice(0, 240), contentTypeForUpload(file.type, extension), file.size, confirmationToken, submittedAt, userAgent.slice(0, 300), ipHash ).first();
const submissionNumber = inserted.submission_number; const storedFilename = `TME_${SUBMISSION_CALL_ID}_submission_${submissionNumber}.${extension}`; const r2Key = `${SUBMISSION_R2_PREFIX}/${storedFilename}`;
await env.PAPERS.put(r2Key, file.stream(), { httpMetadata: { contentType: contentTypeForUpload(file.type, extension) }, customMetadata: { callId: SUBMISSION_CALL_ID, submissionNumber: String(submissionNumber), originalFilename: file.name.slice(0, 240) } });
await env.DB.prepare(` UPDATE paper_submissions SET stored_filename = ?, r2_key = ?, status = 'received' WHERE id = ? `).bind(storedFilename, r2Key, inserted.id).run();
return json({ ok: true, submissionNumber, title, message: "Submission received" });}
async function handleSubmissionsExport(request, env) { if (!authorizeAdmin(request, env)) { return new Response("Unauthorized\n", { status: 401 }); }
const headers = [ "call_id", "submission_number", "title", "abstract", "submitter_name", "submitter_email", "author_names", "author_emails", "coauthors_json", "original_filename", "stored_filename", "r2_key", "content_type", "size_bytes", "status", "submitted_at" ]; const { results } = await env.DB.prepare(` SELECT call_id, submission_number, title, abstract, submitter_name, submitter_email, author_names, author_emails, coauthors_json, original_filename, stored_filename, r2_key, content_type, size_bytes, status, submitted_at FROM paper_submissions ORDER BY call_id ASC, submission_number ASC `).all();
return csvResponse(results, "tme-paper-submissions.csv", headers);}
async function findVoter(db, token) { if (!token || token.length < 24) { return null; }
const tokenHash = await sha256Hex(token); return db.prepare("SELECT * FROM voters WHERE token_hash = ?").bind(tokenHash).first();}
async function findAssessmentReviewer(db, token) { if (!token || token.length < 24) { return null; }
const tokenHash = await sha256Hex(token); return db.prepare("SELECT * FROM assessment_reviewers WHERE token_hash = ?").bind(tokenHash).first();}
async function listPapers(db) { const { results } = await db.prepare(` SELECT id, presenter, title, url FROM papers WHERE active = 1 ORDER BY sort_order ASC, presenter ASC `).all();
return results;}
async function listAssessmentPapers(db, reviewerId) { const { results } = await db.prepare(` SELECT assessment_papers.id, assessment_papers.paper_number AS paperNumber, assessment_papers.title FROM assessment_assignments JOIN assessment_papers ON assessment_papers.id = assessment_assignments.paper_id WHERE assessment_assignments.reviewer_id = ? AND assessment_papers.active = 1 ORDER BY assessment_assignments.assignment_order ASC `).bind(reviewerId).all();
return results;}
async function readJson(request) { try { return await request.json(); } catch (error) { return {}; }}
function json(payload, status = 200) { return new Response(JSON.stringify(payload), { status, headers: JSON_HEADERS });}
function authorizeAdmin(request, env) { const adminKey = env.ADMIN_KEY || ""; const authorization = request.headers.get("Authorization") || ""; const provided = authorization.startsWith("Bearer ") ? authorization.slice("Bearer ".length) : new URL(request.url).searchParams.get("key") || "";
return Boolean(adminKey && provided === adminKey);}
async function sha256Hex(value) { const bytes = new TextEncoder().encode(value); const digest = await crypto.subtle.digest("SHA-256", bytes); return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");}
async function hashIp(ip, salt) { if (!ip || !salt) { return ""; }
return sha256Hex(`${salt}:${ip}`);}
function cleanText(value, maxLength) { return String(value || "").trim().replace(/\s+/g, " ").slice(0, maxLength);}
function cleanEmail(value) { const email = cleanText(value, 200).toLowerCase(); return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) ? email : "";}
function parseCoauthors(value) { try { const parsed = JSON.parse(String(value || "[]")); if (!Array.isArray(parsed)) { return []; } return parsed.slice(0, 20).map((author) => ({ name: cleanText(author.name, 160), email: cleanEmail(author.email) })).filter((author) => author.name || author.email); } catch (error) { return []; }}
function extensionFor(filename, contentType) { const name = String(filename || "").toLowerCase(); const type = String(contentType || "").toLowerCase(); if (name.endsWith(".pdf") && (!type || type === "application/pdf")) { return "pdf"; } return "";}
function contentTypeForUpload(contentType, extension) { return "application/pdf";}
function csvResponse(rows, filename, headers) { const csv = toCsv(rows, headers); return new Response(csv, { headers: { "Content-Type": "text/csv; charset=utf-8", "Content-Disposition": `attachment; filename="${filename}"`, "Cache-Control": "no-store" } });}
function toCsv(rows, headers = null) { const csvHeaders = headers || (rows.length ? Object.keys(rows[0]) : []); if (!csvHeaders.length) { return "\n"; }
const lines = [csvHeaders.join(",")];
for (const row of rows) { lines.push(csvHeaders.map((header) => csvCell(row[header])).join(",")); }
return `${lines.join("\n")}\n`;}
function csvCell(value) { const text = String(value ?? ""); if (/[",\n\r]/.test(text)) { return `"${text.replaceAll('"', '""')}"`; } return text;}