All files / routes/internal/website-build build-pro-data.routes.ts

100% Statements 63/63
100% Branches 16/16
100% Functions 3/3
100% Lines 63/63

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 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194                                                2x     2x 16x 16x 16x 16x   16x 16x 2x     14x 14x 2x       12x 12x 2x       10x     10x       10x 10x 10x   4x         10x 10x 10x   7x     10x                 6x         2x 11x 11x 11x     11x 11x 2x       9x 9x 2x       7x     7x       7x 7x 7x   5x         7x 7x 7x   7x     7x                 4x           2x         2x       9x 9x       9x 9x 1x           8x   8x 4x       4x 2x           2x               1x            
// Internal Pro Data Routes (for build worker and SSR preview)
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";
import type { Dal } from "../../../dal";
import type { Services } from "../../../services";
import { success, handleError } from "../../../lib/response";
import { NotFoundError } from "../../../lib/errors";
import { validatePreviewToken } from "../../../lib/preview-token";
import { logger } from "../../../lib/logger";
import {
	buildAggregatedCompanyProfile,
	fetchProjectsWithDetails,
	fetchProBlogs,
} from "./build-helpers";
 
type Env = {
	Bindings: CloudflareBindings;
	Variables: {
		dal: Dal;
		services: Services;
	};
};
 
const buildProDataRoutes = new Hono<Env>();
 
// Get pro data for building site
buildProDataRoutes.get("/:jobId/pro-data", async (c) => {
	try {
		const dal = c.get("dal");
		const services = c.get("services");
		const jobId = parseInt(c.req.param("jobId"), 10);
 
		const job = await dal.websiteBuildJobs.findBuildJobById(jobId);
		if (!job) {
			throw new NotFoundError("Build job not found");
		}
 
		const website = await dal.proWebsites.findById(job.proWebsiteId);
		if (!website) {
			throw new NotFoundError("Website not found");
		}
 
		// Get full pro data
		const pro = await services.pro.getById(website.proId);
		if (!pro) {
			throw new NotFoundError("Pro not found");
		}
 
		// Get template
		const template = await dal.websiteTemplates.findById(website.templateId);
 
		// Get all published projects with photos, rooms, and media
		const projects = await fetchProjectsWithDetails(dal, pro.id);
 
		// Get aggregated company profile (with leadership, certifications, testimonials)
		// Non-fatal: a build should not fail due to missing/errored company profile
		let companyProfile = null;
		try {
			companyProfile = await buildAggregatedCompanyProfile(dal, pro.id);
		} catch (err) {
			logger.error("[build-pro-data] Failed to fetch company profile for pro", pro.id, ":", err);
		}
 
		// Get published blogs for this pro
		// Non-fatal: a build should not fail due to missing/errored blogs
		let blogs: Awaited<ReturnType<typeof fetchProBlogs>> = [];
		try {
			blogs = await fetchProBlogs(dal, pro.id);
		} catch (err) {
			logger.error("[build-pro-data] Failed to fetch blogs for pro", pro.id, ":", err);
		}
 
		return success(c, {
			website,
			template,
			pro,
			projects,
			companyProfile,
			blogs,
		});
	} catch (err) {
		return handleError(c, err);
	}
});
 
// Get pro data by slug (for SSR preview)
buildProDataRoutes.get("/preview/:slug", async (c) => {
	try {
		const dal = c.get("dal");
		const slug = c.req.param("slug");
 
		// Find pro by slug
		const pro = await dal.pros.findBySlug(slug);
		if (!pro) {
			throw new NotFoundError("Pro not found for slug");
		}
 
		// Find website by proId
		const website = await dal.proWebsites.findByProId(pro.id);
		if (!website) {
			throw new NotFoundError("Website not found for pro");
		}
 
		// Get template
		const template = await dal.websiteTemplates.findById(website.templateId);
 
		// Get all published projects with photos, rooms, and media
		const projects = await fetchProjectsWithDetails(dal, pro.id);
 
		// Get aggregated company profile (with leadership, certifications, testimonials)
		// Non-fatal: a build should not fail due to missing/errored company profile
		let companyProfile = null;
		try {
			companyProfile = await buildAggregatedCompanyProfile(dal, pro.id);
		} catch (err) {
			logger.error("[build-pro-data] Failed to fetch company profile for pro", pro.id, ":", err);
		}
 
		// Get published blogs for this pro
		// Non-fatal: a build should not fail due to missing/errored blogs
		let blogs: Awaited<ReturnType<typeof fetchProBlogs>> = [];
		try {
			blogs = await fetchProBlogs(dal, pro.id);
		} catch (err) {
			logger.error("[build-pro-data] Failed to fetch blogs for pro", pro.id, ":", err);
		}
 
		return success(c, {
			website,
			template,
			pro,
			projects,
			companyProfile,
			blogs,
		});
	} catch (err) {
		return handleError(c, err);
	}
});
 
// Validate preview token (for pro-sites SSR preview)
// This endpoint does NOT require the internal API key - it's called by the public-facing preview
const validateTokenSchema = z.object({
	token: z.string().min(1),
	slug: z.string().min(1),
});
 
buildProDataRoutes.post(
	"/validate-preview-token",
	zValidator("json", validateTokenSchema),
	async (c) => {
		try {
			const { token, slug } = c.req.valid("json");
 
			// Get the secret used for signing tokens
			// Reuses BETTER_AUTH_SECRET for validation (same as used for signing)
			const secret = c.env.BETTER_AUTH_SECRET;
			if (!secret) {
				throw new Error(
					"BETTER_AUTH_SECRET not configured for token validation",
				);
			}
 
			// Validate the token
			const payload = await validatePreviewToken(token, secret);
 
			if (!payload) {
				return success(c, { valid: false, reason: "Invalid or expired token" });
			}
 
			// Check if the token matches the requested slug
			if (payload.sub !== slug) {
				return success(c, {
					valid: false,
					reason: "Token slug mismatch",
				});
			}
 
			return success(c, {
				valid: true,
				slug: payload.sub,
				proId: payload.vid,
				userId: payload.uid,
				expiresAt: new Date(payload.exp * 1000).toISOString(),
			});
		} catch (err) {
			return handleError(c, err);
		}
	},
);
 
export default buildProDataRoutes;