diff --git a/backend/app/routers/__pycache__/auth.cpython-312.pyc b/backend/app/routers/__pycache__/auth.cpython-312.pyc index 25c6831..e61618e 100644 Binary files a/backend/app/routers/__pycache__/auth.cpython-312.pyc and b/backend/app/routers/__pycache__/auth.cpython-312.pyc differ diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index 0ab2a4b..c102a65 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -13,47 +13,6 @@ from app.schemas.token import RefreshResponse, RefreshToken router = APIRouter(tags=["auth"]) -@router.post("/register") -async def register(user: UserCreate, supabase: Client = Depends(get_supabase)): - try: - return supabase.auth.sign_up({ - "email": user.email, - "password": user.password, - "options": {"data": {"first_name": user.first_name, "last_name": user.last_name, "business_name": user.business_name}} - }) - except Exception as e: - headers = {} - if e.code == "user_already_exists": - headers = {"X-Error-Code": e.code, "X-Error-Message": "Cette adresse email est déjà utilisée"} - - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) - -@router.post("/login") -async def login(credentials: UserLogin, supabase: Client = Depends(get_supabase)): - try: - print("Login attempt for:", credentials.email) # Debug log - - response = supabase.auth.sign_in_with_password({ - "email": credentials.email.strip(), - "password": credentials.password.strip() - }) - - print("Login response:", response) - - return { - "access_token": response.session.access_token, - "token_type": "bearer" - } - except Exception as e: - headers = {} - if e.code == "invalid_credentials": - headers = {"X-Error-Code": e.code, "X-Error-Message": "Email ou mot de passe incorrect"} - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid credentials", - headers=headers - ) - @router.get("/login/google") async def login_with_google(supabase: Client = Depends(get_supabase)): try: @@ -79,14 +38,6 @@ async def google_callback(request: Request, supabase: Client = Depends(get_supab supabase.auth.exchange_code_for_session({"auth_code": code}) return RedirectResponse(url="http://localhost:5173") -@router.post("/logout") -async def logout(user=Depends(get_current_user_required), supabase: Client = Depends(get_supabase)): - try: - supabase.auth.sign_out() - return {"message": "Successfully logged out"} - except Exception as e: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) - @router.get("/users/me") async def get_me( user = Depends(get_current_user_required), diff --git a/ui/.env b/ui/.env new file mode 100644 index 0000000..13d91b2 --- /dev/null +++ b/ui/.env @@ -0,0 +1,2 @@ +VITE_SUPABASE_URL=https://mhcafqvzbrrwvahpvvzd.supabase.co +VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im1oY2FmcXZ6YnJyd3ZhaHB2dnpkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDEyNDEzMjEsImV4cCI6MjA1NjgxNzMyMX0.Otxn5BWCPD2ABlMM59hCgeur9Tf_Q7PndAbTkqXDPtM \ No newline at end of file diff --git a/ui/src/App.tsx b/ui/src/App.tsx index c11adc7..40df9cf 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -2,25 +2,17 @@ import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import { LoginPage } from "./pages/login"; import { SignUpPage } from "./pages/signup"; import { ThemeProvider } from "./contexts/ThemeContext"; -import { AuthProvider } from "./contexts/AuthContext"; import { twMerge } from "tailwind-merge"; import { ResetPasswordPage } from "./pages/reset-password"; import { LandingPage } from "./pages/landing"; import { ProtectedRoute } from "./components/ProtectedRoute"; import { TabloPage } from "./pages/tablo"; - -import { createClient } from "@supabase/supabase-js"; - -// Create a single supabase client for interacting with your database -const supabase = createClient( - "https://mhcafqvzbrrwvahpvvzd.supabase.co", - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im1oY2FmcXZ6YnJyd3ZhaHB2dnpkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDEyNDEzMjEsImV4cCI6MjA1NjgxNzMyMX0.Otxn5BWCPD2ABlMM59hCgeur9Tf_Q7PndAbTkqXDPtM" -); +import { AuthProvider } from "./contexts/AuthContext"; export const App = () => { return ( - +
loginWithGoogle()}> diff --git a/ui/src/contexts/AuthContext.tsx b/ui/src/contexts/AuthContext.tsx index 8ab4432..728bbac 100644 --- a/ui/src/contexts/AuthContext.tsx +++ b/ui/src/contexts/AuthContext.tsx @@ -1,109 +1,28 @@ -import { jwtDecode } from "jwt-decode"; -import { createContext, useContext, useState, ReactNode } from "react"; -import { api } from "../lib/api"; -import { SupabaseClient } from "@supabase/supabase-js"; -interface UserMetadata { - email: string; - first_name: string; - last_name: string; - business_name: string; -} +import { createContext, useContext, ReactNode } from "react"; +import { useGetCurrentUser, User } from "../hooks/auth"; interface AuthContextType { + user: User | null; isAuthenticated: boolean; - login: (token: string) => void; - logout: () => void; - user: UserMetadata | null; - loginWithGoogle: () => void; } -type SupabaseToken = { - iss: string; - sub: string; - exp: number; - iat: number; - email: string; - phone: string; - user_metadata: UserMetadata; - role: string; - aal: string; - session_id: string; - is_anonymous: boolean; -}; - const AuthContext = createContext(undefined); -interface AuthProviderProps { - children: ReactNode; - supabase: SupabaseClient; +export function AuthProvider({ children }: { children: ReactNode }) { + const { user } = useGetCurrentUser(); + + const value = { + user, + isAuthenticated: !!user, + }; + + return {children}; } -export const AuthProvider = ({ children, supabase }: AuthProviderProps) => { - const [isAuthenticated, setIsAuthenticated] = useState(() => { - // Check if there's a token in localStorage on initial load - return !!localStorage.getItem("auth_token"); - }); - - const decodeToken = (token: string) => { - try { - const decoded = jwtDecode(token) as SupabaseToken; - return decoded; - } catch (error) { - console.error("Error decoding token:", error); - return null; - } - }; - - const [user, setUser] = useState(() => { - const token = localStorage.getItem("auth_token"); - if (!token) { - return null; - } - return decodeToken(token)?.user_metadata ?? null; - }); - - const login = (token: string) => { - localStorage.setItem("auth_token", token); - setIsAuthenticated(true); - api.interceptors.request.use(function (config) { - config.headers.Authorization = `Bearer ${token}`; - return config; - }); - const dcdToken = decodeToken(token); - if (dcdToken) { - setUser(dcdToken.user_metadata); - } - }; - - const loginWithGoogle = async () => { - await supabase.auth.signInWithOAuth({ - provider: "google", - options: { - redirectTo: "http://localhost:5173/", - scopes: "profile email", - }, - }); - }; - - const logout = () => { - localStorage.removeItem("auth_token"); - setIsAuthenticated(false); - setUser(null); - }; - - return ( - - {children} - - ); -}; - -export const useAuth = () => { +export function useAuth() { const context = useContext(AuthContext); if (context === undefined) { throw new Error("useAuth must be used within an AuthProvider"); } return context; -}; +} diff --git a/ui/src/hooks/auth.ts b/ui/src/hooks/auth.ts index ea65146..3eceb4c 100644 --- a/ui/src/hooks/auth.ts +++ b/ui/src/hooks/auth.ts @@ -1,10 +1,33 @@ -import { useMutation } from "@tanstack/react-query"; -import { api } from "../lib/api"; +import { useMutation, useQuery } from "@tanstack/react-query"; import { useNavigate } from "react-router-dom"; import { useState } from "react"; import { match } from "ts-pattern"; import { toast } from "../ui-library/toast/toast-queue"; -import { useAuth } from "../contexts/AuthContext"; +import { + User as SupabaseUser, + Session, + createClient, +} from "@supabase/supabase-js"; +import { queryClient } from "../lib/api"; + +export type User = SupabaseUser & { + user_metadata: { + email: string; + email_verified: boolean; + first_name: string; + last_name: string; + business_name: string; + }; +}; + +const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; +const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY; + +if (!supabaseUrl || !supabaseAnonKey) { + throw new Error("Missing Supabase environment variables"); +} + +export const supabase = createClient(supabaseUrl, supabaseAnonKey); interface SignUpData { email: string; @@ -20,62 +43,53 @@ interface LoginData { password: string; } -type SignUpErrorCodes = "user_already_exists"; -type LoginErrorCodes = "invalid_credentials" | "user_not_found"; +interface AuthResponse { + user: SupabaseUser | null; + session: Session | null; +} export function useSignUp() { + const navigate = useNavigate(); const [errors, setErrors] = useState>({}); const { mutate, isPending } = useMutation< - unknown, - { - response: { - data: - | { human_error: string; form_location: string }[] - | { detail: string }; - headers: { - "x-error-code": SignUpErrorCodes; - "x-error-message": string; - }; - }; - }, + AuthResponse, + { message: string; code: string }, SignUpData >({ mutationFn: async (data: SignUpData) => { - const response = await api.post("/auth/register", data); - return response.data; + const { data: response, error } = await supabase.auth.signUp({ + email: data.email, + password: data.password, + options: { + data: { + first_name: data.first_name, + last_name: data.last_name, + business_name: data.business_name, + }, + }, + }); + if (error) throw error; + return response; }, - onSuccess: (data) => { - console.log("data", data); + onSuccess: () => { + navigate("/"); }, onError: (error) => { - console.log("error", error); const errMap: Record = {}; - if (Array.isArray(error.response.data)) { - error.response.data.forEach( - ({ - human_error, - form_location, - }: { - human_error: string; - form_location: string; - }) => { - errMap[form_location] = human_error; - } - ); - } else { - match(error.response.headers["x-error-code"]) - .with("user_already_exists", () => { - errMap["email"] = error.response.headers["x-error-message"]; - }) - .otherwise(() => { - toast.add({ - title: "Erreur", - description: error.response.headers["x-error-message"], - type: "error", - position: "top-left", - }); + + match(error.code) + .with("user_already_exists", () => { + errMap["email"] = "Cette adresse email est déjà utilisée"; + }) + .otherwise(() => { + toast.add({ + title: "Erreur", + description: error.message, + type: "error", + position: "top-left", }); - } + }); + setErrors(errMap); }, }); @@ -84,35 +98,32 @@ export function useSignUp() { export function useLoginEmail() { const navigate = useNavigate(); - const { login } = useAuth(); const [errors, setErrors] = useState>({}); const { mutate, isPending } = useMutation< - unknown, - { - response: { - data: { access_token: string }; - headers: { "x-error-code": LoginErrorCodes; "x-error-message": string }; - }; - }, + AuthResponse, + { message: string; code: string }, LoginData >({ mutationFn: async (data: LoginData) => { - const response = await api.post("/auth/login", data); - login(response.data.access_token); - return response.data; + const { data: response, error } = await supabase.auth.signInWithPassword({ + email: data.email.trim(), + password: data.password.trim(), + }); + if (error) throw error; + return response; }, onSuccess: () => { navigate("/"); }, onError: (error) => { - match(error.response.headers["x-error-code"]) + match(error.code) .with("invalid_credentials", () => { - setErrors({ email: error.response.headers["x-error-message"] }); + setErrors({ email: "Email ou mot de passe incorrect" }); }) .otherwise(() => { toast.add({ title: "Erreur", - description: error.response.headers["x-error-message"], + description: error.message, type: "error", position: "top-left", }); @@ -122,17 +133,48 @@ export function useLoginEmail() { return { mutate, isPending, errors }; } +export function useLoginGoogle() { + const { mutate } = useMutation({ + mutationFn: async () => { + const { data, error } = await supabase.auth.signInWithOAuth({ + provider: "google", + options: { + redirectTo: "http://localhost:5173/", + }, + }); + if (error) throw error; + return data; + }, + }); + return { loginWithGoogle: mutate }; +} + +export function useGetCurrentUser() { + const { data: user } = useQuery({ + queryKey: ["currentUser"], + queryFn: async () => { + const { data } = await supabase.auth.getSession(); + if (!data.session) { + throw new Error("No session found"); + } + return data.session?.user; + }, + retryDelay: 1000, + refetchInterval: 1000 * 60 * 10, + }); + return { user: user as User | null }; +} export function useLogout() { - const { logout } = useAuth(); const navigate = useNavigate(); return useMutation({ mutationFn: async () => { - const response = await api.post("/auth/logout"); - return response.data; + const { error } = await supabase.auth.signOut(); + if (error) throw error; }, onSuccess: () => { - logout(); - navigate("/landing"); + console.log("logout"); + queryClient.invalidateQueries({ queryKey: ["currentUser"] }); + navigate("/login"); }, }); } diff --git a/ui/src/pages/tablo.tsx b/ui/src/pages/tablo.tsx index c9ca072..834177a 100644 --- a/ui/src/pages/tablo.tsx +++ b/ui/src/pages/tablo.tsx @@ -1,15 +1,8 @@ import { SignOutButton } from "../components/SignOutButton"; -import { useParams } from "react-router-dom"; +import { useAuth } from "../contexts/AuthContext"; export const TabloPage = () => { - const { id, access_token } = useParams(); - const me = { - first_name: "John", - last_name: "Doe", - }; - - console.log({ access_token }); - + const { user, isAuthenticated } = useAuth(); return (
@@ -27,20 +20,15 @@ export const TabloPage = () => { Tableau de bord
- {me ? "Connected" : "Not connected"} + {isAuthenticated ? "Connected" : "Not connected"}

- Bienvenue sur votre tableau de bord {me?.first_name}{" "} - {me?.last_name} + Bienvenue sur votre tableau de bord{" "} + {user?.user_metadata.first_name} {user?.user_metadata.last_name}

- {id && ( -

- ID du tableau: {id} -

- )}