# Créer sa page de login

> Un bloc à copier qui assemble `AuthLayout`, `AuthBrandPanel`, `AuthForm`, `OAuthButton` et `AuthSeparator` en une page d'authentification complète — puis la liste, explicite, de ce que le paquet ne fait **pas** à votre place.

## Le partage des rôles

Le paquet dessine l'écran. Il ne sait ni ce qu'est une session, ni où vit votre API.

**Ce que le paquet fournit** — la mise en page en deux moitiés et ses fonds, le panneau de marque, le formulaire e-mail / mot de passe / nom d'affichage avec ses deux modes (`login` ⇄ `register`), son état d'erreur, son état de chargement, le bouton « Continue with … » et le « or » qui le sépare des champs.

**Ce qui reste à vous** — la route (`/login`, `/signup`), l'appel HTTP, la redirection après succès, le message d'erreur affiché, les URL des liens, et le pré-remplissage des identifiants en développement.

Deux règles expliquent ce découpage, et elles valent pour tout le paquet :

- **Aucun import de framework.** Pas de `next/navigation`, pas de `next/link`, pas de `next/image` : le design system est consommé par deux apps Next *et* par un showcase Vite. Tout ce qui navigue prend un `href` et, si besoin, un `render.link`.
- **Rien ne fetch.** `onSubmit` rend les valeurs saisies, `error` et `loading` redescendent en props. La requête vous appartient, donc ses deux états aussi.

## Le montage complet

Une page Next.js App Router, de bout en bout. C'est le bloc à copier.

```tsx
"use client"

import { useState } from "react"
import { useRouter } from "next/navigation"
import { ZapIcon } from "lucide-react"
import {
  AuthBrandMark,
  AuthBrandPanel,
  AuthForm,
  AuthLayout,
  AuthSeparator,
  OAuthButton,
  type AuthMode,
  type AuthSubmitValues,
} from "@spunto/design-system"

const ARGUMENTS = [
  "Un workspace complet, IDE compris, en moins de 15 secondes",
  "Vos propres serveurs — aucun verrouillage fournisseur",
  "Secrets, dotfiles et clés SSH injectés automatiquement",
  "API-first — parfait pour les agents de code",
]

export function AuthScreen({ initialMode = "login" }: { initialMode?: AuthMode }) {
  const router = useRouter()
  const [error, setError] = useState<string | null>(null)

  // Le seul endroit qui parle à votre API. Le formulaire tourne tant que cette
  // promesse n'est pas résolue — inutile de gérer un `loading` vous-même.
  async function handleSubmit({ mode, email, password, name }: AuthSubmitValues) {
    setError(null)
    try {
      const res = await fetch(
        mode === "login" ? "/api/auth/login/password" : "/api/auth/register",
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          credentials: "include", // la session est un cookie httpOnly
          body: JSON.stringify({ email, password, ...(name ? { name } : {}) }),
        },
      )
      const data = await res.json()
      if (!res.ok) {
        setError(data.error ?? "Something went wrong")
        return
      }
      router.push("/dashboard") // la redirection est à vous
    } catch {
      setError("Network error")
    }
  }

  return (
    <AuthLayout
      brand={
        <AuthBrandPanel
          logoSrc="/logo_spunto.png"
          name="Spunto"
          title={
            <>
              Vos environnements de dev,
              <br />
              <span className="bg-gradient-to-r from-build to-run bg-clip-text text-transparent">
                enfin sous contrôle.
              </span>
            </>
          }
          description="Des workspaces isolés pour chaque développeur, chaque projet, chaque agent. Docker, sur votre propre compute."
          items={ARGUMENTS}
          quote="Enfin une plateforme d'environnements de dev qui ne cherche pas à nous enfermer."
          quoteAuthor="— Platform Engineering Lead"
        />
      }
      brandCompact={<AuthBrandMark logoSrc="/logo_spunto.png" name="Spunto" variant="compact" />}
    >
      <AuthForm
        initialMode={initialMode}
        error={error}
        onModeChange={() => setError(null)}
        onSubmit={handleSubmit}
        footer={
          <>
            <p className="text-center text-xs text-muted-foreground">
              By signing in, you agree to our{" "}
              <a href="/terms" className="underline underline-offset-4 hover:text-foreground">
                Terms of Service
              </a>
              .
            </p>
            <div className="border-t border-border pt-2 text-center">
              <a
                href="/"
                className="inline-flex items-center gap-1.5 text-sm font-medium text-primary underline-offset-4 hover:underline"
              >
                <ZapIcon className="h-3.5 w-3.5" />
                Learn about Spunto
              </a>
            </div>
          </>
        }
      >
        {/* Le slot entre le titre et les champs : les fournisseurs, puis le « or ». */}
        <OAuthButton provider="google" href="/api/auth/login" />
        <AuthSeparator />
      </AuthForm>
    </AuthLayout>
  )
}
```

## Les deux routes

Le composant ci-dessus prend un `initialMode`, ce qui suffit pour deux routes qui montent le même écran :

```tsx
// app/login/page.tsx
export default function Page() {
  return <AuthScreen initialMode="login" />
}

// app/signup/page.tsx
export default function Page() {
  return <AuthScreen initialMode="register" />
}
```

Pourquoi deux routes plutôt qu'une : un bouton d'accueil qui dit « Sign up for free » doit atterrir sur un formulaire qui dit « Create an account », pas sur « Welcome back ». La bascule en bas du formulaire continue de passer d'un mode à l'autre **sans navigation**, donc personne ne reste coincé sur le mauvais.

Si l'URL doit suivre la bascule, passez en **mode contrôlé** — `mode` au lieu de `initialMode`, et `onModeChange` pousse la route :

```tsx
const router = useRouter()
const pathname = usePathname()
const mode = pathname === "/signup" ? "register" : "login"

<AuthForm
  mode={mode}
  onModeChange={(next) => router.push(next === "register" ? "/signup" : "/login")}
  onSubmit={handleSubmit}
/>
```

## Ce qui reste à la charge de l'app

1. **Les routes.** `/login` et `/signup` (ou une seule, avec la bascule).
2. **Les endpoints.** `POST /api/auth/login/password` et `POST /api/auth/register`, en `credentials: "include"` — la session Spunto est un cookie `httpOnly`, pas un token que le client stocke. Corps attendu : `{ email, password }`, plus `name` à l'inscription.
3. **La redirection après succès.** `router.push("/dashboard")`, un `window.location`, ce que vous voulez : le paquet n'importe pas de routeur.
4. **Le message d'erreur.** L'API répond `{ error }` → `setError(data.error)` → la prop `error`. Pensez à le remettre à `null` dans `onModeChange`, sinon l'erreur du login reste affichée sous le formulaire d'inscription.
5. **Le pré-remplissage en développement.** `defaultEmail` / `defaultPassword` prennent ce que vous leur donnez ; c'est à l'app de décider quand. Attention au piège Next : en build de production (`next start`), `NODE_ENV` vaut `"production"` même en local, donc lisez des variables `NEXT_PUBLIC_*` (inlinées au build) plutôt que `NODE_ENV` :

```tsx
<AuthForm
  defaultEmail={process.env.NEXT_PUBLIC_DEV_LOGIN_EMAIL ?? ""}
  defaultPassword={process.env.NEXT_PUBLIC_DEV_LOGIN_PASSWORD ?? ""}
  onSubmit={handleSubmit}
/>
```

6. **Le lien OAuth.** Son `href` pointe vers une route **API** (celle qui redirige vers le fournisseur), jamais vers une route client : `/api/auth/login`. C'est une vraie navigation, pas un `fetch`.

## Hors Next.js

Rien dans le paquet ne dépend de Next : le même écran se monte avec React Router, TanStack Router ou de simples `<a>`. Les liens qui doivent passer par le routeur utilisent le slot `render.link` — le même motif que `CommandPaletteItem` ou `WorkerCard` :

```tsx
import { Link } from "react-router"

<OAuthButton
  provider="google"
  href="/api/auth/login"
  render={{ link: (props) => <Link {...props} to={props.href} /> }}
/>
```

Sans `render.link`, un `href` retombe sur un `<a>` nu — ce qui est exactement ce qu'il faut pour une redirection OAuth, puisqu'elle sort de l'app.

## Composer autrement

Les cinq briques sont indépendantes ; l'écran complet n'est qu'un assemblage parmi d'autres.

- **Plusieurs fournisseurs, pas de mot de passe** : empilez les `OAuthButton` dans la colonne d'`AuthLayout` sans monter d'`AuthForm`.
- **Un outil interne** : omettez `brand` — la colonne du formulaire prend toute la page.
- **Une page d'invitation** : `initialMode="register"` + `hideModeSwitch` + `copy={{ register: { title: "Rejoindre l'équipe" } }}`.
- **Un autre fournisseur que Google/GitHub** : `icon` et le libellé (les enfants) sont des props ; les deux marques livrées ne sont qu'un défaut.

## Rappels d'installation

Les tokens sont un fichier CSS, à importer une fois dans l'entrée de l'app :

```css
@import "tailwindcss";
@import "@spunto/design-system/styles.css";
@custom-variant dark (&:is(.dark *));
```

Et dans une app Next.js, le paquet est publié en TypeScript source :

```ts
// next.config.ts
const nextConfig = { transpilePackages: ["@spunto/design-system"] }
```
