Integrate with supabase backend

This commit is contained in:
Harshavardhan Musanalli
2025-10-25 12:39:48 +02:00
parent f69e1dbc66
commit 6fd403cb5f
14 changed files with 1154 additions and 124 deletions

410
internal/auth/auth.go Normal file
View File

@@ -0,0 +1,410 @@
package auth
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"nannyagentv2/internal/config"
"nannyagentv2/internal/types"
)
const (
// Token storage location (secure directory)
TokenStorageDir = "/var/lib/nannyagent"
TokenStorageFile = ".agent_token.json"
RefreshTokenFile = ".refresh_token"
// Polling configuration
MaxPollAttempts = 60 // 5 minutes (60 * 5 seconds)
PollInterval = 5 * time.Second
)
// AuthManager handles all authentication-related operations
type AuthManager struct {
config *config.Config
client *http.Client
}
// NewAuthManager creates a new authentication manager
func NewAuthManager(cfg *config.Config) *AuthManager {
return &AuthManager{
config: cfg,
client: &http.Client{
Timeout: 30 * time.Second,
},
}
}
// EnsureTokenStorageDir creates the token storage directory if it doesn't exist
func (am *AuthManager) EnsureTokenStorageDir() error {
// Check if running as root
if os.Geteuid() != 0 {
return fmt.Errorf("must run as root to create secure token storage directory")
}
// Create directory with restricted permissions (0700 - only root can access)
if err := os.MkdirAll(TokenStorageDir, 0700); err != nil {
return fmt.Errorf("failed to create token storage directory: %w", err)
}
return nil
}
// StartDeviceAuthorization initiates the OAuth device authorization flow
func (am *AuthManager) StartDeviceAuthorization() (*types.DeviceAuthResponse, error) {
payload := map[string]interface{}{
"client_id": "nannyagent-cli",
"scope": []string{"agent:register"},
}
jsonData, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("failed to marshal payload: %w", err)
}
url := fmt.Sprintf("%s/device/authorize", am.config.DeviceAuthURL)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := am.client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to start device authorization: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("device authorization failed with status %d: %s", resp.StatusCode, string(body))
}
var deviceResp types.DeviceAuthResponse
if err := json.Unmarshal(body, &deviceResp); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
return &deviceResp, nil
}
// PollForToken polls the token endpoint until authorization is complete
func (am *AuthManager) PollForToken(deviceCode string) (*types.TokenResponse, error) {
fmt.Println("⏳ Waiting for user authorization...")
for attempts := 0; attempts < MaxPollAttempts; attempts++ {
tokenReq := types.TokenRequest{
GrantType: "urn:ietf:params:oauth:grant-type:device_code",
DeviceCode: deviceCode,
}
jsonData, err := json.Marshal(tokenReq)
if err != nil {
return nil, fmt.Errorf("failed to marshal token request: %w", err)
}
url := fmt.Sprintf("%s/token", am.config.DeviceAuthURL)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("failed to create token request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := am.client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to poll for token: %w", err)
}
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("failed to read token response: %w", err)
}
var tokenResp types.TokenResponse
if err := json.Unmarshal(body, &tokenResp); err != nil {
return nil, fmt.Errorf("failed to parse token response: %w", err)
}
if tokenResp.Error != "" {
if tokenResp.Error == "authorization_pending" {
fmt.Print(".")
time.Sleep(PollInterval)
continue
}
return nil, fmt.Errorf("authorization failed: %s", tokenResp.ErrorDescription)
}
if tokenResp.AccessToken != "" {
fmt.Println("\n✅ Authorization successful!")
return &tokenResp, nil
}
time.Sleep(PollInterval)
}
return nil, fmt.Errorf("authorization timed out after %d attempts", MaxPollAttempts)
}
// RefreshAccessToken refreshes an expired access token using the refresh token
func (am *AuthManager) RefreshAccessToken(refreshToken string) (*types.TokenResponse, error) {
tokenReq := types.TokenRequest{
GrantType: "refresh_token",
RefreshToken: refreshToken,
}
jsonData, err := json.Marshal(tokenReq)
if err != nil {
return nil, fmt.Errorf("failed to marshal refresh request: %w", err)
}
url := fmt.Sprintf("%s/token", am.config.DeviceAuthURL)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("failed to create refresh request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := am.client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to refresh token: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read refresh response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("token refresh failed with status %d: %s", resp.StatusCode, string(body))
}
var tokenResp types.TokenResponse
if err := json.Unmarshal(body, &tokenResp); err != nil {
return nil, fmt.Errorf("failed to parse refresh response: %w", err)
}
if tokenResp.Error != "" {
return nil, fmt.Errorf("token refresh failed: %s", tokenResp.ErrorDescription)
}
return &tokenResp, nil
}
// SaveToken saves the authentication token to secure local storage
func (am *AuthManager) SaveToken(token *types.AuthToken) error {
if err := am.EnsureTokenStorageDir(); err != nil {
return fmt.Errorf("failed to ensure token storage directory: %w", err)
}
// Save main token file
tokenPath := am.getTokenPath()
jsonData, err := json.MarshalIndent(token, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal token: %w", err)
}
if err := os.WriteFile(tokenPath, jsonData, 0600); err != nil {
return fmt.Errorf("failed to save token: %w", err)
}
// Also save refresh token separately for backup recovery
if token.RefreshToken != "" {
refreshTokenPath := filepath.Join(TokenStorageDir, RefreshTokenFile)
if err := os.WriteFile(refreshTokenPath, []byte(token.RefreshToken), 0600); err != nil {
// Don't fail if refresh token backup fails, just log
fmt.Printf("Warning: Failed to save backup refresh token: %v\n", err)
}
}
return nil
} // LoadToken loads the authentication token from secure local storage
func (am *AuthManager) LoadToken() (*types.AuthToken, error) {
tokenPath := am.getTokenPath()
data, err := os.ReadFile(tokenPath)
if err != nil {
return nil, fmt.Errorf("failed to read token file: %w", err)
}
var token types.AuthToken
if err := json.Unmarshal(data, &token); err != nil {
return nil, fmt.Errorf("failed to parse token: %w", err)
}
// Check if token is expired
if time.Now().After(token.ExpiresAt.Add(-5 * time.Minute)) {
return nil, fmt.Errorf("token is expired or expiring soon")
}
return &token, nil
}
// IsTokenExpired checks if a token needs refresh
func (am *AuthManager) IsTokenExpired(token *types.AuthToken) bool {
// Consider token expired if it expires within the next 5 minutes
return time.Now().After(token.ExpiresAt.Add(-5 * time.Minute))
}
// RegisterDevice performs the complete device registration flow
func (am *AuthManager) RegisterDevice() (*types.AuthToken, error) {
// Step 1: Start device authorization
deviceAuth, err := am.StartDeviceAuthorization()
if err != nil {
return nil, fmt.Errorf("failed to start device authorization: %w", err)
}
fmt.Printf("Please visit: %s\n", deviceAuth.VerificationURI)
fmt.Printf("And enter code: %s\n", deviceAuth.UserCode)
// Step 2: Poll for token
tokenResp, err := am.PollForToken(deviceAuth.DeviceCode)
if err != nil {
return nil, fmt.Errorf("failed to get token: %w", err)
}
// Step 3: Create token storage
token := &types.AuthToken{
AccessToken: tokenResp.AccessToken,
RefreshToken: tokenResp.RefreshToken,
TokenType: tokenResp.TokenType,
ExpiresAt: time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second),
AgentID: tokenResp.AgentID,
}
// Step 4: Save token
if err := am.SaveToken(token); err != nil {
return nil, fmt.Errorf("failed to save token: %w", err)
}
return token, nil
}
// EnsureAuthenticated ensures the agent has a valid token, refreshing if necessary
func (am *AuthManager) EnsureAuthenticated() (*types.AuthToken, error) {
// Try to load existing token
token, err := am.LoadToken()
if err == nil && !am.IsTokenExpired(token) {
return token, nil
}
// Try to refresh with existing refresh token (even if access token is missing/expired)
var refreshToken string
if err == nil && token.RefreshToken != "" {
// Use refresh token from loaded token
refreshToken = token.RefreshToken
} else {
// Try to load refresh token from main token file even if load failed
if existingToken, loadErr := am.loadTokenIgnoringExpiry(); loadErr == nil && existingToken.RefreshToken != "" {
refreshToken = existingToken.RefreshToken
} else {
// Try to load refresh token from backup file
if backupRefreshToken, backupErr := am.loadRefreshTokenFromBackup(); backupErr == nil {
refreshToken = backupRefreshToken
fmt.Println("🔄 Found backup refresh token, attempting to use it...")
}
}
}
if refreshToken != "" {
fmt.Println("🔄 Attempting to refresh access token...")
refreshResp, refreshErr := am.RefreshAccessToken(refreshToken)
if refreshErr == nil {
// Get existing agent_id from current token or backup
var agentID string
if err == nil && token.AgentID != "" {
agentID = token.AgentID
} else if existingToken, loadErr := am.loadTokenIgnoringExpiry(); loadErr == nil {
agentID = existingToken.AgentID
}
// Create new token with refreshed values
newToken := &types.AuthToken{
AccessToken: refreshResp.AccessToken,
RefreshToken: refreshToken, // Keep existing refresh token
TokenType: refreshResp.TokenType,
ExpiresAt: time.Now().Add(time.Duration(refreshResp.ExpiresIn) * time.Second),
AgentID: agentID, // Preserve agent_id
}
// Update refresh token if a new one was provided
if refreshResp.RefreshToken != "" {
newToken.RefreshToken = refreshResp.RefreshToken
}
if saveErr := am.SaveToken(newToken); saveErr == nil {
return newToken, nil
}
} else {
fmt.Printf("⚠️ Token refresh failed: %v\n", refreshErr)
}
}
fmt.Println("📝 Initiating new device registration...")
return am.RegisterDevice()
}
// loadTokenIgnoringExpiry loads token file without checking expiry
func (am *AuthManager) loadTokenIgnoringExpiry() (*types.AuthToken, error) {
tokenPath := am.getTokenPath()
data, err := os.ReadFile(tokenPath)
if err != nil {
return nil, fmt.Errorf("failed to read token file: %w", err)
}
var token types.AuthToken
if err := json.Unmarshal(data, &token); err != nil {
return nil, fmt.Errorf("failed to parse token: %w", err)
}
return &token, nil
}
// loadRefreshTokenFromBackup tries to load refresh token from backup file
func (am *AuthManager) loadRefreshTokenFromBackup() (string, error) {
refreshTokenPath := filepath.Join(TokenStorageDir, RefreshTokenFile)
data, err := os.ReadFile(refreshTokenPath)
if err != nil {
return "", fmt.Errorf("failed to read refresh token backup: %w", err)
}
refreshToken := strings.TrimSpace(string(data))
if refreshToken == "" {
return "", fmt.Errorf("refresh token backup is empty")
}
return refreshToken, nil
}
func (am *AuthManager) getTokenPath() string {
if am.config.TokenPath != "" {
return am.config.TokenPath
}
return filepath.Join(TokenStorageDir, TokenStorageFile)
}
func getHostname() string {
if hostname, err := os.Hostname(); err == nil {
return hostname
}
return "unknown"
}

131
internal/config/config.go Normal file
View File

@@ -0,0 +1,131 @@
package config
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/joho/godotenv"
)
type Config struct {
// Supabase Configuration
SupabaseProjectURL string
// Edge Function Endpoints (auto-generated from SupabaseProjectURL)
DeviceAuthURL string
AgentAuthURL string
// Agent Configuration
TokenPath string
MetricsInterval int
// Debug/Development
Debug bool
}
var DefaultConfig = Config{
TokenPath: "./token.json",
MetricsInterval: 30,
Debug: false,
}
// LoadConfig loads configuration from environment variables and .env file
func LoadConfig() (*Config, error) {
config := DefaultConfig
// Try to load .env file from current directory or parent directories
envFile := findEnvFile()
if envFile != "" {
if err := godotenv.Load(envFile); err != nil {
fmt.Printf("Warning: Could not load .env file from %s: %v\n", envFile, err)
} else {
fmt.Printf("Loaded configuration from %s\n", envFile)
}
}
// Load from environment variables
if url := os.Getenv("SUPABASE_PROJECT_URL"); url != "" {
config.SupabaseProjectURL = url
}
if tokenPath := os.Getenv("TOKEN_PATH"); tokenPath != "" {
config.TokenPath = tokenPath
}
if debug := os.Getenv("DEBUG"); debug == "true" || debug == "1" {
config.Debug = true
}
// Auto-generate edge function URLs from project URL
if config.SupabaseProjectURL != "" {
config.DeviceAuthURL = fmt.Sprintf("%s/functions/v1/device-auth", config.SupabaseProjectURL)
config.AgentAuthURL = fmt.Sprintf("%s/functions/v1/agent-auth-api", config.SupabaseProjectURL)
}
// Validate required configuration
if err := config.Validate(); err != nil {
return nil, fmt.Errorf("configuration validation failed: %w", err)
}
return &config, nil
}
// Validate checks if all required configuration is present
func (c *Config) Validate() error {
var missing []string
if c.SupabaseProjectURL == "" {
missing = append(missing, "SUPABASE_PROJECT_URL")
}
if c.DeviceAuthURL == "" {
missing = append(missing, "DEVICE_AUTH_URL (or SUPABASE_PROJECT_URL)")
}
if c.AgentAuthURL == "" {
missing = append(missing, "AGENT_AUTH_URL (or SUPABASE_PROJECT_URL)")
}
if len(missing) > 0 {
return fmt.Errorf("missing required environment variables: %s", strings.Join(missing, ", "))
}
return nil
}
// findEnvFile looks for .env file in current directory and parent directories
func findEnvFile() string {
dir, err := os.Getwd()
if err != nil {
return ""
}
for {
envPath := filepath.Join(dir, ".env")
if _, err := os.Stat(envPath); err == nil {
return envPath
}
parent := filepath.Dir(dir)
if parent == dir {
break
}
dir = parent
}
return ""
}
// PrintConfig prints the current configuration (masking sensitive values)
func (c *Config) PrintConfig() {
if !c.Debug {
return
}
fmt.Println("Configuration:")
fmt.Printf(" Supabase Project URL: %s\n", c.SupabaseProjectURL)
fmt.Printf(" Metrics Interval: %d seconds\n", c.MetricsInterval)
fmt.Printf(" Debug: %v\n", c.Debug)
}

View File

@@ -0,0 +1,315 @@
package metrics
import (
"bytes"
"crypto/sha256"
"encoding/json"
"fmt"
"io"
"math"
"net/http"
"strings"
"time"
"github.com/shirou/gopsutil/v3/cpu"
"github.com/shirou/gopsutil/v3/disk"
"github.com/shirou/gopsutil/v3/host"
"github.com/shirou/gopsutil/v3/load"
"github.com/shirou/gopsutil/v3/mem"
psnet "github.com/shirou/gopsutil/v3/net"
"nannyagentv2/internal/types"
)
// Collector handles system metrics collection
type Collector struct {
agentVersion string
}
// NewCollector creates a new metrics collector
func NewCollector(agentVersion string) *Collector {
return &Collector{
agentVersion: agentVersion,
}
}
// GatherSystemMetrics collects comprehensive system metrics
func (c *Collector) GatherSystemMetrics() (*types.SystemMetrics, error) {
metrics := &types.SystemMetrics{
Timestamp: time.Now(),
}
// System Information
if hostInfo, err := host.Info(); err == nil {
metrics.Hostname = hostInfo.Hostname
metrics.Platform = hostInfo.Platform
metrics.PlatformFamily = hostInfo.PlatformFamily
metrics.PlatformVersion = hostInfo.PlatformVersion
metrics.KernelVersion = hostInfo.KernelVersion
metrics.KernelArch = hostInfo.KernelArch
}
// CPU Metrics
if percentages, err := cpu.Percent(time.Second, false); err == nil && len(percentages) > 0 {
metrics.CPUUsage = math.Round(percentages[0]*100) / 100
}
if cpuInfo, err := cpu.Info(); err == nil && len(cpuInfo) > 0 {
metrics.CPUCores = len(cpuInfo)
metrics.CPUModel = cpuInfo[0].ModelName
}
// Memory Metrics
if memInfo, err := mem.VirtualMemory(); err == nil {
metrics.MemoryUsage = math.Round(float64(memInfo.Used)/(1024*1024)*100) / 100 // MB
metrics.MemoryTotal = memInfo.Total
metrics.MemoryUsed = memInfo.Used
metrics.MemoryFree = memInfo.Free
metrics.MemoryAvailable = memInfo.Available
}
if swapInfo, err := mem.SwapMemory(); err == nil {
metrics.SwapTotal = swapInfo.Total
metrics.SwapUsed = swapInfo.Used
metrics.SwapFree = swapInfo.Free
}
// Disk Metrics
if diskInfo, err := disk.Usage("/"); err == nil {
metrics.DiskUsage = math.Round(diskInfo.UsedPercent*100) / 100
metrics.DiskTotal = diskInfo.Total
metrics.DiskUsed = diskInfo.Used
metrics.DiskFree = diskInfo.Free
}
// Load Averages
if loadAvg, err := load.Avg(); err == nil {
metrics.LoadAvg1 = math.Round(loadAvg.Load1*100) / 100
metrics.LoadAvg5 = math.Round(loadAvg.Load5*100) / 100
metrics.LoadAvg15 = math.Round(loadAvg.Load15*100) / 100
}
// Process Count (simplified - using a constant for now)
// Note: gopsutil doesn't have host.Processes(), would need process.Processes()
metrics.ProcessCount = 0 // Placeholder
// Network Metrics
netIn, netOut := c.getNetworkStats()
metrics.NetworkInKbps = netIn
metrics.NetworkOutKbps = netOut
if netIOCounters, err := psnet.IOCounters(false); err == nil && len(netIOCounters) > 0 {
netIO := netIOCounters[0]
metrics.NetworkInBytes = netIO.BytesRecv
metrics.NetworkOutBytes = netIO.BytesSent
}
// IP Address and Location
metrics.IPAddress = c.getIPAddress()
metrics.Location = c.getLocation() // Placeholder
// Filesystem Information
metrics.FilesystemInfo = c.getFilesystemInfo()
// Block Devices
metrics.BlockDevices = c.getBlockDevices()
return metrics, nil
}
// getNetworkStats returns network input/output rates in Kbps
func (c *Collector) getNetworkStats() (float64, float64) {
netIOCounters, err := psnet.IOCounters(false)
if err != nil || len(netIOCounters) == 0 {
return 0.0, 0.0
}
// Use the first interface for aggregate stats
netIO := netIOCounters[0]
// Convert bytes to kilobits per second (simplified - cumulative bytes to kilobits)
netInKbps := float64(netIO.BytesRecv) * 8 / 1024
netOutKbps := float64(netIO.BytesSent) * 8 / 1024
return netInKbps, netOutKbps
}
// getIPAddress returns the primary IP address of the system
func (c *Collector) getIPAddress() string {
interfaces, err := psnet.Interfaces()
if err != nil {
return "unknown"
}
for _, iface := range interfaces {
if len(iface.Addrs) > 0 && !strings.Contains(iface.Addrs[0].Addr, "127.0.0.1") {
return strings.Split(iface.Addrs[0].Addr, "/")[0] // Remove CIDR if present
}
}
return "unknown"
}
// getLocation returns basic location information (placeholder)
func (c *Collector) getLocation() string {
return "unknown" // Would integrate with GeoIP service
}
// getFilesystemInfo returns information about mounted filesystems
func (c *Collector) getFilesystemInfo() []types.FilesystemInfo {
partitions, err := disk.Partitions(false)
if err != nil {
return []types.FilesystemInfo{}
}
var filesystems []types.FilesystemInfo
for _, partition := range partitions {
usage, err := disk.Usage(partition.Mountpoint)
if err != nil {
continue
}
fs := types.FilesystemInfo{
Mountpoint: partition.Mountpoint,
Fstype: partition.Fstype,
Total: usage.Total,
Used: usage.Used,
Free: usage.Free,
UsagePercent: math.Round(usage.UsedPercent*100) / 100,
}
filesystems = append(filesystems, fs)
}
return filesystems
}
// getBlockDevices returns information about block devices
func (c *Collector) getBlockDevices() []types.BlockDevice {
partitions, err := disk.Partitions(true)
if err != nil {
return []types.BlockDevice{}
}
var devices []types.BlockDevice
deviceMap := make(map[string]bool)
for _, partition := range partitions {
// Only include actual block devices
if strings.HasPrefix(partition.Device, "/dev/") {
deviceName := partition.Device
if !deviceMap[deviceName] {
deviceMap[deviceName] = true
device := types.BlockDevice{
Name: deviceName,
Model: "unknown",
Size: 0,
SerialNumber: "unknown",
}
devices = append(devices, device)
}
}
}
return devices
}
// SendMetrics sends system metrics to the agent-auth-api endpoint
func (c *Collector) SendMetrics(agentAuthURL, accessToken, agentID string, metrics *types.SystemMetrics) error {
// Create flattened metrics request for agent-auth-api
metricsReq := c.CreateMetricsRequest(agentID, metrics)
return c.sendMetricsRequest(agentAuthURL, accessToken, metricsReq)
}
// CreateMetricsRequest converts SystemMetrics to the flattened format expected by agent-auth-api
func (c *Collector) CreateMetricsRequest(agentID string, systemMetrics *types.SystemMetrics) *types.MetricsRequest {
return &types.MetricsRequest{
AgentID: agentID,
CPUUsage: systemMetrics.CPUUsage,
MemoryUsage: systemMetrics.MemoryUsage,
DiskUsage: systemMetrics.DiskUsage,
NetworkInKbps: systemMetrics.NetworkInKbps,
NetworkOutKbps: systemMetrics.NetworkOutKbps,
IPAddress: systemMetrics.IPAddress,
Location: systemMetrics.Location,
AgentVersion: c.agentVersion,
KernelVersion: systemMetrics.KernelVersion,
DeviceFingerprint: c.generateDeviceFingerprint(systemMetrics),
LoadAverages: map[string]float64{
"load1": systemMetrics.LoadAvg1,
"load5": systemMetrics.LoadAvg5,
"load15": systemMetrics.LoadAvg15,
},
OSInfo: map[string]string{
"platform": systemMetrics.Platform,
"platform_family": systemMetrics.PlatformFamily,
"platform_version": systemMetrics.PlatformVersion,
"kernel_version": systemMetrics.KernelVersion,
"kernel_arch": systemMetrics.KernelArch,
},
FilesystemInfo: systemMetrics.FilesystemInfo,
BlockDevices: systemMetrics.BlockDevices,
NetworkStats: map[string]uint64{
"bytes_sent": systemMetrics.NetworkOutBytes,
"bytes_recv": systemMetrics.NetworkInBytes,
"total_bytes": systemMetrics.NetworkInBytes + systemMetrics.NetworkOutBytes,
},
}
}
// sendMetricsRequest sends the metrics request to the agent-auth-api
func (c *Collector) sendMetricsRequest(agentAuthURL, accessToken string, metricsReq *types.MetricsRequest) error {
// Wrap metrics in the expected payload structure
payload := map[string]interface{}{
"metrics": metricsReq,
"timestamp": time.Now().UTC().Format(time.RFC3339),
}
jsonData, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("failed to marshal metrics: %w", err)
}
// Send to /metrics endpoint
metricsURL := fmt.Sprintf("%s/metrics", agentAuthURL)
req, err := http.NewRequest("POST", metricsURL, bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("failed to send metrics: %w", err)
}
defer resp.Body.Close()
// Read response
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response: %w", err)
}
// Check response status
if resp.StatusCode == http.StatusUnauthorized {
return fmt.Errorf("unauthorized")
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("metrics request failed with status %d: %s", resp.StatusCode, string(body))
}
return nil
}
// generateDeviceFingerprint creates a unique device identifier
func (c *Collector) generateDeviceFingerprint(metrics *types.SystemMetrics) string {
fingerprint := fmt.Sprintf("%s-%s-%s", metrics.Hostname, metrics.Platform, metrics.KernelVersion)
hasher := sha256.New()
hasher.Write([]byte(fingerprint))
return fmt.Sprintf("%x", hasher.Sum(nil))[:16]
}

170
internal/types/types.go Normal file
View File

@@ -0,0 +1,170 @@
package types
import "time"
// SystemMetrics represents comprehensive system performance metrics
type SystemMetrics struct {
// System Information
Hostname string `json:"hostname"`
Platform string `json:"platform"`
PlatformFamily string `json:"platform_family"`
PlatformVersion string `json:"platform_version"`
KernelVersion string `json:"kernel_version"`
KernelArch string `json:"kernel_arch"`
// CPU Metrics
CPUUsage float64 `json:"cpu_usage"`
CPUCores int `json:"cpu_cores"`
CPUModel string `json:"cpu_model"`
// Memory Metrics
MemoryUsage float64 `json:"memory_usage"`
MemoryTotal uint64 `json:"memory_total"`
MemoryUsed uint64 `json:"memory_used"`
MemoryFree uint64 `json:"memory_free"`
MemoryAvailable uint64 `json:"memory_available"`
SwapTotal uint64 `json:"swap_total"`
SwapUsed uint64 `json:"swap_used"`
SwapFree uint64 `json:"swap_free"`
// Disk Metrics
DiskUsage float64 `json:"disk_usage"`
DiskTotal uint64 `json:"disk_total"`
DiskUsed uint64 `json:"disk_used"`
DiskFree uint64 `json:"disk_free"`
// Network Metrics
NetworkInKbps float64 `json:"network_in_kbps"`
NetworkOutKbps float64 `json:"network_out_kbps"`
NetworkInBytes uint64 `json:"network_in_bytes"`
NetworkOutBytes uint64 `json:"network_out_bytes"`
// System Load
LoadAvg1 float64 `json:"load_avg_1"`
LoadAvg5 float64 `json:"load_avg_5"`
LoadAvg15 float64 `json:"load_avg_15"`
// Process Information
ProcessCount int `json:"process_count"`
// Network Information
IPAddress string `json:"ip_address"`
Location string `json:"location"`
// Filesystem Information
FilesystemInfo []FilesystemInfo `json:"filesystem_info"`
BlockDevices []BlockDevice `json:"block_devices"`
// Timestamp
Timestamp time.Time `json:"timestamp"`
}
// FilesystemInfo represents individual filesystem statistics
type FilesystemInfo struct {
Mountpoint string `json:"mountpoint"`
Fstype string `json:"fstype"`
Total uint64 `json:"total"`
Used uint64 `json:"used"`
Free uint64 `json:"free"`
UsagePercent float64 `json:"usage_percent"`
}
// BlockDevice represents block device information
type BlockDevice struct {
Name string `json:"name"`
Size uint64 `json:"size"`
Model string `json:"model"`
SerialNumber string `json:"serial_number"`
}
// NetworkStats represents detailed network interface statistics
type NetworkStats struct {
InterfaceName string `json:"interface_name"`
BytesSent uint64 `json:"bytes_sent"`
BytesRecv uint64 `json:"bytes_recv"`
PacketsSent uint64 `json:"packets_sent"`
PacketsRecv uint64 `json:"packets_recv"`
ErrorsIn uint64 `json:"errors_in"`
ErrorsOut uint64 `json:"errors_out"`
DropsIn uint64 `json:"drops_in"`
DropsOut uint64 `json:"drops_out"`
}
// AuthToken represents the authentication token structure
type AuthToken struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresAt time.Time `json:"expires_at"`
TokenType string `json:"token_type"`
AgentID string `json:"agent_id"`
}
// DeviceAuthRequest represents the device authorization request
type DeviceAuthRequest struct {
ClientID string `json:"client_id"`
Scope string `json:"scope,omitempty"`
}
// DeviceAuthResponse represents the device authorization response
type DeviceAuthResponse struct {
DeviceCode string `json:"device_code"`
UserCode string `json:"user_code"`
VerificationURI string `json:"verification_uri"`
ExpiresIn int `json:"expires_in"`
Interval int `json:"interval"`
}
// TokenRequest represents the token request for device flow
type TokenRequest struct {
GrantType string `json:"grant_type"`
DeviceCode string `json:"device_code,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`
ClientID string `json:"client_id,omitempty"`
}
// TokenResponse represents the token response
type TokenResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
AgentID string `json:"agent_id,omitempty"`
Error string `json:"error,omitempty"`
ErrorDescription string `json:"error_description,omitempty"`
}
// HeartbeatRequest represents the agent heartbeat request
type HeartbeatRequest struct {
AgentID string `json:"agent_id"`
Status string `json:"status"`
Metrics SystemMetrics `json:"metrics"`
}
// MetricsRequest represents the flattened metrics payload expected by agent-auth-api
type MetricsRequest struct {
// Agent identification
AgentID string `json:"agent_id"`
// Basic metrics
CPUUsage float64 `json:"cpu_usage"`
MemoryUsage float64 `json:"memory_usage"`
DiskUsage float64 `json:"disk_usage"`
// Network metrics
NetworkInKbps float64 `json:"network_in_kbps"`
NetworkOutKbps float64 `json:"network_out_kbps"`
// System information
IPAddress string `json:"ip_address"`
Location string `json:"location"`
AgentVersion string `json:"agent_version"`
KernelVersion string `json:"kernel_version"`
DeviceFingerprint string `json:"device_fingerprint"`
// Structured data (JSON fields in database)
LoadAverages map[string]float64 `json:"load_averages"`
OSInfo map[string]string `json:"os_info"`
FilesystemInfo []FilesystemInfo `json:"filesystem_info"`
BlockDevices []BlockDevice `json:"block_devices"`
NetworkStats map[string]uint64 `json:"network_stats"`
}