diff --git a/src/components/apis/authApiClients.tsx b/src/components/apis/authApiClients.tsx index 4e95edb..083bf1e 100644 --- a/src/components/apis/authApiClients.tsx +++ b/src/components/apis/authApiClients.tsx @@ -129,13 +129,19 @@ const addTokenRefreshInterceptor = (client: typeof authApiClient) => { // No refresh token available clearTokens(); safeLocalStorage.removeItem('codenuk_user'); - window.location.href = '/signin'; + // Prevent redirect loops by checking current location + if (typeof window !== 'undefined' && !window.location.pathname.includes('/signin')) { + window.location.href = '/signin?error=Please sign in to continue.'; + } return Promise.reject(error); } catch (refreshError) { console.error('Token refresh failed:', refreshError); clearTokens(); safeLocalStorage.removeItem('codenuk_user'); - window.location.href = '/signin'; + // Prevent redirect loops by checking current location + if (typeof window !== 'undefined' && !window.location.pathname.includes('/signin')) { + window.location.href = '/signin?error=Session expired. Please sign in again.'; + } return Promise.reject(refreshError); } } diff --git a/src/components/auth/signin-form.tsx b/src/components/auth/signin-form.tsx index 542fe4e..b49b6fa 100644 --- a/src/components/auth/signin-form.tsx +++ b/src/components/auth/signin-form.tsx @@ -48,11 +48,17 @@ export function SignInForm({ }: SignInFormProps) { // Persist for refresh localStorage.setItem("codenuk_user", JSON.stringify(response.data.user)) - // Redirect based on user role - if (response.data.user.role === 'admin') { - router.push("/admin") - } else { - router.push("/") + // Redirect based on user role with error handling + try { + if (response.data.user.role === 'admin') { + router.push("/admin") + } else { + router.push("/") + } + } catch (redirectError) { + console.error('Redirect failed:', redirectError) + // Fallback redirect + window.location.href = response.data.user.role === 'admin' ? '/admin' : '/' } } else { setError("Invalid response from server. Please try again.") diff --git a/src/components/auth/signup-form.tsx b/src/components/auth/signup-form.tsx index 2ac9ab6..7882788 100644 --- a/src/components/auth/signup-form.tsx +++ b/src/components/auth/signup-form.tsx @@ -72,7 +72,14 @@ export function SignUpForm({ onSignUpSuccess }: SignUpFormProps) { } else { // Default behavior - redirect to signin with message if (typeof window !== 'undefined') { - router.push("/signin?message=Account created successfully! Please check your email to verify your account.") + const message = encodeURIComponent("Account created successfully! Please check your email to verify your account.") + try { + router.push(`/signin?message=${message}`) + } catch (redirectError) { + console.error('Signup form redirect failed:', redirectError) + // Fallback redirect + window.location.href = `/signin?message=${message}` + } } } } else { diff --git a/src/components/auth/signup-page.tsx b/src/components/auth/signup-page.tsx index 54bc1e9..b03b2f6 100644 --- a/src/components/auth/signup-page.tsx +++ b/src/components/auth/signup-page.tsx @@ -12,12 +12,19 @@ export function SignUpPage() { const handleSignUpSuccess = () => { setIsSuccess(true) - // Redirect to signin after 3 seconds + // Redirect to signin after 3 seconds with proper URL encoding setTimeout(() => { if (typeof window !== 'undefined') { - router.push("/signin?message=Please check your email to verify your account") + const message = encodeURIComponent("Please check your email to verify your account") + try { + router.push(`/signin?message=${message}`) + } catch (redirectError) { + console.error('Signup redirect failed:', redirectError) + // Fallback redirect + window.location.href = `/signin?message=${message}` + } } - }, 3001) + }, 3000) } if (isSuccess) { diff --git a/src/config/backend.ts b/src/config/backend.ts index 95abe33..aea0e9a 100644 --- a/src/config/backend.ts +++ b/src/config/backend.ts @@ -1,28 +1,14 @@ -/** - * Centralized Backend Configuration - * Single source of truth for all backend URLs - */ -// Main backend URL - change this to update all API calls -// LIVE PRODUCTION URL (Currently Active) -export const BACKEND_URL = 'https://backend.codenuk.com'; +// +// export const BACKEND_URL = 'https://backend.codenuk.com'; -// LOCAL DEVELOPMENT URL (Uncomment this and comment above for local development) -// export const BACKEND_URL = 'http://localhost:8000'; +export const BACKEND_URL = 'http://localhost:8000'; - -// Realtime notifications socket URL (Template Manager emits notifications) -// Uses the same URL as BACKEND_URL for consistency export const SOCKET_URL = BACKEND_URL; - - -// Export for backward compatibility export const API_BASE_URL = BACKEND_URL; -// Helper function to get full API endpoint URL export const getApiUrl = (endpoint: string): string => { - // Remove leading slash if present to avoid double slashes const cleanEndpoint = endpoint.startsWith('/') ? endpoint.slice(1) : endpoint; return `${BACKEND_URL}/${cleanEndpoint}`; }; diff --git a/src/contexts/AdminNotificationContext.tsx b/src/contexts/AdminNotificationContext.tsx index 1b5b910..47c8a8f 100644 --- a/src/contexts/AdminNotificationContext.tsx +++ b/src/contexts/AdminNotificationContext.tsx @@ -21,16 +21,15 @@ const AdminNotificationContext = createContext([]); const [, setIsLoading] = useState(false); - - const { - isConnected, - notificationCounts, - onNewNotification, - onNotificationRead, - onAllNotificationsRead + + const { + isConnected, + notificationCounts, + onNewNotification, + onNotificationRead, + onAllNotificationsRead } = useNotificationSocket(); - // Load initial notifications const refreshNotifications = async () => { try { setIsLoading(true); @@ -43,11 +42,10 @@ export function AdminNotificationProvider({ children }: { children: React.ReactN } }; - // Mark single notification as read const markAsRead = async (id: string) => { try { await adminApi.markNotificationAsRead(id); - setNotifications(prev => + setNotifications(prev => prev.map(n => n.id === id ? { ...n, is_read: true, read_at: new Date().toISOString() } : n) ); } catch (error) { @@ -56,11 +54,10 @@ export function AdminNotificationProvider({ children }: { children: React.ReactN } }; - // Mark all notifications as read const markAllAsRead = async () => { try { await adminApi.markAllNotificationsAsRead(); - setNotifications(prev => + setNotifications(prev => prev.map(n => ({ ...n, is_read: true, read_at: new Date().toISOString() })) ); } catch (error) { @@ -69,43 +66,39 @@ export function AdminNotificationProvider({ children }: { children: React.ReactN } }; - // Clear all notifications from UI (after marking as read server-side) const clearAll = async () => { await markAllAsRead(); setNotifications([]); }; - // Remove notifications tied to a specific backend reference (e.g., feature/template id) const removeByReference = (referenceType: string, referenceId: string) => { setNotifications(prev => prev.filter(n => !(n.reference_type === referenceType && n.reference_id === referenceId))); }; - // Set up WebSocket event handlers + useEffect(() => { onNewNotification((notification: AdminNotification) => { setNotifications(prev => [notification, ...prev]); }); onNotificationRead(({ id }: { id: string }) => { - setNotifications(prev => + setNotifications(prev => prev.map(n => n.id === id ? { ...n, is_read: true, read_at: new Date().toISOString() } : n) ); }); onAllNotificationsRead(() => { - setNotifications(prev => + setNotifications(prev => prev.map(n => ({ ...n, is_read: true, read_at: new Date().toISOString() })) ); }); }, [onNewNotification, onNotificationRead, onAllNotificationsRead]); - // Load notifications on mount useEffect(() => { refreshNotifications(); }, []); - // When socket connects (e.g., navigating to admin), refresh to include any - // notifications created before the connection was established + useEffect(() => { if (isConnected) { refreshNotifications(); diff --git a/src/contexts/auth-context.tsx b/src/contexts/auth-context.tsx index ce4ec3a..d0ed6f2 100644 --- a/src/contexts/auth-context.tsx +++ b/src/contexts/auth-context.tsx @@ -40,7 +40,6 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { timestamp: new Date().toISOString() }) - // Only restore user if we have both user data AND at least one token if (stored && (accessToken || refreshToken)) { try { const userData = JSON.parse(stored) @@ -53,7 +52,6 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { } } else { console.log('🔐 [AuthContext] No valid user session found') - // Clear any partial data if (stored && !accessToken && !refreshToken) { console.log('🔐 [AuthContext] Clearing orphaned user data without tokens') safeLocalStorage.removeItem('codenuk_user') @@ -62,7 +60,6 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { setIsLoading(false) }, []) - // Listen for storage changes to sync auth state across tabs useEffect(() => { const handleStorageChange = (e: StorageEvent) => { if (e.key === 'codenuk_user' || e.key === 'accessToken' || e.key === 'refreshToken') { @@ -99,20 +96,19 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const logout = async () => { try { - // Call the logout API to invalidate tokens on the server await logoutApi() } catch (error) { console.error('Logout API call failed:', error) - // Continue with local logout even if API call fails } finally { - // Always clear local data setUser(null) safeLocalStorage.removeItem('codenuk_user') safeLocalStorage.removeItem('accessToken') safeLocalStorage.removeItem('refreshToken') - // Redirect to signin page - window.location.href = '/signin' + // Prevent redirect loops and add error context + if (typeof window !== 'undefined' && !window.location.pathname.includes('/signin')) { + window.location.href = '/signin?error=You have been logged out' + } } } diff --git a/src/hooks/useNotificationSocket.ts b/src/hooks/useNotificationSocket.ts index aecf5cb..6a39237 100644 --- a/src/hooks/useNotificationSocket.ts +++ b/src/hooks/useNotificationSocket.ts @@ -33,8 +33,7 @@ export function useNotificationSocket(): UseNotificationSocketReturn { const allNotificationsReadCallbackRef = useRef<(() => void) | null>(null); useEffect(() => { - // Initialize socket connection - const templateManagerUrl = SOCKET_URL; // connect directly to template-manager socket server + const templateManagerUrl = SOCKET_URL; const token = getAccessToken(); console.log('[useNotificationSocket] Initializing socket', { url: templateManagerUrl, diff --git a/src/hooks/useTemplates.ts b/src/hooks/useTemplates.ts index f1784ba..dad8f36 100644 --- a/src/hooks/useTemplates.ts +++ b/src/hooks/useTemplates.ts @@ -6,7 +6,6 @@ import { useAuth } from '@/contexts/auth-context'; export function useTemplates() { const { user, isLoading: authLoading } = useAuth(); - // Log user ID for debugging useEffect(() => { console.log('🔍 [useTemplates] Current user ID:', user?.id || 'No user ID') console.log('👤 [useTemplates] User object:', user) @@ -17,7 +16,6 @@ export function useTemplates() { console.log('🔐 [useTemplates] Auth loading:', authLoading) console.log('🔐 [useTemplates] Timestamp:', new Date().toISOString()) - // Check if we have tokens available const accessToken = localStorage.getItem('accessToken') const refreshToken = localStorage.getItem('refreshToken') console.log('🔐 [useTemplates] Token availability:', { @@ -36,12 +34,11 @@ export function useTemplates() { pagination?: { total?: number; limit?: number; offset?: number; hasMore?: boolean }; } | null>(null); - // Stable categories fetched once, independent of paginated data const [categories, setCategories] = useState>([]); const [paginationState, setPaginationState] = useState({ currentPage: 0, - pageSize: 6, // Respect backend pagination: 6 items per page + pageSize: 6, total: 0, hasMore: false, loading: false, @@ -51,7 +48,6 @@ export function useTemplates() { const { show } = useToast(); - // Fetch templates with pagination, category, and search const fetchTemplatesWithPagination = useCallback(async (opts?: { page?: number; pageSize?: number; @@ -59,13 +55,11 @@ export function useTemplates() { search?: string; resetPagination?: boolean; }) => { - // Don't fetch if auth is still loading if (authLoading) { console.log('[useTemplates] Auth still loading, skipping fetch') return; } - // Check if we have authentication tokens available const accessToken = localStorage.getItem('accessToken') const refreshToken = localStorage.getItem('refreshToken') const hasTokens = !!(accessToken || refreshToken) @@ -78,13 +72,10 @@ export function useTemplates() { userId: user?.id }) - // If user exists but no tokens, wait a bit for tokens to be loaded if (user && !hasTokens) { console.log('[useTemplates] User exists but no tokens found, waiting for token loading...') - // Wait a short time for tokens to be loaded from localStorage await new Promise(resolve => setTimeout(resolve, 100)) - // Check again after waiting const retryAccessToken = localStorage.getItem('accessToken') const retryRefreshToken = localStorage.getItem('refreshToken') const retryHasTokens = !!(retryAccessToken || retryRefreshToken) @@ -105,7 +96,7 @@ export function useTemplates() { const { page = 0, pageSize = paginationState.pageSize, category = paginationState.selectedCategory, search = paginationState.searchQuery, resetPagination = false } = opts || {}; const offset = resetPagination ? 0 : page * pageSize; - const limit = pageSize; // Use pageSize for limit + const limit = pageSize; setPaginationState((prev) => ({ ...prev, loading: true })); setError(null); @@ -116,7 +107,6 @@ export function useTemplates() { console.log('[useTemplates] User object:', user); console.log('[useTemplates] includeOthers will be:', !user?.id); - // Only pass userId if it is a valid UUID v4; otherwise, omit and include others const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; const validUserId = typeof user?.id === 'string' && uuidV4Regex.test(user.id) ? user.id : undefined; if (!validUserId && user?.id) { @@ -125,7 +115,7 @@ export function useTemplates() { const res = await templateService.getCombinedTemplates({ userId: validUserId, - includeOthers: true, // always include custom templates from others + defaults + includeOthers: true, limit: pageSize, offset, category: category === 'all' ? undefined : category, diff --git a/src/lib/api-config.ts b/src/lib/api-config.ts index e87d924..6f078c1 100644 --- a/src/lib/api-config.ts +++ b/src/lib/api-config.ts @@ -78,11 +78,9 @@ export const AI_MOCKUP_CONFIG = { // Export the main API base URL for backward compatibility export const API_BASE_URL = API_CONFIG.BASE_URL; -// Temporary debug logging +// Debug logging - shows centralized config is working console.log('🔧 API Config Debug:', { - 'process.env.NEXT_PUBLIC_API_URL': process.env.NEXT_PUBLIC_API_URL, - 'process.env.NEXT_PUBLIC_AUTH_API_URL': process.env.NEXT_PUBLIC_AUTH_API_URL, - 'DEFAULT_API_BASE_URL': DEFAULT_API_BASE_URL, + 'BACKEND_URL (centralized)': BACKEND_URL, 'API_CONFIG.BASE_URL': API_CONFIG.BASE_URL, 'API_CONFIG.AUTH_SERVICE': API_CONFIG.AUTH_SERVICE, 'API_BASE_URL': API_BASE_URL