From 23b9a5b2943d6ff9d1e895caed7d0bc6e52f6a5a Mon Sep 17 00:00:00 2001 From: SergiioB Date: Thu, 17 Sep 2026 21:44:15 +0200 Subject: [PATCH 1/2] Resolve profile handles case-insensitively in /u/ URLs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Handles are stored lowercase (profiles_handle_format requires handle = lower(handle), and the auth trigger lowercases the GitHub login), but the profile page matched the URL param with exact string equality. Every internal link uses the stored lowercase handle, so only a URL typed or shared with GitHub's casing breaks — and it breaks with a bare 'No such user.' for an account that exists. Lowercase the route param before querying and redirect mixed-case URLs to the canonical lowercase path, preserving the tab query. No adapter change needed: the /u/:handle param is the only user-supplied handle in the API surface. Repro on production: /u/SergiioB 404s while /u/sergiiob renders. --- frontend/src/pages/Profile.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx index 8642555..9f9f1df 100644 --- a/frontend/src/pages/Profile.tsx +++ b/frontend/src/pages/Profile.tsx @@ -1,4 +1,4 @@ -import { Link, useParams, useSearchParams } from 'react-router-dom' +import { Link, Navigate, useParams, useSearchParams } from 'react-router-dom' import { ExternalLink, GitFork } from 'lucide-react' import { Skeleton } from '@/components/ui/skeleton' import { UserAvatar } from '@/components/UserAvatar' @@ -33,11 +33,17 @@ export default function Profile() { const [sp, setSp] = useSearchParams() const tab = (sp.get('tab') as Tab) || 'rigs' const cat = useCatalog() + // Handles are stored lowercase, but profile URLs are often typed or shared with + // GitHub's casing (e.g. /u/SergiioB). Resolve against the lowercase handle and + // canonicalize the address bar without dropping the tab query. + const canonical = handle.toLowerCase() const data = useAsync( - () => Promise.all([api.user(handle), api.userRigs(handle), api.userResults(handle), api.customRuntimes({ owner: handle })]), - [handle], + () => Promise.all([api.user(canonical), api.userRigs(canonical), api.userResults(canonical), api.customRuntimes({ owner: canonical })]), + [canonical], ) - usePageTitle(handle ? `@${handle}` : 'Profile') + usePageTitle(canonical ? `@${canonical}` : 'Profile') + if (canonical !== handle) + return if (data.error) return ( From af97c6399e3cd717e17c490ba742f7d3b67f5b52 Mon Sep 17 00:00:00 2001 From: SergiioB Date: Thu, 17 Sep 2026 21:59:17 +0200 Subject: [PATCH 2/2] Derive the session handle the same way the database does MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit toAppUser built the session user's handle from GitHub auth metadata (user_name), which keeps GitHub's original casing, while the auth trigger stores profile handles lowercase (handle = lower(handle)). Handles are compared with exact string equality everywhere, so the mismatch broke two things for anyone whose GitHub login has capitals: - TopNav "My rigs"/"My results" linked to /u/SergiioB, which the profile page 404s (the first report of this bug). - The submit form filtered rigs with owner = session handle, matched nothing, showed "You have no rigs yet. Create the first one." and auto-opened the rig creator — inviting duplicate rigs on every visit. Lowercase the handle in toAppUser, mirroring the trigger's lower(coalesce(user_name, preferred_username, email prefix, id)). api.me() already returned the stored profile handle; the session user now agrees with it. --- frontend/src/lib/auth.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/lib/auth.ts b/frontend/src/lib/auth.ts index 368cdeb..219da7c 100644 --- a/frontend/src/lib/auth.ts +++ b/frontend/src/lib/auth.ts @@ -26,7 +26,10 @@ function metadataString(user: SupabaseUser, ...keys: string[]): string | undefin } export function toAppUser(user: SupabaseUser): User { - const handle = metadataString(user, 'user_name', 'preferred_username') ?? user.email?.split('@')[0] ?? user.id + // The auth trigger stores profile handles lowercase (profiles_handle_format), while GitHub's + // user_name keeps its original casing. Handles are compared exactly everywhere (profile routes, + // owner filters), so the session handle must be derived the same way the trigger does. + const handle = (metadataString(user, 'user_name', 'preferred_username') ?? user.email?.split('@')[0] ?? user.id).toLowerCase() return { id: user.id,