Some improvements with login

This commit is contained in:
Arthur Belleville 2025-03-25 21:45:20 +01:00
parent 45fc8513a1
commit 86f6004f5c
No known key found for this signature in database
8 changed files with 133 additions and 239 deletions

View file

@ -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),

2
ui/.env Normal file
View file

@ -0,0 +1,2 @@
VITE_SUPABASE_URL=https://mhcafqvzbrrwvahpvvzd.supabase.co
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im1oY2FmcXZ6YnJyd3ZhaHB2dnpkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDEyNDEzMjEsImV4cCI6MjA1NjgxNzMyMX0.Otxn5BWCPD2ABlMM59hCgeur9Tf_Q7PndAbTkqXDPtM

View file

@ -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 (
<ThemeProvider>
<AuthProvider supabase={supabase}>
<AuthProvider>
<Router>
<div
className={twMerge(

View file

@ -1,8 +1,8 @@
import "./login-with-google.css";
import { useAuth } from "../../contexts/AuthContext";
import { useLoginGoogle } from "../../hooks/auth";
export function LoginWithGoogle() {
const { loginWithGoogle } = useAuth();
const { loginWithGoogle } = useLoginGoogle();
return (
<button className="gsi-material-button" onClick={() => loginWithGoogle()}>

View file

@ -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<AuthContextType | undefined>(undefined);
interface AuthProviderProps {
children: ReactNode;
supabase: SupabaseClient;
export function AuthProvider({ children }: { children: ReactNode }) {
const { user } = useGetCurrentUser();
const value = {
user,
isAuthenticated: !!user,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
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<UserMetadata | null>(() => {
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 (
<AuthContext.Provider
value={{ isAuthenticated, login, logout, user, loginWithGoogle }}
>
{children}
</AuthContext.Provider>
);
};
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;
};
}

View file

@ -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<Record<string, string>>({});
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<string, string> = {};
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<Record<string, string>>({});
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");
},
});
}

View file

@ -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 (
<div className="min-h-screen">
<header className="bg-white dark:bg-gray-800 shadow">
@ -27,20 +20,15 @@ export const TabloPage = () => {
Tableau de bord
</h1>
<div className="text-sm text-gray-600 dark:text-gray-300">
{me ? "Connected" : "Not connected"}
{isAuthenticated ? "Connected" : "Not connected"}
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<p className="text-gray-600 dark:text-gray-300">
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}
</p>
{id && (
<p className="text-gray-600 dark:text-gray-300 mt-2">
ID du tableau: {id}
</p>
)}
</div>
</div>
</main>