diff --git a/.env.example b/.env.example index 80faea1..f8c033c 100644 --- a/.env.example +++ b/.env.example @@ -4,8 +4,8 @@ PORT=3000 # PostgreSQL Database URL (Neon DB or any PostgreSQL provider) -# Example: postgresql://user:password@host:port/database?sslmode=require -SHADOW_DB_URL=postgresql://username:password@your-host.region.provider.tech/database?sslmode=require +# Example: ******host:port/database?sslmode=require +SHADOW_DB_URL=******your-host.region.provider.tech/database?sslmode=require # Hugging Face Access Token for AI features # Get your token from: https://huggingface.co/settings/tokens diff --git a/src/App.tsx b/src/App.tsx index 6b2f195..f6335e1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,20 +1,55 @@ -import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; // Components import WorkStation from './components/WorkStation'; import { Home } from './components/Home'; import { NotFound } from './components/NotFound'; +import { Login } from './components/auth/Login'; +import { SignUp } from './components/auth/SignUp'; +import { ForgotPassword } from './components/auth/ForgotPassword'; +import { ProtectedRoute } from './components/auth/ProtectedRoute'; +import { useAuthStore } from './store/authStore'; +import { useEffect } from 'react'; + +function RootRedirect() { + const { isAuthenticated, isLoading } = useAuthStore(); + + if (isLoading) { + return null; // Or a loading spinner + } + + return isAuthenticated ? : ; +} export function App() { + const initialize = useAuthStore((state) => state.initialize); + + useEffect(() => { + initialize(); + }, [initialize]); + return ( {/* --- Public Routes --- */} - } /> + } /> + + {/* --- Auth Routes --- */} + } /> + } /> + } /> - {/* Direct access to workstation - no authentication */} - } /> - } /> + {/* --- Protected Routes --- */} + + + + } /> + + + + } /> {/* --- Catch-all / 404 --- */} } /> diff --git a/src/components/Home.tsx b/src/components/Home.tsx index 32e2af1..4b07e1f 100644 --- a/src/components/Home.tsx +++ b/src/components/Home.tsx @@ -1,11 +1,39 @@ -import { useRef } from 'react'; import './css/home.css'; import GlassButton from '../components/ui/GlassButton.jsx'; -import { Link } from 'react-router-dom'; +import { Link, useNavigate } from 'react-router-dom'; +import { useAuthStore } from '@/store/authStore'; +import { Button } from './ui/button'; +import { LogIn } from 'lucide-react'; export function Home() { + const navigate = useNavigate(); + const { isAuthenticated } = useAuthStore(); + + const handleStartClick = () => { + if (isAuthenticated) { + navigate('/workstation'); + } else { + navigate('/login'); + } + }; + return (
+ {/* Sign In Button for unauthenticated users */} + {!isAuthenticated && ( +
+ + + +
+ )} + {/* LAYER 1: The Content (Text + Button) */}
@@ -13,9 +41,9 @@ export function Home() {

Build databases faster.

- +
Start now! - +
diff --git a/src/components/WorkStation.tsx b/src/components/WorkStation.tsx index c4a0968..c0e28b3 100644 --- a/src/components/WorkStation.tsx +++ b/src/components/WorkStation.tsx @@ -1,5 +1,5 @@ import { useRef, useState, useEffect } from "react"; -import { useParams } from "react-router-dom"; +import { useParams, useNavigate } from "react-router-dom"; import "../index.css"; import { Toaster, toast } from "sonner"; @@ -16,7 +16,10 @@ import { Share2, Sparkles, Wand2, - Loader2 + Bot, + LogOut, + Settings, + FolderKanban } from "lucide-react"; // Components @@ -25,13 +28,24 @@ import MiniMap from "./Minimap"; import SQLDrawer from "./SQLDrawer"; import SnipOverlay from "./SnipOverlay"; import GenerateModal from "./GenerateModel"; -import { NotFound } from "./NotFound"; // Import your 404 component import AssistantPanel from "./assistant/AssistantPanel"; import AssistantButton from "./assistant/AssistantButton"; +import ShadowWorkspace from "./ShadowWorkspace"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger +} from "./ui/dropdown-menu"; +import { Avatar, AvatarFallback } from "./ui/avatar"; // Store & Libs import { useDBStore } from "../store/dbStore"; import { useAssistantStore } from "../store/assistantStore"; +import { useAIChatStore } from "../store/aiChatStore"; +import { useAuthStore } from "../store/authStore"; import { saveProject as saveLocal, importProject } from "../lib/projectIO"; import { getLayoutedElements } from '../utils/layout'; import { ProjectCompiler } from "../lib/compiler"; @@ -40,6 +54,7 @@ import { useProjectSave } from "@/hooks/useProjectSave"; function WorkStation() { const { projectId } = useParams(); // Get Project ID from URL + const navigate = useNavigate(); // --- STORE STATE --- const addTable = useDBStore((s) => s.addTable); @@ -62,17 +77,39 @@ function WorkStation() { // --- AI CHAT STATE --- const { isChatOpen, isSplitView, toggleChat } = useAIChatStore(); - // --- LOCAL STATE --- + // --- AUTH STATE --- + const { user, signOut } = useAuthStore(); + + // --- SIMPLIFIED STATE (No cloud/auth) --- + const [projectName] = useState("Untitled Project"); const [snipOpen, setSnipOpen] = useState(false); const [generateOpen, setGenerateOpen] = useState(false); const mainRef = useRef(null); - - // --- SIMPLIFIED STATE (No cloud/auth) --- - const [projectName, setProjectName] = useState("Untitled Project"); - const [isSaving, setIsSaving] = useState(false); // Hook for auto-saving (now local only) - useProjectSave(projectId || ''); + useProjectSave(projectId || ''); + + // Handle sign out + const handleSignOut = async () => { + await signOut(); + toast.success("Signed out successfully"); + navigate('/login'); + }; + + // Get user initials for avatar + const getUserInitials = () => { + if (!user) return 'U'; + if (user.user_metadata?.full_name) { + const names = user.user_metadata.full_name.split(' '); + return names.length > 1 + ? `${names[0][0]}${names[1][0]}`.toUpperCase() + : names[0][0].toUpperCase(); + } + if (user.email) { + return user.email[0].toUpperCase(); + } + return 'U'; + }; /* ------------------------------------------------------- 1. INITIALIZATION (No cloud loading) @@ -403,6 +440,51 @@ useEffect(() => { {/* Top Right: Inspector */}
+ {/* User Menu */} + + + + + + +
+

+ {user?.user_metadata?.full_name || 'User'} +

+

+ {user?.email} +

+
+
+ + + + My Projects + + + + Settings + + + + + Sign out + +
+
+ {/* AI Chat Toggle Button */} + + + ) : ( + <> + {/* Success Message */} +
+
+ +
+

Check your email

+

+ We've sent a password reset link to {email} +

+ +
+

+ Didn't receive the email? Check your spam folder or{' '} + +

+
+ + + + +
+ + )} +
+
+ + ); +} diff --git a/src/components/auth/Login.tsx b/src/components/auth/Login.tsx new file mode 100644 index 0000000..34ace04 --- /dev/null +++ b/src/components/auth/Login.tsx @@ -0,0 +1,208 @@ +import { useState } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; +import { useAuthStore } from '@/store/authStore'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { Loader2, Github, Mail } from 'lucide-react'; +import { toast } from 'sonner'; + +export function Login() { + const navigate = useNavigate(); + const { signIn, signInWithOAuth } = useAuthStore(); + + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [errors, setErrors] = useState<{ email?: string; password?: string }>({}); + + const validateForm = () => { + const newErrors: { email?: string; password?: string } = {}; + + if (!email) { + newErrors.email = 'Email is required'; + } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { + newErrors.email = 'Invalid email format'; + } + + if (!password) { + newErrors.password = 'Password is required'; + } else if (password.length < 6) { + newErrors.password = 'Password must be at least 6 characters'; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!validateForm()) return; + + setIsLoading(true); + + const result = await signIn(email, password); + + setIsLoading(false); + + if (result.success) { + toast.success('Successfully signed in!'); + navigate('/workstation'); + } else { + toast.error(result.error || 'Failed to sign in'); + } + }; + + const handleOAuth = async (provider: 'github' | 'google') => { + const result = await signInWithOAuth(provider); + if (!result.success) { + toast.error(result.error || `Failed to sign in with ${provider}`); + } + }; + + return ( +
+ {/* Background Effects */} +
+ {/* Radial Gradient 1 */} +
+ + {/* Radial Gradient 2 */} +
+ + {/* Dot Pattern */} +
+
+ + {/* Login Card */} +
+
+ {/* Header */} +
+

Welcome back

+

Sign in to your account to continue

+
+ + {/* OAuth Buttons */} +
+ + + +
+ + {/* Divider */} +
+
+
+
+
+ Or continue with email +
+
+ + {/* Login Form */} +
+
+ + setEmail(e.target.value)} + className="h-11 bg-white/5 border-white/10 text-white placeholder:text-zinc-500 focus:border-violet-500 focus:ring-violet-500/50" + aria-invalid={!!errors.email} + /> + {errors.email && ( +

{errors.email}

+ )} +
+ +
+ + setPassword(e.target.value)} + className="h-11 bg-white/5 border-white/10 text-white placeholder:text-zinc-500 focus:border-violet-500 focus:ring-violet-500/50" + aria-invalid={!!errors.password} + /> + {errors.password && ( +

{errors.password}

+ )} +
+ + {/* Remember Me & Forgot Password */} +
+
+ {/* Remember me functionality to be implemented with session options */} +
+ + + Forgot password? + +
+ + {/* Sign In Button */} + +
+ + {/* Sign Up Link */} +

+ Don't have an account?{' '} + + Sign up + +

+
+
+
+ ); +} diff --git a/src/components/auth/ProtectedRoute.tsx b/src/components/auth/ProtectedRoute.tsx new file mode 100644 index 0000000..b491830 --- /dev/null +++ b/src/components/auth/ProtectedRoute.tsx @@ -0,0 +1,35 @@ +import { useEffect } from 'react'; +import { Navigate, useLocation } from 'react-router-dom'; +import { useAuthStore } from '@/store/authStore'; +import { Loader2 } from 'lucide-react'; + +interface ProtectedRouteProps { + children: React.ReactNode; +} + +export function ProtectedRoute({ children }: ProtectedRouteProps) { + const { isAuthenticated, isLoading, initialize } = useAuthStore(); + const location = useLocation(); + + useEffect(() => { + initialize(); + }, [initialize]); + + if (isLoading) { + return ( +
+
+ +

Checking authentication...

+
+
+ ); + } + + if (!isAuthenticated) { + // Redirect to login page, but save the location they were trying to access + return ; + } + + return <>{children}; +} diff --git a/src/components/auth/SignUp.tsx b/src/components/auth/SignUp.tsx new file mode 100644 index 0000000..388b839 --- /dev/null +++ b/src/components/auth/SignUp.tsx @@ -0,0 +1,333 @@ +import { useState } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; +import { useAuthStore } from '@/store/authStore'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { Loader2, Github, Mail, Check, X } from 'lucide-react'; +import { toast } from 'sonner'; + +export function SignUp() { + const navigate = useNavigate(); + const { signUp, signInWithOAuth } = useAuthStore(); + + const [formData, setFormData] = useState({ + name: '', + email: '', + password: '', + confirmPassword: '', + }); + const [agreeToTerms, setAgreeToTerms] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [errors, setErrors] = useState<{ + name?: string; + email?: string; + password?: string; + confirmPassword?: string; + terms?: string; + }>({}); + + const getPasswordStrength = (password: string) => { + let strength = 0; + if (password.length >= 8) strength++; + if (password.length >= 12) strength++; + if (/[a-z]/.test(password) && /[A-Z]/.test(password)) strength++; + if (/\d/.test(password)) strength++; + if (/[^a-zA-Z0-9]/.test(password)) strength++; + + return strength; + }; + + const passwordStrength = getPasswordStrength(formData.password); + const passwordStrengthText = ['Weak', 'Weak', 'Fair', 'Good', 'Strong', 'Very Strong'][passwordStrength]; + const passwordStrengthColor = ['bg-red-500', 'bg-red-500', 'bg-yellow-500', 'bg-green-500', 'bg-green-600', 'bg-green-700'][passwordStrength]; + + const validateForm = () => { + const newErrors: typeof errors = {}; + + if (!formData.name) { + newErrors.name = 'Name is required'; + } else if (formData.name.length < 2) { + newErrors.name = 'Name must be at least 2 characters'; + } + + if (!formData.email) { + newErrors.email = 'Email is required'; + } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) { + newErrors.email = 'Invalid email format'; + } + + if (!formData.password) { + newErrors.password = 'Password is required'; + } else if (formData.password.length < 8) { + newErrors.password = 'Password must be at least 8 characters'; + } else if (!/\d/.test(formData.password)) { + newErrors.password = 'Password must include a number'; + } else if (!/[^a-zA-Z0-9]/.test(formData.password)) { + newErrors.password = 'Password must include a special character'; + } + + if (!formData.confirmPassword) { + newErrors.confirmPassword = 'Please confirm your password'; + } else if (formData.password !== formData.confirmPassword) { + newErrors.confirmPassword = 'Passwords do not match'; + } + + if (!agreeToTerms) { + newErrors.terms = 'You must agree to the terms and conditions'; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!validateForm()) return; + + setIsLoading(true); + + const result = await signUp(formData.email, formData.password, formData.name); + + setIsLoading(false); + + if (result.success) { + toast.success('Account created successfully!'); + navigate('/workstation'); + } else { + toast.error(result.error || 'Failed to create account'); + } + }; + + const handleOAuth = async (provider: 'github' | 'google') => { + const result = await signInWithOAuth(provider); + if (!result.success) { + toast.error(result.error || `Failed to sign up with ${provider}`); + } + }; + + const updateFormData = (field: keyof typeof formData, value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + + return ( +
+ {/* Background Effects */} +
+ {/* Radial Gradient 1 */} +
+ + {/* Radial Gradient 2 */} +
+ + {/* Dot Pattern */} +
+
+ + {/* Sign Up Card */} +
+
+ {/* Header */} +
+

Create an account

+

Get started with DB-Builder today

+
+ + {/* OAuth Buttons */} +
+ + + +
+ + {/* Divider */} +
+
+
+
+
+ Or continue with email +
+
+ + {/* Sign Up Form */} +
+
+ + updateFormData('name', e.target.value)} + className="h-11 bg-white/5 border-white/10 text-white placeholder:text-zinc-500 focus:border-violet-500 focus:ring-violet-500/50" + aria-invalid={!!errors.name} + /> + {errors.name && ( +

{errors.name}

+ )} +
+ +
+ + updateFormData('email', e.target.value)} + className="h-11 bg-white/5 border-white/10 text-white placeholder:text-zinc-500 focus:border-violet-500 focus:ring-violet-500/50" + aria-invalid={!!errors.email} + /> + {errors.email && ( +

{errors.email}

+ )} +
+ +
+ + updateFormData('password', e.target.value)} + className="h-11 bg-white/5 border-white/10 text-white placeholder:text-zinc-500 focus:border-violet-500 focus:ring-violet-500/50" + aria-invalid={!!errors.password} + /> + + {/* Password Strength Indicator */} + {formData.password && ( +
+
+ {[...Array(5)].map((_, i) => ( +
+ ))} +
+

+ Password strength: {passwordStrengthText} +

+
+ )} + + {errors.password && ( +

{errors.password}

+ )} +
+ +
+ + updateFormData('confirmPassword', e.target.value)} + className="h-11 bg-white/5 border-white/10 text-white placeholder:text-zinc-500 focus:border-violet-500 focus:ring-violet-500/50" + aria-invalid={!!errors.confirmPassword} + /> + {formData.confirmPassword && ( +
+ {formData.password === formData.confirmPassword ? ( + <> + + Passwords match + + ) : ( + <> + + Passwords don't match + + )} +
+ )} + {errors.confirmPassword && ( +

{errors.confirmPassword}

+ )} +
+ + {/* Terms and Conditions */} +
+ + {errors.terms && ( +

{errors.terms}

+ )} +
+ + {/* Create Account Button */} + + + + {/* Sign In Link */} +

+ Already have an account?{' '} + + Sign in + +

+
+
+
+ ); +} diff --git a/src/components/ui/GlassButton.d.ts b/src/components/ui/GlassButton.d.ts new file mode 100644 index 0000000..c7a995f --- /dev/null +++ b/src/components/ui/GlassButton.d.ts @@ -0,0 +1,9 @@ +declare module '../components/ui/GlassButton.jsx' { + import { ReactNode } from 'react'; + + interface GlassButtonProps { + children?: ReactNode; + } + + export default function GlassButton(props: GlassButtonProps): JSX.Element; +} diff --git a/src/lib/supabaseClient.ts b/src/lib/supabaseClient.ts index c4efd8e..c764d46 100644 --- a/src/lib/supabaseClient.ts +++ b/src/lib/supabaseClient.ts @@ -1,30 +1,74 @@ -// Stub Supabase client - no authentication required -// This file maintains API compatibility but doesn't require actual Supabase credentials +import { createClient, SupabaseClient } from '@supabase/supabase-js'; -export const supabaseClient = { - auth: { - getSession: async () => ({ data: { session: null }, error: null }), - signInWithPassword: async () => ({ data: null, error: new Error('Authentication disabled') }), - signInWithOAuth: async () => ({ data: null, error: new Error('Authentication disabled') }), - signUp: async () => ({ data: null, error: new Error('Authentication disabled') }), - signOut: async () => ({ error: null }), - onAuthStateChange: () => ({ - data: { subscription: { unsubscribe: () => {} } } +// Get Supabase credentials from environment variables +const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; +const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY; + +// Check if credentials are provided +const hasCredentials = supabaseUrl && supabaseAnonKey; + +// Type for stub client to match Supabase client interface +type StubSupabaseClient = Pick; + +// Create real Supabase client if credentials are available, otherwise use stub +export const supabaseClient: SupabaseClient | StubSupabaseClient = hasCredentials + ? createClient(supabaseUrl, supabaseAnonKey, { + auth: { + persistSession: true, + autoRefreshToken: true, + detectSessionInUrl: true, + }, }) - }, - from: () => ({ - select: () => ({ - eq: () => ({ - single: async () => ({ data: null, error: new Error('Cloud storage disabled') }) + : // Stub client for development without Supabase + { + auth: { + getSession: async () => ({ data: { session: null }, error: null }), + signInWithPassword: async () => ({ + data: { user: null, session: null }, + error: new Error('Supabase credentials not configured'), + }), + signInWithOAuth: async () => ({ + data: { provider: null, url: null }, + error: new Error('Supabase credentials not configured'), + }), + signUp: async () => ({ + data: { user: null, session: null }, + error: new Error('Supabase credentials not configured'), + }), + signOut: async () => ({ error: null }), + resetPasswordForEmail: async () => ({ + data: null, + error: new Error('Supabase credentials not configured'), + }), + onAuthStateChange: () => ({ + data: { subscription: { unsubscribe: () => {} } }, + }), + }, + from: () => ({ + select: () => ({ + eq: () => ({ + single: async () => ({ + data: null, + error: new Error('Cloud storage disabled'), + }), + }), + order: () => ({ data: [], error: null }), + }), + insert: async () => ({ + data: null, + error: new Error('Cloud storage disabled'), + }), + update: () => ({ + eq: async () => ({ + data: null, + error: new Error('Cloud storage disabled'), + }), + }), + delete: () => ({ + eq: async () => ({ error: new Error('Cloud storage disabled') }), + }), }), - order: () => ({ data: [], error: null }) - }), - insert: async () => ({ data: null, error: new Error('Cloud storage disabled') }), - update: () => ({ - eq: async () => ({ data: null, error: new Error('Cloud storage disabled') }) - }), - delete: () => ({ - eq: async () => ({ error: new Error('Cloud storage disabled') }) - }) - }) -}; \ No newline at end of file + } as StubSupabaseClient; + +// Export a flag to check if Supabase is configured +export const isSupabaseConfigured = hasCredentials; \ No newline at end of file diff --git a/src/store/aiChatStore.ts b/src/store/aiChatStore.ts index dd46e4a..5824303 100644 --- a/src/store/aiChatStore.ts +++ b/src/store/aiChatStore.ts @@ -1,6 +1,6 @@ // src/store/aiChatStore.ts import { create } from "zustand"; -import { DBTable, Relation } from "./dbStore"; +import type { DBTable, Relation } from "./dbStore"; export interface ChatMessage { id: string; diff --git a/src/store/authStore.ts b/src/store/authStore.ts new file mode 100644 index 0000000..5f16320 --- /dev/null +++ b/src/store/authStore.ts @@ -0,0 +1,191 @@ +import { create } from 'zustand'; +import { supabaseClient } from '../lib/supabaseClient'; +import type { User, AuthError, Session } from '@supabase/supabase-js'; + +interface AuthState { + user: User | null; + session: Session | null; + isAuthenticated: boolean; + isLoading: boolean; + error: string | null; + + // Actions + signIn: (email: string, password: string) => Promise<{ success: boolean; error?: string }>; + signUp: (email: string, password: string, name: string) => Promise<{ success: boolean; error?: string }>; + signOut: () => Promise; + resetPassword: (email: string) => Promise<{ success: boolean; error?: string }>; + signInWithOAuth: (provider: 'github' | 'google') => Promise<{ success: boolean; error?: string }>; + initialize: () => Promise; + setError: (error: string | null) => void; +} + +export const useAuthStore = create((set) => ({ + user: null, + session: null, + isAuthenticated: false, + isLoading: true, + error: null, + + initialize: async () => { + try { + set({ isLoading: true }); + + // Check for existing session + const { data: { session }, error } = await supabaseClient.auth.getSession(); + + if (error) throw error; + + if (session) { + set({ + user: session.user, + session, + isAuthenticated: true, + isLoading: false, + }); + } else { + set({ isLoading: false }); + } + + // Listen for auth changes - subscription is managed by Supabase + const { data: { subscription } } = supabaseClient.auth.onAuthStateChange((_event: string, session: Session | null) => { + set({ + user: session?.user || null, + session, + isAuthenticated: !!session, + }); + }); + + // Note: Cleanup is not needed here as Zustand store persists for app lifetime + // and Supabase manages the subscription lifecycle + } catch (error) { + console.error('Auth initialization error:', error); + set({ isLoading: false }); + } + }, + + signIn: async (email: string, password: string) => { + try { + set({ isLoading: true, error: null }); + + const { data, error } = await supabaseClient.auth.signInWithPassword({ + email, + password, + }); + + if (error) throw error; + + set({ + user: data.user, + session: data.session, + isAuthenticated: true, + isLoading: false, + }); + + return { success: true }; + } catch (error) { + const errorMessage = (error as AuthError).message || 'Failed to sign in'; + set({ error: errorMessage, isLoading: false }); + return { success: false, error: errorMessage }; + } + }, + + signUp: async (email: string, password: string, name: string) => { + try { + set({ isLoading: true, error: null }); + + const { data, error } = await supabaseClient.auth.signUp({ + email, + password, + options: { + data: { + full_name: name, + }, + }, + }); + + if (error) throw error; + + // If email confirmation is required, user won't be automatically signed in + if (data.user && data.session) { + set({ + user: data.user, + session: data.session, + isAuthenticated: true, + isLoading: false, + }); + } else { + set({ isLoading: false }); + } + + return { success: true }; + } catch (error) { + const errorMessage = (error as AuthError).message || 'Failed to sign up'; + set({ error: errorMessage, isLoading: false }); + return { success: false, error: errorMessage }; + } + }, + + signOut: async () => { + try { + set({ isLoading: true, error: null }); + + await supabaseClient.auth.signOut(); + + set({ + user: null, + session: null, + isAuthenticated: false, + isLoading: false, + }); + } catch (error) { + const errorMessage = (error as AuthError).message || 'Failed to sign out'; + set({ error: errorMessage, isLoading: false }); + console.error('Sign out error:', error); + } + }, + + resetPassword: async (email: string) => { + try { + set({ isLoading: true, error: null }); + + const { error } = await supabaseClient.auth.resetPasswordForEmail(email, { + redirectTo: `${window.location.origin}/reset-password`, + }); + + if (error) throw error; + + set({ isLoading: false }); + return { success: true }; + } catch (error) { + const errorMessage = (error as AuthError).message || 'Failed to send reset email'; + set({ error: errorMessage, isLoading: false }); + return { success: false, error: errorMessage }; + } + }, + + signInWithOAuth: async (provider: 'github' | 'google') => { + try { + set({ isLoading: true, error: null }); + + const { error } = await supabaseClient.auth.signInWithOAuth({ + provider, + options: { + redirectTo: `${window.location.origin}/workstation`, + }, + }); + + if (error) throw error; + + set({ isLoading: false }); + return { success: true }; + } catch (error) { + const errorMessage = (error as AuthError).message || `Failed to sign in with ${provider}`; + set({ error: errorMessage, isLoading: false }); + return { success: false, error: errorMessage }; + } + }, + + setError: (error: string | null) => { + set({ error }); + }, +}));