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}`;
};
// 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>({
links: [
// will print out helpful logs when using client (suppress 401 errors)
loggerLink({
enabled: (opts) => {
// Only log in development, and suppress 401 errors
const isDev = process.env.NODE_ENV === "development";
const is401 =
opts.direction === "down" &&
opts.result instanceof Error &&
opts.result.message?.includes("UNAUTHORIZED");
return isDev && !is401;
}
}),
// Only enable logging in development mode
...(process.env.NODE_ENV === "development"
? [
loggerLink({
enabled: (opts) => {
// Suppress 401 UNAUTHORIZED errors from logs
const is401 =
opts.direction === "down" &&
opts.result instanceof Error &&
opts.result.message?.includes("UNAUTHORIZED");
return !is401;
}
})
]
: []),
// identifies what url will handle trpc requests
httpBatchLink({
url: `${getBaseUrl()}/api/trpc`,
fetch: customFetch
url: `${getBaseUrl()}/api/trpc`
})
]
});

View File

@@ -3,6 +3,26 @@
* 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
* @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."
/>
<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">
<Typewriter speed={30} keepAlive={2000}>
<div class="text-4xl">Hey!</div>
@@ -45,44 +45,7 @@ export default function Home() {
</Typewriter>
<div class="pt-8 text-center">
<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">
{/* 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>
<div class="flex flex-col items-center gap-2 2xl:flex-row 2xl:items-start 2xl:justify-center">
{/* 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>My LÖVE UI library</div>
@@ -125,6 +88,43 @@ export default function Home() {
</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 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 {
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const { payload } = await jwtVerify(userIDToken, secret);
if (payload.id && typeof payload.id === "string") {
userId = payload.id;
privilegeLevel = payload.id === env.ADMIN_ID ? "admin" : "user";
}
} catch (err) {
console.log("Failed to authenticate token:", err);
// Clear invalid token
// Silently clear invalid token (401s are expected for non-authenticated users)
setCookie(event.nativeEvent, "userIDToken", "", {
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 {
event,
userId,
privilegeLevel,
privilegeLevel
};
}
@@ -59,24 +58,24 @@ const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
return next({
ctx: {
...ctx,
userId: ctx.userId, // userId is non-null here
},
userId: ctx.userId // userId is non-null here
}
});
});
// Middleware to enforce admin access
const enforceUserIsAdmin = t.middleware(({ ctx, next }) => {
if (ctx.privilegeLevel !== "admin") {
throw new TRPCError({
code: "FORBIDDEN",
message: "Admin access required"
throw new TRPCError({
code: "FORBIDDEN",
message: "Admin access required"
});
}
return next({
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";
}
} catch (err) {
console.log("Failed to authenticate token.");
// Silently clear invalid token (401s are expected for non-authenticated users)
setCookie(event, "userIDToken", "", {
maxAge: 0,
expires: new Date("2016-10-05")
@@ -45,7 +45,7 @@ export async function getUserID(event: H3Event): Promise<string | null> {
return payload.id;
}
} catch (err) {
console.log("Failed to authenticate token.");
// Silently clear invalid token (401s are expected for non-authenticated users)
setCookie(event, "userIDToken", "", {
maxAge: 0,
expires: new Date("2016-10-05")