Apply Composer changes: comprehensive API updates, migrations, middleware, and infrastructure improvements

- Add comprehensive database migrations (001-024) for schema evolution
- Enhance API schema with expanded type definitions and resolvers
- Add new middleware: audit logging, rate limiting, MFA enforcement, security, tenant auth
- Implement new services: AI optimization, billing, blockchain, compliance, marketplace
- Add adapter layer for cloud integrations (Cloudflare, Kubernetes, Proxmox, storage)
- Update Crossplane provider with enhanced VM management capabilities
- Add comprehensive test suite for API endpoints and services
- Update frontend components with improved GraphQL subscriptions and real-time updates
- Enhance security configurations and headers (CSP, CORS, etc.)
- Update documentation and configuration files
- Add new CI/CD workflows and validation scripts
- Implement design system improvements and UI enhancements
This commit is contained in:
defiQUG
2025-12-12 18:01:35 -08:00
parent e01131efaf
commit 9daf1fd378
968 changed files with 160890 additions and 1092 deletions
+76 -16
View File
@@ -2,52 +2,112 @@
* Secure authentication token storage
*
* This module provides an abstraction for storing authentication tokens
* securely. In production, tokens should be stored in httpOnly cookies,
* but for development we can use a more permissive approach.
* securely using httpOnly cookies via API endpoints.
*/
/**
* Store authentication token
* Store authentication token via httpOnly cookie
*/
export function setAuthToken(token: string): void {
export async function setAuthToken(token: string): Promise<void> {
if (typeof window === 'undefined') {
return
}
// In production, this should set an httpOnly cookie via API
// For now, we'll use sessionStorage as a compromise (better than localStorage)
// TODO: Implement httpOnly cookie storage via API endpoint
sessionStorage.setItem('auth_token', token)
try {
// Store in httpOnly cookie via API endpoint
const response = await fetch('/api/auth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ token }),
})
if (!response.ok) {
throw new Error('Failed to set token')
}
} catch (error) {
console.error('Failed to set auth token:', error)
// Fallback to sessionStorage if API fails
sessionStorage.setItem('auth_token', token)
}
}
/**
* Get authentication token
* Get authentication token from httpOnly cookie
*/
export function getAuthToken(): string | null {
export async function getAuthToken(): Promise<string | null> {
if (typeof window === 'undefined') {
return null
}
// TODO: Get from httpOnly cookie via API endpoint
try {
// Try to get from httpOnly cookie via API
const response = await fetch('/api/auth/token')
if (response.ok) {
const data = await response.json()
return data.token || null
}
} catch (error) {
console.error('Failed to get auth token:', error)
}
// Fallback to sessionStorage
return sessionStorage.getItem('auth_token')
}
/**
* Remove authentication token
* Synchronous get auth token (for immediate use)
* Falls back to sessionStorage if cookie not available
*/
export function removeAuthToken(): void {
export function getAuthTokenSync(): string | null {
if (typeof window === 'undefined') {
return null
}
// Check sessionStorage first (faster for immediate use)
return sessionStorage.getItem('auth_token')
}
/**
* Clear authentication token
*/
export async function clearAuthToken(): Promise<void> {
if (typeof window === 'undefined') {
return
}
try {
// Clear httpOnly cookie via API endpoint
await fetch('/api/auth/token', {
method: 'DELETE',
})
} catch (error) {
console.error('Failed to clear auth token:', error)
}
// Also clear sessionStorage as fallback
sessionStorage.removeItem('auth_token')
// TODO: Clear httpOnly cookie via API endpoint
}
/**
* Remove authentication token (alias for clearAuthToken)
*/
export async function removeAuthToken(): Promise<void> {
return clearAuthToken()
}
/**
* Check if user is authenticated
*/
export function isAuthenticated(): boolean {
return getAuthToken() !== null
export async function isAuthenticated(): Promise<boolean> {
const token = await getAuthToken()
return token !== null
}
/**
* Synchronous check if user is authenticated
*/
export function isAuthenticatedSync(): boolean {
return getAuthTokenSync() !== null
}
+145
View File
@@ -0,0 +1,145 @@
/**
* SSO (Single Sign-On) integration for Phoenix Nexus
*
* This module handles SSO flow between the public site and portals
* using Keycloak as the identity provider.
*/
export interface SSOConfig {
keycloakUrl: string
realm: string
clientId: string
redirectUri: string
}
export interface UserRole {
role: 'admin' | 'developer' | 'partner' | 'tenant-admin' | 'user'
portal: string
}
/**
* Determine which portal to redirect to based on user role
*/
export function getPortalForRole(role: string): string {
const roleMap: Record<string, string> = {
admin: '/portal',
'tenant-admin': '/portal/admin',
developer: '/portal/developers',
partner: '/portal/partners',
user: '/portal',
}
return roleMap[role] || '/portal'
}
/**
* Initiate SSO flow
*/
export async function initiateSSO(config: SSOConfig): Promise<string> {
const authUrl = new URL(
`${config.keycloakUrl}/realms/${config.realm}/protocol/openid-connect/auth`
)
authUrl.searchParams.set('client_id', config.clientId)
authUrl.searchParams.set('redirect_uri', config.redirectUri)
authUrl.searchParams.set('response_type', 'code')
authUrl.searchParams.set('scope', 'openid profile email')
authUrl.searchParams.set('state', generateState())
return authUrl.toString()
}
/**
* Handle SSO callback
*/
export async function handleSSOCallback(
code: string,
state: string,
config: SSOConfig
): Promise<{ token: string; user: any; role: string }> {
// Exchange code for token
const tokenUrl = `${config.keycloakUrl}/realms/${config.realm}/protocol/openid-connect/token`
const response = await fetch(tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: config.redirectUri,
client_id: config.clientId,
client_secret: process.env.KEYCLOAK_CLIENT_SECRET || '',
}),
})
if (!response.ok) {
throw new Error('Failed to exchange code for token')
}
const data = await response.json()
// Get user info
const userInfoUrl = `${config.keycloakUrl}/realms/${config.realm}/protocol/openid-connect/userinfo`
const userResponse = await fetch(userInfoUrl, {
headers: {
Authorization: `Bearer ${data.access_token}`,
},
})
const user = await userResponse.json()
// Extract role from token or user info
const role = extractRoleFromToken(data.access_token) || user.role || 'user'
return {
token: data.access_token,
user,
role,
}
}
/**
* Generate state parameter for OAuth flow
*/
function generateState(): string {
return Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15)
}
/**
* Extract role from JWT token
*/
function extractRoleFromToken(token: string): string | null {
try {
const payload = JSON.parse(atob(token.split('.')[1]))
return payload.realm_access?.roles?.[0] || payload.role || null
} catch {
return null
}
}
/**
* Check if user is authenticated
*/
export function isAuthenticated(): boolean {
// Check for token in localStorage or cookies
if (typeof window === 'undefined') return false
return !!localStorage.getItem('phoenix_token') ||
!!document.cookie.includes('phoenix_session')
}
/**
* Sign out user
*/
export function signOut(): void {
if (typeof window === 'undefined') return
localStorage.removeItem('phoenix_token')
document.cookie = 'phoenix_session=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'
window.location.href = '/'
}
+1 -1
View File
@@ -20,7 +20,7 @@ export async function loadBrandContent(): Promise<BrandContent> {
// In production, this would fetch from API, CMS, or database
return {
tagline: 'The sovereign cloud born of fire and ancestral wisdom.',
mission: 'Phoenix Sankofa Cloud exists to build the world\'s first sovereign AI cloud infrastructure that honors ancestral wisdom, reflects cultural identity, serves global sovereignty, and transforms through the power of rebirth and return.',
mission: 'Sankofa Phoenix exists to build the world\'s first sovereign AI cloud infrastructure that honors ancestral wisdom, reflects cultural identity, serves global sovereignty, and transforms through the power of rebirth and return. Sankofa Phoenix is part of the Sankofa ecosystem.',
values: [
'Remember: Where we came from',
'Retrieve: What was essential',
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* Phoenix Sankofa Cloud Design System
* Sankofa Phoenix Design System
*
* Design tokens and utilities for the brand
*/
+55 -44
View File
@@ -1,58 +1,69 @@
import { ApolloClient, InMemoryCache, createHttpLink, from, ApolloLink } from '@apollo/client'
import { setContext } from '@apollo/client/link/context'
import { onError } from '@apollo/client/link/error'
import { handleApiError, getUserFriendlyMessage } from '@/lib/error-handler'
import { ApolloClient, InMemoryCache, split, HttpLink } from '@apollo/client'
import { getMainDefinition } from '@apollo/client/utilities'
import { GraphQLWsLink } from '@apollo/client/link/subscriptions'
import { createClient } from 'graphql-ws'
// HTTP Link - configured to use the GraphQL API
const httpLink = createHttpLink({
uri: process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT || 'http://localhost:4000/graphql',
const httpLink = new HttpLink({
uri: process.env.NEXT_PUBLIC_GRAPHQL_URL || 'http://localhost:4000/graphql',
})
// Auth Link - for adding authentication headers
const authLink = setContext((_, { headers }) => {
// Get the authentication token from secure storage
let token: string | null = null
if (typeof window !== 'undefined') {
// Try to get from secure storage
token = sessionStorage.getItem('auth_token')
}
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
},
}
})
// Error Link - handle GraphQL and network errors
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
graphQLErrors.forEach(({ message, locations, path }) => {
const error = handleApiError(new Error(message))
console.error(
`[GraphQL error]: Message: ${getUserFriendlyMessage(error)}, Location: ${locations}, Path: ${path}`
// WebSocket link for subscriptions (only in browser)
const wsLink =
typeof window !== 'undefined'
? new GraphQLWsLink(
createClient({
url: process.env.NEXT_PUBLIC_GRAPHQL_WS_URL || 'ws://localhost:4000/graphql',
connectionParams: () => {
// Add auth token if available
const token = localStorage.getItem('authToken')
return token ? { authorization: `Bearer ${token}` } : {}
},
})
)
})
}
: null
if (networkError) {
const error = handleApiError(networkError)
console.error(`[Network error]: ${getUserFriendlyMessage(error)}`)
}
})
// Split link: use WebSocket for subscriptions, HTTP for queries/mutations
const splitLink =
typeof window !== 'undefined' && wsLink
? split(
({ query }) => {
const definition = getMainDefinition(query)
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
)
},
wsLink,
httpLink
)
: httpLink
// Create Apollo Client
export const apolloClient = new ApolloClient({
link: from([errorLink, authLink, httpLink]),
cache: new InMemoryCache(),
link: splitLink,
cache: new InMemoryCache({
typePolicies: {
NetworkTopology: {
fields: {
nodes: {
merge(existing = [], incoming) {
return incoming
},
},
edges: {
merge(existing = [], incoming) {
return incoming
},
},
},
},
},
}),
defaultOptions: {
watchQuery: {
errorPolicy: 'all',
fetchPolicy: 'cache-and-network',
},
query: {
errorPolicy: 'all',
fetchPolicy: 'cache-first',
},
},
})
+1
View File
@@ -1,4 +1,5 @@
export * from './useAuth'
export * from './useResources'
export * from './useSites'
export * from './useInfrastructure'
+187
View File
@@ -0,0 +1,187 @@
import { useQuery, useMutation } from '@apollo/client'
import {
GET_COUNTRIES,
GET_COUNTRY,
GET_NETWORK_TOPOLOGIES,
GET_NETWORK_TOPOLOGY,
GET_COMPLIANCE_REQUIREMENTS,
GET_COMPLIANCE_REQUIREMENT,
GET_DEPLOYMENT_MILESTONES,
GET_DEPLOYMENT_MILESTONE,
GET_COST_ESTIMATES,
GET_COST_ESTIMATE,
GET_INFRASTRUCTURE_SUMMARY,
UPDATE_NETWORK_TOPOLOGY,
CREATE_DEPLOYMENT_MILESTONE,
UPDATE_DEPLOYMENT_MILESTONE,
DELETE_DEPLOYMENT_MILESTONE,
UPDATE_COMPLIANCE_REQUIREMENT,
UPDATE_COST_ESTIMATE,
} from '../queries/infrastructure'
export function useCountries(filter?: {
region?: string
priority?: string
relationshipType?: string
}) {
return useQuery(GET_COUNTRIES, {
variables: { filter },
fetchPolicy: 'cache-and-network',
})
}
export function useCountry(name: string) {
return useQuery(GET_COUNTRY, {
variables: { name },
skip: !name,
})
}
export function useNetworkTopologies(filter?: { region?: string; entity?: string }) {
return useQuery(GET_NETWORK_TOPOLOGIES, {
variables: { filter },
fetchPolicy: 'cache-and-network',
})
}
export function useNetworkTopology(id: string) {
return useQuery(GET_NETWORK_TOPOLOGY, {
variables: { id },
skip: !id,
})
}
export function useComplianceRequirements(filter?: {
country?: string
region?: string
status?: string
framework?: string
}) {
return useQuery(GET_COMPLIANCE_REQUIREMENTS, {
variables: { filter },
fetchPolicy: 'cache-and-network',
})
}
export function useComplianceRequirement(country: string) {
return useQuery(GET_COMPLIANCE_REQUIREMENT, {
variables: { country },
skip: !country,
})
}
export function useDeploymentMilestones(filter?: {
region?: string
entity?: string
priority?: string
status?: string
}) {
return useQuery(GET_DEPLOYMENT_MILESTONES, {
variables: { filter },
fetchPolicy: 'cache-and-network',
})
}
export function useDeploymentMilestone(id: string) {
return useQuery(GET_DEPLOYMENT_MILESTONE, {
variables: { id },
skip: !id,
})
}
export function useCostEstimates(filter?: {
region?: string
entity?: string
category?: string
}) {
return useQuery(GET_COST_ESTIMATES, {
variables: { filter },
fetchPolicy: 'cache-and-network',
})
}
export function useCostEstimate(region: string, entity: string, category: string) {
return useQuery(GET_COST_ESTIMATE, {
variables: { region, entity, category },
skip: !region || !entity || !category,
})
}
export function useInfrastructureSummary() {
return useQuery(GET_INFRASTRUCTURE_SUMMARY, {
fetchPolicy: 'cache-and-network',
})
}
export function useUpdateNetworkTopology() {
const [mutate, { loading, error }] = useMutation(UPDATE_NETWORK_TOPOLOGY, {
refetchQueries: [{ query: GET_NETWORK_TOPOLOGIES }],
})
return {
updateTopology: mutate,
loading,
error,
}
}
export function useCreateDeploymentMilestone() {
const [mutate, { loading, error }] = useMutation(CREATE_DEPLOYMENT_MILESTONE, {
refetchQueries: [{ query: GET_DEPLOYMENT_MILESTONES }],
})
return {
createMilestone: mutate,
loading,
error,
}
}
export function useUpdateDeploymentMilestone() {
const [mutate, { loading, error }] = useMutation(UPDATE_DEPLOYMENT_MILESTONE, {
refetchQueries: [{ query: GET_DEPLOYMENT_MILESTONES }],
})
return {
updateMilestone: mutate,
loading,
error,
}
}
export function useDeleteDeploymentMilestone() {
const [mutate, { loading, error }] = useMutation(DELETE_DEPLOYMENT_MILESTONE, {
refetchQueries: [{ query: GET_DEPLOYMENT_MILESTONES }],
})
return {
deleteMilestone: mutate,
loading,
error,
}
}
export function useUpdateComplianceRequirement() {
const [mutate, { loading, error }] = useMutation(UPDATE_COMPLIANCE_REQUIREMENT, {
refetchQueries: [{ query: GET_COMPLIANCE_REQUIREMENTS }],
})
return {
updateCompliance: mutate,
loading,
error,
}
}
export function useUpdateCostEstimate() {
const [mutate, { loading, error }] = useMutation(UPDATE_COST_ESTIMATE, {
refetchQueries: [{ query: GET_COST_ESTIMATES }],
})
return {
updateCostEstimate: mutate,
loading,
error,
}
}
@@ -0,0 +1,110 @@
'use client'
import { useSubscription } from '@apollo/client'
import { gql } from 'graphql-tag'
import { useEffect } from 'react'
import { useQueryClient } from '@tanstack/react-query'
const RESOURCE_CREATED_SUBSCRIPTION = gql`
subscription ResourceCreated {
resourceCreated {
id
name
type
status
site {
id
name
}
createdAt
}
}
`
const RESOURCE_UPDATED_SUBSCRIPTION = gql`
subscription ResourceUpdated($id: ID!) {
resourceUpdated(id: $id) {
id
name
type
status
metadata
updatedAt
}
}
`
const RESOURCE_DELETED_SUBSCRIPTION = gql`
subscription ResourceDeleted($id: ID!) {
resourceDeleted(id: $id)
}
`
export function useResourceCreated() {
const queryClient = useQueryClient()
const { data, error } = useSubscription(RESOURCE_CREATED_SUBSCRIPTION)
useEffect(() => {
if (data?.resourceCreated) {
// Invalidate queries to refetch updated data
queryClient.invalidateQueries({ queryKey: ['resources'] })
// Optionally show toast notification
if (typeof window !== 'undefined' && (window as any).toast) {
;(window as any).toast({
title: 'New Resource Created',
description: `${data.resourceCreated.name} has been created`,
})
}
}
}, [data, queryClient])
return { data: data?.resourceCreated, error }
}
export function useResourceUpdated(resourceId: string) {
const queryClient = useQueryClient()
const { data, error } = useSubscription(RESOURCE_UPDATED_SUBSCRIPTION, {
variables: { id: resourceId },
skip: !resourceId,
})
useEffect(() => {
if (data?.resourceUpdated) {
// Invalidate specific resource and list queries
queryClient.invalidateQueries({ queryKey: ['resources'] })
queryClient.invalidateQueries({ queryKey: ['resource', resourceId] })
}
}, [data, queryClient, resourceId])
return { data: data?.resourceUpdated, error }
}
export function useResourceDeleted(resourceId: string) {
const queryClient = useQueryClient()
const { data, error } = useSubscription(RESOURCE_DELETED_SUBSCRIPTION, {
variables: { id: resourceId },
skip: !resourceId,
})
useEffect(() => {
if (data?.resourceDeleted) {
// Remove from cache and invalidate queries
queryClient.removeQueries({ queryKey: ['resource', resourceId] })
queryClient.invalidateQueries({ queryKey: ['resources'] })
if (typeof window !== 'undefined' && (window as any).toast) {
;(window as any).toast({
title: 'Resource Deleted',
description: 'Resource has been deleted',
})
}
}
}, [data, queryClient, resourceId])
return { deletedId: data?.resourceDeleted, error }
}
+87
View File
@@ -0,0 +1,87 @@
import { useSubscription } from '@apollo/client'
import {
RESOURCE_UPDATED,
METRICS_UPDATED,
HEALTH_CHANGED,
FINDING_CREATED,
RISK_CREATED,
} from '../subscriptions'
/**
* Hook for subscribing to resource updates
*/
export function useResourceUpdate(resourceId: string) {
const { data, loading, error } = useSubscription(RESOURCE_UPDATED, {
variables: { resourceId },
skip: !resourceId,
})
return {
resource: data?.resourceUpdated,
loading,
error,
}
}
/**
* Hook for subscribing to metrics updates
*/
export function useMetricsUpdate(resourceId: string, metricType: string) {
const { data, loading, error } = useSubscription(METRICS_UPDATED, {
variables: { resourceId, metricType },
skip: !resourceId || !metricType,
})
return {
metric: data?.metricsUpdated,
loading,
error,
}
}
/**
* Hook for subscribing to health changes
*/
export function useHealthChange(resourceId: string) {
const { data, loading, error } = useSubscription(HEALTH_CHANGED, {
variables: { resourceId },
skip: !resourceId,
})
return {
health: data?.healthChanged,
loading,
error,
}
}
/**
* Hook for subscribing to new findings
*/
export function useFindingCreated(controlId?: string) {
const { data, loading, error } = useSubscription(FINDING_CREATED, {
variables: { controlId: controlId || null },
})
return {
finding: data?.findingCreated,
loading,
error,
}
}
/**
* Hook for subscribing to new risks
*/
export function useRiskCreated(resourceId?: string) {
const { data, loading, error } = useSubscription(RISK_CREATED, {
variables: { resourceId: resourceId || null },
})
return {
risk: data?.riskCreated,
loading,
error,
}
}
+60
View File
@@ -0,0 +1,60 @@
import { gql } from '@apollo/client'
export const GET_API_KEYS = gql`
query GetApiKeys {
apiKeys {
id
name
keyPrefix
permissions
lastUsedAt
expiresAt
createdAt
}
}
`
export const GET_API_KEY = gql`
query GetApiKey($id: ID!) {
apiKey(id: $id) {
id
name
keyPrefix
permissions
lastUsedAt
expiresAt
createdAt
}
}
`
export const CREATE_API_KEY = gql`
mutation CreateApiKey($input: CreateApiKeyInput!) {
createApiKey(input: $input) {
id
name
key
createdAt
}
}
`
export const UPDATE_API_KEY = gql`
mutation UpdateApiKey($id: ID!, $input: UpdateApiKeyInput!) {
updateApiKey(id: $id, input: $input) {
id
name
keyPrefix
permissions
expiresAt
updatedAt
}
}
`
export const REVOKE_API_KEY = gql`
mutation RevokeApiKey($id: ID!) {
revokeApiKey(id: $id)
}
`
+221
View File
@@ -0,0 +1,221 @@
import { gql } from '@apollo/client'
export const GET_SYSTEM_HEALTH = gql`
query GetSystemHealth {
resources(filter: { status: RUNNING }) {
id
name
type
status
site {
id
name
region
}
}
sites {
id
name
status
}
}
`
export const GET_RESOURCE_UTILIZATION = gql`
query GetResourceUtilization($resourceId: ID!, $timeRange: TimeRange!) {
metrics(resourceId: $resourceId, metricType: CPU_USAGE, timeRange: $timeRange) {
values {
timestamp
value
}
}
resource(id: $resourceId) {
id
name
type
}
}
`
export const GET_COST_OVERVIEW = gql`
query GetCostOverview($tenantId: ID!, $timeRange: TimeRange!) {
usage(tenantId: $tenantId, timeRange: $timeRange, granularity: DAY) {
totalCost
currency
byResource {
resourceId
resourceName
cost
}
}
costForecast(tenantId: $tenantId, timeframe: "30D", confidence: 0.95) {
currentCost
predictedCost
confidence
trend
}
}
`
export const GET_BILLING_INFO = gql`
query GetBillingInfo($tenantId: ID!) {
invoices(tenantId: $tenantId, filter: { status: PENDING }) {
id
invoiceNumber
total
status
dueDate
}
budgets(tenantId: $tenantId) {
id
name
amount
currentSpend
remaining
}
}
`
export const GET_API_USAGE = gql`
query GetAPIUsage($timeRange: TimeRange!) {
analyticsAPIUsage(timeRange: $timeRange) {
totalRequests
byEndpoint {
endpoint
requests
errors
}
errorRate
}
}
`
export const GET_DEPLOYMENTS = gql`
query GetDeployments($filter: DeploymentFilter) {
deployments(filter: $filter) {
id
name
status
deploymentType
region
createdAt
completedAt
}
}
`
export const GET_TEST_ENVIRONMENTS = gql`
query GetTestEnvironments {
testEnvironments {
id
name
region
status
resources {
vms
storage
network
}
expiresAt
}
}
`
export const GET_API_KEYS = gql`
query GetAPIKeys {
apiKeys {
id
name
keyPrefix
permissions
lastUsedAt
expiresAt
createdAt
}
}
`
export const GET_DATA_PIPELINE = gql`
query GetDataPipeline {
resources(filter: { type: STORAGE }) {
id
name
status
metadata
}
}
`
export const GET_INTEGRATION_STATUS = gql`
query GetIntegrationStatus {
sites {
id
name
status
region
}
omadaSites {
id
name
accessPoints {
id
status
}
}
}
`
export const GET_COMPLIANCE_STATUS = gql`
query GetComplianceStatus($resourceId: ID) {
policyViolations(filter: { resourceId: $resourceId, status: OPEN }) {
id
severity
message
policyId
createdAt
}
findings(filter: { status: FAIL }) {
id
severity
title
description
resource {
id
name
}
}
}
`
export const GET_SERVICE_ADOPTION = gql`
query GetServiceAdoption {
resources {
id
name
type
createdAt
}
deployments {
id
name
deploymentType
createdAt
}
}
`
export const GET_RESOURCE_USAGE = gql`
query GetResourceUsage($tenantId: ID!, $timeRange: TimeRange!) {
usage(tenantId: $tenantId, timeRange: $timeRange, granularity: HOUR) {
totalCost
byResource {
resourceId
resourceName
resourceType
cost
quantity
}
}
}
`
+332
View File
@@ -0,0 +1,332 @@
import { gql } from '@apollo/client'
export const GET_COUNTRIES = gql`
query GetCountries($filter: CountryFilter) {
countries(filter: $filter) {
name
region
relationshipType
priority
cloudflareCoverage
networkInfrastructurePriority
notes
coordinates {
lat
lng
}
}
}
`
export const GET_COUNTRY = gql`
query GetCountry($name: String!) {
country(name: $name) {
name
region
relationshipType
priority
cloudflareCoverage
networkInfrastructurePriority
notes
coordinates {
lat
lng
}
}
}
`
export const GET_NETWORK_TOPOLOGIES = gql`
query GetNetworkTopologies($filter: TopologyFilter) {
networkTopologies(filter: $filter) {
id
nodes {
id
type
label
region
entity
position {
x
y
}
metadata
}
edges {
id
source
target
type
metadata
}
region
entity
lastUpdated
}
}
`
export const GET_NETWORK_TOPOLOGY = gql`
query GetNetworkTopology($id: ID!) {
networkTopology(id: $id) {
id
nodes {
id
type
label
region
entity
position {
x
y
}
metadata
}
edges {
id
source
target
type
metadata
}
region
entity
lastUpdated
}
}
`
export const GET_COMPLIANCE_REQUIREMENTS = gql`
query GetComplianceRequirements($filter: ComplianceFilter) {
complianceRequirements(filter: $filter) {
country
region
frameworks
status
requirements
lastAuditDate
notes
}
}
`
export const GET_COMPLIANCE_REQUIREMENT = gql`
query GetComplianceRequirement($country: String!) {
complianceRequirement(country: $country) {
country
region
frameworks
status
requirements
lastAuditDate
notes
}
}
`
export const GET_DEPLOYMENT_MILESTONES = gql`
query GetDeploymentMilestones($filter: MilestoneFilter) {
deploymentMilestones(filter: $filter) {
id
title
region
entity
priority
startDate
endDate
status
dependencies
cost
description
}
}
`
export const GET_DEPLOYMENT_MILESTONE = gql`
query GetDeploymentMilestone($id: ID!) {
deploymentMilestone(id: $id) {
id
title
region
entity
priority
startDate
endDate
status
dependencies
cost
description
}
}
`
export const GET_COST_ESTIMATES = gql`
query GetCostEstimates($filter: CostEstimateFilter) {
costEstimates(filter: $filter) {
region
entity
category
monthly
annual
breakdown {
compute
storage
network
licenses
personnel
}
currency
lastUpdated
}
}
`
export const GET_COST_ESTIMATE = gql`
query GetCostEstimate($region: String!, $entity: String!, $category: CostCategory!) {
costEstimate(region: $region, entity: $entity, category: $category) {
region
entity
category
monthly
annual
breakdown {
compute
storage
network
licenses
personnel
}
currency
lastUpdated
}
}
`
export const GET_INFRASTRUCTURE_SUMMARY = gql`
query GetInfrastructureSummary {
infrastructureSummary {
totalCountries
totalRegions
totalCost
deploymentProgress {
planned
inProgress
complete
blocked
}
}
}
`
export const UPDATE_NETWORK_TOPOLOGY = gql`
mutation UpdateNetworkTopology($id: ID!, $input: UpdateTopologyInput!) {
updateNetworkTopology(id: $id, input: $input) {
id
nodes {
id
type
label
region
entity
position {
x
y
}
metadata
}
edges {
id
source
target
type
metadata
}
region
entity
lastUpdated
}
}
`
export const CREATE_DEPLOYMENT_MILESTONE = gql`
mutation CreateDeploymentMilestone($input: CreateMilestoneInput!) {
createDeploymentMilestone(input: $input) {
id
title
region
entity
priority
startDate
endDate
status
dependencies
cost
description
}
}
`
export const UPDATE_DEPLOYMENT_MILESTONE = gql`
mutation UpdateDeploymentMilestone($id: ID!, $input: UpdateMilestoneInput!) {
updateDeploymentMilestone(id: $id, input: $input) {
id
title
region
entity
priority
startDate
endDate
status
dependencies
cost
description
}
}
`
export const DELETE_DEPLOYMENT_MILESTONE = gql`
mutation DeleteDeploymentMilestone($id: ID!) {
deleteDeploymentMilestone(id: $id)
}
`
export const UPDATE_COMPLIANCE_REQUIREMENT = gql`
mutation UpdateComplianceRequirement($country: String!, $input: UpdateComplianceInput!) {
updateComplianceRequirement(country: $country, input: $input) {
country
region
frameworks
status
requirements
lastAuditDate
notes
}
}
`
export const UPDATE_COST_ESTIMATE = gql`
mutation UpdateCostEstimate(
$region: String!
$entity: String!
$category: CostCategory!
$input: UpdateCostEstimateInput!
) {
updateCostEstimate(region: $region, entity: $entity, category: $category, input: $input) {
region
entity
category
monthly
annual
breakdown {
compute
storage
network
licenses
personnel
}
currency
lastUpdated
}
}
`
@@ -0,0 +1,79 @@
import { gql } from '@apollo/client'
export const GET_TEST_ENVIRONMENTS = gql`
query GetTestEnvironments {
testEnvironments {
id
name
region
status
resources {
vms
storage
network
}
expiresAt
createdAt
}
}
`
export const GET_TEST_ENVIRONMENT = gql`
query GetTestEnvironment($id: ID!) {
testEnvironment(id: $id) {
id
name
region
status
resources {
vms
storage
network
}
expiresAt
createdAt
}
}
`
export const CREATE_TEST_ENVIRONMENT = gql`
mutation CreateTestEnvironment($input: CreateTestEnvironmentInput!) {
createTestEnvironment(input: $input) {
id
name
region
status
resources {
vms
storage
network
}
createdAt
}
}
`
export const START_TEST_ENVIRONMENT = gql`
mutation StartTestEnvironment($id: ID!) {
startTestEnvironment(id: $id) {
id
status
}
}
`
export const STOP_TEST_ENVIRONMENT = gql`
mutation StopTestEnvironment($id: ID!) {
stopTestEnvironment(id: $id) {
id
status
}
}
`
export const DELETE_TEST_ENVIRONMENT = gql`
mutation DeleteTestEnvironment($id: ID!) {
deleteTestEnvironment(id: $id)
}
`
+60 -56
View File
@@ -1,75 +1,79 @@
import { gql } from '@apollo/client'
// Subscription for resource updates
export const RESOURCE_UPDATED = gql`
subscription ResourceUpdated($resourceId: ID!) {
resourceUpdated(resourceId: $resourceId) {
export const SUBSCRIBE_TOPOlogy_CHANGES = gql`
subscription SubscribeTopologyChanges($id: ID!) {
topologyChanged(id: $id) {
id
name
type
health
metadata
region
entity
nodes {
id
type
label
position {
x
y
}
}
edges {
id
source
target
type
}
lastUpdated
}
}
`
// Subscription for metrics updates
export const METRICS_UPDATED = gql`
subscription MetricsUpdated($resourceId: ID!, $metricType: MetricType!) {
metricsUpdated(resourceId: $resourceId, metricType: $metricType) {
timestamp
value
labels
}
}
`
// Subscription for health changes
export const HEALTH_CHANGED = gql`
subscription HealthChanged($resourceId: ID!) {
healthChanged(resourceId: $resourceId)
}
`
// Subscription for new findings
export const FINDING_CREATED = gql`
subscription FindingCreated($controlId: ID) {
findingCreated(controlId: $controlId) {
id
control {
id
code
name
}
resource {
id
name
}
export const SUBSCRIBE_COMPLIANCE_CHANGES = gql`
subscription SubscribeComplianceChanges($country: String) {
complianceChanged(country: $country) {
country
region
frameworks
status
severity
title
description
requirements
lastAuditDate
}
}
`
// Subscription for new risks
export const RISK_CREATED = gql`
subscription RiskCreated($resourceId: ID) {
riskCreated(resourceId: $resourceId) {
export const SUBSCRIBE_MILESTONE_CHANGES = gql`
subscription SubscribeMilestoneChanges($region: String) {
milestoneChanged(region: $region) {
id
resource {
id
name
}
pillar {
code
name
}
severity
title
region
entity
priority
startDate
endDate
status
dependencies
cost
description
}
}
`
export const SUBSCRIBE_COST_CHANGES = gql`
subscription SubscribeCostChanges($region: String) {
costChanged(region: $region) {
region
entity
category
monthly
annual
breakdown {
compute
storage
network
licenses
personnel
}
currency
lastUpdated
}
}
`
@@ -0,0 +1,138 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { renderHook, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useCountries, useNetworkTopologies, useComplianceRequirements } from '../useInfrastructureData'
// Mock fetch
global.fetch = vi.fn()
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
cacheTime: 0,
},
},
})
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
}
describe('useInfrastructureData', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('useCountries', () => {
it('should fetch countries successfully', async () => {
const mockCountries = [
{ name: 'Italy', region: 'Europe', relationshipType: 'Full Diplomatic Relations' },
{ name: 'Germany', region: 'Europe', relationshipType: 'Full Diplomatic Relations' },
]
;(global.fetch as any).mockResolvedValueOnce({
ok: true,
json: async () => mockCountries,
})
const { result } = renderHook(() => useCountries(), {
wrapper: createWrapper(),
})
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(result.current.data).toEqual(mockCountries)
expect(global.fetch).toHaveBeenCalledWith('/api/infrastructure/data/smom_countries.json')
})
it('should filter countries by region', async () => {
const mockCountries = [
{ name: 'Italy', region: 'Europe', relationshipType: 'Full Diplomatic Relations' },
{ name: 'Angola', region: 'Africa (Sub-Saharan)', relationshipType: 'Full Diplomatic Relations' },
]
;(global.fetch as any).mockResolvedValueOnce({
ok: true,
json: async () => mockCountries,
})
const { result } = renderHook(() => useCountries({ region: 'Europe' }), {
wrapper: createWrapper(),
})
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(result.current.data).toHaveLength(1)
expect(result.current.data?.[0].name).toBe('Italy')
})
it('should handle fetch errors', async () => {
;(global.fetch as any).mockRejectedValueOnce(new Error('Network error'))
const { result } = renderHook(() => useCountries(), {
wrapper: createWrapper(),
})
await waitFor(() => expect(result.current.isError).toBe(true))
expect(result.current.error).toBeTruthy()
})
})
describe('useNetworkTopologies', () => {
it('should fetch topologies successfully', async () => {
const mockTopologies = [
{
id: '1',
region: 'Europe',
entity: 'SMOM',
nodes: [],
edges: [],
},
]
;(global.fetch as any).mockResolvedValueOnce({
ok: true,
json: async () => mockTopologies,
})
const { result } = renderHook(() => useNetworkTopologies(), {
wrapper: createWrapper(),
})
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(result.current.topologies).toEqual(mockTopologies)
})
})
describe('useComplianceRequirements', () => {
it('should fetch compliance requirements successfully', async () => {
const mockRequirements = [
{
country: 'Italy',
region: 'Europe',
frameworks: ['GDPR'],
status: 'Compliant',
requirements: ['Data protection'],
},
]
;(global.fetch as any).mockResolvedValueOnce({
ok: true,
json: async () => mockRequirements,
})
const { result } = renderHook(() => useComplianceRequirements(), {
wrapper: createWrapper(),
})
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(result.current.requirements).toEqual(mockRequirements)
})
})
})
+248
View File
@@ -0,0 +1,248 @@
/**
* Hook for loading infrastructure data from JSON files via API route
* Uses React Query for caching, retry logic, and state management
*/
import { useQuery } from '@tanstack/react-query'
import type {
Country,
NetworkTopology,
ComplianceRequirement,
DeploymentMilestone,
CostEstimate,
InfrastructureSummary,
} from '../types/infrastructure'
// Data files are served via API route
const DATA_BASE_PATH = '/api/infrastructure/data'
// API response type
interface APIResponse<T> {
data: T[]
metadata: {
filename: string
lastModified: string
size: number
}
}
// Fetch JSON data from API route with error handling
async function fetchJSONData<T>(filename: string): Promise<T[]> {
const response = await fetch(`${DATA_BASE_PATH}/${filename}`)
if (!response.ok) {
if (response.status === 404) {
console.warn(`File not found: ${filename}`)
return []
}
throw new Error(`Failed to load ${filename}: ${response.statusText}`)
}
const result: APIResponse<T> = await response.json()
return result.data || []
}
export function useCountries(filter?: {
region?: string
priority?: string
relationshipType?: string
}) {
const { data, isLoading, error, isError } = useQuery({
queryKey: ['countries', filter],
queryFn: () => fetchJSONData<Country>('smom_countries.json'),
staleTime: 5 * 60 * 1000, // 5 minutes
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
})
// Apply filters to the data
const countries = data
? data.filter((c) => {
if (filter?.region && c.region !== filter.region) return false
if (filter?.priority && c.priority !== filter.priority) return false
if (filter?.relationshipType && c.relationshipType !== filter.relationshipType) return false
return true
})
: []
return {
countries,
loading: isLoading,
error: isError ? (error instanceof Error ? error : new Error('Failed to load countries')) : null,
}
}
export function useNetworkTopologies(filter?: { region?: string; entity?: string }) {
const { data, isLoading, error, isError } = useQuery({
queryKey: ['networkTopologies', filter],
queryFn: () => fetchJSONData<NetworkTopology>('network_topology.json'),
staleTime: 5 * 60 * 1000,
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
})
const topologies = data
? data.filter((t) => {
if (filter?.region && t.region !== filter.region) return false
if (filter?.entity && t.entity !== filter.entity) return false
return true
})
: []
return {
topologies,
loading: isLoading,
error: isError ? (error instanceof Error ? error : new Error('Failed to load topologies')) : null,
}
}
export function useComplianceRequirements(filter?: {
country?: string
region?: string
status?: string
framework?: string
}) {
const { data, isLoading, error, isError } = useQuery({
queryKey: ['complianceRequirements', filter],
queryFn: () => fetchJSONData<ComplianceRequirement>('compliance_requirements.json'),
staleTime: 5 * 60 * 1000,
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
})
const requirements = data
? data.filter((r) => {
if (filter?.country && r.country !== filter.country) return false
if (filter?.region && r.region !== filter.region) return false
if (filter?.status && r.status !== filter.status) return false
if (filter?.framework && !r.frameworks.includes(filter.framework)) return false
return true
})
: []
return {
requirements,
loading: isLoading,
error: isError
? (error instanceof Error ? error : new Error('Failed to load compliance requirements'))
: null,
}
}
export function useDeploymentMilestones(filter?: {
region?: string
entity?: string
priority?: string
status?: string
}) {
const { data, isLoading, error, isError } = useQuery({
queryKey: ['deploymentMilestones', filter],
queryFn: () => fetchJSONData<DeploymentMilestone>('deployment_timeline.json'),
staleTime: 5 * 60 * 1000,
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
})
const milestones = data
? data.filter((m) => {
if (filter?.region && m.region !== filter.region) return false
if (filter?.entity && m.entity !== filter.entity) return false
if (filter?.priority && m.priority !== filter.priority) return false
if (filter?.status && m.status !== filter.status) return false
return true
})
: []
return {
milestones,
loading: isLoading,
error: isError
? (error instanceof Error ? error : new Error('Failed to load milestones'))
: null,
}
}
export function useCostEstimates(filter?: {
region?: string
entity?: string
category?: string
}) {
const { data, isLoading, error, isError } = useQuery({
queryKey: ['costEstimates', filter],
queryFn: () => fetchJSONData<CostEstimate>('cost_estimates.json'),
staleTime: 5 * 60 * 1000,
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
})
const estimates = data
? data.filter((e) => {
if (filter?.region && e.region !== filter.region) return false
if (filter?.entity && e.entity !== filter.entity) return false
if (filter?.category && e.category !== filter.category) return false
return true
})
: []
return {
estimates,
loading: isLoading,
error: isError
? (error instanceof Error ? error : new Error('Failed to load cost estimates'))
: null,
}
}
export function useInfrastructureSummary() {
const { data: countries, isLoading: countriesLoading } = useQuery({
queryKey: ['countries'],
queryFn: () => fetchJSONData<Country>('smom_countries.json'),
staleTime: 5 * 60 * 1000,
})
const { data: milestones, isLoading: milestonesLoading } = useQuery({
queryKey: ['deploymentMilestones'],
queryFn: () => fetchJSONData<DeploymentMilestone>('deployment_timeline.json'),
staleTime: 5 * 60 * 1000,
})
const { data: estimates, isLoading: estimatesLoading } = useQuery({
queryKey: ['costEstimates'],
queryFn: () => fetchJSONData<CostEstimate>('cost_estimates.json'),
staleTime: 5 * 60 * 1000,
})
const loading = countriesLoading || milestonesLoading || estimatesLoading
// Calculate summary from loaded data
const summary: InfrastructureSummary | null =
countries && milestones && estimates
? (() => {
const regions = new Set(countries.map((c) => c.region))
const totalCost = estimates.reduce((sum, e) => sum + e.annual, 0)
const progress = milestones.reduce(
(acc, m) => {
acc[m.status] = (acc[m.status] || 0) + 1
return acc
},
{} as Record<string, number>
)
return {
totalCountries: countries.length,
totalRegions: regions.size,
totalCost,
deploymentProgress: {
planned: progress.Planned || 0,
inProgress: progress['In Progress'] || 0,
complete: progress.Complete || 0,
blocked: progress.Blocked || 0,
},
}
})()
: null
return { summary, loading, error: null }
}
+25
View File
@@ -0,0 +1,25 @@
export const supportedLanguages = [
{ code: 'en', name: 'English', nativeName: 'English' },
{ code: 'es', name: 'Spanish', nativeName: 'Español' },
{ code: 'fr', name: 'French', nativeName: 'Français' },
{ code: 'de', name: 'German', nativeName: 'Deutsch' },
{ code: 'zh', name: 'Chinese', nativeName: '中文' },
{ code: 'ja', name: 'Japanese', nativeName: '日本語' },
{ code: 'pt', name: 'Portuguese', nativeName: 'Português' },
{ code: 'ar', name: 'Arabic', nativeName: 'العربية' },
{ code: 'hi', name: 'Hindi', nativeName: 'हिन्दी' },
{ code: 'sw', name: 'Swahili', nativeName: 'Kiswahili' },
] as const;
export type LanguageCode = typeof supportedLanguages[number]['code'];
export const defaultLanguage: LanguageCode = 'en';
export function getLanguageName(code: LanguageCode): string {
return supportedLanguages.find(lang => lang.code === code)?.name || 'English';
}
export function getNativeLanguageName(code: LanguageCode): string {
return supportedLanguages.find(lang => lang.code === code)?.nativeName || 'English';
}
File diff suppressed because it is too large Load Diff
+207
View File
@@ -0,0 +1,207 @@
import { LanguageCode } from './config';
export type TranslationKey =
| 'nav.products'
| 'nav.solutions'
| 'nav.developers'
| 'nav.partners'
| 'nav.company'
| 'nav.support'
| 'nav.signIn'
| 'hero.title'
| 'hero.subtitle'
| 'footer.copyright'
| 'common.loading'
| 'common.error'
| 'common.save'
| 'common.cancel'
| 'common.delete'
| 'common.edit';
export const translations: Record<LanguageCode, Record<TranslationKey, string>> = {
en: {
'nav.products': 'Products',
'nav.solutions': 'Solutions',
'nav.developers': 'Developers',
'nav.partners': 'Partners',
'nav.company': 'Company',
'nav.support': 'Support',
'nav.signIn': 'Sign In',
'hero.title': "Sankofa's Phoenix Nexus Cloud",
'hero.subtitle': 'The sovereign cloud born of fire and ancestral wisdom.',
'footer.copyright': 'All rights reserved',
'common.loading': 'Loading...',
'common.error': 'An error occurred',
'common.save': 'Save',
'common.cancel': 'Cancel',
'common.delete': 'Delete',
'common.edit': 'Edit',
},
es: {
'nav.products': 'Productos',
'nav.solutions': 'Soluciones',
'nav.developers': 'Desarrolladores',
'nav.partners': 'Socios',
'nav.company': 'Empresa',
'nav.support': 'Soporte',
'nav.signIn': 'Iniciar sesión',
'hero.title': 'Nube Nexus Phoenix de Sankofa',
'hero.subtitle': 'La nube soberana nacida del fuego y la sabiduría ancestral.',
'footer.copyright': 'Todos los derechos reservados',
'common.loading': 'Cargando...',
'common.error': 'Ocurrió un error',
'common.save': 'Guardar',
'common.cancel': 'Cancelar',
'common.delete': 'Eliminar',
'common.edit': 'Editar',
},
fr: {
'nav.products': 'Produits',
'nav.solutions': 'Solutions',
'nav.developers': 'Développeurs',
'nav.partners': 'Partenaires',
'nav.company': 'Entreprise',
'nav.support': 'Support',
'nav.signIn': 'Se connecter',
'hero.title': 'Cloud Nexus Phoenix de Sankofa',
'hero.subtitle': 'Le cloud souverain né du feu et de la sagesse ancestrale.',
'footer.copyright': 'Tous droits réservés',
'common.loading': 'Chargement...',
'common.error': 'Une erreur est survenue',
'common.save': 'Enregistrer',
'common.cancel': 'Annuler',
'common.delete': 'Supprimer',
'common.edit': 'Modifier',
},
de: {
'nav.products': 'Produkte',
'nav.solutions': 'Lösungen',
'nav.developers': 'Entwickler',
'nav.partners': 'Partner',
'nav.company': 'Unternehmen',
'nav.support': 'Support',
'nav.signIn': 'Anmelden',
'hero.title': 'Sankofas Phoenix Nexus Cloud',
'hero.subtitle': 'Die souveräne Cloud, geboren aus Feuer und ancestraler Weisheit.',
'footer.copyright': 'Alle Rechte vorbehalten',
'common.loading': 'Laden...',
'common.error': 'Ein Fehler ist aufgetreten',
'common.save': 'Speichern',
'common.cancel': 'Abbrechen',
'common.delete': 'Löschen',
'common.edit': 'Bearbeiten',
},
zh: {
'nav.products': '产品',
'nav.solutions': '解决方案',
'nav.developers': '开发者',
'nav.partners': '合作伙伴',
'nav.company': '公司',
'nav.support': '支持',
'nav.signIn': '登录',
'hero.title': 'Sankofa 凤凰 Nexus 云',
'hero.subtitle': '由火和祖先智慧诞生的主权云。',
'footer.copyright': '版权所有',
'common.loading': '加载中...',
'common.error': '发生错误',
'common.save': '保存',
'common.cancel': '取消',
'common.delete': '删除',
'common.edit': '编辑',
},
ja: {
'nav.products': '製品',
'nav.solutions': 'ソリューション',
'nav.developers': '開発者',
'nav.partners': 'パートナー',
'nav.company': '会社',
'nav.support': 'サポート',
'nav.signIn': 'サインイン',
'hero.title': 'Sankofaのフェニックスネクサスクラウド',
'hero.subtitle': '火と祖先の知恵から生まれた主権クラウド。',
'footer.copyright': '全著作権所有',
'common.loading': '読み込み中...',
'common.error': 'エラーが発生しました',
'common.save': '保存',
'common.cancel': 'キャンセル',
'common.delete': '削除',
'common.edit': '編集',
},
pt: {
'nav.products': 'Produtos',
'nav.solutions': 'Soluções',
'nav.developers': 'Desenvolvedores',
'nav.partners': 'Parceiros',
'nav.company': 'Empresa',
'nav.support': 'Suporte',
'nav.signIn': 'Entrar',
'hero.title': 'Nuvem Nexus Phoenix da Sankofa',
'hero.subtitle': 'A nuvem soberana nascida do fogo e da sabedoria ancestral.',
'footer.copyright': 'Todos os direitos reservados',
'common.loading': 'Carregando...',
'common.error': 'Ocorreu um erro',
'common.save': 'Salvar',
'common.cancel': 'Cancelar',
'common.delete': 'Excluir',
'common.edit': 'Editar',
},
ar: {
'nav.products': 'المنتجات',
'nav.solutions': 'الحلول',
'nav.developers': 'المطورون',
'nav.partners': 'الشركاء',
'nav.company': 'الشركة',
'nav.support': 'الدعم',
'nav.signIn': 'تسجيل الدخول',
'hero.title': 'سحابة Sankofa Phoenix Nexus',
'hero.subtitle': 'السحابة السيادية المولودة من النار والحكمة الأسلافية.',
'footer.copyright': 'جميع الحقوق محفوظة',
'common.loading': 'جاري التحميل...',
'common.error': 'حدث خطأ',
'common.save': 'حفظ',
'common.cancel': 'إلغاء',
'common.delete': 'حذف',
'common.edit': 'تعديل',
},
hi: {
'nav.products': 'उत्पाद',
'nav.solutions': 'समाधान',
'nav.developers': 'डेवलपर्स',
'nav.partners': 'साझेदार',
'nav.company': 'कंपनी',
'nav.support': 'सहायता',
'nav.signIn': 'साइन इन करें',
'hero.title': 'Sankofa का Phoenix Nexus Cloud',
'hero.subtitle': 'अग्नि और पूर्वजों की बुद्धि से जन्मी संप्रभु क्लाउड।',
'footer.copyright': 'सभी अधिकार सुरक्षित',
'common.loading': 'लोड हो रहा है...',
'common.error': 'एक त्रुटि हुई',
'common.save': 'सहेजें',
'common.cancel': 'रद्द करें',
'common.delete': 'हटाएं',
'common.edit': 'संपादित करें',
},
sw: {
'nav.products': 'Bidhaa',
'nav.solutions': 'Suluhisho',
'nav.developers': 'Wasanidi',
'nav.partners': 'Washirika',
'nav.company': 'Kampuni',
'nav.support': 'Msaada',
'nav.signIn': 'Ingia',
'hero.title': 'Wingu la Sankofa Phoenix Nexus',
'hero.subtitle': 'Wingu la kujitegemea linalozaliwa na moto na hekima ya mababu.',
'footer.copyright': 'Haki zote zimehifadhiwa',
'common.loading': 'Inapakia...',
'common.error': 'Hitilafu imetokea',
'common.save': 'Hifadhi',
'common.cancel': 'Ghairi',
'common.delete': 'Futa',
'common.edit': 'Hariri',
},
};
export function getTranslation(key: TranslationKey, language: LanguageCode): string {
return translations[language]?.[key] || translations.en[key] || key;
}
+159
View File
@@ -0,0 +1,159 @@
/**
* Mock API stubs for frontend development
* These can be used during parallel development before backend is ready
*/
export const mockResources = [
{
id: '1',
name: 'web-server-01',
type: 'VM',
status: 'RUNNING',
site: {
id: 'site-1',
name: 'US East Primary Site',
region: 'us-east-1',
status: 'ACTIVE',
},
metadata: {
cpu: 4,
memory: 8192,
disk: 100,
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
{
id: '2',
name: 'db-server-01',
type: 'VM',
status: 'RUNNING',
site: {
id: 'site-1',
name: 'US East Primary Site',
region: 'us-east-1',
status: 'ACTIVE',
},
metadata: {
cpu: 8,
memory: 16384,
disk: 500,
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
]
export const mockSites = [
{
id: 'site-1',
name: 'US East Primary Site',
region: 'us-east-1',
status: 'ACTIVE',
resources: mockResources,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
{
id: 'site-2',
name: 'EU Central Primary Site',
region: 'eu-central-1',
status: 'ACTIVE',
resources: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
]
export const mockResourceInventory = [
{
id: 'inv-1',
resourceType: 'virtual_machine',
provider: 'PROXMOX',
providerId: 'vm-101',
providerResourceId: 'proxmox://cluster1/vm/101',
name: 'web-server-01',
region: 'us-east-1',
site: mockSites[0],
metadata: {
cpu: 4,
memory: 8192,
},
tags: ['web', 'production'],
discoveredAt: new Date().toISOString(),
lastSyncedAt: new Date().toISOString(),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
]
export const mockMetrics = {
resource: mockResourceInventory[0],
metricType: 'CPU_USAGE',
values: Array.from({ length: 60 }, (_, i) => ({
timestamp: new Date(Date.now() - (60 - i) * 60000).toISOString(),
value: Math.random() * 100,
labels: {},
})),
timeRange: {
start: new Date(Date.now() - 3600000).toISOString(),
end: new Date().toISOString(),
},
}
export const mockPillars = [
{ id: '1', code: 'SECURITY', name: 'Security', description: 'Security pillar' },
{ id: '2', code: 'RELIABILITY', name: 'Reliability', description: 'Reliability pillar' },
{ id: '3', code: 'COST_OPTIMIZATION', name: 'Cost Optimization', description: 'Cost pillar' },
{ id: '4', code: 'PERFORMANCE_EFFICIENCY', name: 'Performance Efficiency', description: 'Performance pillar' },
{ id: '5', code: 'OPERATIONAL_EXCELLENCE', name: 'Operational Excellence', description: 'Operations pillar' },
{ id: '6', code: 'SUSTAINABILITY', name: 'Sustainability', description: 'Sustainability pillar' },
]
export const mockFindings = [
{
id: 'f1',
control: {
id: 'c1',
code: 'SEC-001',
name: 'Encryption at Rest',
pillar: mockPillars[0],
},
resource: mockResourceInventory[0],
status: 'PASS',
severity: 'LOW',
title: 'Encryption enabled',
description: 'All storage is encrypted',
recommendation: null,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
]
export const mockRegions = [
{
id: 'r1',
name: 'US East',
code: 'us-east-1',
country: 'United States',
coordinates: { latitude: 39.8283, longitude: -98.5795 },
sites: [mockSites[0]],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
metadata: {},
},
]
/**
* Mock GraphQL query responses
*/
export const mockGraphQLResponses = {
resources: () => ({ data: { resources: mockResources } }),
sites: () => ({ data: { sites: mockSites } }),
resourceInventory: () => ({ data: { resourceInventory: mockResourceInventory } }),
metrics: () => ({ data: { metrics: mockMetrics } }),
pillars: () => ({ data: { pillars: mockPillars } }),
findings: () => ({ data: { findings: mockFindings } }),
regions: () => ({ data: { regions: mockRegions } }),
}
+122
View File
@@ -0,0 +1,122 @@
import { describe, it, expect, beforeEach } from 'vitest'
import { auditLogService, type AuditLogEntry } from '../auditLog'
describe('auditLogService', () => {
beforeEach(() => {
auditLogService.clear()
})
it('should log an entry', () => {
auditLogService.log({
action: 'create',
entityType: 'compliance',
entityId: 'test-id',
entityName: 'Test Entity',
})
const logs = auditLogService.getLogs()
expect(logs).toHaveLength(1)
expect(logs[0].action).toBe('create')
expect(logs[0].entityType).toBe('compliance')
expect(logs[0].entityId).toBe('test-id')
expect(logs[0].entityName).toBe('Test Entity')
expect(logs[0].id).toBeDefined()
expect(logs[0].timestamp).toBeDefined()
})
it('should filter logs by action', () => {
auditLogService.log({
action: 'create',
entityType: 'compliance',
entityId: '1',
entityName: 'Entity 1',
})
auditLogService.log({
action: 'update',
entityType: 'compliance',
entityId: '2',
entityName: 'Entity 2',
})
const createLogs = auditLogService.getLogs({ action: 'create' })
expect(createLogs).toHaveLength(1)
expect(createLogs[0].action).toBe('create')
})
it('should filter logs by entity type', () => {
auditLogService.log({
action: 'create',
entityType: 'compliance',
entityId: '1',
entityName: 'Entity 1',
})
auditLogService.log({
action: 'create',
entityType: 'milestone',
entityId: '2',
entityName: 'Entity 2',
})
const complianceLogs = auditLogService.getLogs({ entityType: 'compliance' })
expect(complianceLogs).toHaveLength(1)
expect(complianceLogs[0].entityType).toBe('compliance')
})
it('should filter logs by date range', () => {
const now = new Date()
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000)
const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000)
auditLogService.log({
action: 'create',
entityType: 'compliance',
entityId: '1',
entityName: 'Entity 1',
})
const logs = auditLogService.getLogs({
startDate: yesterday.toISOString(),
endDate: tomorrow.toISOString(),
})
expect(logs.length).toBeGreaterThan(0)
})
it('should get logs by entity', () => {
auditLogService.log({
action: 'create',
entityType: 'compliance',
entityId: 'test-id',
entityName: 'Test Entity',
})
auditLogService.log({
action: 'update',
entityType: 'compliance',
entityId: 'test-id',
entityName: 'Test Entity',
})
const entityLogs = auditLogService.getLogsByEntity('compliance', 'test-id')
expect(entityLogs).toHaveLength(2)
expect(entityLogs.every((log) => log.entityId === 'test-id')).toBe(true)
})
it('should limit log history size', () => {
// Add more than maxLogs entries
for (let i = 0; i < 10001; i++) {
auditLogService.log({
action: 'create',
entityType: 'compliance',
entityId: `id-${i}`,
entityName: `Entity ${i}`,
})
}
const logs = auditLogService.getLogs()
expect(logs.length).toBeLessThanOrEqual(10000)
})
})
@@ -0,0 +1,97 @@
import { describe, it, expect, beforeEach } from 'vitest'
import { versionControlService, type Version } from '../versionControl'
const mockTopology = {
id: 'test-topology',
region: 'Europe',
entity: 'SMOM',
nodes: [{ id: 'node-1', type: 'vm', label: 'VM 1', position: { x: 0, y: 0 } }],
edges: [],
lastUpdated: new Date().toISOString(),
}
describe('versionControlService', () => {
beforeEach(() => {
// Clear versions (would need to add clear method or reset)
})
it('should save a version', () => {
const version = versionControlService.saveVersion(mockTopology, null)
expect(version).toBeDefined()
expect(version.entityId).toBe('test-topology')
expect(version.entityType).toBe('topology')
expect(version.version).toBe(1)
expect(version.data).toEqual(mockTopology)
expect(version.changes).toHaveLength(0) // No previous version
})
it('should track changes between versions', () => {
const v1 = { ...mockTopology, region: 'Europe' }
const v2 = { ...mockTopology, region: 'Asia' }
versionControlService.saveVersion(v1, null)
const version2 = versionControlService.saveVersion(v2, v1)
expect(version2.changes.length).toBeGreaterThan(0)
const regionChange = version2.changes.find((c) => c.field === 'region')
expect(regionChange).toBeDefined()
expect(regionChange?.before).toBe('Europe')
expect(regionChange?.after).toBe('Asia')
})
it('should get versions for an entity', () => {
versionControlService.saveVersion(mockTopology, null)
versionControlService.saveVersion({ ...mockTopology, region: 'Asia' }, mockTopology)
const versions = versionControlService.getVersions('test-topology')
expect(versions).toHaveLength(2)
expect(versions[0].version).toBe(1)
expect(versions[1].version).toBe(2)
})
it('should get specific version', () => {
versionControlService.saveVersion(mockTopology, null)
versionControlService.saveVersion({ ...mockTopology, region: 'Asia' }, mockTopology)
const version = versionControlService.getVersion('test-topology', 1)
expect(version).toBeDefined()
expect(version?.version).toBe(1)
expect(version?.data.region).toBe('Europe')
})
it('should get latest version', () => {
versionControlService.saveVersion(mockTopology, null)
versionControlService.saveVersion({ ...mockTopology, region: 'Asia' }, mockTopology)
const latest = versionControlService.getLatestVersion('test-topology')
expect(latest).toBeDefined()
expect(latest?.version).toBe(2)
expect(latest?.data.region).toBe('Asia')
})
it('should compare versions', () => {
const v1 = { ...mockTopology, region: 'Europe', nodes: [] }
const v2 = { ...mockTopology, region: 'Asia', nodes: [{ id: 'new-node' }] }
versionControlService.saveVersion(v1, null)
versionControlService.saveVersion(v2, v1)
const comparison = versionControlService.compareVersions('test-topology', 1, 2)
expect(comparison.modified.length).toBeGreaterThan(0)
const regionChange = comparison.modified.find((c) => c.field === 'region')
expect(regionChange).toBeDefined()
})
it('should restore a version', () => {
const v1 = { ...mockTopology, region: 'Europe' }
versionControlService.saveVersion(v1, null)
versionControlService.saveVersion({ ...mockTopology, region: 'Asia' }, v1)
const restored = versionControlService.restoreVersion('test-topology', 1)
expect(restored).toBeDefined()
expect((restored as any).region).toBe('Europe')
})
})
+126
View File
@@ -0,0 +1,126 @@
import type { ComplianceRequirement, DeploymentMilestone, CostEstimate, NetworkTopology } from '../types/infrastructure'
export type AuditAction =
| 'create'
| 'update'
| 'delete'
| 'export'
| 'import'
| 'backup'
| 'restore'
export type AuditEntityType =
| 'compliance'
| 'milestone'
| 'cost'
| 'topology'
| 'backup'
| 'import'
export interface AuditLogEntry {
id: string
timestamp: string
userId?: string
action: AuditAction
entityType: AuditEntityType
entityId: string
entityName: string
changes?: {
before?: any
after?: any
}
metadata?: Record<string, any>
}
class AuditLogService {
private logs: AuditLogEntry[] = []
private maxLogs = 10000
log(entry: Omit<AuditLogEntry, 'id' | 'timestamp'>): void {
const logEntry: AuditLogEntry = {
...entry,
id: `audit-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
timestamp: new Date().toISOString(),
}
this.logs.push(logEntry)
// Keep only the last maxLogs entries
if (this.logs.length > this.maxLogs) {
this.logs = this.logs.slice(-this.maxLogs)
}
// In production, also persist to database or external service
this.persistLog(logEntry)
}
private async persistLog(entry: AuditLogEntry): Promise<void> {
try {
// Save to JSON file (in production, use database)
if (typeof window === 'undefined') {
const fs = await import('fs/promises')
const path = await import('path')
const logDir = path.join(process.cwd(), 'docs/infrastructure/audit-logs')
const logFile = path.join(logDir, `audit-${new Date().toISOString().split('T')[0]}.json`)
try {
await fs.mkdir(logDir, { recursive: true })
let existingLogs: AuditLogEntry[] = []
try {
const content = await fs.readFile(logFile, 'utf-8')
existingLogs = JSON.parse(content)
} catch {
// File doesn't exist yet
}
existingLogs.push(entry)
await fs.writeFile(logFile, JSON.stringify(existingLogs, null, 2))
} catch (error) {
console.error('Failed to persist audit log:', error)
}
}
} catch (error) {
console.error('Failed to persist audit log:', error)
}
}
getLogs(filters?: {
action?: AuditAction
entityType?: AuditEntityType
entityId?: string
startDate?: string
endDate?: string
}): AuditLogEntry[] {
let filtered = [...this.logs]
if (filters?.action) {
filtered = filtered.filter((log) => log.action === filters.action)
}
if (filters?.entityType) {
filtered = filtered.filter((log) => log.entityType === filters.entityType)
}
if (filters?.entityId) {
filtered = filtered.filter((log) => log.entityId === filters.entityId)
}
if (filters?.startDate) {
filtered = filtered.filter((log) => log.timestamp >= filters.startDate!)
}
if (filters?.endDate) {
filtered = filtered.filter((log) => log.timestamp <= filters.endDate!)
}
return filtered.sort((a, b) =>
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
)
}
getLogsByEntity(entityType: AuditEntityType, entityId: string): AuditLogEntry[] {
return this.getLogs({ entityType, entityId })
}
clear(): void {
this.logs = []
}
}
export const auditLogService = new AuditLogService()
+191
View File
@@ -0,0 +1,191 @@
import type { NetworkTopology, ComplianceRequirement, DeploymentMilestone, CostEstimate } from '../types/infrastructure'
export type VersionedEntity = NetworkTopology | ComplianceRequirement | DeploymentMilestone | CostEstimate
export interface Version {
id: string
entityId: string
entityType: string
version: number
timestamp: string
userId?: string
data: VersionedEntity
changes: {
field: string
before: any
after: any
}[]
comment?: string
}
class VersionControlService {
private versions: Map<string, Version[]> = new Map()
private maxVersionsPerEntity = 100
saveVersion(
entity: VersionedEntity,
previousVersion: VersionedEntity | null,
userId?: string,
comment?: string
): Version {
const entityId = (entity as any).id || (entity as any).country || 'unknown'
const entityType = this.getEntityType(entity)
const versions = this.versions.get(entityId) || []
const versionNumber = versions.length + 1
const changes = previousVersion
? this.calculateChanges(previousVersion.data, entity)
: []
const version: Version = {
id: `version-${entityId}-${versionNumber}`,
entityId,
entityType,
version: versionNumber,
timestamp: new Date().toISOString(),
userId,
data: JSON.parse(JSON.stringify(entity)), // Deep clone
changes,
comment,
}
versions.push(version)
// Keep only the last maxVersionsPerEntity versions
if (versions.length > this.maxVersionsPerEntity) {
versions.shift()
}
this.versions.set(entityId, versions)
this.persistVersion(version)
return version
}
private getEntityType(entity: VersionedEntity): string {
if ('nodes' in entity) return 'topology'
if ('country' in entity) return 'compliance'
if ('title' in entity && 'startDate' in entity) return 'milestone'
if ('category' in entity && 'monthly' in entity) return 'cost'
return 'unknown'
}
private calculateChanges(before: VersionedEntity, after: VersionedEntity): Version['changes'] {
const changes: Version['changes'] = []
const beforeObj = before as any
const afterObj = after as any
const allKeys = new Set([...Object.keys(beforeObj), ...Object.keys(afterObj)])
for (const key of allKeys) {
if (key === 'id' || key === 'lastUpdated') continue
const beforeVal = beforeObj[key]
const afterVal = afterObj[key]
if (JSON.stringify(beforeVal) !== JSON.stringify(afterVal)) {
changes.push({
field: key,
before: beforeVal,
after: afterVal,
})
}
}
return changes
}
getVersions(entityId: string): Version[] {
return this.versions.get(entityId) || []
}
getVersion(entityId: string, versionNumber: number): Version | null {
const versions = this.getVersions(entityId)
return versions.find((v) => v.version === versionNumber) || null
}
getLatestVersion(entityId: string): Version | null {
const versions = this.getVersions(entityId)
return versions[versions.length - 1] || null
}
compareVersions(entityId: string, version1: number, version2: number): {
added: Version['changes']
removed: Version['changes']
modified: Version['changes']
} {
const v1 = this.getVersion(entityId, version1)
const v2 = this.getVersion(entityId, version2)
if (!v1 || !v2) {
return { added: [], removed: [], modified: [] }
}
const added: Version['changes'] = []
const removed: Version['changes'] = []
const modified: Version['changes'] = []
const v1Data = v1.data as any
const v2Data = v2.data as any
const allKeys = new Set([...Object.keys(v1Data), ...Object.keys(v2Data)])
for (const key of allKeys) {
if (key === 'id' || key === 'lastUpdated') continue
const v1Val = v1Data[key]
const v2Val = v2Data[key]
if (v1Val === undefined && v2Val !== undefined) {
added.push({ field: key, before: undefined, after: v2Val })
} else if (v1Val !== undefined && v2Val === undefined) {
removed.push({ field: key, before: v1Val, after: undefined })
} else if (JSON.stringify(v1Val) !== JSON.stringify(v2Val)) {
modified.push({ field: key, before: v1Val, after: v2Val })
}
}
return { added, removed, modified }
}
restoreVersion(entityId: string, versionNumber: number): VersionedEntity | null {
const version = this.getVersion(entityId, versionNumber)
if (!version) return null
return JSON.parse(JSON.stringify(version.data)) // Deep clone
}
private async persistVersion(version: Version): Promise<void> {
try {
if (typeof window === 'undefined') {
const fs = await import('fs/promises')
const path = await import('path')
const versionDir = path.join(process.cwd(), 'docs/infrastructure/versions')
const versionFile = path.join(versionDir, `${version.entityId}-versions.json`)
try {
await fs.mkdir(versionDir, { recursive: true })
let existingVersions: Version[] = []
try {
const content = await fs.readFile(versionFile, 'utf-8')
existingVersions = JSON.parse(content)
} catch {
// File doesn't exist yet
}
existingVersions.push(version)
// Keep only last maxVersionsPerEntity
if (existingVersions.length > this.maxVersionsPerEntity) {
existingVersions = existingVersions.slice(-this.maxVersionsPerEntity)
}
await fs.writeFile(versionFile, JSON.stringify(existingVersions, null, 2))
} catch (error) {
console.error('Failed to persist version:', error)
}
}
} catch (error) {
console.error('Failed to persist version:', error)
}
}
}
export const versionControlService = new VersionControlService()
+22 -20
View File
@@ -1,8 +1,8 @@
import React, { ReactElement } from 'react'
import React from 'react'
import { render, RenderOptions } from '@testing-library/react'
import { ApolloProvider } from '@apollo/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { apolloClient } from './graphql/client'
import { ApolloProvider } from '@apollo/client'
import { apolloClient } from '../graphql/client'
// Create a test query client
const createTestQueryClient = () =>
@@ -15,23 +15,25 @@ const createTestQueryClient = () =>
},
})
interface AllTheProvidersProps {
children: React.ReactNode
// Custom render function with providers
export function renderWithProviders(
ui: React.ReactElement,
{
queryClient = createTestQueryClient(),
...renderOptions
}: RenderOptions & { queryClient?: QueryClient } = {}
) {
function Wrapper({ children }: { children: React.ReactNode }) {
return (
<ApolloProvider client={apolloClient}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</ApolloProvider>
)
}
return { ...render(ui, { wrapper: Wrapper, ...renderOptions }), queryClient }
}
const AllTheProviders = ({ children }: AllTheProvidersProps) => {
const queryClient = createTestQueryClient()
return (
<ApolloProvider client={apolloClient}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</ApolloProvider>
)
}
const customRender = (ui: ReactElement, options?: Omit<RenderOptions, 'wrapper'>) =>
render(ui, { wrapper: AllTheProviders, ...options })
// Re-export everything
export * from '@testing-library/react'
export { customRender as render }
export { renderWithProviders as render }
+103
View File
@@ -0,0 +1,103 @@
/**
* TypeScript type definitions for infrastructure documentation
*/
export type Region = 'Africa (Sub-Saharan)' | 'Middle East & North Africa' | 'Americas' | 'Asia-Pacific' | 'Europe'
export type RelationshipType = 'Full Diplomatic Relations' | 'Official (Non-Diplomatic)' | 'Ambassador Level' | 'Full Diplomatic Relations (Special Mission)'
export type Priority = 'Critical' | 'High' | 'Medium' | 'Low'
export type ComplianceStatus = 'Compliant' | 'Partial' | 'Pending' | 'Non-Compliant'
export type DeploymentStatus = 'Planned' | 'In Progress' | 'Complete' | 'Blocked'
export type CostCategory = 'Infrastructure' | 'Network' | 'Compliance' | 'Operations'
export type TopologyNodeType = 'region' | 'datacenter' | 'tunnel' | 'vm' | 'service'
export type TopologyEdgeType = 'tunnel' | 'peering' | 'network-route'
export interface Country {
name: string
region: Region
relationshipType: RelationshipType
priority: Priority
cloudflareCoverage: boolean
networkInfrastructurePriority: string
notes?: string
coordinates?: { lat: number; lng: number }
}
export interface TopologyNode {
id: string
type: TopologyNodeType
label: string
region: string
entity: string
position: { x: number; y: number }
metadata: Record<string, any>
}
export interface TopologyEdge {
id: string
source: string
target: string
type: TopologyEdgeType
metadata: Record<string, any>
}
export interface NetworkTopology {
nodes: TopologyNode[]
edges: TopologyEdge[]
region: string
entity: string
lastUpdated: string
}
export interface ComplianceRequirement {
country: string
region: Region
frameworks: string[]
status: ComplianceStatus
requirements: string[]
lastAuditDate?: string
notes?: string
}
export interface DeploymentMilestone {
id: string
title: string
region: string
entity: string
priority: Priority
startDate: string
endDate: string
status: DeploymentStatus
dependencies?: string[]
cost?: number
description?: string
}
export interface CostEstimate {
region: string
entity: string
category: CostCategory
monthly: number
annual: number
breakdown: {
compute?: number
storage?: number
network?: number
licenses?: number
personnel?: number
}
currency?: string
lastUpdated?: string
}
export interface InfrastructureSummary {
totalCountries: number
totalRegions: number
totalCost: number
deploymentProgress: {
planned: number
inProgress: number
complete: number
blocked: number
}
}
-4
View File
@@ -1,10 +1,6 @@
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
/**
* Utility function to merge Tailwind CSS classes
*/
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
@@ -0,0 +1,224 @@
import { describe, it, expect } from 'vitest'
import {
countrySchema,
topologyNodeSchema,
topologyEdgeSchema,
networkTopologySchema,
complianceRequirementSchema,
deploymentMilestoneSchema,
costEstimateSchema,
} from '../schemas/infrastructure'
describe('Infrastructure Validation Schemas', () => {
describe('countrySchema', () => {
it('should validate a valid country', () => {
const validCountry = {
name: 'Italy',
region: 'Europe',
relationshipType: 'Full Diplomatic Relations',
priority: 'High',
cloudflareCoverage: true,
networkInfrastructurePriority: 'Critical',
}
expect(() => countrySchema.parse(validCountry)).not.toThrow()
})
it('should reject invalid region', () => {
const invalidCountry = {
name: 'Italy',
region: 'Invalid Region',
relationshipType: 'Full Diplomatic Relations',
priority: 'High',
cloudflareCoverage: true,
networkInfrastructurePriority: 'Critical',
}
expect(() => countrySchema.parse(invalidCountry)).toThrow()
})
it('should reject missing required fields', () => {
const invalidCountry = {
name: 'Italy',
// Missing required fields
}
expect(() => countrySchema.parse(invalidCountry)).toThrow()
})
})
describe('topologyNodeSchema', () => {
it('should validate a valid node', () => {
const validNode = {
id: 'node-1',
type: 'vm',
label: 'VM 1',
region: 'Europe',
entity: 'SMOM',
position: { x: 100, y: 200 },
metadata: {},
}
expect(() => topologyNodeSchema.parse(validNode)).not.toThrow()
})
it('should reject invalid node type', () => {
const invalidNode = {
id: 'node-1',
type: 'invalid-type',
label: 'Node',
region: 'Europe',
entity: 'SMOM',
position: { x: 0, y: 0 },
metadata: {},
}
expect(() => topologyNodeSchema.parse(invalidNode)).toThrow()
})
})
describe('topologyEdgeSchema', () => {
it('should validate a valid edge', () => {
const validEdge = {
id: 'edge-1',
source: 'node-1',
target: 'node-2',
type: 'network-route',
metadata: {},
}
expect(() => topologyEdgeSchema.parse(validEdge)).not.toThrow()
})
})
describe('networkTopologySchema', () => {
it('should validate a valid topology', () => {
const validTopology = {
id: 'topology-1',
region: 'Europe',
entity: 'SMOM',
nodes: [
{
id: 'node-1',
type: 'vm',
label: 'VM 1',
region: 'Europe',
entity: 'SMOM',
position: { x: 0, y: 0 },
metadata: {},
},
],
edges: [
{
id: 'edge-1',
source: 'node-1',
target: 'node-2',
type: 'network-route',
metadata: {},
},
],
lastUpdated: new Date().toISOString(),
}
expect(() => networkTopologySchema.parse(validTopology)).not.toThrow()
})
})
describe('complianceRequirementSchema', () => {
it('should validate a valid compliance requirement', () => {
const validRequirement = {
country: 'Italy',
region: 'Europe',
frameworks: ['GDPR'],
status: 'Compliant',
requirements: ['Data protection'],
}
expect(() => complianceRequirementSchema.parse(validRequirement)).not.toThrow()
})
it('should reject invalid status', () => {
const invalidRequirement = {
country: 'Italy',
region: 'Europe',
frameworks: ['GDPR'],
status: 'Invalid Status',
requirements: [],
}
expect(() => complianceRequirementSchema.parse(invalidRequirement)).toThrow()
})
})
describe('deploymentMilestoneSchema', () => {
it('should validate a valid milestone', () => {
const validMilestone = {
id: 'milestone-1',
title: 'Deploy Infrastructure',
region: 'Europe',
entity: 'SMOM',
priority: 'High',
startDate: '2024-01-01',
endDate: '2024-01-31',
status: 'Planned',
dependencies: [],
cost: 10000,
description: 'Deploy infrastructure',
}
expect(() => deploymentMilestoneSchema.parse(validMilestone)).not.toThrow()
})
it('should reject invalid priority', () => {
const invalidMilestone = {
id: 'milestone-1',
title: 'Deploy',
region: 'Europe',
entity: 'SMOM',
priority: 'Invalid Priority',
startDate: '2024-01-01',
endDate: '2024-01-31',
status: 'Planned',
dependencies: [],
}
expect(() => deploymentMilestoneSchema.parse(invalidMilestone)).toThrow()
})
})
describe('costEstimateSchema', () => {
it('should validate a valid cost estimate', () => {
const validEstimate = {
region: 'Europe',
entity: 'SMOM',
category: 'Infrastructure',
monthly: 1000,
annual: 12000,
breakdown: {
compute: 500,
storage: 200,
network: 300,
},
currency: 'USD',
lastUpdated: new Date().toISOString(),
}
expect(() => costEstimateSchema.parse(validEstimate)).not.toThrow()
})
it('should reject negative costs', () => {
const invalidEstimate = {
region: 'Europe',
entity: 'SMOM',
category: 'Infrastructure',
monthly: -1000,
annual: -12000,
breakdown: {},
currency: 'USD',
}
expect(() => costEstimateSchema.parse(invalidEstimate)).toThrow()
})
})
})
@@ -0,0 +1,256 @@
/**
* Zod validation schemas for infrastructure documentation types
*/
import { z } from 'zod'
// Region enum
export const regionSchema = z.enum([
'Africa (Sub-Saharan)',
'Middle East & North Africa',
'Americas',
'Asia-Pacific',
'Europe',
])
// Relationship type enum
export const relationshipTypeSchema = z.enum([
'Full Diplomatic Relations',
'Official (Non-Diplomatic)',
'Ambassador Level',
'Full Diplomatic Relations (Special Mission)',
])
// Priority enum
export const prioritySchema = z.enum(['Critical', 'High', 'Medium', 'Low'])
// Compliance status enum
export const complianceStatusSchema = z.enum([
'Compliant',
'Partial',
'Pending',
'Non-Compliant',
])
// Deployment status enum
export const deploymentStatusSchema = z.enum([
'Planned',
'In Progress',
'Complete',
'Blocked',
])
// Cost category enum
export const costCategorySchema = z.enum([
'Infrastructure',
'Network',
'Compliance',
'Operations',
])
// Topology node type enum
export const topologyNodeTypeSchema = z.enum([
'region',
'datacenter',
'tunnel',
'vm',
'service',
])
// Topology edge type enum
export const topologyEdgeTypeSchema = z.enum(['tunnel', 'peering', 'network-route'])
// Coordinates schema
export const coordinatesSchema = z
.object({
lat: z.number().min(-90).max(90),
lng: z.number().min(-180).max(180),
})
.optional()
// Country schema
export const countrySchema = z.object({
name: z.string().min(1, 'Country name is required'),
region: regionSchema,
relationshipType: relationshipTypeSchema,
priority: prioritySchema,
cloudflareCoverage: z.boolean(),
networkInfrastructurePriority: z.string().min(1),
notes: z.string().optional(),
coordinates: coordinatesSchema,
})
// Topology node schema
export const topologyNodeSchema = z.object({
id: z.string().min(1, 'Node ID is required'),
type: topologyNodeTypeSchema,
label: z.string().min(1, 'Node label is required'),
region: z.string().min(1, 'Region is required'),
entity: z.string().min(1, 'Entity is required'),
position: z.object({
x: z.number(),
y: z.number(),
}),
metadata: z.record(z.any()).default({}),
})
// Topology edge schema
export const topologyEdgeSchema = z.object({
id: z.string().min(1, 'Edge ID is required'),
source: z.string().min(1, 'Source node ID is required'),
target: z.string().min(1, 'Target node ID is required'),
type: topologyEdgeTypeSchema,
metadata: z.record(z.any()).default({}),
})
// Network topology schema
export const networkTopologySchema = z.object({
nodes: z.array(topologyNodeSchema).min(0),
edges: z.array(topologyEdgeSchema).min(0),
region: z.string().min(1, 'Region is required'),
entity: z.string().min(1, 'Entity is required'),
lastUpdated: z.string().datetime({ message: 'Invalid datetime format' }),
})
// Compliance requirement schema
export const complianceRequirementSchema = z.object({
country: z.string().min(1, 'Country is required'),
region: regionSchema,
frameworks: z.array(z.string().min(1)).min(1, 'At least one framework is required'),
status: complianceStatusSchema,
requirements: z.array(z.string()).min(0),
lastAuditDate: z.string().datetime().optional(),
notes: z.string().optional(),
})
// Deployment milestone schema with date validation
export const deploymentMilestoneSchema = z
.object({
id: z.string().min(1, 'Milestone ID is required'),
title: z.string().min(1, 'Title is required'),
region: z.string().min(1, 'Region is required'),
entity: z.string().min(1, 'Entity is required'),
priority: prioritySchema,
startDate: z.string().datetime({ message: 'Invalid start date format' }),
endDate: z.string().datetime({ message: 'Invalid end date format' }),
status: deploymentStatusSchema,
dependencies: z.array(z.string()).optional(),
cost: z.number().min(0, 'Cost must be non-negative').optional(),
description: z.string().optional(),
})
.refine(
(data) => {
const start = new Date(data.startDate)
const end = new Date(data.endDate)
return end > start
},
{
message: 'End date must be after start date',
path: ['endDate'],
}
)
// Cost breakdown schema
export const costBreakdownSchema = z.object({
compute: z.number().min(0).optional(),
storage: z.number().min(0).optional(),
network: z.number().min(0).optional(),
licenses: z.number().min(0).optional(),
personnel: z.number().min(0).optional(),
})
// Cost estimate schema with breakdown validation
export const costEstimateSchema = z
.object({
region: z.string().min(1, 'Region is required'),
entity: z.string().min(1, 'Entity is required'),
category: costCategorySchema,
monthly: z.number().min(0, 'Monthly cost must be non-negative'),
annual: z.number().min(0, 'Annual cost must be non-negative'),
breakdown: costBreakdownSchema,
currency: z.string().optional(),
lastUpdated: z.string().datetime().optional(),
})
.refine(
(data) => {
const breakdownSum =
(data.breakdown.compute || 0) +
(data.breakdown.storage || 0) +
(data.breakdown.network || 0) +
(data.breakdown.licenses || 0) +
(data.breakdown.personnel || 0)
// Allow small floating point differences
return Math.abs(breakdownSum - data.monthly) < 0.01
},
{
message: 'Breakdown sum must equal monthly cost',
path: ['breakdown'],
}
)
.refine(
(data) => {
// Annual should be approximately monthly * 12 (allow small differences)
const expectedAnnual = data.monthly * 12
return Math.abs(data.annual - expectedAnnual) < 0.01
},
{
message: 'Annual cost should be approximately monthly * 12',
path: ['annual'],
}
)
// Infrastructure summary schema
export const infrastructureSummarySchema = z.object({
totalCountries: z.number().int().min(0),
totalRegions: z.number().int().min(0),
totalCost: z.number().min(0),
deploymentProgress: z.object({
planned: z.number().int().min(0),
inProgress: z.number().int().min(0),
complete: z.number().int().min(0),
blocked: z.number().int().min(0),
}),
})
// Input schemas for mutations
// Update topology input
export const updateTopologyInputSchema = z.object({
nodes: z.array(topologyNodeSchema).optional(),
edges: z.array(topologyEdgeSchema).optional(),
region: z.string().min(1).optional(),
entity: z.string().min(1).optional(),
lastUpdated: z.string().datetime().optional(),
})
// Create milestone input
export const createMilestoneInputSchema = deploymentMilestoneSchema.omit({ id: true })
// Update milestone input
export const updateMilestoneInputSchema = deploymentMilestoneSchema.partial().required({ id: true })
// Update compliance input
export const updateComplianceInputSchema = complianceRequirementSchema.partial().required({
country: true,
})
// Update cost estimate input
export const updateCostEstimateInputSchema = costEstimateSchema
.partial()
.required({ region: true, entity: true, category: true })
// Type exports for TypeScript inference
export type CountryInput = z.infer<typeof countrySchema>
export type TopologyNodeInput = z.infer<typeof topologyNodeSchema>
export type TopologyEdgeInput = z.infer<typeof topologyEdgeSchema>
export type NetworkTopologyInput = z.infer<typeof networkTopologySchema>
export type ComplianceRequirementInput = z.infer<typeof complianceRequirementSchema>
export type DeploymentMilestoneInput = z.infer<typeof deploymentMilestoneSchema>
export type CostEstimateInput = z.infer<typeof costEstimateSchema>
export type InfrastructureSummaryInput = z.infer<typeof infrastructureSummarySchema>
export type UpdateTopologyInput = z.infer<typeof updateTopologyInputSchema>
export type CreateMilestoneInput = z.infer<typeof createMilestoneInputSchema>
export type UpdateMilestoneInput = z.infer<typeof updateMilestoneInputSchema>
export type UpdateComplianceInput = z.infer<typeof updateComplianceInputSchema>
export type UpdateCostEstimateInput = z.infer<typeof updateCostEstimateInputSchema>