Some checks failed
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Failing after 14s
229 lines
10 KiB
TypeScript
229 lines
10 KiB
TypeScript
import React, { useState, useEffect, useCallback } from 'react';
|
|
import { supabase, invokeSystemCheckFunction, runDatabaseSelfTest } from '../services/supabaseClient';
|
|
import { ShieldCheckIcon } from './icons/ShieldCheckIcon';
|
|
import { LoadingSpinner } from './LoadingSpinner';
|
|
import { CheckCircleIcon } from './icons/CheckCircleIcon';
|
|
import { XCircleIcon } from './icons/XCircleIcon';
|
|
import { DatabaseSeeder } from './DatabaseSeeder';
|
|
|
|
type TestStatus = 'idle' | 'running' | 'pass' | 'fail';
|
|
|
|
// Using an enum for check IDs improves type safety and autocompletion.
|
|
enum CheckID {
|
|
GEMINI = 'gemini',
|
|
SCHEMA = 'schema',
|
|
RLS = 'rls',
|
|
TRIGGER = 'trigger',
|
|
STORAGE = 'storage',
|
|
FUNCTIONS = 'functions',
|
|
PERMISSIONS = 'permissions',
|
|
SEED = 'seed',
|
|
}
|
|
|
|
interface Check {
|
|
id: CheckID;
|
|
name: string;
|
|
status: TestStatus;
|
|
message: string;
|
|
}
|
|
|
|
const initialChecks: Check[] = [
|
|
{ id: CheckID.GEMINI, name: 'Gemini API Key', status: 'idle', message: 'Verifies the VITE_API_KEY is set.' },
|
|
{ id: CheckID.SCHEMA, name: 'Database Schema', status: 'idle', message: 'Verifies required tables exist.' },
|
|
{ id: CheckID.RLS, name: 'RLS Policies', status: 'idle', message: 'Verifies key security policies are active.' },
|
|
{ id: CheckID.TRIGGER, name: 'User Creation Trigger', status: 'idle', message: 'Checks function security configuration.' },
|
|
{ id: CheckID.STORAGE, name: 'Storage Bucket', status: 'idle', message: "Checks 'flyers' bucket exists and is public." },
|
|
{ id: CheckID.FUNCTIONS, name: 'Edge Functions', status: 'idle', message: "Verifies 'delete-user' and 'seed-database' are deployed." },
|
|
{ id: CheckID.PERMISSIONS, name: 'Client Permissions', status: 'idle', message: 'Verifies anon key can perform basic CRUD.' },
|
|
{ id: CheckID.SEED, name: 'Seeded Users', status: 'idle', message: 'Verifies default development users exist.' },
|
|
];
|
|
|
|
interface SystemCheckProps {
|
|
onReady?: () => void;
|
|
}
|
|
|
|
export const SystemCheck: React.FC<SystemCheckProps> = ({ onReady }) => {
|
|
const [checks, setChecks] = useState<Check[]>(initialChecks);
|
|
const [isRunning, setIsRunning] = useState(false);
|
|
const [hasRunAutoTest, setHasRunAutoTest] = useState(false);
|
|
const [showSeeder, setShowSeeder] = useState(false);
|
|
|
|
const updateCheckStatus = useCallback((id: CheckID, status: TestStatus, message: string) => {
|
|
setChecks(prev => prev.map(c => c.id === id ? { ...c, status, message } : c));
|
|
}, []);
|
|
|
|
// Helper to centralize error message parsing.
|
|
const getErrorMessage = (error: unknown): string => {
|
|
return error instanceof Error ? error.message : String(error);
|
|
};
|
|
|
|
const checkApiKey = useCallback(() => {
|
|
if (import.meta.env.VITE_API_KEY) {
|
|
updateCheckStatus(CheckID.GEMINI, 'pass', 'VITE_API_KEY is present.');
|
|
return true;
|
|
} else {
|
|
updateCheckStatus(CheckID.GEMINI, 'fail', 'VITE_API_KEY is missing from your environment variables.');
|
|
return false;
|
|
}
|
|
}, [updateCheckStatus]);
|
|
|
|
const checkBackendSetup = useCallback(async () => {
|
|
try {
|
|
const results = await invokeSystemCheckFunction();
|
|
if (typeof results === 'object' && results !== null) {
|
|
let backendChecksPassed = true;
|
|
for (const key in results) {
|
|
if (Object.prototype.hasOwnProperty.call(results, key)) {
|
|
const { pass, message } = (results as Record<string, { pass: boolean; message: string; }>)[key];
|
|
updateCheckStatus(key as CheckID, pass ? 'pass' : 'fail', message);
|
|
if (!pass) {
|
|
backendChecksPassed = false;
|
|
}
|
|
}
|
|
}
|
|
return backendChecksPassed;
|
|
} else {
|
|
throw new Error("System check function returned invalid data format.");
|
|
}
|
|
} catch (e) {
|
|
const failedCheckIds = [CheckID.SCHEMA, CheckID.RLS, CheckID.TRIGGER, CheckID.STORAGE];
|
|
const errorMessage = getErrorMessage(e);
|
|
failedCheckIds.forEach(id => updateCheckStatus(id, 'fail', errorMessage));
|
|
return false;
|
|
}
|
|
}, [updateCheckStatus]);
|
|
|
|
const checkFunctionDeployments = useCallback(async () => {
|
|
try {
|
|
const { error: seedError } = await supabase.functions.invoke('seed-database', {body: {}});
|
|
if (seedError && seedError.message.includes('Not found')) throw new Error("'seed-database' function not found.");
|
|
|
|
const { error: deleteError } = await supabase.functions.invoke('delete-user', {body: {}});
|
|
if (deleteError && deleteError.message.includes('Not found')) throw new Error("'delete-user' function not found.");
|
|
|
|
updateCheckStatus(CheckID.FUNCTIONS, 'pass', 'All required Edge Functions are deployed.');
|
|
return true;
|
|
} catch (e) {
|
|
const errorMessage = getErrorMessage(e);
|
|
updateCheckStatus(CheckID.FUNCTIONS, 'fail', `${errorMessage} Please deploy it via the Supabase CLI.`);
|
|
return false;
|
|
}
|
|
}, [updateCheckStatus]);
|
|
|
|
const checkClientPermissions = useCallback(async () => {
|
|
try {
|
|
const { success, error } = await runDatabaseSelfTest();
|
|
if (!success) throw new Error(error || 'Client-side CRUD test failed.');
|
|
updateCheckStatus(CheckID.PERMISSIONS, 'pass', 'Anon key has correct table permissions.');
|
|
return true;
|
|
} catch (e) {
|
|
updateCheckStatus(CheckID.PERMISSIONS, 'fail', getErrorMessage(e));
|
|
return false;
|
|
}
|
|
}, [updateCheckStatus]);
|
|
|
|
const checkSeededUsers = useCallback(async () => {
|
|
try {
|
|
const { error } = await supabase.auth.signInWithPassword({
|
|
email: 'admin@example.com',
|
|
password: 'password123',
|
|
});
|
|
if (error) throw error;
|
|
await supabase.auth.signOut();
|
|
updateCheckStatus(CheckID.SEED, 'pass', 'Default admin user login verified.');
|
|
return true;
|
|
} catch (e) {
|
|
const errorMessage = getErrorMessage(e);
|
|
const message = errorMessage.includes('Invalid login credentials')
|
|
? "Invalid login credentials. The seeded users are missing from your database."
|
|
: `Failed: ${errorMessage}`;
|
|
updateCheckStatus(CheckID.SEED, 'fail', message);
|
|
setShowSeeder(true);
|
|
return false;
|
|
}
|
|
}, [updateCheckStatus]);
|
|
|
|
const runChecks = useCallback(async () => {
|
|
setIsRunning(true);
|
|
setShowSeeder(false);
|
|
setChecks(prev => prev.map(c => ({ ...c, status: 'running', message: 'Checking...' })));
|
|
|
|
if (!checkApiKey()) { setIsRunning(false); return; }
|
|
if (!await checkBackendSetup()) { setIsRunning(false); return; }
|
|
if (!await checkFunctionDeployments()) { setIsRunning(false); return; }
|
|
if (!await checkClientPermissions()) { setIsRunning(false); return; }
|
|
const allPassed = await checkSeededUsers();
|
|
|
|
setIsRunning(false);
|
|
if (allPassed) {
|
|
onReady?.();
|
|
}
|
|
}, [onReady, checkApiKey, checkBackendSetup, checkFunctionDeployments, checkClientPermissions, checkSeededUsers]);
|
|
|
|
useEffect(() => {
|
|
if (supabase && !hasRunAutoTest) {
|
|
setHasRunAutoTest(true);
|
|
runChecks();
|
|
}
|
|
}, [supabase, hasRunAutoTest, runChecks]);
|
|
|
|
const getStatusIndicator = (status: TestStatus) => {
|
|
switch (status) {
|
|
case 'running': return <div className="w-5 h-5 text-blue-500"><LoadingSpinner /></div>;
|
|
case 'pass': return <CheckCircleIcon className="w-5 h-5 text-green-500" />;
|
|
case 'fail': return <XCircleIcon className="w-5 h-5 text-red-500" />;
|
|
case 'idle': return <div className="w-5 h-5 rounded-full border-2 border-gray-400 dark:border-gray-600"></div>;
|
|
default: return null;
|
|
}
|
|
};
|
|
|
|
if (!supabase) return null;
|
|
|
|
return (
|
|
<div className="bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
|
<h3 className="text-lg font-bold text-gray-800 dark:text-white flex items-center mb-3">
|
|
<ShieldCheckIcon className="w-6 h-6 mr-2 text-brand-primary" />
|
|
System Check
|
|
</h3>
|
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
|
This checklist verifies your Supabase setup against the README instructions.
|
|
</p>
|
|
|
|
<ul className="space-y-3 mb-4">
|
|
{checks.map(check => (
|
|
<li key={check.id} className="flex items-start space-x-3">
|
|
<div className="flex-shrink-0 pt-0.5">{getStatusIndicator(check.status)}</div>
|
|
<div>
|
|
<p className="text-sm font-semibold text-gray-800 dark:text-gray-200">{check.name}</p>
|
|
<p className={`text-xs whitespace-pre-wrap ${check.status === 'fail' ? 'text-red-600 dark:text-red-400' : 'text-gray-500 dark:text-gray-400'}`}>
|
|
{check.message}
|
|
</p>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
|
|
{showSeeder && (
|
|
<div className="my-4">
|
|
<DatabaseSeeder onSuccess={runChecks} />
|
|
</div>
|
|
)}
|
|
|
|
<button
|
|
onClick={runChecks}
|
|
disabled={isRunning}
|
|
className="w-full bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-wait text-gray-800 dark:text-white font-bold py-2 px-4 rounded-lg transition-colors duration-300 flex items-center justify-center"
|
|
>
|
|
{isRunning ? (
|
|
<>
|
|
<div className="w-5 h-5 mr-2"><LoadingSpinner /></div>
|
|
Running Checks...
|
|
</>
|
|
) : (
|
|
'Re-run Checks'
|
|
)}
|
|
</button>
|
|
</div>
|
|
);
|
|
};
|