/** * Turns a digest off from the link in its email, without signing in. Mail * clients post here for a one-click unsubscribe (RFC 8058), and so does the * client's `/unsubscribe` page. * * - `POST /api/unsubscribe?token=` — public, with a digest's unsubscribe * token. Turns that digest off and answers its cadence. * - `POST /api/users/unsubscribe?token=` — the same, at its v3 path, which the * `List-Unsubscribe` header of every digest sent before v4 names. Those * tokens never expire, so this path stays for good. * * @module */ import { Router, type RouterMiddleware } from "@oak/oak"; import { type APIResponse } from "../../shared/api.ts"; import { APIException } from "../lib/api-exception.ts"; import { DIGEST_SETTING, type DigestCadence } from "../../shared/settings.ts"; import { verifyUnsubscribeToken } from "../lib/jwt.ts"; import { getUserById } from "../services/user-service.ts"; import { updateSettings } from "../services/settings-service.ts"; /** * Mounted by `api/main.ts` behind the error, Open Graph and CORS middleware. * An `APIException` thrown by a handler becomes the JSON error response there. */ const router: Router = new Router(); // The body is ignored: a mail client's one-click post sends a form saying // "List-Unsubscribe=One-Click", the client's page sends nothing. const unsubscribe: RouterMiddleware = async (ctx) => { const token = ctx.request.url.searchParams.get("token") ?? ""; const payload = await verifyUnsubscribeToken(token); if (!payload) throw new APIException(400, "Invalid unsubscribe link"); // Throws a 404 once the account is gone. getUserById(payload.userId); updateSettings(payload.userId, { [DIGEST_SETTING[payload.cadence]]: false }); const responseBody: APIResponse<{ cadence: DigestCadence }> = { success: true, data: { cadence: payload.cadence }, }; ctx.response.body = responseBody; }; router.post("/api/unsubscribe", unsubscribe); router.post("/api/users/unsubscribe", unsubscribe); export default router;