ScaleRocket/Web

Panneau d'administration

Comment fonctionne le panneau d'administration — gestion des utilisateurs, abonnements, contenu et analyses du tableau de bord.

Panneau d'administration

ScaleRocket inclut un panneau d'administration complet (apps/ops) construit avec Vite + React. Il vous offre un tableau de bord privé pour gérer les utilisateurs, suivre les abonnements, éditer le contenu du blog et consulter les analyses.

Comment fonctionne l'application Ops

Le panneau d'administration est une application Vite séparée qui tourne sur le port 5174. Il partage des packages avec le tableau de bord principal (packages/ui, packages/types, packages/config) mais possède ses propres routes et pages.

apps/ops/
├── src/
│   ├── routes/
│   │   ├── __root.tsx         # Root layout with sidebar
│   │   ├── index.tsx          # Dashboard stats
│   │   ├── users/
│   │   │   ├── index.tsx      # Users list
│   │   │   └── $userId.tsx    # User detail
│   │   ├── subscriptions/
│   │   │   └── index.tsx      # Subscriptions list
│   │   └── blog/
│   │       ├── index.tsx      # Blog posts list
│   │       └── editor.tsx     # Blog post editor
│   ├── lib/
│   │   ├── supabase.ts        # Supabase client
│   │   └── api.ts             # API utilities
│   └── main.tsx
├── .env.example
└── package.json

Liste blanche des administrateurs

L'accès au panneau d'administration est restreint par une liste blanche d'emails. Seuls les emails dans la liste blanche peuvent se connecter.

Configurer la liste blanche

Définissez les emails d'administration dans votre environnement :

# apps/ops/.env.local
VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_ANON_KEY=eyJ...
VITE_ADMIN_EMAILS=you@yourdomain.com,cofounder@yourdomain.com

La route racine vérifie la liste blanche au chargement :

// apps/ops/src/routes/__root.tsx
const adminEmails = import.meta.env.VITE_ADMIN_EMAILS?.split(",") || [];

export const Route = createRootRoute({
  beforeLoad: async () => {
    const { data: { user } } = await supabase.auth.getUser();

    if (!user || !adminEmails.includes(user.email)) {
      throw redirect({ to: "/unauthorized" });
    }
  },
});

Note : Pour une sécurité renforcée, vous pouvez également appliquer l'accès administrateur au niveau de la base de données avec des politiques RLS qui vérifient une table admins.

Statistiques du tableau de bord

La page d'accueil du panneau d'administration affiche les métriques clés en un coup d'oeil :

  • Total utilisateurs — nombre de tous les utilisateurs inscrits
  • Abonnements actifs — nombre par plan
  • Revenu mensuel — calculé à partir des abonnements actifs
  • Nouvelles inscriptions — utilisateurs inscrits dans les 7 derniers jours
  • Utilisation des crédits — total des crédits consommés ce mois-ci
// apps/ops/src/routes/index.tsx
import { supabase } from "../lib/supabase";

async function getDashboardStats() {
  const [
    { count: totalUsers },
    { count: activeSubscriptions },
    { data: recentSignups },
  ] = await Promise.all([
    supabase.from("profiles").select("*", { count: "exact", head: true }),
    supabase
      .from("subscriptions")
      .select("*", { count: "exact", head: true })
      .eq("status", "active"),
    supabase
      .from("profiles")
      .select("id, email, created_at")
      .gte("created_at", new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString())
      .order("created_at", { ascending: false }),
  ]);

  return { totalUsers, activeSubscriptions, recentSignups };
}

Gestion des utilisateurs

La page des utilisateurs liste tous les utilisateurs inscrits avec recherche et pagination :

  • Voir les détails de l'utilisateur — email, date d'inscription, plan, solde de crédits
  • Voir l'abonnement — plan actuel, période de facturation, prochain renouvellement
  • Voir les crédits — solde actuel, allocation mensuelle, historique d'utilisation

Page de détail de l'utilisateur

Cliquez sur n'importe quel utilisateur pour voir son profil complet :

// apps/ops/src/routes/users/$userId.tsx
async function getUserDetail(userId: string) {
  const [
    { data: profile },
    { data: subscription },
    { data: credits },
  ] = await Promise.all([
    supabase.from("profiles").select("*").eq("id", userId).single(),
    supabase.from("subscriptions").select("*").eq("user_id", userId).single(),
    supabase.from("credits").select("*").eq("user_id", userId).single(),
  ]);

  return { profile, subscription, credits };
}

Note : Le panneau d'administration utilise la clé service_role via les Edge Functions pour les opérations qui doivent contourner le RLS (comme consulter les données d'autres utilisateurs). Les requêtes Supabase directes depuis l'application ops utilisent des politiques RLS limitées aux utilisateurs admin.

Gestion des abonnements

La page des abonnements affiche tous les abonnements actifs, annulés et en retard de paiement :

  • Filtrer par statut (active, cancelled, past_due, trialing)
  • Voir les détails de l'abonnement (plan, montant, cycle de facturation)
  • Lien vers le client dans le Stripe Dashboard
// Fetch subscriptions with user info
const { data: subscriptions } = await supabase
  .from("subscriptions")
  .select(`
    *,
    profiles:user_id (email, full_name)
  `)
  .order("created_at", { ascending: false });

Éditeur de blog

Le panneau d'administration inclut un éditeur de blog pour publier du contenu sur le site marketing :

Lister les articles de blog

const { data: posts } = await supabase
  .from("blog_posts")
  .select("id, title, slug, status, published_at, created_at")
  .order("created_at", { ascending: false });

Créer/Modifier un article

L'éditeur de blog supporte :

  • Titre et slug (généré automatiquement à partir du titre)
  • Contenu en Markdown
  • Statut — brouillon ou publié
  • Image de couverture téléversée (stockée dans Supabase Storage)
  • SEO — meta title et meta description
// Save a blog post
async function savePost(post: BlogPost) {
  const { data, error } = await supabase
    .from("blog_posts")
    .upsert({
      id: post.id,
      title: post.title,
      slug: post.slug,
      content: post.content,
      status: post.status,
      cover_image: post.coverImage,
      meta_title: post.metaTitle,
      meta_description: post.metaDescription,
      published_at: post.status === "published" ? new Date().toISOString() : null,
    })
    .select()
    .single();

  if (error) throw error;
  return data;
}

Lancer le panneau d'administration

Démarrez toutes les applications depuis la racine :

pnpm dev

Ou démarrez uniquement le panneau d'administration :

pnpm --filter ops dev

Visitez http://localhost:5174 et connectez-vous avec un email de la liste blanche admin.

Considérations de sécurité

  • La liste blanche admin est une première couche de défense, pas la seule
  • Utilisez des politiques RLS avec une table admins pour une protection au niveau de la base de données
  • N'exposez jamais les clés service_role au client
  • Envisagez d'ajouter un journal d'audit pour les actions d'administration
  • En production, restreignez le panneau d'administration à un sous-domaine séparé (ex. admin.yourdomain.com)

Prochaines étapes

Fini ? Marquez cette page comme terminée.

On this page