Accueil / Articles / Firebase Auth et Firestore dans Next.js sans collections plates

Firebase Auth et Firestore dans Next.js sans collections plates

Protégez une seule instance d’application Firebase, laissez les outils d’authentification générer des erreurs typées, organisez les exercices sous chaque utilisateur, et validez les écritures à l’aide des émulateurs.

1049 mots

Les starters hérités de Firebase + Next.js présentent souvent deux pièges : une collection workouts plate contenant les documents de chaque compte, ainsi que des blocs de gestion qui enregistrent l’erreur puis retournent undefined, empêchant ainsi les appels suivants de savoir qu’une écriture a échoué. Les démos avec un seul point d’authentification masquent ces deux problèmes. Cependant, cela ne fonctionne pas avec le trafic en production.

Le cœur de l’application, chargé de l’authentification et du suivi des activités, a été reconstruit à partir du SDK JavaScript Firebase 12.17.1 et testé sur les émulateurs de Firebase afin de pouvoir vérifier les écritures du début à la fin. Les notes ci-dessous se concentrent sur la correction précoce de la structure.

Une seule instance Firebase, pas cinq

Un piège fréquent consiste à appeler initializeApp en haut d’un module que plusieurs routes importent. Le chargement dynamique de Next.js, combiné à la séparation entre les bundles serveur et client, peut charger ce module deux fois ; la deuxième appelation génère alors une erreur indiquant qu’une application par défaut existe déjà. Il convient donc de le protéger :

import { initializeApp, getApps, getApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";
const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY!,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN!,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID!,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET!,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_SENDER_ID!,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID!,
};const app = getApps().length ? getApp() : initializeApp(firebaseConfig);export const auth = getAuth(app);
export const db = getFirestore(app);

getApps().length ? getApp() : initializeApp(...) constitue l’ensemble de la protection nécessaire. Placez les paramètres de configuration dans les variables d’environnement NEXT_PUBLIC_ plutôt que de les coder directement — non pas parce que la configuration de Firebase pour le web est secrète (elle est destinée à être transmise au navigateur par conception), mais parce que les projets de développement et de production ne devraient pas nécessiter la modification du code source pour être mis à jour.

Les tutoriels plus anciens insèrent des chaînes de caractères telles que "your-api-key" dans le fichier. Cela ne représente pas en soi une faille de sécurité ; c’est plutôt une habitude qui finit par entraîner l’ajout d’une configuration réelle de production dans un dépôt public.

Authentification qui renvoie quelque chose que l’utilisateur peut utiliser

Utilisez cette structure. Remarquez ce qu’elle refuse de faire : capturer l’erreur et renvoyer undefined.

import {
  createUserWithEmailAndPassword,
  type User,
} from "firebase/auth";
import { addDoc, collection, serverTimestamp } from "firebase/firestore";
import { auth, db } from "./firebase";
export async function registerUser(
  email: string,
  password: string,
): Promise<User> {
  const cred = await createUserWithEmailAndPassword(auth, email, password);
  return cred.user;
}

Les anciennes versions utilisaient des blocs try/catch pour encadrer l’appel, affichaient une erreur de registration et renvoyaient undefined. Le code d’interface attendait ensuite registerUser(...) et tentait d’accéder à user.uid sur une valeur éventuellement absente — ce qui faisait en sorte qu’une mauvaise mot de passe ou un email déjà utilisé ne se traduisaient jamais par une erreur d’authentification claire ; cela apparaissait plus tard sous forme d’accès à une propriété sur undefined, loin de la véritable cause du problème.

Préférez le rejet explicite. Les appels d’authentification Firebase génèrent des erreurs bien définies (auth/email-already-in-use, auth/weak-password et similaires). Associez ces codes à des messages lisible dans le formulaire. Faites en sorte que l’outil de traitement des données soit honnête : il doit réussir ou lancer une exception.

Structurer les exercices par utilisateur dès le début

Ce changement est particulièrement important en dehors d’un prototype. Les exemples anciens créaient une collection workouts plate avec un champ userId :

// what everyone copies — one collection for the whole app
addDoc(collection(db, "workouts"), { userId, ...workout });

La recherche de « mes entraînements » sur cette collection, qui s’agrandit avec l’ensemble de la base d’utilisateurs, exige que des règles de sécurité filtrent par userId pour chaque opération. Il est préférable d’utiliser une sous-collection afin que les entraînements de chaque utilisateur soient stockés dans leur propre document :

export type WorkoutInput = {
  type: string;
  durationMinutes: number;
  caloriesBurned: number;
};
export async function addWorkoutSession(
  userId: string,
  workout: WorkoutInput,
): Promise<string> {
  const ref = await addDoc(collection(db, "users", userId, "workouts"), {
    ...workout,
    createdAt: serverTimestamp(),
  });
  return ref.id;
}

collection(db, „users“, userId, „workouts“) cible la sous-collection privée de ce compte. Les règles de sécurité peuvent alors comparer request.auth.uid au segment de chemin {uid} tant pour les lectures que pour les écritures, de sorte que chaque requête reste dans les documents d’un seul utilisateur.

Deux détails à souligner. Préférez serverTimestamp() à new Date() : les horloges des clients (ou des clients malveillants) génèrent des timestamps incorrects ; serverTimestamp() est un indicateur que Firestore remplit avec l’heure du serveur au moment de la mise à jour et qui ne peut pas être falsifié. En spécifiant le payload sous forme de WorkoutInput plutôt que de any, on détecte les fautes d’orthographe dans les noms de champs, qui ne se manifesteraient autrement que lorsque le graphique affiche silencieusement rien.

Prouver qu’il écrit réellement

Ne faites pas confiance à du code Firebase qui n’a jamais été exécuté sur l’émulateur — le SDK acceptera des appels que des règles de sécurité réelles rejetteraient. Dirigez le SDK vers des émulateurs locaux et suivez l’ensemble du processus : enregistrement, écriture, lecture des données :

import { getAuth, connectAuthEmulator, createUserWithEmailAndPassword } from "firebase/auth";
import { getFirestore, connectFirestoreEmulator, addDoc, getDocs, collection, serverTimestamp } from "firebase/firestore";
connectAuthEmulator(auth, "http://127.0.0.1:9099", { disableWarnings: true });
connectFirestoreEmulator(db, "127.0.0.1", 8080);const cred = await createUserWithEmailAndPassword(auth, email, "s3cret-pass");
const ref = await addDoc(
  collection(db, "users", cred.user.uid, "workouts"),
  { type: "run", durationMinutes: 32, caloriesBurned: 410, createdAt: serverTimestamp() },
);
const snap = await getDocs(collection(db, "users", cred.user.uid, "workouts"));

L’exécution du même chemin via l’émulateur a généré un identifiant d’autorisation concret, un identifiant d’écriture ainsi qu’un document de lecture dont la propriété createdAt représentait une vraie date et heure Firestore (seconds/nanoseconds) plutôt que la valeur par défaut — ce qui prouve que le serveur a rempli ce champ. Les chemins ou types de champs incorrects provoquent des erreurs sur l’ordinateur portable, et non après le déploiement.

Avertissement concernant les outils : la Firebase CLI exige désormais Java 21 ou une version ultérieure. Un JRE plus ancien sur un ordinateur portable propre fera planter l’émulateur en raison d’une erreur de version avant même que le code de l’application ne s’exécute. Mettez à jour le JDK, puis réessayez.

Où aller ensuite

Auth plus une écriture correctement ciblée constituent les fondements. Un produit final relie généralement onAuthStateChanged à l’état de l’interface pour les utilisateurs connectés, gère les codes d’erreur d’authentification dans le formulaire, et affiche l’historique à l’aide de getDocs, orderBy("createdAt", "desc") ainsi que d’un limit. Ces fonctionnalités dépendent toujours de deux choix pris précocement : initialiser Firebase une seule fois derrière un mécanisme de protection, et placer les documents relatifs aux exercices sous l’utilisateur propriétaire. En omettant l’un ou l’autre, les fonctionnalités ultérieures en subiront les conséquences.