1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2025-07-03 15:16:40 -04:00

Migrate to app router (#2254)

This commit is contained in:
2025-02-07 11:33:38 -05:00
committed by GitHub
parent e97613dda5
commit 8aabb4a66f
179 changed files with 4095 additions and 4951 deletions

96
app/api/contact/route.ts Normal file
View File

@ -0,0 +1,96 @@
import nodemailer from "nodemailer";
import fetcher from "../../../lib/helpers/fetcher";
import config from "../../../lib/config";
import { headers } from "next/headers";
import { NextResponse, NextRequest } from "next/server";
export async function POST(req: NextRequest): Promise<
NextResponse<{
success?: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
error?: any;
} | null>
> {
try {
// possible weirdness? https://github.com/orgs/vercel/discussions/78#discussioncomment-5089059
const data = await req.formData();
const headersList = await headers();
// these are both backups to client-side validations just in case someone squeezes through without them. the codes
// are identical so they're caught in the same fashion.
if (!data.get("name") || !data.get("email") || !data.get("message")) {
// all fields are required
throw new Error("missing_data");
}
if (
!data.get("cf-turnstile-response") ||
!(await validateCaptcha(
data.get("cf-turnstile-response"),
headersList.get("x-forwarded-for") || headersList.get("x-real-ip") || ""
))
) {
// either the captcha is wrong or completely missing
throw new Error("invalid_captcha");
}
// throw an internal error, not user's fault
if (!(await sendMessage(data))) {
throw new Error("nodemailer_error");
}
// success! let the client know
return NextResponse.json({ success: true }, { status: 201 });
} catch (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
error: any
) {
return NextResponse.json({ error: error.message ?? "Bad request." }, { status: 400 });
}
}
const validateCaptcha = async (formResponse: unknown, ip: string): Promise<unknown> => {
const response = await fetcher("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
// fallback to dummy secret for testing: https://developers.cloudflare.com/turnstile/troubleshooting/testing/
secret: process.env.TURNSTILE_SECRET_KEY || "1x0000000000000000000000000000000AA",
response: formResponse,
remoteip: ip,
}),
});
return response?.success;
};
const sendMessage = async (data: FormData): Promise<boolean> => {
try {
const transporter = nodemailer.createTransport({
// https://resend.com/docs/send-with-nodemailer-smtp
host: "smtp.resend.com",
secure: true,
port: 465,
auth: {
user: "resend",
pass: process.env.RESEND_API_KEY,
},
});
await transporter.sendMail({
from: `${data.get("name")} <${process.env.RESEND_DOMAIN ? `noreply@${process.env.RESEND_DOMAIN}` : "onboarding@resend.dev"}>`,
sender: `nodemailer <${process.env.RESEND_DOMAIN ? `noreply@${process.env.RESEND_DOMAIN}` : "onboarding@resend.dev"}>`,
replyTo: `${data.get("name")} <${data.get("email")}>`,
to: `<${config.authorEmail}>`,
subject: `[${config.siteDomain}] Contact Form Submission`,
// TODO: add markdown parsing as promised on the form.
text: `${data.get("message")}`,
});
} catch (error) {
console.error(error);
return false;
}
return true;
};

31
app/api/count/route.ts Normal file
View File

@ -0,0 +1,31 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "../../../lib/helpers/prisma";
import type { PageStats } from "../../../types";
export const dynamic = "force-dynamic";
export const revalidate = 0;
export async function GET(req: NextRequest): Promise<NextResponse<PageStats>> {
const slug = req.nextUrl.searchParams.get("slug");
// extremely basic input validation.
// TODO: actually check if the note exists before continuing (and allow pages other than notes).
if (typeof slug !== "string" || !new RegExp(/^notes\/([A-Za-z0-9-]+)$/i).test(slug)) {
// @ts-expect-error
return NextResponse.json({ error: "Missing or invalid 'slug' parameter." }, { status: 400 });
}
// +1 hit!
const { hits } = await prisma.hits.upsert({
where: { slug },
create: { slug },
update: {
hits: {
increment: 1,
},
},
});
// add one to this page's count and return the new number
return NextResponse.json({ hits });
}

26
app/api/hits/route.ts Normal file
View File

@ -0,0 +1,26 @@
import { NextResponse } from "next/server";
import { prisma } from "../../../lib/helpers/prisma";
import type { SiteStats } from "../../../types";
export const revalidate = 900; // 15 mins
export async function GET(): Promise<NextResponse<SiteStats>> {
// fetch all rows from db sorted by most hits
const pages = await prisma.hits.findMany({
orderBy: [
{
hits: "desc",
},
],
});
const total = { hits: 0 };
// calculate total hits
pages.forEach((page) => {
// add these hits to running tally
total.hits += page.hits;
});
return NextResponse.json({ total, pages });
}