- 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
91 lines
2.5 KiB
Go
91 lines
2.5 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/aws"
|
|
"github.com/aws/aws-sdk-go-v2/config"
|
|
"github.com/aws/aws-sdk-go-v2/credentials"
|
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
|
)
|
|
|
|
type StorageService struct {
|
|
credentialsService *CredentialsService
|
|
}
|
|
|
|
func NewStorageService(cs *CredentialsService) *StorageService {
|
|
return &StorageService{credentialsService: cs}
|
|
}
|
|
|
|
// UploadConfig holds the necessary keys.
|
|
type UploadConfig struct {
|
|
Endpoint string `json:"endpoint"`
|
|
AccessKey string `json:"accessKey"`
|
|
SecretKey string `json:"secretKey"`
|
|
Bucket string `json:"bucket"`
|
|
Region string `json:"region"`
|
|
}
|
|
|
|
func (s *StorageService) getClient(ctx context.Context) (*s3.PresignClient, string, error) {
|
|
// 1. Fetch Credentials from DB (Encrypted Payload)
|
|
payload, err := s.credentialsService.GetDecryptedKey(ctx, "storage")
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("failed to get storage credentials: %w", err)
|
|
}
|
|
|
|
var uCfg UploadConfig
|
|
if err := json.Unmarshal([]byte(payload), &uCfg); err != nil {
|
|
return nil, "", fmt.Errorf("failed to parse storage credentials: %w", err)
|
|
}
|
|
|
|
if uCfg.Endpoint == "" || uCfg.AccessKey == "" || uCfg.SecretKey == "" || uCfg.Bucket == "" {
|
|
return nil, "", fmt.Errorf("storage credentials incomplete (all fields required)")
|
|
}
|
|
|
|
if uCfg.Region == "" {
|
|
uCfg.Region = "auto"
|
|
}
|
|
|
|
// 2. Setup S3 V2 Client
|
|
cfg, err := config.LoadDefaultConfig(ctx,
|
|
config.WithRegion(uCfg.Region),
|
|
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(uCfg.AccessKey, uCfg.SecretKey, "")),
|
|
)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
// R2/S3 specific endpoint
|
|
client := s3.NewFromConfig(cfg, func(o *s3.Options) {
|
|
o.BaseEndpoint = aws.String(uCfg.Endpoint)
|
|
o.UsePathStyle = true // Often needed for R2/MinIO
|
|
})
|
|
|
|
psClient := s3.NewPresignClient(client)
|
|
return psClient, uCfg.Bucket, nil
|
|
}
|
|
|
|
// GetPresignedUploadURL generates a URL for PUT requests
|
|
func (s *StorageService) GetPresignedUploadURL(ctx context.Context, key string, contentType string) (string, error) {
|
|
psClient, bucket, err := s.getClient(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
req, err := psClient.PresignPutObject(ctx, &s3.PutObjectInput{
|
|
Bucket: aws.String(bucket),
|
|
Key: aws.String(key),
|
|
ContentType: aws.String(contentType),
|
|
}, func(o *s3.PresignOptions) {
|
|
o.Expires = 15 * time.Minute
|
|
})
|
|
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to presign upload: %w", err)
|
|
}
|
|
|
|
return req.URL, nil
|
|
}
|