Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ type Config struct {
DB `env:",prefix=DB_"`
LLM `env:",prefix=LLM_"`
Clerk `env:",prefix=CLERK_"`
S3 `env:",prefix=AWS_S3_"`
}
8 changes: 8 additions & 0 deletions backend/config/s3.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package config

type S3 struct {
AccessKeyID string `env:"ACCESS_KEY_ID,required"`
SecretAccessKey string `env:"SECRET_ACCESS_KEY,required"`
Region string `env:"REGION,required"`
BucketName string `env:"BUCKET_NAME,required"`
}
19 changes: 19 additions & 0 deletions backend/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ module github.com/generate/selfserve
go 1.24.1

require (
github.com/aws/aws-sdk-go-v2 v1.41.1
github.com/aws/aws-sdk-go-v2/config v1.32.7
github.com/aws/aws-sdk-go-v2/credentials v1.19.7
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.1
github.com/clerk/clerk-sdk-go/v2 v2.5.1
github.com/firebase/genkit/go v1.4.0
github.com/go-playground/validator/v10 v10.30.1
Expand All @@ -15,6 +19,21 @@ require (

require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
github.com/aws/smithy-go v1.24.0 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
Expand Down
38 changes: 38 additions & 0 deletions backend/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,44 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU=
github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY=
github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY=
github.com/aws/aws-sdk-go-v2/credentials v1.19.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8=
github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 h1:JqcdRG//czea7Ppjb+g/n4o8i/R50aTBHkA7vu0lK+k=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17/go.mod h1:CO+WeGmIdj/MlPel2KwID9Gt7CNq4M65HUfBW97liM0=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 h1:Z5EiPIzXKewUQK0QTMkutjiaPVeVYXX7KIqhXu/0fXs=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8/go.mod h1:FsTpJtvC4U1fyDXk7c71XoDv3HlRm8V3NiYLeYLh5YE=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 h1:bGeHBsGZx0Dvu/eJC0Lh9adJa3M1xREcndxLNZlve2U=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17/go.mod h1:dcW24lbU0CzHusTE8LLHhRLI42ejmINN8Lcr22bwh/g=
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.1 h1:C2dUPSnEpy4voWFIq3JNd8gN0Y5vYGDo44eUE58a/p8=
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.1/go.mod h1:5jggDlZ2CLQhwJBiZJb4vfk4f0GxWdEDruWKEJ1xOdo=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 h1:gd84Omyu9JLriJVCbGApcLzVR3XtmC4ZDPcAI6Ftvds=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ=
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
Expand Down
47 changes: 47 additions & 0 deletions backend/internal/handler/s3.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package handler

import (
"time"

"github.com/generate/selfserve/internal/errs"
s3storage "github.com/generate/selfserve/internal/service/s3"
"github.com/gofiber/fiber/v2"
)

const expirationTime = 5 * time.Minute // Moved to a package level constant for reusability

type S3Handler struct {
S3Storage *s3storage.Storage
}

func NewS3Handler(s3Storage *s3storage.Storage) *S3Handler {
return &S3Handler{S3Storage: s3Storage}
}

// GeneratePresignedURL godoc
// @Summary Generate a presigned URL for a file
// @Description Generates a presigned URL for a file
// @Tags s3
// @Accept json
// @Produce json
// @Param key path string true "File key"
// @Success 200 {object} map[string]string "Presigned URL response"
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /s3/presigned-url/{key} [get]

func (h *S3Handler) GeneratePresignedURL(c *fiber.Ctx) error {
key := c.Params("key")
if key == "" {
return errs.BadRequest("key is required")
}

presignedURL, err := h.S3Storage.GeneratePresignedURL(c.Context(), key, expirationTime)
if err != nil {
return errs.InternalServerError()
}

return c.JSON(fiber.Map{
"presigned_url": presignedURL,
})
}
80 changes: 80 additions & 0 deletions backend/internal/service/s3/s3storage.go
Copy link
Copy Markdown
Contributor

@danctila danctila Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the S3 storage location being under storage/postgres/s3/ might not make the most sense architecturally because S3 is not related to Postgres

I would recommend:

backend/internal/service/storage/
|-postgres/
|  |----storage.go
|
|-s3/
   |---s3storage.go

Make sure to:

  • move the file
  • update all imports necessary

Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package s3

import (
"context"
"fmt"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
awsConfig "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"
"github.com/generate/selfserve/config"
)

type Storage struct {
Client *s3.Client
BucketName string
URL *s3.PresignClient
}

func NewS3Storage(cfg config.S3) (*Storage, error) {
Comment thread
spokonya marked this conversation as resolved.
// Create AWS config with your credentials
awsCfg, err := awsConfig.LoadDefaultConfig(context.Background(),
awsConfig.WithRegion(cfg.Region),
awsConfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
cfg.AccessKeyID,
cfg.SecretAccessKey,
"",
)),
)
if err != nil {
return nil, fmt.Errorf("failed to create AWS config: %w", err)
}

// Create S3 client
client := s3.NewFromConfig(awsCfg)

return &Storage{
Client: client,
BucketName: cfg.BucketName,
URL: s3.NewPresignClient(client),
}, nil
}

func (s *Storage) GeneratePresignedURL(ctx context.Context, key string, expiration time.Duration) (string, error) {
Comment thread
spokonya marked this conversation as resolved.
if key == "" {
return "", fmt.Errorf("key is required")
}
if expiration <= 0 {
return "", fmt.Errorf("expiration must be greater than 0")
}
presignedURL, err := s.URL.PresignPutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(s.BucketName),
Key: aws.String(key),
}, func(opts *s3.PresignOptions) {
opts.Expires = expiration
Comment thread
spokonya marked this conversation as resolved.
},
)

if err != nil {
return "", fmt.Errorf("failed to generate presigned URL with key %s: %w", key, err)
}
Comment thread
Dao-Ho marked this conversation as resolved.

return presignedURL.URL, nil
}

func (s *Storage) DeleteFile(ctx context.Context, key string) error {
if key == "" {
return fmt.Errorf("key is required")
}
_, err := s.Client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: aws.String(s.BucketName),
Key: aws.String(key),
})
if err != nil {
return fmt.Errorf("failed to delete file with key %s: %w", key, err)
}

return nil
}
33 changes: 25 additions & 8 deletions backend/internal/service/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/generate/selfserve/internal/handler"
"github.com/generate/selfserve/internal/repository"
"github.com/generate/selfserve/internal/service/clerk"
s3storage "github.com/generate/selfserve/internal/service/s3"
storage "github.com/generate/selfserve/internal/service/storage/postgres"
"github.com/generate/selfserve/internal/validation"
"github.com/goccy/go-json"
Expand All @@ -27,41 +28,51 @@ import (
)

type App struct {
Server *fiber.App
Repo *storage.Repository
Server *fiber.App
Repo *storage.Repository
S3Storage *s3storage.Storage
}

func InitApp(cfg *config.Config) (*App, error) {
validation.Init()

// Init DB/repository(ies)

repo, err := storage.NewRepository(cfg.DB)
if err != nil {
return nil, err
}

s3Store, err := s3storage.NewS3Storage(cfg.S3)
if err != nil {
if e := repo.Close(); e != nil {
return nil, errors.Join(err, e)
}
return nil, err
}

genkitInstance := aiflows.InitGenkit(context.Background(), &cfg.LLM)

app := setupApp()
setupClerk(cfg)

if err = setupRoutes(app, repo, genkitInstance, cfg); err != nil {
if err = setupRoutes(app, repo, genkitInstance, cfg, s3Store); err != nil {
if e := repo.Close(); e != nil {
return nil, errors.Join(err, e)
}
return nil, err
}

return &App{
Server: app,
Repo: repo,
Server: app,
Repo: repo,
S3Storage: s3Store,
}, nil

}

func setupRoutes(app *fiber.App, repo *storage.Repository, genkitInstance *aiflows.GenkitService,
cfg *config.Config) error {

cfg *config.Config, s3Store *s3storage.Storage) error {
// Swagger documentation
app.Get("/swagger/*", handler.ServeSwagger)

Expand All @@ -85,13 +96,13 @@ func setupRoutes(app *fiber.App, repo *storage.Repository, genkitInstance *aiflo
guestsHandler := handler.NewGuestsHandler(repository.NewGuestsRepository(repo.DB))
reqsHandler := handler.NewRequestsHandler(repository.NewRequestsRepo(repo.DB), genkitInstance)
hotelsHandler := handler.NewHotelsHandler(repository.NewHotelsRepository(repo.DB))
s3Handler := handler.NewS3Handler(s3Store)

clerkWhSignatureVerifier, err := handler.NewWebhookVerifier(cfg)
if err != nil {
return err
}
clerkWebhookHandler := handler.NewClerkWebHookHandler(usersRepo, clerkWhSignatureVerifier)

// API v1 routes
api := app.Group("/api/v1")

Expand Down Expand Up @@ -141,6 +152,12 @@ func setupRoutes(app *fiber.App, repo *storage.Repository, genkitInstance *aiflo
r.Post("/", hotelsHandler.CreateHotel)
})

// s3 routes

api.Route("/s3", func(r fiber.Router) {
r.Get("/presigned-url/:key", s3Handler.GeneratePresignedURL)
})

Comment thread
spokonya marked this conversation as resolved.
return nil
}

Expand Down
Loading