All files / routes/auth accept-terms.ts

100% Statements 19/19
100% Branches 6/6
100% Functions 1/1
100% Lines 19/19

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84                              7x                           7x 7x 7x 1x     6x 1x     5x 5x 2x           3x 3x                                 3x 3x 3x 3x 3x             1x     3x        
import { eq } from "drizzle-orm";
import { Hono } from "hono";
import type { Dal } from "../../dal";
import type { getDb } from "../../db";
import { users } from "../../db/schema";
import type { createAuth } from "../../lib/auth";
import type { DualCache } from "../../lib/cache";
import { logger } from "../../lib/logger";
import { CURRENT_TERMS_VERSION } from "../../lib/terms";
import type { Services } from "../../services";
 
type AuthSession = NonNullable<
	Awaited<ReturnType<ReturnType<typeof createAuth>["api"]["getSession"]>>
>;
 
const acceptTermsRoute = new Hono<{
	Bindings: CloudflareBindings;
	Variables: {
		user: AuthSession["user"] | null;
		session: AuthSession["session"] | null;
		dal: Dal;
		services: Services;
		db: ReturnType<typeof getDb>;
		cache: DualCache;
		proId: string;
		proRole: string;
	};
}>();
 
acceptTermsRoute.patch("/accept-terms", async (c) => {
	const user = c.get("user");
	if (!user) {
		return c.json({ error: "Unauthorized" }, 401);
	}
 
	if (!CURRENT_TERMS_VERSION) {
		return c.json({ error: "Terms enforcement is not currently active" }, 400);
	}
 
	const body = await c.req.json<{ terms_version?: string }>();
	if (body.terms_version !== CURRENT_TERMS_VERSION) {
		return c.json(
			{ error: `Invalid terms version. Expected ${CURRENT_TERMS_VERSION}` },
			400,
		);
	}
 
	const db = c.get("db");
	await db
		.update(users)
		.set({
			termsAcceptedAt: new Date(),
			termsVersion: CURRENT_TERMS_VERSION,
		})
		.where(eq(users.id, user.id));
 
	// Communication-logging consent (spec: communication_history_log) — pros
	// consent as part of terms acceptance. Best-effort: a consent-write failure
	// must not block terms acceptance.
	//
	// NB: /api/auth/* only runs authSessionMiddleware, which sets user/session/
	// db/cache but NOT `dal` (that's contextMiddleware, mounted on /api/pro etc).
	// Build the DAL inline from `db` — reading c.get("dal") here was undefined,
	// so the consent write silently threw into the catch on every request and no
	// pro consent was ever recorded.
	try {
		const { createDal } = await import("../../dal");
		const { communicationConsentTextHash } = await import("../../lib/consent");
		const dal = createDal(db);
		await dal.consentRecords.record({
			userId: user.id,
			userType: "pro",
			consentType: "communication_logging",
			consentTextHash: await communicationConsentTextHash(),
		});
	} catch (err) {
		logger.error("[ACCEPT-TERMS] consent record write failed:", err);
	}
 
	return c.json({ success: true });
});
 
export default acceptTermsRoute;