feat(auth): useAuth + ProtectedRoute + signUp dans auth-client (Sprint 1 étape 2)
This commit is contained in:
parent
107a37d197
commit
38777796aa
19 changed files with 2620 additions and 191 deletions
42
src/features/auth/components/ProtectedRoute.tsx
Normal file
42
src/features/auth/components/ProtectedRoute.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
/**
|
||||
* Wrapper de route qui exige un utilisateur authentifié.
|
||||
*
|
||||
* - Pendant le chargement de la session : affiche un spinner centré.
|
||||
* - Si non authentifié : redirige vers `/login` avec replace (pas d'entrée
|
||||
* parasite dans l'historique navigateur).
|
||||
* - Si authentifié : rend les `children`.
|
||||
*
|
||||
* Le backend reste l'autorité finale : cette garde est de l'UX. Les routes
|
||||
* sensibles sont protégées par les middlewares Hono côté API (ADR 002).
|
||||
*/
|
||||
|
||||
import { Navigate } from 'react-router-dom'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import { useAuth } from '../hooks/useAuth'
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function ProtectedRoute({ children }: ProtectedRouteProps) {
|
||||
const { isLoading, isAuthenticated } = useAuth()
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div
|
||||
className="flex min-h-screen items-center justify-center bg-canvas text-ink-4"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-label="Chargement de la session"
|
||||
>
|
||||
<Loader2 className="size-6 animate-spin" aria-hidden="true" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/login" replace />
|
||||
}
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
54
src/features/auth/hooks/useAuth.ts
Normal file
54
src/features/auth/hooks/useAuth.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* Hook source de vérité pour l'état d'authentification dans toute l'app.
|
||||
*
|
||||
* Au mount : récupère la session courante depuis Supabase (cookie + localStorage).
|
||||
* S'abonne ensuite à `onAuthStateChange` pour réagir aux login/logout/refresh
|
||||
* token ; se désabonne au unmount.
|
||||
*
|
||||
* Consommé par `ProtectedRoute` (redirect si non authentifié) et par toute page
|
||||
* qui a besoin du profil Supabase (ex. prénom affiché dans le header).
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
getCurrentSession,
|
||||
subscribeToAuthChanges,
|
||||
type User,
|
||||
} from '@/shared/lib/auth-client'
|
||||
|
||||
interface UseAuthResult {
|
||||
user: User | null
|
||||
isLoading: boolean
|
||||
isAuthenticated: boolean
|
||||
}
|
||||
|
||||
export function useAuth(): UseAuthResult {
|
||||
const [user, setUser] = useState<User | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
getCurrentSession().then((session) => {
|
||||
if (cancelled) return
|
||||
setUser(session?.user ?? null)
|
||||
setIsLoading(false)
|
||||
})
|
||||
|
||||
const unsubscribe = subscribeToAuthChanges((session) => {
|
||||
setUser(session?.user ?? null)
|
||||
setIsLoading(false)
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
unsubscribe()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return {
|
||||
user,
|
||||
isLoading,
|
||||
isAuthenticated: user !== null,
|
||||
}
|
||||
}
|
||||
|
|
@ -26,29 +26,39 @@ import {
|
|||
// ─── palette data ────────────────────────────────────────────────────────────
|
||||
|
||||
const PALETTE: { token: string; var: string; light: string; dark: string }[] = [
|
||||
{ token: 'canvas', var: '--color-canvas', light: '#EEF2F8', dark: '#0D1220' },
|
||||
{ token: 'canvas-2', var: '--color-canvas-2', light: '#E6EBF4', dark: '#121A2D' },
|
||||
{ token: 'surface', var: '--color-surface', light: '#FFFFFF', dark: '#182238' },
|
||||
{ token: 'surface-hover',var: '--color-surface-hover',light: '#F8FAFD', dark: '#1E2A42' },
|
||||
{ token: 'line', var: '--color-line', light: '#DDE3ED', dark: '#27324B' },
|
||||
{ token: 'line-strong', var: '--color-line-strong', light: '#C7D0E0', dark: '#364363' },
|
||||
{ token: 'ink-1', var: '--color-ink-1', light: '#0F172A', dark: '#F1F4FA' },
|
||||
{ token: 'ink-2', var: '--color-ink-2', light: '#1E293B', dark: '#DDE3EF' },
|
||||
{ token: 'ink-3', var: '--color-ink-3', light: '#475569', dark: '#A8B2C7' },
|
||||
{ token: 'ink-4', var: '--color-ink-4', light: '#64748B', dark: '#7A8499' },
|
||||
{ token: 'ink-5', var: '--color-ink-5', light: '#94A3B8', dark: '#525C73' },
|
||||
{ token: 'expria', var: '--color-expria', light: '#1B4FD8', dark: '#5B7FFF' },
|
||||
{ token: 'canvas', var: '--color-canvas', light: '#EEF2F8', dark: '#0D1220' },
|
||||
{ token: 'canvas-2', var: '--color-canvas-2', light: '#E6EBF4', dark: '#121A2D' },
|
||||
{ token: 'surface', var: '--color-surface', light: '#FFFFFF', dark: '#182238' },
|
||||
{ token: 'surface-hover', var: '--color-surface-hover', light: '#F8FAFD', dark: '#1E2A42' },
|
||||
{ token: 'line', var: '--color-line', light: '#DDE3ED', dark: '#27324B' },
|
||||
{ token: 'line-strong', var: '--color-line-strong', light: '#C7D0E0', dark: '#364363' },
|
||||
{ token: 'ink-1', var: '--color-ink-1', light: '#0F172A', dark: '#F1F4FA' },
|
||||
{ token: 'ink-2', var: '--color-ink-2', light: '#1E293B', dark: '#DDE3EF' },
|
||||
{ token: 'ink-3', var: '--color-ink-3', light: '#475569', dark: '#A8B2C7' },
|
||||
{ token: 'ink-4', var: '--color-ink-4', light: '#64748B', dark: '#7A8499' },
|
||||
{ token: 'ink-5', var: '--color-ink-5', light: '#94A3B8', dark: '#525C73' },
|
||||
{ token: 'expria', var: '--color-expria', light: '#1B4FD8', dark: '#5B7FFF' },
|
||||
{ token: 'expria-hover', var: '--color-expria-hover', light: '#1741B8', dark: '#6F8EFF' },
|
||||
{ token: 'expria-50', var: '--color-expria-50', light: '#EEF3FF', dark: 'rgba(91,127,255,.12)' },
|
||||
{ token: 'expria-100', var: '--color-expria-100', light: '#DCE6FF', dark: '—' },
|
||||
{ token: 'expria-200', var: '--color-expria-200', light: '#B8CDFF', dark: '—' },
|
||||
{ token: 'deep', var: '--color-deep', light: '#0B1F5C', dark: '#060B1A' },
|
||||
{ token: 'success', var: '--color-success', light: '#0E9F6E', dark: '#3DD68C' },
|
||||
{ token: 'success-bg', var: '--color-success-bg', light: '#E6F6F0', dark: 'rgba(61,214,140,.12)' },
|
||||
{ token: 'warning', var: '--color-warning', light: '#C77A00', dark: '#F5B849' },
|
||||
{ token: 'warning-bg', var: '--color-warning-bg', light: '#FEF3E2', dark: 'rgba(245,184,73,.12)' },
|
||||
{ token: 'danger', var: '--color-danger', light: '#C53030', dark: '#F06B6B' },
|
||||
{ token: 'danger-bg', var: '--color-danger-bg', light: '#FDECEC', dark: 'rgba(240,107,107,.12)' },
|
||||
{ token: 'expria-50', var: '--color-expria-50', light: '#EEF3FF', dark: 'rgba(91,127,255,.12)' },
|
||||
{ token: 'expria-100', var: '--color-expria-100', light: '#DCE6FF', dark: '—' },
|
||||
{ token: 'expria-200', var: '--color-expria-200', light: '#B8CDFF', dark: '—' },
|
||||
{ token: 'deep', var: '--color-deep', light: '#0B1F5C', dark: '#060B1A' },
|
||||
{ token: 'success', var: '--color-success', light: '#0E9F6E', dark: '#3DD68C' },
|
||||
{
|
||||
token: 'success-bg',
|
||||
var: '--color-success-bg',
|
||||
light: '#E6F6F0',
|
||||
dark: 'rgba(61,214,140,.12)',
|
||||
},
|
||||
{ token: 'warning', var: '--color-warning', light: '#C77A00', dark: '#F5B849' },
|
||||
{
|
||||
token: 'warning-bg',
|
||||
var: '--color-warning-bg',
|
||||
light: '#FEF3E2',
|
||||
dark: 'rgba(245,184,73,.12)',
|
||||
},
|
||||
{ token: 'danger', var: '--color-danger', light: '#C53030', dark: '#F06B6B' },
|
||||
{ token: 'danger-bg', var: '--color-danger-bg', light: '#FDECEC', dark: 'rgba(240,107,107,.12)' },
|
||||
]
|
||||
|
||||
// ─── section wrapper ─────────────────────────────────────────────────────────
|
||||
|
|
@ -71,7 +81,6 @@ export default function DesignSystemPage() {
|
|||
|
||||
return (
|
||||
<div className="min-h-screen bg-canvas px-6 py-10 space-y-14">
|
||||
|
||||
{/* ── header ── */}
|
||||
<header className="flex items-center justify-between">
|
||||
<div>
|
||||
|
|
@ -98,12 +107,8 @@ export default function DesignSystemPage() {
|
|||
/>
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-xs font-mono font-medium text-ink-2">{token}</p>
|
||||
<p className="text-xs font-mono text-ink-4 leading-tight">
|
||||
☀ {light}
|
||||
</p>
|
||||
<p className="text-xs font-mono text-ink-4 leading-tight">
|
||||
☾ {dark}
|
||||
</p>
|
||||
<p className="text-xs font-mono text-ink-4 leading-tight">☀ {light}</p>
|
||||
<p className="text-xs font-mono text-ink-4 leading-tight">☾ {dark}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -143,7 +148,9 @@ export default function DesignSystemPage() {
|
|||
</div>
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
<Button disabled>Disabled</Button>
|
||||
<Button variant="outline" disabled>Outline disabled</Button>
|
||||
<Button variant="outline" disabled>
|
||||
Outline disabled
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
|
@ -208,7 +215,7 @@ export default function DesignSystemPage() {
|
|||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<AvatarGroup>
|
||||
{['AB', 'CD', 'EF'].map(initials => (
|
||||
{['AB', 'CD', 'EF'].map((initials) => (
|
||||
<Avatar key={initials}>
|
||||
<AvatarFallback>{initials}</AvatarFallback>
|
||||
</Avatar>
|
||||
|
|
@ -231,8 +238,8 @@ export default function DesignSystemPage() {
|
|||
<DialogHeader>
|
||||
<DialogTitle>Example dialog</DialogTitle>
|
||||
<DialogDescription>
|
||||
This dialog uses Direction H tokens — bg-surface, border-line, text-ink-4.
|
||||
Toggle the theme to see it adapt.
|
||||
This dialog uses Direction H tokens — bg-surface, border-line, text-ink-4. Toggle
|
||||
the theme to see it adapt.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter showCloseButton>
|
||||
|
|
@ -242,7 +249,6 @@ export default function DesignSystemPage() {
|
|||
</Dialog>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue