mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
fix: dashboard stuck on infinite 'Loading...' spinner
Three bugs in src/hooks/use-auth.ts that caused the dashboard layout's
auth loading state to never resolve:
1. fetchProfile queried profiles.id = userId, but profiles.id is the
profile's own UUID. The correct column is profiles.user_id (FK to
auth.users.id). This meant no profile was ever found and — combined
with .single() throwing on 0 rows — could propagate errors.
2. No try/catch/finally around supabase.auth.getUser(). If getUser()
threw (e.g. during auth lock contention), setLoading(false) never
ran and the spinner showed forever with no visible error.
3. useEffect had supabase and fetchProfile in its deps. Since
createClient() previously returned a new instance each call,
both deps changed every render and the effect re-ran forever,
subscribing to onAuthStateChange repeatedly and racing getUser()
calls against the auth lock.
Changes:
- fetchProfile now uses .eq("user_id", userId) and .maybeSingle() so
a missing profile returns { data: null } instead of throwing.
- init() wrapped in try/catch/finally — setLoading(false) ALWAYS runs.
- 10-second safety timer clears the loading state if anything hangs.
- useEffect runs once on mount (createClient is a singleton; deps
intentionally empty).
- Logs Supabase error fields explicitly (message/details/hint/code)
instead of the raw object which serializes as {}.
- AuthSessionMissingError (normal for logged-out users) is suppressed
from error logs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
658d070811
commit
7d7deb5e0b
@ -16,43 +16,79 @@ export function useAuth() {
|
||||
const [profile, setProfile] = useState<Profile | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const supabase = createClient();
|
||||
let mounted = true;
|
||||
|
||||
const fetchProfile = useCallback(
|
||||
async (userId: string) => {
|
||||
const { data } = await supabase
|
||||
// Safety net: if something hangs, surface it instead of an infinite spinner.
|
||||
const safetyTimer = setTimeout(() => {
|
||||
if (mounted) {
|
||||
console.warn("[useAuth] getUser() timed out after 10s, clearing loading state");
|
||||
setLoading(false);
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
const fetchProfile = async (userId: string) => {
|
||||
try {
|
||||
// profiles.user_id (NOT profiles.id) references auth.users.id
|
||||
const { data, error } = await supabase
|
||||
.from("profiles")
|
||||
.select("id, full_name, email, avatar_url")
|
||||
.eq("id", userId)
|
||||
.single();
|
||||
.eq("user_id", userId)
|
||||
.maybeSingle();
|
||||
|
||||
if (data) {
|
||||
if (error) {
|
||||
console.error("[useAuth] fetchProfile error:", {
|
||||
message: error.message,
|
||||
details: error.details,
|
||||
hint: error.hint,
|
||||
code: error.code,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (data && mounted) {
|
||||
setProfile(data);
|
||||
}
|
||||
},
|
||||
[supabase]
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("[useAuth] fetchProfile threw:", err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const getUser = async () => {
|
||||
const init = async () => {
|
||||
try {
|
||||
const {
|
||||
data: { user },
|
||||
error,
|
||||
} = await supabase.auth.getUser();
|
||||
|
||||
if (error) {
|
||||
// AuthSessionMissingError is expected when not logged in — don't log as error
|
||||
if (error.name !== "AuthSessionMissingError") {
|
||||
console.error("[useAuth] getUser error:", error.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
setUser(user);
|
||||
|
||||
if (user) {
|
||||
await fetchProfile(user.id);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
console.error("[useAuth] init threw:", err);
|
||||
} finally {
|
||||
if (mounted) setLoading(false);
|
||||
clearTimeout(safetyTimer);
|
||||
}
|
||||
};
|
||||
|
||||
getUser();
|
||||
init();
|
||||
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange(async (event, session) => {
|
||||
} = supabase.auth.onAuthStateChange(async (_event, session) => {
|
||||
if (!mounted) return;
|
||||
const currentUser = session?.user ?? null;
|
||||
setUser(currentUser);
|
||||
|
||||
@ -66,16 +102,21 @@ export function useAuth() {
|
||||
});
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
clearTimeout(safetyTimer);
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [supabase, fetchProfile]);
|
||||
// Intentionally run once on mount — createClient() is a singleton
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const signOut = useCallback(async () => {
|
||||
const supabase = createClient();
|
||||
await supabase.auth.signOut();
|
||||
setUser(null);
|
||||
setProfile(null);
|
||||
window.location.href = "/login";
|
||||
}, [supabase]);
|
||||
}, []);
|
||||
|
||||
return { user, profile, loading, signOut };
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user