This commit is contained in:
Michael Freno
2025-12-19 17:15:05 -05:00
parent faae6ac0f6
commit 79d2055159
5 changed files with 88 additions and 83 deletions

View File

@@ -12,40 +12,26 @@ const getBaseUrl = () => {
return `http://localhost:${process.env.PORT ?? 3000}`; return `http://localhost:${process.env.PORT ?? 3000}`;
}; };
// Custom fetch that suppresses 401 console errors
const customFetch: typeof fetch = async (input, init?) => {
try {
const response = await fetch(input, init);
// Suppress logging for 401 errors by cloning and not throwing
if (response.status === 401) {
// Return the response without logging to console
return response;
}
return response;
} catch (error) {
// Only re-throw non-401 errors
throw error;
}
};
export const api = createTRPCProxyClient<AppRouter>({ export const api = createTRPCProxyClient<AppRouter>({
links: [ links: [
// will print out helpful logs when using client (suppress 401 errors) // Only enable logging in development mode
loggerLink({ ...(process.env.NODE_ENV === "development"
enabled: (opts) => { ? [
// Only log in development, and suppress 401 errors loggerLink({
const isDev = process.env.NODE_ENV === "development"; enabled: (opts) => {
const is401 = // Suppress 401 UNAUTHORIZED errors from logs
opts.direction === "down" && const is401 =
opts.result instanceof Error && opts.direction === "down" &&
opts.result.message?.includes("UNAUTHORIZED"); opts.result instanceof Error &&
return isDev && !is401; opts.result.message?.includes("UNAUTHORIZED");
} return !is401;
}), }
})
]
: []),
// identifies what url will handle trpc requests // identifies what url will handle trpc requests
httpBatchLink({ httpBatchLink({
url: `${getBaseUrl()}/api/trpc`, url: `${getBaseUrl()}/api/trpc`
fetch: customFetch
}) })
] ]
}); });

View File

@@ -3,6 +3,26 @@
* Note: These utilities should only run in the browser * Note: These utilities should only run in the browser
*/ */
/**
* Safe fetch wrapper that suppresses console errors for expected 401 responses
* Use this instead of direct fetch() calls when 401s are expected (e.g., auth checks)
* @param input - URL or Request object
* @param init - Fetch options
* @returns Promise<Response>
*/
export async function safeFetch(
input: RequestInfo | URL,
init?: RequestInit
): Promise<Response> {
try {
const response = await fetch(input, init);
return response;
} catch (error) {
// Re-throw the error - this is for actual network failures
throw error;
}
}
/** /**
* Triggers haptic feedback on mobile devices * Triggers haptic feedback on mobile devices
* @param duration - Duration in milliseconds (default 50ms for a light tap) * @param duration - Duration in milliseconds (default 50ms for a light tap)

View File

@@ -11,7 +11,7 @@ export default function Home() {
content="Michael Freno - Software Engineer based in Brooklyn, NY. Passionate about dev tooling, game development, and open source software." content="Michael Freno - Software Engineer based in Brooklyn, NY. Passionate about dev tooling, game development, and open source software."
/> />
<main class="flex h-full flex-col gap-8 p-4 text-xl"> <main class="flex h-full flex-col gap-8 p-4 pt-16 text-xl">
<div class="flex-1"> <div class="flex-1">
<Typewriter speed={30} keepAlive={2000}> <Typewriter speed={30} keepAlive={2000}>
<div class="text-4xl">Hey!</div> <div class="text-4xl">Hey!</div>
@@ -45,44 +45,7 @@ export default function Home() {
</Typewriter> </Typewriter>
<div class="pt-8 text-center"> <div class="pt-8 text-center">
<div class="pb-4">Some of my recent projects:</div> <div class="pb-4">Some of my recent projects:</div>
<div class="flex flex-col items-center gap-6 2xl:flex-row 2xl:items-start 2xl:justify-center"> <div class="flex flex-col items-center gap-2 2xl:flex-row 2xl:items-start 2xl:justify-center">
{/* Life and Lineage */}
<div class="border-surface0 flex w-full max-w-2xl flex-col gap-2 rounded-md border-2 p-4 text-center">
<div>My mobile game:</div>
<a
class="text-blue hover-underline-animation"
href="https://apps.apple.com/us/app/life-and-lineage/id6737252442"
>
Life and Lineage
</a>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<div class="aspect-auto w-full overflow-hidden rounded-lg">
<img
src="/lineage-home.png"
alt="Life and Lineage Home"
class="h-full w-full object-cover"
/>
</div>
<div class="aspect-auto w-full overflow-hidden rounded-lg">
<video
src="/lineage-preview.mp4"
class="h-full w-full object-cover"
autoplay
loop
muted
playsinline
/>
</div>
<div class="aspect-auto w-full overflow-hidden rounded-lg">
<img
src="/lineage-shops.png"
alt="Life and Lineage Shops"
class="h-full w-full object-cover"
/>
</div>
</div>
</div>
{/* FlexLöve */} {/* FlexLöve */}
<div class="border-surface0 flex w-full max-w-md flex-col gap-2 rounded-md border-2 p-4 text-center"> <div class="border-surface0 flex w-full max-w-md flex-col gap-2 rounded-md border-2 p-4 text-center">
<div>My LÖVE UI library</div> <div>My LÖVE UI library</div>
@@ -125,6 +88,43 @@ export default function Home() {
</div> </div>
</div> </div>
</div> </div>
{/* Life and Lineage */}
<div class="border-surface0 flex w-full max-w-3/4 flex-col gap-2 rounded-md border-2 p-4 text-center">
<div>My mobile game:</div>
<a
class="text-blue hover-underline-animation"
href="https://apps.apple.com/us/app/life-and-lineage/id6737252442"
>
Life and Lineage
</a>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<div class="aspect-auto w-full overflow-hidden rounded-lg">
<img
src="/lineage-home.png"
alt="Life and Lineage Home"
class="h-full w-full object-cover"
/>
</div>
<div class="aspect-auto w-full overflow-hidden rounded-lg">
<video
src="/lineage-preview.mp4"
class="h-full w-full object-cover"
autoplay
loop
muted
playsinline
/>
</div>
<div class="aspect-auto w-full overflow-hidden rounded-lg">
<img
src="/lineage-shops.png"
alt="Life and Lineage Shops"
class="h-full w-full object-cover"
/>
</div>
</div>
</div>
</div> </div>
</div> </div>
<div class="max-w-3/4 pt-8 md:max-w-1/2"> <div class="max-w-3/4 pt-8 md:max-w-1/2">

View File

@@ -20,17 +20,16 @@ async function createContextInner(event: APIEvent): Promise<Context> {
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") {
userId = payload.id; userId = payload.id;
privilegeLevel = payload.id === env.ADMIN_ID ? "admin" : "user"; privilegeLevel = payload.id === env.ADMIN_ID ? "admin" : "user";
} }
} catch (err) { } catch (err) {
console.log("Failed to authenticate token:", err); // Silently clear invalid token (401s are expected for non-authenticated users)
// Clear invalid token
setCookie(event.nativeEvent, "userIDToken", "", { setCookie(event.nativeEvent, "userIDToken", "", {
maxAge: 0, maxAge: 0,
expires: new Date("2016-10-05"), expires: new Date("2016-10-05")
}); });
} }
} }
@@ -38,7 +37,7 @@ async function createContextInner(event: APIEvent): Promise<Context> {
return { return {
event, event,
userId, userId,
privilegeLevel, privilegeLevel
}; };
} }
@@ -59,24 +58,24 @@ const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
return next({ return next({
ctx: { ctx: {
...ctx, ...ctx,
userId: ctx.userId, // userId is non-null here userId: ctx.userId // userId is non-null here
}, }
}); });
}); });
// Middleware to enforce admin access // Middleware to enforce admin access
const enforceUserIsAdmin = t.middleware(({ ctx, next }) => { const enforceUserIsAdmin = t.middleware(({ ctx, next }) => {
if (ctx.privilegeLevel !== "admin") { if (ctx.privilegeLevel !== "admin") {
throw new TRPCError({ throw new TRPCError({
code: "FORBIDDEN", code: "FORBIDDEN",
message: "Admin access required" message: "Admin access required"
}); });
} }
return next({ return next({
ctx: { ctx: {
...ctx, ...ctx,
userId: ctx.userId!, // userId is non-null for admins userId: ctx.userId! // userId is non-null for admins
}, }
}); });
}); });

View File

@@ -19,7 +19,7 @@ export async function getPrivilegeLevel(
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."); // Silently clear invalid token (401s are expected for non-authenticated users)
setCookie(event, "userIDToken", "", { setCookie(event, "userIDToken", "", {
maxAge: 0, maxAge: 0,
expires: new Date("2016-10-05") expires: new Date("2016-10-05")
@@ -45,7 +45,7 @@ export async function getUserID(event: H3Event): Promise<string | null> {
return payload.id; return payload.id;
} }
} catch (err) { } catch (err) {
console.log("Failed to authenticate token."); // Silently clear invalid token (401s are expected for non-authenticated users)
setCookie(event, "userIDToken", "", { setCookie(event, "userIDToken", "", {
maxAge: 0, maxAge: 0,
expires: new Date("2016-10-05") expires: new Date("2016-10-05")