pre-tiptap

This commit is contained in:
2025-12-19 12:04:18 -05:00
parent 324141441b
commit 3d55dab7b5
13 changed files with 555 additions and 409 deletions

View File

@@ -23,8 +23,6 @@ function AppLayout(props: { children: any }) {
centerWidth, centerWidth,
leftBarVisible, leftBarVisible,
rightBarVisible, rightBarVisible,
toggleLeftBar,
toggleRightBar,
setLeftBarVisible, setLeftBarVisible,
setRightBarVisible, setRightBarVisible,
barsInitialized barsInitialized

View File

@@ -156,6 +156,18 @@ export function LeftBar() {
} | null>(null); } | null>(null);
const [isMounted, setIsMounted] = createSignal(false); const [isMounted, setIsMounted] = createSignal(false);
const [signOutLoading, setSignOutLoading] = createSignal(false);
const handleSignOut = async () => {
setSignOutLoading(true);
try {
await api.auth.signOut.mutate();
window.location.href = "/";
} catch (error) {
console.error("Sign out failed:", error);
setSignOutLoading(false);
}
};
onMount(async () => { onMount(async () => {
// Mark as mounted to avoid hydration mismatch // Mark as mounted to avoid hydration mismatch
@@ -416,6 +428,17 @@ export function LeftBar() {
</a> </a>
</Show> </Show>
</li> </li>
<Show when={isMounted() && userInfo()?.isAuthenticated}>
<li class="hover:text-subtext0 w-fit transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold">
<button
onClick={handleSignOut}
disabled={signOutLoading()}
class="text-left disabled:opacity-50"
>
{signOutLoading() ? "Signing Out..." : "Sign Out"}
</button>
</li>
</Show>
</ul> </ul>
</Typewriter> </Typewriter>

View File

@@ -13,8 +13,6 @@ const BarsContext = createContext<{
setLeftBarVisible: (visible: boolean) => void; setLeftBarVisible: (visible: boolean) => void;
rightBarVisible: Accessor<boolean>; rightBarVisible: Accessor<boolean>;
setRightBarVisible: (visible: boolean) => void; setRightBarVisible: (visible: boolean) => void;
toggleLeftBar: () => void;
toggleRightBar: () => void;
barsInitialized: Accessor<boolean>; barsInitialized: Accessor<boolean>;
}>({ }>({
leftBarSize: () => 0, leftBarSize: () => 0,
@@ -27,8 +25,6 @@ const BarsContext = createContext<{
setLeftBarVisible: () => {}, setLeftBarVisible: () => {},
rightBarVisible: () => true, rightBarVisible: () => true,
setRightBarVisible: () => {}, setRightBarVisible: () => {},
toggleLeftBar: () => {},
toggleRightBar: () => {},
barsInitialized: () => false barsInitialized: () => false
}); });
@@ -112,10 +108,6 @@ export function BarsProvider(props: { children: any }) {
hapticFeedback(50); hapticFeedback(50);
_setRightBarVisible(visible); _setRightBarVisible(visible);
}; };
const toggleLeftBar = () => setLeftBarVisible(!leftBarVisible());
const toggleRightBar = () => setRightBarVisible(!rightBarVisible());
return ( return (
<BarsContext.Provider <BarsContext.Provider
value={{ value={{
@@ -129,8 +121,6 @@ export function BarsProvider(props: { children: any }) {
setLeftBarVisible, setLeftBarVisible,
rightBarVisible, rightBarVisible,
setRightBarVisible, setRightBarVisible,
toggleLeftBar,
toggleRightBar,
barsInitialized barsInitialized
}} }}
> >

View File

@@ -6,7 +6,6 @@ export default function Home() {
<Typewriter speed={30} keepAlive={2000}> <Typewriter speed={30} keepAlive={2000}>
<div class="text-4xl">Hey!</div> <div class="text-4xl">Hey!</div>
</Typewriter> </Typewriter>
<Typewriter speed={80} keepAlive={2000}> <Typewriter speed={80} keepAlive={2000}>
<div> <div>
My name is <span class="text-green">Mike Freno</span>, I'm a{" "} My name is <span class="text-green">Mike Freno</span>, I'm a{" "}
@@ -15,10 +14,16 @@ export default function Home() {
</div> </div>
</Typewriter> </Typewriter>
<Typewriter speed={100}> <Typewriter speed={100}>
I'm a passionate dev tooling and game developer, recently been working I'm a passionate dev tooling, game, and open source software developer.
in the world of Love2D and you can see some of my work here: <a></a> Recently been working in the world of{" "}
I'm a huge lover of open source software, and <a
href="https://www.love2d.org"
class="text-blue hover-underline-animation"
>
LÖVE
</a>{" "}
</Typewriter> </Typewriter>
You can see some of my work <a>here</a>(github)
<Typewriter speed={50} keepAlive={false}> <Typewriter speed={50} keepAlive={false}>
<div>My Collection of By-the-ways:</div> <div>My Collection of By-the-ways:</div>
</Typewriter> </Typewriter>

View File

@@ -9,7 +9,10 @@ import { setCookie, getCookie } from "vinxi/http";
import type { User } from "~/types/user"; import type { User } from "~/types/user";
// Helper to create JWT token // Helper to create JWT token
async function createJWT(userId: string, expiresIn: string = "14d"): Promise<string> { async function createJWT(
userId: string,
expiresIn: string = "14d"
): Promise<string> {
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY); const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const token = await new SignJWT({ id: userId }) const token = await new SignJWT({ id: userId })
.setProtectedHeader({ alg: "HS256" }) .setProtectedHeader({ alg: "HS256" })
@@ -26,11 +29,11 @@ async function sendEmail(to: string, subject: string, htmlContent: string) {
const sendinblueData = { const sendinblueData = {
sender: { sender: {
name: "freno.me", name: "freno.me",
email: "no_reply@freno.me", email: "no_reply@freno.me"
}, },
to: [{ email: to }], to: [{ email: to }],
htmlContent, htmlContent,
subject, subject
}; };
const response = await fetch(apiUrl, { const response = await fetch(apiUrl, {
@@ -38,9 +41,9 @@ async function sendEmail(to: string, subject: string, htmlContent: string) {
headers: { headers: {
accept: "application/json", accept: "application/json",
"api-key": apiKey, "api-key": apiKey,
"content-type": "application/json", "content-type": "application/json"
}, },
body: JSON.stringify(sendinblueData), body: JSON.stringify(sendinblueData)
}); });
if (!response.ok) { if (!response.ok) {
@@ -65,22 +68,22 @@ export const authRouter = createTRPCRouter({
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Accept: "application/json", Accept: "application/json"
}, },
body: JSON.stringify({ body: JSON.stringify({
client_id: env.VITE_GITHUB_CLIENT_ID || env.NEXT_PUBLIC_GITHUB_CLIENT_ID, client_id: env.VITE_GITHUB_CLIENT_ID,
client_secret: env.GITHUB_CLIENT_SECRET, client_secret: env.GITHUB_CLIENT_SECRET,
code, code
}), })
}, }
); );
const { access_token } = await tokenResponse.json(); const { access_token } = await tokenResponse.json();
// Fetch user info from GitHub // Fetch user info from GitHub
const userResponse = await fetch("https://api.github.com/user", { const userResponse = await fetch("https://api.github.com/user", {
headers: { headers: {
Authorization: `token ${access_token}`, Authorization: `token ${access_token}`
}, }
}); });
const user = await userResponse.json(); const user = await userResponse.json();
@@ -117,18 +120,18 @@ export const authRouter = createTRPCRouter({
path: "/", path: "/",
httpOnly: true, httpOnly: true,
secure: env.NODE_ENV === "production", secure: env.NODE_ENV === "production",
sameSite: "lax", sameSite: "lax"
}); });
return { return {
success: true, success: true,
redirectTo: "/account", redirectTo: "/account"
}; };
} catch (error) { } catch (error) {
console.error("GitHub authentication failed:", error); console.error("GitHub authentication failed:", error);
throw new TRPCError({ throw new TRPCError({
code: "INTERNAL_SERVER_ERROR", code: "INTERNAL_SERVER_ERROR",
message: "GitHub authentication failed", message: "GitHub authentication failed"
}); });
} }
}), }),
@@ -141,19 +144,22 @@ export const authRouter = createTRPCRouter({
try { try {
// Exchange code for access token // Exchange code for access token
const tokenResponse = await fetch("https://oauth2.googleapis.com/token", { const tokenResponse = await fetch(
"https://oauth2.googleapis.com/token",
{
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/x-www-form-urlencoded", "Content-Type": "application/x-www-form-urlencoded"
}, },
body: new URLSearchParams({ body: new URLSearchParams({
code: code, code: code,
client_id: env.VITE_GOOGLE_CLIENT_ID || env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || "", client_id: env.VITE_GOOGLE_CLIENT_ID || "",
client_secret: env.GOOGLE_CLIENT_SECRET, client_secret: env.GOOGLE_CLIENT_SECRET,
redirect_uri: `${env.VITE_DOMAIN || env.NEXT_PUBLIC_DOMAIN}/api/auth/callback/google`, redirect_uri: `${env.VITE_DOMAIN || "https://freno.me"}/api/auth/callback/google`,
grant_type: "authorization_code", grant_type: "authorization_code"
}), })
}); }
);
const { access_token } = await tokenResponse.json(); const { access_token } = await tokenResponse.json();
@@ -162,9 +168,9 @@ export const authRouter = createTRPCRouter({
"https://www.googleapis.com/oauth2/v3/userinfo", "https://www.googleapis.com/oauth2/v3/userinfo",
{ {
headers: { headers: {
Authorization: `Bearer ${access_token}`, Authorization: `Bearer ${access_token}`
}, }
}, }
); );
const userData = await userResponse.json(); const userData = await userResponse.json();
@@ -196,11 +202,11 @@ export const authRouter = createTRPCRouter({
email_verified, email_verified,
name, name,
"google", "google",
image, image
]; ];
await conn.execute({ await conn.execute({
sql: insertQuery, sql: insertQuery,
args: insertParams, args: insertParams
}); });
} }
@@ -213,18 +219,18 @@ export const authRouter = createTRPCRouter({
path: "/", path: "/",
httpOnly: true, httpOnly: true,
secure: env.NODE_ENV === "production", secure: env.NODE_ENV === "production",
sameSite: "lax", sameSite: "lax"
}); });
return { return {
success: true, success: true,
redirectTo: "/account", redirectTo: "/account"
}; };
} catch (error) { } catch (error) {
console.error("Google authentication failed:", error); console.error("Google authentication failed:", error);
throw new TRPCError({ throw new TRPCError({
code: "INTERNAL_SERVER_ERROR", code: "INTERNAL_SERVER_ERROR",
message: "Google authentication failed", message: "Google authentication failed"
}); });
} }
}), }),
@@ -235,8 +241,8 @@ export const authRouter = createTRPCRouter({
z.object({ z.object({
email: z.string().email(), email: z.string().email(),
token: z.string(), token: z.string(),
rememberMe: z.boolean().optional(), rememberMe: z.boolean().optional()
}), })
) )
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const { email, token, rememberMe } = input; const { email, token, rememberMe } = input;
@@ -250,7 +256,7 @@ export const authRouter = createTRPCRouter({
if (payload.email !== email) { if (payload.email !== email) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "Email mismatch", message: "Email mismatch"
}); });
} }
@@ -262,7 +268,7 @@ export const authRouter = createTRPCRouter({
if (!res.rows[0]) { if (!res.rows[0]) {
throw new TRPCError({ throw new TRPCError({
code: "NOT_FOUND", code: "NOT_FOUND",
message: "User not found", message: "User not found"
}); });
} }
@@ -276,7 +282,7 @@ export const authRouter = createTRPCRouter({
path: "/", path: "/",
httpOnly: true, httpOnly: true,
secure: env.NODE_ENV === "production", secure: env.NODE_ENV === "production",
sameSite: "lax", sameSite: "lax"
}; };
if (rememberMe) { if (rememberMe) {
@@ -284,11 +290,16 @@ export const authRouter = createTRPCRouter({
} }
// If rememberMe is false, cookie will be session-only (no maxAge) // If rememberMe is false, cookie will be session-only (no maxAge)
setCookie(ctx.event.nativeEvent, "userIDToken", userToken, cookieOptions); setCookie(
ctx.event.nativeEvent,
"userIDToken",
userToken,
cookieOptions
);
return { return {
success: true, success: true,
redirectTo: "/account", redirectTo: "/account"
}; };
} catch (error) { } catch (error) {
if (error instanceof TRPCError) { if (error instanceof TRPCError) {
@@ -297,7 +308,7 @@ export const authRouter = createTRPCRouter({
console.error("Email login failed:", error); console.error("Email login failed:", error);
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "Authentication failed", message: "Authentication failed"
}); });
} }
}), }),
@@ -307,8 +318,8 @@ export const authRouter = createTRPCRouter({
.input( .input(
z.object({ z.object({
email: z.string().email(), email: z.string().email(),
token: z.string(), token: z.string()
}), })
) )
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
const { email, token } = input; const { email, token } = input;
@@ -322,7 +333,7 @@ export const authRouter = createTRPCRouter({
if (payload.email !== email) { if (payload.email !== email) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "Email mismatch", message: "Email mismatch"
}); });
} }
@@ -333,7 +344,7 @@ export const authRouter = createTRPCRouter({
return { return {
success: true, success: true,
message: "Email verification success, you may close this window", message: "Email verification success, you may close this window"
}; };
} catch (error) { } catch (error) {
if (error instanceof TRPCError) { if (error instanceof TRPCError) {
@@ -342,7 +353,7 @@ export const authRouter = createTRPCRouter({
console.error("Email verification failed:", error); console.error("Email verification failed:", error);
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "Invalid token", message: "Invalid token"
}); });
} }
}), }),
@@ -353,8 +364,8 @@ export const authRouter = createTRPCRouter({
z.object({ z.object({
email: z.string().email(), email: z.string().email(),
password: z.string().min(8), password: z.string().min(8),
passwordConfirmation: z.string().min(8), passwordConfirmation: z.string().min(8)
}), })
) )
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const { email, password, passwordConfirmation } = input; const { email, password, passwordConfirmation } = input;
@@ -362,7 +373,7 @@ export const authRouter = createTRPCRouter({
if (password !== passwordConfirmation) { if (password !== passwordConfirmation) {
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: "passwordMismatch", message: "passwordMismatch"
}); });
} }
@@ -373,7 +384,7 @@ export const authRouter = createTRPCRouter({
try { try {
await conn.execute({ await conn.execute({
sql: "INSERT INTO User (id, email, password_hash, provider) VALUES (?, ?, ?, ?)", sql: "INSERT INTO User (id, email, password_hash, provider) VALUES (?, ?, ?, ?)",
args: [userId, email, passwordHash, "email"], args: [userId, email, passwordHash, "email"]
}); });
// Create JWT token // Create JWT token
@@ -385,7 +396,7 @@ export const authRouter = createTRPCRouter({
path: "/", path: "/",
httpOnly: true, httpOnly: true,
secure: env.NODE_ENV === "production", secure: env.NODE_ENV === "production",
sameSite: "lax", sameSite: "lax"
}); });
return { success: true, message: "success" }; return { success: true, message: "success" };
@@ -393,7 +404,7 @@ export const authRouter = createTRPCRouter({
console.error("Registration error:", e); console.error("Registration error:", e);
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: "duplicate", message: "duplicate"
}); });
} }
}), }),
@@ -404,8 +415,8 @@ export const authRouter = createTRPCRouter({
z.object({ z.object({
email: z.string().email(), email: z.string().email(),
password: z.string(), password: z.string(),
rememberMe: z.boolean().optional(), rememberMe: z.boolean().optional()
}), })
) )
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const { email, password, rememberMe } = input; const { email, password, rememberMe } = input;
@@ -413,13 +424,13 @@ export const authRouter = createTRPCRouter({
const conn = ConnectionFactory(); const conn = ConnectionFactory();
const res = await conn.execute({ const res = await conn.execute({
sql: "SELECT * FROM User WHERE email = ? AND provider = ?", sql: "SELECT * FROM User WHERE email = ? AND provider = ?",
args: [email, "email"], args: [email, "email"]
}); });
if (res.rows.length === 0) { if (res.rows.length === 0) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "no-match", message: "no-match"
}); });
} }
@@ -428,7 +439,7 @@ export const authRouter = createTRPCRouter({
if (!user.password_hash) { if (!user.password_hash) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "no-match", message: "no-match"
}); });
} }
@@ -437,7 +448,7 @@ export const authRouter = createTRPCRouter({
if (!passwordMatch) { if (!passwordMatch) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "no-match", message: "no-match"
}); });
} }
@@ -450,7 +461,7 @@ export const authRouter = createTRPCRouter({
path: "/", path: "/",
httpOnly: true, httpOnly: true,
secure: env.NODE_ENV === "production", secure: env.NODE_ENV === "production",
sameSite: "lax", sameSite: "lax"
}; };
if (rememberMe) { if (rememberMe) {
@@ -467,21 +478,24 @@ export const authRouter = createTRPCRouter({
.input( .input(
z.object({ z.object({
email: z.string().email(), email: z.string().email(),
rememberMe: z.boolean().optional(), rememberMe: z.boolean().optional()
}), })
) )
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const { email, rememberMe } = input; const { email, rememberMe } = input;
// Check rate limiting // Check rate limiting
const requested = getCookie(ctx.event.nativeEvent, "emailLoginLinkRequested"); const requested = getCookie(
ctx.event.nativeEvent,
"emailLoginLinkRequested"
);
if (requested) { if (requested) {
const expires = new Date(requested); const expires = new Date(requested);
const remaining = expires.getTime() - Date.now(); const remaining = expires.getTime() - Date.now();
if (remaining > 0) { if (remaining > 0) {
throw new TRPCError({ throw new TRPCError({
code: "TOO_MANY_REQUESTS", code: "TOO_MANY_REQUESTS",
message: "countdown not expired", message: "countdown not expired"
}); });
} }
} }
@@ -489,25 +503,28 @@ export const authRouter = createTRPCRouter({
const conn = ConnectionFactory(); const conn = ConnectionFactory();
const res = await conn.execute({ const res = await conn.execute({
sql: "SELECT * FROM User WHERE email = ?", sql: "SELECT * FROM User WHERE email = ?",
args: [email], args: [email]
}); });
if (res.rows.length === 0) { if (res.rows.length === 0) {
throw new TRPCError({ throw new TRPCError({
code: "NOT_FOUND", code: "NOT_FOUND",
message: "User not found", message: "User not found"
}); });
} }
// Create JWT token for email link (15min expiry) // Create JWT token for email link (15min expiry)
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY); const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const token = await new SignJWT({ email, rememberMe: rememberMe ?? false }) const token = await new SignJWT({
email,
rememberMe: rememberMe ?? false
})
.setProtectedHeader({ alg: "HS256" }) .setProtectedHeader({ alg: "HS256" })
.setExpirationTime("15m") .setExpirationTime("15m")
.sign(secret); .sign(secret);
// Send email // Send email
const domain = env.VITE_DOMAIN || env.NEXT_PUBLIC_DOMAIN; const domain = env.VITE_DOMAIN || "https://freno.me";
const htmlContent = `<html> const htmlContent = `<html>
<head> <head>
<style> <style>
@@ -550,10 +567,15 @@ export const authRouter = createTRPCRouter({
// Set rate limit cookie (2 minutes) // Set rate limit cookie (2 minutes)
const exp = new Date(Date.now() + 2 * 60 * 1000); const exp = new Date(Date.now() + 2 * 60 * 1000);
setCookie(ctx.event.nativeEvent, "emailLoginLinkRequested", exp.toUTCString(), { setCookie(
ctx.event.nativeEvent,
"emailLoginLinkRequested",
exp.toUTCString(),
{
maxAge: 2 * 60, maxAge: 2 * 60,
path: "/", path: "/"
}); }
);
return { success: true, message: "email sent" }; return { success: true, message: "email sent" };
}), }),
@@ -565,14 +587,17 @@ export const authRouter = createTRPCRouter({
const { email } = input; const { email } = input;
// Check rate limiting // Check rate limiting
const requested = getCookie(ctx.event.nativeEvent, "passwordResetRequested"); const requested = getCookie(
ctx.event.nativeEvent,
"passwordResetRequested"
);
if (requested) { if (requested) {
const expires = new Date(requested); const expires = new Date(requested);
const remaining = expires.getTime() - Date.now(); const remaining = expires.getTime() - Date.now();
if (remaining > 0) { if (remaining > 0) {
throw new TRPCError({ throw new TRPCError({
code: "TOO_MANY_REQUESTS", code: "TOO_MANY_REQUESTS",
message: "countdown not expired", message: "countdown not expired"
}); });
} }
} }
@@ -580,7 +605,7 @@ export const authRouter = createTRPCRouter({
const conn = ConnectionFactory(); const conn = ConnectionFactory();
const res = await conn.execute({ const res = await conn.execute({
sql: "SELECT * FROM User WHERE email = ?", sql: "SELECT * FROM User WHERE email = ?",
args: [email], args: [email]
}); });
if (res.rows.length === 0) { if (res.rows.length === 0) {
@@ -598,7 +623,7 @@ export const authRouter = createTRPCRouter({
.sign(secret); .sign(secret);
// Send email // Send email
const domain = env.VITE_DOMAIN || env.NEXT_PUBLIC_DOMAIN; const domain = env.VITE_DOMAIN || "https://freno.me";
const htmlContent = `<html> const htmlContent = `<html>
<head> <head>
<style> <style>
@@ -641,10 +666,15 @@ export const authRouter = createTRPCRouter({
// Set rate limit cookie (5 minutes) // Set rate limit cookie (5 minutes)
const exp = new Date(Date.now() + 5 * 60 * 1000); const exp = new Date(Date.now() + 5 * 60 * 1000);
setCookie(ctx.event.nativeEvent, "passwordResetRequested", exp.toUTCString(), { setCookie(
ctx.event.nativeEvent,
"passwordResetRequested",
exp.toUTCString(),
{
maxAge: 5 * 60, maxAge: 5 * 60,
path: "/", path: "/"
}); }
);
return { success: true, message: "email sent" }; return { success: true, message: "email sent" };
}), }),
@@ -655,8 +685,8 @@ export const authRouter = createTRPCRouter({
z.object({ z.object({
token: z.string(), token: z.string(),
newPassword: z.string().min(8), newPassword: z.string().min(8),
newPasswordConfirmation: z.string().min(8), newPasswordConfirmation: z.string().min(8)
}), })
) )
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const { token, newPassword, newPasswordConfirmation } = input; const { token, newPassword, newPasswordConfirmation } = input;
@@ -664,7 +694,7 @@ export const authRouter = createTRPCRouter({
if (newPassword !== newPasswordConfirmation) { if (newPassword !== newPasswordConfirmation) {
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: "Password Mismatch", message: "Password Mismatch"
}); });
} }
@@ -676,7 +706,7 @@ export const authRouter = createTRPCRouter({
if (!payload.id || typeof payload.id !== "string") { if (!payload.id || typeof payload.id !== "string") {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "bad token", message: "bad token"
}); });
} }
@@ -685,17 +715,17 @@ export const authRouter = createTRPCRouter({
await conn.execute({ await conn.execute({
sql: "UPDATE User SET password_hash = ? WHERE id = ?", sql: "UPDATE User SET password_hash = ? WHERE id = ?",
args: [passwordHash, payload.id], args: [passwordHash, payload.id]
}); });
// Clear any session cookies // Clear any session cookies
setCookie(ctx.event.nativeEvent, "emailToken", "", { setCookie(ctx.event.nativeEvent, "emailToken", "", {
maxAge: 0, maxAge: 0,
path: "/", path: "/"
}); });
setCookie(ctx.event.nativeEvent, "userIDToken", "", { setCookie(ctx.event.nativeEvent, "userIDToken", "", {
maxAge: 0, maxAge: 0,
path: "/", path: "/"
}); });
return { success: true, message: "success" }; return { success: true, message: "success" };
@@ -706,7 +736,7 @@ export const authRouter = createTRPCRouter({
console.error("Password reset error:", error); console.error("Password reset error:", error);
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "token expired", message: "token expired"
}); });
} }
}), }),
@@ -718,7 +748,10 @@ export const authRouter = createTRPCRouter({
const { email } = input; const { email } = input;
// Check rate limiting // Check rate limiting
const requested = getCookie(ctx.event.nativeEvent, "emailVerificationRequested"); const requested = getCookie(
ctx.event.nativeEvent,
"emailVerificationRequested"
);
if (requested) { if (requested) {
const time = parseInt(requested); const time = parseInt(requested);
const currentTime = Date.now(); const currentTime = Date.now();
@@ -727,7 +760,7 @@ export const authRouter = createTRPCRouter({
if (difference < 15) { if (difference < 15) {
throw new TRPCError({ throw new TRPCError({
code: "TOO_MANY_REQUESTS", code: "TOO_MANY_REQUESTS",
message: "Please wait before requesting another verification email", message: "Please wait before requesting another verification email"
}); });
} }
} }
@@ -735,13 +768,13 @@ export const authRouter = createTRPCRouter({
const conn = ConnectionFactory(); const conn = ConnectionFactory();
const res = await conn.execute({ const res = await conn.execute({
sql: "SELECT * FROM User WHERE email = ?", sql: "SELECT * FROM User WHERE email = ?",
args: [email], args: [email]
}); });
if (res.rows.length === 0) { if (res.rows.length === 0) {
throw new TRPCError({ throw new TRPCError({
code: "NOT_FOUND", code: "NOT_FOUND",
message: "User not found", message: "User not found"
}); });
} }
@@ -753,7 +786,7 @@ export const authRouter = createTRPCRouter({
.sign(secret); .sign(secret);
// Send email // Send email
const domain = env.VITE_DOMAIN || env.NEXT_PUBLIC_DOMAIN; const domain = env.VITE_DOMAIN || "https://freno.me";
const htmlContent = `<html> const htmlContent = `<html>
<head> <head>
<style> <style>
@@ -792,10 +825,15 @@ export const authRouter = createTRPCRouter({
await sendEmail(email, "freno.me email verification", htmlContent); await sendEmail(email, "freno.me email verification", htmlContent);
// Set rate limit cookie // Set rate limit cookie
setCookie(ctx.event.nativeEvent, "emailVerificationRequested", Date.now().toString(), { setCookie(
ctx.event.nativeEvent,
"emailVerificationRequested",
Date.now().toString(),
{
maxAge: 15 * 60, maxAge: 15 * 60,
path: "/", path: "/"
}); }
);
return { success: true, message: "Verification email sent" }; return { success: true, message: "Verification email sent" };
}), }),
@@ -804,13 +842,14 @@ export const authRouter = createTRPCRouter({
signOut: publicProcedure.mutation(async ({ ctx }) => { signOut: publicProcedure.mutation(async ({ ctx }) => {
setCookie(ctx.event.nativeEvent, "userIDToken", "", { setCookie(ctx.event.nativeEvent, "userIDToken", "", {
maxAge: 0, maxAge: 0,
path: "/", path: "/"
}); });
setCookie(ctx.event.nativeEvent, "emailToken", "", { setCookie(ctx.event.nativeEvent, "emailToken", "", {
maxAge: 0, maxAge: 0,
path: "/", path: "/"
}); });
return { success: true }; return { success: true };
}), })
}); });

View File

@@ -1,5 +1,6 @@
import { createTRPCRouter, publicProcedure } from "../utils"; import { createTRPCRouter, publicProcedure } from "../utils";
import { ConnectionFactory } from "~/server/utils"; import { ConnectionFactory } from "~/server/utils";
import { withCache } from "~/server/cache";
// Simple in-memory cache for blog posts to reduce DB load // Simple in-memory cache for blog posts to reduce DB load
let cachedPosts: { let cachedPosts: {
@@ -11,6 +12,7 @@ let cacheTimestamp: number = 0;
export const blogRouter = createTRPCRouter({ export const blogRouter = createTRPCRouter({
getRecentPosts: publicProcedure.query(async () => { getRecentPosts: publicProcedure.query(async () => {
return withCache("recent-posts", 10 * 60 * 1000, async () => {
// Get database connection // Get database connection
const conn = ConnectionFactory(); const conn = ConnectionFactory();
@@ -39,6 +41,7 @@ export const blogRouter = createTRPCRouter({
const results = await conn.execute(query); const results = await conn.execute(query);
return results.rows; return results.rows;
});
}), }),
getPosts: publicProcedure.query(async ({ ctx }) => { getPosts: publicProcedure.query(async ({ ctx }) => {

View File

@@ -264,7 +264,7 @@ export const databaseRouter = createTRPCRouter({
try { try {
const conn = ConnectionFactory(); const conn = ConnectionFactory();
const fullURL = input.banner_photo const fullURL = input.banner_photo
? env.NEXT_PUBLIC_AWS_BUCKET_STRING + input.banner_photo ? env.VITE_AWS_BUCKET_STRING + input.banner_photo
: null; : null;
const query = ` const query = `
@@ -346,7 +346,7 @@ export const databaseRouter = createTRPCRouter({
if (input.banner_photo === "_DELETE_IMAGE_") { if (input.banner_photo === "_DELETE_IMAGE_") {
params.push(null); params.push(null);
} else { } else {
params.push(env.NEXT_PUBLIC_AWS_BUCKET_STRING + input.banner_photo); params.push(env.VITE_AWS_BUCKET_STRING + input.banner_photo);
} }
first = false; first = false;
} }
@@ -593,7 +593,7 @@ export const databaseRouter = createTRPCRouter({
try { try {
const conn = ConnectionFactory(); const conn = ConnectionFactory();
const fullURL = input.imageURL const fullURL = input.imageURL
? env.NEXT_PUBLIC_AWS_BUCKET_STRING + input.imageURL ? env.VITE_AWS_BUCKET_STRING + input.imageURL
: null; : null;
const query = `UPDATE User SET image = ? WHERE id = ?`; const query = `UPDATE User SET image = ? WHERE id = ?`;
await conn.execute({ await conn.execute({

View File

@@ -1,6 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import { createTRPCRouter, publicProcedure } from "../utils"; import { createTRPCRouter, publicProcedure } from "../utils";
import { env } from "~/env/server"; import { env } from "~/env/server";
import { withCache } from "~/server/cache";
// Types for commits // Types for commits
interface GitCommit { interface GitCommit {
@@ -22,6 +23,10 @@ export const gitActivityRouter = createTRPCRouter({
getGitHubCommits: publicProcedure getGitHubCommits: publicProcedure
.input(z.object({ limit: z.number().default(3) })) .input(z.object({ limit: z.number().default(3) }))
.query(async ({ input }) => { .query(async ({ input }) => {
return withCache(
`github-commits-${input.limit}`,
10 * 60 * 1000, // 10 minutes
async () => {
try { try {
// Get user's repositories sorted by most recently pushed // Get user's repositories sorted by most recently pushed
const reposResponse = await fetch( const reposResponse = await fetch(
@@ -69,13 +74,15 @@ export const gitActivityRouter = createTRPCRouter({
allCommits.push({ allCommits.push({
sha: commit.sha?.substring(0, 7) || "unknown", sha: commit.sha?.substring(0, 7) || "unknown",
message: message:
commit.commit?.message?.split("\n")[0] || "No message", commit.commit?.message?.split("\n")[0] ||
"No message",
author: author:
commit.commit?.author?.name || commit.commit?.author?.name ||
commit.author?.login || commit.author?.login ||
"Unknown", "Unknown",
date: date:
commit.commit?.author?.date || new Date().toISOString(), commit.commit?.author?.date ||
new Date().toISOString(),
repo: repo.full_name, repo: repo.full_name,
url: `https://github.com/${repo.full_name}/commit/${commit.sha}` url: `https://github.com/${repo.full_name}/commit/${commit.sha}`
}); });
@@ -100,12 +107,18 @@ export const gitActivityRouter = createTRPCRouter({
console.error("Error fetching GitHub commits:", error); console.error("Error fetching GitHub commits:", error);
return []; return [];
} }
}
);
}), }),
// Get recent commits from Gitea // Get recent commits from Gitea
getGiteaCommits: publicProcedure getGiteaCommits: publicProcedure
.input(z.object({ limit: z.number().default(3) })) .input(z.object({ limit: z.number().default(3) }))
.query(async ({ input }) => { .query(async ({ input }) => {
return withCache(
`gitea-commits-${input.limit}`,
10 * 60 * 1000, // 10 minutes
async () => {
try { try {
// First, get user's repositories // First, get user's repositories
const reposResponse = await fetch( const reposResponse = await fetch(
@@ -119,7 +132,9 @@ export const gitActivityRouter = createTRPCRouter({
); );
if (!reposResponse.ok) { if (!reposResponse.ok) {
throw new Error(`Gitea repos API error: ${reposResponse.statusText}`); throw new Error(
`Gitea repos API error: ${reposResponse.statusText}`
);
} }
const repos = await reposResponse.json(); const repos = await reposResponse.json();
@@ -145,7 +160,9 @@ export const gitActivityRouter = createTRPCRouter({
for (const commit of commits) { for (const commit of commits) {
if ( if (
(commit.commit?.author?.email && (commit.commit?.author?.email &&
commit.commit.author.email.includes("michael@freno.me")) || commit.commit.author.email.includes(
"michael@freno.me"
)) ||
commit.commit.author.email.includes( commit.commit.author.email.includes(
"michaelt.freno@gmail.com" "michaelt.freno@gmail.com"
) // Filter for your commits ) // Filter for your commits
@@ -153,10 +170,12 @@ export const gitActivityRouter = createTRPCRouter({
allCommits.push({ allCommits.push({
sha: commit.sha?.substring(0, 7) || "unknown", sha: commit.sha?.substring(0, 7) || "unknown",
message: message:
commit.commit?.message?.split("\n")[0] || "No message", commit.commit?.message?.split("\n")[0] ||
"No message",
author: commit.commit?.author?.name || repo.owner.login, author: commit.commit?.author?.name || repo.owner.login,
date: date:
commit.commit?.author?.date || new Date().toISOString(), commit.commit?.author?.date ||
new Date().toISOString(),
repo: repo.full_name, repo: repo.full_name,
url: `${env.GITEA_URL}/${repo.full_name}/commit/${commit.sha}` url: `${env.GITEA_URL}/${repo.full_name}/commit/${commit.sha}`
}); });
@@ -164,7 +183,10 @@ export const gitActivityRouter = createTRPCRouter({
} }
} }
} catch (error) { } catch (error) {
console.error(`Error fetching commits for ${repo.name}:`, error); console.error(
`Error fetching commits for ${repo.name}:`,
error
);
} }
} }
@@ -178,10 +200,13 @@ export const gitActivityRouter = createTRPCRouter({
console.error("Error fetching Gitea commits:", error); console.error("Error fetching Gitea commits:", error);
return []; return [];
} }
}
);
}), }),
// Get GitHub contribution activity (for heatmap) // Get GitHub contribution activity (for heatmap)
getGitHubActivity: publicProcedure.query(async () => { getGitHubActivity: publicProcedure.query(async () => {
return withCache("github-activity", 10 * 60 * 1000, async () => {
try { try {
// Use GitHub GraphQL API for contribution data // Use GitHub GraphQL API for contribution data
const query = ` const query = `
@@ -227,8 +252,8 @@ export const gitActivityRouter = createTRPCRouter({
// Extract contribution days from the response // Extract contribution days from the response
const contributions: ContributionDay[] = []; const contributions: ContributionDay[] = [];
const weeks = const weeks =
data.data?.user?.contributionsCollection?.contributionCalendar?.weeks || data.data?.user?.contributionsCollection?.contributionCalendar
[]; ?.weeks || [];
for (const week of weeks) { for (const week of weeks) {
for (const day of week.contributionDays) { for (const day of week.contributionDays) {
@@ -244,10 +269,12 @@ export const gitActivityRouter = createTRPCRouter({
console.error("Error fetching GitHub activity:", error); console.error("Error fetching GitHub activity:", error);
return []; return [];
} }
});
}), }),
// Get Gitea contribution activity (for heatmap) // Get Gitea contribution activity (for heatmap)
getGiteaActivity: publicProcedure.query(async () => { getGiteaActivity: publicProcedure.query(async () => {
return withCache("gitea-activity", 10 * 60 * 1000, async () => {
try { try {
// Get user's repositories // Get user's repositories
const reposResponse = await fetch( const reposResponse = await fetch(
@@ -310,5 +337,6 @@ export const gitActivityRouter = createTRPCRouter({
console.error("Error fetching Gitea activity:", error); console.error("Error fetching Gitea activity:", error);
return []; return [];
} }
});
}) })
}); });

View File

@@ -1,14 +1,14 @@
import { createTRPCRouter, publicProcedure } from "../../utils";
import { z } from "zod"; import { z } from "zod";
import { import {
LineageConnectionFactory, LineageConnectionFactory,
validateLineageRequest, validateLineageRequest,
dumpAndSendDB, dumpAndSendDB
} from "~/server/utils"; } from "~/server/utils";
import { env } from "~/env/server"; import { env } from "~/env/server";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { OAuth2Client } from "google-auth-library"; import { OAuth2Client } from "google-auth-library";
import { jwtVerify } from "jose"; import { jwtVerify } from "jose";
import { createTRPCRouter, publicProcedure } from "~/server/api/utils";
export const lineageDatabaseRouter = createTRPCRouter({ export const lineageDatabaseRouter = createTRPCRouter({
credentials: publicProcedure credentials: publicProcedure
@@ -16,7 +16,7 @@ export const lineageDatabaseRouter = createTRPCRouter({
z.object({ z.object({
email: z.string().email(), email: z.string().email(),
provider: z.enum(["email", "google", "apple"]), provider: z.enum(["email", "google", "apple"]),
authToken: z.string(), authToken: z.string()
}) })
) )
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
@@ -32,17 +32,17 @@ export const lineageDatabaseRouter = createTRPCRouter({
valid_request = true; valid_request = true;
} }
} else if (provider === "google") { } else if (provider === "google") {
const CLIENT_ID = env.VITE_GOOGLE_CLIENT_ID_MAGIC_DELVE || env.NEXT_PUBLIC_GOOGLE_CLIENT_ID_MAGIC_DELVE; const CLIENT_ID = env.VITE_GOOGLE_CLIENT_ID_MAGIC_DELVE;
if (!CLIENT_ID) { if (!CLIENT_ID) {
throw new TRPCError({ throw new TRPCError({
code: "INTERNAL_SERVER_ERROR", code: "INTERNAL_SERVER_ERROR",
message: "Google client ID not configured", message: "Google client ID not configured"
}); });
} }
const client = new OAuth2Client(CLIENT_ID); const client = new OAuth2Client(CLIENT_ID);
const ticket = await client.verifyIdToken({ const ticket = await client.verifyIdToken({
idToken: authToken, idToken: authToken,
audience: CLIENT_ID, audience: CLIENT_ID
}); });
if (ticket.getPayload()?.email === email) { if (ticket.getPayload()?.email === email) {
valid_request = true; valid_request = true;
@@ -67,25 +67,25 @@ export const lineageDatabaseRouter = createTRPCRouter({
return { return {
success: true, success: true,
db_name: user.database_name as string, db_name: user.database_name as string,
db_token: user.database_token as string, db_token: user.database_token as string
}; };
} }
throw new TRPCError({ throw new TRPCError({
code: "NOT_FOUND", code: "NOT_FOUND",
message: "No user found", message: "No user found"
}); });
} else { } else {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "Invalid credentials", message: "Invalid credentials"
}); });
} }
} catch (error) { } catch (error) {
if (error instanceof TRPCError) throw error; if (error instanceof TRPCError) throw error;
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "Authentication failed", message: "Authentication failed"
}); });
} }
}), }),
@@ -98,35 +98,42 @@ export const lineageDatabaseRouter = createTRPCRouter({
db_token: z.string(), db_token: z.string(),
authToken: z.string(), authToken: z.string(),
skip_cron: z.boolean().optional(), skip_cron: z.boolean().optional(),
send_dump_target: z.string().email().optional(), send_dump_target: z.string().email().optional()
}) })
) )
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
const { email, db_name, db_token, authToken, skip_cron, send_dump_target } = input; const {
email,
db_name,
db_token,
authToken,
skip_cron,
send_dump_target
} = input;
const conn = LineageConnectionFactory(); const conn = LineageConnectionFactory();
const res = await conn.execute({ const res = await conn.execute({
sql: `SELECT * FROM User WHERE email = ?`, sql: `SELECT * FROM User WHERE email = ?`,
args: [email], args: [email]
}); });
const userRow = res.rows[0]; const userRow = res.rows[0];
if (!userRow) { if (!userRow) {
throw new TRPCError({ throw new TRPCError({
code: "NOT_FOUND", code: "NOT_FOUND",
message: "User not found", message: "User not found"
}); });
} }
const valid = await validateLineageRequest({ const valid = await validateLineageRequest({
auth_token: authToken, auth_token: authToken,
userRow, userRow
}); });
if (!valid) { if (!valid) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "Invalid Verification", message: "Invalid Verification"
}); });
} }
@@ -135,7 +142,7 @@ export const lineageDatabaseRouter = createTRPCRouter({
if (database_token !== db_token || database_name !== db_name) { if (database_token !== db_token || database_name !== db_name) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "Incorrect Verification", message: "Incorrect Verification"
}); });
} }
@@ -144,7 +151,7 @@ export const lineageDatabaseRouter = createTRPCRouter({
const dumpRes = await dumpAndSendDB({ const dumpRes = await dumpAndSendDB({
dbName: db_name, dbName: db_name,
dbToken: db_token, dbToken: db_token,
sendTarget: send_dump_target, sendTarget: send_dump_target
}); });
if (dumpRes.success) { if (dumpRes.success) {
@@ -153,31 +160,31 @@ export const lineageDatabaseRouter = createTRPCRouter({
{ {
method: "DELETE", method: "DELETE",
headers: { headers: {
Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`, Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`
}, }
} }
); );
if (deleteRes.ok) { if (deleteRes.ok) {
await conn.execute({ await conn.execute({
sql: `DELETE FROM User WHERE email = ?`, sql: `DELETE FROM User WHERE email = ?`,
args: [email], args: [email]
}); });
return { return {
ok: true, ok: true,
status: 200, status: 200,
message: `Account and Database deleted, db dump sent to email: ${send_dump_target}`, message: `Account and Database deleted, db dump sent to email: ${send_dump_target}`
}; };
} else { } else {
throw new TRPCError({ throw new TRPCError({
code: "INTERNAL_SERVER_ERROR", code: "INTERNAL_SERVER_ERROR",
message: "Failed to delete database", message: "Failed to delete database"
}); });
} }
} else { } else {
throw new TRPCError({ throw new TRPCError({
code: "INTERNAL_SERVER_ERROR", code: "INTERNAL_SERVER_ERROR",
message: dumpRes.reason || "Failed to dump database", message: dumpRes.reason || "Failed to dump database"
}); });
} }
} else { } else {
@@ -186,44 +193,44 @@ export const lineageDatabaseRouter = createTRPCRouter({
{ {
method: "DELETE", method: "DELETE",
headers: { headers: {
Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`, Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`
}, }
} }
); );
if (deleteRes.ok) { if (deleteRes.ok) {
await conn.execute({ await conn.execute({
sql: `DELETE FROM User WHERE email = ?`, sql: `DELETE FROM User WHERE email = ?`,
args: [email], args: [email]
}); });
return { return {
ok: true, ok: true,
status: 200, status: 200,
message: `Account and Database deleted`, message: `Account and Database deleted`
}; };
} else { } else {
throw new TRPCError({ throw new TRPCError({
code: "INTERNAL_SERVER_ERROR", code: "INTERNAL_SERVER_ERROR",
message: "Failed to delete database", message: "Failed to delete database"
}); });
} }
} }
} else { } else {
const insertRes = await conn.execute({ const insertRes = await conn.execute({
sql: `INSERT INTO cron (email, db_name, db_token, send_dump_target) VALUES (?, ?, ?, ?)`, sql: `INSERT INTO cron (email, db_name, db_token, send_dump_target) VALUES (?, ?, ?, ?)`,
args: [email, db_name, db_token, send_dump_target], args: [email, db_name, db_token, send_dump_target]
}); });
if (insertRes.rowsAffected > 0) { if (insertRes.rowsAffected > 0) {
return { return {
ok: true, ok: true,
status: 200, status: 200,
message: `Deletion scheduled.`, message: `Deletion scheduled.`
}; };
} else { } else {
throw new TRPCError({ throw new TRPCError({
code: "INTERNAL_SERVER_ERROR", code: "INTERNAL_SERVER_ERROR",
message: `Deletion not scheduled, due to server failure`, message: `Deletion not scheduled, due to server failure`
}); });
} }
} }
@@ -238,7 +245,7 @@ export const lineageDatabaseRouter = createTRPCRouter({
try { try {
const res = await conn.execute({ const res = await conn.execute({
sql: `SELECT * FROM cron WHERE email = ?`, sql: `SELECT * FROM cron WHERE email = ?`,
args: [email], args: [email]
}); });
const cronRow = res.rows[0]; const cronRow = res.rows[0];
@@ -249,12 +256,12 @@ export const lineageDatabaseRouter = createTRPCRouter({
return { return {
ok: true, ok: true,
status: 200, status: 200,
created_at: cronRow.created_at as string, created_at: cronRow.created_at as string
}; };
} catch (e) { } catch (e) {
throw new TRPCError({ throw new TRPCError({
code: "INTERNAL_SERVER_ERROR", code: "INTERNAL_SERVER_ERROR",
message: "Failed to check deletion status", message: "Failed to check deletion status"
}); });
} }
}), }),
@@ -263,7 +270,7 @@ export const lineageDatabaseRouter = createTRPCRouter({
.input( .input(
z.object({ z.object({
email: z.string().email(), email: z.string().email(),
authToken: z.string(), authToken: z.string()
}) })
) )
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
@@ -273,13 +280,13 @@ export const lineageDatabaseRouter = createTRPCRouter({
const resUser = await conn.execute({ const resUser = await conn.execute({
sql: `SELECT * FROM User WHERE email = ?;`, sql: `SELECT * FROM User WHERE email = ?;`,
args: [email], args: [email]
}); });
if (resUser.rows.length === 0) { if (resUser.rows.length === 0) {
throw new TRPCError({ throw new TRPCError({
code: "NOT_FOUND", code: "NOT_FOUND",
message: "User not found.", message: "User not found."
}); });
} }
@@ -287,31 +294,31 @@ export const lineageDatabaseRouter = createTRPCRouter({
const valid = await validateLineageRequest({ const valid = await validateLineageRequest({
auth_token: authToken, auth_token: authToken,
userRow, userRow
}); });
if (!valid) { if (!valid) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "Invalid credentials for cancelation.", message: "Invalid credentials for cancelation."
}); });
} }
const result = await conn.execute({ const result = await conn.execute({
sql: `DELETE FROM cron WHERE email = ?;`, sql: `DELETE FROM cron WHERE email = ?;`,
args: [email], args: [email]
}); });
if (result.rowsAffected > 0) { if (result.rowsAffected > 0) {
return { return {
status: 200, status: 200,
ok: true, ok: true,
message: "Cron job(s) canceled successfully.", message: "Cron job(s) canceled successfully."
}; };
} else { } else {
throw new TRPCError({ throw new TRPCError({
code: "NOT_FOUND", code: "NOT_FOUND",
message: "No cron job found for the given email.", message: "No cron job found for the given email."
}); });
} }
}), }),
@@ -332,7 +339,7 @@ export const lineageDatabaseRouter = createTRPCRouter({
const dumpRes = await dumpAndSendDB({ const dumpRes = await dumpAndSendDB({
dbName: db_name as string, dbName: db_name as string,
dbToken: db_token as string, dbToken: db_token as string,
sendTarget: send_dump_target as string, sendTarget: send_dump_target as string
}); });
if (dumpRes.success) { if (dumpRes.success) {
@@ -341,15 +348,15 @@ export const lineageDatabaseRouter = createTRPCRouter({
{ {
method: "DELETE", method: "DELETE",
headers: { headers: {
Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`, Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`
}, }
} }
); );
if (deleteRes.ok) { if (deleteRes.ok) {
await conn.execute({ await conn.execute({
sql: `DELETE FROM User WHERE email = ?`, sql: `DELETE FROM User WHERE email = ?`,
args: [email], args: [email]
}); });
executed_ids.push(id as number); executed_ids.push(id as number);
} }
@@ -360,15 +367,15 @@ export const lineageDatabaseRouter = createTRPCRouter({
{ {
method: "DELETE", method: "DELETE",
headers: { headers: {
Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`, Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`
}, }
} }
); );
if (deleteRes.ok) { if (deleteRes.ok) {
await conn.execute({ await conn.execute({
sql: `DELETE FROM User WHERE email = ?`, sql: `DELETE FROM User WHERE email = ?`,
args: [email], args: [email]
}); });
executed_ids.push(id as number); executed_ids.push(id as number);
} }
@@ -383,11 +390,11 @@ export const lineageDatabaseRouter = createTRPCRouter({
return { return {
status: 200, status: 200,
message: message:
"Processed databases deleted and corresponding cron rows removed.", "Processed databases deleted and corresponding cron rows removed."
}; };
} }
} }
return { status: 200, ok: true }; return { status: 200, ok: true };
}), })
}); });

View File

@@ -12,11 +12,11 @@ export async function getPrivilegeLevel(
if (userIDToken) { if (userIDToken) {
try { try {
const secret = new TextEncoder().encode(env().JWT_SECRET_KEY); const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const { payload } = await jwtVerify(userIDToken, secret); const { payload } = await jwtVerify(userIDToken, secret);
if (payload.id && typeof payload.id === "string") { if (payload.id && typeof payload.id === "string") {
return payload.id === env().ADMIN_ID ? "admin" : "user"; return payload.id === env.ADMIN_ID ? "admin" : "user";
} }
} catch (err) { } catch (err) {
console.log("Failed to authenticate token."); console.log("Failed to authenticate token.");
@@ -38,7 +38,7 @@ export async function getUserID(event: H3Event): Promise<string | null> {
if (userIDToken) { if (userIDToken) {
try { try {
const secret = new TextEncoder().encode(env().JWT_SECRET_KEY); const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const { payload } = await jwtVerify(userIDToken, secret); const { payload } = await jwtVerify(userIDToken, secret);
if (payload.id && typeof payload.id === "string") { if (payload.id && typeof payload.id === "string") {
@@ -79,7 +79,7 @@ export async function validateLineageRequest({
const { provider, email } = userRow; const { provider, email } = userRow;
if (provider === "email") { if (provider === "email") {
try { try {
const secret = new TextEncoder().encode(env().JWT_SECRET_KEY); const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const { payload } = await jwtVerify(auth_token, secret); const { payload } = await jwtVerify(auth_token, secret);
if (email !== payload.email) { if (email !== payload.email) {
return false; return false;

54
src/server/cache.ts Normal file
View File

@@ -0,0 +1,54 @@
interface CacheEntry<T> {
data: T;
timestamp: number;
}
class SimpleCache {
private cache: Map<string, CacheEntry<any>> = new Map();
get<T>(key: string, ttlMs: number): T | null {
const entry = this.cache.get(key);
if (!entry) return null;
const now = Date.now();
if (now - entry.timestamp > ttlMs) {
this.cache.delete(key);
return null;
}
return entry.data as T;
}
set<T>(key: string, data: T): void {
this.cache.set(key, {
data,
timestamp: Date.now()
});
}
clear(): void {
this.cache.clear();
}
delete(key: string): void {
this.cache.delete(key);
}
}
export const cache = new SimpleCache();
// Helper function to wrap async operations with caching
export async function withCache<T>(
key: string,
ttlMs: number,
fn: () => Promise<T>
): Promise<T> {
const cached = cache.get<T>(key, ttlMs);
if (cached !== null) {
return cached;
}
const result = await fn();
cache.set(key, result);
return result;
}

View File

@@ -10,8 +10,8 @@ let lineageDBConnection: ReturnType<typeof createClient> | null = null;
export function ConnectionFactory() { export function ConnectionFactory() {
if (!mainDBConnection) { if (!mainDBConnection) {
const config = { const config = {
url: env().TURSO_DB_URL, url: env.TURSO_DB_URL,
authToken: env().TURSO_DB_TOKEN authToken: env.TURSO_DB_TOKEN
}; };
mainDBConnection = createClient(config); mainDBConnection = createClient(config);
} }
@@ -21,8 +21,8 @@ export function ConnectionFactory() {
export function LineageConnectionFactory() { export function LineageConnectionFactory() {
if (!lineageDBConnection) { if (!lineageDBConnection) {
const config = { const config = {
url: env().TURSO_LINEAGE_URL, url: env.TURSO_LINEAGE_URL,
authToken: env().TURSO_LINEAGE_TOKEN authToken: env.TURSO_LINEAGE_TOKEN
}; };
lineageDBConnection = createClient(config); lineageDBConnection = createClient(config);
} }
@@ -32,7 +32,7 @@ export function LineageConnectionFactory() {
export async function LineageDBInit() { export async function LineageDBInit() {
const turso = createAPIClient({ const turso = createAPIClient({
org: "mikefreno", org: "mikefreno",
token: env().TURSO_DB_API_TOKEN token: env.TURSO_DB_API_TOKEN
}); });
const db_name = uuid(); const db_name = uuid();
@@ -95,7 +95,7 @@ export async function dumpAndSendDB({
const text = await res.text(); const text = await res.text();
const base64Content = Buffer.from(text, "utf-8").toString("base64"); const base64Content = Buffer.from(text, "utf-8").toString("base64");
const apiKey = env().SENDINBLUE_KEY as string; const apiKey = env.SENDINBLUE_KEY as string;
const apiUrl = "https://api.brevo.com/v3/smtp/email"; const apiUrl = "https://api.brevo.com/v3/smtp/email";
const emailPayload = { const emailPayload = {

View File

@@ -8,17 +8,16 @@ export async function sendEmailVerification(userEmail: string): Promise<{
messageId?: string; messageId?: string;
message?: string; message?: string;
}> { }> {
const apiKey = env().SENDINBLUE_KEY; const apiKey = env.SENDINBLUE_KEY;
const apiUrl = "https://api.brevo.com/v3/smtp/email"; const apiUrl = "https://api.brevo.com/v3/smtp/email";
const secret = new TextEncoder().encode(env().JWT_SECRET_KEY); const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const token = await new SignJWT({ email: userEmail }) const token = await new SignJWT({ email: userEmail })
.setProtectedHeader({ alg: "HS256" }) .setProtectedHeader({ alg: "HS256" })
.setExpirationTime("15m") .setExpirationTime("15m")
.sign(secret); .sign(secret);
const domain = const domain = env.VITE_DOMAIN || "https://freno.me";
env().VITE_DOMAIN || env().NEXT_PUBLIC_DOMAIN || "https://freno.me";
const emailPayload = { const emailPayload = {
sender: { sender: {