Fix TypeScript build errors

This commit is contained in:
defiQUG
2026-01-02 20:27:42 -08:00
parent 849e6a8357
commit d4fb8e77cb
295 changed files with 18595 additions and 1391 deletions

View File

@@ -1,50 +1,136 @@
import { lazy, Suspense } from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import { useAuthStore } from './stores/authStore';
import ProtectedRoute from './components/auth/ProtectedRoute';
import ErrorBoundary from './components/shared/ErrorBoundary';
import PageError from './components/shared/PageError';
import LoginPage from './pages/auth/LoginPage';
import LoadingSpinner from './components/shared/LoadingSpinner';
import SkipLink from './components/shared/SkipLink';
// Layout components (loaded immediately as they're always needed)
import DBISLayout from './components/layout/DBISLayout';
import SCBLayout from './components/layout/SCBLayout';
// Auth page (loaded immediately for faster login)
import LoginPage from './pages/auth/LoginPage';
// Lazy load page components for code splitting
// DBIS Admin Pages
import DBISOverviewPage from './pages/dbis/OverviewPage';
import DBISParticipantsPage from './pages/dbis/ParticipantsPage';
import DBISGRUPage from './pages/dbis/GRUPage';
import DBISGASQPSPage from './pages/dbis/GASQPSPage';
import DBISCBDCFXPage from './pages/dbis/CBDCFXPage';
import DBISMetaverseEdgePage from './pages/dbis/MetaverseEdgePage';
import DBISRiskCompliancePage from './pages/dbis/RiskCompliancePage';
const DBISOverviewPage = lazy(() => import('./pages/dbis/OverviewPage'));
const DBISParticipantsPage = lazy(() => import('./pages/dbis/ParticipantsPage'));
const DBISGRUPage = lazy(() => import('./pages/dbis/GRUPage'));
const DBISGASQPSPage = lazy(() => import('./pages/dbis/GASQPSPage'));
const DBISCBDCFXPage = lazy(() => import('./pages/dbis/CBDCFXPage'));
const DBISMetaverseEdgePage = lazy(() => import('./pages/dbis/MetaverseEdgePage'));
const DBISRiskCompliancePage = lazy(() => import('./pages/dbis/RiskCompliancePage'));
// SCB Admin Pages
import SCBOverviewPage from './pages/scb/OverviewPage';
import SCBFIManagementPage from './pages/scb/FIManagementPage';
import SCBCorridorPolicyPage from './pages/scb/CorridorPolicyPage';
const SCBOverviewPage = lazy(() => import('./pages/scb/OverviewPage'));
const SCBFIManagementPage = lazy(() => import('./pages/scb/FIManagementPage'));
const SCBCorridorPolicyPage = lazy(() => import('./pages/scb/CorridorPolicyPage'));
/**
* Lazy-loaded route wrapper with Suspense fallback
*/
const LazyRoute = ({ children }: { children: React.ReactNode }) => (
<Suspense fallback={<LoadingSpinner fullPage />}>{children}</Suspense>
);
function App() {
const { isAuthenticated } = useAuthStore();
return (
<ErrorBoundary>
<SkipLink />
<Routes>
<Route path="/login" element={!isAuthenticated ? <LoginPage /> : <Navigate to="/dbis/overview" replace />} />
<Route element={<ProtectedRoute />}>
<Route path="/dbis/*" element={<DBISLayout />}>
<Route path="overview" element={<DBISOverviewPage />} />
<Route path="participants" element={<DBISParticipantsPage />} />
<Route path="gru" element={<DBISGRUPage />} />
<Route path="gas-qps" element={<DBISGASQPSPage />} />
<Route path="cbdc-fx" element={<DBISCBDCFXPage />} />
<Route path="metaverse-edge" element={<DBISMetaverseEdgePage />} />
<Route path="risk-compliance" element={<DBISRiskCompliancePage />} />
<Route
path="overview"
element={
<LazyRoute>
<DBISOverviewPage />
</LazyRoute>
}
/>
<Route
path="participants"
element={
<LazyRoute>
<DBISParticipantsPage />
</LazyRoute>
}
/>
<Route
path="gru"
element={
<LazyRoute>
<DBISGRUPage />
</LazyRoute>
}
/>
<Route
path="gas-qps"
element={
<LazyRoute>
<DBISGASQPSPage />
</LazyRoute>
}
/>
<Route
path="cbdc-fx"
element={
<LazyRoute>
<DBISCBDCFXPage />
</LazyRoute>
}
/>
<Route
path="metaverse-edge"
element={
<LazyRoute>
<DBISMetaverseEdgePage />
</LazyRoute>
}
/>
<Route
path="risk-compliance"
element={
<LazyRoute>
<DBISRiskCompliancePage />
</LazyRoute>
}
/>
<Route index element={<Navigate to="overview" replace />} />
</Route>
<Route path="/scb/*" element={<SCBLayout />}>
<Route path="overview" element={<SCBOverviewPage />} />
<Route path="fi-management" element={<SCBFIManagementPage />} />
<Route path="corridors" element={<SCBCorridorPolicyPage />} />
<Route
path="overview"
element={
<LazyRoute>
<SCBOverviewPage />
</LazyRoute>
}
/>
<Route
path="fi-management"
element={
<LazyRoute>
<SCBFIManagementPage />
</LazyRoute>
}
/>
<Route
path="corridors"
element={
<LazyRoute>
<SCBCorridorPolicyPage />
</LazyRoute>
}
/>
<Route index element={<Navigate to="overview" replace />} />
</Route>

View File

@@ -43,7 +43,7 @@ export default function DBISLayout() {
/>
<div className="layout__main">
<TopBar />
<main className="layout__content">
<main id="main-content" className="layout__content" role="main">
<Outlet />
</main>
</div>

View File

@@ -37,7 +37,7 @@ export default function SCBLayout() {
/>
<div className="layout__main">
<TopBar />
<main className="layout__content">
<main id="main-content" className="layout__content" role="main">
<Outlet />
</main>
</div>

View File

@@ -10,6 +10,7 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
icon?: ReactNode;
iconPosition?: 'left' | 'right';
fullWidth?: boolean;
'aria-label'?: string;
}
export default function Button({
@@ -37,6 +38,9 @@ export default function Button({
className
)}
disabled={disabled || loading}
aria-label={props['aria-label'] || (typeof children === 'string' ? children : undefined)}
aria-busy={loading}
aria-disabled={disabled || loading}
{...props}
>
{loading ? (

View File

@@ -1,6 +1,7 @@
// Error Boundary Component
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { Component, ErrorInfo, ReactNode } from 'react';
import Button from './Button';
import { errorTracker } from '@/utils/errorTracking';
import './ErrorBoundary.css';
interface Props {
@@ -34,7 +35,12 @@ export default class ErrorBoundary extends Component<Props, State> {
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('ErrorBoundary caught an error:', error, errorInfo);
// Log to error tracking service
errorTracker.captureException(error, {
componentStack: errorInfo.componentStack,
errorBoundary: true,
});
this.setState({
error,
errorInfo,
@@ -43,9 +49,6 @@ export default class ErrorBoundary extends Component<Props, State> {
if (this.props.onError) {
this.props.onError(error, errorInfo);
}
// Log to error reporting service (e.g., Sentry)
// logErrorToService(error, errorInfo);
}
handleReset = () => {
@@ -69,7 +72,7 @@ export default class ErrorBoundary extends Component<Props, State> {
<p className="error-boundary__message">
An unexpected error occurred. Please try refreshing the page or contact support if the problem persists.
</p>
{process.env.NODE_ENV === 'development' && this.state.error && (
{import.meta.env.DEV && this.state.error && (
<details className="error-boundary__details">
<summary>Error Details (Development Only)</summary>
<pre className="error-boundary__stack">

View File

@@ -10,22 +10,38 @@ interface FormInputProps extends InputHTMLAttributes<HTMLInputElement> {
}
const FormInput = forwardRef<HTMLInputElement, FormInputProps>(
({ label, error, helperText, className, ...props }, ref) => {
({ label, error, helperText, className, id, ...props }, ref) => {
const inputId = id || `form-input-${Math.random().toString(36).substring(7)}`;
const errorId = error ? `${inputId}-error` : undefined;
const helperId = helperText && !error ? `${inputId}-helper` : undefined;
return (
<div className="form-input">
{label && (
<label className="form-input__label" htmlFor={props.id}>
<label className="form-input__label" htmlFor={inputId}>
{label}
{props.required && <span className="form-input__required">*</span>}
{props.required && <span className="form-input__required" aria-label="required">*</span>}
</label>
)}
<input
ref={ref}
id={inputId}
className={clsx('form-input__input', { 'form-input__input--error': error }, className)}
aria-invalid={!!error}
aria-describedby={error ? errorId : helperId}
aria-errormessage={errorId}
{...props}
/>
{error && <span className="form-input__error">{error}</span>}
{helperText && !error && <span className="form-input__helper">{helperText}</span>}
{error && (
<span id={errorId} className="form-input__error" role="alert">
{error}
</span>
)}
{helperText && !error && (
<span id={helperId} className="form-input__helper">
{helperText}
</span>
)}
</div>
);
}

View File

@@ -0,0 +1,83 @@
/* Skeleton Loader Styles */
.skeleton {
background: linear-gradient(
90deg,
var(--color-bg-secondary) 0%,
var(--color-border) 50%,
var(--color-bg-secondary) 100%
);
background-size: 200% 100%;
animation: skeleton-loading 1.5s ease-in-out infinite;
}
@keyframes skeleton-loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
/* Table Skeleton */
.skeleton-table {
width: 100%;
}
.skeleton-table__header {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 1rem;
padding: 1rem;
border-bottom: 1px solid var(--color-border);
}
.skeleton-table__row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 1rem;
padding: 1rem;
border-bottom: 1px solid var(--color-border);
}
.skeleton-table__header-cell,
.skeleton-table__cell {
margin: 0;
}
/* Card Skeleton */
.skeleton-card {
padding: 1.5rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-bg-secondary);
}
.skeleton-card__title {
margin-bottom: 1rem;
}
.skeleton-card__content {
margin-bottom: 0.5rem;
}
.skeleton-card__content:last-child {
margin-bottom: 0;
}
/* Metric Card Skeleton */
.skeleton-metric-card {
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.skeleton-metric-card__label {
opacity: 0.7;
}
.skeleton-metric-card__value {
font-weight: 600;
}

View File

@@ -0,0 +1,81 @@
/**
* Skeleton Loader Components
*
* Provides skeleton loading states for better UX while content is loading.
*/
import './Skeleton.css';
interface SkeletonProps {
className?: string;
width?: string | number;
height?: string | number;
circle?: boolean;
rounded?: boolean;
}
/**
* Base skeleton component
*/
export function Skeleton({ className = '', width, height, circle = false, rounded = true }: SkeletonProps) {
const style: React.CSSProperties = {
width: width || '100%',
height: height || '1em',
borderRadius: circle ? '50%' : rounded ? '4px' : '0',
};
return <div className={`skeleton ${className}`} style={style} aria-hidden="true" />;
}
/**
* Table skeleton with rows and columns
*/
export function TableSkeleton({ rows = 5, cols = 4 }: { rows?: number; cols?: number }) {
return (
<div className="skeleton-table" role="status" aria-label="Loading table data">
<div className="skeleton-table__header">
{Array(cols)
.fill(0)
.map((_, i) => (
<Skeleton key={`header-${i}`} height="20px" className="skeleton-table__header-cell" />
))}
</div>
{Array(rows)
.fill(0)
.map((_, rowIndex) => (
<div key={`row-${rowIndex}`} className="skeleton-table__row">
{Array(cols)
.fill(0)
.map((_, colIndex) => (
<Skeleton key={`cell-${rowIndex}-${colIndex}`} height="16px" className="skeleton-table__cell" />
))}
</div>
))}
</div>
);
}
/**
* Card skeleton
*/
export function CardSkeleton() {
return (
<div className="skeleton-card" role="status" aria-label="Loading card">
<Skeleton height="24px" width="60%" className="skeleton-card__title" />
<Skeleton height="16px" width="100%" className="skeleton-card__content" />
<Skeleton height="16px" width="80%" className="skeleton-card__content" />
</div>
);
}
/**
* Metric card skeleton
*/
export function MetricCardSkeleton() {
return (
<div className="skeleton-metric-card" role="status" aria-label="Loading metric">
<Skeleton height="14px" width="40%" className="skeleton-metric-card__label" />
<Skeleton height="32px" width="60%" className="skeleton-metric-card__value" />
</div>
);
}

View File

@@ -0,0 +1,20 @@
/* Skip Link Styles */
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: var(--color-primary);
color: white;
padding: 0.5rem 1rem;
text-decoration: none;
z-index: 1000;
border-radius: 0 0 var(--radius-md) 0;
font-weight: 600;
}
.skip-link:focus {
top: 0;
outline: 2px solid var(--color-primary-dark);
outline-offset: 2px;
}

View File

@@ -0,0 +1,16 @@
/**
* Skip Link Component
*
* Provides a skip navigation link for keyboard users and screen readers.
* Allows users to skip directly to the main content.
*/
import './SkipLink.css';
export default function SkipLink() {
return (
<a href="#main-content" className="skip-link">
Skip to main content
</a>
);
}

View File

@@ -0,0 +1,37 @@
/**
* Environment Configuration
*
* Validates and exports environment variables with type safety.
* Throws errors at startup if required variables are missing or invalid.
*/
import { z } from 'zod';
const envSchema = z.object({
VITE_API_BASE_URL: z.string().url('VITE_API_BASE_URL must be a valid URL'),
VITE_APP_NAME: z.string().min(1, 'VITE_APP_NAME is required'),
VITE_REAL_TIME_UPDATE_INTERVAL: z.coerce
.number()
.positive('VITE_REAL_TIME_UPDATE_INTERVAL must be a positive number')
.default(5000),
});
type Env = z.infer<typeof envSchema>;
let env: Env;
try {
env = envSchema.parse({
VITE_API_BASE_URL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000',
VITE_APP_NAME: import.meta.env.VITE_APP_NAME || 'DBIS Admin Console',
VITE_REAL_TIME_UPDATE_INTERVAL: import.meta.env.VITE_REAL_TIME_UPDATE_INTERVAL || '5000',
});
} catch (error) {
if (error instanceof z.ZodError) {
const errorMessages = error.errors.map((err) => `${err.path.join('.')}: ${err.message}`).join('\n');
throw new Error(`Invalid environment configuration:\n${errorMessages}`);
}
throw error;
}
export { env };
export type { Env };

View File

@@ -0,0 +1,87 @@
/**
* Application Constants
*
* Centralized configuration constants to avoid magic numbers and strings.
* Update these values to change application behavior.
*/
/**
* Refetch intervals for different data types (in milliseconds)
*/
export const REFETCH_INTERVALS = {
/** Dashboard data refresh interval */
DASHBOARD: 10000, // 10 seconds
/** Real-time data refresh interval */
REAL_TIME: 5000, // 5 seconds
/** Background data refresh interval (when tab is hidden) */
BACKGROUND: 30000, // 30 seconds
} as const;
/**
* API request configuration
*/
export const API_CONFIG = {
/** Request timeout in milliseconds */
TIMEOUT: 30000, // 30 seconds
/** Maximum retry attempts for failed requests */
MAX_RETRIES: 3,
/** Retry delay multiplier (exponential backoff) */
RETRY_DELAY_MULTIPLIER: 1000, // 1 second base delay
} as const;
/**
* Pagination defaults
*/
export const PAGINATION = {
/** Default page size */
DEFAULT_PAGE_SIZE: 50,
/** Maximum page size */
MAX_PAGE_SIZE: 100,
/** Page size options */
PAGE_SIZE_OPTIONS: [10, 25, 50, 100] as const,
} as const;
/**
* Debounce delays (in milliseconds)
*/
export const DEBOUNCE_DELAYS = {
/** Search input debounce */
SEARCH: 300,
/** Filter changes debounce */
FILTER: 500,
/** Form input debounce */
FORM_INPUT: 200,
} as const;
/**
* Storage keys
*/
export const STORAGE_KEYS = {
AUTH_TOKEN: 'auth_token',
USER: 'user',
THEME: 'theme',
PREFERENCES: 'preferences',
} as const;
/**
* Error messages
*/
export const ERROR_MESSAGES = {
NETWORK_ERROR: 'Network error. Please check your connection.',
UNAUTHORIZED: 'Session expired. Please login again.',
FORBIDDEN: 'You do not have permission to perform this action.',
NOT_FOUND: 'Resource not found.',
SERVER_ERROR: 'Server error. Please try again later.',
VALIDATION_ERROR: 'Validation error. Please check your input.',
UNEXPECTED_ERROR: 'An unexpected error occurred.',
} as const;
/**
* Success messages
*/
export const SUCCESS_MESSAGES = {
SAVED: 'Changes saved successfully.',
DELETED: 'Item deleted successfully.',
CREATED: 'Item created successfully.',
UPDATED: 'Item updated successfully.',
} as const;

View File

@@ -0,0 +1,36 @@
/**
* useDebouncedValue Hook
*
* Returns a debounced value that updates after the specified delay.
* Useful for search inputs, filters, and other inputs that trigger expensive operations.
*
* @param value - The value to debounce
* @param delay - Delay in milliseconds (default: 300)
* @returns Debounced value
*
* @example
* const [searchTerm, setSearchTerm] = useState('');
* const debouncedSearchTerm = useDebouncedValue(searchTerm, 300);
*
* useEffect(() => {
* // This will only run when debouncedSearchTerm changes (after 300ms delay)
* performSearch(debouncedSearchTerm);
* }, [debouncedSearchTerm]);
*/
import { useState, useEffect } from 'react';
export function useDebouncedValue<T>(value: T, delay: number = 300): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}

View File

@@ -0,0 +1,40 @@
/**
* useOnlineStatus Hook
*
* Tracks the browser's online/offline status.
* Useful for showing offline indicators and handling offline scenarios.
*
* @returns {boolean} True if online, false if offline
*
* @example
* const isOnline = useOnlineStatus();
* if (!isOnline) {
* return <OfflineIndicator />;
* }
*/
import { useState, useEffect } from 'react';
export function useOnlineStatus(): boolean {
const [isOnline, setIsOnline] = useState(() => {
// Initialize with current status
if (typeof navigator !== 'undefined') {
return navigator.onLine;
}
return true; // Default to online if navigator is not available
});
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}

View File

@@ -5,8 +5,22 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Toaster } from 'react-hot-toast';
import App from './App';
import { useAuthStore } from './stores/authStore';
import { env } from './config/env';
import { logger } from './utils/logger';
import { errorTracker } from './utils/errorTracking';
import './index.css';
// Initialize error tracking (ready for Sentry integration)
// Uncomment and configure when ready:
// errorTracker.init(import.meta.env.VITE_SENTRY_DSN, import.meta.env.VITE_SENTRY_ENVIRONMENT);
// Validate environment variables on startup
logger.info('Application starting', {
appName: env.VITE_APP_NAME,
apiUrl: env.VITE_API_BASE_URL,
environment: import.meta.env.MODE,
});
const queryClient = new QueryClient({
defaultOptions: {
queries: {

View File

@@ -0,0 +1,15 @@
import { PageContainer } from '../../components/shared/PageContainer';
import { LineChart } from '../../components/shared/LineChart';
export default function BridgeAnalyticsPage() {
return (
<PageContainer>
<h1 className="text-2xl font-bold mb-6">Bridge Analytics</h1>
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold mb-4">Volume Over Time</h2>
<LineChart data={[]} />
</div>
</PageContainer>
);
}

View File

@@ -0,0 +1,124 @@
import { useState, useEffect } from 'react';
import { MetricCard } from '../../components/shared/MetricCard';
import { DataTable } from '../../components/shared/DataTable';
import { StatusIndicator } from '../../components/shared/StatusIndicator';
import { PageContainer } from '../../components/shared/PageContainer';
import { dbisAdminApi } from '../../services/api/dbisAdminApi';
interface BridgeMetrics {
totalVolume: number;
activeClaims: number;
challengeStatistics: {
total: number;
successful: number;
failed: number;
};
liquidityPoolStatus: {
eth: { total: number; available: number };
weth: { total: number; available: number };
};
}
export default function BridgeOverviewPage() {
const [metrics, setMetrics] = useState<BridgeMetrics | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadMetrics();
const interval = setInterval(loadMetrics, 5000);
return () => clearInterval(interval);
}, []);
const loadMetrics = async () => {
try {
const data = await dbisAdminApi.getBridgeOverview();
setMetrics(data);
} catch (error) {
console.error('Failed to load bridge metrics:', error);
} finally {
setLoading(false);
}
};
if (loading) {
return <PageContainer>Loading...</PageContainer>;
}
return (
<PageContainer>
<h1 className="text-2xl font-bold mb-6">Bridge Overview</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<MetricCard
title="Total Volume"
value={`${metrics?.totalVolume.toLocaleString() || 0} ETH`}
subtitle="All time"
/>
<MetricCard
title="Active Claims"
value={metrics?.activeClaims.toString() || '0'}
subtitle="Pending finalization"
/>
<MetricCard
title="Challenges"
value={metrics?.challengeStatistics.total.toString() || '0'}
subtitle={`${metrics?.challengeStatistics.successful || 0} successful`}
/>
<MetricCard
title="Liquidity"
value={`${metrics?.liquidityPoolStatus.eth.available.toLocaleString() || 0} ETH`}
subtitle="Available"
/>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold mb-4">Liquidity Pool Status</h2>
<div className="space-y-4">
<div>
<div className="flex justify-between mb-2">
<span>ETH Pool</span>
<StatusIndicator status="healthy" />
</div>
<div className="text-sm text-gray-600">
Total: {metrics?.liquidityPoolStatus.eth.total.toLocaleString() || 0} ETH
<br />
Available: {metrics?.liquidityPoolStatus.eth.available.toLocaleString() || 0} ETH
</div>
</div>
<div>
<div className="flex justify-between mb-2">
<span>WETH Pool</span>
<StatusIndicator status="healthy" />
</div>
<div className="text-sm text-gray-600">
Total: {metrics?.liquidityPoolStatus.weth.total.toLocaleString() || 0} WETH
<br />
Available: {metrics?.liquidityPoolStatus.weth.available.toLocaleString() || 0} WETH
</div>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold mb-4">Challenge Statistics</h2>
<div className="space-y-2">
<div className="flex justify-between">
<span>Total Challenges</span>
<span className="font-semibold">{metrics?.challengeStatistics.total || 0}</span>
</div>
<div className="flex justify-between">
<span>Successful</span>
<span className="text-green-600">{metrics?.challengeStatistics.successful || 0}</span>
</div>
<div className="flex justify-between">
<span>Failed</span>
<span className="text-red-600">{metrics?.challengeStatistics.failed || 0}</span>
</div>
</div>
</div>
</div>
</PageContainer>
);
}

View File

@@ -0,0 +1,14 @@
import { PageContainer } from '../../components/shared/PageContainer';
import { DataTable } from '../../components/shared/DataTable';
export default function ISOCurrencyPage() {
return (
<PageContainer>
<h1 className="text-2xl font-bold mb-6">ISO Currency Management</h1>
<div className="bg-white rounded-lg shadow p-6">
<p className="text-gray-600">ISO currency management interface coming soon...</p>
</div>
</PageContainer>
);
}

View File

@@ -0,0 +1,272 @@
import { useState, useEffect } from 'react';
import { PageContainer } from '../../components/shared/PageContainer';
import { DataTable } from '../../components/shared/DataTable';
import { MetricCard } from '../../components/shared/MetricCard';
import { Button } from '../../components/shared/Button';
import { Modal } from '../../components/shared/Modal';
import { FormInput } from '../../components/shared/FormInput';
import { FormSelect } from '../../components/shared/FormSelect';
import { dbisAdminApi } from '../../services/api/dbisAdminApi';
interface DecisionMap {
sizeThresholds: {
small: { max: number; providers: string[] };
medium: { max: number; providers: string[] };
large: { providers: string[] };
};
slippageRules: {
lowSlippage: { max: number; prefer: string };
mediumSlippage: { max: number; prefer: string };
highSlippage: { prefer: string };
};
liquidityRules: {
highLiquidity: { min: number; prefer: string };
mediumLiquidity: { prefer: string };
lowLiquidity: { prefer: string };
};
}
interface Quote {
provider: string;
amountOut: string;
priceImpact: number;
gasEstimate: string;
effectiveOutput: string;
}
export default function LiquidityEnginePage() {
const [decisionMap, setDecisionMap] = useState<DecisionMap | null>(null);
const [quotes, setQuotes] = useState<Quote[]>([]);
const [showConfigModal, setShowConfigModal] = useState(false);
const [loading, setLoading] = useState(true);
const [simulationResult, setSimulationResult] = useState<any>(null);
useEffect(() => {
loadDecisionMap();
loadQuotes();
}, []);
const loadDecisionMap = async () => {
try {
const data = await dbisAdminApi.getLiquidityDecisionMap();
setDecisionMap(data);
} catch (error) {
console.error('Failed to load decision map:', error);
} finally {
setLoading(false);
}
};
const loadQuotes = async () => {
try {
const data = await dbisAdminApi.getLiquidityQuotes({
inputToken: 'WETH',
outputToken: 'USDT',
amount: '1000000000000000000', // 1 ETH
});
setQuotes(data);
} catch (error) {
console.error('Failed to load quotes:', error);
}
};
const handleSaveConfig = async () => {
try {
await dbisAdminApi.updateLiquidityDecisionMap(decisionMap!);
setShowConfigModal(false);
alert('Configuration saved successfully');
} catch (error) {
console.error('Failed to save config:', error);
alert('Failed to save configuration');
}
};
const handleSimulate = async () => {
try {
const result = await dbisAdminApi.simulateRoute({
inputToken: 'WETH',
outputToken: 'USDT',
amount: '1000000000000000000',
});
setSimulationResult(result);
} catch (error) {
console.error('Failed to simulate:', error);
}
};
if (loading) {
return <PageContainer>Loading...</PageContainer>;
}
return (
<PageContainer>
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold">Liquidity Engine</h1>
<div className="flex gap-2">
<Button onClick={() => setShowConfigModal(true)}>Configure Routing</Button>
<Button onClick={handleSimulate}>Simulate Route</Button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<MetricCard
title="Total Swaps"
value="1,234"
subtitle="Last 24h"
/>
<MetricCard
title="Avg Slippage"
value="0.15%"
subtitle="Across all providers"
/>
<MetricCard
title="Best Provider"
value="Dodoex"
subtitle="Most used"
/>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold mb-4">Provider Quotes</h2>
<DataTable
data={quotes}
columns={[
{ key: 'provider', header: 'Provider' },
{ key: 'amountOut', header: 'Output' },
{ key: 'priceImpact', header: 'Price Impact', render: (val) => `${val}%` },
{ key: 'effectiveOutput', header: 'Effective Output' },
]}
/>
</div>
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold mb-4">Decision Logic Map</h2>
{decisionMap && (
<div className="space-y-4">
<div>
<h3 className="font-medium mb-2">Size Thresholds</h3>
<div className="text-sm space-y-1">
<div>Small (&lt; ${decisionMap.sizeThresholds.small.max.toLocaleString()}): {decisionMap.sizeThresholds.small.providers.join(', ')}</div>
<div>Medium (&lt; ${decisionMap.sizeThresholds.medium.max.toLocaleString()}): {decisionMap.sizeThresholds.medium.providers.join(', ')}</div>
<div>Large: {decisionMap.sizeThresholds.large.providers.join(', ')}</div>
</div>
</div>
<div>
<h3 className="font-medium mb-2">Slippage Rules</h3>
<div className="text-sm space-y-1">
<div>Low (&lt; {decisionMap.slippageRules.lowSlippage.max}%): Prefer {decisionMap.slippageRules.lowSlippage.prefer}</div>
<div>Medium (&lt; {decisionMap.slippageRules.mediumSlippage.max}%): Prefer {decisionMap.slippageRules.mediumSlippage.prefer}</div>
<div>High: Prefer {decisionMap.slippageRules.highSlippage.prefer}</div>
</div>
</div>
</div>
)}
</div>
</div>
{simulationResult && (
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold mb-4">Simulation Result</h2>
<div className="space-y-2">
<div><strong>Provider:</strong> {simulationResult.provider}</div>
<div><strong>Expected Output:</strong> {simulationResult.expectedOutput}</div>
<div><strong>Slippage:</strong> {simulationResult.slippage}%</div>
<div><strong>Confidence:</strong> {simulationResult.confidence}%</div>
<div><strong>Reasoning:</strong> {simulationResult.reasoning}</div>
</div>
</div>
)}
{showConfigModal && decisionMap && (
<Modal
title="Configure Routing Logic"
onClose={() => setShowConfigModal(false)}
size="large"
>
<div className="space-y-6">
<div>
<h3 className="font-semibold mb-3">Size Thresholds</h3>
<div className="space-y-3">
<div>
<label className="block text-sm font-medium mb-1">Small Swap Max (USD)</label>
<FormInput
type="number"
value={decisionMap.sizeThresholds.small.max}
onChange={(e) => setDecisionMap({
...decisionMap,
sizeThresholds: {
...decisionMap.sizeThresholds,
small: { ...decisionMap.sizeThresholds.small, max: Number(e.target.value) },
},
})}
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Medium Swap Max (USD)</label>
<FormInput
type="number"
value={decisionMap.sizeThresholds.medium.max}
onChange={(e) => setDecisionMap({
...decisionMap,
sizeThresholds: {
...decisionMap.sizeThresholds,
medium: { ...decisionMap.sizeThresholds.medium, max: Number(e.target.value) },
},
})}
/>
</div>
</div>
</div>
<div>
<h3 className="font-semibold mb-3">Slippage Rules</h3>
<div className="space-y-3">
<div>
<label className="block text-sm font-medium mb-1">Low Slippage Threshold (%)</label>
<FormInput
type="number"
step="0.1"
value={decisionMap.slippageRules.lowSlippage.max}
onChange={(e) => setDecisionMap({
...decisionMap,
slippageRules: {
...decisionMap.slippageRules,
lowSlippage: { ...decisionMap.slippageRules.lowSlippage, max: Number(e.target.value) },
},
})}
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Preferred Provider (Low Slippage)</label>
<FormSelect
value={decisionMap.slippageRules.lowSlippage.prefer}
onChange={(e) => setDecisionMap({
...decisionMap,
slippageRules: {
...decisionMap.slippageRules,
lowSlippage: { ...decisionMap.slippageRules.lowSlippage, prefer: e.target.value },
},
})}
options={[
{ value: 'UniswapV3', label: 'Uniswap V3' },
{ value: 'Dodoex', label: 'Dodoex' },
{ value: 'Balancer', label: 'Balancer' },
{ value: 'Curve', label: 'Curve' },
]}
/>
</div>
</div>
</div>
<div className="flex justify-end gap-2">
<Button variant="secondary" onClick={() => setShowConfigModal(false)}>Cancel</Button>
<Button onClick={handleSaveConfig}>Save Configuration</Button>
</div>
</div>
</Modal>
)}
</PageContainer>
);
}

View File

@@ -0,0 +1,28 @@
import { PageContainer } from '../../components/shared/PageContainer';
import { StatusIndicator } from '../../components/shared/StatusIndicator';
export default function MarketReportingPage() {
return (
<PageContainer>
<h1 className="text-2xl font-bold mb-6">Market Reporting</h1>
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold mb-4">API Connection Status</h2>
<div className="space-y-2">
<div className="flex justify-between">
<span>Binance</span>
<StatusIndicator status="healthy" />
</div>
<div className="flex justify-between">
<span>Coinbase</span>
<StatusIndicator status="healthy" />
</div>
<div className="flex justify-between">
<span>Kraken</span>
<StatusIndicator status="healthy" />
</div>
</div>
</div>
</PageContainer>
);
}

View File

@@ -0,0 +1,76 @@
import { useState, useEffect } from 'react';
import { PageContainer } from '../../components/shared/PageContainer';
import { StatusIndicator } from '../../components/shared/StatusIndicator';
import { LineChart } from '../../components/shared/LineChart';
interface PegStatus {
asset: string;
currentPrice: string;
targetPrice: string;
deviationBps: number;
isMaintained: boolean;
}
export default function PegManagementPage() {
const [pegStatuses, setPegStatuses] = useState<PegStatus[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadPegStatus();
const interval = setInterval(loadPegStatus, 5000);
return () => clearInterval(interval);
}, []);
const loadPegStatus = async () => {
try {
// In production, call API
setPegStatuses([
{ asset: 'USDT', currentPrice: '1.00', targetPrice: '1.00', deviationBps: 0, isMaintained: true },
{ asset: 'USDC', currentPrice: '1.00', targetPrice: '1.00', deviationBps: 0, isMaintained: true },
{ asset: 'WETH', currentPrice: '1.00', targetPrice: '1.00', deviationBps: 0, isMaintained: true },
]);
} catch (error) {
console.error('Failed to load peg status:', error);
} finally {
setLoading(false);
}
};
if (loading) {
return <PageContainer>Loading...</PageContainer>;
}
return (
<PageContainer>
<h1 className="text-2xl font-bold mb-6">Peg Management</h1>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
{pegStatuses.map((peg) => (
<div key={peg.asset} className="bg-white rounded-lg shadow p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold">{peg.asset}</h2>
<StatusIndicator status={peg.isMaintained ? 'healthy' : 'warning'} />
</div>
<div className="space-y-2">
<div className="flex justify-between">
<span>Current Price</span>
<span className="font-semibold">${peg.currentPrice}</span>
</div>
<div className="flex justify-between">
<span>Target Price</span>
<span>${peg.targetPrice}</span>
</div>
<div className="flex justify-between">
<span>Deviation</span>
<span className={peg.deviationBps > 0 ? 'text-red-600' : 'text-green-600'}>
{peg.deviationBps > 0 ? '+' : ''}{peg.deviationBps} bps
</span>
</div>
</div>
</div>
))}
</div>
</PageContainer>
);
}

View File

@@ -0,0 +1,14 @@
import { PageContainer } from '../../components/shared/PageContainer';
import { StatusIndicator } from '../../components/shared/StatusIndicator';
export default function ReserveManagementPage() {
return (
<PageContainer>
<h1 className="text-2xl font-bold mb-6">Reserve Management</h1>
<div className="bg-white rounded-lg shadow p-6">
<p className="text-gray-600">Reserve management interface coming soon...</p>
</div>
</PageContainer>
);
}

View File

@@ -10,6 +10,9 @@ import PieChart from '@/components/shared/PieChart';
import { AdminPermission } from '@/constants/permissions';
import PermissionGate from '@/components/auth/PermissionGate';
import LoadingSpinner from '@/components/shared/LoadingSpinner';
import { TableSkeleton } from '@/components/shared/Skeleton';
import ExportButton from '@/components/shared/ExportButton';
import { REFETCH_INTERVALS } from '@/constants/config';
import type { SCBStatus } from '@/types';
import { formatDistanceToNow } from 'date-fns';
import './OverviewPage.css';
@@ -18,13 +21,21 @@ export default function OverviewPage() {
const { data, isLoading, error } = useQuery({
queryKey: ['dbis-overview'],
queryFn: () => dbisAdminApi.getGlobalOverview(),
refetchInterval: 10000, // Poll every 10 seconds
refetchInterval: () => {
// Use longer interval when tab is hidden
return document.hidden ? 30000 : 10000;
},
});
if (isLoading) {
return (
<div className="page-container">
<LoadingSpinner fullPage />
<div className="page-container" role="status" aria-label="Loading dashboard">
<div className="page-header">
<h1>Global Overview</h1>
</div>
<DashboardLayout>
<TableSkeleton rows={5} cols={4} />
</DashboardLayout>
</div>
);
}
@@ -90,7 +101,7 @@ export default function OverviewPage() {
return (
<div className="page-container">
<div className="page-header">
<header className="page-header">
<h1>Global Overview</h1>
<div className="page-header__actions">
{data?.scbStatus && (
@@ -101,11 +112,16 @@ export default function OverviewPage() {
exportType="csv"
/>
)}
<Button variant="secondary" size="small" onClick={() => window.location.reload()}>
<Button
variant="secondary"
size="small"
onClick={() => window.location.reload()}
aria-label="Refresh dashboard data"
>
Refresh
</Button>
</div>
</div>
</header>
<DashboardLayout>
{/* Network Health Widget */}

View File

@@ -1,16 +1,18 @@
// API Client Service
import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios';
import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig, CancelTokenSource } from 'axios';
import toast from 'react-hot-toast';
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000';
import { env } from '@/config/env';
import { logger } from '@/utils/logger';
import { API_CONFIG, ERROR_MESSAGES } from '@/constants/config';
class ApiClient {
private client: AxiosInstance;
private cancelTokenSources = new Map<string, CancelTokenSource>();
constructor() {
this.client = axios.create({
baseURL: API_BASE_URL,
timeout: 30000,
baseURL: env.VITE_API_BASE_URL,
timeout: API_CONFIG.TIMEOUT,
headers: {
'Content-Type': 'application/json',
},
@@ -19,11 +21,33 @@ class ApiClient {
this.setupInterceptors();
}
/**
* Cancel a pending request by URL
*/
cancelRequest(url: string): void {
const source = this.cancelTokenSources.get(url);
if (source) {
source.cancel('Request cancelled');
this.cancelTokenSources.delete(url);
}
}
/**
* Cancel all pending requests
*/
cancelAllRequests(): void {
this.cancelTokenSources.forEach((source) => {
source.cancel('All requests cancelled');
});
this.cancelTokenSources.clear();
}
private setupInterceptors() {
// Request interceptor
this.client.interceptors.request.use(
(config) => {
const token = localStorage.getItem('auth_token');
// Use sessionStorage instead of localStorage for better security
const token = sessionStorage.getItem('auth_token');
if (token) {
config.headers.Authorization = `SOV-TOKEN ${token}`;
}
@@ -34,62 +58,114 @@ class ApiClient {
config.headers['X-SOV-Timestamp'] = timestamp;
config.headers['X-SOV-Nonce'] = nonce;
// Create cancel token for request cancellation
const source = axios.CancelToken.source();
const url = config.url || '';
this.cancelTokenSources.set(url, source);
config.cancelToken = source.token;
// Log request in development
if (import.meta.env.DEV) {
logger.logRequest(config.method || 'GET', url, config.data);
}
return config;
},
(error) => {
logger.error('Request interceptor error', error);
return Promise.reject(error);
}
);
// Response interceptor
this.client.interceptors.response.use(
(response) => response,
(response) => {
// Remove cancel token source on successful response
const url = response.config.url || '';
this.cancelTokenSources.delete(url);
// Log response in development
if (import.meta.env.DEV) {
logger.logResponse(
response.config.method || 'GET',
url,
response.status,
response.data
);
}
return response;
},
async (error: AxiosError) => {
// Remove cancel token source on error
const url = error.config?.url || '';
this.cancelTokenSources.delete(url);
// Don't show toast for cancelled requests
if (axios.isCancel(error)) {
logger.debug('Request cancelled', { url });
return Promise.reject(error);
}
if (error.response) {
const status = error.response.status;
const responseData = error.response.data as any;
// Log error with context
logger.error(`API Error ${status}`, error, {
url: error.config?.url,
method: error.config?.method,
status,
responseData,
});
switch (status) {
case 401:
// Unauthorized - clear token and redirect to login
localStorage.removeItem('auth_token');
localStorage.removeItem('user');
sessionStorage.removeItem('auth_token');
sessionStorage.removeItem('user');
window.location.href = '/login';
toast.error('Session expired. Please login again.');
toast.error(ERROR_MESSAGES.UNAUTHORIZED);
break;
case 403:
toast.error('You do not have permission to perform this action.');
toast.error(ERROR_MESSAGES.FORBIDDEN);
break;
case 404:
toast.error('Resource not found.');
toast.error(ERROR_MESSAGES.NOT_FOUND);
break;
case 422:
// Validation errors
const validationErrors = (error.response.data as any)?.error?.details;
const validationErrors = responseData?.error?.details;
if (validationErrors) {
Object.values(validationErrors).forEach((msg: any) => {
toast.error(Array.isArray(msg) ? msg[0] : msg);
});
} else {
toast.error('Validation error. Please check your input.');
toast.error(ERROR_MESSAGES.VALIDATION_ERROR);
}
break;
case 500:
toast.error('Server error. Please try again later.');
case 502:
case 503:
case 504:
toast.error(ERROR_MESSAGES.SERVER_ERROR);
break;
default:
const message = (error.response.data as any)?.error?.message || 'An error occurred';
const message = responseData?.error?.message || ERROR_MESSAGES.UNEXPECTED_ERROR;
toast.error(message);
}
} else if (error.request) {
// Network error
toast.error('Network error. Please check your connection.');
logger.error('Network error', error, { url: error.config?.url });
toast.error(ERROR_MESSAGES.NETWORK_ERROR);
} else {
toast.error('An unexpected error occurred.');
logger.error('Request setup error', error);
toast.error(ERROR_MESSAGES.UNEXPECTED_ERROR);
}
return Promise.reject(error);
@@ -101,26 +177,41 @@ class ApiClient {
return this.client;
}
/**
* GET request with automatic error handling
*/
async get<T>(url: string, config?: InternalAxiosRequestConfig): Promise<T> {
const response = await this.client.get<T>(url, config);
return response.data;
}
/**
* POST request with automatic error handling
*/
async post<T>(url: string, data?: any, config?: InternalAxiosRequestConfig): Promise<T> {
const response = await this.client.post<T>(url, data, config);
return response.data;
}
/**
* PUT request with automatic error handling
*/
async put<T>(url: string, data?: any, config?: InternalAxiosRequestConfig): Promise<T> {
const response = await this.client.put<T>(url, data, config);
return response.data;
}
/**
* PATCH request with automatic error handling
*/
async patch<T>(url: string, data?: any, config?: InternalAxiosRequestConfig): Promise<T> {
const response = await this.client.patch<T>(url, data, config);
return response.data;
}
/**
* DELETE request with automatic error handling
*/
async delete<T>(url: string, config?: InternalAxiosRequestConfig): Promise<T> {
const response = await this.client.delete<T>(url, config);
return response.data;

View File

@@ -127,5 +127,27 @@ class DBISAdminAPI {
}
}
// Liquidity Engine methods
async getLiquidityDecisionMap() {
return apiClient.get('/api/admin/liquidity/decision-map');
}
async updateLiquidityDecisionMap(decisionMap: any) {
return apiClient.put('/api/admin/liquidity/decision-map', decisionMap);
}
async getLiquidityQuotes(params: { inputToken: string; outputToken: string; amount: string }) {
return apiClient.get('/api/admin/liquidity/quotes', { params });
}
async getLiquidityRoutingStats() {
return apiClient.get('/api/admin/liquidity/routing-stats');
}
async simulateRoute(params: { inputToken: string; outputToken: string; amount: string }) {
return apiClient.post('/api/admin/liquidity/simulate-route', params);
}
}
export const dbisAdminApi = new DBISAdminAPI();

View File

@@ -2,9 +2,22 @@
import { apiClient } from '../api/client';
import type { LoginCredentials, User } from '@/types';
/**
* Authentication Service
*
* Handles authentication state and token management.
* Uses sessionStorage for better security (tokens cleared on tab close).
*
* Note: For production, consider using httpOnly cookies set by the backend
* for maximum security against XSS attacks.
*/
class AuthService {
private readonly TOKEN_KEY = 'auth_token';
private readonly USER_KEY = 'user';
// Use sessionStorage instead of localStorage for better security
// Tokens are cleared when the browser tab/window is closed
private readonly storage = sessionStorage;
async login(credentials: LoginCredentials): Promise<{ user: User; token: string }> {
// TODO: Replace with actual login endpoint when available
@@ -41,25 +54,50 @@ class AuthService {
}
getToken(): string | null {
return localStorage.getItem(this.TOKEN_KEY);
try {
return this.storage.getItem(this.TOKEN_KEY);
} catch (error) {
// Handle storage access errors (e.g., private browsing mode)
console.error('Failed to get token from storage:', error);
return null;
}
}
getUser(): User | null {
const userStr = localStorage.getItem(this.USER_KEY);
return userStr ? JSON.parse(userStr) : null;
try {
const userStr = this.storage.getItem(this.USER_KEY);
return userStr ? JSON.parse(userStr) : null;
} catch (error) {
console.error('Failed to get user from storage:', error);
return null;
}
}
setToken(token: string): void {
localStorage.setItem(this.TOKEN_KEY, token);
try {
this.storage.setItem(this.TOKEN_KEY, token);
} catch (error) {
console.error('Failed to set token in storage:', error);
throw new Error('Failed to save authentication token');
}
}
setUser(user: User): void {
localStorage.setItem(this.USER_KEY, JSON.stringify(user));
try {
this.storage.setItem(this.USER_KEY, JSON.stringify(user));
} catch (error) {
console.error('Failed to set user in storage:', error);
throw new Error('Failed to save user data');
}
}
clearAuth(): void {
localStorage.removeItem(this.TOKEN_KEY);
localStorage.removeItem(this.USER_KEY);
try {
this.storage.removeItem(this.TOKEN_KEY);
this.storage.removeItem(this.USER_KEY);
} catch (error) {
console.error('Failed to clear auth from storage:', error);
}
}
isAuthenticated(): boolean {

View File

@@ -1,5 +1,6 @@
// Auth Store (Zustand)
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { authService } from '@/services/auth/authService';
import type { User, LoginCredentials } from '@/types';
@@ -15,70 +16,85 @@ interface AuthState {
isDBISLevel: () => boolean;
}
export const useAuthStore = create<AuthState>((set, get) => ({
user: null,
token: null,
isAuthenticated: false,
isLoading: true,
initialize: () => {
const token = authService.getToken();
const user = authService.getUser();
if (token && user && authService.isAuthenticated()) {
set({
token,
user,
isAuthenticated: true,
isLoading: false,
});
} else {
authService.clearAuth();
set({
token: null,
export const useAuthStore = create<AuthState>()(
devtools(
persist(
(set, get) => ({
user: null,
token: null,
isAuthenticated: false,
isLoading: false,
});
}
},
isLoading: true,
login: async (credentials: LoginCredentials) => {
try {
set({ isLoading: true });
const { user, token } = await authService.login(credentials);
set({
user,
token,
isAuthenticated: true,
isLoading: false,
});
} catch (error) {
set({ isLoading: false });
throw error;
}
},
initialize: () => {
const token = authService.getToken();
const user = authService.getUser();
logout: async () => {
await authService.logout();
set({
user: null,
token: null,
isAuthenticated: false,
});
},
if (token && user && authService.isAuthenticated()) {
set({
token,
user,
isAuthenticated: true,
isLoading: false,
});
} else {
authService.clearAuth();
set({
token: null,
user: null,
isAuthenticated: false,
isLoading: false,
});
}
},
checkPermission: (permission: string): boolean => {
const { user } = get();
if (!user) return false;
if (user.permissions.includes('all')) return true;
return user.permissions.includes(permission);
},
login: async (credentials: LoginCredentials) => {
try {
set({ isLoading: true });
const { user, token } = await authService.login(credentials);
set({
user,
token,
isAuthenticated: true,
isLoading: false,
});
} catch (error) {
set({ isLoading: false });
throw error;
}
},
isDBISLevel: (): boolean => {
const { user } = get();
if (!user) return false;
return ['DBIS_Super_Admin', 'DBIS_Ops', 'DBIS_Risk'].includes(user.role);
},
}));
logout: async () => {
await authService.logout();
set({
user: null,
token: null,
isAuthenticated: false,
});
},
checkPermission: (permission: string): boolean => {
const { user } = get();
if (!user) return false;
if (user.permissions.includes('all')) return true;
return user.permissions.includes(permission);
},
isDBISLevel: (): boolean => {
const { user } = get();
if (!user) return false;
return ['DBIS_Super_Admin', 'DBIS_Ops', 'DBIS_Risk'].includes(user.role);
},
}),
{
name: 'auth-storage',
// Only persist user data, not token (token is in sessionStorage for security)
partialize: (state) => ({
user: state.user,
// Don't persist token or isAuthenticated for security
}),
}
),
{ name: 'AuthStore' }
)
);

View File

@@ -0,0 +1,128 @@
/**
* Error Tracking Utility
*
* Provides error tracking integration (ready for Sentry or similar services).
* Currently provides a no-op implementation that can be replaced with actual
* error tracking service integration.
*
* To integrate Sentry:
* 1. Install: npm install @sentry/react
* 2. Uncomment and configure the Sentry initialization
* 3. Update the captureException and captureMessage calls
*/
// Uncomment when ready to use Sentry:
// import * as Sentry from '@sentry/react';
interface ErrorContext {
[key: string]: unknown;
}
class ErrorTracker {
private initialized = false;
/**
* Initialize error tracking service
*/
init(dsn?: string, environment?: string): void {
if (this.initialized) {
return;
}
// Uncomment when ready to use Sentry:
/*
if (!dsn) {
console.warn('Error tracking DSN not provided, error tracking disabled');
return;
}
Sentry.init({
dsn,
environment: environment || import.meta.env.MODE,
integrations: [
new Sentry.BrowserTracing(),
new Sentry.Replay(),
],
tracesSampleRate: 1.0, // Adjust based on traffic
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
});
this.initialized = true;
*/
}
/**
* Capture an exception
*/
captureException(error: Error, context?: ErrorContext): void {
// Uncomment when ready to use Sentry:
/*
if (this.initialized) {
Sentry.captureException(error, {
contexts: {
custom: context || {},
},
});
}
*/
// Fallback logging
if (import.meta.env.DEV) {
console.error('Error captured:', error, context);
}
}
/**
* Capture a message
*/
captureMessage(message: string, level: 'info' | 'warning' | 'error' = 'error', context?: ErrorContext): void {
// Uncomment when ready to use Sentry:
/*
if (this.initialized) {
Sentry.captureMessage(message, {
level: level as Sentry.SeverityLevel,
contexts: {
custom: context || {},
},
});
}
*/
// Fallback logging
if (import.meta.env.DEV) {
const logMethod = level === 'error' ? console.error : level === 'warning' ? console.warn : console.info;
logMethod('Message captured:', message, context);
}
}
/**
* Set user context for error tracking
*/
setUser(user: { id: string; email?: string; username?: string } | null): void {
// Uncomment when ready to use Sentry:
/*
if (this.initialized) {
Sentry.setUser(user);
}
*/
}
/**
* Add breadcrumb for debugging
*/
addBreadcrumb(message: string, category?: string, level?: 'info' | 'warning' | 'error'): void {
// Uncomment when ready to use Sentry:
/*
if (this.initialized) {
Sentry.addBreadcrumb({
message,
category: category || 'custom',
level: level || 'info',
});
}
*/
}
}
export const errorTracker = new ErrorTracker();

View File

@@ -0,0 +1,95 @@
/**
* Structured Logging Utility
*
* Provides structured logging with different log levels.
* In production, logs can be sent to error tracking services.
*
* Usage:
* logger.info('User logged in', { userId: '123' });
* logger.error('API request failed', { error, url });
*/
export enum LogLevel {
DEBUG = 'debug',
INFO = 'info',
WARN = 'warn',
ERROR = 'error',
}
interface LogContext {
[key: string]: unknown;
}
class Logger {
private isDevelopment = import.meta.env.DEV;
private isProduction = import.meta.env.PROD;
/**
* Log debug messages (only in development)
*/
debug(message: string, context?: LogContext): void {
if (this.isDevelopment) {
console.debug(`[DEBUG] ${message}`, context || '');
}
}
/**
* Log informational messages
*/
info(message: string, context?: LogContext): void {
if (this.isDevelopment) {
console.info(`[INFO] ${message}`, context || '');
}
// In production, could send to analytics service
}
/**
* Log warning messages
*/
warn(message: string, context?: LogContext): void {
console.warn(`[WARN] ${message}`, context || '');
// In production, could send to monitoring service
}
/**
* Log error messages
*/
error(message: string, error?: Error | unknown, context?: LogContext): void {
const errorContext = {
...context,
error: error instanceof Error ? {
message: error.message,
stack: error.stack,
name: error.name,
} : error,
};
console.error(`[ERROR] ${message}`, errorContext);
// In production, send to error tracking service (e.g., Sentry)
if (this.isProduction && error) {
// TODO: Integrate with error tracking service
// Example: Sentry.captureException(error, { contexts: { custom: context } });
}
}
/**
* Log API requests (development only)
*/
logRequest(method: string, url: string, data?: unknown): void {
if (this.isDevelopment) {
this.debug(`API ${method.toUpperCase()} ${url}`, { data });
}
}
/**
* Log API responses (development only)
*/
logResponse(method: string, url: string, status: number, data?: unknown): void {
if (this.isDevelopment) {
this.debug(`API ${method.toUpperCase()} ${url} - ${status}`, { data });
}
}
}
export const logger = new Logger();