diff --git a/src/App.tsx b/src/App.tsx index 18daf2e..60ccefd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,26 +1,61 @@ + import { Toaster } from "@/components/ui/toaster"; import { Toaster as Sonner } from "@/components/ui/sonner"; import { TooltipProvider } from "@/components/ui/tooltip"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { BrowserRouter, Routes, Route } from "react-router-dom"; +import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; +import { AuthProvider, useAuth } from "@/contexts/AuthContext"; import Index from "./pages/Index"; +import Auth from "./pages/Auth"; import NotFound from "./pages/NotFound"; const queryClient = new QueryClient(); +// Protected route component +const ProtectedRoute = ({ children }: { children: React.ReactNode }) => { + const { user, loading } = useAuth(); + + if (loading) { + return
Loading...
; + } + + if (!user) { + return ; + } + + return <>{children}; +}; + const App = () => ( - - - - - - } /> - {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} - } /> - - - + + + + + + + } /> + } /> + {/* Protected routes example (Dashboard will be implemented later) */} + +
+
+

Dashboard

+

This is a protected route. Dashboard implementation coming soon.

+
+
+ + } + /> + {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} + } /> +
+
+
+
); diff --git a/src/components/Header.tsx b/src/components/Header.tsx index b4290ab..b913eea 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -2,13 +2,24 @@ import { useState, useEffect } from 'react'; import { Link, useLocation } from 'react-router-dom'; import { Button } from "@/components/ui/button"; -import { ShieldCheck, Menu, X } from 'lucide-react'; +import { ShieldCheck, Menu, X, User, LogOut } from 'lucide-react'; +import { useAuth } from '@/contexts/AuthContext'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; const Header = () => { const [isScrolled, setIsScrolled] = useState(false); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const location = useLocation(); const isHome = location.pathname === '/'; + const { user, profile, signOut } = useAuth(); useEffect(() => { const handleScroll = () => { @@ -18,6 +29,17 @@ const Header = () => { return () => window.removeEventListener('scroll', handleScroll); }, []); + const getInitials = () => { + if (profile?.full_name) { + return profile.full_name + .split(' ') + .map((n: string) => n[0]) + .join('') + .toUpperCase(); + } + return user?.email?.substring(0, 2).toUpperCase() || 'U'; + }; + return (
@@ -46,13 +68,38 @@ const Header = () => { {/* Desktop CTA */}
- {isHome ? ( + {user ? ( + + + + + + My Account + + + Dashboard + + + Profile + + + signOut()} className="text-destructive"> + + Log out + + + + ) : isHome ? ( ) : ( - )}
@@ -80,9 +127,28 @@ const Header = () => { setMobileMenuOpen(false)}> Pricing - {isHome && ( + {user ? ( + <> + setMobileMenuOpen(false)}> + Profile + + + + ) : ( )}
diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx index 991f56e..1b502d1 100644 --- a/src/components/ui/avatar.tsx +++ b/src/components/ui/avatar.tsx @@ -1,3 +1,4 @@ + import * as React from "react" import * as AvatarPrimitive from "@radix-ui/react-avatar" diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx index 769ff7a..30d5d91 100644 --- a/src/components/ui/dropdown-menu.tsx +++ b/src/components/ui/dropdown-menu.tsx @@ -1,3 +1,4 @@ + import * as React from "react" import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" import { Check, ChevronRight, Circle } from "lucide-react" @@ -81,7 +82,7 @@ const DropdownMenuItem = React.forwardRef< Promise; + signUp: (email: string, password: string, fullName: string) => Promise; + signOut: () => Promise; +}; + +const AuthContext = createContext(undefined); + +export const AuthProvider = ({ children }: { children: ReactNode }) => { + const [session, setSession] = useState(null); + const [user, setUser] = useState(null); + const [profile, setProfile] = useState(null); + const [loading, setLoading] = useState(true); + const { toast } = useToast(); + + useEffect(() => { + // Get the initial session + supabase.auth.getSession().then(({ data: { session } }) => { + setSession(session); + setUser(session?.user ?? null); + if (session?.user) { + fetchProfile(session.user.id); + } + setLoading(false); + }); + + // Listen for auth changes + const { data: { subscription } } = supabase.auth.onAuthStateChange( + (_event, session) => { + setSession(session); + setUser(session?.user ?? null); + if (session?.user) { + fetchProfile(session.user.id); + } else { + setProfile(null); + } + setLoading(false); + } + ); + + return () => { + subscription.unsubscribe(); + }; + }, []); + + const fetchProfile = async (userId: string) => { + try { + const { data, error } = await supabase + .from('profiles') + .select('*') + .eq('id', userId) + .single(); + + if (error) { + console.error('Error fetching profile:', error); + return; + } + + setProfile(data); + } catch (error) { + console.error('Error fetching profile:', error); + } + }; + + const signIn = async (email: string, password: string) => { + try { + setLoading(true); + const { error } = await supabase.auth.signInWithPassword({ email, password }); + + if (error) throw error; + + toast({ + title: "Success!", + description: "You are now signed in.", + }); + } catch (error: any) { + toast({ + title: "Error signing in", + description: error.message, + variant: "destructive", + }); + console.error('Error signing in:', error); + throw error; + } finally { + setLoading(false); + } + }; + + const signUp = async (email: string, password: string, fullName: string) => { + try { + setLoading(true); + const { error } = await supabase.auth.signUp({ + email, + password, + options: { + data: { + full_name: fullName, + } + } + }); + + if (error) throw error; + + toast({ + title: "Account created!", + description: "Please check your email to confirm your account.", + }); + } catch (error: any) { + toast({ + title: "Error creating account", + description: error.message, + variant: "destructive", + }); + console.error('Error signing up:', error); + throw error; + } finally { + setLoading(false); + } + }; + + const signOut = async () => { + try { + setLoading(true); + const { error } = await supabase.auth.signOut(); + if (error) throw error; + toast({ + title: "Signed out", + description: "You have been successfully signed out.", + }); + } catch (error: any) { + toast({ + title: "Error signing out", + description: error.message, + variant: "destructive", + }); + console.error('Error signing out:', error); + } finally { + setLoading(false); + } + }; + + return ( + + {children} + + ); +}; + +export const useAuth = () => { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +}; diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index a4d65a8..e768ae4 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -9,7 +9,33 @@ export type Json = export type Database = { public: { Tables: { - [_ in never]: never + profiles: { + Row: { + company: string | null + created_at: string + full_name: string | null + id: string + role: string | null + updated_at: string + } + Insert: { + company?: string | null + created_at?: string + full_name?: string | null + id: string + role?: string | null + updated_at?: string + } + Update: { + company?: string | null + created_at?: string + full_name?: string | null + id?: string + role?: string | null + updated_at?: string + } + Relationships: [] + } } Views: { [_ in never]: never diff --git a/src/pages/Auth.tsx b/src/pages/Auth.tsx new file mode 100644 index 0000000..a06961a --- /dev/null +++ b/src/pages/Auth.tsx @@ -0,0 +1,194 @@ + +import { useState } from 'react'; +import { Navigate } from 'react-router-dom'; +import { useAuth } from '@/contexts/AuthContext'; +import { ShieldCheck, Mail, Lock, User } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; + +const Auth = () => { + const { user, signIn, signUp, loading } = useAuth(); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [fullName, setFullName] = useState(''); + const [authError, setAuthError] = useState(null); + + // If user is already logged in, redirect to home page + if (user) { + return ; + } + + const handleSignIn = async (e: React.FormEvent) => { + e.preventDefault(); + setAuthError(null); + try { + await signIn(email, password); + } catch (error: any) { + setAuthError(error.message); + } + }; + + const handleSignUp = async (e: React.FormEvent) => { + e.preventDefault(); + setAuthError(null); + try { + await signUp(email, password, fullName); + } catch (error: any) { + setAuthError(error.message); + } + }; + + return ( +
+
+
+
+ +
+

SecuPolicy

+

Sign in to access your account

+
+ + + + Login + Register + + + + + + Welcome back + + Enter your credentials to access your account + + +
+ +
+ +
+ + setEmail(e.target.value)} + required + /> +
+
+
+ +
+ + setPassword(e.target.value)} + required + /> +
+
+ {authError && ( +
{authError}
+ )} +
+ + + +
+
+
+ + + + + Create an account + + Enter your details to create your account + + +
+ +
+ +
+ + setFullName(e.target.value)} + required + /> +
+
+
+ +
+ + setEmail(e.target.value)} + required + /> +
+
+
+ +
+ + setPassword(e.target.value)} + required + minLength={6} + /> +
+
+ {authError && ( +
{authError}
+ )} +
+ + + +
+
+
+
+
+
+ ); +}; + +export default Auth;