package handlers import ( "crypto/rand" "encoding/base64" "math/big" "strings" "time" "github.com/gin-gonic/gin" "github.com/google/uuid" "gitlab.com/patrickbritton3/sojorn/go-backend/internal/models" "gitlab.com/patrickbritton3/sojorn/go-backend/internal/repository" ) type BackupHandler struct { repo *repository.BackupRepository } func NewBackupHandler(repo *repository.BackupRepository) *BackupHandler { return &BackupHandler{repo: repo} } // GenerateSyncCode generates a 6-digit code for device pairing func (h *BackupHandler) GenerateSyncCode(c *gin.Context) { userIDStr, _ := c.Get("user_id") userID, _ := uuid.Parse(userIDStr.(string)) var req models.GenerateSyncCodeRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } // Generate 6-digit code code, err := h.repo.GenerateSyncCode(c.Request.Context(), userID, req.DeviceName, req.DeviceFingerprint) if err != nil { c.JSON(500, gin.H{"error": "Failed to generate sync code"}) return } c.JSON(200, models.GenerateSyncCodeResponse{ Code: code.Code, ExpiresAt: code.ExpiresAt, ExpiresIn: int(code.ExpiresAt.Sub(time.Now()).Seconds()), }) } // VerifySyncCode verifies a sync code and initiates device pairing func (h *BackupHandler) VerifySyncCode(c *gin.Context) { userIDStr, _ := c.Get("user_id") userID, _ := uuid.Parse(userIDStr.(string)) var req models.VerifySyncCodeRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } // Verify the sync code syncCode, err := h.repo.VerifySyncCode( c.Request.Context(), req.Code, userID, userID, req.DeviceName, req.DeviceFingerprint, ) if err != nil { if strings.Contains(err.Error(), "expired") { c.JSON(400, models.VerifySyncCodeResponse{ Valid: false, ErrorMessage: "Sync code has expired", }) } else if strings.Contains(err.Error(), "invalid") { c.JSON(400, models.VerifySyncCodeResponse{ Valid: false, ErrorMessage: "Invalid sync code", }) } else if strings.Contains(err.Error(), "attempts") { c.JSON(429, models.VerifySyncCodeResponse{ Valid: false, ErrorMessage: "Too many attempts. Please generate a new code.", }) } else { c.JSON(500, gin.H{"error": "Failed to verify sync code"}) } return } // Register the new device _, err = h.repo.RegisterDevice(c.Request.Context(), userID, req.DeviceName, req.DeviceFingerprint, "web") if err != nil { c.JSON(500, gin.H{"error": "Failed to register device"}) return } c.JSON(200, models.VerifySyncCodeResponse{ Valid: true, DeviceAID: syncCode.UserID.String(), DeviceAName: syncCode.DeviceName, // WebRTCOffer would be generated here in a real implementation }) } // UploadBackup uploads an encrypted backup to cloud storage func (h *BackupHandler) UploadBackup(c *gin.Context) { userIDStr, _ := c.Get("user_id") userID, _ := uuid.Parse(userIDStr.(string)) var req models.UploadBackupRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } // Decode base64 data encryptedBlob, err := base64.StdEncoding.DecodeString(req.EncryptedBlob) if err != nil { c.JSON(400, gin.H{"error": "Invalid encrypted blob encoding"}) return } salt, err := base64.StdEncoding.DecodeString(req.Salt) if err != nil { c.JSON(400, gin.H{"error": "Invalid salt encoding"}) return } nonce, err := base64.StdEncoding.DecodeString(req.Nonce) if err != nil { c.JSON(400, gin.H{"error": "Invalid nonce encoding"}) return } mac, err := base64.StdEncoding.DecodeString(req.Mac) if err != nil { c.JSON(400, gin.H{"error": "Invalid MAC encoding"}) return } // Create backup record backup := &models.CloudBackup{ UserID: userID, EncryptedBlob: encryptedBlob, Salt: salt, Nonce: nonce, Mac: mac, Version: req.Version, DeviceName: req.DeviceName, SizeBytes: int64(len(encryptedBlob)), } backupID, err := h.repo.UploadBackup(c.Request.Context(), backup) if err != nil { c.JSON(500, gin.H{"error": "Failed to upload backup"}) return } // Update last backup time h.repo.UpdateLastBackupTime(c.Request.Context(), userID) c.JSON(200, models.UploadBackupResponse{ BackupID: backupID.String(), UploadedAt: time.Now(), Size: backup.SizeBytes, }) } // DownloadBackup downloads an encrypted backup from cloud storage func (h *BackupHandler) DownloadBackup(c *gin.Context) { userIDStr, _ := c.Get("user_id") userID, _ := uuid.Parse(userIDStr.(string)) backupID := c.Param("backup_id") if backupID == "" { // If no specific backup ID, get the latest backup, err := h.repo.GetLatestBackup(c.Request.Context(), userID) if err != nil { c.JSON(404, gin.H{"error": "No backup found"}) return } c.JSON(200, models.DownloadBackupResponse{ EncryptedBlob: base64.StdEncoding.EncodeToString(backup.EncryptedBlob), Salt: base64.StdEncoding.EncodeToString(backup.Salt), Nonce: base64.StdEncoding.EncodeToString(backup.Nonce), Mac: base64.StdEncoding.EncodeToString(backup.Mac), Version: backup.Version, DeviceName: backup.DeviceName, CreatedAt: backup.CreatedAt, }) return } // Get specific backup backupUUID, err := uuid.Parse(backupID) if err != nil { c.JSON(400, gin.H{"error": "Invalid backup ID"}) return } backup, err := h.repo.GetBackup(c.Request.Context(), backupUUID, userID) if err != nil { c.JSON(404, gin.H{"error": "Backup not found"}) return } c.JSON(200, models.DownloadBackupResponse{ EncryptedBlob: base64.StdEncoding.EncodeToString(backup.EncryptedBlob), Salt: base64.StdEncoding.EncodeToString(backup.Salt), Nonce: base64.StdEncoding.EncodeToString(backup.Nonce), Mac: base64.StdEncoding.EncodeToString(backup.Mac), Version: backup.Version, DeviceName: backup.DeviceName, CreatedAt: backup.CreatedAt, }) } // ListBackups lists all available backups for a user func (h *BackupHandler) ListBackups(c *gin.Context) { userIDStr, _ := c.Get("user_id") userID, _ := uuid.Parse(userIDStr.(string)) backups, err := h.repo.ListBackups(c.Request.Context(), userID) if err != nil { c.JSON(500, gin.H{"error": "Failed to list backups"}) return } c.JSON(200, models.ListBackupsResponse{ Backups: backups, }) } // DeleteBackup deletes a specific backup func (h *BackupHandler) DeleteBackup(c *gin.Context) { userIDStr, _ := c.Get("user_id") userID, _ := uuid.Parse(userIDStr.(string)) backupID := c.Param("backup_id") backupUUID, err := uuid.Parse(backupID) if err != nil { c.JSON(400, gin.H{"error": "Invalid backup ID"}) return } err = h.repo.DeleteBackup(c.Request.Context(), backupUUID, userID) if err != nil { c.JSON(500, gin.H{"error": "Failed to delete backup"}) return } c.JSON(200, gin.H{"message": "Backup deleted successfully"}) } // SetupSocialRecovery sets up social recovery with trusted guardians func (h *BackupHandler) SetupSocialRecovery(c *gin.Context) { userIDStr, _ := c.Get("user_id") userID, _ := uuid.Parse(userIDStr.(string)) var req models.SetupSocialRecoveryRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } // Validate guardian count if len(req.GuardianUserIDs) < 3 || len(req.GuardianUserIDs) > 5 { c.JSON(400, gin.H{"error": "Must have between 3 and 5 guardians"}) return } // Convert string IDs to UUIDs guardianIDs := make([]uuid.UUID, len(req.GuardianUserIDs)) for i, idStr := range req.GuardianUserIDs { id, err := uuid.Parse(idStr) if err != nil { c.JSON(400, gin.H{"error": "Invalid guardian user ID"}) return } guardianIDs[i] = id } // Generate master secret and split into shards masterSecret := make([]byte, 32) if _, err := rand.Read(masterSecret); err != nil { c.JSON(500, gin.H{"error": "Failed to generate master secret"}) return } // For now, we'll store the master secret encrypted with user's keys // In a real implementation, you'd use Shamir's Secret Sharing err := h.repo.SetupSocialRecovery(c.Request.Context(), userID, guardianIDs, masterSecret) if err != nil { c.JSON(500, gin.H{"error": "Failed to setup social recovery"}) return } c.JSON(200, gin.H{"message": "Social recovery setup complete"}) } // InitiateRecovery starts a recovery session func (h *BackupHandler) InitiateRecovery(c *gin.Context) { userIDStr, _ := c.Get("user_id") userID, _ := uuid.Parse(userIDStr.(string)) var req models.InitiateRecoveryRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } session, err := h.repo.InitiateRecovery(c.Request.Context(), userID, req.Method) if err != nil { c.JSON(500, gin.H{"error": "Failed to initiate recovery"}) return } c.JSON(200, session) } // SubmitShard submits a recovery shard from a guardian func (h *BackupHandler) SubmitShard(c *gin.Context) { userIDStr, _ := c.Get("user_id") userID, _ := uuid.Parse(userIDStr.(string)) var req models.SubmitShardRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } sessionUUID, err := uuid.Parse(req.SessionID) if err != nil { c.JSON(400, gin.H{"error": "Invalid session ID"}) return } // Decode base64 shard shardEncrypted, err := base64.StdEncoding.DecodeString(req.ShardEncrypted) if err != nil { c.JSON(400, gin.H{"error": "Invalid shard encoding"}) return } submission, err := h.repo.SubmitShard(c.Request.Context(), sessionUUID, userID, shardEncrypted) if err != nil { c.JSON(500, gin.H{"error": "Failed to submit shard"}) return } c.JSON(200, models.SubmitShardResponse{ ShardsReceived: submission.ShardsReceived, ShardsNeeded: submission.ShardsNeeded, CanComplete: submission.ShardsReceived >= submission.ShardsNeeded, }) } // CompleteRecovery attempts to complete the recovery process func (h *BackupHandler) CompleteRecovery(c *gin.Context) { userIDStr, _ := c.Get("user_id") userID, _ := uuid.Parse(userIDStr.(string)) sessionID := c.Param("session_id") sessionUUID, err := uuid.Parse(sessionID) if err != nil { c.JSON(400, gin.H{"error": "Invalid session ID"}) return } masterKey, err := h.repo.CompleteRecovery(c.Request.Context(), sessionUUID, userID) if err != nil { if strings.Contains(err.Error(), "insufficient") { c.JSON(400, models.CompleteRecoveryResponse{ Success: false, ErrorMessage: "Insufficient shards to complete recovery", }) } else { c.JSON(500, models.CompleteRecoveryResponse{ Success: false, ErrorMessage: "Failed to complete recovery", }) } return } c.JSON(200, models.CompleteRecoveryResponse{ Success: true, MasterKey: base64.StdEncoding.EncodeToString(masterKey), }) } // GetBackupPreferences gets user's backup preferences func (h *BackupHandler) GetBackupPreferences(c *gin.Context) { userIDStr, _ := c.Get("user_id") userID, _ := uuid.Parse(userIDStr.(string)) preferences, err := h.repo.GetBackupPreferences(c.Request.Context(), userID) if err != nil { c.JSON(500, gin.H{"error": "Failed to get backup preferences"}) return } c.JSON(200, preferences) } // UpdateBackupPreferences updates user's backup preferences func (h *BackupHandler) UpdateBackupPreferences(c *gin.Context) { userIDStr, _ := c.Get("user_id") userID, _ := uuid.Parse(userIDStr.(string)) var req models.UpdateBackupPreferencesRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } err := h.repo.UpdateBackupPreferences(c.Request.Context(), userID, req) if err != nil { c.JSON(500, gin.H{"error": "Failed to update backup preferences"}) return } c.JSON(200, gin.H{"message": "Backup preferences updated"}) } // GetUserDevices gets all registered devices for a user func (h *BackupHandler) GetUserDevices(c *gin.Context) { userIDStr, _ := c.Get("user_id") userID, _ := uuid.Parse(userIDStr.(string)) devices, err := h.repo.GetUserDevices(c.Request.Context(), userID) if err != nil { c.JSON(500, gin.H{"error": "Failed to get user devices"}) return } c.JSON(200, gin.H{"devices": devices}) } // Helper function to generate random 6-digit code func generateSyncCode() string { code := make([]byte, 6) for i := range code { digit, err := rand.Int(rand.Reader, big.NewInt(10)) if err != nil { code[i] = '0' continue } code[i] = byte(digit.Int64()) + '0' } return string(code) }