ScaleRocket/Web

Authentification

Comment fonctionne l'authentification dans ScaleRocket — email/mot de passe, OAuth, gestion des sessions et routes protégées.

Authentification

ScaleRocket gère toute l'authentification via l'application dashboard (apps/app) — connexion, inscription, réinitialisation du mot de passe et flux OAuth. Les routes protégées sont appliquées via TanStack Router.

Comment l'authentification fonctionne

  1. L'utilisateur s'inscrit ou se connecte via l'application dashboard
  2. Supabase Auth crée une session et retourne un JWT
  3. Le client Supabase stocke la session dans localStorage
  4. Chaque requête API inclut le JWT pour l'autorisation
  5. Les Edge Functions vérifient le JWT pour identifier l'utilisateur
  6. Le Row Level Security dans PostgreSQL utilise le JWT pour filtrer les données

Comment l'authentification fonctionne

  1. L'utilisateur s'inscrit ou se connecte via l'application dashboard
  2. Convex Auth crée une session et la stocke côté serveur
  3. Le client Convex gère automatiquement le token d'authentification
  4. Chaque query/mutation Convex est authentifiée automatiquement
  5. Les fonctions serveur utilisent getAuthUserId(ctx) pour identifier l'utilisateur
  6. Le système de permissions de Convex contrôle l'accès aux données par fonction

Inscription par email/mot de passe

Le flux d'inscription par défaut :

  1. L'utilisateur remplit email + mot de passe sur /signup
  2. L'application appelle supabase.auth.signUp()
  3. Supabase envoie un email de confirmation
  4. L'utilisateur clique sur le lien de confirmation
  5. L'utilisateur est redirigé vers l'application en tant qu'authentifié
// apps/app/src/lib/auth.ts
import { supabase } from "./supabase";

export async function signUp(email: string, password: string) {
  const { data, error } = await supabase.auth.signUp({
    email,
    password,
    options: {
      emailRedirectTo: `${window.location.origin}/auth/callback`,
    },
  });

  if (error) throw error;
  return data;
}

export async function signIn(email: string, password: string) {
  const { data, error } = await supabase.auth.signInWithPassword({
    email,
    password,
  });

  if (error) throw error;
  return data;
}
  1. L'utilisateur remplit email + mot de passe sur /signup
  2. L'application appelle signIn("password", formData) avec flow: "signUp"
  3. Convex Auth envoie un email de vérification
  4. L'utilisateur clique sur le lien de vérification
  5. L'utilisateur est redirigé vers l'application en tant qu'authentifié
// apps/app/src/lib/auth.ts
import { useAuthActions } from "@convex-dev/auth/react";

// Dans un composant React
const { signIn } = useAuthActions();

// Inscription
async function handleSignUp(email: string, password: string) {
  const formData = new FormData();
  formData.set("email", email);
  formData.set("password", password);
  formData.set("flow", "signUp");
  await signIn("password", formData);
}

// Connexion
async function handleSignIn(email: string, password: string) {
  const formData = new FormData();
  formData.set("email", email);
  formData.set("password", password);
  formData.set("flow", "signIn");
  await signIn("password", formData);
}

Configuration OAuth (Google, GitHub)

ScaleRocket supporte les fournisseurs OAuth nativement.

Après avoir configuré les fournisseurs dans Supabase (voir Configuration de Supabase), le code côté client est simple :

// apps/app/src/lib/auth.ts
export async function signInWithGoogle() {
  const { data, error } = await supabase.auth.signInWithOAuth({
    provider: "google",
    options: {
      redirectTo: `${window.location.origin}/auth/callback`,
    },
  });

  if (error) throw error;
  return data;
}

export async function signInWithGitHub() {
  const { data, error } = await supabase.auth.signInWithOAuth({
    provider: "github",
    options: {
      redirectTo: `${window.location.origin}/auth/callback`,
    },
  });

  if (error) throw error;
  return data;
}

Après avoir configuré les fournisseurs dans Convex Auth, le code côté client est simple :

// apps/app/src/lib/auth.ts
import { useAuthActions } from "@convex-dev/auth/react";

// Dans un composant React
const { signIn } = useAuthActions();

// Google OAuth
async function signInWithGoogle() {
  await signIn("google");
}

// GitHub OAuth
async function signInWithGitHub() {
  await signIn("github");
}

Page de callback d'authentification

Après la redirection OAuth, la page de callback échange le code contre une session :

// apps/app/src/routes/auth/callback.tsx
import { useEffect } from "react";
import { useNavigate } from "@tanstack/react-router";
import { supabase } from "../../lib/supabase";

export default function AuthCallback() {
  const navigate = useNavigate();

  useEffect(() => {
    supabase.auth.onAuthStateChange((event) => {
      if (event === "SIGNED_IN") {
        navigate({ to: "/dashboard" });
      }
    });
  }, [navigate]);

  return <div>Completing sign in...</div>;
}

Avec Convex Auth, les callbacks OAuth sont gérés automatiquement. Aucune page de callback n'est nécessaire — l'utilisateur est redirigé vers l'application et la session est établie par le client Convex.

Réinitialisation du mot de passe

ScaleRocket inclut un flux complet de réinitialisation du mot de passe :

// Request password reset
export async function resetPassword(email: string) {
  const { error } = await supabase.auth.resetPasswordForEmail(email, {
    redirectTo: `${window.location.origin}/auth/reset-password`,
  });

  if (error) throw error;
}

// Update password (on the reset page)
export async function updatePassword(newPassword: string) {
  const { error } = await supabase.auth.updateUser({
    password: newPassword,
  });

  if (error) throw error;
}

Le flux :

  1. L'utilisateur entre son email sur /forgot-password
  2. Supabase envoie un email de réinitialisation avec un lien magique
  3. L'utilisateur clique sur le lien et arrive sur /auth/reset-password
  4. L'utilisateur entre un nouveau mot de passe
  5. L'application appelle updateUser() pour définir le nouveau mot de passe

ScaleRocket inclut un flux complet de réinitialisation du mot de passe :

import { useAuthActions } from "@convex-dev/auth/react";

const { signIn } = useAuthActions();

// Demander la réinitialisation du mot de passe
async function resetPassword(email: string) {
  const formData = new FormData();
  formData.set("email", email);
  formData.set("flow", "reset");
  await signIn("password", formData);
}

// Le code/lien de réinitialisation est envoyé par email.
// Quand l'utilisateur soumet le nouveau mot de passe avec le token :
async function updatePassword(code: string, newPassword: string) {
  const formData = new FormData();
  formData.set("code", code);
  formData.set("newPassword", newPassword);
  formData.set("flow", "reset-verification");
  await signIn("password", formData);
}

Le flux :

  1. L'utilisateur entre son email sur /forgot-password
  2. Convex Auth envoie un email de réinitialisation avec un code/lien
  3. L'utilisateur clique sur le lien ou entre le code
  4. L'utilisateur entre un nouveau mot de passe
  5. L'application appelle signIn("password", formData) avec le flux de vérification de réinitialisation

Gestion des sessions

Supabase gère le rafraîchissement des sessions automatiquement. Vous pouvez écouter les changements d'état d'authentification :

// apps/app/src/lib/supabase.ts
import { createClient } from "@supabase/supabase-js";

export const supabase = createClient(
  import.meta.env.VITE_SUPABASE_URL,
  import.meta.env.VITE_SUPABASE_ANON_KEY
);
// Listen for auth changes anywhere in the app
supabase.auth.onAuthStateChange((event, session) => {
  if (event === "SIGNED_OUT") {
    // Redirect to login
  }
  if (event === "TOKEN_REFRESHED") {
    // Session was refreshed automatically
  }
});

Obtenir l'utilisateur actuel :

const { data: { user } } = await supabase.auth.getUser();

Se déconnecter :

await supabase.auth.signOut();

Convex gère les sessions automatiquement. Utilisez le hook useConvexAuth() pour vérifier l'état d'authentification :

import { useConvexAuth } from "convex/react";

function MyComponent() {
  const { isAuthenticated, isLoading } = useConvexAuth();

  if (isLoading) return <div>Chargement...</div>;
  if (!isAuthenticated) return <div>Non connecté</div>;

  return <div>Bienvenue !</div>;
}

Obtenir l'utilisateur actuel côté serveur :

import { getAuthUserId } from "@convex-dev/auth/server";

// Dans une query ou mutation Convex
const userId = await getAuthUserId(ctx);

Se déconnecter :

import { useAuthActions } from "@convex-dev/auth/react";

const { signOut } = useAuthActions();
await signOut();

Routes protégées (TanStack Router)

ScaleRocket utilise TanStack Router avec un guard beforeLoad pour protéger les routes :

// apps/app/src/routes/__root.tsx
import { createRootRoute } from "@tanstack/react-router";
import { supabase } from "../lib/supabase";

export const Route = createRootRoute({
  beforeLoad: async ({ location }) => {
    const { data: { session } } = await supabase.auth.getSession();

    const publicRoutes = ["/login", "/signup", "/forgot-password", "/auth/callback"];
    const isPublicRoute = publicRoutes.some((route) =>
      location.pathname.startsWith(route)
    );

    if (!session && !isPublicRoute) {
      throw redirect({ to: "/login" });
    }

    if (session && (location.pathname === "/login" || location.pathname === "/signup")) {
      throw redirect({ to: "/dashboard" });
    }
  },
});
// apps/app/src/components/AuthGuard.tsx
import { useConvexAuth } from "convex/react";
import { Navigate } from "@tanstack/react-router";

export function AuthGuard({ children }: { children: React.ReactNode }) {
  const { isAuthenticated, isLoading } = useConvexAuth();

  if (isLoading) return <div>Chargement...</div>;
  if (!isAuthenticated) return <Navigate to="/login" />;

  return <>{children}</>;
}

Enveloppez vos layouts de routes protégées avec AuthGuard pour appliquer l'authentification.

Cela garantit :

  • Les utilisateurs non authentifiés sont redirigés vers /login
  • Les utilisateurs authentifiés sont redirigés loin des pages de connexion/inscription
  • Les routes publiques (connexion, inscription, callback) sont toujours accessibles

Profil utilisateur

Après l'inscription, ScaleRocket crée un profil pour l'utilisateur :

Une ligne est créée dans la table profiles via un trigger de base de données :

-- supabase/migrations/create_profile_trigger.sql
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS trigger AS $$
BEGIN
  INSERT INTO public.profiles (id, email, full_name, avatar_url)
  VALUES (
    NEW.id,
    NEW.email,
    NEW.raw_user_meta_data->>'full_name',
    NEW.raw_user_meta_data->>'avatar_url'
  );
  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER on_auth_user_created
  AFTER INSERT ON auth.users
  FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();

Un profil est créé via une mutation getOrCreateProfile, appelée automatiquement après l'authentification :

// convex/users.ts
import { mutation, query } from "./_generated/server";
import { getAuthUserId } from "@convex-dev/auth/server";

export const getOrCreateProfile = mutation({
  handler: async (ctx) => {
    const userId = await getAuthUserId(ctx);
    if (!userId) throw new Error("Not authenticated");

    const existing = await ctx.db
      .query("profiles")
      .withIndex("by_userId", (q) => q.eq("userId", userId))
      .unique();

    if (existing) return existing._id;

    const user = await ctx.db.get(userId);
    return await ctx.db.insert("profiles", {
      userId,
      email: user?.email ?? "",
      fullName: user?.name ?? "",
      avatarUrl: user?.image ?? "",
    });
  },
});

Prochaines étapes

Fini ? Marquez cette page comme terminée.

On this page