feat: afficher les erreurs d'authentification dans un bandeau visuel

Le callback transmet maintenant le détail de l'erreur (message Supabase)
dans le query param auth_error. Un composant AuthErrorBanner dans le layout
lit ce param et affiche un bandeau rouge fermable en haut de toutes les pages.
This commit is contained in:
Jalil Arfaoui 2026-02-12 14:34:53 +01:00
parent 89f46d01cb
commit 414843062b
5 changed files with 96 additions and 17 deletions

View file

@ -3,7 +3,7 @@ NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:64321
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=
SUPABASE_SECRET_KEY=
# Migrations CI/CD (production)
# Migrations CI/CD (production) — nécessaires pour `npm run migrate`
SUPABASE_ACCESS_TOKEN=
SUPABASE_DB_PASSWORD=
SUPABASE_PROJECT_ID=

View file

@ -1,5 +1,4 @@
import { NextResponse } from 'next/server'
import { headers } from 'next/headers'
import { createServerSupabaseClient } from '../../../../infra/supabase/ssr'
function getOrigin(request: Request): string {
@ -14,30 +13,47 @@ function getOrigin(request: Request): string {
return new URL(request.url).origin
}
function redirectWithError(origin: string, message: string) {
return NextResponse.redirect(`${origin}/?auth_error=${encodeURIComponent(message)}`)
}
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const origin = getOrigin(request)
const code = searchParams.get('code')
const next = searchParams.get('next') ?? '/'
if (code) {
const supabase = await createServerSupabaseClient()
const { error } = await supabase.auth.exchangeCodeForSession(code)
if (!error) {
const {
data: { user },
} = await supabase.auth.getUser()
if (!code) {
return redirectWithError(origin, 'Code d\u2019authentification manquant.')
}
if (user) {
await supabase.from('contributors').upsert(
{ id: user.id, reputation: 0 },
{ onConflict: 'id' },
)
}
const supabase = await createServerSupabaseClient()
const { error: exchangeError } = await supabase.auth.exchangeCodeForSession(code)
return NextResponse.redirect(`${origin}${next}`)
if (exchangeError) {
return redirectWithError(
origin,
`Le lien de confirmation a expiré ou est invalide. (${exchangeError.message})`,
)
}
const {
data: { user },
} = await supabase.auth.getUser()
if (user) {
const { error: upsertError } = await supabase.from('contributors').upsert(
{ id: user.id, reputation: 0 },
{ onConflict: 'id' },
)
if (upsertError) {
return redirectWithError(
origin,
`Erreur lors de la création du profil contributeur. (${upsertError.message})`,
)
}
}
return NextResponse.redirect(`${origin}/?auth_error=true`)
return NextResponse.redirect(`${origin}${next}`)
}

View file

@ -1,3 +1,4 @@
import { Suspense } from 'react'
import './global.css'
import '../styles/debats-colors.css'
import '../styles/layout.css'
@ -5,6 +6,7 @@ import PlausibleProvider from 'next-plausible'
import { StyledComponentsRegistry } from './registry'
import Header from '../components/layout/header'
import Footer from '../components/layout/footer'
import AuthErrorBanner from '../components/layout/AuthErrorBanner'
import { Metadata } from 'next'
@ -36,6 +38,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<StyledComponentsRegistry>
<div className="layout-container">
<Header />
<Suspense>
<AuthErrorBanner />
</Suspense>
<main className="main-content">
<div className="page-content">{children}</div>
</main>

View file

@ -0,0 +1,30 @@
.banner {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 12px 20px;
background-color: #fef2f2;
border-bottom: 2px solid #f21e40;
color: #991b1b;
}
.message {
margin: 0;
font-size: 14px;
line-height: 1.4;
}
.dismiss {
background: none;
border: none;
font-size: 20px;
color: #991b1b;
cursor: pointer;
padding: 0 4px;
line-height: 1;
}
.dismiss:hover {
color: #f21e40;
}

View file

@ -0,0 +1,28 @@
'use client'
import { useSearchParams, useRouter } from 'next/navigation'
import styles from './AuthErrorBanner.module.css'
export default function AuthErrorBanner() {
const searchParams = useSearchParams()
const router = useRouter()
const authError = searchParams.get('auth_error')
if (!authError) return null
const dismiss = () => {
const params = new URLSearchParams(searchParams.toString())
params.delete('auth_error')
const remaining = params.toString()
router.replace(remaining ? `?${remaining}` : window.location.pathname)
}
return (
<div className={styles.banner} role="alert">
<p className={styles.message}>{authError}</p>
<button className={styles.dismiss} onClick={dismiss} aria-label="Fermer">
&times;
</button>
</div>
)
}