Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions components/RequireAuth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,36 @@
import React, { useEffect, useState } from 'react';
import { getRedirectUri, normalizeAuth0Audience, normalizeAuth0ClientId, normalizeAuth0Domain } from '../lib/auth';

const AUTH_STATUS_KEY = 'auth.status';
const AUTH_TOKEN_KEY = 'auth.token';

const persistAuthState = (
state: { authed: boolean; userId?: string | null; token?: string | null },
): void => {
if (typeof window === 'undefined') return;

try {
const payload = {
authed: state.authed,
updatedAt: Date.now(),
...(state.userId ? { userId: state.userId } : {}),
};
window.localStorage.setItem(AUTH_STATUS_KEY, JSON.stringify(payload));
} catch {
// Ignore localStorage failures so auth flow can continue
}

try {
if (state.authed && state.token) {
window.localStorage.setItem(AUTH_TOKEN_KEY, state.token);
} else {
window.localStorage.removeItem(AUTH_TOKEN_KEY);
}
} catch {
// Ignore token storage errors
}
};

const authEnabled = (() => {
const raw = process.env.NEXT_PUBLIC_ENABLE_AUTH;
const isProduction = process.env.NODE_ENV === 'production';
Expand Down Expand Up @@ -79,6 +109,26 @@ export default function RequireAuth({ children }: { children: React.ReactNode })
}

const isAuthed = await client.isAuthenticated();
let userId: string | null = null;
let token: string | null = null;

if (isAuthed) {
try {
const user = await client.getUser();
userId = user?.sub ?? null;
} catch {
userId = null;
}

try {
token = await client.getTokenSilently({ detailedResponse: false } as any);
} catch {
token = null;
}
}

persistAuthState({ authed: isAuthed, userId, token });

if (!cancelled) {
setAuthed(isAuthed);
setReady(true);
Expand All @@ -92,6 +142,7 @@ export default function RequireAuth({ children }: { children: React.ReactNode })
if (!cancelled) {
setError(e?.message || 'Auth error');
setReady(true);
persistAuthState({ authed: false });
// Fallback: send back to home
window.location.replace('/');
}
Expand Down
28 changes: 23 additions & 5 deletions hooks/useRavenRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export function useRavenRequest({
const [typing, setTyping] = useState(false);
const abortRef = useRef<AbortController | null>(null);

const commitError = useCallback(
const commitError = useCallback(
(messageId: string, message: string) => {
const friendly = formatFriendlyErrorMessage(message);
setMessages((prev) =>
Expand All @@ -85,6 +85,17 @@ export function useRavenRequest({
[setMessages],
);

const getAuthToken = useCallback((): string | null => {
if (typeof window === "undefined") return null;
try {
const token = window.localStorage.getItem("auth.token");
if (typeof token === "string" && token.trim()) return token;
} catch {
// Ignore token read failures
}
return null;
}, []);

const updateMessage = useCallback(
(messageId: string, updates: Partial<Message>) => {
setMessages((prev) =>
Expand Down Expand Up @@ -190,14 +201,21 @@ export function useRavenRequest({
mergedPayload = { ...mergedPayload, persona: personaMode };
}

const headers: Record<string, string> = {
"Content-Type": "application/json",
"X-Request-Id": generateId(),
};

const token = getAuthToken();
if (token) {
headers.Authorization = `Bearer ${token}`;
}
Comment on lines +209 to +212

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Refresh auth tokens before Raven requests

The Authorization header for every Raven call is populated from whatever value happens to be in localStorage (getAuthToken), but that token is only fetched once during the initial Auth0 bootstrap in RequireAuth (lines 111‑130) and is never refreshed. Auth0 access tokens expire (typically ~1h) and app/api/raven/route.ts rejects expired tokens via verifyToken, so any user who keeps the chat open past the token lifetime will start getting 401s even though the UI still marks them as authenticated. Without re‑calling getTokenSilently or another refresh path before setting this header, long-running sessions will break until the page is reloaded.

Useful? React with 👍 / 👎.


const response = await fetchWithRetry(
"/api/raven",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Request-Id": generateId(),
},
headers,
body: JSON.stringify(mergedPayload),
signal: controller.signal,
},
Expand Down
Loading