gohorsejobs/backend/internal/api/handlers/storage_handler.go
Tiago Yamamoto 841b1d780c feat: Email System, Avatar Upload, Email Templates UI, and Public Job Posting
- Backend: Email producer (LavinMQ), EmailService interface
- Backend: CRUD API for email_templates and email_settings
- Backend: avatar_url field in users table + UpdateMyProfile support
- Backend: StorageService for pre-signed URLs
- NestJS: Email consumer with Nodemailer and Handlebars
- Frontend: Email Templates admin pages (list/edit)
- Frontend: Updated profileApi.uploadAvatar with pre-signed URL flow
- Frontend: New /post-job public page (company registration + job creation wizard)
- Migrations: 027_create_email_system.sql, 028_add_avatar_url_to_users.sql
2025-12-26 12:21:34 -03:00

83 lines
2.3 KiB
Go

package handlers
import (
"encoding/json"
"fmt"
"net/http"
"path/filepath"
"strings"
"time"
"github.com/rede5/gohorsejobs/backend/internal/api/middleware"
"github.com/rede5/gohorsejobs/backend/internal/services"
)
type StorageHandler struct {
storageService *services.StorageService
}
func NewStorageHandler(s *services.StorageService) *StorageHandler {
return &StorageHandler{storageService: s}
}
// GetUploadURL returns a pre-signed URL for uploading a file.
// Clients upload directly to this URL.
// Query Params:
// - filename: Original filename
// - contentType: MIME type
// - folder: Optional folder (e.g. 'avatars', 'resumes')
func (h *StorageHandler) GetUploadURL(w http.ResponseWriter, r *http.Request) {
// Authentication required
userIDVal := r.Context().Value(middleware.ContextUserID)
userID, ok := userIDVal.(string)
if !ok || userID == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
filename := r.URL.Query().Get("filename")
contentType := r.URL.Query().Get("contentType")
folder := r.URL.Query().Get("folder")
if filename == "" {
http.Error(w, "Filename is required", http.StatusBadRequest)
return
}
if folder == "" {
folder = "uploads" // Default
}
// Validate folder
validFolders := map[string]bool{"avatars": true, "resumes": true, "logos": true, "uploads": true}
if !validFolders[folder] {
http.Error(w, "Invalid folder", http.StatusBadRequest)
return
}
// Generate a unique key
ext := filepath.Ext(filename)
if ext == "" {
// Attempt to guess from contentType if needed, or just allow no ext
}
// Key format: {folder}/{userID}/{timestamp}_{random}{ext}
// Using user ID ensures isolation if needed, or use a UUID.
key := fmt.Sprintf("%s/%s/%d_%s", folder, userID, time.Now().Unix(), strings.ReplaceAll(filename, " ", "_"))
url, err := h.storageService.GetPresignedUploadURL(r.Context(), key, contentType)
if err != nil {
// If credentials missing, log error and return 500
// "storage credentials incomplete" might mean admin needs to configure them.
http.Error(w, "Failed to generate upload URL: "+err.Error(), http.StatusInternalServerError)
return
}
// Return simple JSON
resp := map[string]string{
"url": url,
"key": key, // Client needs key to save to DB profile
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}