diff --git a/src/components/blog/PostForm.tsx b/src/components/blog/PostForm.tsx index 8ccae99..a56aa35 100644 --- a/src/components/blog/PostForm.tsx +++ b/src/components/blog/PostForm.tsx @@ -255,81 +255,83 @@ export default function PostForm(props: PostFormProps) { }; return ( -
+
{props.mode === "edit" ? "Edit a Blog" : "Create a Blog"}
-
- {/* Title */} -
- setTitle(e.currentTarget.value)} - name="title" - placeholder=" " - class="underlinedInput w-full bg-transparent" - /> - - -
+ +
+ {/* Title */} +
+ setTitle(e.currentTarget.value)} + name="title" + placeholder=" " + class="underlinedInput w-full bg-transparent" + /> + + +
- {/* Subtitle */} -
- setSubtitle(e.currentTarget.value)} - name="subtitle" - placeholder=" " - class="underlinedInput w-full bg-transparent" - /> - - -
+ {/* Subtitle */} +
+ setSubtitle(e.currentTarget.value)} + name="subtitle" + placeholder=" " + class="underlinedInput w-full bg-transparent" + /> + + +
- {/* Banner */} -
Banner
-
- Banner
+
+ + +
+ + {/* Attachments */} + -
- {/* Attachments */} - - {/* Text Editor */} -
+
diff --git a/src/server/api/routers/lineage/database.ts b/src/server/api/routers/lineage/database.ts index 16bfe47..ea5ab33 100644 --- a/src/server/api/routers/lineage/database.ts +++ b/src/server/api/routers/lineage/database.ts @@ -9,6 +9,13 @@ import { TRPCError } from "@trpc/server"; import { OAuth2Client } from "google-auth-library"; import { jwtVerify } from "jose"; import { createTRPCRouter, publicProcedure } from "~/server/api/utils"; +import { + fetchWithTimeout, + checkResponse, + NetworkError, + TimeoutError, + APIError +} from "~/server/fetch-utils"; export const lineageDatabaseRouter = createTRPCRouter({ credentials: publicProcedure @@ -155,17 +162,20 @@ export const lineageDatabaseRouter = createTRPCRouter({ }); if (dumpRes.success) { - const deleteRes = await fetch( - `https://api.turso.tech/v1/organizations/mikefreno/databases/${db_name}`, - { - method: "DELETE", - headers: { - Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}` + try { + const deleteRes = await fetchWithTimeout( + `https://api.turso.tech/v1/organizations/mikefreno/databases/${db_name}`, + { + method: "DELETE", + headers: { + Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}` + }, + timeout: 20000 // 20s for database deletion } - } - ); + ); + + await checkResponse(deleteRes); - if (deleteRes.ok) { await conn.execute({ sql: `DELETE FROM User WHERE email = ?`, args: [email] @@ -175,11 +185,35 @@ export const lineageDatabaseRouter = createTRPCRouter({ status: 200, message: `Account and Database deleted, db dump sent to email: ${send_dump_target}` }; - } else { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Failed to delete database" - }); + } catch (error) { + if (error instanceof TimeoutError) { + console.error("Database deletion timeout:", error.message); + throw new TRPCError({ + code: "TIMEOUT", + message: + "Database deletion timed out. Please contact support." + }); + } else if (error instanceof NetworkError) { + console.error( + "Network error during database deletion:", + error.message + ); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Network error deleting database. Please try again." + }); + } else if (error instanceof APIError) { + console.error( + "API error deleting database:", + error.status, + error.statusText + ); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to delete database" + }); + } + throw error; } } else { throw new TRPCError({ @@ -188,17 +222,20 @@ export const lineageDatabaseRouter = createTRPCRouter({ }); } } else { - const deleteRes = await fetch( - `https://api.turso.tech/v1/organizations/mikefreno/databases/${db_name}`, - { - method: "DELETE", - headers: { - Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}` + try { + const deleteRes = await fetchWithTimeout( + `https://api.turso.tech/v1/organizations/mikefreno/databases/${db_name}`, + { + method: "DELETE", + headers: { + Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}` + }, + timeout: 20000 } - } - ); + ); + + await checkResponse(deleteRes); - if (deleteRes.ok) { await conn.execute({ sql: `DELETE FROM User WHERE email = ?`, args: [email] @@ -208,11 +245,34 @@ export const lineageDatabaseRouter = createTRPCRouter({ status: 200, message: `Account and Database deleted` }; - } else { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Failed to delete database" - }); + } catch (error) { + if (error instanceof TimeoutError) { + console.error("Database deletion timeout:", error.message); + throw new TRPCError({ + code: "TIMEOUT", + message: "Database deletion timed out. Please contact support." + }); + } else if (error instanceof NetworkError) { + console.error( + "Network error during database deletion:", + error.message + ); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Network error deleting database. Please try again." + }); + } else if (error instanceof APIError) { + console.error( + "API error deleting database:", + error.status, + error.statusText + ); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to delete database" + }); + } + throw error; } } } else { @@ -343,41 +403,59 @@ export const lineageDatabaseRouter = createTRPCRouter({ }); if (dumpRes.success) { - const deleteRes = await fetch( - `https://api.turso.tech/v1/organizations/mikefreno/databases/${db_name}`, - { - method: "DELETE", - headers: { - Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}` + try { + const deleteRes = await fetchWithTimeout( + `https://api.turso.tech/v1/organizations/mikefreno/databases/${db_name}`, + { + method: "DELETE", + headers: { + Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}` + }, + timeout: 20000 } - } - ); + ); + + await checkResponse(deleteRes); - if (deleteRes.ok) { await conn.execute({ sql: `DELETE FROM User WHERE email = ?`, args: [email] }); executed_ids.push(id as number); + } catch (error) { + console.error( + `Failed to delete database ${db_name} in cron job:`, + error + ); + // Continue with other deletions even if one fails } } } else { - const deleteRes = await fetch( - `https://api.turso.tech/v1/organizations/mikefreno/databases/${db_name}`, - { - method: "DELETE", - headers: { - Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}` + try { + const deleteRes = await fetchWithTimeout( + `https://api.turso.tech/v1/organizations/mikefreno/databases/${db_name}`, + { + method: "DELETE", + headers: { + Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}` + }, + timeout: 20000 } - } - ); + ); + + await checkResponse(deleteRes); - if (deleteRes.ok) { await conn.execute({ sql: `DELETE FROM User WHERE email = ?`, args: [email] }); executed_ids.push(id as number); + } catch (error) { + console.error( + `Failed to delete database ${db_name} in cron job:`, + error + ); + // Continue with other deletions even if one fails } } } diff --git a/src/server/api/routers/misc.ts b/src/server/api/routers/misc.ts index c5c8322..7099638 100644 --- a/src/server/api/routers/misc.ts +++ b/src/server/api/routers/misc.ts @@ -12,7 +12,14 @@ import { TRPCError } from "@trpc/server"; import { ConnectionFactory } from "~/server/utils"; import * as bcrypt from "bcrypt"; import { getCookie, setCookie } from "vinxi/http"; - +import { + fetchWithTimeout, + checkResponse, + fetchWithRetry, + NetworkError, + TimeoutError, + APIError +} from "~/server/fetch-utils"; const assets: Record = { "shapes-with-abigail": "shapes-with-abigail.apk", "magic-delve": "magic-delve.apk", @@ -285,15 +292,27 @@ export const miscRouter = createTRPCRouter({ }; try { - await fetch(apiUrl, { - method: "POST", - headers: { - accept: "application/json", - "api-key": apiKey, - "content-type": "application/json" + await fetchWithRetry( + async () => { + const response = await fetchWithTimeout(apiUrl, { + method: "POST", + headers: { + accept: "application/json", + "api-key": apiKey, + "content-type": "application/json" + }, + body: JSON.stringify(sendinblueData), + timeout: 15000 + }); + + await checkResponse(response); + return response; }, - body: JSON.stringify(sendinblueData) - }); + { + maxRetries: 2, + retryDelay: 1000 + } + ); // Set cookie to prevent spam (60 second cooldown) const exp = new Date(Date.now() + 1 * 60 * 1000); @@ -304,11 +323,37 @@ export const miscRouter = createTRPCRouter({ return { message: "email sent" }; } catch (error) { - console.error(error); + // Provide specific error messages for different failure types + if (error instanceof TimeoutError) { + console.error("Contact form email timeout:", error.message); + throw new TRPCError({ + code: "TIMEOUT", + message: + "Email service timed out. Please try again or contact michael@freno.me" + }); + } else if (error instanceof NetworkError) { + console.error("Contact form network error:", error.message); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + "Network error. Please try again or contact michael@freno.me" + }); + } else if (error instanceof APIError) { + console.error( + "Contact form API error:", + error.status, + error.statusText + ); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Email service error. You can reach me at michael@freno.me" + }); + } + + console.error("Contact form error:", error); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", - message: - "SMTP server error: Sorry! You can reach me at michael@freno.me" + message: "Sorry! You can reach me at michael@freno.me" }); } }), @@ -362,26 +407,43 @@ export const miscRouter = createTRPCRouter({ }; try { - // Send both emails - await fetch(apiUrl, { - method: "POST", - headers: { - accept: "application/json", - "api-key": apiKey, - "content-type": "application/json" - }, - body: JSON.stringify(sendinblueMyData) - }); - - await fetch(apiUrl, { - method: "POST", - headers: { - accept: "application/json", - "api-key": apiKey, - "content-type": "application/json" - }, - body: JSON.stringify(sendinblueUserData) - }); + // Send both emails with retry logic + await Promise.all([ + fetchWithRetry( + async () => { + const response = await fetchWithTimeout(apiUrl, { + method: "POST", + headers: { + accept: "application/json", + "api-key": apiKey, + "content-type": "application/json" + }, + body: JSON.stringify(sendinblueMyData), + timeout: 15000 + }); + await checkResponse(response); + return response; + }, + { maxRetries: 2, retryDelay: 1000 } + ), + fetchWithRetry( + async () => { + const response = await fetchWithTimeout(apiUrl, { + method: "POST", + headers: { + accept: "application/json", + "api-key": apiKey, + "content-type": "application/json" + }, + body: JSON.stringify(sendinblueUserData), + timeout: 15000 + }); + await checkResponse(response); + return response; + }, + { maxRetries: 2, retryDelay: 1000 } + ) + ]); // Set cookie to prevent spam (60 second cooldown) const exp = new Date(Date.now() + 1 * 60 * 1000); @@ -392,11 +454,35 @@ export const miscRouter = createTRPCRouter({ return { message: "request sent" }; } catch (error) { - console.error(error); + // Provide specific error messages + if (error instanceof TimeoutError) { + console.error("Deletion request email timeout:", error.message); + throw new TRPCError({ + code: "TIMEOUT", + message: "Email service timed out. Please try again." + }); + } else if (error instanceof NetworkError) { + console.error("Deletion request network error:", error.message); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Network error. Please try again later." + }); + } else if (error instanceof APIError) { + console.error( + "Deletion request API error:", + error.status, + error.statusText + ); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Email service error. Please try again later." + }); + } + + console.error("Deletion request error:", error); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", - message: - "SMTP server error: Sorry! You can reach me at michael@freno.me" + message: "Failed to send deletion request. Please try again." }); } }) diff --git a/src/server/database.ts b/src/server/database.ts index 51f2dc2..7ff0ee9 100644 --- a/src/server/database.ts +++ b/src/server/database.ts @@ -4,6 +4,14 @@ import { v4 as uuid } from "uuid"; import { env } from "~/env/server"; import type { H3Event } from "vinxi/http"; import { getUserID } from "./auth"; +import { + fetchWithTimeout, + checkResponse, + fetchWithRetry, + NetworkError, + TimeoutError, + APIError +} from "~/server/fetch-utils"; let mainDBConnection: ReturnType | null = null; let lineageDBConnection: ReturnType | null = null; @@ -83,56 +91,90 @@ export async function dumpAndSendDB({ success: boolean; reason?: string; }> { - const res = await fetch(`https://${dbName}-mikefreno.turso.io/dump`, { - method: "GET", - headers: { - Authorization: `Bearer ${dbToken}` - } - }); - if (!res.ok) { - console.error(res); - return { success: false, reason: "bad dump request response" }; - } - const text = await res.text(); - const base64Content = Buffer.from(text, "utf-8").toString("base64"); - - const apiKey = env.SENDINBLUE_KEY as string; - const apiUrl = "https://api.brevo.com/v3/smtp/email"; - - const emailPayload = { - sender: { - name: "no_reply@freno.me", - email: "no_reply@freno.me" - }, - to: [ + try { + // Fetch database dump with timeout + const res = await fetchWithTimeout( + `https://${dbName}-mikefreno.turso.io/dump`, { - email: sendTarget + method: "GET", + headers: { + Authorization: `Bearer ${dbToken}` + }, + timeout: 30000 // 30s for database dump } - ], - subject: "Your Lineage Database Dump", - htmlContent: - "

Please find the attached database dump. This contains the state of your person remote Lineage remote saves. Should you ever return to Lineage, you can upload this file to reinstate the saves you had.

", - attachment: [ - { - content: base64Content, - name: "database_dump.txt" - } - ] - }; - const sendRes = await fetch(apiUrl, { - method: "POST", - headers: { - accept: "application/json", - "api-key": apiKey, - "content-type": "application/json" - }, - body: JSON.stringify(emailPayload) - }); + ); + + await checkResponse(res); + const text = await res.text(); + const base64Content = Buffer.from(text, "utf-8").toString("base64"); + + const apiKey = env.SENDINBLUE_KEY as string; + const apiUrl = "https://api.brevo.com/v3/smtp/email"; + + const emailPayload = { + sender: { + name: "no_reply@freno.me", + email: "no_reply@freno.me" + }, + to: [ + { + email: sendTarget + } + ], + subject: "Your Lineage Database Dump", + htmlContent: + "

Please find the attached database dump. This contains the state of your person remote Lineage remote saves. Should you ever return to Lineage, you can upload this file to reinstate the saves you had.

", + attachment: [ + { + content: base64Content, + name: "database_dump.txt" + } + ] + }; + + // Send email with retry logic + await fetchWithRetry( + async () => { + const sendRes = await fetchWithTimeout(apiUrl, { + method: "POST", + headers: { + accept: "application/json", + "api-key": apiKey, + "content-type": "application/json" + }, + body: JSON.stringify(emailPayload), + timeout: 20000 // 20s for email with attachment + }); + + await checkResponse(sendRes); + return sendRes; + }, + { + maxRetries: 2, + retryDelay: 2000 + } + ); - if (!sendRes.ok) { - return { success: false, reason: "email send failure" }; - } else { return { success: true }; + } catch (error) { + // Log specific error types for debugging + if (error instanceof TimeoutError) { + console.error("Database dump timeout:", error.message); + return { success: false, reason: "Database dump timed out" }; + } else if (error instanceof NetworkError) { + console.error("Network error during database dump:", error.message); + return { success: false, reason: "Network error" }; + } else if (error instanceof APIError) { + console.error( + "API error during database dump:", + error.status, + error.statusText + ); + return { success: false, reason: `API error: ${error.statusText}` }; + } + + console.error("Unexpected error during database dump:", error); + return { success: false, reason: "Unknown error occurred" }; } }