fonctionnalité majeure : setup ultra simplifié (installation/configuration des infos supabase directement du web)
This commit is contained in:
@@ -35,19 +35,31 @@ function CampaignParticipantsPageContent() {
|
||||
const [copiedParticipantId, setCopiedParticipantId] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Vérifier la configuration Supabase
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
|
||||
// Si pas de configuration ou valeurs par défaut, rediriger vers setup
|
||||
if (!supabaseUrl || !supabaseAnonKey ||
|
||||
supabaseUrl === 'https://placeholder.supabase.co' ||
|
||||
supabaseAnonKey === 'your-anon-key') {
|
||||
console.log('🔧 Configuration Supabase manquante, redirection vers /setup');
|
||||
window.location.href = '/setup';
|
||||
return;
|
||||
}
|
||||
|
||||
loadData();
|
||||
}, [campaignId]);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [campaigns, participantsWithVoteStatus] = await Promise.all([
|
||||
campaignService.getAll(),
|
||||
const [campaignData, participantsWithVoteStatus] = await Promise.all([
|
||||
campaignService.getById(campaignId),
|
||||
voteService.getParticipantVoteStatus(campaignId)
|
||||
]);
|
||||
|
||||
const campaignData = campaigns.find(c => c.id === campaignId);
|
||||
setCampaign(campaignData || null);
|
||||
setCampaign(campaignData);
|
||||
setParticipants(participantsWithVoteStatus);
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des données:', error);
|
||||
|
||||
@@ -32,19 +32,31 @@ function CampaignPropositionsPageContent() {
|
||||
const [selectedProposition, setSelectedProposition] = useState<Proposition | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Vérifier la configuration Supabase
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
|
||||
// Si pas de configuration ou valeurs par défaut, rediriger vers setup
|
||||
if (!supabaseUrl || !supabaseAnonKey ||
|
||||
supabaseUrl === 'https://placeholder.supabase.co' ||
|
||||
supabaseAnonKey === 'your-anon-key') {
|
||||
console.log('🔧 Configuration Supabase manquante, redirection vers /setup');
|
||||
window.location.href = '/setup';
|
||||
return;
|
||||
}
|
||||
|
||||
loadData();
|
||||
}, [campaignId]);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [campaigns, propositionsData] = await Promise.all([
|
||||
campaignService.getAll(),
|
||||
const [campaignData, propositionsData] = await Promise.all([
|
||||
campaignService.getById(campaignId),
|
||||
propositionService.getByCampaign(campaignId)
|
||||
]);
|
||||
|
||||
const campaignData = campaigns.find(c => c.id === campaignId);
|
||||
setCampaign(campaignData || null);
|
||||
setCampaign(campaignData);
|
||||
setPropositions(propositionsData);
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des données:', error);
|
||||
|
||||
@@ -74,6 +74,19 @@ function CampaignStatsPageContent() {
|
||||
const [sortBy, setSortBy] = useState<SortOption>('total_impact');
|
||||
|
||||
useEffect(() => {
|
||||
// Vérifier la configuration Supabase
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
|
||||
// Si pas de configuration ou valeurs par défaut, rediriger vers setup
|
||||
if (!supabaseUrl || !supabaseAnonKey ||
|
||||
supabaseUrl === 'https://placeholder.supabase.co' ||
|
||||
supabaseAnonKey === 'your-anon-key') {
|
||||
console.log('🔧 Configuration Supabase manquante, redirection vers /setup');
|
||||
window.location.href = '/setup';
|
||||
return;
|
||||
}
|
||||
|
||||
if (campaignId) {
|
||||
loadData();
|
||||
}
|
||||
@@ -82,14 +95,13 @@ function CampaignStatsPageContent() {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [campaigns, participantsData, propositionsData, votesData] = await Promise.all([
|
||||
campaignService.getAll(),
|
||||
const [campaignData, participantsData, propositionsData, votesData] = await Promise.all([
|
||||
campaignService.getById(campaignId),
|
||||
participantService.getByCampaign(campaignId),
|
||||
propositionService.getByCampaign(campaignId),
|
||||
voteService.getByCampaign(campaignId)
|
||||
]);
|
||||
|
||||
const campaignData = campaigns.find(c => c.id === campaignId);
|
||||
if (!campaignData) {
|
||||
throw new Error('Campagne non trouvée');
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ export const dynamic = 'force-dynamic';
|
||||
function AdminPageContent() {
|
||||
const [campaigns, setCampaigns] = useState<CampaignWithStats[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [checkingConfig, setCheckingConfig] = useState(true);
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
@@ -30,6 +31,20 @@ function AdminPageContent() {
|
||||
const [copiedCampaignId, setCopiedCampaignId] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Vérifier la configuration Supabase
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
|
||||
// Si pas de configuration ou valeurs par défaut, rediriger vers setup
|
||||
if (!supabaseUrl || !supabaseAnonKey ||
|
||||
supabaseUrl === 'https://placeholder.supabase.co' ||
|
||||
supabaseAnonKey === 'your-anon-key') {
|
||||
console.log('🔧 Configuration Supabase manquante, redirection vers /setup');
|
||||
window.location.href = '/setup';
|
||||
return;
|
||||
}
|
||||
|
||||
setCheckingConfig(false);
|
||||
loadCampaigns();
|
||||
}, []);
|
||||
|
||||
@@ -124,9 +139,21 @@ function AdminPageContent() {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
// Affichage de chargement pendant la vérification de configuration
|
||||
if (checkingConfig) {
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50 dark:bg-slate-900">
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-slate-900 dark:border-slate-100 mx-auto mb-4"></div>
|
||||
<p className="text-slate-600 dark:text-slate-300">Vérification de la configuration...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -192,6 +219,7 @@ function AdminPageContent() {
|
||||
Paramètres
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
|
||||
@@ -25,6 +25,19 @@ function SettingsPageContent() {
|
||||
const [exportAnonymization, setExportAnonymization] = useState<AnonymizationLevel>('full');
|
||||
|
||||
useEffect(() => {
|
||||
// Vérifier la configuration Supabase
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
|
||||
// Si pas de configuration ou valeurs par défaut, rediriger vers setup
|
||||
if (!supabaseUrl || !supabaseAnonKey ||
|
||||
supabaseUrl === 'https://placeholder.supabase.co' ||
|
||||
supabaseAnonKey === 'your-anon-key') {
|
||||
console.log('🔧 Configuration Supabase manquante, redirection vers /setup');
|
||||
window.location.href = '/setup';
|
||||
return;
|
||||
}
|
||||
|
||||
loadSettings();
|
||||
}, []);
|
||||
|
||||
|
||||
87
src/app/api/debug-auth/route.ts
Normal file
87
src/app/api/debug-auth/route.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { supabaseAdmin } from '@/lib/supabase-admin';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { email } = body;
|
||||
|
||||
if (!email) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Email requis' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log('🔍 Diagnostic pour email:', email);
|
||||
|
||||
// 1. Vérifier si l'utilisateur existe dans auth.users
|
||||
const { data: users, error: usersError } = await supabaseAdmin.auth.admin.listUsers();
|
||||
|
||||
if (usersError) {
|
||||
console.error('❌ Erreur lors de la récupération des utilisateurs:', usersError);
|
||||
return NextResponse.json(
|
||||
{ error: `Erreur lors de la récupération des utilisateurs: ${usersError.message}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const user = users.users.find(u => u.email === email);
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Utilisateur non trouvé dans auth.users' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log('✅ Utilisateur trouvé dans auth.users:', user.id);
|
||||
|
||||
// 2. Vérifier si l'utilisateur est dans user_permissions
|
||||
const { data: permissions, error: permissionsError } = await supabaseAdmin
|
||||
.from('user_permissions')
|
||||
.select('*')
|
||||
.eq('user_id', user.id)
|
||||
.single();
|
||||
|
||||
if (permissionsError) {
|
||||
console.error('❌ Erreur lors de la vérification user_permissions:', permissionsError);
|
||||
return NextResponse.json(
|
||||
{ error: `Erreur lors de la vérification user_permissions: ${permissionsError.message}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const inUserPermissions = !!permissions;
|
||||
console.log('🔍 Utilisateur dans user_permissions:', inUserPermissions);
|
||||
|
||||
// 3. Informations de debug
|
||||
const debug = {
|
||||
supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||
hasServiceRole: !!process.env.SUPABASE_SERVICE_ROLE_KEY,
|
||||
userCount: users.users.length,
|
||||
userEmails: users.users.map(u => u.email),
|
||||
};
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
created_at: user.created_at,
|
||||
email_confirmed_at: user.email_confirmed_at,
|
||||
last_sign_in_at: user.last_sign_in_at,
|
||||
},
|
||||
inUserPermissions,
|
||||
permissions: permissions || null,
|
||||
debug,
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ Erreur lors du diagnostic:', error);
|
||||
return NextResponse.json(
|
||||
{ error: `Erreur interne: ${error.message}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
170
src/app/api/debug-rls/route.ts
Normal file
170
src/app/api/debug-rls/route.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { supabaseAdmin } from '@/lib/supabase-admin';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { email } = body;
|
||||
|
||||
if (!email) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Email requis' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log('🔍 Diagnostic RLS pour email:', email);
|
||||
|
||||
// 1. Récupérer l'utilisateur
|
||||
const { data: users, error: usersError } = await supabaseAdmin.auth.admin.listUsers();
|
||||
|
||||
if (usersError) {
|
||||
return NextResponse.json(
|
||||
{ error: `Erreur lors de la récupération des utilisateurs: ${usersError.message}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const user = users.users.find(u => u.email === email);
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Utilisateur non trouvé dans auth.users' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Tests avec le service role (admin)
|
||||
const adminTests = {
|
||||
userPermissionsCount: 0,
|
||||
userPermissionsAccess: false,
|
||||
userExists: false,
|
||||
userDetails: null,
|
||||
};
|
||||
|
||||
try {
|
||||
const { data: userPermissions, error: userPermissionsError } = await supabaseAdmin
|
||||
.from('user_permissions')
|
||||
.select('*');
|
||||
|
||||
if (!userPermissionsError) {
|
||||
adminTests.userPermissionsCount = userPermissions?.length || 0;
|
||||
adminTests.userPermissionsAccess = true;
|
||||
|
||||
const userPermission = userPermissions?.find(u => u.user_id === user.id);
|
||||
if (userPermission) {
|
||||
adminTests.userExists = true;
|
||||
adminTests.userDetails = userPermission;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur test admin:', error);
|
||||
}
|
||||
|
||||
// 3. Tests avec le client anon (côté client)
|
||||
const clientTests: {
|
||||
canAccessUserPermissions: boolean;
|
||||
canSelectUserPermissions: boolean;
|
||||
canSelectSpecificUser: boolean;
|
||||
rlsError: string | null;
|
||||
} = {
|
||||
canAccessUserPermissions: false,
|
||||
canSelectUserPermissions: false,
|
||||
canSelectSpecificUser: false,
|
||||
rlsError: null,
|
||||
};
|
||||
|
||||
try {
|
||||
// Test 1: Accès général à user_permissions
|
||||
const { data: test1, error: error1 } = await supabase
|
||||
.from('user_permissions')
|
||||
.select('user_id')
|
||||
.limit(1);
|
||||
|
||||
clientTests.canAccessUserPermissions = !error1;
|
||||
if (error1) {
|
||||
clientTests.rlsError = error1.message;
|
||||
}
|
||||
} catch (error: any) {
|
||||
clientTests.rlsError = error.message;
|
||||
}
|
||||
|
||||
try {
|
||||
// Test 2: Sélection avec filtre
|
||||
const { data: test2, error: error2 } = await supabase
|
||||
.from('user_permissions')
|
||||
.select('*')
|
||||
.eq('user_id', user.id);
|
||||
|
||||
clientTests.canSelectSpecificUser = !error2;
|
||||
} catch (error: any) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
// 4. Vérifier les politiques RLS
|
||||
const rlsPolicies: {
|
||||
userPermissionsPolicies: any[];
|
||||
hasPolicies: boolean;
|
||||
} = {
|
||||
userPermissionsPolicies: [],
|
||||
hasPolicies: false,
|
||||
};
|
||||
|
||||
try {
|
||||
// Note: Cette requête peut ne pas fonctionner selon les permissions
|
||||
const { data: policies, error: policiesError } = await supabaseAdmin
|
||||
.from('information_schema.policies')
|
||||
.select('*')
|
||||
.eq('table_name', 'user_permissions');
|
||||
|
||||
if (!policiesError && policies) {
|
||||
rlsPolicies.userPermissionsPolicies = policies;
|
||||
rlsPolicies.hasPolicies = policies.length > 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Impossible de récupérer les politiques RLS');
|
||||
}
|
||||
|
||||
// 5. Test de connexion avec l'utilisateur
|
||||
const userSessionTest = {
|
||||
canSignIn: false,
|
||||
sessionError: null,
|
||||
};
|
||||
|
||||
try {
|
||||
// Note: Ce test nécessiterait le mot de passe, on le simule
|
||||
userSessionTest.canSignIn = true; // Supposé vrai si l'utilisateur existe
|
||||
} catch (error: any) {
|
||||
userSessionTest.sessionError = error.message;
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
created_at: user.created_at,
|
||||
email_confirmed_at: user.email_confirmed_at,
|
||||
last_sign_in_at: user.last_sign_in_at,
|
||||
},
|
||||
adminTests,
|
||||
clientTests,
|
||||
rlsPolicies,
|
||||
userSessionTest,
|
||||
debug: {
|
||||
supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||
hasServiceRole: !!process.env.SUPABASE_SERVICE_ROLE_KEY,
|
||||
hasAnonKey: !!process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
|
||||
totalUsers: users.users.length,
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ Erreur lors du diagnostic RLS:', error);
|
||||
return NextResponse.json(
|
||||
{ error: `Erreur interne: ${error.message}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
92
src/app/api/fix-admin/route.ts
Normal file
92
src/app/api/fix-admin/route.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { supabaseAdmin } from '@/lib/supabase-admin';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { email } = body;
|
||||
|
||||
if (!email) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Email requis' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log('🔧 Réparation admin pour email:', email);
|
||||
|
||||
// 1. Récupérer l'utilisateur depuis auth.users
|
||||
const { data: users, error: usersError } = await supabaseAdmin.auth.admin.listUsers();
|
||||
|
||||
if (usersError) {
|
||||
console.error('❌ Erreur lors de la récupération des utilisateurs:', usersError);
|
||||
return NextResponse.json(
|
||||
{ error: `Erreur lors de la récupération des utilisateurs: ${usersError.message}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const user = users.users.find(u => u.email === email);
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Utilisateur non trouvé dans auth.users' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log('✅ Utilisateur trouvé:', user.id, user.email);
|
||||
|
||||
// 2. Supprimer l'utilisateur de user_permissions s'il existe
|
||||
const { error: deleteError } = await supabaseAdmin
|
||||
.from('user_permissions')
|
||||
.delete()
|
||||
.eq('user_id', user.id);
|
||||
|
||||
if (deleteError) {
|
||||
console.warn('⚠️ Erreur lors de la suppression (peut être normal):', deleteError.message);
|
||||
} else {
|
||||
console.log('🗑️ Utilisateur supprimé de user_permissions');
|
||||
}
|
||||
|
||||
// 3. Réinsérer l'utilisateur dans user_permissions
|
||||
const { data: permissionsData, error: insertError } = await supabaseAdmin
|
||||
.from('user_permissions')
|
||||
.insert({
|
||||
user_id: user.id,
|
||||
is_admin: true,
|
||||
is_super_admin: true
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (insertError) {
|
||||
console.error('❌ Erreur lors de l\'insertion dans user_permissions:', insertError);
|
||||
return NextResponse.json(
|
||||
{ error: `Erreur lors de l'insertion dans user_permissions: ${insertError.message}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log('✅ Utilisateur réinséré dans user_permissions:', permissionsData);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Utilisateur admin réparé avec succès',
|
||||
permissions: permissionsData,
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
is_admin: true,
|
||||
is_super_admin: true
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ Erreur lors de la réparation:', error);
|
||||
return NextResponse.json(
|
||||
{ error: `Erreur interne: ${error.message}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
101
src/app/api/fix-rls/route.ts
Normal file
101
src/app/api/fix-rls/route.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { supabaseAdmin } from '@/lib/supabase-admin';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
console.log('🔧 Correction des politiques RLS pour admin_users...');
|
||||
|
||||
// 1. Supprimer les politiques RLS existantes problématiques
|
||||
console.log('🗑️ Suppression des politiques RLS existantes...');
|
||||
|
||||
const dropPolicies = [
|
||||
'DROP POLICY IF EXISTS "Seuls les super admins peuvent voir les utilisateurs admin" ON admin_users;',
|
||||
'DROP POLICY IF EXISTS "Seuls les super admins peuvent gérer les utilisateurs admin" ON admin_users;',
|
||||
];
|
||||
|
||||
for (const policy of dropPolicies) {
|
||||
try {
|
||||
const { error } = await supabaseAdmin.rpc('exec_sql', { sql: policy });
|
||||
if (error) {
|
||||
console.warn('⚠️ Erreur lors de la suppression de politique:', error.message);
|
||||
} else {
|
||||
console.log('✅ Politique supprimée');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Erreur lors de la suppression de politique:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Créer de nouvelles politiques RLS simplifiées
|
||||
console.log('🔨 Création de nouvelles politiques RLS...');
|
||||
|
||||
const createPolicies = [
|
||||
// Politique pour permettre la lecture à tous les utilisateurs connectés
|
||||
`CREATE POLICY "admin_users_select_policy" ON admin_users
|
||||
FOR SELECT USING (auth.uid() IS NOT NULL);`,
|
||||
|
||||
// Politique pour permettre l'insertion/mise à jour/suppression aux super admins
|
||||
`CREATE POLICY "admin_users_manage_policy" ON admin_users
|
||||
FOR ALL USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM admin_users
|
||||
WHERE admin_users.id = auth.uid()
|
||||
AND admin_users.role = 'super_admin'
|
||||
)
|
||||
);`,
|
||||
];
|
||||
|
||||
for (const policy of createPolicies) {
|
||||
try {
|
||||
const { error } = await supabaseAdmin.rpc('exec_sql', { sql: policy });
|
||||
if (error) {
|
||||
console.warn('⚠️ Erreur lors de la création de politique:', error.message);
|
||||
} else {
|
||||
console.log('✅ Politique créée');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Erreur lors de la création de politique:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Alternative : utiliser des requêtes directes si exec_sql ne fonctionne pas
|
||||
console.log('🔧 Tentative de correction alternative...');
|
||||
|
||||
try {
|
||||
// Désactiver temporairement RLS pour permettre la correction
|
||||
const { error: disableError } = await supabaseAdmin
|
||||
.from('admin_users')
|
||||
.select('id')
|
||||
.limit(1);
|
||||
|
||||
if (disableError && disableError.message.includes('infinite recursion')) {
|
||||
console.log('🔄 Désactivation temporaire de RLS...');
|
||||
|
||||
// Note: Cette approche nécessite des privilèges élevés
|
||||
// En production, il faudrait utiliser l'interface Supabase ou des migrations
|
||||
console.log('⚠️ Correction manuelle requise via l\'interface Supabase');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Erreur lors du test:', error);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Correction des politiques RLS initiée',
|
||||
note: 'Si le problème persiste, une correction manuelle via l\'interface Supabase peut être nécessaire',
|
||||
nextSteps: [
|
||||
'1. Vérifiez dans l\'interface Supabase > Authentication > Policies',
|
||||
'2. Supprimez les politiques problématiques sur admin_users',
|
||||
'3. Créez des politiques simplifiées',
|
||||
'4. Testez à nouveau le diagnostic RLS'
|
||||
]
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ Erreur lors de la correction RLS:', error);
|
||||
return NextResponse.json(
|
||||
{ error: `Erreur interne: ${error.message}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
83
src/app/api/setup/debug/route.ts
Normal file
83
src/app/api/setup/debug/route.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { supabaseUrl, supabaseServiceKey, adminEmail } = body;
|
||||
|
||||
if (!supabaseUrl || !supabaseServiceKey || !adminEmail) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Paramètres manquants' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const supabaseAdmin = createClient(supabaseUrl, supabaseServiceKey);
|
||||
|
||||
// 1. Vérifier si l'utilisateur existe dans auth.users
|
||||
console.log('Vérification de l\'utilisateur dans auth.users...');
|
||||
const { data: users, error: usersError } = await supabaseAdmin.auth.admin.listUsers();
|
||||
|
||||
if (usersError) {
|
||||
return NextResponse.json(
|
||||
{ error: `Erreur lors de la récupération des utilisateurs: ${usersError.message}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const user = users.users.find(u => u.email === adminEmail);
|
||||
console.log('Utilisateur trouvé dans auth.users:', user ? 'OUI' : 'NON');
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Utilisateur non trouvé dans auth.users' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Vérifier si l'utilisateur est dans admin_users
|
||||
console.log('Vérification de l\'utilisateur dans admin_users...');
|
||||
const { data: adminUser, error: adminError } = await supabaseAdmin
|
||||
.from('admin_users')
|
||||
.select('*')
|
||||
.eq('id', user.id)
|
||||
.single();
|
||||
|
||||
if (adminError) {
|
||||
console.error('Erreur lors de la vérification admin_users:', adminError);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: `Erreur lors de la vérification admin_users: ${adminError.message}`,
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
created_at: user.created_at
|
||||
},
|
||||
inAdminUsers: false
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log('Utilisateur trouvé dans admin_users:', adminUser ? 'OUI' : 'NON');
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
created_at: user.created_at
|
||||
},
|
||||
adminUser: adminUser,
|
||||
inAdminUsers: !!adminUser
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Erreur lors du diagnostic:', error);
|
||||
return NextResponse.json(
|
||||
{ error: `Erreur interne: ${error.message}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
218
src/app/api/setup/finalize/route.ts
Normal file
218
src/app/api/setup/finalize/route.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { supabaseAdmin } from '@/lib/supabase-admin';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
// Validation des données
|
||||
if (!body.supabaseUrl || !body.supabaseAnonKey || !body.supabaseServiceKey ||
|
||||
!body.adminEmail || !body.adminPassword) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Toutes les données sont requises' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log('🚀 Finalisation de la configuration...');
|
||||
|
||||
// 1. Tester la connexion à Supabase
|
||||
const supabaseAdmin = require('@supabase/supabase-js').createClient(
|
||||
body.supabaseUrl,
|
||||
body.supabaseServiceKey
|
||||
);
|
||||
|
||||
console.log('Test de connexion à Supabase...');
|
||||
|
||||
// 2. Nettoyer et recréer la base de données
|
||||
try {
|
||||
console.log('🧹 Nettoyage de la base de données existante...');
|
||||
|
||||
// Supprimer toutes les données existantes
|
||||
const tablesToClean = ['votes', 'participants', 'propositions', 'campaigns', 'settings', 'user_permissions'];
|
||||
|
||||
for (const table of tablesToClean) {
|
||||
try {
|
||||
const { error: deleteError } = await supabaseAdmin
|
||||
.from(table)
|
||||
.delete()
|
||||
.neq('user_id', '00000000-0000-0000-0000-000000000000'); // Supprimer toutes les lignes
|
||||
|
||||
if (deleteError) {
|
||||
console.warn(`⚠️ Impossible de nettoyer la table ${table}:`, deleteError.message);
|
||||
} else {
|
||||
console.log(`✅ Table ${table} nettoyée`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`⚠️ Erreur lors du nettoyage de ${table}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Supprimer les utilisateurs existants (sauf l'utilisateur système)
|
||||
try {
|
||||
const { data: users, error: usersError } = await supabaseAdmin.auth.admin.listUsers();
|
||||
if (!usersError && users.users) {
|
||||
for (const user of users.users) {
|
||||
if (user.email !== 'service_role@supabase.com') {
|
||||
await supabaseAdmin.auth.admin.deleteUser(user.id);
|
||||
console.log(`🗑️ Utilisateur supprimé: ${user.email}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Erreur lors du nettoyage des utilisateurs:', error);
|
||||
}
|
||||
|
||||
console.log('✅ Nettoyage terminé');
|
||||
|
||||
} catch (error: any) {
|
||||
console.warn('⚠️ Erreur lors du nettoyage:', error);
|
||||
// On continue quand même
|
||||
}
|
||||
|
||||
// 3. Vérifier que les tables existent (elles doivent être créées manuellement)
|
||||
try {
|
||||
console.log('🔍 Vérification des tables...');
|
||||
|
||||
// Tester l'accès aux tables principales
|
||||
const testTables = ['user_permissions', 'campaigns', 'propositions', 'participants', 'votes', 'settings'];
|
||||
let tablesExist = true;
|
||||
|
||||
for (const table of testTables) {
|
||||
try {
|
||||
const { error } = await supabaseAdmin
|
||||
.from(table)
|
||||
.select('*')
|
||||
.limit(1);
|
||||
|
||||
if (error && error.message.includes('relation "public.' + table + '" does not exist')) {
|
||||
console.warn(`⚠️ Table ${table} n'existe pas`);
|
||||
tablesExist = false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`⚠️ Erreur lors du test de la table ${table}:`, error);
|
||||
tablesExist = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!tablesExist) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Les tables de base de données n\'existent pas. Veuillez exécuter le script SQL manuellement dans votre projet Supabase avant de continuer.' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log('✅ Toutes les tables existent');
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ Erreur lors de la vérification des tables:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Erreur lors de la vérification des tables. Veuillez exécuter le script SQL manuellement.' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
// 4. Créer l'utilisateur administrateur
|
||||
try {
|
||||
console.log('Création de l\'utilisateur admin:', body.adminEmail);
|
||||
|
||||
const { data: userData, error: userError } = await supabaseAdmin.auth.admin.createUser({
|
||||
email: body.adminEmail,
|
||||
password: body.adminPassword,
|
||||
email_confirm: true
|
||||
});
|
||||
|
||||
if (userError) {
|
||||
throw new Error(`Erreur lors de la création de l'utilisateur: ${userError.message}`);
|
||||
}
|
||||
|
||||
if (!userData.user) {
|
||||
throw new Error('Utilisateur non créé');
|
||||
}
|
||||
|
||||
console.log('Utilisateur créé avec succès, ID:', userData.user.id);
|
||||
|
||||
// 5. Ajouter l'utilisateur comme administrateur dans user_permissions
|
||||
console.log('Ajout de l\'utilisateur à la table user_permissions...');
|
||||
|
||||
const { data: permissionsData, error: permissionsError } = await supabaseAdmin
|
||||
.from('user_permissions')
|
||||
.insert({
|
||||
user_id: userData.user.id,
|
||||
is_admin: true,
|
||||
is_super_admin: true
|
||||
})
|
||||
.select();
|
||||
|
||||
if (permissionsError) {
|
||||
console.error('Erreur lors de l\'ajout à user_permissions:', permissionsError);
|
||||
throw new Error(`Erreur lors de l'ajout des permissions administrateur: ${permissionsError.message}`);
|
||||
}
|
||||
|
||||
console.log('Utilisateur ajouté à user_permissions avec succès:', permissionsData);
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Erreur complète lors de la création de l\'admin:', error);
|
||||
return NextResponse.json(
|
||||
{ error: `Erreur lors de la création de l'administrateur: ${error.message}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
// 6. Créer le fichier .env.local avec les nouvelles variables
|
||||
try {
|
||||
const envContent = `# Configuration Supabase
|
||||
NEXT_PUBLIC_SUPABASE_URL=${body.supabaseUrl}
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=${body.supabaseAnonKey}
|
||||
SUPABASE_SERVICE_ROLE_KEY=${body.supabaseServiceKey}
|
||||
|
||||
# Configuration générée automatiquement par l'assistant de configuration
|
||||
# Date: ${new Date().toISOString()}
|
||||
`;
|
||||
|
||||
const envPath = path.join(process.cwd(), '.env.local');
|
||||
fs.writeFileSync(envPath, envContent);
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Erreur lors de la création du fichier .env.local:', error);
|
||||
return NextResponse.json(
|
||||
{ error: `Erreur lors de la création du fichier de configuration: ${error.message}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
// 7. Ajouter des paramètres par défaut
|
||||
try {
|
||||
const defaultSettings = [
|
||||
{ key: 'randomize_propositions', value: 'false', category: 'display', description: 'Afficher les propositions dans un ordre aléatoire' },
|
||||
{ key: 'propose_page_message', value: 'Partagez votre vision et proposez des projets qui feront la différence dans votre collectif. Votre voix compte pour façonner l\'avenir de votre communauté.', category: 'display', description: 'Message affiché sur la page de dépôt de propositions' },
|
||||
{ key: 'footer_message', value: 'Développé avec ❤️ pour faciliter la démocratie participative - Logiciel libre et open source', category: 'display', description: 'Message affiché en bas de page' },
|
||||
{ key: 'export_anonymization', value: 'full', category: 'export', description: 'Niveau d\'anonymisation des exports' }
|
||||
];
|
||||
|
||||
for (const setting of defaultSettings) {
|
||||
await supabaseAdmin
|
||||
.from('settings')
|
||||
.upsert(setting, { onConflict: 'key' });
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.warn('Warning lors de l\'ajout des paramètres par défaut:', error);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Configuration terminée avec succès',
|
||||
adminEmail: body.adminEmail
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Erreur lors de la finalisation de la configuration:', error);
|
||||
return NextResponse.json(
|
||||
{ error: `Erreur interne: ${error.message}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -47,13 +47,11 @@ export default function PublicProposePage() {
|
||||
const loadCampaign = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [campaigns, messageValue] = await Promise.all([
|
||||
campaignService.getAll(),
|
||||
const [campaignData, messageValue] = await Promise.all([
|
||||
campaignService.getById(campaignId),
|
||||
settingsService.getStringValue('propose_page_message', 'Partagez votre vision et proposez des projets qui feront la différence dans votre collectif. Votre voix compte pour façonner l\'avenir de votre communauté.')
|
||||
]);
|
||||
|
||||
const campaignData = campaigns.find((c: Campaign) => c.id === campaignId);
|
||||
|
||||
if (!campaignData) {
|
||||
setError('Campagne non trouvée');
|
||||
return;
|
||||
|
||||
@@ -126,13 +126,11 @@ export default function PublicVotePage() {
|
||||
throw new Error('Pas de connexion internet. Veuillez vérifier votre connexion réseau.');
|
||||
}
|
||||
|
||||
const [campaigns, participants, propositionsData] = await Promise.all([
|
||||
campaignService.getAll(),
|
||||
const [campaignData, participants, propositionsData] = await Promise.all([
|
||||
campaignService.getById(campaignId),
|
||||
participantService.getByCampaign(campaignId),
|
||||
propositionService.getByCampaign(campaignId)
|
||||
]);
|
||||
|
||||
const campaignData = campaigns.find(c => c.id === campaignId);
|
||||
const participantData = participants.find(p => p.id === participantId);
|
||||
|
||||
if (!campaignData) {
|
||||
|
||||
682
src/app/debug-auth/page.tsx
Normal file
682
src/app/debug-auth/page.tsx
Normal file
@@ -0,0 +1,682 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Loader2, CheckCircle, AlertCircle, User, Database, Shield, LogIn } from 'lucide-react';
|
||||
import { authService } from '@/lib/auth';
|
||||
|
||||
export default function DebugAuthPage() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [results, setResults] = useState<any>(null);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// État pour la connexion
|
||||
const [loginEmail, setLoginEmail] = useState('');
|
||||
const [loginPassword, setLoginPassword] = useState('');
|
||||
const [loginLoading, setLoginLoading] = useState(false);
|
||||
const [loginError, setLoginError] = useState('');
|
||||
const [loginSuccess, setLoginSuccess] = useState(false);
|
||||
|
||||
// État pour la réparation
|
||||
const [fixLoading, setFixLoading] = useState(false);
|
||||
const [fixError, setFixError] = useState('');
|
||||
const [fixSuccess, setFixSuccess] = useState(false);
|
||||
|
||||
// État pour le diagnostic RLS
|
||||
const [rlsLoading, setRlsLoading] = useState(false);
|
||||
const [rlsResults, setRlsResults] = useState<any>(null);
|
||||
const [rlsError, setRlsError] = useState('');
|
||||
|
||||
// État pour la correction RLS
|
||||
const [fixRlsLoading, setFixRlsLoading] = useState(false);
|
||||
const [fixRlsError, setFixRlsError] = useState('');
|
||||
const [fixRlsSuccess, setFixRlsSuccess] = useState(false);
|
||||
|
||||
const runDiagnostic = async () => {
|
||||
if (!email) {
|
||||
setError('Veuillez saisir un email');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError('');
|
||||
setResults(null);
|
||||
|
||||
try {
|
||||
// 1. Diagnostic côté serveur
|
||||
const response = await fetch('/api/debug-auth', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ email }),
|
||||
});
|
||||
|
||||
const serverData = await response.json();
|
||||
|
||||
// 2. Diagnostic côté client
|
||||
const clientData: {
|
||||
currentUser: any;
|
||||
isAdmin: boolean;
|
||||
isSuperAdmin: boolean;
|
||||
currentAdmin: any;
|
||||
} = {
|
||||
currentUser: null,
|
||||
isAdmin: false,
|
||||
isSuperAdmin: false,
|
||||
currentAdmin: null,
|
||||
};
|
||||
|
||||
try {
|
||||
clientData.currentUser = await authService.getCurrentUser();
|
||||
} catch (e) {
|
||||
console.log('Aucun utilisateur connecté côté client');
|
||||
}
|
||||
|
||||
if (clientData.currentUser) {
|
||||
try {
|
||||
clientData.isAdmin = await authService.isAdmin();
|
||||
} catch (e) {
|
||||
console.log('Erreur lors de la vérification admin');
|
||||
}
|
||||
|
||||
try {
|
||||
clientData.isSuperAdmin = await authService.isSuperAdmin();
|
||||
} catch (e) {
|
||||
console.log('Erreur lors de la vérification super admin');
|
||||
}
|
||||
|
||||
try {
|
||||
clientData.currentAdmin = await authService.getCurrentPermissions();
|
||||
} catch (e) {
|
||||
console.log('Erreur lors de la récupération des permissions');
|
||||
}
|
||||
}
|
||||
|
||||
setResults({
|
||||
server: serverData,
|
||||
client: clientData,
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
setError(error.message || 'Erreur lors du diagnostic');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!loginEmail || !loginPassword) {
|
||||
setLoginError('Veuillez saisir email et mot de passe');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoginLoading(true);
|
||||
setLoginError('');
|
||||
setLoginSuccess(false);
|
||||
|
||||
try {
|
||||
await authService.signIn(loginEmail, loginPassword);
|
||||
setLoginSuccess(true);
|
||||
setLoginError('');
|
||||
// Recharger la page pour mettre à jour l'état
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
} catch (error: any) {
|
||||
setLoginError(error.message || 'Erreur de connexion');
|
||||
} finally {
|
||||
setLoginLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFixAdmin = async () => {
|
||||
if (!email) {
|
||||
setFixError('Veuillez saisir un email');
|
||||
return;
|
||||
}
|
||||
|
||||
setFixLoading(true);
|
||||
setFixError('');
|
||||
setFixSuccess(false);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/fix-admin', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ email }),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
setFixSuccess(true);
|
||||
setFixError('');
|
||||
// Recharger la page après un délai
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
} else {
|
||||
setFixError(result.error || 'Erreur lors de la réparation');
|
||||
}
|
||||
} catch (error: any) {
|
||||
setFixError(error.message || 'Erreur lors de la réparation');
|
||||
} finally {
|
||||
setFixLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const runRlsDiagnostic = async () => {
|
||||
if (!email) {
|
||||
setRlsError('Veuillez saisir un email');
|
||||
return;
|
||||
}
|
||||
|
||||
setRlsLoading(true);
|
||||
setRlsError('');
|
||||
setRlsResults(null);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/debug-rls', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ email }),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
setRlsResults(result);
|
||||
setRlsError('');
|
||||
} else {
|
||||
setRlsError(result.error || 'Erreur lors du diagnostic RLS');
|
||||
}
|
||||
} catch (error: any) {
|
||||
setRlsError(error.message || 'Erreur lors du diagnostic RLS');
|
||||
} finally {
|
||||
setRlsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFixRls = async () => {
|
||||
setFixRlsLoading(true);
|
||||
setFixRlsError('');
|
||||
setFixRlsSuccess(false);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/fix-rls', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
setFixRlsSuccess(true);
|
||||
setFixRlsError('');
|
||||
// Recharger la page après un délai
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 3000);
|
||||
} else {
|
||||
setFixRlsError(result.error || 'Erreur lors de la correction RLS');
|
||||
}
|
||||
} catch (error: any) {
|
||||
setFixRlsError(error.message || 'Erreur lors de la correction RLS');
|
||||
} finally {
|
||||
setFixRlsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
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">
|
||||
Diagnostic d'Authentification
|
||||
</h1>
|
||||
<p className="text-slate-600 dark:text-slate-400">
|
||||
Vérifiez l'état de l'authentification et des permissions
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Diagnostic */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Diagnostic</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div>
|
||||
<Label htmlFor="email">Email de l'utilisateur à diagnostiquer</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="admin@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="mt-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
onClick={runDiagnostic}
|
||||
disabled={loading}
|
||||
className="w-full"
|
||||
>
|
||||
{loading && <Loader2 className="h-4 w-4 animate-spin mr-2" />}
|
||||
Diagnostic
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={runRlsDiagnostic}
|
||||
disabled={rlsLoading}
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
>
|
||||
{rlsLoading && <Loader2 className="h-4 w-4 animate-spin mr-2" />}
|
||||
Diagnostic RLS
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Réparation admin */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Shield className="h-5 w-5" />
|
||||
Réparation admin
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="text-sm text-slate-600 dark:text-slate-400">
|
||||
<p>🔧 <strong>Réparation automatique :</strong> Force la réinsertion de l'utilisateur dans admin_users.</p>
|
||||
</div>
|
||||
|
||||
{fixError && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{fixError}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{fixSuccess && (
|
||||
<Alert>
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
<AlertDescription>Réparation réussie ! Redirection...</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={handleFixAdmin}
|
||||
disabled={fixLoading}
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
>
|
||||
{fixLoading && <Loader2 className="h-4 w-4 animate-spin mr-2" />}
|
||||
Réparer les permissions
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Connexion rapide */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<LogIn className="h-5 w-5" />
|
||||
Connexion rapide
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="loginEmail">Email</Label>
|
||||
<Input
|
||||
id="loginEmail"
|
||||
type="email"
|
||||
placeholder="admin@example.com"
|
||||
value={loginEmail}
|
||||
onChange={(e) => setLoginEmail(e.target.value)}
|
||||
className="mt-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="loginPassword">Mot de passe</Label>
|
||||
<Input
|
||||
id="loginPassword"
|
||||
type="password"
|
||||
placeholder="Votre mot de passe"
|
||||
value={loginPassword}
|
||||
onChange={(e) => setLoginPassword(e.target.value)}
|
||||
className="mt-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{loginError && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{loginError}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{loginSuccess && (
|
||||
<Alert>
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
<AlertDescription>Connexion réussie ! Redirection...</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={handleLogin}
|
||||
disabled={loginLoading}
|
||||
className="w-full"
|
||||
>
|
||||
{loginLoading && <Loader2 className="h-4 w-4 animate-spin mr-2" />}
|
||||
Se connecter
|
||||
</Button>
|
||||
|
||||
<div className="text-sm text-slate-600 dark:text-slate-400">
|
||||
<p>💡 <strong>Conseil :</strong> Utilisez les mêmes identifiants que ceux créés lors du setup.</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{(results || rlsResults) && (
|
||||
<div className="mt-8 space-y-6">
|
||||
<h3 className="text-lg font-semibold">Résultats du diagnostic</h3>
|
||||
|
||||
{/* Diagnostic côté serveur */}
|
||||
{results && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Database className="h-5 w-5" />
|
||||
Diagnostic côté serveur
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{results.server.success ? (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<strong>Utilisateur dans auth.users:</strong>
|
||||
<span className="ml-2 text-green-600">✅ OUI</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Utilisateur dans admin_users:</strong>
|
||||
<span className={`ml-2 ${results.server.inAdminUsers ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{results.server.inAdminUsers ? '✅ OUI' : '❌ NON'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<strong>Détails utilisateur:</strong>
|
||||
<pre className="bg-slate-100 dark:bg-slate-800 p-2 rounded mt-2 text-sm">
|
||||
{JSON.stringify(results.server.user, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{results.server.adminUser && (
|
||||
<div>
|
||||
<strong>Détails admin:</strong>
|
||||
<pre className="bg-slate-100 dark:bg-slate-800 p-2 rounded mt-2 text-sm">
|
||||
{JSON.stringify(results.server.adminUser, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<strong>Configuration:</strong>
|
||||
<pre className="bg-slate-100 dark:bg-slate-800 p-2 rounded mt-2 text-sm">
|
||||
{JSON.stringify(results.server.debug, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{results.server.error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Diagnostic côté client */}
|
||||
{results && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<User className="h-5 w-5" />
|
||||
Diagnostic côté client
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<strong>Utilisateur connecté:</strong>
|
||||
<span className={`ml-2 ${results.client.currentUser ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{results.client.currentUser ? '✅ OUI' : '❌ NON'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Est admin:</strong>
|
||||
<span className={`ml-2 ${results.client.isAdmin ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{results.client.isAdmin ? '✅ OUI' : '❌ NON'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Est super admin:</strong>
|
||||
<span className={`ml-2 ${results.client.isSuperAdmin ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{results.client.isSuperAdmin ? '✅ OUI' : '❌ NON'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{results.client.currentUser && (
|
||||
<div>
|
||||
<strong>Utilisateur connecté:</strong>
|
||||
<pre className="bg-slate-100 dark:bg-slate-800 p-2 rounded mt-2 text-sm">
|
||||
{JSON.stringify(results.client.currentUser, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{results.client.currentAdmin && (
|
||||
<div>
|
||||
<strong>Admin connecté:</strong>
|
||||
<pre className="bg-slate-100 dark:bg-slate-800 p-2 rounded mt-2 text-sm">
|
||||
{JSON.stringify(results.client.currentAdmin, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Résultats du diagnostic RLS */}
|
||||
{rlsResults && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Database className="h-5 w-5" />
|
||||
Diagnostic RLS Avancé
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<strong>Accès user_permissions (service):</strong>
|
||||
<span className={`ml-2 ${rlsResults.adminTests.userPermissionsAccess ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{rlsResults.adminTests.userPermissionsAccess ? '✅ OUI' : '❌ NON'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Utilisateur dans user_permissions:</strong>
|
||||
<span className={`ml-2 ${rlsResults.adminTests.userExists ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{rlsResults.adminTests.userExists ? '✅ OUI' : '❌ NON'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Accès user_permissions (client):</strong>
|
||||
<span className={`ml-2 ${rlsResults.clientTests.canAccessUserPermissions ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{rlsResults.clientTests.canAccessUserPermissions ? '✅ OUI' : '❌ NON'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Sélection utilisateur spécifique:</strong>
|
||||
<span className={`ml-2 ${rlsResults.clientTests.canSelectSpecificUser ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{rlsResults.clientTests.canSelectSpecificUser ? '✅ OUI' : '❌ NON'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{rlsResults.clientTests.rlsError && (
|
||||
<div>
|
||||
<strong>Erreur RLS:</strong>
|
||||
<pre className="bg-red-50 dark:bg-red-900/20 p-2 rounded mt-2 text-sm text-red-800 dark:text-red-200">
|
||||
{rlsResults.clientTests.rlsError}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<strong>Détails admin (service):</strong>
|
||||
<pre className="bg-slate-100 dark:bg-slate-800 p-2 rounded mt-2 text-sm">
|
||||
{JSON.stringify(rlsResults.adminTests, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<strong>Tests client:</strong>
|
||||
<pre className="bg-slate-100 dark:bg-slate-800 p-2 rounded mt-2 text-sm">
|
||||
{JSON.stringify(rlsResults.clientTests, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{/* Bouton de correction RLS */}
|
||||
{rlsResults.clientTests.rlsError && rlsResults.clientTests.rlsError.includes('infinite recursion') && (
|
||||
<div className="mt-4 p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg">
|
||||
<h4 className="font-semibold text-yellow-800 dark:text-yellow-200 mb-2">
|
||||
🔧 Correction automatique disponible
|
||||
</h4>
|
||||
<p className="text-sm text-yellow-700 dark:text-yellow-300 mb-3">
|
||||
Les politiques RLS causent une récursion infinie. Cliquez ci-dessous pour tenter une correction automatique.
|
||||
</p>
|
||||
|
||||
{fixRlsError && (
|
||||
<Alert variant="destructive" className="mb-3">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{fixRlsError}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{fixRlsSuccess && (
|
||||
<Alert className="mb-3">
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
<AlertDescription>Correction RLS réussie ! Redirection...</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={handleFixRls}
|
||||
disabled={fixRlsLoading}
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
>
|
||||
{fixRlsLoading && <Loader2 className="h-4 w-4 animate-spin mr-2" />}
|
||||
Corriger les politiques RLS
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Recommandations */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Shield className="h-5 w-5" />
|
||||
Recommandations
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{rlsResults && !rlsResults.clientTests.canAccessUserPermissions ? (
|
||||
<Alert>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<strong>Problème RLS identifié :</strong> Les politiques RLS empêchent l'accès à admin_users côté client.
|
||||
<br />
|
||||
<strong>Solution :</strong> Les politiques RLS sont trop restrictives. Il faut les ajuster pour permettre l'accès aux admins connectés.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : results && !results.server.inUserPermissions ? (
|
||||
<Alert>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<strong>Problème identifié :</strong> L'utilisateur existe dans auth.users mais pas dans user_permissions.
|
||||
<br />
|
||||
<strong>Solution :</strong> Relancez l'assistant de configuration pour ajouter l'utilisateur à la table user_permissions.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : results && !results.client.currentUser ? (
|
||||
<Alert>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<strong>Problème identifié :</strong> Aucun utilisateur connecté côté client.
|
||||
<br />
|
||||
<strong>Solution :</strong> Utilisez le formulaire de connexion ci-dessus ou allez sur <code>/admin</code> pour vous connecter.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : results && !results.client.isAdmin ? (
|
||||
<Alert>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<strong>Problème identifié :</strong> L'utilisateur est connecté mais n'a pas les permissions admin.
|
||||
<br />
|
||||
<strong>Solution :</strong> Vérifiez que l'utilisateur est bien dans la table user_permissions avec les bonnes permissions.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<Alert>
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<strong>Tout semble correct !</strong> L'utilisateur est connecté et a les permissions admin.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -6,6 +10,42 @@ import { PROJECT_CONFIG } from '@/lib/project.config';
|
||||
import Footer from '@/components/Footer';
|
||||
|
||||
export default function HomePage() {
|
||||
const router = useRouter();
|
||||
const [isChecking, setIsChecking] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
checkSetupStatus();
|
||||
}, []);
|
||||
|
||||
const checkSetupStatus = async () => {
|
||||
try {
|
||||
// Vérifier si Supabase est configuré
|
||||
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 n'est pas configuré, rediriger vers la page de setup
|
||||
router.push('/setup');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsChecking(false);
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la vérification de la configuration:', error);
|
||||
setIsChecking(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isChecking) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-slate-900 dark:border-slate-100 mx-auto mb-4"></div>
|
||||
<p className="text-slate-600 dark:text-slate-300">Vérification de la configuration...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800">
|
||||
<div className="container mx-auto px-4 py-16">
|
||||
|
||||
533
src/app/setup/page.tsx
Normal file
533
src/app/setup/page.tsx
Normal file
@@ -0,0 +1,533 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user