All files / routes/pro feedback.routes.ts

100% Statements 24/24
100% Branches 37/37
100% Functions 1/1
100% Lines 24/24

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 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106              1x                     1x   1x 15x 15x 15x 15x 15x     15x     15x         3x       12x     15x                               11x 11x 4x       4x 4x     4x   4x                                   1x       11x           1x 1x          
import { Hono } from "hono";
import { feedback } from "../../db/schema";
import { ValidationError } from "../../lib/errors";
import { escapeHtml } from "../../lib/html-escape";
import { logger } from "../../lib/logger";
import { handleError, success } from "../../lib/response";
 
const feedbackRoutes = new Hono<{
	Bindings: CloudflareBindings;
	Variables: {
		user: { id: string; name: string; email: string } | null;
		session: unknown;
		proId: string;
		proRole: string;
		db: ReturnType<typeof import("../../db").getDb>;
	};
}>();
 
const VALID_CATEGORIES = ["general", "bug", "feature_request"] as const;
 
feedbackRoutes.post("/feedback", async (c) => {
	try {
		const user = c.get("user");
		const proId = c.get("proId");
		const db = c.get("db");
		const body = await c.req.json();
 
		const { category, message, pageUrl, browser, screenResolution, location } =
			body;
 
		// Validate
		if (
			!message ||
			typeof message !== "string" ||
			message.trim().length === 0
		) {
			return handleError(c, new ValidationError("Message is required"));
		}
 
		const validCategory =
			category && VALID_CATEGORIES.includes(category) ? category : "general";
 
		// Insert feedback
		const [inserted] = await db
			.insert(feedback)
			.values({
				userId: user?.id ?? null,
				proId: proId ?? null,
				category: validCategory,
				message: message.trim(),
				pageUrl: pageUrl ?? null,
				userEmail: user?.email ?? null,
				browser: browser ?? null,
				screenResolution: screenResolution ?? null,
				location: location ?? null,
			})
			.returning({ id: feedback.id });
 
		// Send email notification (non-blocking — don't fail the request if email fails)
		const notifyEmail = c.env.FEEDBACK_NOTIFY_EMAIL;
		if (notifyEmail) {
			try {
				// Loaded lazily: lib/email pulls @react-email/render (prettier,
				// html-to-text, entities, react). Evaluating that at module scope costs
				// startup CPU on every cold isolate, including the ones that never send.
				const { createEmailService } = await import("../../lib/email");
				const emailService = createEmailService(c.env);
 
				const truncatedMessage =
					message.length > 50 ? `${message.substring(0, 50)}...` : message;
 
				await emailService.sendRawEmail(
					notifyEmail,
					`[Interioring Feedback] ${validCategory}: ${truncatedMessage}`,
					`
						<h2>New Feedback Received</h2>
						<p><strong>Category:</strong> ${escapeHtml(validCategory)}</p>
						<p><strong>From:</strong> ${escapeHtml(user?.email ?? "Unknown")}</p>
						<p><strong>Page:</strong> ${escapeHtml(String(pageUrl ?? "N/A"))}</p>
						<p><strong>Message:</strong></p>
						<blockquote>${escapeHtml(message)}</blockquote>
						<hr>
						<p><strong>Browser:</strong> ${escapeHtml(String(browser ?? "N/A"))}</p>
						<p><strong>Resolution:</strong> ${escapeHtml(String(screenResolution ?? "N/A"))}</p>
						<p><strong>Timezone:</strong> ${escapeHtml(String(location ?? "N/A"))}</p>
						<p><strong>Submitted:</strong> ${new Date().toISOString()}</p>
					`,
				);
			} catch (emailErr) {
				logger.error("[FEEDBACK] Email notification failed:", emailErr);
			}
		}
 
		return success(
			c,
			{ id: inserted.id, message: "Feedback submitted successfully" },
			201,
		);
	} catch (err) {
		logger.error("[FEEDBACK] Error submitting feedback:", err);
		return handleError(c, err);
	}
});
 
export default feedbackRoutes;