Connexion via Google (OAuth 2.0)

Ajoute l'authentification Google en complément de la connexion par mot de
passe. Le callback OAuth émet le même JWT que la connexion classique, donc
le middleware d'authentification et ProtectedRoute restent inchangés.

Backend
- stratégie passport-google-oauth20 dans src/config/passport.js
- routes GET /api/auth/google et /google/callback, documentées dans Swagger
- un compte existant avec le même email est rattaché automatiquement au
  compte Google, et son email passe vérifié
- les routes répondent 503 si les identifiants ne sont pas configurés,
  plutôt que de faire échouer le démarrage du serveur
- login et changePassword ne comparent plus un hash inexistant sur les
  comptes sans mot de passe

Base de données
- password devient nullable
- colonnes provider, providerId et avatarUrl, index unique sur le couple
  provider/providerId

Frontend
- page /auth/callback qui récupère le token et redirige selon le rôle
- boutons Google de Connexion et Inscription branchés
- boutons Facebook désactivés en attendant les identifiants Meta

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Emma781227
2026-08-17 00:23:18 +02:00
parent 20c865a071
commit 464d666343
13 changed files with 418 additions and 7 deletions

View File

@@ -0,0 +1,68 @@
import { useEffect, useRef, useState } from 'react'
import { useNavigate, useSearchParams, Link } from 'react-router-dom'
import { useAuth } from '../context/AuthContext'
// Point d'arrivée après un aller-retour OAuth : le backend nous renvoie ici
// avec le JWT et le profil encodés dans l'URL.
export default function AuthCallback() {
const navigate = useNavigate()
const [searchParams] = useSearchParams()
const { login } = useAuth()
const [error, setError] = useState('')
// StrictMode monte deux fois en dev : on ne traite le token qu'une seule fois.
const handled = useRef(false)
useEffect(() => {
if (handled.current) return
handled.current = true
const errorParam = searchParams.get('error')
if (errorParam) {
setError(errorParam)
return
}
const token = searchParams.get('token')
const encodedUser = searchParams.get('user')
if (!token || !encodedUser) {
setError('Réponse de connexion incomplète.')
return
}
try {
const user = JSON.parse(atob(encodedUser))
login(user, token)
if (user.role === 'ADMIN') navigate('/admin', { replace: true })
else if (user.role === 'EMPLOYEE') navigate('/employe', { replace: true })
else navigate('/mes-gains', { replace: true })
} catch {
setError('Réponse de connexion illisible.')
}
}, [searchParams, login, navigate])
if (error) {
return (
<div className="min-h-screen bg-white flex flex-col justify-center items-center px-8 max-w-sm mx-auto text-center">
<div className="mb-4 px-4 py-3 rounded-lg bg-red-50 border border-red-200 text-red-600 text-sm">
{error}
</div>
<Link
to="/connexion"
className="text-sm text-[#006B6B] font-medium hover:underline"
>
Retour à la connexion
</Link>
</div>
)
}
return (
<div className="min-h-screen bg-white flex flex-col justify-center items-center px-8">
<p className="text-gray-500 text-sm" role="status">
Connexion en cours…
</p>
</div>
)
}