diff --git a/backend/app/__pycache__/main.cpython-312.pyc b/backend/app/__pycache__/main.cpython-312.pyc index 062418e..c956909 100644 Binary files a/backend/app/__pycache__/main.cpython-312.pyc and b/backend/app/__pycache__/main.cpython-312.pyc differ diff --git a/backend/app/main.py b/backend/app/main.py index 89aa2bb..75d965e 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -5,7 +5,7 @@ import inspect from typing import Annotated, Dict, List, Optional from contextlib import contextmanager -from .auth import get_supabase +from app.routers.auth import get_supabase, router as auth_router from fastapi import FastAPI, Depends, HTTPException, status, Request from fastapi.exceptions import RequestValidationError from fastapi.middleware.cors import CORSMiddleware @@ -17,7 +17,6 @@ from pydantic_core.core_schema import FieldValidationInfo from dotenv import load_dotenv from supabase import Client -from .auth import get_current_user from uuid import uuid4 from datetime import datetime @@ -34,6 +33,8 @@ app.add_middleware( expose_headers=["X-Error-Code", "X-Error-Message"] ) +app.include_router(auth_router, prefix="/auth") + # Security security = HTTPBearer() @@ -54,222 +55,9 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE }) return JSONResponse(status_code=422, content=jsonable_encoder(custom_errors)) - -# ====================================== -# 🚀 MODELS -# ====================================== -class UserCreate(BaseModel): - email: EmailStr - first_name: str - last_name: str - password: str - confirm_password: str - business_name: str - @field_validator("email") - def email_must_contain_at_symbol(cls, v): - if '@' not in v: - raise ValueError("Entrer un email valide") - return v - - @field_validator("password") - def password_must_contain_at_least_8_characters(cls, v): - if len(v) < 8: - raise ValueError("Le mot de passe doit contenir au moins 8 caractères") - return v - - @field_validator("business_name") - def business_name_must_contain_at_least_3_characters(cls, v): - if len(v) < 3: - raise ValueError("Le nom de la société doit contenir au moins 3 caractères") - return v - - @field_validator('confirm_password', mode='before') - def passwords_match(cls, v, info: FieldValidationInfo): - if 'password' in info.data and v != info.data['password']: - raise ValueError('Les mots de passe ne correspondent pas') - return v -class UserLogin(BaseModel): - email: EmailStr - password: str - -class UserOut(BaseModel): - email: EmailStr - business_name: str - -class GameState(BaseModel): - id: str - word: str - guessed_letters: List[str] = [] - attempts_left: int - status: str - wrong_guesses: List[str] = [] - correct_guesses: List[str] = [] - created_by: str - created_at: str - hints: List[str] = [] - -class PublicGameState(BaseModel): - id: str - masked_word: str - guessed_letters: List[str] = [] - attempts_left: int - status: str - wrong_guesses: List[str] = [] - correct_guesses: List[str] = [] - created_by: str - created_at: str - hints: List[str] = [] - -class PublicGameResponse(BaseModel): - game_id: str - status: str - created_by: str - created_at: str - attempts_left: int - -class LetterGuess(BaseModel): - letter: str - -class CreateGame(BaseModel): - word: str - -class HintRequest(BaseModel): - hint: str - -class RefreshToken(BaseModel): - refresh_token: str - -class RefreshResponse(BaseModel): - access_token: str - refresh_token: str - expires_at: int - user: dict - -# ====================================== -# 🔥 AUTH ROUTES -# ====================================== -@app.post("/auth/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)) - -@app.post("/auth/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 - ) - -@app.get("/auth/login/google") -async def login_with_google(supabase: Client = Depends(get_supabase)): - try: - response = supabase.auth.sign_in_with_oauth({ - "provider": "google", - "options": { - "redirect_to": "https://mhcafqvzbrrwvahpvvzd.supabase.co/auth/v1/callback" - } - }) - return {"auth_url": response.url} - except Exception as e: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=str(e) - ) - - -@app.post("/auth/logout") -async def logout(user=Depends(get_current_user), 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)) - -@app.get("/me", response_model=UserOut) -async def get_me( - user = Depends(get_current_user), # Now properly imported - supabase: Client = Depends(get_supabase) -): - try: - # Get user details from public.users table - db_user = supabase.table("users").select("*").eq("id", user.user.id).execute().data[0] - return { - "username": db_user["username"], - "email": user.user.email, - "business_name": db_user["business_name"] - } - except IndexError: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="User not found in database" - ) - except Exception as e: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=str(e) - ) - -@app.post("/auth/refresh", response_model=RefreshResponse) -async def refresh_token(refresh_request: RefreshToken, supabase: Client = Depends(get_supabase)): - """Refresh the access token using a valid refresh token.""" - try: - # Validate the refresh token and get new tokens - response = supabase.auth.refresh_session(refresh_request.refresh_token) - - # Extract user data - user_data = { - "id": response.user.id, - "email": response.user.email, - "first_name": response.user.user_metadata.get("first_name", "Unknown"), - "last_name": response.user.user_metadata.get("last_name", "Unknown"), - "business_name": response.user.user_metadata.get("business_name", "Unknown") - } - - # Return the new tokens and user data - return { - "access_token": response.session.access_token, - "refresh_token": response.session.refresh_token, - "expires_at": int(response.session.expires_at), - "user": user_data - } - except Exception as e: - raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=f"Failed to refresh token: {str(e)}") - -# ====================================== -# 🔍 HEALTH CHECK ROUTES -# ====================================== @app.get("/ping") async def ping(): """Health check endpoint that returns a success status.""" diff --git a/backend/app/routers/__init__.py b/backend/app/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/routers/__pycache__/__init__.cpython-312.pyc b/backend/app/routers/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..e50baeb Binary files /dev/null and b/backend/app/routers/__pycache__/__init__.cpython-312.pyc differ diff --git a/backend/app/routers/__pycache__/auth.cpython-312.pyc b/backend/app/routers/__pycache__/auth.cpython-312.pyc new file mode 100644 index 0000000..61493de Binary files /dev/null and b/backend/app/routers/__pycache__/auth.cpython-312.pyc differ diff --git a/backend/app/routers/__pycache__/helpers.cpython-312.pyc b/backend/app/routers/__pycache__/helpers.cpython-312.pyc new file mode 100644 index 0000000..1605ee8 Binary files /dev/null and b/backend/app/routers/__pycache__/helpers.cpython-312.pyc differ diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py new file mode 100644 index 0000000..4e1e4ca --- /dev/null +++ b/backend/app/routers/auth.py @@ -0,0 +1,126 @@ +from fastapi import Depends, HTTPException, status +from fastapi.routing import APIRouter +from fastapi.encoders import jsonable_encoder +from fastapi.security import OAuth2PasswordBearer +from supabase import Client +from app.config import settings +from jose import JWTError, jwt +import os +from app.routers.helpers import get_supabase, get_current_user_required, get_current_user_optional +from app.schemas.user import UserCreate, UserLogin, UserOut +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: + response = supabase.auth.sign_in_with_oauth({ + "provider": "google", + "options": { + "redirect_to": "https://mhcafqvzbrrwvahpvvzd.supabase.co/auth/v1/callback" + } + }) + return {"auth_url": response.url} + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=str(e) + ) + + +@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), + supabase: Client = Depends(get_supabase) +): + try: + return { + "user": jsonable_encoder(user) + } + except IndexError: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found in database" + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=str(e) + ) + +@router.post("/refresh", response_model=RefreshResponse) +async def refresh_token(refresh_request: RefreshToken, supabase: Client = Depends(get_supabase)): + """Refresh the access token using a valid refresh token.""" + try: + # Validate the refresh token and get new tokens + response = supabase.auth.refresh_session(refresh_request.refresh_token) + + # Extract user data + user_data = { + "id": response.user.id, + "email": response.user.email, + "first_name": response.user.user_metadata.get("first_name", "Unknown"), + "last_name": response.user.user_metadata.get("last_name", "Unknown"), + "business_name": response.user.user_metadata.get("business_name", "Unknown") + } + + # Return the new tokens and user data + return { + "access_token": response.session.access_token, + "refresh_token": response.session.refresh_token, + "expires_at": int(response.session.expires_at), + "user": user_data + } + except Exception as e: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=f"Failed to refresh token: {str(e)}") + \ No newline at end of file diff --git a/backend/app/auth.py b/backend/app/routers/helpers.py similarity index 64% rename from backend/app/auth.py rename to backend/app/routers/helpers.py index c235e2c..f9c2c34 100644 --- a/backend/app/auth.py +++ b/backend/app/routers/helpers.py @@ -1,11 +1,10 @@ from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer +from typing import Optional from supabase import Client -from .config import settings -from jose import JWTError, jwt -import os +from app.config import settings -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login") +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login", auto_error=False) def get_supabase() -> Client: from supabase import create_client @@ -26,21 +25,28 @@ def get_supabase() -> Client: return create_client(url, key) # Updated current user dependency -async def get_current_user( +async def get_user_from_token( token: str = Depends(oauth2_scheme), supabase: Client = Depends(get_supabase) ): + try: + # Get user from Supabase auth + return supabase.auth.get_user(token) + except Exception as e: + return None + +def get_current_user_required(user: Optional[dict] = Depends(get_user_from_token)): credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) - - try: - # Get user from Supabase auth - user = supabase.auth.get_user(token) - if not user: - raise credentials_exception - return user - except Exception as e: - raise credentials_exception \ No newline at end of file + if not user: + raise credentials_exception + + return user + +def get_current_user_optional( + user: Optional[dict] = Depends(get_user_from_token) +) -> Optional[dict]: + return user \ No newline at end of file diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/schemas/__pycache__/__init__.cpython-312.pyc b/backend/app/schemas/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..cd596f3 Binary files /dev/null and b/backend/app/schemas/__pycache__/__init__.cpython-312.pyc differ diff --git a/backend/app/schemas/__pycache__/token.cpython-312.pyc b/backend/app/schemas/__pycache__/token.cpython-312.pyc new file mode 100644 index 0000000..32a0623 Binary files /dev/null and b/backend/app/schemas/__pycache__/token.cpython-312.pyc differ diff --git a/backend/app/schemas/__pycache__/user.cpython-312.pyc b/backend/app/schemas/__pycache__/user.cpython-312.pyc new file mode 100644 index 0000000..fdd8914 Binary files /dev/null and b/backend/app/schemas/__pycache__/user.cpython-312.pyc differ diff --git a/backend/app/schemas/token.py b/backend/app/schemas/token.py new file mode 100644 index 0000000..9de02d8 --- /dev/null +++ b/backend/app/schemas/token.py @@ -0,0 +1,11 @@ +from pydantic import BaseModel + +class RefreshToken(BaseModel): + refresh_token: str + +class RefreshResponse(BaseModel): + access_token: str + refresh_token: str + expires_at: int + user: dict + diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py new file mode 100644 index 0000000..f2809cd --- /dev/null +++ b/backend/app/schemas/user.py @@ -0,0 +1,43 @@ +from pydantic import BaseModel, EmailStr, field_validator, Field, SecretStr +from pydantic_core.core_schema import FieldValidationInfo + +class UserCreate(BaseModel): + email: EmailStr + first_name: str + last_name: str + password: str + confirm_password: str + business_name: str + + @field_validator("email") + def email_must_contain_at_symbol(cls, v): + if '@' not in v: + raise ValueError("Entrer un email valide") + return v + + @field_validator("password") + def password_must_contain_at_least_8_characters(cls, v): + if len(v) < 8: + raise ValueError("Le mot de passe doit contenir au moins 8 caractères") + return v + + @field_validator("business_name") + def business_name_must_contain_at_least_3_characters(cls, v): + if len(v) < 3: + raise ValueError("Le nom de la société doit contenir au moins 3 caractères") + return v + + @field_validator('confirm_password', mode='before') + def passwords_match(cls, v, info: FieldValidationInfo): + if 'password' in info.data and v != info.data['password']: + raise ValueError('Les mots de passe ne correspondent pas') + return v + + +class UserLogin(BaseModel): + email: EmailStr + password: str + +class UserOut(BaseModel): + email: EmailStr + business_name: str diff --git a/ui/package.json b/ui/package.json index 5a768d1..ded9fc5 100644 --- a/ui/package.json +++ b/ui/package.json @@ -48,6 +48,7 @@ "@tanstack/react-query": "^5.69.0", "@types/react-router-dom": "^5.3.3", "axios": "^1.8.4", + "jwt-decode": "^4.0.0", "react-router-dom": "^7.3.0", "react-stately": "^3.36.1", "ts-pattern": "^5.6.2" diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 716fecd..1d03a8c 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: axios: specifier: ^1.8.4 version: 1.8.4 + jwt-decode: + specifier: ^4.0.0 + version: 4.0.0 react-router-dom: specifier: ^7.3.0 version: 7.3.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -2073,6 +2076,10 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} + jwt-decode@4.0.0: + resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} + engines: {node: '>=18'} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -5295,6 +5302,8 @@ snapshots: object.assign: 4.1.7 object.values: 1.2.1 + jwt-decode@4.0.0: {} + keyv@4.5.4: dependencies: json-buffer: 3.0.1 diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 6caa8a9..5a8dc9c 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -2,39 +2,52 @@ 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"; export const App = () => { return ( - -
- - } /> - } /> - } /> - } /> - - -
-
+ + +
+ + + + + } + /> + } /> + } /> + } /> + } /> + + +
+
+
); }; diff --git a/ui/src/components/BrandButtons/LoginWIthGoogle.tsx b/ui/src/components/BrandButtons/LoginWIthGoogle.tsx index 1b82c20..588aefc 100644 --- a/ui/src/components/BrandButtons/LoginWIthGoogle.tsx +++ b/ui/src/components/BrandButtons/LoginWIthGoogle.tsx @@ -1,5 +1,5 @@ import "./login-with-google.css"; -import { useLoginWithGoogle } from "../../hooks/useAuth"; +import { useLoginWithGoogle } from "../../hooks/auth"; export function LoginWithGoogle() { const { mutate: loginWithGoogle } = useLoginWithGoogle(); diff --git a/ui/src/components/ProtectedRoute.tsx b/ui/src/components/ProtectedRoute.tsx new file mode 100644 index 0000000..9c5d0a7 --- /dev/null +++ b/ui/src/components/ProtectedRoute.tsx @@ -0,0 +1,18 @@ +import { Navigate } from "react-router-dom"; +import { ReactNode } from "react"; +import { useAuth } from "../contexts/AuthContext"; +interface ProtectedRouteProps { + children: ReactNode; +} + +export const ProtectedRoute = ({ children }: ProtectedRouteProps) => { + const { isAuthenticated } = useAuth(); + + if (!isAuthenticated) { + // Redirect to login page if user is not authenticated + return ; + } + + // If authenticated, render the protected component + return <>{children}; +}; diff --git a/ui/src/components/SignOutButton.tsx b/ui/src/components/SignOutButton.tsx new file mode 100644 index 0000000..7d38512 --- /dev/null +++ b/ui/src/components/SignOutButton.tsx @@ -0,0 +1,15 @@ +import { useAuth } from "../contexts/AuthContext"; +import { useNavigate } from "react-router-dom"; +import { Button } from "../ui-library/button"; + +export const SignOutButton = () => { + const { logout } = useAuth(); + const navigate = useNavigate(); + + const handleSignOut = () => { + logout(); + navigate("/landing"); + }; + + return ; +}; diff --git a/ui/src/contexts/AuthContext.tsx b/ui/src/contexts/AuthContext.tsx new file mode 100644 index 0000000..5c5f548 --- /dev/null +++ b/ui/src/contexts/AuthContext.tsx @@ -0,0 +1,90 @@ +import { jwtDecode } from "jwt-decode"; +import { createContext, useContext, useState, ReactNode } from "react"; + +interface UserMetadata { + email: string; + first_name: string; + last_name: string; + business_name: string; +} + +interface AuthContextType { + isAuthenticated: boolean; + login: (token: string) => void; + logout: () => void; + user: UserMetadata | null; +} + +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; +} + +export const AuthProvider = ({ children }: 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); + const dcdToken = decodeToken(token); + if (dcdToken) { + setUser(dcdToken.user_metadata); + } + }; + + const logout = () => { + localStorage.removeItem("auth_token"); + setIsAuthenticated(false); + setUser(null); + }; + + 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/ui/src/hooks/useAuth.ts b/ui/src/hooks/auth.ts similarity index 96% rename from ui/src/hooks/useAuth.ts rename to ui/src/hooks/auth.ts index c125a13..8d5ee01 100644 --- a/ui/src/hooks/useAuth.ts +++ b/ui/src/hooks/auth.ts @@ -4,6 +4,8 @@ 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"; + interface SignUpData { email: string; password: string; @@ -81,6 +83,7 @@ export function useSignUp() { export function useLoginEmail() { const navigate = useNavigate(); + const { login } = useAuth(); const [errors, setErrors] = useState>({}); const { mutate, isPending } = useMutation< unknown, @@ -94,10 +97,10 @@ export function useLoginEmail() { >({ mutationFn: async (data: LoginData) => { const response = await api.post("/auth/login", data); + login(response.data.access_token); return response.data; }, - onSuccess: (data) => { - console.log("data", data); + onSuccess: () => { navigate("/"); }, onError: (error) => { diff --git a/ui/src/pages/login.tsx b/ui/src/pages/login.tsx index 9fdc7cb..ed89e81 100644 --- a/ui/src/pages/login.tsx +++ b/ui/src/pages/login.tsx @@ -1,10 +1,10 @@ import { Button } from "../ui-library/button"; import { twMerge } from "tailwind-merge"; import { useNavigate } from "react-router-dom"; -import { LoginWithGoogle } from "../components/BrandButtons/LoginWIthGoogle"; +import { LoginWithGoogle } from "../components/BrandButtons/LoginWithGoogle"; import { useState } from "react"; import { Label, Input, TextField, FieldError } from "../ui-library/field"; -import { useLoginEmail } from "../hooks/useAuth"; +import { useLoginEmail } from "../hooks/auth"; import { Form } from "../ui-library/form"; export function LoginPage() { diff --git a/ui/src/pages/signup.tsx b/ui/src/pages/signup.tsx index ef2fc2e..638a349 100644 --- a/ui/src/pages/signup.tsx +++ b/ui/src/pages/signup.tsx @@ -1,10 +1,10 @@ import { Button } from "../ui-library/button"; import { twMerge } from "tailwind-merge"; import { useNavigate } from "react-router-dom"; -import { LoginWithGoogle } from "../components/BrandButtons/LoginWIthGoogle"; +import { LoginWithGoogle } from "../components/BrandButtons/LoginWithGoogle"; import { useState } from "react"; import { Label, Input, TextField, FieldError } from "../ui-library/field"; -import { useSignUp } from "../hooks/useAuth"; +import { useSignUp } from "../hooks/auth"; import { Form } from "../ui-library/form"; import { Text } from "../ui-library/text"; diff --git a/ui/src/pages/tablo.tsx b/ui/src/pages/tablo.tsx new file mode 100644 index 0000000..fdccffa --- /dev/null +++ b/ui/src/pages/tablo.tsx @@ -0,0 +1,38 @@ +import { useAuth } from "../contexts/AuthContext"; +import { SignOutButton } from "../components/SignOutButton"; + +export const TabloPage = () => { + const { isAuthenticated, user } = useAuth(); + + return ( +
+
+
+

+ Tablo +

+ +
+
+
+
+
+

+ Tableau de bord +

+
+ {isAuthenticated ? "Connected" : "Not connected"} +
+
+ +
+

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

+
+
+
+
+ ); +};