5 deletePostsByAuthorEmail,
6 getActiveFollowers,
7 getAllImages,
8 getAllPosts,
9 getEmailVerificationByToken,
10 getFollowerCount,
11 getImagesByPostSlug,
12 getImagesByUser,
13 getPostActivityCounts,
14 getPostBySlug,
29import { isEmailAllowed } from "./services/email-security.ts";
30import {
31 deleteImageCompletely,
32 getImageData,
33 getImageUrl,
34 storeImageFromFile,
35 validateImageFile,
36} from "./services/images.ts";
37import { generateRSSFeed } from "./services/rss.ts";
38import { extractDomain, generateWebFingerResponse, isValidWebFingerResource } from "./services/webfinger.ts";
215
216 <!-- Twitter -->
217 <meta property="twitter:card" content="summary_large_image">
218 <meta property="twitter:url" content="${baseUrl}">
219 <meta property="twitter:title" content="${escapeHtml(config.blog_title)}">
225 <link rel="alternate" type="application/activity+json" href="${baseUrl}/actor" />
226 <!-- Favicon -->
227 <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>${config.blog_icon}</text></svg>">
228
229 <!-- Styles -->
319
320 <!-- Twitter -->
321 <meta property="twitter:card" content="summary_large_image">
322 <meta property="twitter:url" content="${postUrl}">
323 <meta property="twitter:title" content="${escapeHtml(post.title)}">
329
330 <!-- Favicon -->
331 <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>${config.blog_icon}</text></svg>">
332
333 <!-- Styles -->
1417 icon: follower.actor_icon_url
1418 ? {
1419 type: "Image",
1420 url: follower.actor_icon_url,
1421 }
1454 icon: follower.actor_icon_url
1455 ? {
1456 type: "Image",
1457 url: follower.actor_icon_url,
1458 }
1750});
1751
1752// ===== IMAGE ROUTES =====
1753
1754// Serve image upload page
1755app.get("/upload", async c => {
1756 try {
1769 <div class="max-w-md mx-auto bg-white rounded-lg shadow-md p-6 text-center">
1770 <h1 class="text-2xl font-bold text-gray-900 mb-4">Upload Disabled</h1>
1771 <p class="text-gray-600 mb-4">Image upload is currently disabled.</p>
1772 <p class="text-sm text-gray-500">Administrator: Set the UPLOAD_PASSWORD environment variable to enable uploads.</p>
1773 <a href="/" class="text-blue-600 hover:text-blue-800 text-sm">← Back to Blog</a>
1922});
1923
1924// Serve images
1925app.get("/images/:filename", async c => {
1926 try {
1927 await ensureDbInitialized();
1928 const filename = c.req.param("filename");
1929
1930 const imageData = await getImageData(filename);
1931 if (!imageData) {
1932 return c.text("Image not found", 404);
1933 }
1934
1935 // Return the image data as a proper Response
1936 return new Response(imageData.data, {
1937 headers: {
1938 "Content-Type": imageData.mimeType,
1939 "Cache-Control": "public, max-age=31536000", // Cache for 1 year
1940 },
1941 });
1942 } catch (error) {
1943 console.error("Error serving image:", error);
1944 return c.text("Error serving image", 500);
1945 }
1946});
1947
1948// Upload image endpoint
1949app.post("/api/images/upload", async c => {
1950 try {
1951 await ensureDbInitialized();
1953 // Get the uploaded file and form data
1954 const body = await c.req.formData();
1955 const file = body.get("image") as File;
1956 const password = body.get("password") as string;
1957 const uploaderEmail = body.get("email") as string;
1960
1961 if (!file) {
1962 return c.json({ error: "No image file provided" }, 400);
1963 }
1964
1979 console.log(`✅ Valid upload password from ${uploaderEmail}`);
1980
1981 // Store the image
1982 const image = await storeImageFromFile(file, uploaderEmail, {
1983 altText: altText || undefined,
1984 postSlug: postSlug || undefined,
1986
1987 const baseUrl = getBaseUrl(c);
1988 const imageUrl = getImageUrl(image.filename, baseUrl);
1989
1990 return c.json({
1991 success: true,
1992 image: {
1993 id: image.id,
1994 filename: image.filename,
1995 originalFilename: image.original_filename,
1996 url: imageUrl,
1997 mimeType: image.mime_type,
1998 fileSize: image.file_size,
1999 altText: image.alt_text,
2000 postSlug: image.post_slug,
2001 createdAt: image.created_at,
2002 },
2003 });
2004 } catch (error) {
2005 console.error("Error uploading image:", error);
2006 return c.json({ error: error.message || "Failed to upload image" }, 500);
2007 }
2008});
2009
2010// Get images for a specific post
2011app.get("/api/posts/:slug/images", async c => {
2012 try {
2013 await ensureDbInitialized();
2014 const slug = c.req.param("slug");
2015
2016 const images = await getImagesByPostSlug(slug);
2017 const baseUrl = getBaseUrl(c);
2018
2019 return c.json({
2020 images: images.map(image => ({
2021 id: image.id,
2022 filename: image.filename,
2023 originalFilename: image.original_filename,
2024 url: getImageUrl(image.filename, baseUrl),
2025 mimeType: image.mime_type,
2026 fileSize: image.file_size,
2027 altText: image.alt_text,
2028 createdAt: image.created_at,
2029 })),
2030 });
2031 } catch (error) {
2032 console.error("Error fetching post images:", error);
2033 return c.json({ error: "Failed to fetch images" }, 500);
2034 }
2035});
2036
2037// Get images uploaded by a user
2038app.get("/api/images/user/:email", async c => {
2039 try {
2040 await ensureDbInitialized();
2047 }
2048
2049 const images = await getImagesByUser(email);
2050 const baseUrl = getBaseUrl(c);
2051
2052 return c.json({
2053 images: images.map(image => ({
2054 id: image.id,
2055 filename: image.filename,
2056 originalFilename: image.original_filename,
2057 url: getImageUrl(image.filename, baseUrl),
2058 mimeType: image.mime_type,
2059 fileSize: image.file_size,
2060 altText: image.alt_text,
2061 postSlug: image.post_slug,
2062 createdAt: image.created_at,
2063 })),
2064 });
2065 } catch (error) {
2066 console.error("Error fetching user images:", error);
2067 return c.json({ error: "Failed to fetch images" }, 500);
2068 }
2069});
2070
2071// Delete an image
2072app.delete("/api/images/:filename", async c => {
2073 try {
2074 await ensureDbInitialized();
2080 }
2081
2082 const success = await deleteImageCompletely(filename);
2083
2084 if (!success) {
2085 return c.json({ error: "Image not found" }, 404);
2086 }
2087
2088 return c.json({ success: true, message: "Image deleted successfully" });
2089 } catch (error) {
2090 console.error("Error deleting image:", error);
2091 return c.json({ error: "Failed to delete image" }, 500);
2092 }
2093});
2094
2095// Get all images (admin endpoint)
2096app.get("/api/images", async c => {
2097 try {
2098 await ensureDbInitialized();
2106 const offset = parseInt(c.req.query("offset") || "0");
2107
2108 const images = await getAllImages(limit, offset);
2109 const baseUrl = getBaseUrl(c);
2110
2111 return c.json({
2112 images: images.map(image => ({
2113 id: image.id,
2114 filename: image.filename,
2115 originalFilename: image.original_filename,
2116 url: getImageUrl(image.filename, baseUrl),
2117 mimeType: image.mime_type,
2118 fileSize: image.file_size,
2119 altText: image.alt_text,
2120 postSlug: image.post_slug,
2121 uploadedByEmail: image.uploaded_by_email,
2122 createdAt: image.created_at,
2123 })),
2124 pagination: {
2125 limit,
2126 offset,
2127 hasMore: images.length === limit,
2128 },
2129 });
2130 } catch (error) {
2131 console.error("Error fetching all images:", error);
2132 return c.json({ error: "Failed to fetch images" }, 500);
2133 }
2134});
2135
2136// Serve images from blob storage
2137app.get("/images/:filename", async c => {
2138 try {
2139 const filename = c.req.param("filename");
2143 }
2144
2145 console.log(`📷 Serving image: ${filename}`);
2146
2147 const imageData = await getImageData(filename);
2148
2149 if (!imageData) {
2150 console.log(`❌ Image not found: ${filename}`);
2151 return c.text("Image not found", 404);
2152 }
2153
2154 console.log(`✅ Image found: ${filename}, size: ${imageData.data.length} bytes, type: ${imageData.mimeType}`);
2155
2156 // Set appropriate headers
2157 const headers = {
2158 "Content-Type": imageData.mimeType,
2159 "Content-Length": imageData.data.length.toString(),
2160 "Cache-Control": "public, max-age=31536000", // Cache for 1 year
2161 "ETag": `"${filename}"`,
2162 };
2163
2164 return new Response(imageData.data, { headers });
2165 } catch (error) {
2166 console.error("Error serving image:", error);
2167 return c.text("Error serving image", 500);
2168 }
2169});