Some improvements with login
This commit is contained in:
parent
45fc8513a1
commit
86f6004f5c
8 changed files with 133 additions and 239 deletions
Binary file not shown.
|
|
@ -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
2
ui/.env
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
VITE_SUPABASE_URL=https://mhcafqvzbrrwvahpvvzd.supabase.co
|
||||
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im1oY2FmcXZ6YnJyd3ZhaHB2dnpkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDEyNDEzMjEsImV4cCI6MjA1NjgxNzMyMX0.Otxn5BWCPD2ABlMM59hCgeur9Tf_Q7PndAbTkqXDPtM
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()}>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue