package handlers import ( "archive/zip" "crypto/rand" "fmt" "io" "os" "path/filepath" "strings" "time" "github.com/gin-gonic/gin" "github.com/trackeep/backend/config" "github.com/trackeep/backend/models" ) // CreateAPIKeyRequest represents a request to create an API key type CreateAPIKeyRequest struct { Name string `json:"name" binding:"required,min=1,max=100"` Permissions []string `json:"permissions" binding:"required"` ExpiresIn *int `json:"expires_in,omitempty"` // Days until expiration } // APIKeyResponse represents API key response type APIKeyResponse struct { ID uint `json:"id"` Name string `json:"name"` Key string `json:"key"` Permissions []string `json:"permissions"` ExpiresAt *time.Time `json:"expires_at,omitempty"` CreatedAt time.Time `json:"created_at"` } // GenerateAPIKey creates a new API key for browser extension func GenerateAPIKey(c *gin.Context) { user, exists := c.Get("user") if !exists { c.JSON(401, gin.H{"error": "User not authenticated"}) return } currentUser := user.(models.User) var req CreateAPIKeyRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } // Validate permissions validPermissions := map[string]bool{ "bookmarks:read": true, "bookmarks:write": true, "files:read": true, "files:write": true, "files:share": true, "notes:read": true, "notes:write": true, "tasks:read": true, "tasks:write": true, } for _, perm := range req.Permissions { if !validPermissions[perm] { c.JSON(400, gin.H{"error": fmt.Sprintf("Invalid permission: %s", perm)}) return } } // Generate API key key := generateAPIKey() // Set expiration if provided var expiresAt *time.Time if req.ExpiresIn != nil && *req.ExpiresIn > 0 { expiration := time.Now().AddDate(0, 0, *req.ExpiresIn) expiresAt = &expiration } // Create API key record now := time.Now() apiKey := models.APIKey{ Name: req.Name, Key: key, UserID: currentUser.ID, Permissions: req.Permissions, IsActive: true, LastUsed: &now, ExpiresAt: expiresAt, } db := config.GetDB() if err := db.Create(&apiKey).Error; err != nil { c.JSON(500, gin.H{"error": "Failed to create API key"}) return } response := APIKeyResponse{ ID: apiKey.ID, Name: apiKey.Name, Key: apiKey.Key, Permissions: apiKey.Permissions, ExpiresAt: apiKey.ExpiresAt, CreatedAt: apiKey.CreatedAt, } c.JSON(201, response) } // GetAPIKeys retrieves user's API keys func GetAPIKeys(c *gin.Context) { user, exists := c.Get("user") if !exists { c.JSON(401, gin.H{"error": "User not authenticated"}) return } currentUser := user.(models.User) var apiKeys []models.APIKey db := config.GetDB() if err := db.Where("user_id = ? AND is_active = ?", currentUser.ID, true).Order("created_at desc").Find(&apiKeys).Error; err != nil { c.JSON(500, gin.H{"error": "Failed to retrieve API keys"}) return } // Don't return the actual keys in list view var response []map[string]interface{} for _, key := range apiKeys { response = append(response, map[string]interface{}{ "id": key.ID, "name": key.Name, "permissions": key.Permissions, "is_active": key.IsActive, "last_used": key.LastUsed, "expires_at": key.ExpiresAt, "created_at": key.CreatedAt, "updated_at": key.UpdatedAt, }) } c.JSON(200, response) } // RevokeAPIKey revokes an API key func RevokeAPIKey(c *gin.Context) { user, exists := c.Get("user") if !exists { c.JSON(401, gin.H{"error": "User not authenticated"}) return } currentUser := user.(models.User) keyID := c.Param("id") db := config.GetDB() var apiKey models.APIKey if err := db.Where("id = ? AND user_id = ?", keyID, currentUser.ID).First(&apiKey).Error; err != nil { c.JSON(404, gin.H{"error": "API key not found"}) return } // Deactivate the key if err := db.Model(&apiKey).Update("is_active", false).Error; err != nil { c.JSON(500, gin.H{"error": "Failed to revoke API key"}) return } c.JSON(200, gin.H{"message": "API key revoked successfully"}) } // ValidateAPIKey validates an API key from browser extension func ValidateAPIKey(c *gin.Context) { authHeader := c.GetHeader("Authorization") if authHeader == "" { c.JSON(401, gin.H{"error": "Authorization header required"}) return } // Extract Bearer token parts := strings.SplitN(authHeader, " ", 2) if len(parts) != 2 || parts[0] != "Bearer" { c.JSON(401, gin.H{"error": "Invalid authorization format"}) return } apiKey := parts[1] db := config.GetDB() var keyRecord models.APIKey if err := db.Where("key = ? AND is_active = ?", apiKey, true).Preload("User").First(&keyRecord).Error; err != nil { c.JSON(401, gin.H{"error": "Invalid API key"}) return } // Check expiration if keyRecord.ExpiresAt != nil && keyRecord.ExpiresAt.Before(time.Now()) { c.JSON(401, gin.H{"error": "API key expired"}) return } // Update last used timestamp now := time.Now() keyRecord.LastUsed = &now db.Model(&keyRecord).Update("last_used", now) // Return user info for extension c.JSON(200, gin.H{ "valid": true, "user_id": keyRecord.UserID, "permissions": keyRecord.Permissions, }) } // generateAPIKey generates a secure API key func generateAPIKey() string { const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" keyLength := 32 bytes := make([]byte, keyLength) rand.Read(bytes) for i, b := range bytes { bytes[i] = charset[b%byte(len(charset))] } return "tk_" + string(bytes) } // RegisterBrowserExtension registers a browser extension instance func RegisterBrowserExtension(c *gin.Context) { user, exists := c.Get("user") if !exists { c.JSON(401, gin.H{"error": "User not authenticated"}) return } currentUser := user.(models.User) var req struct { ExtensionID string `json:"extension_id" binding:"required"` Name string `json:"name" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } // Check if extension already registered db := config.GetDB() var existingAuth models.BrowserExtension if err := db.Where("user_id = ? AND extension_id = ?", currentUser.ID, req.ExtensionID).First(&existingAuth).Error; err == nil { c.JSON(409, gin.H{"error": "Extension already registered"}) return } // Create new extension registration extAuth := models.BrowserExtension{ UserID: currentUser.ID, ExtensionID: req.ExtensionID, Name: req.Name, IsActive: true, LastSeen: &time.Time{}, } if err := db.Create(&extAuth).Error; err != nil { c.JSON(500, gin.H{"error": "Failed to register extension"}) return } c.JSON(201, gin.H{ "message": "Extension registered successfully", "extension_id": extAuth.ExtensionID, }) } // GetBrowserExtensions retrieves user's registered browser extensions func GetBrowserExtensions(c *gin.Context) { user, exists := c.Get("user") if !exists { c.JSON(401, gin.H{"error": "User not authenticated"}) return } currentUser := user.(models.User) var extensions []models.BrowserExtension db := config.GetDB() if err := db.Where("user_id = ?", currentUser.ID).Order("created_at desc").Find(&extensions).Error; err != nil { c.JSON(500, gin.H{"error": "Failed to retrieve extensions"}) return } c.JSON(200, extensions) } // RevokeBrowserExtension revokes a browser extension func RevokeBrowserExtension(c *gin.Context) { user, exists := c.Get("user") if !exists { c.JSON(401, gin.H{"error": "User not authenticated"}) return } currentUser := user.(models.User) extensionID := c.Param("id") db := config.GetDB() var extAuth models.BrowserExtension if err := db.Where("extension_id = ? AND user_id = ?", extensionID, currentUser.ID).First(&extAuth).Error; err != nil { c.JSON(404, gin.H{"error": "Extension not found"}) return } // Deactivate the extension if err := db.Model(&extAuth).Update("is_active", false).Error; err != nil { c.JSON(500, gin.H{"error": "Failed to revoke extension"}) return } c.JSON(200, gin.H{"message": "Extension revoked successfully"}) } // DownloadBrowserExtension serves the browser extension as a downloadable zip file func DownloadBrowserExtension(c *gin.Context) { // Path to the browser extension directory extDir := "../browser-extension" // Create a temporary zip file zipPath := "/tmp/browser-extension.zip" // Create zip file err := createZip(extDir, zipPath) if err != nil { c.JSON(500, gin.H{"error": "Failed to create zip file"}) return } // Open the zip file zipFile, err := os.Open(zipPath) if err != nil { c.JSON(500, gin.H{"error": "Failed to open zip file"}) return } defer zipFile.Close() // Get file info fileInfo, err := zipFile.Stat() if err != nil { c.JSON(500, gin.H{"error": "Failed to get file info"}) return } // Set headers for download c.Header("Content-Type", "application/zip") c.Header("Content-Disposition", "attachment; filename=browser-extension.zip") c.Header("Content-Length", fmt.Sprintf("%d", fileInfo.Size())) // Copy file to response io.Copy(c.Writer, zipFile) // Clean up temporary file os.Remove(zipPath) } // createZip creates a zip file from a directory func createZip(source, target string) error { zipfile, err := os.Create(target) if err != nil { return err } defer zipfile.Close() archive := zip.NewWriter(zipfile) defer archive.Close() info, err := os.Stat(source) if err != nil { return nil } var baseDir string if info.IsDir() { baseDir = filepath.Base(source) } return filepath.Walk(source, func(path string, info os.FileInfo, err error) error { if err != nil { return err } header, err := zip.FileInfoHeader(info) if err != nil { return err } if baseDir != "" { header.Name = filepath.Join(baseDir, strings.TrimPrefix(path, source)) } if info.IsDir() { header.Name += "/" } else { header.Method = zip.Deflate } writer, err := archive.CreateHeader(header) if err != nil { return err } if info.IsDir() { return nil } file, err := os.Open(path) if err != nil { return err } defer file.Close() _, err = io.Copy(writer, file) return err }) }