/** * Email checklist routes — mounted at top level to bypass space auth middleware. * GET /rtasks/check/:token — verify HMAC, toggle AC, render page * POST /api/rtasks/send — build + send checklist email */ import { Hono } from "hono"; import { checklistConfig } from "./lib/checklist-config"; import { createToken, verifyToken } from "./lib/token"; import { readTask, toggleAC } from "./lib/backlog"; import { renderEmailHTML, renderWebPage, renderError } from "./lib/render"; export const checklistCheckRoutes = new Hono(); export const checklistApiRoutes = new Hono(); // ── GET /rtasks/check/:token ── checklistCheckRoutes.get("/:token", async (c) => { const token = c.req.param("token"); let verified; try { verified = await verifyToken(token); } catch (err) { const message = err instanceof Error ? err.message : "Unknown error"; const title = message === "Token expired" ? "Link Expired" : "Invalid Link"; const body = message === "Token expired" ? "This checklist link has expired. Request a new checklist email." : "This link is not valid. It may have been corrupted."; return c.html(renderError(title, body), title === "Link Expired" ? 410 : 400); } try { const result = await toggleAC(verified.repo, verified.taskId, verified.acIndex); const tokens = new Map(); for (const ac of result.criteria) { tokens.set(ac.index, await createToken(verified.repo, verified.taskId, ac.index)); } return c.html(renderWebPage(result.title, verified.taskId, result.criteria, tokens, verified.acIndex, result.toggled)); } catch (err) { console.error("[Tasks/checklist] Check error:", err); return c.html(renderError("Error", "Could not update task. Please try again."), 500); } }); // ── POST /api/rtasks/send ── checklistApiRoutes.post("/send", async (c) => { const authHeader = c.req.header("Authorization"); if (!authHeader || authHeader !== `Bearer ${checklistConfig.apiKey}`) { return c.json({ error: "Unauthorized" }, 401); } const body = await c.req.json<{ repo: string; taskId: string; to: string; subject?: string; }>(); if (!body.repo || !body.taskId || !body.to) { return c.json({ error: "Missing required fields: repo, taskId, to" }, 400); } try { const task = await readTask(body.repo, body.taskId); const tokens = new Map(); for (const ac of task.criteria) { if (!ac.checked) { tokens.set(ac.index, await createToken(body.repo, body.taskId, ac.index)); } } if (tokens.size === 0) { return c.json({ error: "All acceptance criteria already checked" }, 400); } const subject = body.subject || `Checklist: ${task.title}`; const html = renderEmailHTML(task.title, body.taskId, task.criteria, tokens); const nodemailer = await import("nodemailer"); const transport = nodemailer.createTransport({ host: process.env.SMTP_HOST || "mailcowdockerized-postfix-mailcow-1", port: Number(process.env.SMTP_PORT) || 587, secure: false, auth: { user: process.env.SMTP_USER || "noreply@rmail.online", pass: process.env.SMTP_PASS, }, tls: { rejectUnauthorized: false }, }); const result = await transport.sendMail({ from: process.env.SMTP_FROM || `rTasks <${process.env.SMTP_USER || "noreply@rmail.online"}>`, to: body.to, subject, html, }); const firstToken = tokens.values().next().value; const url = `${checklistConfig.baseUrl}/rtasks/check/${firstToken}`; return c.json({ ok: true, messageId: result.messageId, url, unchecked: tokens.size }); } catch (err) { console.error("[Tasks/checklist] Send error:", err); const message = err instanceof Error ? err.message : "Unknown error"; return c.json({ error: message }, 500); } });