Files
mes-budgets-participatifs/src/app/setup/page.tsx

534 lines
19 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Loader2, CheckCircle, AlertCircle, Database, Key, User, Shield } from 'lucide-react';
import SqlSchemaDisplay from '@/components/SqlSchemaDisplay';
interface SetupStep {
id: string;
title: string;
description: string;
status: 'pending' | 'current' | 'completed' | 'error';
icon: React.ReactNode;
}
export default function SetupPage() {
const router = useRouter();
const [currentStep, setCurrentStep] = useState(0);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState(false);
const [formData, setFormData] = useState({
supabaseUrl: '',
supabaseAnonKey: '',
supabaseServiceKey: '',
adminEmail: '',
adminPassword: '',
adminConfirmPassword: ''
});
const steps: SetupStep[] = [
{
id: 'supabase-project',
title: 'Créer un projet Supabase',
description: 'Créez un nouveau projet sur Supabase.com',
status: 'pending',
icon: <Database className="h-5 w-5" />
},
{
id: 'supabase-keys',
title: 'Récupérer les clés Supabase',
description: 'Copiez les clés de votre projet',
status: 'pending',
icon: <Key className="h-5 w-5" />
},
{
id: 'database-setup',
title: 'Configurer la base de données',
description: 'Créer les tables et politiques de sécurité',
status: 'pending',
icon: <Database className="h-5 w-5" />
},
{
id: 'admin-creation',
title: 'Créer l\'administrateur',
description: 'Créer le premier compte administrateur',
status: 'pending',
icon: <User className="h-5 w-5" />
},
{
id: 'security-setup',
title: 'Configurer la sécurité',
description: 'Activer les politiques RLS',
status: 'pending',
icon: <Shield className="h-5 w-5" />
}
];
useEffect(() => {
// Vérifier si Supabase est déjà configuré
checkExistingSetup();
}, []);
const checkExistingSetup = async () => {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
if (supabaseUrl && supabaseAnonKey && supabaseUrl !== 'https://placeholder.supabase.co') {
// Supabase est déjà configuré, rediriger vers l'accueil
router.push('/');
}
};
const updateStepStatus = (stepIndex: number, status: SetupStep['status']) => {
steps[stepIndex].status = status;
};
const handleInputChange = (field: string, value: string) => {
setFormData(prev => ({
...prev,
[field]: value
}));
};
const validateSupabaseKeys = () => {
if (!formData.supabaseUrl || !formData.supabaseAnonKey || !formData.supabaseServiceKey) {
setError('Veuillez remplir tous les champs Supabase');
return false;
}
return true;
};
const validateAdminCredentials = () => {
if (!formData.adminEmail || !formData.adminPassword || !formData.adminConfirmPassword) {
setError('Veuillez remplir tous les champs administrateur');
return false;
}
if (formData.adminPassword !== formData.adminConfirmPassword) {
setError('Les mots de passe ne correspondent pas');
return false;
}
if (formData.adminPassword.length < 6) {
setError('Le mot de passe doit contenir au moins 6 caractères');
return false;
}
return true;
};
const handleNextStep = async () => {
setError('');
if (currentStep === 1) {
// Validation des clés Supabase
if (!validateSupabaseKeys()) return;
}
if (currentStep === 3) {
// Validation des credentials admin
if (!validateAdminCredentials()) return;
}
if (currentStep === steps.length - 1) {
// Dernière étape : finaliser la configuration
await finalizeSetup();
return;
}
setCurrentStep(prev => prev + 1);
};
const handlePreviousStep = () => {
setCurrentStep(prev => Math.max(0, prev - 1));
};
const finalizeSetup = async () => {
setLoading(true);
setError('');
try {
// Ici nous appellerons l'API pour finaliser la configuration
const response = await fetch('/api/setup/finalize', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Erreur lors de la configuration');
}
setSuccess(true);
setTimeout(() => {
router.push('/admin');
}, 2000);
} catch (error: any) {
setError(error.message || 'Erreur lors de la configuration');
} finally {
setLoading(false);
}
};
const renderStepContent = () => {
switch (currentStep) {
case 0:
return (
<div className="space-y-6">
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
Vous devez créer un projet Supabase pour utiliser cette application.
</AlertDescription>
</Alert>
<div className="space-y-4">
<h3 className="text-lg font-semibold">Étapes pour créer un projet Supabase :</h3>
<ol className="list-decimal list-inside space-y-2 text-sm">
<li>Allez sur <a href="https://supabase.com" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">supabase.com</a></li>
<li>Cliquez sur "Start your project"</li>
<li>Connectez-vous ou créez un compte</li>
<li>Cliquez sur "New project"</li>
<li>Choisissez votre organisation</li>
<li>Donnez un nom à votre projet (ex: "mes-budgets-participatifs")</li>
<li>Créez un mot de passe pour la base de données</li>
<li>Choisissez une région proche de vous</li>
<li>Cliquez sur "Create new project"</li>
<li>Attendez que le projet soit créé (2-3 minutes)</li>
</ol>
</div>
<div className="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg">
<p className="text-sm text-blue-800 dark:text-blue-200">
<strong>Note :</strong> Une fois votre projet créé, vous aurez besoin de l'URL et des clés API que nous configurerons dans l'étape suivante.
</p>
</div>
</div>
);
case 1:
return (
<div className="space-y-6">
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
Récupérez les clés de votre projet Supabase dans les paramètres.
</AlertDescription>
</Alert>
<div className="space-y-4">
<h3 className="text-lg font-semibold">Comment récupérer vos clés :</h3>
<ol className="list-decimal list-inside space-y-2 text-sm">
<li>Dans votre projet Supabase, allez dans "Settings" ()</li>
<li>Cliquez sur "API" dans le menu de gauche</li>
<li>Copiez l'URL du projet (Project URL)</li>
<li>Copiez la clé anon/public (anon public key)</li>
<li>Copiez la clé service_role (service_role key)</li>
</ol>
</div>
<div className="space-y-4">
<div>
<Label htmlFor="supabaseUrl">URL du projet Supabase</Label>
<Input
id="supabaseUrl"
type="url"
placeholder="https://your-project.supabase.co"
value={formData.supabaseUrl}
onChange={(e) => handleInputChange('supabaseUrl', e.target.value)}
/>
</div>
<div>
<Label htmlFor="supabaseAnonKey">Clé anon/public</Label>
<Input
id="supabaseAnonKey"
type="password"
placeholder="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
value={formData.supabaseAnonKey}
onChange={(e) => handleInputChange('supabaseAnonKey', e.target.value)}
/>
</div>
<div>
<Label htmlFor="supabaseServiceKey">Clé service_role</Label>
<Input
id="supabaseServiceKey"
type="password"
placeholder="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
value={formData.supabaseServiceKey}
onChange={(e) => handleInputChange('supabaseServiceKey', e.target.value)}
/>
</div>
</div>
</div>
);
case 2:
return (
<div className="space-y-6">
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
Vous devez créer les tables de base de données dans votre projet Supabase. L'assistant nettoiera automatiquement les données existantes.
</AlertDescription>
</Alert>
<div className="space-y-4">
<h3 className="text-lg font-semibold">Comment créer les tables :</h3>
<ol className="list-decimal list-inside space-y-2 text-sm">
<li>Dans votre projet Supabase, allez dans "SQL Editor"</li>
<li>Cliquez sur "New query"</li>
<li>Copiez le schéma SQL ci-dessous</li>
<li>Collez-le dans l'éditeur SQL</li>
<li>Cliquez sur "Run" pour exécuter le script</li>
<li>Vérifiez que les tables sont créées dans "Table Editor"</li>
</ol>
</div>
<SqlSchemaDisplay />
<div className="space-y-4">
<h3 className="text-lg font-semibold">Ce qui va être créé :</h3>
<ul className="list-disc list-inside space-y-2 text-sm">
<li>Tables : campaigns, propositions, participants, votes, settings, admin_users</li>
<li>Politiques de sécurité (RLS)</li>
<li>Fonctions utilitaires</li>
<li>Index et contraintes</li>
</ul>
</div>
<div className="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg">
<p className="text-sm text-blue-800 dark:text-blue-200">
<strong>Info :</strong> L'assistant nettoiera automatiquement toutes les données existantes avant de créer le nouvel administrateur.
</p>
</div>
<div className="bg-yellow-50 dark:bg-yellow-900/20 p-4 rounded-lg">
<p className="text-sm text-yellow-800 dark:text-yellow-200">
<strong>Important :</strong> Cette étape est manuelle. Vous devez exécuter le script SQL dans votre projet Supabase avant de continuer.
</p>
</div>
</div>
);
case 3:
return (
<div className="space-y-6">
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
Créez le premier compte administrateur pour accéder à l'interface d'administration.
</AlertDescription>
</Alert>
<div className="space-y-4">
<div>
<Label htmlFor="adminEmail">Email administrateur</Label>
<Input
id="adminEmail"
type="email"
placeholder="admin@example.com"
value={formData.adminEmail}
onChange={(e) => handleInputChange('adminEmail', e.target.value)}
/>
</div>
<div>
<Label htmlFor="adminPassword">Mot de passe</Label>
<Input
id="adminPassword"
type="password"
placeholder="Mot de passe sécurisé"
value={formData.adminPassword}
onChange={(e) => handleInputChange('adminPassword', e.target.value)}
/>
</div>
<div>
<Label htmlFor="adminConfirmPassword">Confirmer le mot de passe</Label>
<Input
id="adminConfirmPassword"
type="password"
placeholder="Confirmez votre mot de passe"
value={formData.adminConfirmPassword}
onChange={(e) => handleInputChange('adminConfirmPassword', e.target.value)}
/>
</div>
</div>
<div className="bg-yellow-50 dark:bg-yellow-900/20 p-4 rounded-lg">
<p className="text-sm text-yellow-800 dark:text-yellow-200">
<strong>Important :</strong> Gardez ces identifiants en sécurité. Vous en aurez besoin pour accéder à l'administration.
</p>
</div>
</div>
);
case 4:
return (
<div className="space-y-6">
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
Configuration finale de la sécurité et activation du mode production.
</AlertDescription>
</Alert>
<div className="space-y-4">
<h3 className="text-lg font-semibold">Configuration finale :</h3>
<ul className="list-disc list-inside space-y-2 text-sm">
<li>Activation des politiques RLS (Row Level Security)</li>
<li>Configuration des permissions utilisateur</li>
<li>Création des variables d'environnement</li>
<li>Test de connexion à la base de données</li>
<li>Activation du mode production</li>
</ul>
</div>
<div className="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg">
<p className="text-sm text-blue-800 dark:text-blue-200">
<strong>Prêt !</strong> Une fois cette étape terminée, vous pourrez accéder à l'interface d'administration et commencer à créer vos campagnes.
</p>
</div>
</div>
);
default:
return null;
}
};
if (success) {
return (
<div className="min-h-screen bg-slate-50 dark:bg-slate-900 flex items-center justify-center">
<Card className="w-full max-w-md">
<CardHeader className="text-center">
<CheckCircle className="h-12 w-12 text-green-600 mx-auto mb-4" />
<CardTitle>Configuration terminée !</CardTitle>
<CardDescription>
Votre application est maintenant configurée et prête à être utilisée.
</CardDescription>
</CardHeader>
<CardContent className="text-center">
<p className="text-sm text-slate-600 dark:text-slate-400 mb-4">
Redirection vers l'administration...
</p>
<Loader2 className="h-6 w-6 animate-spin mx-auto" />
</CardContent>
</Card>
</div>
);
}
return (
<div className="min-h-screen bg-slate-50 dark:bg-slate-900 py-8">
<div className="container mx-auto px-4 max-w-4xl">
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-slate-900 dark:text-slate-100 mb-2">
Configuration de Mes Budgets Participatifs
</h1>
<p className="text-slate-600 dark:text-slate-400">
Assistant de configuration pour votre nouvelle installation
</p>
</div>
{/* Étapes */}
<div className="mb-8">
<div className="flex items-center justify-between">
{steps.map((step, index) => (
<div key={step.id} className="flex items-center">
<div className={`flex items-center justify-center w-10 h-10 rounded-full border-2 ${
step.status === 'completed' ? 'bg-green-500 border-green-500 text-white' :
step.status === 'current' ? 'bg-blue-500 border-blue-500 text-white' :
'bg-slate-200 dark:bg-slate-700 border-slate-300 dark:border-slate-600 text-slate-600 dark:text-slate-400'
}`}>
{step.status === 'completed' ? (
<CheckCircle className="h-5 w-5" />
) : (
step.icon
)}
</div>
{index < steps.length - 1 && (
<div className={`w-16 h-0.5 mx-2 ${
step.status === 'completed' ? 'bg-green-500' : 'bg-slate-300 dark:bg-slate-600'
}`} />
)}
</div>
))}
</div>
<div className="mt-4 grid grid-cols-5 gap-4">
{steps.map((step) => (
<div key={step.id} className="text-center">
<p className={`text-xs font-medium ${
step.status === 'current' ? 'text-blue-600 dark:text-blue-400' :
step.status === 'completed' ? 'text-green-600 dark:text-green-400' :
'text-slate-500 dark:text-slate-400'
}`}>
{step.title}
</p>
</div>
))}
</div>
</div>
{/* Contenu de l'étape */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
{steps[currentStep].icon}
{steps[currentStep].title}
</CardTitle>
<CardDescription>
{steps[currentStep].description}
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{renderStepContent()}
{/* Boutons de navigation */}
<div className="flex justify-between pt-6">
<Button
variant="outline"
onClick={handlePreviousStep}
disabled={currentStep === 0}
>
Précédent
</Button>
<Button
onClick={handleNextStep}
disabled={loading}
>
{loading && <Loader2 className="h-4 w-4 animate-spin mr-2" />}
{currentStep === steps.length - 1 ? 'Terminer la configuration' : 'Suivant'}
</Button>
</div>
</CardContent>
</Card>
</div>
</div>
);
}