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:
parent
89f46d01cb
commit
414843062b
5 changed files with 96 additions and 17 deletions
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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}`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
30
components/layout/AuthErrorBanner/AuthErrorBanner.module.css
Normal file
30
components/layout/AuthErrorBanner/AuthErrorBanner.module.css
Normal 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;
|
||||
}
|
||||
28
components/layout/AuthErrorBanner/index.tsx
Normal file
28
components/layout/AuthErrorBanner/index.tsx
Normal 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">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue