Spaces:
Running
Running
Pulastya B commited on
Commit ·
1d8f0c9
1
Parent(s): 150d34c
Fix auth issues: sign out working, store signup form data, OAuth users onboarding
Browse files- FRRONTEEEND/App.tsx +16 -3
- FRRONTEEEND/components/AuthPage.tsx +139 -63
- FRRONTEEEND/lib/AuthContext.tsx +27 -5
- FRRONTEEEND/lib/supabase.ts +67 -0
- supabase_schema.sql +80 -0
FRRONTEEEND/App.tsx
CHANGED
|
@@ -16,9 +16,17 @@ import { User, LogOut, Loader2 } from 'lucide-react';
|
|
| 16 |
// Inner app component that uses auth context
|
| 17 |
const AppContent: React.FC = () => {
|
| 18 |
const [view, setView] = useState<'landing' | 'chat' | 'auth'>('landing');
|
| 19 |
-
const { user, isAuthenticated, loading, signOut, isConfigured } = useAuth();
|
| 20 |
const [showUserMenu, setShowUserMenu] = useState(false);
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
// Handle launch console - redirect to auth if not logged in
|
| 23 |
const handleLaunchConsole = () => {
|
| 24 |
if (isAuthenticated) {
|
|
@@ -91,8 +99,13 @@ const AppContent: React.FC = () => {
|
|
| 91 |
</div>
|
| 92 |
<button
|
| 93 |
onClick={async () => {
|
| 94 |
-
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
}}
|
| 97 |
className="w-full flex items-center gap-2 px-4 py-2 text-sm text-red-400 hover:bg-white/5 transition-colors"
|
| 98 |
>
|
|
|
|
| 16 |
// Inner app component that uses auth context
|
| 17 |
const AppContent: React.FC = () => {
|
| 18 |
const [view, setView] = useState<'landing' | 'chat' | 'auth'>('landing');
|
| 19 |
+
const { user, isAuthenticated, loading, signOut, isConfigured, needsOnboarding } = useAuth();
|
| 20 |
const [showUserMenu, setShowUserMenu] = useState(false);
|
| 21 |
|
| 22 |
+
// If user is authenticated but needs onboarding, show auth page
|
| 23 |
+
React.useEffect(() => {
|
| 24 |
+
if (isAuthenticated && needsOnboarding && view !== 'auth') {
|
| 25 |
+
console.log('User needs onboarding, showing form...');
|
| 26 |
+
setView('auth');
|
| 27 |
+
}
|
| 28 |
+
}, [isAuthenticated, needsOnboarding, view]);
|
| 29 |
+
|
| 30 |
// Handle launch console - redirect to auth if not logged in
|
| 31 |
const handleLaunchConsole = () => {
|
| 32 |
if (isAuthenticated) {
|
|
|
|
| 99 |
</div>
|
| 100 |
<button
|
| 101 |
onClick={async () => {
|
| 102 |
+
try {
|
| 103 |
+
await signOut();
|
| 104 |
+
setShowUserMenu(false);
|
| 105 |
+
setView('landing');
|
| 106 |
+
} catch (error) {
|
| 107 |
+
console.error('Sign out failed:', error);
|
| 108 |
+
}
|
| 109 |
}}
|
| 110 |
className="w-full flex items-center gap-2 px-4 py-2 text-sm text-red-400 hover:bg-white/5 transition-colors"
|
| 111 |
>
|
FRRONTEEEND/components/AuthPage.tsx
CHANGED
|
@@ -26,6 +26,8 @@ import {
|
|
| 26 |
import { cn } from "../lib/utils";
|
| 27 |
import { useAuth } from "../lib/AuthContext";
|
| 28 |
import { Logo } from "./Logo";
|
|
|
|
|
|
|
| 29 |
|
| 30 |
const steps = [
|
| 31 |
{ id: "personal", title: "Personal Info" },
|
|
@@ -63,7 +65,7 @@ interface AuthPageProps {
|
|
| 63 |
}
|
| 64 |
|
| 65 |
export const AuthPage: React.FC<AuthPageProps> = ({ onSuccess, onSkip }) => {
|
| 66 |
-
const { signIn, signUp, signInWithGoogle, signInWithGithub, isConfigured } = useAuth();
|
| 67 |
const [mode, setMode] = useState<'signin' | 'signup'>('signin');
|
| 68 |
const [currentStep, setCurrentStep] = useState(0);
|
| 69 |
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
@@ -84,6 +86,18 @@ export const AuthPage: React.FC<AuthPageProps> = ({ onSuccess, onSkip }) => {
|
|
| 84 |
industry: "",
|
| 85 |
});
|
| 86 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
const updateFormData = (field: keyof FormData, value: string) => {
|
| 88 |
setFormData((prev) => ({ ...prev, [field]: value }));
|
| 89 |
setError(null);
|
|
@@ -139,22 +153,66 @@ export const AuthPage: React.FC<AuthPageProps> = ({ onSuccess, onSkip }) => {
|
|
| 139 |
setIsSubmitting(true);
|
| 140 |
setError(null);
|
| 141 |
|
| 142 |
-
if
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
return;
|
| 146 |
-
}
|
| 147 |
-
|
| 148 |
try {
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
|
|
|
|
|
|
| 152 |
} else {
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
} catch (err: any) {
|
| 159 |
if (err.message?.includes('Failed to fetch')) {
|
| 160 |
setError('Unable to connect to authentication server. Please try again later.');
|
|
@@ -192,8 +250,15 @@ export const AuthPage: React.FC<AuthPageProps> = ({ onSuccess, onSkip }) => {
|
|
| 192 |
};
|
| 193 |
|
| 194 |
const isStepValid = () => {
|
|
|
|
|
|
|
|
|
|
| 195 |
switch (currentStep) {
|
| 196 |
case 0:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
return formData.name.trim() !== "" && formData.email.trim() !== "" &&
|
| 198 |
formData.password.length >= 6 && formData.password === formData.confirmPassword;
|
| 199 |
case 1:
|
|
@@ -481,57 +546,66 @@ export const AuthPage: React.FC<AuthPageProps> = ({ onSuccess, onSkip }) => {
|
|
| 481 |
/>
|
| 482 |
</div>
|
| 483 |
</motion.div>
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
<
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
<
|
| 510 |
-
<
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 519 |
)}
|
| 520 |
-
/>
|
| 521 |
-
</
|
| 522 |
-
|
| 523 |
-
<p className="text-xs text-red-400">Passwords don't match</p>
|
| 524 |
-
)}
|
| 525 |
-
</motion.div>
|
| 526 |
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
|
|
|
|
|
|
|
|
|
| 535 |
|
| 536 |
<div className="flex gap-3">
|
| 537 |
<Button
|
|
@@ -560,6 +634,8 @@ export const AuthPage: React.FC<AuthPageProps> = ({ onSuccess, onSkip }) => {
|
|
| 560 |
GitHub
|
| 561 |
</Button>
|
| 562 |
</div>
|
|
|
|
|
|
|
| 563 |
</CardContent>
|
| 564 |
</>
|
| 565 |
)}
|
|
|
|
| 26 |
import { cn } from "../lib/utils";
|
| 27 |
import { useAuth } from "../lib/AuthContext";
|
| 28 |
import { Logo } from "./Logo";
|
| 29 |
+
import { saveUserProfile } from "../lib/supabase";
|
| 30 |
+
import { supabase } from "../lib/supabase";
|
| 31 |
|
| 32 |
const steps = [
|
| 33 |
{ id: "personal", title: "Personal Info" },
|
|
|
|
| 65 |
}
|
| 66 |
|
| 67 |
export const AuthPage: React.FC<AuthPageProps> = ({ onSuccess, onSkip }) => {
|
| 68 |
+
const { signIn, signUp, signInWithGoogle, signInWithGithub, isConfigured, user } = useAuth();
|
| 69 |
const [mode, setMode] = useState<'signin' | 'signup'>('signin');
|
| 70 |
const [currentStep, setCurrentStep] = useState(0);
|
| 71 |
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
|
|
| 86 |
industry: "",
|
| 87 |
});
|
| 88 |
|
| 89 |
+
// If user is already authenticated (OAuth), pre-fill email and switch to signup mode for onboarding
|
| 90 |
+
React.useEffect(() => {
|
| 91 |
+
if (user && user.email) {
|
| 92 |
+
setFormData(prev => ({
|
| 93 |
+
...prev,
|
| 94 |
+
email: user.email || '',
|
| 95 |
+
name: user.user_metadata?.full_name || user.user_metadata?.name || ''
|
| 96 |
+
}));
|
| 97 |
+
setMode('signup');
|
| 98 |
+
}
|
| 99 |
+
}, [user]);
|
| 100 |
+
|
| 101 |
const updateFormData = (field: keyof FormData, value: string) => {
|
| 102 |
setFormData((prev) => ({ ...prev, [field]: value }));
|
| 103 |
setError(null);
|
|
|
|
| 153 |
setIsSubmitting(true);
|
| 154 |
setError(null);
|
| 155 |
|
| 156 |
+
// Check if user is already authenticated (OAuth flow)
|
| 157 |
+
const isOAuthUser = !!user;
|
| 158 |
+
|
|
|
|
|
|
|
|
|
|
| 159 |
try {
|
| 160 |
+
let userId: string;
|
| 161 |
+
|
| 162 |
+
if (isOAuthUser) {
|
| 163 |
+
// User already authenticated via OAuth, just save profile
|
| 164 |
+
userId = user.id;
|
| 165 |
} else {
|
| 166 |
+
// Email/password signup
|
| 167 |
+
if (formData.password !== formData.confirmPassword) {
|
| 168 |
+
setError("Passwords don't match");
|
| 169 |
+
setIsSubmitting(false);
|
| 170 |
+
return;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
const { error } = await signUp(formData.email, formData.password);
|
| 174 |
+
if (error) {
|
| 175 |
+
setError(error.message);
|
| 176 |
+
setIsSubmitting(false);
|
| 177 |
+
return;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
// Wait for Supabase to create the auth user
|
| 181 |
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
| 182 |
+
|
| 183 |
+
// Get the user ID from auth session
|
| 184 |
+
const { data: { session } } = await supabase.auth.getSession();
|
| 185 |
+
if (!session?.user) {
|
| 186 |
+
setError('Failed to get user session. Please sign in to continue.');
|
| 187 |
+
setIsSubmitting(false);
|
| 188 |
+
return;
|
| 189 |
+
}
|
| 190 |
+
userId = session.user.id;
|
| 191 |
}
|
| 192 |
+
|
| 193 |
+
// Save user profile data to database
|
| 194 |
+
const profileData = {
|
| 195 |
+
user_id: userId,
|
| 196 |
+
name: formData.name,
|
| 197 |
+
email: formData.email,
|
| 198 |
+
primary_goal: formData.primaryGoal,
|
| 199 |
+
target_outcome: formData.targetOutcome,
|
| 200 |
+
data_types: formData.dataTypes,
|
| 201 |
+
profession: formData.profession,
|
| 202 |
+
experience: formData.experience,
|
| 203 |
+
industry: formData.industry,
|
| 204 |
+
onboarding_completed: true
|
| 205 |
+
};
|
| 206 |
+
|
| 207 |
+
const savedProfile = await saveUserProfile(profileData);
|
| 208 |
+
if (!savedProfile) {
|
| 209 |
+
console.warn('Failed to save profile data, but auth succeeded');
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
setSuccess(isOAuthUser ? 'Profile completed! Redirecting...' : 'Account created successfully! Redirecting...');
|
| 213 |
+
setTimeout(() => {
|
| 214 |
+
onSuccess?.();
|
| 215 |
+
}, 1500);
|
| 216 |
} catch (err: any) {
|
| 217 |
if (err.message?.includes('Failed to fetch')) {
|
| 218 |
setError('Unable to connect to authentication server. Please try again later.');
|
|
|
|
| 250 |
};
|
| 251 |
|
| 252 |
const isStepValid = () => {
|
| 253 |
+
// For OAuth users (already authenticated), skip password validation
|
| 254 |
+
const isOAuthUser = !!user;
|
| 255 |
+
|
| 256 |
switch (currentStep) {
|
| 257 |
case 0:
|
| 258 |
+
if (isOAuthUser) {
|
| 259 |
+
// OAuth users don't need password fields
|
| 260 |
+
return formData.name.trim() !== "" && formData.email.trim() !== "";
|
| 261 |
+
}
|
| 262 |
return formData.name.trim() !== "" && formData.email.trim() !== "" &&
|
| 263 |
formData.password.length >= 6 && formData.password === formData.confirmPassword;
|
| 264 |
case 1:
|
|
|
|
| 546 |
/>
|
| 547 |
</div>
|
| 548 |
</motion.div>
|
| 549 |
+
|
| 550 |
+
{/* Only show password fields for email/password signup (not OAuth) */}
|
| 551 |
+
{!user && (
|
| 552 |
+
<>
|
| 553 |
+
<motion.div variants={fadeInUp} className="space-y-2">
|
| 554 |
+
<Label htmlFor="signup-password" className="text-white/70">Password</Label>
|
| 555 |
+
<div className="relative">
|
| 556 |
+
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-white/30" />
|
| 557 |
+
<Input
|
| 558 |
+
id="signup-password"
|
| 559 |
+
type={showPassword ? "text" : "password"}
|
| 560 |
+
placeholder="••••••••"
|
| 561 |
+
value={formData.password}
|
| 562 |
+
onChange={(e) => updateFormData("password", e.target.value)}
|
| 563 |
+
className="pl-10 pr-10 bg-white/5 border-white/10 text-white placeholder:text-white/30 focus:border-indigo-500/50"
|
| 564 |
+
/>
|
| 565 |
+
<button
|
| 566 |
+
type="button"
|
| 567 |
+
onClick={() => setShowPassword(!showPassword)}
|
| 568 |
+
className="absolute right-3 top-1/2 -translate-y-1/2 text-white/30 hover:text-white/50"
|
| 569 |
+
>
|
| 570 |
+
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
| 571 |
+
</button>
|
| 572 |
+
</div>
|
| 573 |
+
<p className="text-xs text-white/40">Minimum 6 characters</p>
|
| 574 |
+
</motion.div>
|
| 575 |
+
<motion.div variants={fadeInUp} className="space-y-2">
|
| 576 |
+
<Label htmlFor="confirm-password" className="text-white/70">Confirm Password</Label>
|
| 577 |
+
<div className="relative">
|
| 578 |
+
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-white/30" />
|
| 579 |
+
<Input
|
| 580 |
+
id="confirm-password"
|
| 581 |
+
type={showPassword ? "text" : "password"}
|
| 582 |
+
placeholder="••••••••"
|
| 583 |
+
value={formData.confirmPassword}
|
| 584 |
+
onChange={(e) => updateFormData("confirmPassword", e.target.value)}
|
| 585 |
+
className={cn(
|
| 586 |
+
"pl-10 bg-white/5 border-white/10 text-white placeholder:text-white/30 focus:border-indigo-500/50",
|
| 587 |
+
formData.confirmPassword && formData.password !== formData.confirmPassword && "border-red-500/50"
|
| 588 |
+
)}
|
| 589 |
+
/>
|
| 590 |
+
</div>
|
| 591 |
+
{formData.confirmPassword && formData.password !== formData.confirmPassword && (
|
| 592 |
+
<p className="text-xs text-red-400">Passwords don't match</p>
|
| 593 |
)}
|
| 594 |
+
</motion.div>
|
| 595 |
+
</>
|
| 596 |
+
)}
|
|
|
|
|
|
|
|
|
|
| 597 |
|
| 598 |
+
{/* Only show OAuth buttons for non-authenticated users */}
|
| 599 |
+
{!user && (
|
| 600 |
+
<>
|
| 601 |
+
<div className="relative my-4">
|
| 602 |
+
<div className="absolute inset-0 flex items-center">
|
| 603 |
+
<div className="w-full border-t border-white/10"></div>
|
| 604 |
+
</div>
|
| 605 |
+
<div className="relative flex justify-center text-sm">
|
| 606 |
+
<span className="px-4 bg-[#0a0a0a] text-white/40">or sign up with</span>
|
| 607 |
+
</div>
|
| 608 |
+
</div>
|
| 609 |
|
| 610 |
<div className="flex gap-3">
|
| 611 |
<Button
|
|
|
|
| 634 |
GitHub
|
| 635 |
</Button>
|
| 636 |
</div>
|
| 637 |
+
</>
|
| 638 |
+
)}
|
| 639 |
</CardContent>
|
| 640 |
</>
|
| 641 |
)}
|
FRRONTEEEND/lib/AuthContext.tsx
CHANGED
|
@@ -1,12 +1,13 @@
|
|
| 1 |
import React, { createContext, useContext, useEffect, useState } from 'react';
|
| 2 |
import { User, Session, AuthChangeEvent } from '@supabase/supabase-js';
|
| 3 |
-
import { supabase, startUserSession, endUserSession, isSupabaseConfigured } from './supabase';
|
| 4 |
|
| 5 |
interface AuthContextType {
|
| 6 |
user: User | null;
|
| 7 |
session: Session | null;
|
| 8 |
dbSessionId: string | null;
|
| 9 |
loading: boolean;
|
|
|
|
| 10 |
signIn: (email: string, password: string) => Promise<{ error: any }>;
|
| 11 |
signUp: (email: string, password: string) => Promise<{ error: any }>;
|
| 12 |
signInWithGoogle: () => Promise<{ error: any }>;
|
|
@@ -23,6 +24,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|
| 23 |
const [session, setSession] = useState<Session | null>(null);
|
| 24 |
const [dbSessionId, setDbSessionId] = useState<string | null>(null);
|
| 25 |
const [loading, setLoading] = useState(true);
|
|
|
|
| 26 |
const configured = isSupabaseConfigured();
|
| 27 |
|
| 28 |
useEffect(() => {
|
|
@@ -44,6 +46,11 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|
| 44 |
if (dbSession) {
|
| 45 |
setDbSessionId(dbSession.id);
|
| 46 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
});
|
| 48 |
}
|
| 49 |
}).catch((err) => {
|
|
@@ -64,12 +71,17 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|
| 64 |
if (dbSession) {
|
| 65 |
setDbSessionId(dbSession.id);
|
| 66 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
} else if (event === 'SIGNED_OUT') {
|
| 68 |
// End tracking session
|
| 69 |
if (dbSessionId) {
|
| 70 |
await endUserSession(dbSessionId);
|
| 71 |
setDbSessionId(null);
|
| 72 |
}
|
|
|
|
| 73 |
}
|
| 74 |
}
|
| 75 |
);
|
|
@@ -115,11 +127,20 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|
| 115 |
};
|
| 116 |
|
| 117 |
const signOut = async () => {
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
}
|
| 122 |
-
await supabase.auth.signOut();
|
| 123 |
};
|
| 124 |
|
| 125 |
return (
|
|
@@ -129,6 +150,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|
| 129 |
session,
|
| 130 |
dbSessionId,
|
| 131 |
loading,
|
|
|
|
| 132 |
signIn,
|
| 133 |
signUp,
|
| 134 |
signInWithGoogle,
|
|
|
|
| 1 |
import React, { createContext, useContext, useEffect, useState } from 'react';
|
| 2 |
import { User, Session, AuthChangeEvent } from '@supabase/supabase-js';
|
| 3 |
+
import { supabase, startUserSession, endUserSession, isSupabaseConfigured, getUserProfile } from './supabase';
|
| 4 |
|
| 5 |
interface AuthContextType {
|
| 6 |
user: User | null;
|
| 7 |
session: Session | null;
|
| 8 |
dbSessionId: string | null;
|
| 9 |
loading: boolean;
|
| 10 |
+
needsOnboarding: boolean;
|
| 11 |
signIn: (email: string, password: string) => Promise<{ error: any }>;
|
| 12 |
signUp: (email: string, password: string) => Promise<{ error: any }>;
|
| 13 |
signInWithGoogle: () => Promise<{ error: any }>;
|
|
|
|
| 24 |
const [session, setSession] = useState<Session | null>(null);
|
| 25 |
const [dbSessionId, setDbSessionId] = useState<string | null>(null);
|
| 26 |
const [loading, setLoading] = useState(true);
|
| 27 |
+
const [needsOnboarding, setNeedsOnboarding] = useState(false);
|
| 28 |
const configured = isSupabaseConfigured();
|
| 29 |
|
| 30 |
useEffect(() => {
|
|
|
|
| 46 |
if (dbSession) {
|
| 47 |
setDbSessionId(dbSession.id);
|
| 48 |
}
|
| 49 |
+
|
| 50 |
+
// Check if user needs onboarding
|
| 51 |
+
getUserProfile(session.user.id).then((profile) => {
|
| 52 |
+
setNeedsOnboarding(!profile || !profile.onboarding_completed);
|
| 53 |
+
});
|
| 54 |
});
|
| 55 |
}
|
| 56 |
}).catch((err) => {
|
|
|
|
| 71 |
if (dbSession) {
|
| 72 |
setDbSessionId(dbSession.id);
|
| 73 |
}
|
| 74 |
+
|
| 75 |
+
// Check if user needs onboarding
|
| 76 |
+
const profile = await getUserProfile(session.user.id);
|
| 77 |
+
setNeedsOnboarding(!profile || !profile.onboarding_completed);
|
| 78 |
} else if (event === 'SIGNED_OUT') {
|
| 79 |
// End tracking session
|
| 80 |
if (dbSessionId) {
|
| 81 |
await endUserSession(dbSessionId);
|
| 82 |
setDbSessionId(null);
|
| 83 |
}
|
| 84 |
+
setNeedsOnboarding(false);
|
| 85 |
}
|
| 86 |
}
|
| 87 |
);
|
|
|
|
| 127 |
};
|
| 128 |
|
| 129 |
const signOut = async () => {
|
| 130 |
+
try {
|
| 131 |
+
if (dbSessionId) {
|
| 132 |
+
await endUserSession(dbSessionId);
|
| 133 |
+
setDbSessionId(null);
|
| 134 |
+
}
|
| 135 |
+
const { error } = await supabase.auth.signOut();
|
| 136 |
+
if (error) {
|
| 137 |
+
console.error('Sign out error:', error);
|
| 138 |
+
throw error;
|
| 139 |
+
}
|
| 140 |
+
} catch (error) {
|
| 141 |
+
console.error('Sign out failed:', error);
|
| 142 |
+
throw error;
|
| 143 |
}
|
|
|
|
| 144 |
};
|
| 145 |
|
| 146 |
return (
|
|
|
|
| 150 |
session,
|
| 151 |
dbSessionId,
|
| 152 |
loading,
|
| 153 |
+
needsOnboarding,
|
| 154 |
signIn,
|
| 155 |
signUp,
|
| 156 |
signInWithGoogle,
|
FRRONTEEEND/lib/supabase.ts
CHANGED
|
@@ -206,3 +206,70 @@ export const getUniqueUsersCount = async (days: number = 7) => {
|
|
| 206 |
return 0;
|
| 207 |
}
|
| 208 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
return 0;
|
| 207 |
}
|
| 208 |
};
|
| 209 |
+
|
| 210 |
+
// User profile management
|
| 211 |
+
export interface UserProfile {
|
| 212 |
+
id?: string;
|
| 213 |
+
user_id: string;
|
| 214 |
+
name: string;
|
| 215 |
+
email: string;
|
| 216 |
+
primary_goal?: string;
|
| 217 |
+
target_outcome?: string;
|
| 218 |
+
data_types?: string[];
|
| 219 |
+
profession?: string;
|
| 220 |
+
experience?: string;
|
| 221 |
+
industry?: string;
|
| 222 |
+
onboarding_completed: boolean;
|
| 223 |
+
created_at?: string;
|
| 224 |
+
updated_at?: string;
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
// Create or update user profile (for signup form data)
|
| 228 |
+
export const saveUserProfile = async (profile: Omit<UserProfile, 'id' | 'created_at' | 'updated_at'>) => {
|
| 229 |
+
try {
|
| 230 |
+
const { data, error } = await supabase
|
| 231 |
+
.from('user_profiles')
|
| 232 |
+
.upsert([{
|
| 233 |
+
...profile,
|
| 234 |
+
updated_at: new Date().toISOString()
|
| 235 |
+
}], {
|
| 236 |
+
onConflict: 'user_id'
|
| 237 |
+
})
|
| 238 |
+
.select()
|
| 239 |
+
.single();
|
| 240 |
+
|
| 241 |
+
if (error) {
|
| 242 |
+
console.error('Failed to save user profile:', error);
|
| 243 |
+
return null;
|
| 244 |
+
}
|
| 245 |
+
return data;
|
| 246 |
+
} catch (err) {
|
| 247 |
+
console.error('Profile save error:', err);
|
| 248 |
+
return null;
|
| 249 |
+
}
|
| 250 |
+
};
|
| 251 |
+
|
| 252 |
+
// Check if user has completed onboarding
|
| 253 |
+
export const getUserProfile = async (userId: string) => {
|
| 254 |
+
try {
|
| 255 |
+
const { data, error } = await supabase
|
| 256 |
+
.from('user_profiles')
|
| 257 |
+
.select('*')
|
| 258 |
+
.eq('user_id', userId)
|
| 259 |
+
.single();
|
| 260 |
+
|
| 261 |
+
if (error) {
|
| 262 |
+
// User not found is not an error (first time user)
|
| 263 |
+
if (error.code === 'PGRST116') {
|
| 264 |
+
return null;
|
| 265 |
+
}
|
| 266 |
+
console.error('Failed to get user profile:', error);
|
| 267 |
+
return null;
|
| 268 |
+
}
|
| 269 |
+
return data as UserProfile;
|
| 270 |
+
} catch (err) {
|
| 271 |
+
console.error('Profile fetch error:', err);
|
| 272 |
+
return null;
|
| 273 |
+
}
|
| 274 |
+
};
|
| 275 |
+
|
supabase_schema.sql
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-- User Profiles Table
|
| 2 |
+
-- Stores onboarding form data for each user
|
| 3 |
+
CREATE TABLE IF NOT EXISTS user_profiles (
|
| 4 |
+
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
| 5 |
+
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
| 6 |
+
name TEXT NOT NULL,
|
| 7 |
+
email TEXT NOT NULL,
|
| 8 |
+
primary_goal TEXT,
|
| 9 |
+
target_outcome TEXT,
|
| 10 |
+
data_types TEXT[],
|
| 11 |
+
profession TEXT,
|
| 12 |
+
experience TEXT,
|
| 13 |
+
industry TEXT,
|
| 14 |
+
onboarding_completed BOOLEAN DEFAULT FALSE,
|
| 15 |
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
| 16 |
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
| 17 |
+
UNIQUE(user_id)
|
| 18 |
+
);
|
| 19 |
+
|
| 20 |
+
-- Index for faster lookups
|
| 21 |
+
CREATE INDEX IF NOT EXISTS idx_user_profiles_user_id ON user_profiles(user_id);
|
| 22 |
+
|
| 23 |
+
-- RLS (Row Level Security) Policies
|
| 24 |
+
ALTER TABLE user_profiles ENABLE ROW LEVEL SECURITY;
|
| 25 |
+
|
| 26 |
+
-- Allow users to read their own profile
|
| 27 |
+
CREATE POLICY "Users can read own profile" ON user_profiles
|
| 28 |
+
FOR SELECT USING (auth.uid() = user_id);
|
| 29 |
+
|
| 30 |
+
-- Allow users to insert their own profile
|
| 31 |
+
CREATE POLICY "Users can insert own profile" ON user_profiles
|
| 32 |
+
FOR INSERT WITH CHECK (auth.uid() = user_id);
|
| 33 |
+
|
| 34 |
+
-- Allow users to update their own profile
|
| 35 |
+
CREATE POLICY "Users can update own profile" ON user_profiles
|
| 36 |
+
FOR UPDATE USING (auth.uid() = user_id);
|
| 37 |
+
|
| 38 |
+
-- Usage Analytics Table (if not exists)
|
| 39 |
+
CREATE TABLE IF NOT EXISTS usage_analytics (
|
| 40 |
+
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
| 41 |
+
user_id TEXT NOT NULL,
|
| 42 |
+
user_email TEXT,
|
| 43 |
+
session_id TEXT NOT NULL,
|
| 44 |
+
query TEXT NOT NULL,
|
| 45 |
+
agent_used TEXT,
|
| 46 |
+
tools_executed TEXT[],
|
| 47 |
+
tokens_used INTEGER,
|
| 48 |
+
duration_ms INTEGER,
|
| 49 |
+
success BOOLEAN NOT NULL,
|
| 50 |
+
error_message TEXT,
|
| 51 |
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
| 52 |
+
);
|
| 53 |
+
|
| 54 |
+
CREATE INDEX IF NOT EXISTS idx_usage_analytics_user_id ON usage_analytics(user_id);
|
| 55 |
+
CREATE INDEX IF NOT EXISTS idx_usage_analytics_created_at ON usage_analytics(created_at);
|
| 56 |
+
|
| 57 |
+
-- User Sessions Table (if not exists)
|
| 58 |
+
CREATE TABLE IF NOT EXISTS user_sessions (
|
| 59 |
+
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
| 60 |
+
user_id TEXT NOT NULL,
|
| 61 |
+
user_email TEXT,
|
| 62 |
+
started_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
| 63 |
+
ended_at TIMESTAMP WITH TIME ZONE,
|
| 64 |
+
queries_count INTEGER DEFAULT 0,
|
| 65 |
+
browser_info TEXT,
|
| 66 |
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
| 67 |
+
);
|
| 68 |
+
|
| 69 |
+
CREATE INDEX IF NOT EXISTS idx_user_sessions_user_id ON user_sessions(user_id);
|
| 70 |
+
CREATE INDEX IF NOT EXISTS idx_user_sessions_started_at ON user_sessions(started_at);
|
| 71 |
+
|
| 72 |
+
-- RPC function for incrementing queries count
|
| 73 |
+
CREATE OR REPLACE FUNCTION increment_session_queries(session_id UUID)
|
| 74 |
+
RETURNS VOID AS $$
|
| 75 |
+
BEGIN
|
| 76 |
+
UPDATE user_sessions
|
| 77 |
+
SET queries_count = queries_count + 1
|
| 78 |
+
WHERE id = session_id;
|
| 79 |
+
END;
|
| 80 |
+
$$ LANGUAGE plpgsql;
|