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": [] } 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 */ 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..889c2362 --- /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); + // 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(); + } + } 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..ffd818b6 --- /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..89ddcea4 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -58,14 +58,17 @@ root.render( {import.meta.env.VITE_PUBLIC_POSTHOG_KEY ? ( diff --git a/src/pages/PrivacyPolicy.tsx b/src/pages/PrivacyPolicy.tsx index 0bef484a..32d3544a 100644 --- a/src/pages/PrivacyPolicy.tsx +++ b/src/pages/PrivacyPolicy.tsx @@ -33,28 +33,31 @@ const PrivacyPolicy = () => {

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.