add auth
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
)
|
||||
|
||||
// R2 wraps an S3 client pointed at Cloudflare's R2 endpoint.
|
||||
//
|
||||
// We use R2 for avatar uploads through presigned PUT URLs:
|
||||
//
|
||||
// frontend asks BFF for a presigned URL ─► BFF returns URL + final public URL
|
||||
// frontend PUTs the file directly to R2 ─► R2 stores it
|
||||
// frontend POSTs the public URL back to BFF profile-update endpoint
|
||||
//
|
||||
// The BFF never proxies the file bytes; bandwidth stays browser↔R2.
|
||||
type R2 struct {
|
||||
client *s3.Client
|
||||
bucket string
|
||||
publicBase string // R2_PUBLIC_URL, no trailing slash
|
||||
}
|
||||
|
||||
// R2Config captures the env vars needed to talk to R2.
|
||||
type R2Config struct {
|
||||
AccountID string
|
||||
AccessKeyID string
|
||||
SecretAccessKey string
|
||||
Bucket string
|
||||
PublicURL string
|
||||
}
|
||||
|
||||
// Configured returns true if every required field is present.
|
||||
func (c R2Config) Configured() bool {
|
||||
return c.AccountID != "" && c.AccessKeyID != "" && c.SecretAccessKey != "" &&
|
||||
c.Bucket != "" && c.PublicURL != ""
|
||||
}
|
||||
|
||||
// NewR2 builds an S3 client pointed at the per-account R2 endpoint.
|
||||
// Returns (nil, nil) if cfg is not Configured — the avatar route will then
|
||||
// return 503 instead of panicking. That keeps the BFF bootable without R2.
|
||||
func NewR2(cfg R2Config) (*R2, error) {
|
||||
if !cfg.Configured() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
endpoint := fmt.Sprintf("https://%s.r2.cloudflarestorage.com", cfg.AccountID)
|
||||
|
||||
creds := credentials.NewStaticCredentialsProvider(cfg.AccessKeyID, cfg.SecretAccessKey, "")
|
||||
client := s3.New(s3.Options{
|
||||
Region: "auto", // R2 ignores region but the SDK requires one
|
||||
Credentials: creds,
|
||||
BaseEndpoint: aws.String(endpoint),
|
||||
UsePathStyle: true,
|
||||
})
|
||||
|
||||
return &R2{
|
||||
client: client,
|
||||
bucket: cfg.Bucket,
|
||||
publicBase: trimTrailingSlash(cfg.PublicURL),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PresignPut returns a one-shot PUT URL the browser can use to upload a file
|
||||
// directly to R2, plus the final public URL where the object will be served.
|
||||
//
|
||||
// expiry caps how long the upload URL is valid; 5 minutes is plenty and
|
||||
// limits replay if the URL leaks.
|
||||
func (r *R2) PresignPut(ctx context.Context, key, contentType string, expiry time.Duration) (uploadURL, publicURL string, err error) {
|
||||
if r == nil {
|
||||
return "", "", errors.New("R2 not configured")
|
||||
}
|
||||
if key == "" {
|
||||
return "", "", errors.New("empty key")
|
||||
}
|
||||
|
||||
presigner := s3.NewPresignClient(r.client)
|
||||
req, err := presigner.PresignPutObject(ctx, &s3.PutObjectInput{
|
||||
Bucket: aws.String(r.bucket),
|
||||
Key: aws.String(key),
|
||||
ContentType: aws.String(contentType),
|
||||
}, s3.WithPresignExpires(expiry))
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("presign put: %w", err)
|
||||
}
|
||||
|
||||
return req.URL, fmt.Sprintf("%s/%s", r.publicBase, key), nil
|
||||
}
|
||||
|
||||
// PublicURL builds the public URL for an arbitrary key. Useful when reading
|
||||
// existing user.avatar_url values back.
|
||||
func (r *R2) PublicURL(key string) string {
|
||||
if r == nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%s/%s", r.publicBase, key)
|
||||
}
|
||||
|
||||
func trimTrailingSlash(s string) string {
|
||||
for len(s) > 0 && s[len(s)-1] == '/' {
|
||||
s = s[:len(s)-1]
|
||||
}
|
||||
return s
|
||||
}
|
||||
Reference in New Issue
Block a user