GT AI OS Community Edition v2.0.33

Security hardening release addressing CodeQL and Dependabot alerts:

- Fix stack trace exposure in error responses
- Add SSRF protection with DNS resolution checking
- Implement proper URL hostname validation (replaces substring matching)
- Add centralized path sanitization to prevent path traversal
- Fix ReDoS vulnerability in email validation regex
- Improve HTML sanitization in validation utilities
- Fix capability wildcard matching in auth utilities
- Update glob dependency to address CVE
- Add CodeQL suppression comments for verified false positives

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
HackWeasel
2025-12-12 17:04:45 -05:00
commit b9dfb86260
746 changed files with 232071 additions and 0 deletions

View File

@@ -0,0 +1,447 @@
import axios, { AxiosInstance, AxiosError } from 'axios';
import { useAuthStore } from '@/stores/auth-store';
import toast from 'react-hot-toast';
// Determine the correct API URL based on environment
const getApiBaseUrl = () => {
// Always use relative URLs to go through Next.js proxy
// This ensures all requests (SSR and client-side) use the proxy
return '';
};
// Helper function to check if JWT token is expired
const isTokenExpired = (token: string): boolean => {
try {
const payload = JSON.parse(atob(token.split('.')[1]));
if (!payload || !payload.exp) return true;
const now = Math.floor(Date.now() / 1000);
return payload.exp < now;
} catch (error) {
return true;
}
};
// Create axios instance
const api: AxiosInstance = axios.create({
baseURL: getApiBaseUrl(),
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor to add auth token and check expiry
api.interceptors.request.use(
(config) => {
const token = useAuthStore.getState().token;
// Check if token exists and is expired
if (token && isTokenExpired(token)) {
const { logout } = useAuthStore.getState();
logout();
toast.info('Your session has expired. Please login again.');
window.location.href = '/auth/login';
return Promise.reject(new Error('Token expired'));
}
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
/**
* Handle server-side session headers (Issue #264)
* Server is authoritative for session state - dispatch events for IdleTimerProvider
*/
function handleSessionHeaders(response: { headers: Record<string, string> }): void {
// Check for session expired header (401 responses)
const sessionExpired = response.headers?.['x-session-expired'];
if (sessionExpired) {
console.log('[API] Server session expired:', sessionExpired);
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('session-expired', {
detail: { reason: sessionExpired }
}));
}
return; // Don't process warning if already expired
}
// Check for session warning header
const sessionWarning = response.headers?.['x-session-warning'];
if (sessionWarning && sessionWarning !== 'validation-unavailable') {
const secondsRemaining = parseInt(sessionWarning, 10);
if (!isNaN(secondsRemaining) && secondsRemaining > 0) {
console.log('[API] Server session warning:', secondsRemaining, 'seconds remaining');
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('session-warning', {
detail: { secondsRemaining }
}));
}
}
}
}
// Response interceptor for error handling
api.interceptors.response.use(
(response) => {
// Handle session headers on successful responses
handleSessionHeaders(response);
return response;
},
(error: AxiosError) => {
const { response } = error;
// Handle session headers on error responses too
if (response) {
handleSessionHeaders(response as unknown as { headers: Record<string, string> });
}
// Handle authentication errors
if (response?.status === 401) {
// Check if this is a session expiry (let the event handler deal with it)
const sessionExpired = response.headers?.['x-session-expired'];
if (sessionExpired) {
// Event already dispatched by handleSessionHeaders, just reject
return Promise.reject(error);
}
const { logout } = useAuthStore.getState();
logout();
toast.error('Session expired. Please login again.');
window.location.href = '/auth/login';
return Promise.reject(error);
}
// Handle forbidden errors
if (response?.status === 403) {
toast.error('Access denied. Insufficient permissions.');
return Promise.reject(error);
}
// Handle server errors
if (response?.status && response.status >= 500) {
toast.error('Server error. Please try again later.');
return Promise.reject(error);
}
// Handle network errors
if (!response) {
toast.error('Network error. Please check your connection.');
return Promise.reject(error);
}
return Promise.reject(error);
}
);
// API endpoints
export const authApi = {
login: async (email: string, password: string) =>
api.post('/api/v1/login', { email, password }),
logout: async () =>
api.post('/api/v1/logout'),
me: async () =>
api.get('/api/v1/me'),
verifyToken: async () =>
api.get('/api/v1/verify-token'),
changePassword: async (currentPassword: string, newPassword: string) =>
api.post('/api/v1/change-password', {
current_password: currentPassword,
new_password: newPassword,
}),
};
export const tenantsApi = {
list: async (page = 1, limit = 20, search?: string, status?: string) =>
api.get('/api/v1/tenants/', { params: { page, limit, search, status } }),
get: async (id: number) =>
api.get(`/api/v1/tenants/${id}/`),
create: async (data: any) =>
api.post('/api/v1/tenants/', data),
update: async (id: number, data: any) =>
api.put(`/api/v1/tenants/${id}/`, data),
delete: async (id: number) =>
api.delete(`/api/v1/tenants/${id}/`),
deploy: async (id: number) =>
api.post(`/api/v1/tenants/${id}/deploy/`),
suspend: async (id: number) =>
api.post(`/api/v1/tenants/${id}/suspend/`),
activate: async (id: number) =>
api.post(`/api/v1/tenants/${id}/activate/`),
// Optics feature toggle
getOpticsStatus: async (id: number) =>
api.get(`/api/v1/tenants/${id}/optics`),
setOpticsEnabled: async (id: number, enabled: boolean) =>
api.put(`/api/v1/tenants/${id}/optics`, { enabled }),
};
export const usersApi = {
list: async (page = 1, limit = 20, search?: string, tenantId?: number, userType?: string) =>
api.get('/api/v1/users/', { params: { page, limit, search, tenant_id: tenantId, user_type: userType } }),
get: async (id: number) =>
api.get(`/api/v1/users/${id}/`),
create: async (data: any) =>
api.post('/api/v1/users/', data),
update: async (id: number, data: any) =>
api.put(`/api/v1/users/${id}/`, data),
delete: async (id: number) =>
api.delete(`/api/v1/users/${id}/`),
activate: async (id: number) =>
api.post(`/api/v1/users/${id}/activate/`),
deactivate: async (id: number) =>
api.post(`/api/v1/users/${id}/deactivate/`),
bulkUpload: async (formData: FormData) =>
api.post('/api/v1/users/bulk-upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
}),
bulkResetTFA: async (userIds: number[]) =>
api.post('/api/v1/users/bulk/reset-tfa', { user_ids: userIds }),
bulkEnforceTFA: async (userIds: number[]) =>
api.post('/api/v1/users/bulk/enforce-tfa', { user_ids: userIds }),
bulkDisableTFA: async (userIds: number[]) =>
api.post('/api/v1/users/bulk/disable-tfa', { user_ids: userIds }),
};
export const resourcesApi = {
list: async (page = 1, limit = 20) =>
api.get('/api/v1/resources/', { params: { page, limit } }),
get: async (id: number) =>
api.get(`/api/v1/resources/${id}/`),
create: async (data: any) =>
api.post('/api/v1/resources/', data),
update: async (id: number, data: any) =>
api.put(`/api/v1/resources/${id}/`, data),
delete: async (id: number) =>
api.delete(`/api/v1/resources/${id}/`),
testConnection: async (id: number) =>
api.post(`/api/v1/resources/${id}/test/`),
};
export const monitoringApi = {
systemMetrics: async () =>
api.get('/api/v1/monitoring/system/'),
tenantMetrics: async (tenantId?: number) =>
api.get('/api/v1/monitoring/tenants/', { params: { tenant_id: tenantId } }),
usageStats: async (period = '24h', tenantId?: number) =>
api.get('/api/v1/monitoring/usage/', { params: { period, tenant_id: tenantId } }),
alerts: async (page = 1, limit = 20) =>
api.get('/api/v1/monitoring/alerts/', { params: { page, limit } }),
};
export const dashboardApi = {
getMetrics: async () =>
api.get('/api/v1/dashboard/metrics/'),
};
export const systemApi = {
health: async () =>
api.get('/health'),
healthDetailed: async () =>
api.get('/api/v1/system/health-detailed'),
info: async () =>
api.get('/api/v1/system/info/'),
config: async () =>
api.get('/api/v1/system/config/'),
updateConfig: async (data: any) =>
api.put('/api/v1/system/config/', data),
// Software Update Management
version: async () =>
api.get('/api/v1/system/version'),
checkUpdate: async () =>
api.get('/api/v1/system/check-update'),
validateUpdate: async (version: string) =>
api.post('/api/v1/system/validate-update', { target_version: version }),
startUpdate: async (version: string, createBackup = true) =>
api.post('/api/v1/system/update', { target_version: version, create_backup: createBackup }),
getUpdateStatus: async (updateId: string) =>
api.get(`/api/v1/system/update/${updateId}/status`),
rollback: async (updateId: string) =>
api.post(`/api/v1/system/update/${updateId}/rollback`),
// Backup Management
listBackups: async () =>
api.get('/api/v1/system/backups'),
createBackup: async (type = 'full') =>
api.post('/api/v1/system/backups', { backup_type: type }),
getBackup: async (backupId: string) =>
api.get(`/api/v1/system/backups/${backupId}`),
deleteBackup: async (backupId: string) =>
api.delete(`/api/v1/system/backups/${backupId}`),
restoreBackup: async (backupId: string) =>
api.post('/api/v1/system/restore', { backup_id: backupId }),
};
export const assistantLibraryApi = {
listTemplates: async (page = 1, limit = 20, category?: string, status?: string) =>
api.get('/api/v1/resource-management/templates/', { params: { page, limit, category, status } }),
getTemplate: async (id: string) =>
api.get(`/api/v1/resource-management/templates/${id}/`),
createTemplate: async (data: any) =>
api.post('/api/v1/resource-management/templates/', data),
updateTemplate: async (id: string, data: any) =>
api.put(`/api/v1/resource-management/templates/${id}/`, data),
deleteTemplate: async (id: string) =>
api.delete(`/api/v1/resource-management/templates/${id}/`),
deployTemplate: async (templateId: string, tenantIds: string[]) =>
api.post(`/api/v1/resource-management/templates/${templateId}/deploy/`, { tenant_ids: tenantIds }),
getDeployments: async (templateId?: string) =>
api.get('/api/v1/resource-management/deployments/', { params: { template_id: templateId } }),
listAccessGroups: async () =>
api.get('/api/v1/resource-management/access-groups/'),
createAccessGroup: async (data: any) =>
api.post('/api/v1/resource-management/access-groups/', data),
};
export const securityApi = {
getSecurityEvents: async (page = 1, limit = 20, severity?: string, timeRange?: string) =>
api.get('/api/v1/security/events/', { params: { page, limit, severity, time_range: timeRange } }),
getAccessLogs: async (page = 1, limit = 20, timeRange?: string) =>
api.get('/api/v1/security/access-logs/', { params: { page, limit, time_range: timeRange } }),
getSecurityPolicies: async () =>
api.get('/api/v1/security/policies/'),
updateSecurityPolicy: async (id: number, data: any) =>
api.put(`/api/v1/security/policies/${id}/`, data),
getSecurityMetrics: async () =>
api.get('/api/v1/security/metrics/'),
acknowledgeEvent: async (eventId: number) =>
api.post(`/api/v1/security/events/${eventId}/acknowledge/`),
exportSecurityReport: async (timeRange?: string) =>
api.get('/api/v1/security/export-report/', { params: { time_range: timeRange } }),
};
export const tfaApi = {
enable: async () =>
api.post('/api/v1/tfa/enable'),
verifySetup: async (code: string) =>
api.post('/api/v1/tfa/verify-setup', { code }),
disable: async (password: string) =>
api.post('/api/v1/tfa/disable', { password }),
verifyLogin: async (code: string) =>
api.post('/api/v1/tfa/verify-login', { code }, { withCredentials: true }),
getStatus: async () =>
api.get('/api/v1/tfa/status'),
getSessionData: async () =>
api.get('/api/v1/tfa/session-data', { withCredentials: true }),
getQRCodeBlob: async () =>
api.get('/api/v1/tfa/session-qr-code', {
responseType: 'blob',
withCredentials: true,
}),
};
export const apiKeysApi = {
// Get API key status for a tenant (without decryption)
getTenantKeys: async (tenantId: number) =>
api.get(`/api/v1/api-keys/tenant/${tenantId}`),
// Set or update an API key
setKey: async (data: {
tenant_id: number;
provider: string;
api_key: string;
api_secret?: string;
enabled?: boolean;
metadata?: Record<string, unknown>;
}) => api.post('/api/v1/api-keys/set', data),
// Test if an API key is valid
testKey: async (tenantId: number, provider: string) =>
api.post(`/api/v1/api-keys/test/${tenantId}/${provider}`),
// Disable an API key (keeps it but marks disabled)
disableKey: async (tenantId: number, provider: string) =>
api.put(`/api/v1/api-keys/disable/${tenantId}/${provider}`),
// Enable an API key
enableKey: async (tenantId: number, provider: string, apiKey: string) =>
api.post('/api/v1/api-keys/set', {
tenant_id: tenantId,
provider: provider,
api_key: apiKey,
enabled: true,
}),
// Remove an API key completely
removeKey: async (tenantId: number, provider: string) =>
api.delete(`/api/v1/api-keys/remove/${tenantId}/${provider}`),
// Get supported providers list
getProviders: async () =>
api.get('/api/v1/api-keys/providers'),
};
export default api;

View File

@@ -0,0 +1,37 @@
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useState } from 'react';
import { SessionMonitor } from '@/providers/session-monitor';
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
gcTime: 10 * 60 * 1000, // 10 minutes (renamed from cacheTime in v5)
retry: (failureCount, error: any) => {
// Don't retry on 401 (unauthorized) or 403 (forbidden)
if (error?.response?.status === 401 || error?.response?.status === 403) {
return false;
}
return failureCount < 2;
},
},
mutations: {
retry: false,
},
},
})
);
return (
<QueryClientProvider client={queryClient}>
<SessionMonitor>
{children}
</SessionMonitor>
</QueryClientProvider>
);
}

View File

@@ -0,0 +1,72 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export function formatDate(date: string | Date): string {
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
}).format(new Date(date))
}
export function formatNumber(num: number): string {
return new Intl.NumberFormat('en-US').format(num)
}
export function formatCurrency(cents: number): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(cents / 100)
}
export function formatBytes(bytes: number, decimals: number = 2): string {
if (bytes === 0) return '0 Bytes'
const k = 1024
const dm = decimals < 0 ? 0 : decimals
const sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
}
export function getStatusColor(status: string): string {
switch (status.toLowerCase()) {
case 'active':
return 'gt-status-active'
case 'pending':
return 'gt-status-pending'
case 'deploying':
return 'gt-status-deploying'
case 'suspended':
case 'terminated':
case 'failed':
return 'gt-status-suspended'
default:
return 'gt-status-pending'
}
}
export function truncate(str: string, length: number): string {
if (str.length <= length) return str
return str.slice(0, length) + '...'
}
export function 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)
}
}