Session 4 complete - RedFlag update management platform
🚩 Private development - version retention only ✅ Complete web dashboard (React + TypeScript + TailwindCSS) ✅ Production-ready server backend (Go + Gin + PostgreSQL) ✅ Linux agent with APT + Docker scanning + local CLI tools ✅ JWT authentication and REST API ✅ Update discovery and approval workflow 🚧 Status: Alpha software - active development 📦 Purpose: Version retention during development ⚠️ Not for public use or deployment
This commit is contained in:
201
aggregator-web/src/lib/api.ts
Normal file
201
aggregator-web/src/lib/api.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import {
|
||||
Agent,
|
||||
UpdatePackage,
|
||||
DashboardStats,
|
||||
AgentListResponse,
|
||||
UpdateListResponse,
|
||||
UpdateApprovalRequest,
|
||||
ScanRequest,
|
||||
ListQueryParams,
|
||||
ApiResponse,
|
||||
ApiError
|
||||
} from '@/types';
|
||||
|
||||
// Create axios instance
|
||||
const api = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_URL || '/api/v1',
|
||||
timeout: 30000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// Request interceptor to add auth token
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
// Response interceptor to handle errors
|
||||
api.interceptors.response.use(
|
||||
(response: AxiosResponse) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
// Clear token and redirect to login
|
||||
localStorage.removeItem('auth_token');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// API endpoints
|
||||
export const agentApi = {
|
||||
// Get all agents
|
||||
getAgents: async (params?: ListQueryParams): Promise<AgentListResponse> => {
|
||||
const response = await api.get('/agents', { params });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get single agent
|
||||
getAgent: async (id: string): Promise<Agent> => {
|
||||
const response = await api.get(`/agents/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Trigger scan on agents
|
||||
triggerScan: async (request: ScanRequest): Promise<void> => {
|
||||
await api.post('/agents/scan', request);
|
||||
},
|
||||
|
||||
// Trigger scan on single agent
|
||||
scanAgent: async (id: string): Promise<void> => {
|
||||
await api.post(`/agents/${id}/scan`);
|
||||
},
|
||||
};
|
||||
|
||||
export const updateApi = {
|
||||
// Get all updates
|
||||
getUpdates: async (params?: ListQueryParams): Promise<UpdateListResponse> => {
|
||||
const response = await api.get('/updates', { params });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get single update
|
||||
getUpdate: async (id: string): Promise<UpdatePackage> => {
|
||||
const response = await api.get(`/updates/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Approve updates
|
||||
approveUpdates: async (request: UpdateApprovalRequest): Promise<void> => {
|
||||
await api.post('/updates/approve', request);
|
||||
},
|
||||
|
||||
// Approve single update
|
||||
approveUpdate: async (id: string, scheduledAt?: string): Promise<void> => {
|
||||
await api.post(`/updates/${id}/approve`, { scheduled_at: scheduledAt });
|
||||
},
|
||||
|
||||
// Reject/cancel update
|
||||
rejectUpdate: async (id: string): Promise<void> => {
|
||||
await api.post(`/updates/${id}/reject`);
|
||||
},
|
||||
|
||||
// Install update immediately
|
||||
installUpdate: async (id: string): Promise<void> => {
|
||||
await api.post(`/updates/${id}/install`);
|
||||
},
|
||||
};
|
||||
|
||||
export const statsApi = {
|
||||
// Get dashboard statistics
|
||||
getDashboardStats: async (): Promise<DashboardStats> => {
|
||||
const response = await api.get('/stats/summary');
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
export const authApi = {
|
||||
// Simple login (using API key or token)
|
||||
login: async (credentials: { token: string }): Promise<{ token: string }> => {
|
||||
const response = await api.post('/auth/login', credentials);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Verify token
|
||||
verifyToken: async (): Promise<{ valid: boolean }> => {
|
||||
const response = await api.get('/auth/verify');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Logout
|
||||
logout: async (): Promise<void> => {
|
||||
await api.post('/auth/logout');
|
||||
},
|
||||
};
|
||||
|
||||
// Utility functions
|
||||
export const createQueryString = (params: Record<string, any>): string => {
|
||||
const searchParams = new URLSearchParams();
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(v => searchParams.append(key, v));
|
||||
} else {
|
||||
searchParams.append(key, value.toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
return searchParams.toString();
|
||||
};
|
||||
|
||||
// Error handling utility
|
||||
export const handleApiError = (error: any): ApiError => {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const status = error.response?.status;
|
||||
const data = error.response?.data;
|
||||
|
||||
if (status === 401) {
|
||||
return {
|
||||
message: 'Authentication required. Please log in.',
|
||||
code: 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (status === 403) {
|
||||
return {
|
||||
message: 'Access denied. You do not have permission to perform this action.',
|
||||
code: 'FORBIDDEN',
|
||||
};
|
||||
}
|
||||
|
||||
if (status === 404) {
|
||||
return {
|
||||
message: 'The requested resource was not found.',
|
||||
code: 'NOT_FOUND',
|
||||
};
|
||||
}
|
||||
|
||||
if (status === 429) {
|
||||
return {
|
||||
message: 'Too many requests. Please try again later.',
|
||||
code: 'RATE_LIMIT_EXCEEDED',
|
||||
};
|
||||
}
|
||||
|
||||
if (status >= 500) {
|
||||
return {
|
||||
message: 'Server error. Please try again later.',
|
||||
code: 'SERVER_ERROR',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
message: data?.message || error.message || 'An error occurred',
|
||||
code: data?.code || 'UNKNOWN_ERROR',
|
||||
details: data?.details,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
message: error.message || 'An unexpected error occurred',
|
||||
code: 'UNKNOWN_ERROR',
|
||||
};
|
||||
};
|
||||
|
||||
export default api;
|
||||
241
aggregator-web/src/lib/store.ts
Normal file
241
aggregator-web/src/lib/store.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||
import { Agent, UpdatePackage, FilterState } from '@/types';
|
||||
|
||||
// Auth store
|
||||
interface AuthState {
|
||||
token: string | null;
|
||||
isAuthenticated: boolean;
|
||||
setToken: (token: string) => void;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
token: null,
|
||||
isAuthenticated: false,
|
||||
setToken: (token) => set({ token, isAuthenticated: true }),
|
||||
logout: () => set({ token: null, isAuthenticated: false }),
|
||||
}),
|
||||
{
|
||||
name: 'auth-storage',
|
||||
partialize: (state) => ({ token: state.token, isAuthenticated: state.isAuthenticated }),
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// UI store for global state
|
||||
interface UIState {
|
||||
sidebarOpen: boolean;
|
||||
theme: 'light' | 'dark';
|
||||
activeTab: string;
|
||||
setSidebarOpen: (open: boolean) => void;
|
||||
setTheme: (theme: 'light' | 'dark') => void;
|
||||
setActiveTab: (tab: string) => void;
|
||||
}
|
||||
|
||||
export const useUIStore = create<UIState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
sidebarOpen: true,
|
||||
theme: 'light',
|
||||
activeTab: 'dashboard',
|
||||
setSidebarOpen: (open) => set({ sidebarOpen: open }),
|
||||
setTheme: (theme) => set({ theme }),
|
||||
setActiveTab: (tab) => set({ activeTab: tab }),
|
||||
}),
|
||||
{
|
||||
name: 'ui-storage',
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Agent store
|
||||
interface AgentState {
|
||||
agents: Agent[];
|
||||
selectedAgent: Agent | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
setAgents: (agents: Agent[]) => void;
|
||||
setSelectedAgent: (agent: Agent | null) => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
setError: (error: string | null) => void;
|
||||
updateAgentStatus: (agentId: string, status: Agent['status'], lastCheckin: string) => void;
|
||||
addAgent: (agent: Agent) => void;
|
||||
removeAgent: (agentId: string) => void;
|
||||
}
|
||||
|
||||
export const useAgentStore = create<AgentState>((set, get) => ({
|
||||
agents: [],
|
||||
selectedAgent: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
|
||||
setAgents: (agents) => set({ agents }),
|
||||
setSelectedAgent: (agent) => set({ selectedAgent: agent }),
|
||||
setLoading: (loading) => set({ loading }),
|
||||
setError: (error) => set({ error }),
|
||||
|
||||
updateAgentStatus: (agentId, status, lastCheckin) => {
|
||||
const { agents } = get();
|
||||
const updatedAgents = agents.map(agent =>
|
||||
agent.id === agentId
|
||||
? { ...agent, status, last_checkin: lastCheckin }
|
||||
: agent
|
||||
);
|
||||
set({ agents: updatedAgents });
|
||||
},
|
||||
|
||||
addAgent: (agent) => {
|
||||
const { agents } = get();
|
||||
set({ agents: [...agents, agent] });
|
||||
},
|
||||
|
||||
removeAgent: (agentId) => {
|
||||
const { agents } = get();
|
||||
set({ agents: agents.filter(agent => agent.id !== agentId) });
|
||||
},
|
||||
}));
|
||||
|
||||
// Updates store
|
||||
interface UpdateState {
|
||||
updates: UpdatePackage[];
|
||||
selectedUpdate: UpdatePackage | null;
|
||||
filters: FilterState;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
setUpdates: (updates: UpdatePackage[]) => void;
|
||||
setSelectedUpdate: (update: UpdatePackage | null) => void;
|
||||
setFilters: (filters: Partial<FilterState>) => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
setError: (error: string | null) => void;
|
||||
updateUpdateStatus: (updateId: string, status: UpdatePackage['status']) => void;
|
||||
bulkUpdateStatus: (updateIds: string[], status: UpdatePackage['status']) => void;
|
||||
}
|
||||
|
||||
export const useUpdateStore = create<UpdateState>((set, get) => ({
|
||||
updates: [],
|
||||
selectedUpdate: null,
|
||||
filters: {
|
||||
status: [],
|
||||
severity: [],
|
||||
type: [],
|
||||
search: '',
|
||||
},
|
||||
loading: false,
|
||||
error: null,
|
||||
|
||||
setUpdates: (updates) => set({ updates }),
|
||||
setSelectedUpdate: (update) => set({ selectedUpdate: update }),
|
||||
setLoading: (loading) => set({ loading }),
|
||||
setError: (error) => set({ error }),
|
||||
|
||||
setFilters: (newFilters) => {
|
||||
const { filters } = get();
|
||||
set({ filters: { ...filters, ...newFilters } });
|
||||
},
|
||||
|
||||
updateUpdateStatus: (updateId, status) => {
|
||||
const { updates } = get();
|
||||
const updatedUpdates = updates.map(update =>
|
||||
update.id === updateId
|
||||
? { ...update, status, updated_at: new Date().toISOString() }
|
||||
: update
|
||||
);
|
||||
set({ updates: updatedUpdates });
|
||||
},
|
||||
|
||||
bulkUpdateStatus: (updateIds, status) => {
|
||||
const { updates } = get();
|
||||
const updatedUpdates = updates.map(update =>
|
||||
updateIds.includes(update.id)
|
||||
? { ...update, status, updated_at: new Date().toISOString() }
|
||||
: update
|
||||
);
|
||||
set({ updates: updatedUpdates });
|
||||
},
|
||||
}));
|
||||
|
||||
// Real-time updates store
|
||||
interface RealtimeState {
|
||||
isConnected: boolean;
|
||||
lastUpdate: string | null;
|
||||
notifications: Array<{
|
||||
id: string;
|
||||
type: 'info' | 'success' | 'warning' | 'error';
|
||||
title: string;
|
||||
message: string;
|
||||
timestamp: string;
|
||||
read: boolean;
|
||||
}>;
|
||||
setConnected: (connected: boolean) => void;
|
||||
setLastUpdate: (timestamp: string) => void;
|
||||
addNotification: (notification: Omit<typeof RealtimeState.prototype.notifications[0], 'id' | 'timestamp' | 'read'>) => void;
|
||||
markNotificationRead: (id: string) => void;
|
||||
clearNotifications: () => void;
|
||||
}
|
||||
|
||||
export const useRealtimeStore = create<RealtimeState>((set, get) => ({
|
||||
isConnected: false,
|
||||
lastUpdate: null,
|
||||
notifications: [],
|
||||
|
||||
setConnected: (isConnected) => set({ isConnected }),
|
||||
setLastUpdate: (lastUpdate) => set({ lastUpdate }),
|
||||
|
||||
addNotification: (notification) => {
|
||||
const { notifications } = get();
|
||||
const newNotification = {
|
||||
...notification,
|
||||
id: Math.random().toString(36).substring(7),
|
||||
timestamp: new Date().toISOString(),
|
||||
read: false,
|
||||
};
|
||||
set({ notifications: [newNotification, ...notifications] });
|
||||
},
|
||||
|
||||
markNotificationRead: (id) => {
|
||||
const { notifications } = get();
|
||||
const updatedNotifications = notifications.map(notification =>
|
||||
notification.id === id ? { ...notification, read: true } : notification
|
||||
);
|
||||
set({ notifications: updatedNotifications });
|
||||
},
|
||||
|
||||
clearNotifications: () => set({ notifications: [] }),
|
||||
}));
|
||||
|
||||
// Settings store
|
||||
interface SettingsState {
|
||||
autoRefresh: boolean;
|
||||
refreshInterval: number;
|
||||
notificationsEnabled: boolean;
|
||||
compactView: boolean;
|
||||
setAutoRefresh: (enabled: boolean) => void;
|
||||
setRefreshInterval: (interval: number) => void;
|
||||
setNotificationsEnabled: (enabled: boolean) => void;
|
||||
setCompactView: (enabled: boolean) => void;
|
||||
}
|
||||
|
||||
export const useSettingsStore = create<SettingsState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
autoRefresh: true,
|
||||
refreshInterval: 30000, // 30 seconds
|
||||
notificationsEnabled: true,
|
||||
compactView: false,
|
||||
|
||||
setAutoRefresh: (autoRefresh) => set({ autoRefresh }),
|
||||
setRefreshInterval: (refreshInterval) => set({ refreshInterval }),
|
||||
setNotificationsEnabled: (notificationsEnabled) => set({ notificationsEnabled }),
|
||||
setCompactView: (compactView) => set({ compactView }),
|
||||
}),
|
||||
{
|
||||
name: 'settings-storage',
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
}
|
||||
)
|
||||
);
|
||||
247
aggregator-web/src/lib/utils.ts
Normal file
247
aggregator-web/src/lib/utils.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
// Utility function for combining class names
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
// Date formatting utilities
|
||||
export const formatDate = (dateString: string): string => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
export const formatRelativeTime = (dateString: string): string => {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMins / 60);
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
|
||||
if (diffMins < 1) {
|
||||
return 'Just now';
|
||||
} else if (diffMins < 60) {
|
||||
return `${diffMins} minute${diffMins !== 1 ? 's' : ''} ago`;
|
||||
} else if (diffHours < 24) {
|
||||
return `${diffHours} hour${diffHours !== 1 ? 's' : ''} ago`;
|
||||
} else if (diffDays < 7) {
|
||||
return `${diffDays} day${diffDays !== 1 ? 's' : ''} ago`;
|
||||
} else {
|
||||
return formatDate(dateString);
|
||||
}
|
||||
};
|
||||
|
||||
export const isOnline = (lastCheckin: string): boolean => {
|
||||
const lastCheck = new Date(lastCheckin);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - lastCheck.getTime();
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
return diffMins < 10; // Consider online if checked in within 10 minutes
|
||||
};
|
||||
|
||||
// Size formatting utilities
|
||||
export const formatBytes = (bytes: number): string => {
|
||||
if (bytes === 0) return '0 B';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
// Version comparison utilities
|
||||
export const versionCompare = (v1: string, v2: string): number => {
|
||||
const parts1 = v1.split('.').map(Number);
|
||||
const parts2 = v2.split('.').map(Number);
|
||||
|
||||
const maxLength = Math.max(parts1.length, parts2.length);
|
||||
|
||||
for (let i = 0; i < maxLength; i++) {
|
||||
const part1 = parts1[i] || 0;
|
||||
const part2 = parts2[i] || 0;
|
||||
|
||||
if (part1 > part2) return 1;
|
||||
if (part1 < part2) return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
// Status and severity utilities
|
||||
export const getStatusColor = (status: string): string => {
|
||||
switch (status) {
|
||||
case 'online':
|
||||
return 'text-success-600 bg-success-100';
|
||||
case 'offline':
|
||||
return 'text-danger-600 bg-danger-100';
|
||||
case 'pending':
|
||||
return 'text-warning-600 bg-warning-100';
|
||||
case 'approved':
|
||||
case 'scheduled':
|
||||
return 'text-blue-600 bg-blue-100';
|
||||
case 'installing':
|
||||
return 'text-indigo-600 bg-indigo-100';
|
||||
case 'installed':
|
||||
return 'text-success-600 bg-success-100';
|
||||
case 'failed':
|
||||
return 'text-danger-600 bg-danger-100';
|
||||
default:
|
||||
return 'text-gray-600 bg-gray-100';
|
||||
}
|
||||
};
|
||||
|
||||
export const getSeverityColor = (severity: string): string => {
|
||||
switch (severity) {
|
||||
case 'critical':
|
||||
return 'text-danger-600 bg-danger-100';
|
||||
case 'high':
|
||||
return 'text-warning-600 bg-warning-100';
|
||||
case 'medium':
|
||||
return 'text-blue-600 bg-blue-100';
|
||||
case 'low':
|
||||
return 'text-gray-600 bg-gray-100';
|
||||
default:
|
||||
return 'text-gray-600 bg-gray-100';
|
||||
}
|
||||
};
|
||||
|
||||
export const getPackageTypeIcon = (type: string): string => {
|
||||
switch (type) {
|
||||
case 'apt':
|
||||
return '📦';
|
||||
case 'docker':
|
||||
return '🐳';
|
||||
case 'yum':
|
||||
case 'dnf':
|
||||
return '🐧';
|
||||
case 'windows':
|
||||
return '🪟';
|
||||
case 'winget':
|
||||
return '📱';
|
||||
default:
|
||||
return '📋';
|
||||
}
|
||||
};
|
||||
|
||||
// Filter and search utilities
|
||||
export const filterUpdates = (
|
||||
updates: any[],
|
||||
filters: {
|
||||
status: string[];
|
||||
severity: string[];
|
||||
type: string[];
|
||||
search: string;
|
||||
}
|
||||
): any[] => {
|
||||
return updates.filter(update => {
|
||||
// Status filter
|
||||
if (filters.status.length > 0 && !filters.status.includes(update.status)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Severity filter
|
||||
if (filters.severity.length > 0 && !filters.severity.includes(update.severity)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Type filter
|
||||
if (filters.type.length > 0 && !filters.type.includes(update.package_type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Search filter
|
||||
if (filters.search) {
|
||||
const searchLower = filters.search.toLowerCase();
|
||||
return (
|
||||
update.package_name.toLowerCase().includes(searchLower) ||
|
||||
update.current_version.toLowerCase().includes(searchLower) ||
|
||||
update.available_version.toLowerCase().includes(searchLower)
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
// Error handling utilities
|
||||
export const getErrorMessage = (error: any): string => {
|
||||
if (typeof error === 'string') {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (error?.message) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
if (error?.response?.data?.message) {
|
||||
return error.response.data.message;
|
||||
}
|
||||
|
||||
return 'An unexpected error occurred';
|
||||
};
|
||||
|
||||
// Debounce utility
|
||||
export const debounce = <T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
wait: number
|
||||
): ((...args: Parameters<T>) => void) => {
|
||||
let timeout: NodeJS.Timeout;
|
||||
|
||||
return (...args: Parameters<T>) => {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func(...args), wait);
|
||||
};
|
||||
};
|
||||
|
||||
// Local storage utilities
|
||||
export const storage = {
|
||||
get: (key: string): string | null => {
|
||||
try {
|
||||
return localStorage.getItem(key);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
set: (key: string, value: string): void => {
|
||||
try {
|
||||
localStorage.setItem(key, value);
|
||||
} catch {
|
||||
// Silent fail for storage issues
|
||||
}
|
||||
},
|
||||
|
||||
remove: (key: string): void => {
|
||||
try {
|
||||
localStorage.removeItem(key);
|
||||
} catch {
|
||||
// Silent fail for storage issues
|
||||
}
|
||||
},
|
||||
|
||||
getJSON: <T = any>(key: string): T | null => {
|
||||
try {
|
||||
const item = localStorage.getItem(key);
|
||||
return item ? JSON.parse(item) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
setJSON: (key: string, value: any): void => {
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
} catch {
|
||||
// Silent fail for storage issues
|
||||
}
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user