* clear

* feat: Add everything

* fix: Commenht
This commit is contained in:
Prad Nukala
2025-10-03 14:45:52 -04:00
committed by GitHub
parent 43b4a11c06
commit 13e6c3e84d
1935 changed files with 655061 additions and 40058 deletions
+16
View File
@@ -0,0 +1,16 @@
[tool.commitizen]
name = "cz_customize"
tag_format = "web-dash/v$version"
ignored_tag_formats = ["*/v${version}", "v${version}"]
version_scheme = "semver"
version_provider = "npm"
update_changelog_on_bump = true
major_version_zero = true
pre_bump_hooks = ["bash ../../scripts/hook-bump-pre.sh"]
post_bump_hooks = ["pnpm --filter '@sonr.io/dash' deploy:cloudflare"]
[tool.commitizen.customize]
bump_pattern = "^(feat|fix|refactor|perf|BREAKING CHANGE)"
bump_map = { "BREAKING CHANGE" = "MAJOR", "feat" = "MINOR", "fix" = "PATCH", "refactor" = "PATCH", "perf" = "PATCH" }
default_bump = "PATCH"
changelog_pattern = "^(feat|fix|refactor|docs|build)\\(dash\\)(!)?:"
+61
View File
@@ -0,0 +1,61 @@
# Chain Configuration
NEXT_PUBLIC_CHAIN_ID=sonrtest_1-1
NEXT_PUBLIC_CHAIN_ENDPOINT=http://localhost:26657
NEXT_PUBLIC_RPC_ENDPOINT=http://localhost:1317
NEXT_PUBLIC_GRPC_ENDPOINT=http://localhost:9090
NEXT_PUBLIC_WS_ENDPOINT=ws://localhost:26657/websocket
# Authentication
NEXT_PUBLIC_AUTH_URL=http://localhost:3001
NEXT_PUBLIC_AUTH_DOMAIN=localhost
# API Configuration
NEXT_PUBLIC_API_URL=http://localhost:3000
NEXT_PUBLIC_API_VERSION=v1
# Service Configuration
NEXT_PUBLIC_SERVICE_DOMAIN=api.example.com
NEXT_PUBLIC_SERVICE_NAME=Sonr Dashboard
# Feature Flags
NEXT_PUBLIC_ENABLE_ANALYTICS=true
NEXT_PUBLIC_ENABLE_REALTIME=true
NEXT_PUBLIC_ENABLE_MOCK_DATA=false
# Development Settings
NEXT_PUBLIC_DEBUG=false
NEXT_PUBLIC_LOG_LEVEL=info
# Domain Verification
NEXT_PUBLIC_DNS_CHECK_INTERVAL=5000
NEXT_PUBLIC_DNS_MAX_ATTEMPTS=60
NEXT_PUBLIC_VERIFICATION_PREFIX=sonr-verification
# Rate Limiting
NEXT_PUBLIC_MAX_REQUESTS_PER_MINUTE=100
NEXT_PUBLIC_MAX_REQUESTS_PER_HOUR=1000
# Monitoring
NEXT_PUBLIC_SENTRY_DSN=
NEXT_PUBLIC_GA_TRACKING_ID=
# External Services
NEXT_PUBLIC_IPFS_GATEWAY=https://ipfs.io/ipfs/
NEXT_PUBLIC_IPFS_API=http://localhost:5001
# Testnet Configuration
NEXT_PUBLIC_FAUCET_URL=https://faucet.sonr.io
NEXT_PUBLIC_EXPLORER_URL=https://explorer.sonr.io
# WebAuthn Configuration
NEXT_PUBLIC_WEBAUTHN_RP_NAME=Sonr Dashboard
NEXT_PUBLIC_WEBAUTHN_RP_ID=localhost
NEXT_PUBLIC_WEBAUTHN_ORIGIN=http://localhost:3000
# Cache Settings
NEXT_PUBLIC_CACHE_TTL=300000
NEXT_PUBLIC_STALE_TIME=60000
# Pagination
NEXT_PUBLIC_DEFAULT_PAGE_SIZE=20
NEXT_PUBLIC_MAX_PAGE_SIZE=100
+24
View File
@@ -0,0 +1,24 @@
# Staging Environment Configuration for Dashboard App
NEXT_PUBLIC_ENVIRONMENT=staging
NEXT_PUBLIC_API_URL=https://highway-api-staging.workers.dev
NEXT_PUBLIC_APP_URL=https://staging.app.sonr.id
NEXT_PUBLIC_AUTH_URL=https://staging.auth.sonr.id
NEXT_PUBLIC_MAIN_SITE_URL=https://staging.sonr.id
# WebAuthn Configuration
NEXT_PUBLIC_RP_ID=staging.highway.sonr.id
NEXT_PUBLIC_RP_NAME="Highway Staging"
# Feature Flags
NEXT_PUBLIC_ENABLE_DEBUG=true
NEXT_PUBLIC_ENABLE_DEV_TOOLS=true
NEXT_PUBLIC_ENABLE_ADMIN_PANEL=true
# Analytics
NEXT_PUBLIC_ANALYTICS_ID=staging-dashboard-analytics-id
# Error Tracking
NEXT_PUBLIC_SENTRY_DSN=staging-dashboard-sentry-dsn
# Logging
NEXT_PUBLIC_LOG_LEVEL=info
+71
View File
@@ -0,0 +1,71 @@
# Use Node.js Alpine as base image
FROM node:20-alpine AS base
# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install pnpm
RUN corepack enable && corepack prepare pnpm@latest --activate
# Copy root package files and workspace configuration
COPY ../../package.json ../../pnpm-lock.yaml ../../pnpm-workspace.yaml ./
COPY ../../turbo.json ./
# Copy package.json files for all workspace dependencies
COPY ../../packages/es/package.json ./packages/es/
COPY ../../packages/sdk/package.json ./packages/sdk/
COPY ../../packages/com/package.json ./packages/com/
COPY ../../packages/ui/package.json ./packages/ui/
COPY ../../packages/pkl/package.json ./packages/pkl/
COPY ../../packages/cli/package.json ./packages/cli/
COPY ./package.json ./web/dash/
# Install dependencies
RUN pnpm install --frozen-lockfile
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
# Install pnpm
RUN corepack enable && corepack prepare pnpm@latest --activate
# Copy installed dependencies
COPY --from=deps /app/node_modules ./node_modules
COPY --from=deps /app/.pnpm ./.pnpm
# Copy source code
COPY ../.. .
# Build the application
WORKDIR /app/web/dash
RUN pnpm build
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
# Create non-root user
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy built application
COPY --from=builder /app/web/dash/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/web/dash/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/web/dash/.next/static ./.next/static
USER nextjs
# Expose port
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
# Start the application
CMD ["node", "server.js"]
+113
View File
@@ -0,0 +1,113 @@
# Dashboard App
This is the user dashboard for Highway WebAuthn authentication gateway. It provides a comprehensive interface for user management, console interactions, and system monitoring, integrating with the Blockchain x/svc module.
## Features
- **User Management**: Complete user account management and profile editing
- **Console Interface**: Interactive console for system operations
- **System Monitoring**: Real-time monitoring of service health and performance
- **Blockchain Integration**: Direct integration with Sonr Blockchain x/svc module
- **Authentication**: WebAuthn-based secure authentication
- **Responsive Design**: Mobile-first approach with modern UI/UX
## Development
To start the development server:
```bash
pnpm dev
```
This will start the Next.js development server with hot reload at `http://localhost:3001`.
## Testing
```bash
# Run tests
pnpm test
# Test dashboard functionality
# Visit http://localhost:3001 and test management features
```
## Deployment
To deploy to Cloudflare Pages:
```bash
pnpm deploy
```
The app is configured for static export and optimized for Cloudflare Pages deployment.
## Configuration
### Environment Variables
```env
# API endpoints
NEXT_PUBLIC_API_URL=https://api.yourdomain.com
NEXT_PUBLIC_BLOCKCHAIN_URL=https://blockchain.yourdomain.com
# Development
NEXT_PUBLIC_API_URL=http://localhost:8080
NEXT_PUBLIC_BLOCKCHAIN_URL=http://localhost:8080
```
## Development Roadmap
### 🔮 **Future Implementation** (Planned)
- [ ] **Dashboard Framework**: Next.js 14 setup with TypeScript configuration
- [ ] **User Management**: User account creation, editing, and deletion
- [ ] **Console Interface**: Interactive console for system operations
- [ ] **System Monitoring**: Real-time service health and performance metrics
- [ ] **Blockchain Integration**: Direct integration with Sonr Blockchain x/svc module
- [ ] **Authentication**: WebAuthn-based secure authentication
- [ ] **Responsive Design**: Mobile-first approach with modern UI/UX
### 🚧 **Production Readiness** (Next)
- [ ] **Performance Optimization**: Code splitting and bundle optimization
- [ ] **Testing Suite**: Unit tests for components and hooks
- [ ] **E2E Testing**: Cypress or Playwright for full user flow testing
- [ ] **Error Boundaries**: React error boundaries for graceful error handling
- [ ] **Analytics**: User behavior tracking and conversion metrics
- [ ] **Security**: Role-based access control and audit logging
### 🔮 **Future Enhancements**
- [ ] **Advanced Analytics**: Detailed system analytics and reporting
- [ ] **Multi-tenant Support**: Organization-based dashboard isolation
- [ ] **Advanced UI**: Dark mode, animations, and enhanced UX
- [ ] **Mobile App**: React Native or native mobile applications
- [ ] **API Management**: Advanced API key management and monitoring
- [ ] **Backup & Recovery**: System backup and disaster recovery features
## Architecture
The dashboard follows modern React patterns:
- **App Router**: Next.js 13+ app directory structure
- **Custom Hooks**: Reusable logic for dashboard operations
- **Component Library**: Shared UI components from `@sonr.io/ui`
- **State Management**: React hooks for local state management
- **Styling**: Tailwind CSS with custom dashboard styling
## Security
- **WebAuthn Authentication**: Secure admin authentication
- **Role-based Access**: Different permission levels for dashboard features
- **Input Validation**: Client-side validation with server-side verification
- **Audit Logging**: Complete audit trail for dashboard operations
- **Session Management**: Secure session handling and timeout
## Integration
The dashboard integrates with:
- **API Gateway**: Provides data for dashboard views
- **Blockchain Service**: Direct blockchain operations and monitoring
- **Monitoring**: System metrics and performance data
- **Authentication**: User authentication and session management
+17
View File
@@ -0,0 +1,17 @@
{
"$schema": "https://biomejs.dev/schemas/2.1.4/schema.json",
"extends": ["../../biome.json"],
"linter": {
"rules": {
"suspicious": {
"noExplicitAny": "off"
},
"correctness": {
"noUnusedVariables": "warn"
},
"complexity": {
"noStaticOnlyClass": "off"
}
}
}
}
+21
View File
@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "../../packages/ui/src/styles/globals.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"hooks": "@/hooks",
"lib": "@/lib",
"utils": "@sonr.io/ui/lib/utils",
"ui": "@sonr.io/ui/components"
},
"iconLibrary": "lucide-react"
}
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+90
View File
@@ -0,0 +1,90 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
// OpenNext handles the runtime configuration
// Removed experimental.runtime and output configuration
transpilePackages: ['@sonr.io/ui', '@sonr.io/shared', '@sonr.io/es', '@sonr.io/sdk'],
// API proxy configuration for development
async rewrites() {
// Only apply in development
if (process.env.NODE_ENV !== 'production') {
return [
{
source: '/api/chain/:path*',
destination: `${process.env.NEXT_PUBLIC_CHAIN_ENDPOINT || 'http://localhost:26657'}/:path*`,
},
{
source: '/api/rpc/:path*',
destination: `${process.env.NEXT_PUBLIC_RPC_ENDPOINT || 'http://localhost:1317'}/:path*`,
},
{
source: '/api/grpc/:path*',
destination: `${process.env.NEXT_PUBLIC_GRPC_ENDPOINT || 'http://localhost:9090'}/:path*`,
},
];
}
return [];
},
// CORS headers for API routes
async headers() {
return [
{
source: '/api/:path*',
headers: [
{ key: 'Access-Control-Allow-Credentials', value: 'true' },
{ key: 'Access-Control-Allow-Origin', value: '*' },
{ key: 'Access-Control-Allow-Methods', value: 'GET,DELETE,PATCH,POST,PUT' },
{
key: 'Access-Control-Allow-Headers',
value:
'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version',
},
],
},
];
},
// Environment variables passed to the client
env: {
NEXT_PUBLIC_CHAIN_ID: process.env.NEXT_PUBLIC_CHAIN_ID || 'sonrtest_1-1',
NEXT_PUBLIC_AUTH_URL: process.env.NEXT_PUBLIC_AUTH_URL || 'http://localhost:3001',
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000',
},
// Webpack configuration
webpack: (config) => {
// Handle WebAssembly modules
config.experiments = {
...config.experiments,
asyncWebAssembly: true,
layers: true,
};
// Ignore optional dependencies warnings
config.resolve.fallback = {
...config.resolve.fallback,
fs: false,
net: false,
tls: false,
crypto: false,
};
return config;
},
// TypeScript and ESLint configuration
typescript: {
// Allow production builds to succeed even if there are type errors
ignoreBuildErrors: process.env.NODE_ENV === 'production',
},
eslint: {
// Warning: This allows production builds to successfully complete even if
// your project has ESLint errors.
ignoreDuringBuilds: process.env.NODE_ENV === 'production',
},
};
module.exports = nextConfig;
+3
View File
@@ -0,0 +1,3 @@
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
export default defineCloudflareConfig();
+75
View File
@@ -0,0 +1,75 @@
{
"name": "@sonr.io/dash",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "wrangler dev",
"build": "next build",
"build:cloudflare": "wrangler build",
"start": "next start",
"preview": "wrangler preview",
"lint": "biome lint .",
"format": "biome format . --write",
"check": "biome check . --write",
"deploy:cloudflare": "wrangler deploy",
"release": "cz --no-raise 6,21 bump --yes --increment PATCH"
},
"dependencies": {
"@heroicons/react": "^2.2.0",
"@hookform/resolvers": "^5.2.1",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7",
"@simplewebauthn/browser": "^9.0.0",
"@sonr.io/es": "workspace:*",
"@sonr.io/sdk": "workspace:*",
"@sonr.io/com": "workspace:*",
"@sonr.io/ui": "workspace:*",
"@tanstack/react-query": "^5.59.20",
"@tanstack/react-query-devtools": "^5.59.20",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"date-fns": "^3.3.1",
"lucide-react": "^0.263.1",
"next": "15.5.2",
"next-themes": "^0.2.1",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-hook-form": "^7.62.0",
"recharts": "^2.10.3",
"tailwind-merge": "^2.2.0",
"tailwindcss-animate": "^1.0.7",
"zod": "^4.1.5"
},
"devDependencies": {
"@biomejs/biome": "^2.1.2",
"@browser-echo/next": "^1.0.1",
"@cloudflare/next-on-pages": "^1.13.16",
"@opennextjs/cloudflare": "^1.8.0",
"@types/node": "^20.11.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"autoprefixer": "^10.4.16",
"postcss": "^8.4.33",
"tailwindcss": "^3.4.0",
"typescript": "^5.3.0",
"vercel": "^47.0.5",
"wrangler": "^4.34.0"
},
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwindcss": "^3.4.0"
}
}
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
+311
View File
@@ -0,0 +1,311 @@
'use client';
import type { Domain, DomainVerificationStatus } from '@sonr.io/com/types/domain';
import {
DNSInstructions,
DomainList,
DomainSelector,
VerificationStatus,
VerificationWizard,
} from '@sonr.io/ui';
import { DashboardContent, DashboardHeader } from '@sonr.io/ui';
import { Alert, AlertDescription } from '@sonr.io/ui/components/ui/alert';
import { Badge } from '@sonr.io/ui/components/ui/badge';
import { Button } from '@sonr.io/ui/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@sonr.io/ui/components/ui/card';
import { Dialog, DialogContent, DialogTrigger } from '@sonr.io/ui/components/ui/dialog';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@sonr.io/ui/components/ui/tabs';
import { Globe, Plus, RefreshCw, Shield } from 'lucide-react';
import React, { useEffect, useState } from 'react';
/**
* Domain Management Page
* Handles domain verification flow, status tracking, and DNS instructions
*/
export default function DomainsPage() {
const [domains, setDomains] = useState<Domain[]>([]);
const [selectedDomain, setSelectedDomain] = useState<string | null>(null);
const [_isLoading, setIsLoading] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false);
const [showVerificationWizard, setShowVerificationWizard] = useState(false);
const [verificationStatus, setVerificationStatus] = useState<DomainVerificationStatus | null>(
null
);
// Fetch domains on mount
useEffect(() => {
fetchDomains();
}, []);
const fetchDomains = async () => {
setIsLoading(true);
try {
// TODO: Replace with actual API call
const mockDomains: Domain[] = [
{
id: '1',
name: 'example.com',
status: 'verified',
verifiedAt: new Date('2024-01-15'),
txtRecord: 'sonr-verify=abc123def456',
serviceCount: 3,
},
{
id: '2',
name: 'api.example.com',
status: 'pending',
verifiedAt: null,
txtRecord: 'sonr-verify=xyz789ghi012',
serviceCount: 0,
},
];
setDomains(mockDomains);
} catch (error) {
console.error('Failed to fetch domains:', error);
} finally {
setIsLoading(false);
}
};
const refreshVerificationStatus = async () => {
setIsRefreshing(true);
try {
// TODO: Replace with actual API call to check DNS records
await new Promise((resolve) => setTimeout(resolve, 2000));
// Mock status update
const updatedStatus: DomainVerificationStatus = {
isVerified: false,
lastChecked: new Date(),
dnsRecordsFound: true,
expectedRecord: 'sonr-verify=abc123def456',
};
setVerificationStatus(updatedStatus);
// Refresh domain list
await fetchDomains();
} catch (error) {
console.error('Failed to refresh verification status:', error);
} finally {
setIsRefreshing(false);
}
};
const handleDomainVerification = async (domain: string) => {
try {
// TODO: Replace with actual API call
console.log('Starting verification for:', domain);
setShowVerificationWizard(false);
await fetchDomains();
} catch (error) {
console.error('Failed to verify domain:', error);
}
};
const handleDomainSelection = (domainId: string) => {
setSelectedDomain(domainId);
const domain = domains.find((d) => d.id === domainId);
if (domain && domain.status === 'pending') {
refreshVerificationStatus();
}
};
return (
<div className="flex flex-col gap-6">
<DashboardHeader
title="Domain Management"
description="Verify and manage your domains for service registration"
>
<Dialog open={showVerificationWizard} onOpenChange={setShowVerificationWizard}>
<DialogTrigger asChild>
<Button>
<Plus className="mr-2 h-4 w-4" />
Add Domain
</Button>
</DialogTrigger>
<DialogContent className="max-w-3xl">
<VerificationWizard
onComplete={handleDomainVerification}
onCancel={() => setShowVerificationWizard(false)}
/>
</DialogContent>
</Dialog>
</DashboardHeader>
<DashboardContent>
<div className="grid gap-6">
{/* Domain Overview Cards */}
<div className="grid gap-4 md:grid-cols-3">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Domains</CardTitle>
<Globe className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{domains.length}</div>
<p className="text-xs text-muted-foreground">
{domains.filter((d) => d.status === 'verified').length} verified
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Pending Verification</CardTitle>
<RefreshCw className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{domains.filter((d) => d.status === 'pending').length}
</div>
<p className="text-xs text-muted-foreground">Awaiting DNS verification</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Active Services</CardTitle>
<Shield className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{domains.reduce((sum, d) => sum + (d.serviceCount || 0), 0)}
</div>
<p className="text-xs text-muted-foreground">Across all domains</p>
</CardContent>
</Card>
</div>
{/* Domain Management Tabs */}
<Card>
<CardHeader>
<CardTitle>Your Domains</CardTitle>
<CardDescription>
Manage your verified domains and track verification status
</CardDescription>
</CardHeader>
<CardContent>
<Tabs defaultValue="all" className="space-y-4">
<TabsList>
<TabsTrigger value="all">All Domains</TabsTrigger>
<TabsTrigger value="verified">Verified</TabsTrigger>
<TabsTrigger value="pending">Pending</TabsTrigger>
</TabsList>
<TabsContent value="all" className="space-y-4">
<DomainList
domains={domains}
onDomainSelect={handleDomainSelection}
onRefresh={refreshVerificationStatus}
isRefreshing={isRefreshing}
selectedDomainId={selectedDomain}
/>
</TabsContent>
<TabsContent value="verified" className="space-y-4">
<DomainList
domains={domains.filter((d) => d.status === 'verified')}
onDomainSelect={handleDomainSelection}
onRefresh={refreshVerificationStatus}
isRefreshing={isRefreshing}
selectedDomainId={selectedDomain}
/>
</TabsContent>
<TabsContent value="pending" className="space-y-4">
<DomainList
domains={domains.filter((d) => d.status === 'pending')}
onDomainSelect={handleDomainSelection}
onRefresh={refreshVerificationStatus}
isRefreshing={isRefreshing}
selectedDomainId={selectedDomain}
/>
</TabsContent>
</Tabs>
</CardContent>
</Card>
{/* DNS Instructions for Selected Domain */}
{selectedDomain &&
domains.find((d) => d.id === selectedDomain && d.status === 'pending') && (
<Card>
<CardHeader>
<CardTitle>DNS Verification Instructions</CardTitle>
<CardDescription>
Add the following TXT record to your domain's DNS settings
</CardDescription>
</CardHeader>
<CardContent>
<DNSInstructions
domain={domains.find((d) => d.id === selectedDomain)?.name || ''}
txtRecord={domains.find((d) => d.id === selectedDomain)?.txtRecord || ''}
/>
{verificationStatus && (
<div className="mt-4">
<VerificationStatus
status={verificationStatus}
domain={domains.find((d) => d.id === selectedDomain)?.name || ''}
/>
</div>
)}
<div className="mt-4">
<Button
onClick={refreshVerificationStatus}
disabled={isRefreshing}
variant="outline"
>
{isRefreshing ? (
<>
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
Checking DNS Records...
</>
) : (
<>
<RefreshCw className="mr-2 h-4 w-4" />
Check Verification Status
</>
)}
</Button>
</div>
</CardContent>
</Card>
)}
{/* Domain Selector for Service Registration */}
<Card>
<CardHeader>
<CardTitle>Quick Actions</CardTitle>
<CardDescription>Select a verified domain to register new services</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center gap-4">
<DomainSelector
domains={domains.filter((d) => d.status === 'verified')}
onSelect={(domain) => {
// Navigate to service registration with selected domain
window.location.href = `/services/new?domain=${domain}`;
}}
placeholder="Select a verified domain"
/>
<Alert>
<AlertDescription>
Only verified domains can be used to register new services. Complete domain
verification first.
</AlertDescription>
</Alert>
</div>
</CardContent>
</Card>
</div>
</DashboardContent>
</div>
);
}
+77
View File
@@ -0,0 +1,77 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
/* Additional dashboard-specific styles */
:root {
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
body {
font-feature-settings: "rlig" 1, "calt" 1;
}
/* Additional app-specific styles */
.animate-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: .5;
}
}
+38
View File
@@ -0,0 +1,38 @@
import { DashboardSidebar, SidebarInset, SidebarProvider, SidebarTrigger } from '@sonr.io/ui';
import type { Metadata } from 'next';
import { AuthWrapper } from '../components/auth-wrapper';
import { ThemeProvider } from '../components/theme-provider';
import './globals.css';
export const metadata: Metadata = {
title: 'Sonr Developer Dashboard',
description: 'Manage your Sonr Services, domains, and analytics',
};
export default function RootLayout({ children }: { children: React.ReactNode }): React.JSX.Element {
return (
<html lang="en" suppressHydrationWarning>
<head />
<body className="min-h-screen bg-background font-sans antialiased">
<ThemeProvider defaultTheme="system" storageKey="sonr-dashboard-theme">
<AuthWrapper>
<SidebarProvider>
<div className="relative flex min-h-screen w-full">
<DashboardSidebar />
<SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
<SidebarTrigger className="-ml-1" />
<div className="flex items-center gap-2 px-3">
<h1 className="text-lg font-semibold">Sonr Dashboard</h1>
</div>
</header>
<div className="flex-1 px-4 py-6 lg:px-8">{children}</div>
</SidebarInset>
</div>
</SidebarProvider>
</AuthWrapper>
</ThemeProvider>
</body>
</html>
);
}
+212
View File
@@ -0,0 +1,212 @@
'use client';
import { Card, CardContent, CardHeader, CardTitle } from '@sonr.io/ui/components/ui/card';
import { Activity, Clock, Server, Users } from 'lucide-react';
import { useEffect, useState } from 'react';
export default function DashboardHome() {
const [isLoading, setIsLoading] = useState(true);
const [metrics, setMetrics] = useState({
totalServices: 0,
activeServices: 0,
totalRequests: 0,
avgResponseTime: 0,
});
useEffect(() => {
// Simulate loading and fetching metrics
const fetchMetrics = async () => {
try {
setIsLoading(true);
await new Promise((resolve) => setTimeout(resolve, 1000));
setMetrics({
totalServices: 12,
activeServices: 8,
totalRequests: 24658,
avgResponseTime: 142,
});
} catch (_err) {
// Handle error
} finally {
setIsLoading(false);
}
};
fetchMetrics();
}, []);
return (
<div className="min-h-screen bg-gray-50 p-8">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">Sonr Developer Dashboard</h1>
<p className="text-gray-600 mt-2">Manage your services, domains, and analytics</p>
</div>
{/* Metrics Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<MetricCard
title="Total Services"
value={isLoading ? '...' : metrics.totalServices.toString()}
icon={<Server className="h-6 w-6" />}
trend="+12%"
isLoading={isLoading}
/>
<MetricCard
title="Active Services"
value={isLoading ? '...' : metrics.activeServices.toString()}
icon={<Activity className="h-6 w-6" />}
trend="+8%"
isLoading={isLoading}
/>
<MetricCard
title="Total Requests"
value={isLoading ? '...' : metrics.totalRequests.toLocaleString()}
icon={<Users className="h-6 w-6" />}
trend="+23%"
isLoading={isLoading}
/>
<MetricCard
title="Avg Response Time"
value={isLoading ? '...' : `${metrics.avgResponseTime}ms`}
icon={<Clock className="h-6 w-6" />}
trend="-5%"
isLoading={isLoading}
/>
</div>
{/* Quick Actions */}
<Card className="mb-8">
<CardHeader>
<CardTitle>Quick Actions</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<ActionButton
title="Register Service"
description="Add a new service to your dashboard"
href="/services?action=register"
/>
<ActionButton
title="Verify Domain"
description="Verify domain ownership for your services"
href="/domains?action=verify"
/>
<ActionButton
title="View Analytics"
description="Monitor your service performance and usage"
href="/analytics"
/>
</div>
</CardContent>
</Card>
{/* Recent Activity */}
<Card>
<CardHeader>
<CardTitle>Recent Activity</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<ActivityItem
title="Service registered"
description="API Gateway service was successfully registered"
time="2 hours ago"
type="success"
/>
<ActivityItem
title="Domain verified"
description="Domain api.example.com verification completed"
time="5 hours ago"
type="success"
/>
<ActivityItem
title="API key generated"
description="New API key created for Data Service"
time="1 day ago"
type="info"
/>
</div>
</CardContent>
</Card>
</div>
</div>
);
}
interface MetricCardProps {
title: string;
value: string;
icon: React.ReactNode;
trend: string;
isLoading: boolean;
}
function MetricCard({ title, value, icon, trend, isLoading }: MetricCardProps) {
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">{title}</CardTitle>
<div className="text-muted-foreground">{icon}</div>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
{isLoading ? (
<div className="h-8 bg-muted rounded animate-pulse flex-1" />
) : (
<div className="text-2xl font-bold">{value}</div>
)}
<div className="text-xs text-green-600 ml-2">{trend}</div>
</div>
</CardContent>
</Card>
);
}
interface ActionButtonProps {
title: string;
description: string;
href: string;
}
function ActionButton({ title, description, href }: ActionButtonProps) {
return (
<a
href={href}
className="block p-4 border border-gray-200 rounded-lg hover:border-blue-300 hover:bg-blue-50 transition-colors"
>
<h3 className="font-medium text-gray-900">{title}</h3>
<p className="text-sm text-gray-600 mt-1">{description}</p>
</a>
);
}
interface ActivityItemProps {
title: string;
description: string;
time: string;
type: 'success' | 'info' | 'warning';
}
function ActivityItem({ title, description, time, type }: ActivityItemProps) {
const colors = {
success: 'bg-green-100 text-green-800',
info: 'bg-blue-100 text-blue-800',
warning: 'bg-yellow-100 text-yellow-800',
};
return (
<div className="flex items-start space-x-3">
<div className={`px-2 py-1 rounded-full text-xs ${colors[type]}`}>
{type === 'success' ? '✓' : type === 'info' ? 'i' : '!'}
</div>
<div className="flex-1">
<p className="text-sm font-medium text-gray-900">{title}</p>
<p className="text-sm text-gray-600">{description}</p>
<p className="text-xs text-gray-400 mt-1">{time}</p>
</div>
</div>
);
}
+29
View File
@@ -0,0 +1,29 @@
'use client';
import { useEffect } from 'react';
export default function TestEcho() {
useEffect(() => {
console.log('Testing browser-echo from dashboard');
console.info('This is an info message');
console.warn('This is a warning message');
console.error('This is an error message');
console.debug('This is a debug message');
}, []);
return (
<div className="p-8">
<h1 className="text-2xl font-bold mb-4">Browser Echo Test</h1>
<p>Check the terminal to see if console logs are being streamed!</p>
<button
type="button"
onClick={() => {
console.log('Button clicked at:', new Date().toISOString());
}}
className="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Click to test console.log
</button>
</div>
);
}
+326
View File
@@ -0,0 +1,326 @@
'use client';
import {
Alert,
AlertDescription,
Badge,
Button,
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
Input,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@sonr.io/ui';
import { CheckCircle, Copy, Eye, EyeOff, Key, Plus, RefreshCw, Trash2 } from 'lucide-react';
import { useState } from 'react';
interface APIKey {
id: string;
name: string;
key: string;
createdAt: string;
lastUsed?: string;
expiresAt?: string;
status: 'active' | 'expired' | 'revoked';
}
export function APIKeyManager() {
const [apiKeys, setApiKeys] = useState<APIKey[]>([
{
id: '1',
name: 'Production API Key',
key: 'sk_live_abc123...',
createdAt: '2024-01-15',
lastUsed: '2024-01-20',
status: 'active',
},
{
id: '2',
name: 'Development API Key',
key: 'sk_test_xyz789...',
createdAt: '2024-01-10',
lastUsed: '2024-01-19',
status: 'active',
},
]);
const [showKey, setShowKey] = useState<string | null>(null);
const [copiedKey, setCopiedKey] = useState<string | null>(null);
const [showCreateDialog, setShowCreateDialog] = useState(false);
const [newKeyName, setNewKeyName] = useState('');
const [newKey, setNewKey] = useState<string | null>(null);
const [isCreating, setIsCreating] = useState(false);
const handleCreateKey = async () => {
setIsCreating(true);
try {
// TODO: Implement actual API key creation
await new Promise((resolve) => setTimeout(resolve, 1500));
const generatedKey = `sk_${Math.random().toString(36).substring(2, 15)}`;
setNewKey(generatedKey);
const newApiKey: APIKey = {
id: Date.now().toString(),
name: newKeyName,
key: generatedKey,
createdAt: new Date().toISOString().split('T')[0],
status: 'active',
};
setApiKeys((prev) => [...prev, newApiKey]);
setNewKeyName('');
} catch (error) {
console.error('Failed to create API key:', error);
} finally {
setIsCreating(false);
}
};
const handleCopyKey = (key: string) => {
navigator.clipboard.writeText(key);
setCopiedKey(key);
setTimeout(() => setCopiedKey(null), 2000);
};
const handleRevokeKey = async (keyId: string) => {
try {
// TODO: Implement actual API key revocation
setApiKeys((prev) =>
prev.map((key) => (key.id === keyId ? { ...key, status: 'revoked' as const } : key))
);
} catch (error) {
console.error('Failed to revoke API key:', error);
}
};
const handleRegenerateKey = async (keyId: string) => {
try {
// TODO: Implement actual API key regeneration
const newKey = `sk_${Math.random().toString(36).substring(2, 15)}`;
setApiKeys((prev) => prev.map((key) => (key.id === keyId ? { ...key, key: newKey } : key)));
} catch (error) {
console.error('Failed to regenerate API key:', error);
}
};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold">API Keys</h3>
<p className="text-sm text-muted-foreground">
Manage API keys for authenticating requests
</p>
</div>
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
<DialogTrigger asChild>
<Button>
<Plus className="mr-2 h-4 w-4" />
Create API Key
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create New API Key</DialogTitle>
<DialogDescription>Generate a new API key for your service</DialogDescription>
</DialogHeader>
{!newKey ? (
<div className="space-y-4">
<div className="space-y-2">
<label htmlFor="keyName" className="text-sm font-medium">
Key Name
</label>
<Input
id="keyName"
value={newKeyName}
onChange={(e) => setNewKeyName(e.target.value)}
placeholder="e.g., Production API Key"
/>
</div>
<Button
onClick={handleCreateKey}
disabled={!newKeyName || isCreating}
className="w-full"
>
{isCreating ? (
<>
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
Creating...
</>
) : (
'Create Key'
)}
</Button>
</div>
) : (
<div className="space-y-4">
<Alert>
<CheckCircle className="h-4 w-4" />
<AlertDescription>
API key created successfully! Copy it now as it won't be shown again.
</AlertDescription>
</Alert>
<div className="space-y-2">
<label htmlFor="generatedKey" className="text-sm font-medium">
Your API Key
</label>
<div className="flex items-center gap-2">
<Code id="generatedKey" className="flex-1 p-2 text-xs font-mono">
{newKey}
</Code>
<Button size="sm" variant="outline" onClick={() => handleCopyKey(newKey)}>
{copiedKey === newKey ? (
<CheckCircle className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
</div>
<Button
onClick={() => {
setShowCreateDialog(false);
setNewKey(null);
}}
className="w-full"
>
Done
</Button>
</div>
)}
</DialogContent>
</Dialog>
</div>
{/* API Keys Table */}
<Card>
<CardHeader>
<CardTitle className="text-base">Active API Keys</CardTitle>
<CardDescription>View and manage your service API keys</CardDescription>
</CardHeader>
<CardContent>
{apiKeys.length > 0 ? (
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Key</TableHead>
<TableHead>Created</TableHead>
<TableHead>Last Used</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{apiKeys.map((apiKey) => (
<TableRow key={apiKey.id}>
<TableCell className="font-medium">{apiKey.name}</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<code className="text-xs">
{showKey === apiKey.id ? apiKey.key : `${apiKey.key.substring(0, 10)}...`}
</code>
<Button
size="sm"
variant="ghost"
onClick={() => setShowKey(showKey === apiKey.id ? null : apiKey.id)}
>
{showKey === apiKey.id ? (
<EyeOff className="h-3 w-3" />
) : (
<Eye className="h-3 w-3" />
)}
</Button>
<Button size="sm" variant="ghost" onClick={() => handleCopyKey(apiKey.key)}>
{copiedKey === apiKey.key ? (
<CheckCircle className="h-3 w-3" />
) : (
<Copy className="h-3 w-3" />
)}
</Button>
</div>
</TableCell>
<TableCell>{apiKey.createdAt}</TableCell>
<TableCell>{apiKey.lastUsed || '-'}</TableCell>
<TableCell>
<Badge
variant={
apiKey.status === 'active'
? 'success'
: apiKey.status === 'expired'
? 'warning'
: 'destructive'
}
>
{apiKey.status}
</Badge>
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Button
size="sm"
variant="ghost"
onClick={() => handleRegenerateKey(apiKey.id)}
disabled={apiKey.status === 'revoked'}
>
<RefreshCw className="h-3 w-3" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => handleRevokeKey(apiKey.id)}
disabled={apiKey.status === 'revoked'}
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
) : (
<div className="text-center py-8">
<Key className="mx-auto h-12 w-12 text-muted-foreground mb-4" />
<p className="text-muted-foreground mb-4">No API keys created yet</p>
<Button onClick={() => setShowCreateDialog(true)}>Create Your First API Key</Button>
</div>
)}
</CardContent>
</Card>
{/* Usage Instructions */}
<Alert>
<AlertDescription>
Use your API key in the <code>Authorization</code> header:{' '}
<code>Bearer YOUR_API_KEY</code>
</AlertDescription>
</Alert>
</div>
);
}
function Code({ children, className }: { children: React.ReactNode; className?: string }) {
return (
<div className={`bg-muted rounded px-2 py-1 font-mono ${className || ''}`}>{children}</div>
);
}
@@ -0,0 +1,167 @@
'use client';
import { Alert, AlertDescription, Badge, Button, Card, CardContent, Progress } from '@sonr.io/ui';
import { CheckCircle, Clock, Copy, ExternalLink, RefreshCw, XCircle } from 'lucide-react';
import { useEffect, useState } from 'react';
interface DomainVerificationStatusProps {
domain: string;
status?: 'verified' | 'pending' | 'failed' | 'unverified';
}
export function DomainVerificationStatus({
domain,
status = 'unverified',
}: DomainVerificationStatusProps) {
const [verificationStatus, setVerificationStatus] = useState(status);
const [isVerifying, setIsVerifying] = useState(false);
const [copySuccess, setCopySuccess] = useState(false);
const txtRecord = `sonr-verify=${domain.replace(/^https?:\/\//, '')}-${Date.now()}`;
const handleVerify = async () => {
setIsVerifying(true);
try {
// TODO: Implement actual domain verification API call
await new Promise((resolve) => setTimeout(resolve, 2000));
setVerificationStatus('verified');
} catch (_error) {
setVerificationStatus('failed');
} finally {
setIsVerifying(false);
}
};
const handleCopyTxtRecord = () => {
navigator.clipboard.writeText(txtRecord);
setCopySuccess(true);
setTimeout(() => setCopySuccess(false), 2000);
};
const getStatusIcon = () => {
switch (verificationStatus) {
case 'verified':
return <CheckCircle className="h-5 w-5 text-green-500" />;
case 'pending':
return <Clock className="h-5 w-5 text-yellow-500" />;
case 'failed':
return <XCircle className="h-5 w-5 text-red-500" />;
default:
return <Clock className="h-5 w-5 text-gray-500" />;
}
};
const getStatusBadge = () => {
switch (verificationStatus) {
case 'verified':
return <Badge variant="success">Verified</Badge>;
case 'pending':
return <Badge variant="warning">Pending</Badge>;
case 'failed':
return <Badge variant="destructive">Failed</Badge>;
default:
return <Badge variant="secondary">Unverified</Badge>;
}
};
return (
<div className="space-y-4">
{/* Status Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{getStatusIcon()}
<div>
<p className="font-medium">{domain}</p>
<p className="text-sm text-muted-foreground">Domain ownership verification</p>
</div>
</div>
{getStatusBadge()}
</div>
{/* Verification Steps */}
{verificationStatus !== 'verified' && (
<Card>
<CardContent className="pt-6">
<h4 className="font-medium mb-4">Verification Steps</h4>
<div className="space-y-4">
<div className="space-y-2">
<p className="text-sm font-medium">1. Add TXT Record to DNS</p>
<div className="flex items-center gap-2">
<code className="flex-1 p-2 text-xs bg-muted rounded font-mono">{txtRecord}</code>
<Button size="sm" variant="outline" onClick={handleCopyTxtRecord}>
{copySuccess ? (
<CheckCircle className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
</div>
<div className="space-y-2">
<p className="text-sm font-medium">2. Wait for DNS Propagation</p>
<p className="text-xs text-muted-foreground">
This typically takes 5-30 minutes but can take up to 48 hours
</p>
<Progress value={33} className="h-2" />
</div>
<div className="space-y-2">
<p className="text-sm font-medium">3. Verify Domain Ownership</p>
<Button onClick={handleVerify} disabled={isVerifying} className="w-full">
{isVerifying ? (
<>
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
Verifying...
</>
) : (
'Verify Now'
)}
</Button>
</div>
</div>
</CardContent>
</Card>
)}
{/* Success Message */}
{verificationStatus === 'verified' && (
<Alert>
<CheckCircle className="h-4 w-4" />
<AlertDescription>
Domain verification successful! Your service is now linked to {domain}.
</AlertDescription>
</Alert>
)}
{/* Failed Message */}
{verificationStatus === 'failed' && (
<Alert variant="destructive">
<XCircle className="h-4 w-4" />
<AlertDescription>
Domain verification failed. Please check your DNS records and try again.
</AlertDescription>
</Alert>
)}
{/* Help Link */}
<div className="flex items-center justify-between text-sm">
<a
href="/docs/domain-verification"
className="text-primary hover:underline flex items-center gap-1"
>
Domain verification help
<ExternalLink className="h-3 w-3" />
</a>
{verificationStatus === 'pending' && (
<Button variant="ghost" size="sm" onClick={handleVerify}>
<RefreshCw className="mr-2 h-3 w-3" />
Check Status
</Button>
)}
</div>
</div>
);
}
@@ -0,0 +1,210 @@
'use client';
import {
Alert,
AlertDescription,
Badge,
Button,
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
Checkbox,
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@sonr.io/ui';
import { Edit, Info, Plus, Shield, Trash2 } from 'lucide-react';
import { useState } from 'react';
interface Permission {
id: string;
name: string;
description: string;
scope: string;
granted: boolean;
}
interface PermissionManagerProps {
serviceId: string;
permissions?: Permission[];
}
export function PermissionManager({ permissions = [] }: PermissionManagerProps) {
const [selectedPermissions, setSelectedPermissions] = useState<string[]>([]);
const [showAddDialog, setShowAddDialog] = useState(false);
const permissionScopes = {
'read:profile': 'Read user profile information',
'write:profile': 'Update user profile',
'read:data': 'Access user data',
'write:data': 'Modify user data',
'read:credentials': 'View credentials',
'manage:credentials': 'Create and manage credentials',
'read:vault': 'Access vault contents',
'manage:vault': 'Full vault management',
'execute:transactions': 'Execute blockchain transactions',
'delegate:permissions': 'Delegate permissions to others',
};
const handlePermissionToggle = (permissionId: string) => {
setSelectedPermissions((prev) =>
prev.includes(permissionId)
? prev.filter((id) => id !== permissionId)
: [...prev, permissionId]
);
};
const handleSavePermissions = async () => {
try {
// TODO: Implement API call to save permissions
console.log('Saving permissions:', selectedPermissions);
} catch (error) {
console.error('Failed to save permissions:', error);
}
};
return (
<div className="space-y-6">
{/* Permission Summary */}
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold">Current Permissions</h3>
<p className="text-sm text-muted-foreground">
{permissions.filter((p) => p.granted).length} of {permissions.length} permissions
granted
</p>
</div>
<Dialog open={showAddDialog} onOpenChange={setShowAddDialog}>
<DialogTrigger asChild>
<Button>
<Plus className="mr-2 h-4 w-4" />
Add Permission
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Add New Permission</DialogTitle>
<DialogDescription>Select permissions to add to your service</DialogDescription>
</DialogHeader>
<div className="grid grid-cols-1 gap-3 mt-4">
{Object.entries(permissionScopes).map(([scope, description]) => (
<label
key={scope}
htmlFor={`permission-${scope}`}
className="flex items-start space-x-3 p-3 rounded-lg border cursor-pointer hover:bg-muted/50"
>
<Checkbox
id={`permission-${scope}`}
checked={selectedPermissions.includes(scope)}
onCheckedChange={() => handlePermissionToggle(scope)}
/>
<div className="space-y-1">
<div className="font-medium text-sm">{scope}</div>
<div className="text-xs text-muted-foreground">{description}</div>
</div>
</label>
))}
</div>
<div className="flex justify-end gap-2 mt-4">
<Button variant="outline" onClick={() => setShowAddDialog(false)}>
Cancel
</Button>
<Button onClick={handleSavePermissions}>Add Permissions</Button>
</div>
</DialogContent>
</Dialog>
</div>
{/* UCAN Capabilities */}
<Card>
<CardHeader>
<CardTitle className="text-base">UCAN Capabilities</CardTitle>
<CardDescription>User-Controlled Authorization Network permissions</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
{permissions.length > 0 ? (
permissions.map((permission) => (
<div
key={permission.id}
className="flex items-center justify-between p-3 rounded-lg border"
>
<div className="flex items-center gap-3">
<Shield className="h-4 w-4 text-muted-foreground" />
<div>
<div className="font-medium text-sm">{permission.name}</div>
<div className="text-xs text-muted-foreground">{permission.description}</div>
</div>
</div>
<div className="flex items-center gap-2">
<Badge variant={permission.granted ? 'success' : 'secondary'}>
{permission.granted ? 'Granted' : 'Pending'}
</Badge>
<Button size="sm" variant="ghost">
<Edit className="h-3 w-3" />
</Button>
<Button size="sm" variant="ghost">
<Trash2 className="h-3 w-3" />
</Button>
</div>
</div>
))
) : (
<div className="text-center py-6 text-muted-foreground">
No permissions configured yet
</div>
)}
</div>
</CardContent>
</Card>
{/* Permission Audit Log */}
<Card>
<CardHeader>
<CardTitle className="text-base">Permission Audit Log</CardTitle>
<CardDescription>Recent permission changes and access attempts</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2">
{[
{
id: 'log1',
action: 'Permission granted',
scope: 'read:profile',
time: '2 hours ago',
},
{ id: 'log2', action: 'Permission revoked', scope: 'write:data', time: '1 day ago' },
{ id: 'log3', action: 'Access attempted', scope: 'manage:vault', time: '3 days ago' },
].map((log) => (
<div key={log.id} className="flex items-center justify-between py-2 text-sm">
<div>
<span className="font-medium">{log.action}:</span>{' '}
<code className="text-xs bg-muted px-1 py-0.5 rounded">{log.scope}</code>
</div>
<span className="text-xs text-muted-foreground">{log.time}</span>
</div>
))}
</div>
</CardContent>
</Card>
{/* Info Alert */}
<Alert>
<Info className="h-4 w-4" />
<AlertDescription>
Permissions are managed through UCAN tokens. Changes may take up to 5 minutes to
propagate.
</AlertDescription>
</Alert>
</div>
);
}
@@ -0,0 +1,267 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import {
Alert,
AlertDescription,
Button,
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
Input,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Stepper,
StepperItem,
Textarea,
} from '@sonr.io/ui';
import { Loader2 } from 'lucide-react';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import * as z from 'zod';
const serviceSchema = z.object({
name: z.string().min(3, 'Service name must be at least 3 characters'),
description: z.string().min(10, 'Description must be at least 10 characters'),
domain: z.string().url('Must be a valid domain'),
category: z.string().min(1, 'Please select a category'),
permissions: z.array(z.string()).min(1, 'Select at least one permission'),
});
type ServiceFormData = z.infer<typeof serviceSchema>;
interface ServiceRegistrationFormProps {
onSuccess?: () => void;
}
export function ServiceRegistrationForm({ onSuccess }: ServiceRegistrationFormProps) {
const [isSubmitting, setIsSubmitting] = useState(false);
const [currentStep, setCurrentStep] = useState(0);
const [error, setError] = useState<string | null>(null);
const form = useForm<ServiceFormData>({
resolver: zodResolver(serviceSchema),
defaultValues: {
name: '',
description: '',
domain: '',
category: '',
permissions: [],
},
});
const onSubmit = async (data: ServiceFormData) => {
setIsSubmitting(true);
setError(null);
try {
// TODO: Implement actual service registration API call
const response = await fetch('/api/services', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error('Failed to register service');
}
onSuccess?.();
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setIsSubmitting(false);
}
};
const steps = [
{ title: 'Basic Info', description: 'Service name and description' },
{ title: 'Domain', description: 'Configure domain verification' },
{ title: 'Permissions', description: 'Set required permissions' },
];
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
{error && (
<Alert variant="destructive">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<div className="space-y-4">
{/* Step 1: Basic Info */}
<div className={currentStep === 0 ? 'block' : 'hidden'}>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Service Name</FormLabel>
<FormControl>
<Input placeholder="My Awesome Service" {...field} />
</FormControl>
<FormDescription>A unique name for your service</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem className="mt-4">
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea
placeholder="Describe what your service does..."
{...field}
rows={4}
/>
</FormControl>
<FormDescription>Help users understand your service's purpose</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="category"
render={({ field }) => (
<FormItem className="mt-4">
<FormLabel>Category</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a category" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="api">API Service</SelectItem>
<SelectItem value="webapp">Web Application</SelectItem>
<SelectItem value="mobile">Mobile App</SelectItem>
<SelectItem value="iot">IoT Device</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectContent>
</Select>
<FormDescription>Choose the category that best fits your service</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* Step 2: Domain */}
<div className={currentStep === 1 ? 'block' : 'hidden'}>
<FormField
control={form.control}
name="domain"
render={({ field }) => (
<FormItem>
<FormLabel>Domain</FormLabel>
<FormControl>
<Input placeholder="https://example.com" {...field} />
</FormControl>
<FormDescription>The domain where your service is hosted</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Alert className="mt-4">
<AlertDescription>
After registration, you'll need to verify domain ownership by adding a TXT record to
your DNS.
</AlertDescription>
</Alert>
</div>
{/* Step 3: Permissions */}
<div className={currentStep === 2 ? 'block' : 'hidden'}>
<FormField
control={form.control}
name="permissions"
render={({ field }) => (
<FormItem>
<FormLabel>Required Permissions</FormLabel>
<div className="space-y-2 mt-2">
{['read:profile', 'write:data', 'read:credentials', 'manage:vault'].map(
(permission) => (
<label
key={permission}
className="flex items-center space-x-2 cursor-pointer"
>
<input
type="checkbox"
value={permission}
checked={field.value?.includes(permission)}
onChange={(e) => {
const value = e.target.value;
const current = field.value || [];
if (e.target.checked) {
field.onChange([...current, value]);
} else {
field.onChange(current.filter((v) => v !== value));
}
}}
className="rounded border-gray-300"
/>
<span className="text-sm">{permission}</span>
</label>
)
)}
</div>
<FormDescription className="mt-2">
Select the permissions your service requires
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
{/* Navigation */}
<div className="flex justify-between pt-4">
<Button
type="button"
variant="outline"
onClick={() => setCurrentStep((prev) => Math.max(0, prev - 1))}
disabled={currentStep === 0}
>
Previous
</Button>
{currentStep < steps.length - 1 ? (
<Button
type="button"
onClick={() => setCurrentStep((prev) => Math.min(steps.length - 1, prev + 1))}
>
Next
</Button>
) : (
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Registering...
</>
) : (
'Register Service'
)}
</Button>
)}
</div>
</form>
</Form>
);
}
+53
View File
@@ -0,0 +1,53 @@
'use client';
import { useEffect, useState } from 'react';
interface AuthWrapperProps {
children: React.ReactNode;
}
export function AuthWrapper({ children }: AuthWrapperProps) {
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
useEffect(() => {
// Check authentication status
const checkAuth = () => {
// In development mode, skip authentication for easier testing
if (process.env.NODE_ENV === 'development') {
setIsAuthenticated(true);
return;
}
// Check for auth token in localStorage or cookie
const authToken =
typeof window !== 'undefined' ? localStorage.getItem('sonr_auth_token') : null;
if (!authToken) {
// Redirect to auth app
window.location.href = process.env.NEXT_PUBLIC_AUTH_URL || 'https://auth.sonr.io';
return;
}
setIsAuthenticated(true);
};
checkAuth();
}, []);
// Show loading while checking auth
if (isAuthenticated === null) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900" />
</div>
);
}
// Show children if authenticated
if (isAuthenticated) {
return <>{children}</>;
}
// Should not reach here due to redirect
return null;
}
@@ -0,0 +1,74 @@
'use client';
import { createContext, useContext, useEffect, useState } from 'react';
type Theme = 'dark' | 'light' | 'system';
type ThemeProviderProps = {
children: React.ReactNode;
defaultTheme?: Theme;
storageKey?: string;
};
type ThemeProviderState = {
theme: Theme;
setTheme: (theme: Theme) => void;
};
const initialState: ThemeProviderState = {
theme: 'system',
setTheme: () => null,
};
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
export function ThemeProvider({
children,
defaultTheme = 'system',
storageKey = 'sonr-ui-theme',
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(
() =>
((typeof window !== 'undefined' && localStorage.getItem(storageKey)) as Theme) || defaultTheme
);
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove('light', 'dark');
if (theme === 'system') {
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
root.classList.add(systemTheme);
return;
}
root.classList.add(theme);
}, [theme]);
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme);
setTheme(theme);
},
};
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
);
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext);
if (context === undefined) throw new Error('useTheme must be used within a ThemeProvider');
return context;
};
+230
View File
@@ -0,0 +1,230 @@
import type { DomainVerification } from '@sonr.io/com/types';
import { useCallback, useEffect, useRef, useState } from 'react';
import { svcApi } from '../lib/api';
interface UseDomainVerificationReturn {
verification: DomainVerification | null;
isVerifying: boolean;
isPolling: boolean;
error: Error | null;
startVerification: (domain: string) => Promise<void>;
checkStatus: (domain: string) => Promise<void>;
startPolling: (domain: string) => void;
stopPolling: () => void;
}
interface PollingConfig {
interval?: number;
maxAttempts?: number;
onSuccess?: (verification: DomainVerification) => void;
onFailure?: (error: Error) => void;
}
/**
* Hook for domain verification with polling support
*/
export function useDomainVerification(
initialDomain?: string,
config?: PollingConfig
): UseDomainVerificationReturn {
const [verification, setVerification] = useState<DomainVerification | null>(null);
const [isVerifying, setIsVerifying] = useState(false);
const [isPolling, setIsPolling] = useState(false);
const [error, setError] = useState<Error | null>(null);
const pollingIntervalRef = useRef<NodeJS.Timeout | null>(null);
const pollingAttemptsRef = useRef(0);
const currentDomainRef = useRef<string | null>(initialDomain || null);
const { interval = 5000, maxAttempts = 60, onSuccess, onFailure } = config || {};
/**
* Check domain verification status once
*/
const checkStatus = useCallback(
async (domain: string) => {
setError(null);
try {
const status = await svcApi.checkDomainStatus(domain);
if (!status) {
throw new Error('Failed to fetch domain status');
}
setVerification(status);
// Check if verification is complete
if (status.status === 'DOMAIN_VERIFICATION_STATUS_VERIFIED') {
onSuccess?.(status);
stopPolling();
} else if (status.status === 'DOMAIN_VERIFICATION_STATUS_FAILED') {
const error = new Error('Domain verification failed');
setError(error);
onFailure?.(error);
stopPolling();
}
return status;
} catch (err) {
const error = err instanceof Error ? err : new Error('Failed to check domain status');
setError(error);
throw error;
}
},
[onSuccess, onFailure]
);
/**
* Start domain verification process
*/
const startVerification = useCallback(
async (domain: string) => {
setIsVerifying(true);
setError(null);
currentDomainRef.current = domain;
try {
// In a real implementation, this would initiate the verification
// For now, we just start polling the status
await checkStatus(domain);
startPolling(domain);
} catch (err) {
const error = err instanceof Error ? err : new Error('Failed to start verification');
setError(error);
onFailure?.(error);
} finally {
setIsVerifying(false);
}
},
[checkStatus, onFailure]
);
/**
* Start polling for verification status
*/
const startPolling = useCallback(
(domain: string) => {
// Clear any existing polling
stopPolling();
setIsPolling(true);
pollingAttemptsRef.current = 0;
currentDomainRef.current = domain;
// Set up polling interval
pollingIntervalRef.current = setInterval(async () => {
pollingAttemptsRef.current++;
// Check if we've exceeded max attempts
if (pollingAttemptsRef.current >= maxAttempts) {
const error = new Error('Domain verification timeout');
setError(error);
onFailure?.(error);
stopPolling();
return;
}
try {
await checkStatus(domain);
} catch (err) {
// Continue polling even if individual checks fail
console.error('Polling check failed:', err);
// Stop polling after multiple consecutive failures
if (pollingAttemptsRef.current > 3) {
const error = err instanceof Error ? err : new Error('Polling failed');
setError(error);
onFailure?.(error);
stopPolling();
}
}
}, interval);
},
[interval, maxAttempts, checkStatus, onFailure]
);
/**
* Stop polling
*/
const stopPolling = useCallback(() => {
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
pollingIntervalRef.current = null;
}
setIsPolling(false);
pollingAttemptsRef.current = 0;
}, []);
/**
* Clean up on unmount
*/
useEffect(() => {
return () => {
stopPolling();
};
}, [stopPolling]);
/**
* Auto-start polling if initial domain is provided
*/
useEffect(() => {
if (initialDomain) {
startVerification(initialDomain);
}
}, [initialDomain, startVerification]);
return {
verification,
isVerifying,
isPolling,
error,
startVerification,
checkStatus,
startPolling,
stopPolling,
};
}
/**
* Hook for managing multiple domain verifications
*/
export function useDomainVerifications() {
const [verifications, setVerifications] = useState<Map<string, DomainVerification>>(new Map());
const [activePolls, setActivePolls] = useState<Set<string>>(new Set());
const addVerification = useCallback((domain: string, verification: DomainVerification) => {
setVerifications((prev) => new Map(prev).set(domain, verification));
}, []);
const removeVerification = useCallback((domain: string) => {
setVerifications((prev) => {
const next = new Map(prev);
next.delete(domain);
return next;
});
}, []);
const startPollingFor = useCallback((domain: string) => {
setActivePolls((prev) => new Set(prev).add(domain));
}, []);
const stopPollingFor = useCallback((domain: string) => {
setActivePolls((prev) => {
const next = new Set(prev);
next.delete(domain);
return next;
});
}, []);
return {
verifications: Array.from(verifications.values()),
activePolls: Array.from(activePolls),
addVerification,
removeVerification,
startPollingFor,
stopPollingFor,
isPolling: (domain: string) => activePolls.has(domain),
getVerification: (domain: string) => verifications.get(domain),
};
}
+259
View File
@@ -0,0 +1,259 @@
import type { Service } from '@sonr.io/com';
import { useCallback, useEffect, useRef, useState } from 'react';
import { svcApi } from '../lib/api';
interface UseServiceReturn {
service: Service | null;
isLoading: boolean;
error: Error | null;
refetch: () => Promise<void>;
mutate: (updates: Partial<Service>) => void;
subscribe: (callback: (service: Service) => void) => () => void;
}
// Service cache for optimistic updates
const serviceCache = new Map<string, Service>();
// Subscribers for real-time updates
const serviceSubscribers = new Map<string, Set<(service: Service) => void>>();
/**
* Hook for individual service data with real-time updates
*/
export function useService(serviceId: string): UseServiceReturn {
const [service, setService] = useState<Service | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const retryTimeoutRef = useRef<NodeJS.Timeout | null>(null);
/**
* Fetch service with error recovery
*/
const fetchService = useCallback(async () => {
if (!serviceId) {
setError(new Error('Service ID is required'));
setIsLoading(false);
return;
}
setIsLoading(true);
setError(null);
// Check cache first
const cached = serviceCache.get(serviceId);
if (cached) {
setService(cached);
setIsLoading(false);
}
try {
const fetchedService = await svcApi.getServiceDetails(serviceId);
if (!fetchedService) {
throw new Error('Service not found');
}
// Update cache
serviceCache.set(serviceId, fetchedService);
// Update state
setService(fetchedService);
// Notify subscribers
const subscribers = serviceSubscribers.get(serviceId);
if (subscribers) {
for (const callback of subscribers) {
callback(fetchedService);
}
}
} catch (err) {
setError(err instanceof Error ? err : new Error('Unknown error'));
// Retry with exponential backoff
if (!retryTimeoutRef.current) {
retryTimeoutRef.current = setTimeout(() => {
retryTimeoutRef.current = null;
fetchService();
}, 5000);
}
// Use mock data for development
if (process.env.NODE_ENV === 'development') {
const mockService = getMockService(serviceId);
setService(mockService);
serviceCache.set(serviceId, mockService);
}
} finally {
setIsLoading(false);
}
}, [serviceId]);
/**
* Optimistic update
*/
const mutate = useCallback(
(updates: Partial<Service>) => {
if (!service) return;
const updated = { ...service, ...updates };
// Update cache
serviceCache.set(serviceId, updated);
// Update state
setService(updated);
// Notify subscribers
const subscribers = serviceSubscribers.get(serviceId);
if (subscribers) {
for (const callback of subscribers) {
callback(updated);
}
}
// Sync with server in background
svcApi
.getServiceDetails(serviceId)
.then((freshService) => {
if (freshService) {
serviceCache.set(serviceId, freshService);
setService(freshService);
}
})
.catch(console.error);
},
[service, serviceId]
);
/**
* Subscribe to real-time updates
*/
const subscribe = useCallback(
(callback: (service: Service) => void) => {
if (!serviceSubscribers.has(serviceId)) {
serviceSubscribers.set(serviceId, new Set());
}
const subscribers = serviceSubscribers.get(serviceId);
if (subscribers) {
subscribers.add(callback);
}
// Return unsubscribe function
return () => {
subscribers.delete(callback);
if (subscribers.size === 0) {
serviceSubscribers.delete(serviceId);
}
};
},
[serviceId]
);
// Initial fetch
useEffect(() => {
fetchService();
}, [fetchService]);
// Clean up on unmount
useEffect(() => {
return () => {
if (retryTimeoutRef.current) {
clearTimeout(retryTimeoutRef.current);
}
};
}, []);
// Set up real-time updates (WebSocket in production)
useEffect(() => {
if (process.env.NODE_ENV === 'production') {
// TODO: Implement WebSocket connection for real-time updates
// const ws = new WebSocket(`${wsEndpoint}/services/${serviceId}`);
// ws.onmessage = (event) => {
// const updatedService = JSON.parse(event.data);
// serviceCache.set(serviceId, updatedService);
// setService(updatedService);
// };
}
// Simulate real-time updates in development
if (process.env.NODE_ENV === 'development') {
const interval = setInterval(() => {
if (service) {
const updated = {
...service,
metadata: {
...service.metadata,
totalRequests:
(service.metadata?.totalRequests || 0) + Math.floor(Math.random() * 100),
},
};
mutate(updated);
}
}, 30000); // Update every 30 seconds
return () => clearInterval(interval);
}
}, [service, mutate]);
return {
service,
isLoading,
error,
refetch: fetchService,
mutate,
subscribe,
};
}
/**
* Mock service data for development
*/
function getMockService(serviceId: string): Service {
return {
id: serviceId,
name: 'My API Service',
description: 'A powerful API service for data processing and management',
domain: 'api.example.com',
status: 'active',
owner: 'did:sonr:alice',
createdAt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(),
updatedAt: new Date().toISOString(),
permissions: [
{
id: 'perm_1',
name: 'read:data',
description: 'Read user data',
scope: 'data',
granted: true,
},
{
id: 'perm_2',
name: 'write:data',
description: 'Write user data',
scope: 'data',
granted: true,
},
{
id: 'perm_3',
name: 'manage:vault',
description: 'Manage vault contents',
scope: 'vault',
granted: false,
},
],
apiKeys: [
{
id: 'key_1',
name: 'Production Key',
lastUsed: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(),
},
],
domainVerificationStatus: 'verified',
metadata: {
totalRequests: 142523,
activeUsers: 1250,
averageLatency: 45,
},
};
}
+99
View File
@@ -0,0 +1,99 @@
import { useCallback, useEffect, useState } from 'react';
interface ServiceMetrics {
totalRequests: number;
successRate: number;
averageLatency: number;
errorRate: number;
requestsPerMinute: number[];
topEndpoints: Array<{
endpoint: string;
count: number;
averageLatency: number;
}>;
dailyStats: Array<{
date: string;
requests: number;
errors: number;
}>;
}
interface UseServiceMetricsReturn {
metrics: ServiceMetrics | null;
isLoading: boolean;
error: Error | null;
refetch: () => Promise<void>;
}
export function useServiceMetrics(serviceId: string): UseServiceMetricsReturn {
const [metrics, setMetrics] = useState<ServiceMetrics | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const fetchMetrics = useCallback(async () => {
if (!serviceId) {
setError(new Error('Service ID is required'));
setIsLoading(false);
return;
}
setIsLoading(true);
setError(null);
try {
// TODO: Replace with actual API call
const response = await fetch(`/api/services/${serviceId}/metrics`);
if (!response.ok) {
throw new Error('Failed to fetch metrics');
}
const data = await response.json();
setMetrics(data.metrics);
} catch (err) {
setError(err instanceof Error ? err : new Error('Unknown error'));
// Mock data for development
setMetrics({
totalRequests: 142523,
successRate: 99.2,
averageLatency: 45,
errorRate: 0.8,
requestsPerMinute: Array.from({ length: 60 }, () => Math.floor(Math.random() * 100) + 20),
topEndpoints: [
{ endpoint: '/api/v1/data', count: 45230, averageLatency: 32 },
{ endpoint: '/api/v1/auth', count: 28450, averageLatency: 28 },
{ endpoint: '/api/v1/users', count: 18230, averageLatency: 45 },
{ endpoint: '/api/v1/vault', count: 12450, averageLatency: 67 },
{ endpoint: '/api/v1/credentials', count: 8230, averageLatency: 52 },
],
dailyStats: Array.from({ length: 30 }, (_, i) => {
const date = new Date();
date.setDate(date.getDate() - (29 - i));
return {
date: date.toISOString().split('T')[0],
requests: Math.floor(Math.random() * 5000) + 3000,
errors: Math.floor(Math.random() * 50) + 10,
};
}),
});
} finally {
setIsLoading(false);
}
}, [serviceId]);
useEffect(() => {
fetchMetrics();
// Poll for updates every 30 seconds
const interval = setInterval(fetchMetrics, 30000);
return () => clearInterval(interval);
}, [fetchMetrics]);
return {
metrics,
isLoading,
error,
refetch: fetchMetrics,
};
}
+177
View File
@@ -0,0 +1,177 @@
import type { Service } from '@sonr.io/com';
import { useCallback, useEffect, useRef, useState } from 'react';
import { authApi, svcApi } from '../lib/api';
interface UseServicesReturn {
services: Service[] | null;
isLoading: boolean;
error: Error | null;
refetch: () => Promise<void>;
mutate: (updater: (services: Service[]) => Service[]) => void;
}
interface CacheEntry {
data: Service[];
timestamp: number;
}
// Cache configuration
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
const servicesCache = new Map<string, CacheEntry>();
export function useServices(): UseServicesReturn {
const [services, setServices] = useState<Service[] | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const retryCountRef = useRef(0);
const maxRetries = 3;
/**
* Fetch services with caching and error recovery
*/
const fetchServices = useCallback(async (forceRefresh = false) => {
const user = authApi.getCurrentUser();
if (!user?.address) {
setError(new Error('Authentication required'));
setIsLoading(false);
return;
}
const cacheKey = `services-${user.address}`;
// Check cache unless force refresh
if (!forceRefresh) {
const cached = servicesCache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
setServices(cached.data);
setIsLoading(false);
return;
}
}
setIsLoading(true);
setError(null);
try {
// Use the API client to fetch services
const fetchedServices = await svcApi.getMyServices(user.address);
// Update cache
servicesCache.set(cacheKey, {
data: fetchedServices,
timestamp: Date.now(),
});
setServices(fetchedServices);
retryCountRef.current = 0; // Reset retry count on success
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to fetch services';
// Implement exponential backoff for retries
if (retryCountRef.current < maxRetries) {
retryCountRef.current++;
const delay = Math.min(1000 * 2 ** retryCountRef.current, 10000);
setTimeout(() => {
fetchServices(forceRefresh);
}, delay);
setError(
new Error(`${errorMessage}. Retrying... (${retryCountRef.current}/${maxRetries})`)
);
} else {
setError(new Error(errorMessage));
// Fallback to cached data if available
const cached = servicesCache.get(cacheKey);
if (cached) {
setServices(cached.data);
} else {
// Use mock data as last resort in development
if (process.env.NODE_ENV === 'development') {
setServices(getMockServices());
}
}
}
} finally {
setIsLoading(false);
}
}, []);
/**
* Optimistic update function
*/
const mutate = useCallback((updater: (services: Service[]) => Service[]) => {
setServices((current) => {
if (!current) return null;
const updated = updater(current);
// Update cache with mutated data
const user = authApi.getCurrentUser();
if (user?.address) {
servicesCache.set(`services-${user.address}`, {
data: updated,
timestamp: Date.now(),
});
}
return updated;
});
}, []);
// Initial fetch on mount
useEffect(() => {
fetchServices();
}, [fetchServices]);
// Set up periodic refresh
useEffect(() => {
const interval = setInterval(() => {
fetchServices(false); // Use cache if valid
}, CACHE_DURATION);
return () => clearInterval(interval);
}, [fetchServices]);
return {
services,
isLoading,
error,
refetch: () => fetchServices(true), // Force refresh on manual refetch
mutate,
};
}
/**
* Mock data for development
*/
function getMockServices(): Service[] {
return [
{
id: 'svc_1',
name: 'My API Service',
description: 'A powerful API service for data processing',
domain: 'api.example.com',
status: 'active',
owner: 'did:sonr:alice',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
permissions: ['read:data', 'write:data'],
apiKeys: [],
domainVerificationStatus: 'verified',
},
{
id: 'svc_2',
name: 'Web Application',
description: 'Main web application frontend',
domain: 'app.example.com',
status: 'pending',
owner: 'did:sonr:alice',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
permissions: ['read:profile', 'manage:vault'],
apiKeys: [],
domainVerificationStatus: 'pending',
},
];
}
+349
View File
@@ -0,0 +1,349 @@
// Temporary stub to fix module resolution - TODO: Replace with actual @sonr.io/es/client import
const getAccount = async (params: { address: string; rpcEndpoint: string }): Promise<any> => {
console.warn('Using stub implementation for getAccount');
return {
address: params.address,
accountNumber: '1',
sequence: '0',
pubKey: null,
};
};
import type { ApiResponse, AuthStatus, User } from '@sonr.io/com/types';
/**
* Authentication API Client
* Integrates with the web/auth WebAuthn authentication system
*/
export class AuthApiClient {
private authUrl: string;
private rpcEndpoint: string;
constructor(authUrl: string, rpcEndpoint: string) {
this.authUrl = authUrl;
this.rpcEndpoint = rpcEndpoint;
}
/**
* Check if user is authenticated
*/
async checkAuthStatus(): Promise<AuthStatus> {
try {
// Check for stored session
const session = this.getStoredSession();
if (!session) {
return {
isAuthenticated: false,
user: null,
};
}
// Verify session is still valid by checking account on chain
const account = await getAccount({
address: session.address,
rpcEndpoint: this.rpcEndpoint,
});
if (account) {
return {
isAuthenticated: true,
user: {
id: session.userId,
address: session.address,
username: session.username,
did: session.did,
createdAt: session.createdAt,
},
};
}
// Session invalid, clear it
this.clearSession();
return {
isAuthenticated: false,
user: null,
};
} catch (error) {
console.error('Auth status check failed:', error);
return {
isAuthenticated: false,
user: null,
};
}
}
/**
* Redirect to authentication app
*/
redirectToAuth(returnUrl?: string): void {
const currentUrl = returnUrl || window.location.href;
const authRedirectUrl = `${this.authUrl}/login?returnUrl=${encodeURIComponent(currentUrl)}`;
window.location.href = authRedirectUrl;
}
/**
* Handle authentication callback
*/
async handleAuthCallback(params: URLSearchParams): Promise<ApiResponse<User>> {
try {
const token = params.get('token');
const address = params.get('address');
const username = params.get('username');
const did = params.get('did');
if (!token || !address) {
return {
success: false,
error: 'Missing authentication parameters',
};
}
// Verify the token with the auth service
const verified = await this.verifyAuthToken(token, address);
if (!verified) {
return {
success: false,
error: 'Invalid authentication token',
};
}
// Store session
const user: User = {
id: did || address,
address,
username: username || 'User',
did: did || null,
createdAt: new Date().toISOString(),
};
this.storeSession({
userId: user.id,
address: user.address,
username: user.username,
did: user.did,
token,
createdAt: user.createdAt,
});
return {
success: true,
data: user,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Authentication failed',
};
}
}
/**
* Verify authentication token with auth service
*/
private async verifyAuthToken(token: string, address: string): Promise<boolean> {
try {
const response = await fetch(`${this.authUrl}/api/verify`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ token, address }),
});
if (!response.ok) {
return false;
}
const data = await response.json();
return data.valid === true;
} catch (error) {
console.error('Token verification failed:', error);
return false;
}
}
/**
* Sign out the current user
*/
async signOut(): Promise<void> {
try {
// Clear local session
this.clearSession();
// Notify auth service
await fetch(`${this.authUrl}/api/logout`, {
method: 'POST',
credentials: 'include',
});
} catch (error) {
console.error('Sign out error:', error);
}
}
/**
* Get current user from session
*/
getCurrentUser(): User | null {
const session = this.getStoredSession();
if (!session) {
return null;
}
return {
id: session.userId,
address: session.address,
username: session.username,
did: session.did,
createdAt: session.createdAt,
};
}
/**
* Store session in localStorage
*/
private storeSession(session: any): void {
if (typeof window !== 'undefined') {
localStorage.setItem('sonr_auth_session', JSON.stringify(session));
}
}
/**
* Get stored session from localStorage
*/
private getStoredSession(): any {
if (typeof window === 'undefined') {
return null;
}
try {
const sessionStr = localStorage.getItem('sonr_auth_session');
return sessionStr ? JSON.parse(sessionStr) : null;
} catch {
return null;
}
}
/**
* Clear stored session
*/
private clearSession(): void {
if (typeof window !== 'undefined') {
localStorage.removeItem('sonr_auth_session');
}
}
}
// Singleton instance
let authClient: AuthApiClient | null = null;
/**
* Get or create the auth API client
*/
export function getAuthApiClient(): AuthApiClient {
const authUrl = process.env.NEXT_PUBLIC_AUTH_URL || 'http://localhost:3001';
const rpcEndpoint = process.env.NEXT_PUBLIC_RPC_ENDPOINT || 'http://localhost:26657';
if (!authClient) {
authClient = new AuthApiClient(authUrl, rpcEndpoint);
}
return authClient;
}
/**
* Authentication helper functions
*/
export const authApi = {
/**
* Check if user is authenticated
*/
async isAuthenticated(): Promise<boolean> {
const client = getAuthApiClient();
const status = await client.checkAuthStatus();
return status.isAuthenticated;
},
/**
* Get current user
*/
getCurrentUser(): User | null {
const client = getAuthApiClient();
return client.getCurrentUser();
},
/**
* Require authentication (redirect if not authenticated)
*/
async requireAuth(): Promise<User | null> {
const client = getAuthApiClient();
const status = await client.checkAuthStatus();
if (!status.isAuthenticated) {
client.redirectToAuth();
return null;
}
return status.user;
},
/**
* Sign in (redirect to auth app)
*/
signIn(returnUrl?: string): void {
const client = getAuthApiClient();
client.redirectToAuth(returnUrl);
},
/**
* Sign out
*/
async signOut(): Promise<void> {
const client = getAuthApiClient();
await client.signOut();
window.location.href = '/';
},
/**
* Handle OAuth-style callback
*/
async handleCallback(): Promise<User | null> {
const client = getAuthApiClient();
const params = new URLSearchParams(window.location.search);
if (!params.has('token')) {
return null;
}
const result = await client.handleAuthCallback(params);
if (result.success) {
// Clear URL parameters
window.history.replaceState({}, '', window.location.pathname);
return result.data;
}
return null;
},
};
/**
* React hook helper for authentication
*/
export function useAuthCheck() {
if (typeof window === 'undefined') {
return { loading: true, authenticated: false, user: null };
}
const user = authApi.getCurrentUser();
return {
loading: false,
authenticated: !!user,
user,
};
}
export default authApi;
+215
View File
@@ -0,0 +1,215 @@
import { authApi } from './auth';
import { svcApi } from './svc';
/**
* Centralized API configuration and error handling
*/
export class ApiConfig {
private static instance: ApiConfig;
public rpcEndpoint: string;
public restEndpoint: string;
public authUrl: string;
public chainId: string;
public requestInterceptors: Array<(config: any) => any> = [];
public responseInterceptors: Array<(response: any) => any> = [];
private constructor() {
// Load from environment variables with defaults
this.rpcEndpoint = process.env.NEXT_PUBLIC_RPC_ENDPOINT || 'http://localhost:26657';
this.restEndpoint = process.env.NEXT_PUBLIC_REST_ENDPOINT || 'http://localhost:1317';
this.authUrl = process.env.NEXT_PUBLIC_AUTH_URL || 'http://localhost:3001';
this.chainId = process.env.NEXT_PUBLIC_CHAIN_ID || 'sonrtest_1-1';
}
static getInstance(): ApiConfig {
if (!ApiConfig.instance) {
ApiConfig.instance = new ApiConfig();
}
return ApiConfig.instance;
}
/**
* Configure endpoints
*/
configure(
config: Partial<{
rpcEndpoint: string;
restEndpoint: string;
authUrl: string;
chainId: string;
}>
): void {
Object.assign(this, config);
}
/**
* Add request interceptor
*/
addRequestInterceptor(interceptor: (config: any) => any): void {
this.requestInterceptors.push(interceptor);
}
/**
* Add response interceptor
*/
addResponseInterceptor(interceptor: (response: any) => any): void {
this.responseInterceptors.push(interceptor);
}
/**
* Apply interceptors to fetch config
*/
applyRequestInterceptors(config: RequestInit): RequestInit {
return this.requestInterceptors.reduce((acc, interceptor) => interceptor(acc), config);
}
/**
* Apply interceptors to response
*/
applyResponseInterceptors(response: Response): Response {
return this.responseInterceptors.reduce((acc, interceptor) => interceptor(acc), response);
}
}
/**
* Error handling utilities
*/
export class ApiError extends Error {
public code: string;
public statusCode?: number;
public details?: any;
constructor(message: string, code: string, statusCode?: number, details?: any) {
super(message);
this.name = 'ApiError';
this.code = code;
this.statusCode = statusCode;
this.details = details;
}
static fromResponse(response: Response, body?: any): ApiError {
const message = body?.message || body?.error || `HTTP ${response.status}`;
const code = body?.code || 'API_ERROR';
return new ApiError(message, code, response.status, body);
}
static networkError(error: Error): ApiError {
return new ApiError('Network request failed', 'NETWORK_ERROR', undefined, {
originalError: error.message,
});
}
static validationError(message: string, details?: any): ApiError {
return new ApiError(message, 'VALIDATION_ERROR', 400, details);
}
}
/**
* Enhanced fetch with interceptors and error handling
*/
export async function apiFetch(url: string, options?: RequestInit): Promise<Response> {
const config = ApiConfig.getInstance();
try {
// Apply request interceptors
const requestConfig = config.applyRequestInterceptors({
...options,
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
});
// Make the request
const response = await fetch(url, requestConfig);
// Apply response interceptors
const processedResponse = config.applyResponseInterceptors(response);
// Check for errors
if (!processedResponse.ok) {
const body = await processedResponse.json().catch(() => null);
throw ApiError.fromResponse(processedResponse, body);
}
return processedResponse;
} catch (error) {
if (error instanceof ApiError) {
throw error;
}
if (error instanceof TypeError && error.message === 'Failed to fetch') {
throw ApiError.networkError(error);
}
throw new ApiError(error instanceof Error ? error.message : 'Unknown error', 'UNKNOWN_ERROR');
}
}
/**
* Setup default interceptors
*/
export function setupDefaultInterceptors(): void {
const config = ApiConfig.getInstance();
// Add authentication header
config.addRequestInterceptor((requestConfig) => {
const user = authApi.getCurrentUser();
if (user?.address) {
return {
...requestConfig,
headers: {
...requestConfig.headers,
'X-User-Address': user.address,
},
};
}
return requestConfig;
});
// Handle authentication errors
config.addResponseInterceptor((response) => {
if (response.status === 401) {
// Clear session and redirect to auth
authApi.signIn();
}
return response;
});
// Add request ID for tracking
config.addRequestInterceptor((requestConfig) => {
return {
...requestConfig,
headers: {
...requestConfig.headers,
'X-Request-ID': generateRequestId(),
},
};
});
}
/**
* Generate unique request ID
*/
function generateRequestId(): string {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
/**
* API client factory
*/
export const api = {
auth: authApi,
svc: svcApi,
config: ApiConfig.getInstance(),
fetch: apiFetch,
Error: ApiError,
setupInterceptors: setupDefaultInterceptors,
};
export type { AuthApiClient } from './auth';
// Export individual APIs for convenience
export { authApi } from './auth';
export type { SvcApiClient } from './svc';
export { svcApi } from './svc';
export default api;
+318
View File
@@ -0,0 +1,318 @@
// Temporary stub to fix module resolution - TODO: Replace with actual @sonr.io/es/client import
class RpcClient {
constructor(_endpoint: string) {
console.warn('Using stub implementation for RpcClient');
}
}
import type {
ApiResponse,
DomainVerification,
Service,
ServiceCapability,
} from '@sonr.io/com/types';
/**
* Service Module API Client
* Handles all interactions with the x/svc module
*/
export class SvcApiClient {
private rpcClient: RpcClient;
private baseUrl: string;
constructor(rpcEndpoint: string) {
this.rpcClient = new RpcClient(rpcEndpoint);
this.baseUrl = rpcEndpoint.replace('/rpc', '');
}
/**
* Query module parameters
*/
async getParams(): Promise<ApiResponse<any>> {
try {
const response = await fetch(`${this.baseUrl}/svc/v1/params`);
const data = await response.json();
return {
success: true,
data: data.params,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch params',
};
}
}
/**
* Get domain verification status
*/
async getDomainVerification(domain: string): Promise<ApiResponse<DomainVerification>> {
try {
const response = await fetch(`${this.baseUrl}/svc/v1/domain/${encodeURIComponent(domain)}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return {
success: true,
data: data.verification,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch domain verification',
};
}
}
/**
* Get service by ID
*/
async getService(serviceId: string): Promise<ApiResponse<Service>> {
try {
const response = await fetch(
`${this.baseUrl}/svc/v1/service/${encodeURIComponent(serviceId)}`
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return {
success: true,
data: data.service,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch service',
};
}
}
/**
* Get all services owned by an address
*/
async getServicesByOwner(owner: string): Promise<ApiResponse<Service[]>> {
try {
const response = await fetch(
`${this.baseUrl}/svc/v1/services/owner/${encodeURIComponent(owner)}`
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return {
success: true,
data: data.services || [],
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch services by owner',
};
}
}
/**
* Get services bound to a domain
*/
async getServicesByDomain(domain: string): Promise<ApiResponse<Service[]>> {
try {
const response = await fetch(
`${this.baseUrl}/svc/v1/services/domain/${encodeURIComponent(domain)}`
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return {
success: true,
data: data.services || [],
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch services by domain',
};
}
}
/**
* Initiate domain verification
* This would typically broadcast a transaction
*/
async initiateDomainVerification(_domain: string, _signer: any): Promise<ApiResponse<string>> {
try {
// TODO: Use @sonr.io/es transaction broadcasting
// This requires proper message construction with protobuf types
// For now, returning a placeholder
return {
success: false,
error: 'Transaction broadcasting not yet implemented',
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to initiate domain verification',
};
}
}
/**
* Complete domain verification
*/
async verifyDomain(_domain: string, _signer: any): Promise<ApiResponse<boolean>> {
try {
// TODO: Use @sonr.io/es transaction broadcasting
// This requires proper message construction with protobuf types
return {
success: false,
error: 'Transaction broadcasting not yet implemented',
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to verify domain',
};
}
}
/**
* Register a new service
*/
async registerService(
_serviceData: {
name: string;
domain: string;
description: string;
capabilities: ServiceCapability[];
},
_signer: any
): Promise<ApiResponse<Service>> {
try {
// TODO: Use @sonr.io/es transaction broadcasting
// This requires proper message construction with protobuf types
return {
success: false,
error: 'Transaction broadcasting not yet implemented',
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to register service',
};
}
}
/**
* Helper method to check if the API is reachable
*/
async healthCheck(): Promise<boolean> {
try {
const response = await fetch(`${this.baseUrl}/cosmos/base/tendermint/v1beta1/node_info`);
return response.ok;
} catch {
return false;
}
}
}
// Singleton instance with configuration
let apiClient: SvcApiClient | null = null;
/**
* Get or create the service API client
*/
export function getSvcApiClient(endpoint?: string): SvcApiClient {
const rpcEndpoint = endpoint || process.env.NEXT_PUBLIC_RPC_ENDPOINT || 'http://localhost:26657';
if (!apiClient || endpoint) {
apiClient = new SvcApiClient(rpcEndpoint);
}
return apiClient;
}
/**
* Service API helper functions for common operations
*/
export const svcApi = {
/**
* Get all services for the current user
*/
async getMyServices(ownerAddress: string): Promise<Service[]> {
const client = getSvcApiClient();
const result = await client.getServicesByOwner(ownerAddress);
return result.success ? result.data : [];
},
/**
* Get service details with capabilities
*/
async getServiceDetails(serviceId: string): Promise<Service | null> {
const client = getSvcApiClient();
const result = await client.getService(serviceId);
return result.success ? result.data : null;
},
/**
* Check domain verification status
*/
async checkDomainStatus(domain: string): Promise<DomainVerification | null> {
const client = getSvcApiClient();
const result = await client.getDomainVerification(domain);
return result.success ? result.data : null;
},
/**
* Poll domain verification status
*/
async pollDomainVerification(
domain: string,
intervalMs = 5000,
maxAttempts = 60
): Promise<DomainVerification | null> {
const client = getSvcApiClient();
let attempts = 0;
return new Promise((resolve) => {
const checkStatus = async () => {
attempts++;
const result = await client.getDomainVerification(domain);
if (result.success && result.data) {
const verification = result.data;
if (
verification.status === 'DOMAIN_VERIFICATION_STATUS_VERIFIED' ||
verification.status === 'DOMAIN_VERIFICATION_STATUS_FAILED' ||
attempts >= maxAttempts
) {
resolve(verification);
return;
}
}
if (attempts >= maxAttempts) {
resolve(null);
return;
}
setTimeout(checkStatus, intervalMs);
};
checkStatus();
});
},
};
export default svcApi;
+444
View File
@@ -0,0 +1,444 @@
import { z } from 'zod';
/**
* Time range presets
*/
export enum TimeRangePreset {
LAST_HOUR = 'last_hour',
LAST_24_HOURS = 'last_24_hours',
LAST_7_DAYS = 'last_7_days',
LAST_30_DAYS = 'last_30_days',
LAST_90_DAYS = 'last_90_days',
CUSTOM = 'custom',
}
/**
* Metric aggregation type
*/
export enum AggregationType {
SUM = 'sum',
AVG = 'avg',
MIN = 'min',
MAX = 'max',
COUNT = 'count',
P50 = 'p50',
P95 = 'p95',
P99 = 'p99',
}
/**
* Chart type for visualization
*/
export enum ChartType {
LINE = 'line',
BAR = 'bar',
AREA = 'area',
PIE = 'pie',
DONUT = 'donut',
SCATTER = 'scatter',
HEATMAP = 'heatmap',
METRIC = 'metric',
}
/**
* Time range configuration
*/
export interface TimeRange {
preset?: TimeRangePreset;
start: string;
end: string;
timezone?: string;
granularity?: 'minute' | 'hour' | 'day' | 'week' | 'month';
}
/**
* Metric definition
*/
export interface Metric {
id: string;
name: string;
value: number;
unit?: string;
change?: {
value: number;
percentage: number;
direction: 'up' | 'down' | 'stable';
};
sparkline?: number[];
timestamp: string;
}
/**
* Time series data point
*/
export interface TimeSeriesDataPoint {
timestamp: string;
value: number;
label?: string;
metadata?: Record<string, any>;
}
/**
* Time series dataset
*/
export interface TimeSeriesDataset {
id: string;
name: string;
data: TimeSeriesDataPoint[];
color?: string;
aggregation?: AggregationType;
visible?: boolean;
}
/**
* Analytics query configuration
*/
export interface AnalyticsQuery {
metrics: string[];
dimensions?: string[];
filters?: Array<{
field: string;
operator: 'eq' | 'neq' | 'gt' | 'lt' | 'gte' | 'lte' | 'in' | 'nin';
value: any;
}>;
groupBy?: string[];
orderBy?: Array<{
field: string;
direction: 'asc' | 'desc';
}>;
timeRange: TimeRange;
limit?: number;
offset?: number;
}
/**
* Service analytics metrics
*/
export interface ServiceAnalytics {
serviceId: string;
timeRange: TimeRange;
summary: {
totalRequests: number;
uniqueUsers: number;
averageLatency: number;
errorRate: number;
successRate: number;
bandwidth: number;
};
timeSeries: {
requests: TimeSeriesDataset;
latency: TimeSeriesDataset;
errors: TimeSeriesDataset;
users: TimeSeriesDataset;
};
breakdown: {
byEndpoint: Array<{ endpoint: string; count: number; percentage: number }>;
byStatus: Array<{ status: string; count: number; percentage: number }>;
byUser: Array<{ user: string; count: number; percentage: number }>;
byCountry: Array<{ country: string; code: string; count: number }>;
};
}
/**
* API usage analytics
*/
export interface ApiUsageAnalytics {
apiKeyId?: string;
timeRange: TimeRange;
usage: {
totalCalls: number;
successfulCalls: number;
failedCalls: number;
quotaUsed: number;
quotaLimit: number;
averageResponseTime: number;
};
endpoints: Array<{
path: string;
method: string;
calls: number;
avgLatency: number;
errorRate: number;
}>;
errors: Array<{
code: string;
message: string;
count: number;
lastOccurred: string;
}>;
rateLimits: {
current: number;
limit: number;
resetsAt: string;
};
}
/**
* Performance metrics
*/
export interface PerformanceMetrics {
serviceId: string;
timestamp: string;
cpu: {
usage: number;
cores: number;
loadAverage: [number, number, number];
};
memory: {
used: number;
total: number;
percentage: number;
};
latency: {
p50: number;
p95: number;
p99: number;
max: number;
};
throughput: {
requestsPerSecond: number;
bytesPerSecond: number;
};
availability: {
uptime: number;
downtime: number;
percentage: number;
};
}
/**
* Alert configuration
*/
export interface AlertConfig {
id: string;
name: string;
description?: string;
metric: string;
condition: {
operator: 'gt' | 'lt' | 'gte' | 'lte' | 'eq' | 'neq';
threshold: number;
duration?: string;
};
actions: Array<{
type: 'email' | 'webhook' | 'slack' | 'discord';
config: Record<string, any>;
}>;
enabled: boolean;
createdAt: string;
lastTriggered?: string;
}
/**
* Dashboard configuration
*/
export interface DashboardConfig {
id: string;
name: string;
description?: string;
widgets: Array<{
id: string;
type: 'chart' | 'metric' | 'table' | 'list';
title: string;
config: {
chartType?: ChartType;
metrics?: string[];
query?: AnalyticsQuery;
display?: Record<string, any>;
};
position: {
x: number;
y: number;
w: number;
h: number;
};
}>;
filters?: Record<string, any>;
refreshInterval?: number;
createdAt: string;
updatedAt: string;
}
/**
* Validation schemas
*/
export const TimeRangeSchema = z.object({
preset: z.nativeEnum(TimeRangePreset).optional(),
start: z.string(),
end: z.string(),
timezone: z.string().optional(),
granularity: z.enum(['minute', 'hour', 'day', 'week', 'month']).optional(),
});
export const MetricSchema = z.object({
id: z.string(),
name: z.string(),
value: z.number(),
unit: z.string().optional(),
change: z
.object({
value: z.number(),
percentage: z.number(),
direction: z.enum(['up', 'down', 'stable']),
})
.optional(),
sparkline: z.array(z.number()).optional(),
timestamp: z.string(),
});
export const AnalyticsQuerySchema = z.object({
metrics: z.array(z.string()).min(1),
dimensions: z.array(z.string()).optional(),
filters: z
.array(
z.object({
field: z.string(),
operator: z.enum(['eq', 'neq', 'gt', 'lt', 'gte', 'lte', 'in', 'nin']),
value: z.any(),
})
)
.optional(),
groupBy: z.array(z.string()).optional(),
orderBy: z
.array(
z.object({
field: z.string(),
direction: z.enum(['asc', 'desc']),
})
)
.optional(),
timeRange: TimeRangeSchema,
limit: z.number().min(1).max(1000).optional(),
offset: z.number().min(0).optional(),
});
export const AlertConfigSchema = z.object({
id: z.string(),
name: z.string().min(1).max(100),
description: z.string().max(500).optional(),
metric: z.string(),
condition: z.object({
operator: z.enum(['gt', 'lt', 'gte', 'lte', 'eq', 'neq']),
threshold: z.number(),
duration: z.string().optional(),
}),
actions: z.array(
z.object({
type: z.enum(['email', 'webhook', 'slack', 'discord']),
config: z.record(z.any()),
})
),
enabled: z.boolean(),
createdAt: z.string(),
lastTriggered: z.string().optional(),
});
/**
* Utility functions
*/
export function formatMetricValue(value: number, unit?: string): string {
if (unit === 'bytes') {
return formatBytes(value);
}
if (unit === 'percentage') {
return `${value.toFixed(1)}%`;
}
if (unit === 'ms') {
return `${value.toFixed(0)}ms`;
}
if (value >= 1000000) {
return `${(value / 1000000).toFixed(1)}M`;
}
if (value >= 1000) {
return `${(value / 1000).toFixed(1)}K`;
}
return value.toFixed(0);
}
export function formatBytes(bytes: number, decimals = 1): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${(bytes / k ** i).toFixed(decimals)} ${sizes[i]}`;
}
export function getTimeRangeFromPreset(preset: TimeRangePreset): TimeRange {
const end = new Date();
const start = new Date();
switch (preset) {
case TimeRangePreset.LAST_HOUR:
start.setHours(start.getHours() - 1);
break;
case TimeRangePreset.LAST_24_HOURS:
start.setDate(start.getDate() - 1);
break;
case TimeRangePreset.LAST_7_DAYS:
start.setDate(start.getDate() - 7);
break;
case TimeRangePreset.LAST_30_DAYS:
start.setDate(start.getDate() - 30);
break;
case TimeRangePreset.LAST_90_DAYS:
start.setDate(start.getDate() - 90);
break;
default:
start.setDate(start.getDate() - 7);
}
return {
preset,
start: start.toISOString(),
end: end.toISOString(),
};
}
export function getMetricChangeDirection(
current: number,
previous: number
): 'up' | 'down' | 'stable' {
if (current > previous) return 'up';
if (current < previous) return 'down';
return 'stable';
}
export function calculatePercentageChange(current: number, previous: number): number {
if (previous === 0) return current > 0 ? 100 : 0;
return ((current - previous) / previous) * 100;
}
export function aggregateTimeSeries(
data: TimeSeriesDataPoint[],
aggregation: AggregationType
): number {
if (data.length === 0) return 0;
const values = data.map((d) => d.value);
switch (aggregation) {
case AggregationType.SUM:
return values.reduce((sum, val) => sum + val, 0);
case AggregationType.AVG:
return values.reduce((sum, val) => sum + val, 0) / values.length;
case AggregationType.MIN:
return Math.min(...values);
case AggregationType.MAX:
return Math.max(...values);
case AggregationType.COUNT:
return values.length;
case AggregationType.P50:
return percentile(values, 0.5);
case AggregationType.P95:
return percentile(values, 0.95);
case AggregationType.P99:
return percentile(values, 0.99);
default:
return 0;
}
}
function percentile(values: number[], p: number): number {
const sorted = [...values].sort((a, b) => a - b);
const index = Math.ceil(sorted.length * p) - 1;
return sorted[Math.max(0, index)] || 0;
}
+394
View File
@@ -0,0 +1,394 @@
import { z } from 'zod';
import type { Service, ServiceDomain } from './service';
/**
* API error codes
*/
export enum ApiErrorCode {
UNKNOWN = 'UNKNOWN',
NETWORK_ERROR = 'NETWORK_ERROR',
VALIDATION_ERROR = 'VALIDATION_ERROR',
AUTHENTICATION_ERROR = 'AUTHENTICATION_ERROR',
AUTHORIZATION_ERROR = 'AUTHORIZATION_ERROR',
NOT_FOUND = 'NOT_FOUND',
RATE_LIMIT = 'RATE_LIMIT',
SERVER_ERROR = 'SERVER_ERROR',
TIMEOUT = 'TIMEOUT',
CONFLICT = 'CONFLICT',
PRECONDITION_FAILED = 'PRECONDITION_FAILED',
}
/**
* API response status
*/
export enum ApiResponseStatus {
SUCCESS = 'success',
ERROR = 'error',
PARTIAL = 'partial',
}
/**
* Base API response interface
*/
export interface ApiResponse<T = any> {
data?: T;
error?: ApiError;
status?: ApiResponseStatus;
timestamp?: string;
requestId?: string;
}
/**
* API error interface
*/
export interface ApiError {
code: ApiErrorCode | string;
message: string;
details?: any;
statusCode?: number;
timestamp?: string;
path?: string;
requestId?: string;
}
/**
* Paginated API response
*/
export interface PaginatedResponse<T> extends ApiResponse<T[]> {
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
hasNext: boolean;
hasPrev: boolean;
};
}
/**
* Batch API response
*/
export interface BatchResponse<T> extends ApiResponse {
results: Array<{
id: string;
success: boolean;
data?: T;
error?: ApiError;
}>;
summary: {
total: number;
successful: number;
failed: number;
partial: number;
};
}
/**
* Authentication response
*/
export interface AuthResponse extends ApiResponse {
data?: {
user: AuthUser;
token: string;
refreshToken?: string;
expiresAt: string;
};
}
/**
* Authenticated user
*/
export interface AuthUser {
id: string;
username: string;
did?: string;
address?: string;
email?: string;
createdAt: string;
lastLogin?: string;
roles?: string[];
permissions?: string[];
}
/**
* Authentication status
*/
export interface AuthStatus {
isAuthenticated: boolean;
user: AuthUser | null;
expiresAt?: string;
}
/**
* Service API responses
*/
export interface ServiceListResponse extends PaginatedResponse<Service> {
filters?: {
status?: string[];
owner?: string;
domain?: string;
tags?: string[];
};
}
export interface ServiceDetailResponse extends ApiResponse<Service> {
related?: {
domains?: ServiceDomain[];
apiKeys?: number;
permissions?: string[];
};
}
export interface ServiceCreateResponse extends ApiResponse<Service> {
verificationRequired?: boolean;
verificationInstructions?: {
txtRecord: string;
domain: string;
ttl: number;
};
}
export interface ServiceUpdateResponse extends ApiResponse<Service> {
changes?: {
field: string;
oldValue: any;
newValue: any;
}[];
}
export interface ServiceDeleteResponse extends ApiResponse {
data?: {
id: string;
deletedAt: string;
cascaded?: string[];
};
}
/**
* Domain verification responses
*/
export interface DomainVerificationInitResponse extends ApiResponse {
data?: {
domain: string;
challengeToken: string;
txtRecord: string;
expiresAt: string;
status: string;
};
}
export interface DomainVerificationStatusResponse extends ApiResponse {
data?: {
domain: string;
status: string;
verifiedAt?: string;
lastChecked: string;
dnsRecordsFound: boolean;
expectedRecord: string;
actualRecord?: string;
};
}
/**
* API key responses
*/
export interface ApiKeyCreateResponse extends ApiResponse {
data?: {
id: string;
name: string;
key: string; // Full key only returned on creation
prefix: string;
createdAt: string;
expiresAt?: string;
permissions: string[];
};
}
export interface ApiKeyListResponse
extends PaginatedResponse<{
id: string;
name: string;
prefix: string;
createdAt: string;
lastUsed?: string;
status: string;
}> {}
/**
* Analytics responses
*/
export interface AnalyticsResponse extends ApiResponse {
data?: {
timeRange: {
start: string;
end: string;
};
metrics: {
totalRequests: number;
uniqueUsers: number;
averageLatency: number;
errorRate: number;
successRate: number;
};
timeSeries?: Array<{
timestamp: string;
requests: number;
errors: number;
latency: number;
}>;
breakdown?: {
byEndpoint?: Record<string, number>;
byStatus?: Record<string, number>;
byUser?: Record<string, number>;
};
};
}
/**
* WebSocket message types
*/
export interface WebSocketMessage<T = any> {
type: 'update' | 'delete' | 'create' | 'error' | 'ping' | 'pong';
channel: string;
data?: T;
timestamp: string;
id?: string;
}
/**
* Request configuration
*/
export interface ApiRequestConfig {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
headers?: Record<string, string>;
params?: Record<string, any>;
body?: any;
timeout?: number;
retries?: number;
cache?: boolean;
signal?: AbortSignal;
}
/**
* API validation schemas
*/
export const ApiErrorSchema = z.object({
code: z.string(),
message: z.string(),
details: z.any().optional(),
statusCode: z.number().optional(),
timestamp: z.string().optional(),
path: z.string().optional(),
requestId: z.string().optional(),
});
export const ApiResponseSchema = z.object({
data: z.any().optional(),
error: ApiErrorSchema.optional(),
status: z.enum(['success', 'error', 'partial']).optional(),
timestamp: z.string().optional(),
requestId: z.string().optional(),
});
export const PaginatedResponseSchema = <T extends z.ZodType>(itemSchema: T) =>
z.object({
data: z.array(itemSchema).optional(),
error: ApiErrorSchema.optional(),
status: z.enum(['success', 'error', 'partial']).optional(),
timestamp: z.string().optional(),
requestId: z.string().optional(),
pagination: z.object({
page: z.number(),
limit: z.number(),
total: z.number(),
totalPages: z.number(),
hasNext: z.boolean(),
hasPrev: z.boolean(),
}),
});
/**
* Response type guards
*/
export function isApiError(response: any): response is ApiError {
return response && typeof response.code === 'string' && typeof response.message === 'string';
}
export function isSuccessResponse<T>(response: ApiResponse<T>): boolean {
return !response.error && response.status !== 'error';
}
export function isPaginatedResponse<T>(response: any): response is PaginatedResponse<T> {
return response && typeof response.pagination === 'object' && Array.isArray(response.data);
}
/**
* Error factory functions
*/
export function createApiError(
code: ApiErrorCode | string,
message: string,
details?: any
): ApiError {
return {
code,
message,
details,
timestamp: new Date().toISOString(),
};
}
export function createNetworkError(message = 'Network request failed'): ApiError {
return createApiError(ApiErrorCode.NETWORK_ERROR, message);
}
export function createValidationError(message: string, details?: any): ApiError {
return createApiError(ApiErrorCode.VALIDATION_ERROR, message, details);
}
export function createAuthError(message = 'Authentication required'): ApiError {
return createApiError(ApiErrorCode.AUTHENTICATION_ERROR, message);
}
/**
* Response builder functions
*/
export function createSuccessResponse<T>(data: T, requestId?: string): ApiResponse<T> {
return {
data,
status: ApiResponseStatus.SUCCESS,
timestamp: new Date().toISOString(),
requestId,
};
}
export function createErrorResponse(error: ApiError, requestId?: string): ApiResponse {
return {
error,
status: ApiResponseStatus.ERROR,
timestamp: new Date().toISOString(),
requestId,
};
}
export function createPaginatedResponse<T>(
data: T[],
page: number,
limit: number,
total: number
): PaginatedResponse<T> {
const totalPages = Math.ceil(total / limit);
return {
data,
status: ApiResponseStatus.SUCCESS,
timestamp: new Date().toISOString(),
pagination: {
page,
limit,
total,
totalPages,
hasNext: page < totalPages,
hasPrev: page > 1,
},
};
}
+394
View File
@@ -0,0 +1,394 @@
import { z } from 'zod';
/**
* Domain verification method
*/
export enum DomainVerificationMethod {
DNS_TXT = 'dns_txt',
DNS_CNAME = 'dns_cname',
HTTP_FILE = 'http_file',
META_TAG = 'meta_tag',
}
/**
* Domain verification status
*/
export enum DomainVerificationStatus {
UNVERIFIED = 'unverified',
PENDING = 'pending',
VERIFIED = 'verified',
FAILED = 'failed',
EXPIRED = 'expired',
}
/**
* DNS record type for verification
*/
export enum DnsRecordType {
TXT = 'TXT',
CNAME = 'CNAME',
A = 'A',
AAAA = 'AAAA',
}
/**
* Domain verification challenge
*/
export interface DomainChallenge {
id: string;
domain: string;
method: DomainVerificationMethod;
token: string;
expiresAt: string;
createdAt: string;
attempts: number;
maxAttempts: number;
}
/**
* DNS record for verification
*/
export interface DnsRecord {
type: DnsRecordType;
name: string;
value: string;
ttl?: number;
priority?: number;
}
/**
* Domain verification instructions
*/
export interface DomainVerificationInstructions {
method: DomainVerificationMethod;
dnsRecords?: DnsRecord[];
httpFilePath?: string;
httpFileContent?: string;
metaTagName?: string;
metaTagContent?: string;
instructions: string[];
estimatedTime?: string;
}
/**
* Domain verification attempt
*/
export interface DomainVerificationAttempt {
id: string;
domain: string;
timestamp: string;
success: boolean;
method: DomainVerificationMethod;
recordsFound?: DnsRecord[];
expectedRecords?: DnsRecord[];
error?: string;
responseTime?: number;
}
/**
* Domain ownership details
*/
export interface DomainOwnership {
domain: string;
owner: string;
verifiedAt?: string;
expiresAt?: string;
autoRenew: boolean;
delegatedTo?: string[];
}
/**
* Domain configuration
*/
export interface DomainConfig {
domain: string;
subdomains: string[];
wildcardEnabled: boolean;
sslEnabled: boolean;
cors?: {
enabled: boolean;
origins: string[];
methods: string[];
headers: string[];
};
rateLimit?: {
enabled: boolean;
requestsPerMinute: number;
requestsPerHour: number;
};
redirects?: Array<{
from: string;
to: string;
statusCode: number;
}>;
}
/**
* Domain verification state
*/
export interface DomainVerification {
domain: string;
status: DomainVerificationStatus;
method?: DomainVerificationMethod;
challenge?: DomainChallenge;
instructions?: DomainVerificationInstructions;
attempts?: DomainVerificationAttempt[];
ownership?: DomainOwnership;
config?: DomainConfig;
createdAt: string;
updatedAt: string;
verifiedAt?: string;
lastChecked?: string;
nextCheckAt?: string;
}
/**
* Domain health status
*/
export interface DomainHealth {
domain: string;
status: 'healthy' | 'degraded' | 'offline';
sslValid: boolean;
sslExpiresAt?: string;
dnsResolvable: boolean;
httpReachable: boolean;
averageResponseTime?: number;
lastChecked: string;
issues?: string[];
}
/**
* Domain analytics
*/
export interface DomainAnalytics {
domain: string;
timeRange: {
start: string;
end: string;
};
metrics: {
totalRequests: number;
uniqueVisitors: number;
bandwidth: number;
cacheHitRate: number;
errorRate: number;
};
topPaths?: Array<{
path: string;
requests: number;
}>;
topReferers?: Array<{
referer: string;
requests: number;
}>;
geographic?: Array<{
country: string;
requests: number;
}>;
}
/**
* Validation schemas
*/
export const DomainChallengeSchema = z.object({
id: z.string(),
domain: z.string(),
method: z.nativeEnum(DomainVerificationMethod),
token: z.string().min(32),
expiresAt: z.string(),
createdAt: z.string(),
attempts: z.number().min(0),
maxAttempts: z.number().min(1).max(100),
});
export const DnsRecordSchema = z.object({
type: z.nativeEnum(DnsRecordType),
name: z.string(),
value: z.string(),
ttl: z.number().min(60).max(86400).optional(),
priority: z.number().min(0).max(65535).optional(),
});
export const DomainVerificationInstructionsSchema = z.object({
method: z.nativeEnum(DomainVerificationMethod),
dnsRecords: z.array(DnsRecordSchema).optional(),
httpFilePath: z.string().optional(),
httpFileContent: z.string().optional(),
metaTagName: z.string().optional(),
metaTagContent: z.string().optional(),
instructions: z.array(z.string()),
estimatedTime: z.string().optional(),
});
export const DomainOwnershipSchema = z.object({
domain: z.string(),
owner: z.string(),
verifiedAt: z.string().optional(),
expiresAt: z.string().optional(),
autoRenew: z.boolean(),
delegatedTo: z.array(z.string()).optional(),
});
export const DomainConfigSchema = z.object({
domain: z.string(),
subdomains: z.array(z.string()),
wildcardEnabled: z.boolean(),
sslEnabled: z.boolean(),
cors: z
.object({
enabled: z.boolean(),
origins: z.array(z.string()),
methods: z.array(z.string()),
headers: z.array(z.string()),
})
.optional(),
rateLimit: z
.object({
enabled: z.boolean(),
requestsPerMinute: z.number().min(1),
requestsPerHour: z.number().min(1),
})
.optional(),
redirects: z
.array(
z.object({
from: z.string(),
to: z.string(),
statusCode: z.number().min(300).max(399),
})
)
.optional(),
});
export const DomainVerificationSchema = z.object({
domain: z.string(),
status: z.nativeEnum(DomainVerificationStatus),
method: z.nativeEnum(DomainVerificationMethod).optional(),
challenge: DomainChallengeSchema.optional(),
instructions: DomainVerificationInstructionsSchema.optional(),
attempts: z.array(z.any()).optional(), // Simplified for brevity
ownership: DomainOwnershipSchema.optional(),
config: DomainConfigSchema.optional(),
createdAt: z.string(),
updatedAt: z.string(),
verifiedAt: z.string().optional(),
lastChecked: z.string().optional(),
nextCheckAt: z.string().optional(),
});
/**
* Type guards
*/
export function isDomainVerified(verification: DomainVerification): boolean {
return verification.status === DomainVerificationStatus.VERIFIED;
}
export function isDomainPending(verification: DomainVerification): boolean {
return verification.status === DomainVerificationStatus.PENDING;
}
export function isDomainExpired(verification: DomainVerification): boolean {
if (verification.status === DomainVerificationStatus.EXPIRED) return true;
if (verification.ownership?.expiresAt) {
return new Date(verification.ownership.expiresAt) < new Date();
}
return false;
}
export function hasDnsMethod(verification: DomainVerification): boolean {
return (
verification.method === DomainVerificationMethod.DNS_TXT ||
verification.method === DomainVerificationMethod.DNS_CNAME
);
}
/**
* Utility functions
*/
export function formatDomainStatus(status: DomainVerificationStatus): string {
const statusMap: Record<DomainVerificationStatus, string> = {
[DomainVerificationStatus.UNVERIFIED]: 'Unverified',
[DomainVerificationStatus.PENDING]: 'Pending Verification',
[DomainVerificationStatus.VERIFIED]: 'Verified',
[DomainVerificationStatus.FAILED]: 'Verification Failed',
[DomainVerificationStatus.EXPIRED]: 'Expired',
};
return statusMap[status] || status;
}
export function getDomainStatusColor(status: DomainVerificationStatus): string {
const colorMap: Record<DomainVerificationStatus, string> = {
[DomainVerificationStatus.VERIFIED]: 'green',
[DomainVerificationStatus.PENDING]: 'yellow',
[DomainVerificationStatus.FAILED]: 'red',
[DomainVerificationStatus.EXPIRED]: 'orange',
[DomainVerificationStatus.UNVERIFIED]: 'gray',
};
return colorMap[status] || 'gray';
}
export function formatDnsRecord(record: DnsRecord): string {
return `${record.type} ${record.name} ${record.value}${record.ttl ? ` TTL:${record.ttl}` : ''}`;
}
export function generateTxtRecordValue(token: string, prefix = 'sonr-verification'): string {
return `${prefix}=${token}`;
}
export function parseTxtRecordValue(value: string): { prefix: string; token: string } | null {
const match = value.match(/^([^=]+)=(.+)$/);
if (!match) return null;
return { prefix: match[1], token: match[2] };
}
export function estimateVerificationTime(method: DomainVerificationMethod): string {
const estimates: Record<DomainVerificationMethod, string> = {
[DomainVerificationMethod.DNS_TXT]: '5-60 minutes (DNS propagation)',
[DomainVerificationMethod.DNS_CNAME]: '5-60 minutes (DNS propagation)',
[DomainVerificationMethod.HTTP_FILE]: '1-2 minutes',
[DomainVerificationMethod.META_TAG]: '1-2 minutes',
};
return estimates[method] || 'Unknown';
}
export function getVerificationMethodName(method: DomainVerificationMethod): string {
const names: Record<DomainVerificationMethod, string> = {
[DomainVerificationMethod.DNS_TXT]: 'DNS TXT Record',
[DomainVerificationMethod.DNS_CNAME]: 'DNS CNAME Record',
[DomainVerificationMethod.HTTP_FILE]: 'HTTP File Upload',
[DomainVerificationMethod.META_TAG]: 'HTML Meta Tag',
};
return names[method] || method;
}
/**
* Domain validation helpers
*/
export function isValidDomain(domain: string): boolean {
const domainRegex = /^[a-z0-9]+([-.]{1}[a-z0-9]+)*\.[a-z]{2,}$/i;
return domainRegex.test(domain);
}
export function isSubdomain(domain: string): boolean {
const parts = domain.split('.');
return parts.length > 2;
}
export function getBaseDomain(domain: string): string {
const parts = domain.split('.');
if (parts.length <= 2) return domain;
return parts.slice(-2).join('.');
}
export function getSubdomainPrefix(domain: string): string | null {
const parts = domain.split('.');
if (parts.length <= 2) return null;
return parts.slice(0, -2).join('.');
}
export function normalizeDomain(domain: string): string {
return domain
.toLowerCase()
.replace(/^https?:\/\//, '')
.replace(/\/$/, '');
}
+77
View File
@@ -0,0 +1,77 @@
/**
* Export all type definitions
*/
export type {
AggregationType,
AlertConfig,
AnalyticsQuery,
ApiUsageAnalytics,
ChartType,
DashboardConfig,
Metric,
PerformanceMetrics,
ServiceAnalytics,
TimeRange,
TimeRangePreset,
TimeSeriesDataPoint,
TimeSeriesDataset,
} from './analytics';
// Analytics types
export * from './analytics';
export type {
AnalyticsResponse,
ApiError,
ApiErrorCode,
ApiKeyCreateResponse,
ApiKeyListResponse,
ApiRequestConfig,
ApiResponse,
ApiResponseStatus,
AuthResponse,
AuthStatus,
AuthUser,
BatchResponse,
DomainVerificationInitResponse,
DomainVerificationStatusResponse,
PaginatedResponse,
ServiceCreateResponse,
ServiceDeleteResponse,
ServiceDetailResponse,
ServiceListResponse,
ServiceUpdateResponse,
WebSocketMessage,
} from './api';
// API types
export * from './api';
export type {
DnsRecord,
DnsRecordType,
DomainAnalytics,
DomainChallenge,
DomainConfig,
DomainHealth,
DomainOwnership,
DomainVerification,
DomainVerificationAttempt,
DomainVerificationInstructions,
DomainVerificationMethod,
} from './domain';
// Domain types
export * from './domain';
export type {
DomainVerificationStatus,
PermissionScope,
Service,
ServiceApiKey,
ServiceCapability,
ServiceConfig,
ServiceCreateRequest,
ServiceDomain,
ServiceMetadata,
ServicePermission,
ServiceStatus,
ServiceUpdateRequest,
} from './service';
// Service types
export * from './service';
+351
View File
@@ -0,0 +1,351 @@
import { z } from 'zod';
/**
* Service status enum
*/
export enum ServiceStatus {
ACTIVE = 'active',
PENDING = 'pending',
SUSPENDED = 'suspended',
INACTIVE = 'inactive',
}
/**
* Domain verification status enum
*/
export enum DomainVerificationStatus {
UNVERIFIED = 'unverified',
PENDING = 'pending',
VERIFIED = 'verified',
FAILED = 'failed',
EXPIRED = 'expired',
}
/**
* Permission scope enum
*/
export enum PermissionScope {
DATA = 'data',
VAULT = 'vault',
PROFILE = 'profile',
SERVICE = 'service',
ADMIN = 'admin',
}
/**
* Service capability interface
*/
export interface ServiceCapability {
id: string;
name: string;
description: string;
scope: PermissionScope | string;
granted: boolean;
expiresAt?: string;
constraints?: Record<string, any>;
}
/**
* Service permission interface
*/
export interface ServicePermission {
id: string;
name: string;
description: string;
scope: string;
granted: boolean;
grantedAt?: string;
grantedBy?: string;
revokedAt?: string;
revokedBy?: string;
}
/**
* Service API key interface
*/
export interface ServiceApiKey {
id: string;
name: string;
key?: string; // Only returned on creation
prefix?: string; // Key prefix for identification
createdAt: string;
lastUsed?: string;
expiresAt?: string;
status: 'active' | 'expired' | 'revoked';
permissions?: string[];
}
/**
* Service metadata interface
*/
export interface ServiceMetadata {
totalRequests?: number;
activeUsers?: number;
averageLatency?: number;
errorRate?: number;
uptime?: number;
lastHealthCheck?: string;
version?: string;
environment?: string;
[key: string]: any;
}
/**
* Service domain interface
*/
export interface ServiceDomain {
domain: string;
verificationStatus: DomainVerificationStatus;
verifiedAt?: string;
txtRecord?: string;
challengeToken?: string;
expiresAt?: string;
}
/**
* Service configuration interface
*/
export interface ServiceConfig {
webhookUrl?: string;
callbackUrl?: string;
allowedOrigins?: string[];
rateLimits?: {
requestsPerMinute?: number;
requestsPerHour?: number;
requestsPerDay?: number;
};
features?: {
webhooksEnabled?: boolean;
analyticsEnabled?: boolean;
loggingEnabled?: boolean;
};
customSettings?: Record<string, any>;
}
/**
* Main Service interface
*/
export interface Service {
id: string;
name: string;
description: string;
domain: string;
domains?: ServiceDomain[];
status: ServiceStatus | string;
owner: string;
createdAt: string;
updatedAt: string;
permissions?: ServicePermission[] | string[];
capabilities?: ServiceCapability[];
apiKeys?: ServiceApiKey[];
domainVerificationStatus?: DomainVerificationStatus | string;
metadata?: ServiceMetadata;
config?: ServiceConfig;
tags?: string[];
}
/**
* Service creation request
*/
export interface ServiceCreateRequest {
name: string;
description: string;
domain: string;
permissions?: string[];
config?: ServiceConfig;
tags?: string[];
}
/**
* Service update request
*/
export interface ServiceUpdateRequest {
name?: string;
description?: string;
status?: ServiceStatus;
permissions?: string[];
config?: ServiceConfig;
tags?: string[];
}
/**
* Service validation schemas using Zod
*/
export const ServiceStatusSchema = z.enum([
ServiceStatus.ACTIVE,
ServiceStatus.PENDING,
ServiceStatus.SUSPENDED,
ServiceStatus.INACTIVE,
]);
export const DomainVerificationStatusSchema = z.enum([
DomainVerificationStatus.UNVERIFIED,
DomainVerificationStatus.PENDING,
DomainVerificationStatus.VERIFIED,
DomainVerificationStatus.FAILED,
DomainVerificationStatus.EXPIRED,
]);
export const ServiceCapabilitySchema = z.object({
id: z.string(),
name: z.string().min(1).max(100),
description: z.string().max(500),
scope: z.string(),
granted: z.boolean(),
expiresAt: z.string().optional(),
constraints: z.record(z.string(), z.any()).optional(),
});
export const ServiceApiKeySchema = z.object({
id: z.string(),
name: z.string().min(1).max(100),
key: z.string().optional(),
prefix: z.string().optional(),
createdAt: z.string(),
lastUsed: z.string().optional(),
expiresAt: z.string().optional(),
status: z.enum(['active', 'expired', 'revoked']),
permissions: z.array(z.string()).optional(),
});
export const ServiceDomainSchema = z.object({
domain: z.string(),
verificationStatus: DomainVerificationStatusSchema,
verifiedAt: z.string().optional(),
txtRecord: z.string().optional(),
challengeToken: z.string().optional(),
expiresAt: z.string().optional(),
});
export const ServiceConfigSchema = z.object({
webhookUrl: z.string().url().optional(),
callbackUrl: z.string().url().optional(),
allowedOrigins: z.array(z.string()).optional(),
rateLimits: z
.object({
requestsPerMinute: z.number().min(1).max(10000).optional(),
requestsPerHour: z.number().min(1).max(100000).optional(),
requestsPerDay: z.number().min(1).max(1000000).optional(),
})
.optional(),
features: z
.object({
webhooksEnabled: z.boolean().optional(),
analyticsEnabled: z.boolean().optional(),
loggingEnabled: z.boolean().optional(),
})
.optional(),
customSettings: z.record(z.any()).optional(),
});
export const ServiceSchema = z.object({
id: z.string(),
name: z.string().min(3).max(100),
description: z.string().min(10).max(500),
domain: z.string(),
domains: z.array(ServiceDomainSchema).optional(),
status: z.union([ServiceStatusSchema, z.string()]),
owner: z.string(),
createdAt: z.string(),
updatedAt: z.string(),
permissions: z.array(z.union([z.string(), z.any()])).optional(),
capabilities: z.array(ServiceCapabilitySchema).optional(),
apiKeys: z.array(ServiceApiKeySchema).optional(),
domainVerificationStatus: z.union([DomainVerificationStatusSchema, z.string()]).optional(),
metadata: z.record(z.any()).optional(),
config: ServiceConfigSchema.optional(),
tags: z.array(z.string()).optional(),
});
export const ServiceCreateRequestSchema = z.object({
name: z.string().min(3).max(100),
description: z.string().min(10).max(500),
domain: z.string(),
permissions: z.array(z.string()).optional(),
config: ServiceConfigSchema.optional(),
tags: z.array(z.string()).optional(),
});
export const ServiceUpdateRequestSchema = z.object({
name: z.string().min(3).max(100).optional(),
description: z.string().min(10).max(500).optional(),
status: ServiceStatusSchema.optional(),
permissions: z.array(z.string()).optional(),
config: ServiceConfigSchema.optional(),
tags: z.array(z.string()).optional(),
});
/**
* Type guards
*/
export function isServiceActive(service: Service): boolean {
return service.status === ServiceStatus.ACTIVE || service.status === 'active';
}
export function isDomainVerified(domain: ServiceDomain | string): boolean {
if (typeof domain === 'string') {
return false;
}
return domain.verificationStatus === DomainVerificationStatus.VERIFIED;
}
export function hasPermission(service: Service, permission: string): boolean {
if (!service.permissions) return false;
if (Array.isArray(service.permissions)) {
return service.permissions.some((p) => {
if (typeof p === 'string') {
return p === permission;
}
return p.name === permission && p.granted;
});
}
return false;
}
/**
* Utility functions
*/
export function formatServiceStatus(status: ServiceStatus | string): string {
const statusMap: Record<string, string> = {
[ServiceStatus.ACTIVE]: 'Active',
[ServiceStatus.PENDING]: 'Pending',
[ServiceStatus.SUSPENDED]: 'Suspended',
[ServiceStatus.INACTIVE]: 'Inactive',
};
return statusMap[status] || status;
}
export function formatDomainStatus(status: DomainVerificationStatus | string): string {
const statusMap: Record<string, string> = {
[DomainVerificationStatus.UNVERIFIED]: 'Unverified',
[DomainVerificationStatus.PENDING]: 'Pending Verification',
[DomainVerificationStatus.VERIFIED]: 'Verified',
[DomainVerificationStatus.FAILED]: 'Verification Failed',
[DomainVerificationStatus.EXPIRED]: 'Expired',
};
return statusMap[status] || status;
}
export function getServiceStatusColor(status: ServiceStatus | string): string {
const colorMap: Record<string, string> = {
[ServiceStatus.ACTIVE]: 'green',
[ServiceStatus.PENDING]: 'yellow',
[ServiceStatus.SUSPENDED]: 'orange',
[ServiceStatus.INACTIVE]: 'gray',
};
return colorMap[status] || 'gray';
}
export function getDomainStatusColor(status: DomainVerificationStatus | string): string {
const colorMap: Record<string, string> = {
[DomainVerificationStatus.VERIFIED]: 'green',
[DomainVerificationStatus.PENDING]: 'yellow',
[DomainVerificationStatus.FAILED]: 'red',
[DomainVerificationStatus.EXPIRED]: 'orange',
[DomainVerificationStatus.UNVERIFIED]: 'gray',
};
return colorMap[status] || 'gray';
}
+137
View File
@@ -0,0 +1,137 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ['class'],
content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
'./src/**/*.{js,ts,jsx,tsx,mdx}',
// Include @sonr.io/ui components
'../../packages/ui/src/**/*.{js,ts,jsx,tsx}',
],
prefix: '',
theme: {
container: {
center: true,
padding: '2rem',
screens: {
'2xl': '1400px',
},
},
extend: {
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
// Chart colors
chart: {
1: 'hsl(var(--chart-1))',
2: 'hsl(var(--chart-2))',
3: 'hsl(var(--chart-3))',
4: 'hsl(var(--chart-4))',
5: 'hsl(var(--chart-5))',
},
// Status colors
success: {
DEFAULT: 'hsl(142, 76%, 36%)',
foreground: 'hsl(355.7, 100%, 97.3%)',
},
warning: {
DEFAULT: 'hsl(32, 95%, 44%)',
foreground: 'hsl(355.7, 100%, 97.3%)',
},
info: {
DEFAULT: 'hsl(221, 83%, 53%)',
foreground: 'hsl(355.7, 100%, 97.3%)',
},
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
keyframes: {
'accordion-down': {
from: { height: '0' },
to: { height: 'var(--radix-accordion-content-height)' },
},
'accordion-up': {
from: { height: 'var(--radix-accordion-content-height)' },
to: { height: '0' },
},
'fade-in': {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
'fade-out': {
'0%': { opacity: '1' },
'100%': { opacity: '0' },
},
'slide-in-from-top': {
'0%': { transform: 'translateY(-100%)' },
'100%': { transform: 'translateY(0)' },
},
'slide-out-to-top': {
'0%': { transform: 'translateY(0)' },
'100%': { transform: 'translateY(-100%)' },
},
'pulse-subtle': {
'0%, 100%': { opacity: '1' },
'50%': { opacity: '0.8' },
},
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
'fade-in': 'fade-in 0.5s ease-in-out',
'fade-out': 'fade-out 0.5s ease-in-out',
'slide-in-from-top': 'slide-in-from-top 0.3s ease-out',
'slide-out-to-top': 'slide-out-to-top 0.3s ease-in',
'pulse-subtle': 'pulse-subtle 2s ease-in-out infinite',
},
fontFamily: {
sans: ['var(--font-sans)', 'system-ui', 'sans-serif'],
mono: ['var(--font-mono)', 'Consolas', 'monospace'],
},
spacing: {
18: '4.5rem',
88: '22rem',
128: '32rem',
},
screens: {
xs: '475px',
},
},
},
plugins: [require('tailwindcss-animate')],
};
+28
View File
@@ -0,0 +1,28 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
+28
View File
@@ -0,0 +1,28 @@
main = ".open-next/worker.js"
name = "highway-dash"
compatibility_date = "2025-03-25"
compatibility_flags = ["nodejs_compat"]
[dev]
port = 3200
[assets]
directory = ".open-next/assets"
binding = "ASSETS"
# Development environment (default)
[env.development]
# Add any development-specific environment variables here
# Production environment
[env.production]
# Add any production-specific environment variables here
# Build configuration is handled separately by opennextjs-cloudflare
[build]
command = "opennextjs-cloudflare build"
cwd = "."
# Analytics and observability
[observability]
enabled = true