feat: Implement proactive access token refreshing within the session and reactive token renewal for socket authentication errors.
This commit is contained in:
@@ -65,22 +65,31 @@ export function useMessaging(options: UseMessagingOptions = {}): UseMessagingRet
|
||||
const currentConversationIdRef = useRef<string | null>(null);
|
||||
const isInitializedRef = useRef(false);
|
||||
|
||||
// Get access token from session
|
||||
const accessToken = session?.user?.accessToken;
|
||||
// Use localStorage token (same source as REST API, always fresh via interceptor)
|
||||
const getAccessToken = useCallback(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
return localStorage.getItem('accessToken');
|
||||
}
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
// Check if user is authenticated (session exists)
|
||||
const isAuthenticated = !!session?.user;
|
||||
|
||||
// Connect to WebSocket
|
||||
const connect = useCallback(async () => {
|
||||
if (!accessToken) {
|
||||
const token = getAccessToken();
|
||||
if (!token) {
|
||||
console.warn('No access token available for socket connection');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await socketService.connect(accessToken);
|
||||
await socketService.connect(token);
|
||||
} catch (error) {
|
||||
console.error('Failed to connect to socket:', error);
|
||||
}
|
||||
}, [accessToken]);
|
||||
}, [getAccessToken]);
|
||||
|
||||
// Disconnect from WebSocket
|
||||
const disconnect = useCallback(() => {
|
||||
@@ -89,7 +98,7 @@ export function useMessaging(options: UseMessagingOptions = {}): UseMessagingRet
|
||||
|
||||
// Load conversations
|
||||
const loadConversations = useCallback(async () => {
|
||||
if (!accessToken) return;
|
||||
if (!getAccessToken()) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
@@ -100,7 +109,7 @@ export function useMessaging(options: UseMessagingOptions = {}): UseMessagingRet
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [accessToken]);
|
||||
}, [getAccessToken]);
|
||||
|
||||
// Select a conversation and load its messages
|
||||
const selectConversation = useCallback(
|
||||
@@ -404,6 +413,39 @@ export function useMessaging(options: UseMessagingOptions = {}): UseMessagingRet
|
||||
}
|
||||
);
|
||||
|
||||
// Handle auth errors (token expired, server disconnected us)
|
||||
const unsubscribeAuthError = socketService.onAuthError(async () => {
|
||||
console.log('Socket auth error detected, refreshing token via REST...');
|
||||
try {
|
||||
const refreshToken = localStorage.getItem('refreshToken');
|
||||
if (!refreshToken) {
|
||||
console.error('No refresh token available for socket reconnection');
|
||||
return;
|
||||
}
|
||||
// Refresh token using the same endpoint as the REST API interceptor
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api/v1'}/auth/refresh`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ refreshToken }),
|
||||
}
|
||||
);
|
||||
const data = await res.json();
|
||||
if (res.ok && data.success) {
|
||||
const newToken = data.data.accessToken;
|
||||
localStorage.setItem('accessToken', newToken);
|
||||
localStorage.setItem('refreshToken', data.data.refreshToken);
|
||||
console.log('Token refreshed, reconnecting socket...');
|
||||
socketService.reconnectWithToken(newToken);
|
||||
} else {
|
||||
console.error('Token refresh failed:', data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh token for socket reconnection:', error);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribeConnection();
|
||||
unsubscribeMessage();
|
||||
@@ -411,12 +453,13 @@ export function useMessaging(options: UseMessagingOptions = {}): UseMessagingRet
|
||||
unsubscribeTypingStop();
|
||||
unsubscribeStatus();
|
||||
unsubscribeRead();
|
||||
unsubscribeAuthError();
|
||||
};
|
||||
}, [updateUserStatus]);
|
||||
|
||||
// Auto-connect on mount
|
||||
useEffect(() => {
|
||||
if (autoConnect && accessToken && !isInitializedRef.current) {
|
||||
if (autoConnect && isAuthenticated && !isInitializedRef.current) {
|
||||
isInitializedRef.current = true;
|
||||
connect();
|
||||
loadConversations();
|
||||
@@ -424,11 +467,8 @@ export function useMessaging(options: UseMessagingOptions = {}): UseMessagingRet
|
||||
// Get initial unread count
|
||||
messagesService.getUnreadCount().then(setUnreadCount).catch(console.error);
|
||||
}
|
||||
|
||||
// Don't disconnect on cleanup - socket is singleton and should stay connected
|
||||
// Disconnect only happens when user logs out (handled elsewhere)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [autoConnect, accessToken]);
|
||||
}, [autoConnect, isAuthenticated]);
|
||||
|
||||
// Re-join conversation room when reconnecting
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user