sojorn/go-backend/internal/handlers/backup_handler.go
2026-02-15 00:33:24 -06:00

455 lines
12 KiB
Go

package handlers
import (
"crypto/rand"
"encoding/base64"
"math/big"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/patbritton/sojorn-backend/internal/models"
"github.com/patbritton/sojorn-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)
}