From fde761ade581d047a71ddb4016cb593bb9508227 Mon Sep 17 00:00:00 2001 From: otdoges Date: Thu, 7 Aug 2025 00:58:41 -0500 Subject: [PATCH 1/8] fixing the convex error --- convex/rateLimit.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/convex/rateLimit.ts b/convex/rateLimit.ts index 7b56b6e1..48f4349b 100644 --- a/convex/rateLimit.ts +++ b/convex/rateLimit.ts @@ -38,6 +38,11 @@ export async function checkRateLimit( throw new Error("Authentication required for rate limiting"); } + // Periodically clean up expired entries to prevent memory leaks + if (Math.random() < 0.01) { // 1% chance to cleanup on each call + cleanupExpiredRateLimits(); + } + const config = RATE_LIMITS[operation]; const key = `${identity.subject}:${operation}`; const now = Date.now(); @@ -95,9 +100,9 @@ export async function enforceRateLimit( } /** - * Clean up expired rate limit entries (should be called periodically) + * Clean up expired rate limit entries (called during rate limit checks) */ -export function cleanupExpiredRateLimits(): void { +function cleanupExpiredRateLimits(): void { const now = Date.now(); for (const [key, entry] of rateLimitStore.entries()) { if (now > entry.resetTime) { @@ -106,9 +111,6 @@ export function cleanupExpiredRateLimits(): void { } } -// Clean up expired entries every 5 minutes -setInterval(cleanupExpiredRateLimits, 5 * 60 * 1000); - /** * Get current rate limit status for a user and operation */ From 666c278899423d8803cbe4d1129efb6081132b03 Mon Sep 17 00:00:00 2001 From: otdoges Date: Thu, 7 Aug 2025 04:59:56 -0500 Subject: [PATCH 2/8] making sure that clerk is working --- src/App.tsx | 7 ++- src/components/AuthProvider.tsx | 94 +++++++++++++++++++++++++++++++++ src/components/AuthWrapper.tsx | 57 ++++++++++++++++++++ src/components/UserSync.tsx | 22 +++++++- src/lib/auth-cookies.ts | 83 +++++++++++++++++++++++++++++ src/lib/auth-provider.tsx | 58 ++++++++++++++++++++ src/main.tsx | 3 +- 7 files changed, 320 insertions(+), 4 deletions(-) create mode 100644 src/components/AuthProvider.tsx create mode 100644 src/components/AuthWrapper.tsx create mode 100644 src/lib/auth-cookies.ts create mode 100644 src/lib/auth-provider.tsx diff --git a/src/App.tsx b/src/App.tsx index cdcff644..99b71429 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,6 +16,7 @@ import PrivacyPolicy from "./pages/PrivacyPolicy"; import Chat from "./pages/Chat"; import AuthGuard from "./components/AuthGuard"; import UserSync from "./components/UserSync"; +import { AuthWrapper } from "./components/AuthWrapper"; import E2BDemo from "./pages/E2BDemo"; const queryClient = new QueryClient(); @@ -23,7 +24,8 @@ const queryClient = new QueryClient(); const App = () => ( - + +
@@ -56,7 +58,8 @@ const App = () => (
-
+
+
); diff --git a/src/components/AuthProvider.tsx b/src/components/AuthProvider.tsx new file mode 100644 index 00000000..f0eb2bd6 --- /dev/null +++ b/src/components/AuthProvider.tsx @@ -0,0 +1,94 @@ +import React, { createContext, useContext, useEffect, useState, useCallback } from 'react'; +import { useAuth as useClerkAuth, useUser } from '@clerk/clerk-react'; +import { AuthCookies } from '@/lib/auth-cookies'; + +interface AuthContextType { + isAuthenticated: boolean; + isLoading: boolean; + user: unknown; + token: string | null; + refreshAuth: () => Promise; +} + +const AuthContext = createContext(undefined); + +export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const { getToken, isSignedIn, isLoaded } = useClerkAuth(); + const { user } = useUser(); + const [token, setToken] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + const refreshAuth = useCallback(async () => { + setIsLoading(true); + try { + if (isSignedIn && isLoaded) { + const newToken = await getToken(); + if (newToken) { + setToken(newToken); + AuthCookies.set(newToken); + } + } else { + setToken(null); + AuthCookies.remove(); + } + } catch (error) { + console.error('Failed to refresh auth token:', error); + // Try to use cached token if available + const cachedToken = AuthCookies.get(); + if (cachedToken && AuthCookies.isValid()) { + setToken(cachedToken); + } else { + setToken(null); + AuthCookies.remove(); + } + } finally { + setIsLoading(false); + } + }, [isSignedIn, isLoaded, getToken]); + + useEffect(() => { + refreshAuth(); + }, [isSignedIn, isLoaded, user?.id, refreshAuth]); + + // Periodic token refresh to prevent expiration + useEffect(() => { + if (isSignedIn) { + const interval = setInterval(() => { + refreshAuth(); + }, 4 * 60 * 1000); // Refresh every 4 minutes + + return () => clearInterval(interval); + } + }, [isSignedIn, refreshAuth]); + + // Initialize token from cookie on app start + useEffect(() => { + const cachedToken = AuthCookies.get(); + if (cachedToken && AuthCookies.isValid() && !token) { + setToken(cachedToken); + } + setIsLoading(false); + }, [token]); + + const value: AuthContextType = { + isAuthenticated: isLoaded && isSignedIn && !!token, + isLoading: !isLoaded || isLoading, + user, + token, + refreshAuth, + }; + + return ( + + {children} + + ); +}; + +export const useAuthContext = () => { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error('useAuthContext must be used within an AuthProvider'); + } + return context; +}; \ No newline at end of file diff --git a/src/components/AuthWrapper.tsx b/src/components/AuthWrapper.tsx new file mode 100644 index 00000000..d40e966e --- /dev/null +++ b/src/components/AuthWrapper.tsx @@ -0,0 +1,57 @@ +import React, { useEffect } from 'react'; +import { useConvexAuth } from 'convex/react'; +import { useAuth as useClerkAuth } from '@clerk/clerk-react'; +import { AuthCookies, useAuthCookies } from '@/lib/auth-cookies'; + +interface AuthWrapperProps { + children: React.ReactNode; +} + +export const AuthWrapper: React.FC = ({ children }) => { + const convexAuth = useConvexAuth(); + const clerkAuth = useClerkAuth(); + const { getStoredToken, clearToken } = useAuthCookies(); + + useEffect(() => { + // Handle authentication recovery on page load/refresh + const handleAuthRecovery = async () => { + // If Convex shows not authenticated but we have a valid cookie token + if (!convexAuth.isAuthenticated && !convexAuth.isLoading) { + const storedToken = getStoredToken(); + + if (storedToken && AuthCookies.isValid()) { + // Try to refresh Clerk session if needed + try { + if (clerkAuth.isSignedIn) { + const freshToken = await clerkAuth.getToken({ skipCache: true }); + if (freshToken) { + AuthCookies.set(freshToken); + // Force Convex to re-authenticate + window.location.reload(); + } + } + } catch (error) { + console.warn('Auth recovery failed:', error); + clearToken(); + } + } else if (storedToken) { + // Remove invalid token + clearToken(); + } + } + }; + + // Run recovery after initial auth check + const timeout = setTimeout(handleAuthRecovery, 1000); + return () => clearTimeout(timeout); + }, [convexAuth.isAuthenticated, convexAuth.isLoading, clerkAuth.isSignedIn, getStoredToken, clearToken, clerkAuth]); + + // Handle sign out cleanup + useEffect(() => { + if (!clerkAuth.isSignedIn && clerkAuth.isLoaded) { + clearToken(); + } + }, [clerkAuth.isSignedIn, clerkAuth.isLoaded, clearToken]); + + return <>{children}; +}; \ No newline at end of file diff --git a/src/components/UserSync.tsx b/src/components/UserSync.tsx index 704d57f0..0aa6a9f6 100644 --- a/src/components/UserSync.tsx +++ b/src/components/UserSync.tsx @@ -1,14 +1,34 @@ import { useEffect } from 'react'; import { useConvexAuth } from 'convex/react'; -import { useUser } from '@clerk/clerk-react'; +import { useUser, useAuth as useClerkAuth } from '@clerk/clerk-react'; import { useMutation } from 'convex/react'; import { api } from '../../convex/_generated/api'; +import { AuthCookies } from '@/lib/auth-cookies'; export default function UserSync({ children }: { children: React.ReactNode }) { const { isAuthenticated, isLoading } = useConvexAuth(); const { user: clerkUser } = useUser(); + const { getToken, isSignedIn } = useClerkAuth(); const upsertUser = useMutation(api.users.upsertUser); + // Store auth token in cookies when user signs in + useEffect(() => { + const storeAuthToken = async () => { + if (isSignedIn && clerkUser) { + try { + const token = await getToken(); + if (token) { + AuthCookies.set(token); + } + } catch (error) { + console.error('Failed to store auth token:', error); + } + } + }; + + storeAuthToken(); + }, [isSignedIn, clerkUser?.id, getToken, clerkUser]); + useEffect(() => { if (isAuthenticated && !isLoading && clerkUser) { // Validate required user data diff --git a/src/lib/auth-cookies.ts b/src/lib/auth-cookies.ts new file mode 100644 index 00000000..7a74eec5 --- /dev/null +++ b/src/lib/auth-cookies.ts @@ -0,0 +1,83 @@ +import { useAuth as useClerkAuth } from '@clerk/clerk-react'; +import { useEffect } from 'react'; + +// Cookie utilities for auth token management +export const AuthCookies = { + TOKEN_KEY: 'clerk_session_token', + + set(token: string, expiresInDays = 7) { + const expires = new Date(); + expires.setTime(expires.getTime() + (expiresInDays * 24 * 60 * 60 * 1000)); + document.cookie = `${this.TOKEN_KEY}=${token}; expires=${expires.toUTCString()}; path=/; secure; samesite=strict`; + }, + + get(): string | null { + const cookies = document.cookie.split(';'); + for (const cookie of cookies) { + const [name, value] = cookie.trim().split('='); + if (name === this.TOKEN_KEY) { + return decodeURIComponent(value); + } + } + return null; + }, + + remove() { + document.cookie = `${this.TOKEN_KEY}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; + }, + + isValid(): boolean { + const token = this.get(); + if (!token) return false; + + try { + // Basic JWT validation - check if it's properly formatted + const parts = token.split('.'); + if (parts.length !== 3) return false; + + // Decode payload to check expiration + const payload = JSON.parse(atob(parts[1])); + const now = Math.floor(Date.now() / 1000); + + return payload.exp > now; + } catch (error) { + console.warn('Invalid token format:', error); + return false; + } + } +}; + +// Hook to manage auth cookies automatically +export const useAuthCookies = () => { + const { getToken, isSignedIn } = useClerkAuth(); + + useEffect(() => { + const syncToken = async () => { + if (isSignedIn) { + try { + const token = await getToken(); + if (token) { + AuthCookies.set(token); + } + } catch (error) { + console.error('Failed to get or set auth token:', error); + } + } else { + AuthCookies.remove(); + } + }; + + syncToken(); + + // Set up periodic token refresh + const interval = setInterval(syncToken, 5 * 60 * 1000); // Every 5 minutes + + return () => clearInterval(interval); + }, [isSignedIn, getToken]); + + return { + getStoredToken: AuthCookies.get, + isTokenValid: AuthCookies.isValid, + clearToken: AuthCookies.remove + }; +}; \ No newline at end of file diff --git a/src/lib/auth-provider.tsx b/src/lib/auth-provider.tsx new file mode 100644 index 00000000..312ed482 --- /dev/null +++ b/src/lib/auth-provider.tsx @@ -0,0 +1,58 @@ +import { useAuth as useClerkAuth } from '@clerk/clerk-react'; +import { useConvexAuth } from 'convex/react'; +import { AuthCookies } from './auth-cookies'; + +// Enhanced auth hook that combines Clerk and Convex auth with cookie fallback +export const useAuthWithCookies = () => { + const clerkAuth = useClerkAuth(); + const convexAuth = useConvexAuth(); + + // Enhanced getToken that uses cookies as fallback + const getTokenWithFallback = async () => { + try { + // First, try to get token from Clerk + if (clerkAuth.isSignedIn) { + const token = await clerkAuth.getToken(); + if (token) { + // Store token in cookie for persistence + AuthCookies.set(token); + return token; + } + } + + // Fallback to cookie if Clerk fails + const cookieToken = AuthCookies.get(); + if (cookieToken && AuthCookies.isValid()) { + return cookieToken; + } + + return null; + } catch (error) { + console.error('Error getting auth token:', error); + + // Try cookie as last resort + const cookieToken = AuthCookies.get(); + if (cookieToken && AuthCookies.isValid()) { + return cookieToken; + } + + return null; + } + }; + + // Clear tokens on sign out + const signOut = async () => { + AuthCookies.remove(); + if (clerkAuth.signOut) { + await clerkAuth.signOut(); + } + }; + + return { + ...clerkAuth, + ...convexAuth, + getToken: getTokenWithFallback, + signOut, + isFullyAuthenticated: convexAuth.isAuthenticated && clerkAuth.isSignedIn, + }; +}; \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx index 01738510..bb90bf2d 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -59,13 +59,14 @@ root.render( {import.meta.env.VITE_PUBLIC_POSTHOG_KEY ? ( From 79bf6031928ab4e99374792b68d47f5294a09805 Mon Sep 17 00:00:00 2001 From: otdoges Date: Thu, 7 Aug 2025 05:04:16 -0500 Subject: [PATCH 3/8] trying to make sure that our fucking auth will fucking work --- src/lib/auth-cookies.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/auth-cookies.ts b/src/lib/auth-cookies.ts index 7a74eec5..ffd818b6 100644 --- a/src/lib/auth-cookies.ts +++ b/src/lib/auth-cookies.ts @@ -76,8 +76,8 @@ export const useAuthCookies = () => { }, [isSignedIn, getToken]); return { - getStoredToken: AuthCookies.get, - isTokenValid: AuthCookies.isValid, - clearToken: AuthCookies.remove + getStoredToken: () => AuthCookies.get(), + isTokenValid: () => AuthCookies.isValid(), + clearToken: () => AuthCookies.remove() }; }; \ No newline at end of file From 5dca1aa9acae578f80652e1a1842cbb30231d547 Mon Sep 17 00:00:00 2001 From: otdoges Date: Thu, 7 Aug 2025 05:11:16 -0500 Subject: [PATCH 4/8] I broke my auth on vite somehow --- src/components/AuthWrapper.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/AuthWrapper.tsx b/src/components/AuthWrapper.tsx index d40e966e..889c2362 100644 --- a/src/components/AuthWrapper.tsx +++ b/src/components/AuthWrapper.tsx @@ -26,8 +26,8 @@ export const AuthWrapper: React.FC = ({ children }) => { const freshToken = await clerkAuth.getToken({ skipCache: true }); if (freshToken) { AuthCookies.set(freshToken); - // Force Convex to re-authenticate - window.location.reload(); + // Let Convex naturally re-authenticate without forcing reload + console.log('Auth token refreshed, waiting for Convex sync'); } } } catch (error) { From 7c2ef3d84030133e7082e1395771f91d07c0c962 Mon Sep 17 00:00:00 2001 From: otdoges Date: Thu, 7 Aug 2025 07:41:42 -0500 Subject: [PATCH 5/8] Making auth work --- .claude/settings.local.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 89badecd..f4af6ee6 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -47,7 +47,9 @@ "Bash(vercel:*)", "WebFetch(domain:console.groq.com)", "Bash(ls:*)", - "Bash(sed:*)" + "Bash(sed:*)", + "Bash(echo $CLERK_JWT_ISSUER_DOMAIN)", + "Bash(curl:*)" ], "deny": [] } From 40f9e668d5a6960a85553245aa051441f6381c7a Mon Sep 17 00:00:00 2001 From: otdoges Date: Thu, 7 Aug 2025 08:01:18 -0500 Subject: [PATCH 6/8] I am a retard --- src/main.tsx | 2 + src/pages/PrivacyPolicy.tsx | 87 +++++++++++++++++++++++-------------- 2 files changed, 57 insertions(+), 32 deletions(-) diff --git a/src/main.tsx b/src/main.tsx index bb90bf2d..89ddcea4 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -58,6 +58,8 @@ root.render( {

2. INFORMATION WE COLLECT

-

2.1. Information You Provide Directly:

+

2.1. Messages and Communications:

    +
  • Chat messages and conversations you have with our AI platform
  • +
  • All messages are encrypted and stored securely
  • Account registration information (name, email address)
  • -
  • Profile information you choose to provide
  • -
  • Code and content you create or upload
  • Communications with our support team
-

2.2. Information Collected Automatically:

+

2.2. Analytics Data:

    -
  • Usage data and analytics
  • -
  • Device and browser information
  • -
  • IP addresses and location data
  • -
  • Cookies and similar tracking technologies
  • +
  • Usage patterns and platform interactions (anonymized)
  • +
  • Performance metrics to improve service quality
  • +
  • Basic device and browser information
  • +
  • Session duration and feature usage statistics
+

+ Important: We collect analytics to improve our service, but we do not sell your data to third parties. +

-

2.3. Information from Third Parties:

+

2.3. Authentication Information:

    -
  • OAuth provider information (GitHub, Google) when you choose to authenticate
  • -
  • Analytics and performance monitoring data
  • +
  • OAuth provider information when you choose to authenticate
  • +
  • Session tokens and authentication credentials (encrypted)
@@ -98,27 +101,33 @@ const PrivacyPolicy = () => {

5. INFORMATION SHARING AND DISCLOSURE

-

We do not sell your personal information. We may share your information only in these limited circumstances:

+
+

Our Data Commitment

+

+ We do not sell your personal information or data to third parties. Your privacy is fundamental to our service, and we are committed to protecting your encrypted messages and personal information. +

+
+

We may share your information only in these strictly limited circumstances:

-

5.1. With Your Consent:

-

When you explicitly authorize us to share specific information.

+

5.1. With Your Explicit Consent:

+

Only when you explicitly authorize us to share specific information for a particular purpose.

-

5.2. Service Providers:

-

With trusted third-party providers who assist in operating our platform (hosting, analytics, payment processing) under strict confidentiality agreements.

+

5.2. Essential Service Providers:

+

With trusted third-party providers who assist in operating our platform (secure hosting, payment processing) under strict confidentiality agreements. These providers cannot access your encrypted messages.

-

5.3. AI Service Providers:

-

Code generation requests may be sent to third-party AI providers (OpenAI, Anthropic, etc.) with privacy protections in place.

+

5.3. Analytics Partners:

+

Anonymized usage analytics may be shared with analytics services to help improve our platform. No personal messages or identifiable information is included.

5.4. Legal Requirements:

-

When required by law, court order, or to protect our rights and safety.

+

Only when required by valid legal process, court order, or to protect our users' safety and security.

5.5. Business Transfers:

-

In connection with mergers, acquisitions, or asset sales, with appropriate privacy protections.

+

In the unlikely event of a merger or acquisition, with appropriate privacy protections and user notification.

@@ -153,26 +162,40 @@ const PrivacyPolicy = () => {

8. SECURITY MEASURES

+
+

Message Encryption

+

+ All messages and conversations are encrypted both in transit and at rest. This means your conversations are protected and unreadable even if unauthorized access occurs. +

+

We implement comprehensive security measures including:

    -
  • Encryption in transit and at rest
  • -
  • Regular security audits and monitoring
  • -
  • Access controls and authentication requirements
  • -
  • Incident response procedures
  • -
  • Regular security training for our team
  • +
  • End-to-end encryption for all messages and sensitive data
  • +
  • Encryption in transit (TLS/SSL) and at rest (AES-256)
  • +
  • Regular security audits and penetration testing
  • +
  • Multi-factor authentication and access controls
  • +
  • 24/7 monitoring and incident response procedures
  • +
  • Regular security training for our development team
  • +
  • Secure data centers with physical access controls
-

9. COOKIES AND TRACKING TECHNOLOGIES

-

We use cookies and similar technologies to:

+

9. ANALYTICS AND TRACKING

+

We use cookies and analytics technologies to:

    -
  • Maintain your login session
  • -
  • Remember your preferences
  • -
  • Analyze usage patterns
  • -
  • Improve our services
  • +
  • Maintain your secure login session
  • +
  • Remember your preferences and settings
  • +
  • Analyze usage patterns to improve our AI platform
  • +
  • Monitor performance and identify technical issues
  • +
  • Understand which features are most valuable to users
-

You can control cookie settings through your browser preferences.

+
+

+ Analytics Transparency: We collect analytics to make ZapDev better for everyone, but we never sell this data or use it for advertising. All analytics data is anonymized and aggregated. +

+
+

You can control cookie settings through your browser preferences, though some features may require certain cookies to function properly.

From 98ca211c9469fefbe8455748c80ad5e507000e22 Mon Sep 17 00:00:00 2001 From: otdoges Date: Thu, 7 Aug 2025 08:32:24 -0500 Subject: [PATCH 7/8] Fixing security mistakes --- convex/rateLimit.ts | 27 +++++++-- src/App.tsx | 11 ++-- src/components/AuthErrorBoundary.tsx | 73 ++++++++++++++++++++++++ src/components/AuthProvider.tsx | 68 ++++++++++------------- src/components/AuthWrapper.tsx | 42 ++++++-------- src/components/UserSync.tsx | 21 +------ src/lib/auth-cookies.ts | 83 ---------------------------- src/lib/auth-provider.tsx | 42 +++++--------- src/lib/auth-token.ts | 75 +++++++++++++++++++++++++ src/lib/security.ts | 2 +- 10 files changed, 238 insertions(+), 206 deletions(-) create mode 100644 src/components/AuthErrorBoundary.tsx delete mode 100644 src/lib/auth-cookies.ts create mode 100644 src/lib/auth-token.ts diff --git a/convex/rateLimit.ts b/convex/rateLimit.ts index 48f4349b..aa43704d 100644 --- a/convex/rateLimit.ts +++ b/convex/rateLimit.ts @@ -23,7 +23,8 @@ interface RateLimitEntry { resetTime: number; } -// Simple in-memory rate limiting (for production, use Redis or similar) +// Simple in-memory rate limiting with bounded size (for production, use Redis or similar) +const MAX_RATE_LIMIT_ENTRIES = 10000; // Prevent unbounded growth const rateLimitStore = new Map(); /** @@ -38,8 +39,8 @@ export async function checkRateLimit( throw new Error("Authentication required for rate limiting"); } - // Periodically clean up expired entries to prevent memory leaks - if (Math.random() < 0.01) { // 1% chance to cleanup on each call + // More aggressive cleanup to prevent memory leaks + if (Math.random() < 0.05 || rateLimitStore.size > MAX_RATE_LIMIT_ENTRIES) { cleanupExpiredRateLimits(); } @@ -104,9 +105,27 @@ export async function enforceRateLimit( */ function cleanupExpiredRateLimits(): void { const now = Date.now(); + const keysToDelete: string[] = []; + for (const [key, entry] of rateLimitStore.entries()) { if (now > entry.resetTime) { - rateLimitStore.delete(key); + keysToDelete.push(key); + } + } + + // Delete expired entries + for (const key of keysToDelete) { + rateLimitStore.delete(key); + } + + // If still too many entries, delete oldest ones (LRU-style) + if (rateLimitStore.size > MAX_RATE_LIMIT_ENTRIES) { + const entries = Array.from(rateLimitStore.entries()); + entries.sort((a, b) => a[1].resetTime - b[1].resetTime); + + const numToDelete = rateLimitStore.size - MAX_RATE_LIMIT_ENTRIES; + for (let i = 0; i < numToDelete; i++) { + rateLimitStore.delete(entries[i][0]); } } } diff --git a/src/App.tsx b/src/App.tsx index 99b71429..6d8b9885 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,14 +17,16 @@ import Chat from "./pages/Chat"; import AuthGuard from "./components/AuthGuard"; import UserSync from "./components/UserSync"; import { AuthWrapper } from "./components/AuthWrapper"; +import { AuthErrorBoundary } from "./components/AuthErrorBoundary"; import E2BDemo from "./pages/E2BDemo"; const queryClient = new QueryClient(); const App = () => ( - - + + + @@ -59,8 +61,9 @@ const App = () => ( - - + + + ); diff --git a/src/components/AuthErrorBoundary.tsx b/src/components/AuthErrorBoundary.tsx new file mode 100644 index 00000000..5a2a675a --- /dev/null +++ b/src/components/AuthErrorBoundary.tsx @@ -0,0 +1,73 @@ +import React, { Component, ErrorInfo, ReactNode } from 'react'; + +interface Props { + children: ReactNode; + fallback?: ReactNode; +} + +interface State { + hasError: boolean; + error?: Error; +} + +export class AuthErrorBoundary extends Component { + constructor(props: Props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error: Error): State { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + // Log error without exposing sensitive auth details + console.error('Authentication error caught by boundary:', { + message: error.message, + stack: error.stack, + componentStack: errorInfo.componentStack + }); + } + + render() { + if (this.state.hasError) { + return this.props.fallback || ( +
+
+
+
+ + + +
+
+

+ Authentication Error +

+

+ There was a problem with authentication. Please refresh the page to try again. +

+ +
+
+ ); + } + + return this.props.children; + } +} \ No newline at end of file diff --git a/src/components/AuthProvider.tsx b/src/components/AuthProvider.tsx index f0eb2bd6..b7e3e66e 100644 --- a/src/components/AuthProvider.tsx +++ b/src/components/AuthProvider.tsx @@ -1,11 +1,12 @@ import React, { createContext, useContext, useEffect, useState, useCallback } from 'react'; import { useAuth as useClerkAuth, useUser } from '@clerk/clerk-react'; -import { AuthCookies } from '@/lib/auth-cookies'; +import type { UserResource } from '@clerk/types'; +import { useAuthToken } from '@/lib/auth-token'; interface AuthContextType { isAuthenticated: boolean; isLoading: boolean; - user: unknown; + user: UserResource | null | undefined; token: string | null; refreshAuth: () => Promise; } @@ -13,66 +14,55 @@ interface AuthContextType { const AuthContext = createContext(undefined); export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const { getToken, isSignedIn, isLoaded } = useClerkAuth(); + const { isSignedIn, isLoaded } = useClerkAuth(); const { user } = useUser(); + const { getValidToken, clearStoredToken } = useAuthToken(); const [token, setToken] = useState(null); const [isLoading, setIsLoading] = useState(true); const refreshAuth = useCallback(async () => { - setIsLoading(true); try { if (isSignedIn && isLoaded) { - const newToken = await getToken(); - if (newToken) { - setToken(newToken); - AuthCookies.set(newToken); - } + const newToken = await getValidToken(); + setToken(newToken); } else { setToken(null); - AuthCookies.remove(); + clearStoredToken(); } } catch (error) { - console.error('Failed to refresh auth token:', error); - // Try to use cached token if available - const cachedToken = AuthCookies.get(); - if (cachedToken && AuthCookies.isValid()) { - setToken(cachedToken); - } else { - setToken(null); - AuthCookies.remove(); - } - } finally { - setIsLoading(false); + console.error('Failed to refresh auth token'); + setToken(null); + clearStoredToken(); } - }, [isSignedIn, isLoaded, getToken]); + }, [isSignedIn, isLoaded, getValidToken, clearStoredToken]); + // Main auth effect - handles initial load and auth state changes useEffect(() => { - refreshAuth(); + const handleAuth = async () => { + if (!isLoaded) { + // Still loading Clerk, keep loading state + return; + } + + setIsLoading(true); + await refreshAuth(); + setIsLoading(false); + }; + + handleAuth(); }, [isSignedIn, isLoaded, user?.id, refreshAuth]); // Periodic token refresh to prevent expiration useEffect(() => { - if (isSignedIn) { - const interval = setInterval(() => { - refreshAuth(); - }, 4 * 60 * 1000); // Refresh every 4 minutes - + if (isSignedIn && isLoaded) { + const interval = setInterval(refreshAuth, 4 * 60 * 1000); // Every 4 minutes return () => clearInterval(interval); } - }, [isSignedIn, refreshAuth]); - - // Initialize token from cookie on app start - useEffect(() => { - const cachedToken = AuthCookies.get(); - if (cachedToken && AuthCookies.isValid() && !token) { - setToken(cachedToken); - } - setIsLoading(false); - }, [token]); + }, [isSignedIn, isLoaded, refreshAuth]); const value: AuthContextType = { isAuthenticated: isLoaded && isSignedIn && !!token, - isLoading: !isLoaded || isLoading, + isLoading: isLoading || !isLoaded, user, token, refreshAuth, diff --git a/src/components/AuthWrapper.tsx b/src/components/AuthWrapper.tsx index 889c2362..7d308e80 100644 --- a/src/components/AuthWrapper.tsx +++ b/src/components/AuthWrapper.tsx @@ -1,7 +1,7 @@ import React, { useEffect } from 'react'; import { useConvexAuth } from 'convex/react'; import { useAuth as useClerkAuth } from '@clerk/clerk-react'; -import { AuthCookies, useAuthCookies } from '@/lib/auth-cookies'; +import { useAuthToken } from '@/lib/auth-token'; interface AuthWrapperProps { children: React.ReactNode; @@ -10,33 +10,23 @@ interface AuthWrapperProps { export const AuthWrapper: React.FC = ({ children }) => { const convexAuth = useConvexAuth(); const clerkAuth = useClerkAuth(); - const { getStoredToken, clearToken } = useAuthCookies(); + const { getValidToken, clearStoredToken } = useAuthToken(); useEffect(() => { // Handle authentication recovery on page load/refresh const handleAuthRecovery = async () => { - // If Convex shows not authenticated but we have a valid cookie token - if (!convexAuth.isAuthenticated && !convexAuth.isLoading) { - const storedToken = getStoredToken(); - - if (storedToken && AuthCookies.isValid()) { - // Try to refresh Clerk session if needed - try { - if (clerkAuth.isSignedIn) { - const freshToken = await clerkAuth.getToken({ skipCache: true }); - if (freshToken) { - AuthCookies.set(freshToken); - // Let Convex naturally re-authenticate without forcing reload - console.log('Auth token refreshed, waiting for Convex sync'); - } - } - } catch (error) { - console.warn('Auth recovery failed:', error); - clearToken(); + // If Convex shows not authenticated but Clerk is signed in + if (!convexAuth.isAuthenticated && !convexAuth.isLoading && clerkAuth.isSignedIn && clerkAuth.isLoaded) { + try { + // Get a fresh token to sync with Convex + const freshToken = await getValidToken(); + if (freshToken) { + // Let Convex naturally re-authenticate without forcing reload + console.log('Auth token refreshed for Convex sync'); } - } else if (storedToken) { - // Remove invalid token - clearToken(); + } catch (error) { + console.error('Auth recovery failed'); + clearStoredToken(); } } }; @@ -44,14 +34,14 @@ export const AuthWrapper: React.FC = ({ children }) => { // Run recovery after initial auth check const timeout = setTimeout(handleAuthRecovery, 1000); return () => clearTimeout(timeout); - }, [convexAuth.isAuthenticated, convexAuth.isLoading, clerkAuth.isSignedIn, getStoredToken, clearToken, clerkAuth]); + }, [convexAuth.isAuthenticated, convexAuth.isLoading, clerkAuth.isSignedIn, clerkAuth.isLoaded, getValidToken, clearStoredToken]); // Handle sign out cleanup useEffect(() => { if (!clerkAuth.isSignedIn && clerkAuth.isLoaded) { - clearToken(); + clearStoredToken(); } - }, [clerkAuth.isSignedIn, clerkAuth.isLoaded, clearToken]); + }, [clerkAuth.isSignedIn, clerkAuth.isLoaded, clearStoredToken]); return <>{children}; }; \ No newline at end of file diff --git a/src/components/UserSync.tsx b/src/components/UserSync.tsx index 0aa6a9f6..6b6f0c74 100644 --- a/src/components/UserSync.tsx +++ b/src/components/UserSync.tsx @@ -3,32 +3,13 @@ import { useConvexAuth } from 'convex/react'; import { useUser, useAuth as useClerkAuth } from '@clerk/clerk-react'; import { useMutation } from 'convex/react'; import { api } from '../../convex/_generated/api'; -import { AuthCookies } from '@/lib/auth-cookies'; export default function UserSync({ children }: { children: React.ReactNode }) { const { isAuthenticated, isLoading } = useConvexAuth(); const { user: clerkUser } = useUser(); - const { getToken, isSignedIn } = useClerkAuth(); + const { isSignedIn } = useClerkAuth(); const upsertUser = useMutation(api.users.upsertUser); - // Store auth token in cookies when user signs in - useEffect(() => { - const storeAuthToken = async () => { - if (isSignedIn && clerkUser) { - try { - const token = await getToken(); - if (token) { - AuthCookies.set(token); - } - } catch (error) { - console.error('Failed to store auth token:', error); - } - } - }; - - storeAuthToken(); - }, [isSignedIn, clerkUser?.id, getToken, clerkUser]); - useEffect(() => { if (isAuthenticated && !isLoading && clerkUser) { // Validate required user data diff --git a/src/lib/auth-cookies.ts b/src/lib/auth-cookies.ts deleted file mode 100644 index ffd818b6..00000000 --- a/src/lib/auth-cookies.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { useAuth as useClerkAuth } from '@clerk/clerk-react'; -import { useEffect } from 'react'; - -// Cookie utilities for auth token management -export const AuthCookies = { - TOKEN_KEY: 'clerk_session_token', - - set(token: string, expiresInDays = 7) { - const expires = new Date(); - expires.setTime(expires.getTime() + (expiresInDays * 24 * 60 * 60 * 1000)); - document.cookie = `${this.TOKEN_KEY}=${token}; expires=${expires.toUTCString()}; path=/; secure; samesite=strict`; - }, - - get(): string | null { - const cookies = document.cookie.split(';'); - for (const cookie of cookies) { - const [name, value] = cookie.trim().split('='); - if (name === this.TOKEN_KEY) { - return decodeURIComponent(value); - } - } - return null; - }, - - remove() { - document.cookie = `${this.TOKEN_KEY}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; - }, - - isValid(): boolean { - const token = this.get(); - if (!token) return false; - - try { - // Basic JWT validation - check if it's properly formatted - const parts = token.split('.'); - if (parts.length !== 3) return false; - - // Decode payload to check expiration - const payload = JSON.parse(atob(parts[1])); - const now = Math.floor(Date.now() / 1000); - - return payload.exp > now; - } catch (error) { - console.warn('Invalid token format:', error); - return false; - } - } -}; - -// Hook to manage auth cookies automatically -export const useAuthCookies = () => { - const { getToken, isSignedIn } = useClerkAuth(); - - useEffect(() => { - const syncToken = async () => { - if (isSignedIn) { - try { - const token = await getToken(); - if (token) { - AuthCookies.set(token); - } - } catch (error) { - console.error('Failed to get or set auth token:', error); - } - } else { - AuthCookies.remove(); - } - }; - - syncToken(); - - // Set up periodic token refresh - const interval = setInterval(syncToken, 5 * 60 * 1000); // Every 5 minutes - - return () => clearInterval(interval); - }, [isSignedIn, getToken]); - - return { - getStoredToken: () => AuthCookies.get(), - isTokenValid: () => AuthCookies.isValid(), - clearToken: () => AuthCookies.remove() - }; -}; \ No newline at end of file diff --git a/src/lib/auth-provider.tsx b/src/lib/auth-provider.tsx index 312ed482..6eb90719 100644 --- a/src/lib/auth-provider.tsx +++ b/src/lib/auth-provider.tsx @@ -1,48 +1,32 @@ import { useAuth as useClerkAuth } from '@clerk/clerk-react'; import { useConvexAuth } from 'convex/react'; -import { AuthCookies } from './auth-cookies'; +import { useAuthToken } from './auth-token'; -// Enhanced auth hook that combines Clerk and Convex auth with cookie fallback -export const useAuthWithCookies = () => { +// Enhanced auth hook that combines Clerk and Convex auth with secure token management +export const useAuthWithTokens = () => { const clerkAuth = useClerkAuth(); const convexAuth = useConvexAuth(); + const { getValidToken, clearStoredToken } = useAuthToken(); - // Enhanced getToken that uses cookies as fallback - const getTokenWithFallback = async () => { + // Enhanced getToken that uses secure memory-based storage + const getTokenSecure = async () => { try { - // First, try to get token from Clerk - if (clerkAuth.isSignedIn) { - const token = await clerkAuth.getToken(); - if (token) { - // Store token in cookie for persistence - AuthCookies.set(token); - return token; - } - } - - // Fallback to cookie if Clerk fails - const cookieToken = AuthCookies.get(); - if (cookieToken && AuthCookies.isValid()) { - return cookieToken; + // Get token using the secure token manager + if (clerkAuth.isSignedIn && clerkAuth.isLoaded) { + return await getValidToken(); } return null; } catch (error) { - console.error('Error getting auth token:', error); - - // Try cookie as last resort - const cookieToken = AuthCookies.get(); - if (cookieToken && AuthCookies.isValid()) { - return cookieToken; - } - + console.error('Error getting auth token'); + clearStoredToken(); return null; } }; // Clear tokens on sign out const signOut = async () => { - AuthCookies.remove(); + clearStoredToken(); if (clerkAuth.signOut) { await clerkAuth.signOut(); } @@ -51,7 +35,7 @@ export const useAuthWithCookies = () => { return { ...clerkAuth, ...convexAuth, - getToken: getTokenWithFallback, + getToken: getTokenSecure, signOut, isFullyAuthenticated: convexAuth.isAuthenticated && clerkAuth.isSignedIn, }; diff --git a/src/lib/auth-token.ts b/src/lib/auth-token.ts new file mode 100644 index 00000000..f5329fad --- /dev/null +++ b/src/lib/auth-token.ts @@ -0,0 +1,75 @@ +import { useAuth as useClerkAuth } from '@clerk/clerk-react'; +import { useCallback } from 'react'; + +// Memory-based auth token management (secure alternative to cookies) +class AuthTokenManager { + private token: string | null = null; + private tokenFetchTime: number | null = null; + private readonly TOKEN_REFRESH_THRESHOLD = 4 * 60 * 1000; // 4 minutes + + setToken(token: string | null): void { + this.token = token; + this.tokenFetchTime = token ? Date.now() : null; + } + + getToken(): string | null { + return this.token; + } + + shouldRefreshToken(): boolean { + if (!this.token || !this.tokenFetchTime) return false; + return Date.now() - this.tokenFetchTime > this.TOKEN_REFRESH_THRESHOLD; + } + + clearToken(): void { + this.token = null; + this.tokenFetchTime = null; + } + + hasToken(): boolean { + return !!this.token; + } +} + +export const authTokenManager = new AuthTokenManager(); + +// Simplified hook that relies on Clerk's built-in session management +export const useAuthToken = () => { + const { getToken, isSignedIn, isLoaded } = useClerkAuth(); + + const getValidToken = useCallback(async (): Promise => { + if (!isSignedIn || !isLoaded) { + authTokenManager.clearToken(); + return null; + } + + const cachedToken = authTokenManager.getToken(); + + // Return cached token if it's recent enough + if (cachedToken && !authTokenManager.shouldRefreshToken()) { + return cachedToken; + } + + try { + // Get fresh token from Clerk (Clerk handles validation internally) + const freshToken = await getToken(); + authTokenManager.setToken(freshToken); + return freshToken; + } catch (error) { + // Log error without exposing token details + console.error('Failed to refresh auth token'); + authTokenManager.clearToken(); + return null; + } + }, [getToken, isSignedIn, isLoaded]); + + const clearStoredToken = useCallback(() => { + authTokenManager.clearToken(); + }, []); + + return { + getValidToken, + clearStoredToken, + hasStoredToken: () => authTokenManager.hasToken() + }; +}; \ No newline at end of file diff --git a/src/lib/security.ts b/src/lib/security.ts index 595a0aa7..8b54c712 100644 --- a/src/lib/security.ts +++ b/src/lib/security.ts @@ -507,7 +507,7 @@ export const csrf = { export const createTrustedTypesPolicy = () => { if (typeof window !== 'undefined' && 'trustedTypes' in window) { try { - return (window as any).trustedTypes.createPolicy('default', { + return (window as typeof window & { trustedTypes: { createPolicy: (name: string, policy: unknown) => unknown } }).trustedTypes.createPolicy('default', { createHTML: (input: string) => sanitizeHTML(input), createScriptURL: (input: string) => { const url = sanitizeUrl(input); From 7ef9b9e38cc6155d6b5bebaf882c7b61dd40aad8 Mon Sep 17 00:00:00 2001 From: otdoges Date: Thu, 7 Aug 2025 08:40:29 -0500 Subject: [PATCH 8/8] Fixing the conflict error --- .claude/settings.local.json | 3 ++- convex/_generated/api.d.ts | 2 ++ convex/aiRateLimit.ts | 4 ++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index f4af6ee6..7904ed68 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -49,7 +49,8 @@ "Bash(ls:*)", "Bash(sed:*)", "Bash(echo $CLERK_JWT_ISSUER_DOMAIN)", - "Bash(curl:*)" + "Bash(curl:*)", + "Bash(bunx:*)" ], "deny": [] } diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index 7060e2d8..97859ca3 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -13,6 +13,7 @@ import type { FilterApi, FunctionReference, } from "convex/server"; +import type * as aiRateLimit from "../aiRateLimit.js"; import type * as chats from "../chats.js"; import type * as messages from "../messages.js"; import type * as rateLimit from "../rateLimit.js"; @@ -29,6 +30,7 @@ import type * as users from "../users.js"; * ``` */ declare const fullApi: ApiFromModules<{ + aiRateLimit: typeof aiRateLimit; chats: typeof chats; messages: typeof messages; rateLimit: typeof rateLimit; diff --git a/convex/aiRateLimit.ts b/convex/aiRateLimit.ts index fecdf1b5..bcad4a6f 100644 --- a/convex/aiRateLimit.ts +++ b/convex/aiRateLimit.ts @@ -75,6 +75,8 @@ export const aiRateLimitState = mutation({ windowStart: now, lastRequest: now, tokens: args.tokens || 0, + createdAt: now, + updatedAt: now, }); return { allowed: true, remaining: limits.maxRequests - 1 }; } @@ -214,6 +216,8 @@ export async function enforceAIRateLimit( windowStart: now, lastRequest: now, tokens: options?.tokens || 0, + createdAt: now, + updatedAt: now, }); return; // First request is always allowed }