Add delete account endpoint and file encryption at rest

Delete Account (Plan #2):
- DELETE /api/auth/account/ with password or "DELETE" confirmation
- Cascade delete across 15+ tables in correct FK order
- Auth provider detection (email/apple/google) for /auth/me/
- File cleanup after account deletion
- Handler + repository tests (12 tests)

Encryption at Rest (Plan #3):
- AES-256-GCM envelope encryption (per-file DEK wrapped by KEK)
- Encrypt on upload, auto-decrypt on serve via StorageService.ReadFile()
- MediaHandler serves decrypted files via c.Blob()
- TaskService email image loading uses ReadFile()
- cmd/migrate-encrypt CLI tool with --dry-run for existing files
- Encryption service + storage service tests (18 tests)
This commit is contained in:
Trey T
2026-03-26 10:41:01 -05:00
parent 72866e935e
commit 4abc57535e
22 changed files with 1675 additions and 82 deletions

View File

@@ -1,6 +1,7 @@
package config
import (
"encoding/hex"
"fmt"
"net/url"
"os"
@@ -137,10 +138,11 @@ type SecurityConfig struct {
// StorageConfig holds file storage settings
type StorageConfig struct {
UploadDir string // Directory to store uploaded files
BaseURL string // Public URL prefix for serving files (e.g., "/uploads")
MaxFileSize int64 // Max file size in bytes (default 10MB)
AllowedTypes string // Comma-separated MIME types
UploadDir string // Directory to store uploaded files
BaseURL string // Public URL prefix for serving files (e.g., "/uploads")
MaxFileSize int64 // Max file size in bytes (default 10MB)
AllowedTypes string // Comma-separated MIME types
EncryptionKey string // 64-char hex key for file encryption at rest (optional)
}
// FeatureFlags holds kill switches for major subsystems.
@@ -262,10 +264,11 @@ func Load() (*Config, error) {
MaxPasswordResetRate: 3,
},
Storage: StorageConfig{
UploadDir: viper.GetString("STORAGE_UPLOAD_DIR"),
BaseURL: viper.GetString("STORAGE_BASE_URL"),
MaxFileSize: viper.GetInt64("STORAGE_MAX_FILE_SIZE"),
AllowedTypes: viper.GetString("STORAGE_ALLOWED_TYPES"),
UploadDir: viper.GetString("STORAGE_UPLOAD_DIR"),
BaseURL: viper.GetString("STORAGE_BASE_URL"),
MaxFileSize: viper.GetInt64("STORAGE_MAX_FILE_SIZE"),
AllowedTypes: viper.GetString("STORAGE_ALLOWED_TYPES"),
EncryptionKey: viper.GetString("STORAGE_ENCRYPTION_KEY"),
},
AppleAuth: AppleAuthConfig{
ClientID: viper.GetString("APPLE_CLIENT_ID"),
@@ -414,6 +417,16 @@ func validate(cfg *Config) error {
// Database password might come from DATABASE_URL, don't require it separately
// The actual connection will fail if credentials are wrong
// Validate STORAGE_ENCRYPTION_KEY if set: must be exactly 64 hex characters
if cfg.Storage.EncryptionKey != "" {
if len(cfg.Storage.EncryptionKey) != 64 {
return fmt.Errorf("STORAGE_ENCRYPTION_KEY must be exactly 64 hex characters (got %d)", len(cfg.Storage.EncryptionKey))
}
if _, err := hex.DecodeString(cfg.Storage.EncryptionKey); err != nil {
return fmt.Errorf("STORAGE_ENCRYPTION_KEY contains invalid hex: %w", err)
}
}
return nil
}