cleanup - froentend pages removed
This commit is contained in:
@@ -1,25 +0,0 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { useAuthStore } from "../../store/authStore";
|
||||
|
||||
interface AdminGuardProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* AdminGuard - restricts access to system account (aws-admin/default) or developer
|
||||
*/
|
||||
export default function AdminGuard({ children }: AdminGuardProps) {
|
||||
const { user } = useAuthStore();
|
||||
const role = user?.role;
|
||||
const accountSlug = user?.account?.slug;
|
||||
const isSystemAccount = accountSlug === 'aws-admin' || accountSlug === 'default-account' || accountSlug === 'default';
|
||||
const allowed = role === 'developer' || isSystemAccount;
|
||||
|
||||
if (!allowed) {
|
||||
return <Navigate to="/" replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
31
frontend/src/components/auth/AwsAdminGuard.tsx
Normal file
31
frontend/src/components/auth/AwsAdminGuard.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
|
||||
interface AwsAdminGuardProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Route guard that only allows access to users of the aws-admin account
|
||||
* Used for the single remaining admin dashboard page
|
||||
*/
|
||||
export const AwsAdminGuard: React.FC<AwsAdminGuardProps> = ({ children }) => {
|
||||
const { user, loading } = useAuthStore();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user belongs to aws-admin account
|
||||
const isAwsAdmin = user?.account?.slug === 'aws-admin';
|
||||
|
||||
if (!isAwsAdmin) {
|
||||
return <Navigate to="/dashboard" replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
@@ -124,15 +124,12 @@ export default function ProtectedRoute({ children }: ProtectedRouteProps) {
|
||||
const accountStatus = user?.account?.status;
|
||||
const accountInactive = accountStatus && ['suspended', 'cancelled'].includes(accountStatus);
|
||||
const pendingPayment = accountStatus === 'pending_payment';
|
||||
const isPrivileged = user?.role === 'developer' || user?.is_superuser;
|
||||
|
||||
if (!isPrivileged) {
|
||||
if (pendingPayment && !isPlanAllowedPath) {
|
||||
return <Navigate to="/account/plans" state={{ from: location }} replace />;
|
||||
}
|
||||
if (accountInactive && !isPlanAllowedPath) {
|
||||
return <Navigate to="/account/plans" state={{ from: location }} replace />;
|
||||
}
|
||||
if (pendingPayment && !isPlanAllowedPath) {
|
||||
return <Navigate to="/account/plans" state={{ from: location }} replace />;
|
||||
}
|
||||
if (accountInactive && !isPlanAllowedPath) {
|
||||
return <Navigate to="/account/plans" state={{ from: location }} replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
|
||||
@@ -1,412 +0,0 @@
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { API_BASE_URL } from "../../services/api";
|
||||
import { useAuthStore } from "../../store/authStore";
|
||||
|
||||
interface GroupStatus {
|
||||
name: string;
|
||||
abbreviation: string;
|
||||
healthy: number;
|
||||
total: number;
|
||||
isHealthy: boolean;
|
||||
}
|
||||
|
||||
const endpointGroups = [
|
||||
{
|
||||
name: "Core Health & Auth",
|
||||
abbreviation: "CO",
|
||||
endpoints: [
|
||||
{ path: "/v1/system/status/", method: "GET" },
|
||||
{ path: "/v1/auth/login/", method: "POST" },
|
||||
{ path: "/v1/auth/me/", method: "GET" },
|
||||
{ path: "/v1/auth/register/", method: "POST" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Auth & User Management",
|
||||
abbreviation: "AU",
|
||||
endpoints: [
|
||||
{ path: "/v1/auth/users/", method: "GET" },
|
||||
{ path: "/v1/auth/accounts/", method: "GET" },
|
||||
{ path: "/v1/auth/sites/", method: "GET" },
|
||||
{ path: "/v1/auth/sectors/", method: "GET" },
|
||||
{ path: "/v1/auth/plans/", method: "GET" },
|
||||
{ path: "/v1/auth/industries/", method: "GET" },
|
||||
{ path: "/v1/auth/seed-keywords/", method: "GET" },
|
||||
{ path: "/v1/auth/site-access/", method: "GET" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Planner Module",
|
||||
abbreviation: "PM",
|
||||
endpoints: [
|
||||
{ path: "/v1/planner/keywords/", method: "GET" },
|
||||
{ path: "/v1/planner/keywords/auto_cluster/", method: "POST" },
|
||||
{ path: "/v1/planner/keywords/bulk_delete/", method: "POST" },
|
||||
{ path: "/v1/planner/clusters/", method: "GET" },
|
||||
{ path: "/v1/planner/clusters/auto_generate_ideas/", method: "POST" },
|
||||
{ path: "/v1/planner/ideas/", method: "GET" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Writer Module",
|
||||
abbreviation: "WM",
|
||||
endpoints: [
|
||||
{ path: "/v1/writer/tasks/", method: "GET" },
|
||||
{ path: "/v1/writer/tasks/auto_generate_content/", method: "POST" },
|
||||
{ path: "/v1/writer/tasks/bulk_update/", method: "POST" },
|
||||
{ path: "/v1/writer/content/", method: "GET" },
|
||||
{ path: "/v1/writer/content/generate_image_prompts/", method: "POST" },
|
||||
{ path: "/v1/writer/images/", method: "GET" },
|
||||
{ path: "/v1/writer/images/generate_images/", method: "POST" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "CRUD Operations - Planner",
|
||||
abbreviation: "PC",
|
||||
endpoints: [
|
||||
{ path: "/v1/planner/keywords/", method: "GET" },
|
||||
{ path: "/v1/planner/keywords/", method: "POST" },
|
||||
{ path: "/v1/planner/keywords/1/", method: "GET" },
|
||||
{ path: "/v1/planner/keywords/1/", method: "PUT" },
|
||||
{ path: "/v1/planner/keywords/1/", method: "DELETE" },
|
||||
{ path: "/v1/planner/clusters/", method: "GET" },
|
||||
{ path: "/v1/planner/clusters/", method: "POST" },
|
||||
{ path: "/v1/planner/clusters/1/", method: "GET" },
|
||||
{ path: "/v1/planner/clusters/1/", method: "PUT" },
|
||||
{ path: "/v1/planner/clusters/1/", method: "DELETE" },
|
||||
{ path: "/v1/planner/ideas/", method: "GET" },
|
||||
{ path: "/v1/planner/ideas/", method: "POST" },
|
||||
{ path: "/v1/planner/ideas/1/", method: "GET" },
|
||||
{ path: "/v1/planner/ideas/1/", method: "PUT" },
|
||||
{ path: "/v1/planner/ideas/1/", method: "DELETE" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "CRUD Operations - Writer",
|
||||
abbreviation: "WC",
|
||||
endpoints: [
|
||||
{ path: "/v1/writer/tasks/", method: "GET" },
|
||||
{ path: "/v1/writer/tasks/", method: "POST" },
|
||||
{ path: "/v1/writer/tasks/1/", method: "GET" },
|
||||
{ path: "/v1/writer/tasks/1/", method: "PUT" },
|
||||
{ path: "/v1/writer/tasks/1/", method: "DELETE" },
|
||||
{ path: "/v1/writer/content/", method: "GET" },
|
||||
{ path: "/v1/writer/content/", method: "POST" },
|
||||
{ path: "/v1/writer/content/1/", method: "GET" },
|
||||
{ path: "/v1/writer/content/1/", method: "PUT" },
|
||||
{ path: "/v1/writer/content/1/", method: "DELETE" },
|
||||
{ path: "/v1/writer/images/", method: "GET" },
|
||||
{ path: "/v1/writer/images/", method: "POST" },
|
||||
{ path: "/v1/writer/images/1/", method: "GET" },
|
||||
{ path: "/v1/writer/images/1/", method: "PUT" },
|
||||
{ path: "/v1/writer/images/1/", method: "DELETE" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "System & Billing",
|
||||
abbreviation: "SY",
|
||||
endpoints: [
|
||||
{ path: "/v1/system/prompts/", method: "GET" },
|
||||
{ path: "/v1/system/author-profiles/", method: "GET" },
|
||||
{ path: "/v1/system/strategies/", method: "GET" },
|
||||
{ path: "/v1/system/settings/integrations/openai/test/", method: "POST" },
|
||||
{ path: "/v1/system/settings/account/", method: "GET" },
|
||||
{ path: "/v1/billing/credits/balance/", method: "GET" },
|
||||
{ path: "/v1/billing/credits/usage/", method: "GET" },
|
||||
{ path: "/v1/billing/credits/usage/summary/", method: "GET" },
|
||||
{ path: "/v1/billing/credits/transactions/", method: "GET" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default function ApiStatusIndicator() {
|
||||
const { user } = useAuthStore();
|
||||
const location = useLocation();
|
||||
const [groupStatuses, setGroupStatuses] = useState<GroupStatus[]>([]);
|
||||
const [isChecking, setIsChecking] = useState(false);
|
||||
const intervalRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
// Only show and run for aws-admin accounts
|
||||
const isAwsAdmin = user?.account?.slug === 'aws-admin';
|
||||
|
||||
// Only run API checks on API monitor page to avoid console errors on other pages
|
||||
const isApiMonitorPage = location.pathname === '/settings/api-monitor';
|
||||
|
||||
const checkEndpoint = useCallback(async (path: string, method: string): Promise<'healthy' | 'warning' | 'error'> => {
|
||||
try {
|
||||
const token = localStorage.getItem('auth_token') ||
|
||||
(() => {
|
||||
try {
|
||||
const authStorage = localStorage.getItem('auth-storage');
|
||||
if (authStorage) {
|
||||
const parsed = JSON.parse(authStorage);
|
||||
return parsed?.state?.token || '';
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore parsing errors
|
||||
}
|
||||
return '';
|
||||
})();
|
||||
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const isExpensiveAIEndpoint =
|
||||
path.includes('/auto_generate_content') ||
|
||||
path.includes('/auto_cluster') ||
|
||||
path.includes('/auto_generate_ideas') ||
|
||||
path.includes('/generate_image_prompts') ||
|
||||
path.includes('/generate_images');
|
||||
|
||||
let actualMethod = method;
|
||||
let fetchOptions: RequestInit = {
|
||||
method: actualMethod,
|
||||
headers,
|
||||
credentials: 'include',
|
||||
};
|
||||
|
||||
if (method === 'POST' && isExpensiveAIEndpoint) {
|
||||
actualMethod = 'OPTIONS';
|
||||
fetchOptions.method = 'OPTIONS';
|
||||
delete (fetchOptions as any).body;
|
||||
} else if (method === 'POST') {
|
||||
let body: any = {};
|
||||
if (path.includes('/test/')) {
|
||||
body = {};
|
||||
} else if (path.includes('/login/')) {
|
||||
body = { username: 'test', password: 'test' };
|
||||
} else if (path.includes('/register/')) {
|
||||
body = { username: 'test', email: 'test@test.com', password: 'test' };
|
||||
} else if (path.includes('/bulk_delete/')) {
|
||||
body = { ids: [] }; // Empty array to trigger validation error
|
||||
} else if (path.includes('/bulk_update/')) {
|
||||
body = { ids: [] }; // Empty array to trigger validation error
|
||||
}
|
||||
fetchOptions.body = JSON.stringify(body);
|
||||
} else if (method === 'PUT' || method === 'DELETE') {
|
||||
// For PUT/DELETE, we need to send a body for PUT or handle DELETE
|
||||
if (method === 'PUT') {
|
||||
fetchOptions.body = JSON.stringify({}); // Empty object to trigger validation
|
||||
}
|
||||
}
|
||||
|
||||
// Suppress console errors for expected 400 responses (validation errors from test data)
|
||||
// These are expected and indicate the endpoint is working
|
||||
const isExpected400 = method === 'POST' && (
|
||||
path.includes('/login/') ||
|
||||
path.includes('/register/') ||
|
||||
path.includes('/bulk_') ||
|
||||
path.includes('/test/')
|
||||
);
|
||||
|
||||
// Use a silent fetch that won't log to console for expected errors
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(`${API_BASE_URL}${path}`, fetchOptions);
|
||||
} catch (fetchError) {
|
||||
// Network errors are real errors
|
||||
return 'error';
|
||||
}
|
||||
|
||||
if (actualMethod === 'OPTIONS') {
|
||||
if (response.status === 200) {
|
||||
return 'healthy';
|
||||
} else if (response.status === 404) {
|
||||
return 'error';
|
||||
} else if (response.status >= 500) {
|
||||
return 'error';
|
||||
}
|
||||
return 'warning';
|
||||
} else if (method === 'GET') {
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
return 'healthy';
|
||||
} else if (response.status === 401 || response.status === 403) {
|
||||
return 'warning';
|
||||
} else if (response.status === 404) {
|
||||
// For GET requests to specific resource IDs (e.g., /v1/planner/keywords/1/),
|
||||
// 404 is expected and healthy (resource doesn't exist, but endpoint works correctly)
|
||||
// For other GET requests (like list endpoints), 404 means endpoint doesn't exist
|
||||
const isResourceByIdRequest = /\/\d+\/?$/.test(path); // Path ends with /number/ or /number
|
||||
if (isResourceByIdRequest) {
|
||||
return 'healthy'; // GET to specific ID returning 404 is healthy (endpoint exists, resource doesn't)
|
||||
}
|
||||
return 'error'; // Endpoint doesn't exist
|
||||
} else if (response.status >= 500) {
|
||||
return 'error';
|
||||
}
|
||||
return 'warning';
|
||||
} else if (method === 'POST') {
|
||||
// Suppress console errors for expected 400 responses (validation errors from test data)
|
||||
// CRUD POST endpoints (like /v1/planner/keywords/, /v1/writer/tasks/) return 400 for empty/invalid test data
|
||||
const isExpected400 = path.includes('/login/') ||
|
||||
path.includes('/register/') ||
|
||||
path.includes('/bulk_') ||
|
||||
path.includes('/test/') ||
|
||||
// CRUD CREATE endpoints - POST to list endpoints (no ID in path, ends with / or exact match)
|
||||
/\/v1\/(planner|writer)\/(keywords|clusters|ideas|tasks|content|images)\/?$/.test(path);
|
||||
|
||||
if (response.status === 400) {
|
||||
// 400 is expected for test requests - endpoint is working
|
||||
// Don't log warnings for expected 400s - they're normal validation errors
|
||||
return 'healthy';
|
||||
} else if (response.status >= 200 && response.status < 300) {
|
||||
return 'healthy';
|
||||
} else if (response.status === 401 || response.status === 403) {
|
||||
return 'warning';
|
||||
} else if (response.status === 404) {
|
||||
return 'error';
|
||||
} else if (response.status >= 500) {
|
||||
return 'error';
|
||||
}
|
||||
return 'warning';
|
||||
} else if (method === 'PUT' || method === 'DELETE') {
|
||||
// UPDATE/DELETE operations
|
||||
if (response.status === 400 || response.status === 404) {
|
||||
// 400/404 expected for test requests - endpoint is working
|
||||
return 'healthy';
|
||||
} else if (response.status === 204 || (response.status >= 200 && response.status < 300)) {
|
||||
return 'healthy';
|
||||
} else if (response.status === 401 || response.status === 403) {
|
||||
return 'warning';
|
||||
} else if (response.status >= 500) {
|
||||
return 'error';
|
||||
}
|
||||
return 'warning';
|
||||
}
|
||||
|
||||
return 'warning';
|
||||
} catch (err) {
|
||||
return 'error';
|
||||
}
|
||||
}, []);
|
||||
|
||||
const checkAllGroups = useCallback(async () => {
|
||||
setIsChecking(true);
|
||||
|
||||
const statusPromises = endpointGroups.map(async (group) => {
|
||||
const endpointChecks = group.endpoints.map(ep => checkEndpoint(ep.path, ep.method));
|
||||
const results = await Promise.all(endpointChecks);
|
||||
|
||||
const healthy = results.filter(s => s === 'healthy').length;
|
||||
const total = results.length;
|
||||
const isHealthy = healthy === total;
|
||||
|
||||
return {
|
||||
name: group.name,
|
||||
abbreviation: group.abbreviation,
|
||||
healthy,
|
||||
total,
|
||||
isHealthy,
|
||||
};
|
||||
});
|
||||
|
||||
const statuses = await Promise.all(statusPromises);
|
||||
setGroupStatuses(statuses);
|
||||
setIsChecking(false);
|
||||
}, [checkEndpoint]);
|
||||
|
||||
useEffect(() => {
|
||||
// Only run if aws-admin and on API monitor page
|
||||
if (!isAwsAdmin || !isApiMonitorPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initial check
|
||||
checkAllGroups();
|
||||
|
||||
// Get refresh interval from localStorage (same as API Monitor page)
|
||||
const getRefreshInterval = () => {
|
||||
const saved = localStorage.getItem('api-monitor-refresh-interval');
|
||||
return saved ? parseInt(saved, 10) * 1000 : 30000; // Convert to milliseconds
|
||||
};
|
||||
|
||||
// Setup interval function that reads fresh interval value each time
|
||||
const setupInterval = () => {
|
||||
if (intervalRef.current) {
|
||||
clearTimeout(intervalRef.current);
|
||||
}
|
||||
|
||||
// Use a recursive timeout that reads the interval each time
|
||||
const scheduleNext = () => {
|
||||
const interval = getRefreshInterval();
|
||||
intervalRef.current = setTimeout(() => {
|
||||
checkAllGroups();
|
||||
scheduleNext(); // Schedule next check
|
||||
}, interval);
|
||||
};
|
||||
|
||||
scheduleNext();
|
||||
};
|
||||
|
||||
// Initial interval setup
|
||||
setupInterval();
|
||||
|
||||
// Listen for storage changes (when user changes interval in another tab)
|
||||
const handleStorageChange = (e: StorageEvent) => {
|
||||
if (e.key === 'api-monitor-refresh-interval') {
|
||||
setupInterval();
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for custom event (when user changes interval in same tab)
|
||||
const handleCustomStorageChange = () => {
|
||||
setupInterval();
|
||||
};
|
||||
|
||||
window.addEventListener('storage', handleStorageChange);
|
||||
window.addEventListener('api-monitor-interval-changed', handleCustomStorageChange);
|
||||
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearTimeout(intervalRef.current);
|
||||
}
|
||||
window.removeEventListener('storage', handleStorageChange);
|
||||
window.removeEventListener('api-monitor-interval-changed', handleCustomStorageChange);
|
||||
};
|
||||
}, [checkAllGroups, isAwsAdmin, isApiMonitorPage]);
|
||||
|
||||
const getStatusColor = (isHealthy: boolean) => {
|
||||
if (isHealthy) {
|
||||
return 'bg-green-500 dark:bg-green-400'; // Success color for 100%
|
||||
} else {
|
||||
return 'bg-yellow-500 dark:bg-yellow-400'; // Warning color for < 100%
|
||||
}
|
||||
};
|
||||
|
||||
// Return null if not aws-admin account or not on API monitor page
|
||||
// This check must come AFTER all hooks are called
|
||||
if (!isAwsAdmin || !isApiMonitorPage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (groupStatuses.length === 0 && !isChecking) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-6 px-2">
|
||||
<div className="flex items-center justify-center gap-2 flex-wrap">
|
||||
{groupStatuses.map((group, index) => (
|
||||
<div key={index} className="flex flex-col items-center gap-1">
|
||||
<div
|
||||
className={`w-3 h-3 rounded-full ${getStatusColor(group.isHealthy)} transition-colors duration-300 ${
|
||||
isChecking ? 'opacity-50' : ''
|
||||
}`}
|
||||
title={`${group.name}: ${group.healthy}/${group.total} healthy`}
|
||||
/>
|
||||
<span className="text-[10px] text-gray-500 dark:text-gray-400 font-medium">
|
||||
{group.abbreviation}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user