Skip to content

Commit

Permalink
Cover
Browse files Browse the repository at this point in the history
  • Loading branch information
abaldeweg authored Oct 18, 2024
1 parent 69ce8ba commit 056e813
Show file tree
Hide file tree
Showing 7 changed files with 329 additions and 153 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ func main() {
}
```

Mount cover directory under `/usr/src/app/uploads/cover`.

### Config

```go
Expand Down Expand Up @@ -72,6 +74,7 @@ r.Use(corsConfig.SetCorsHeaders())
|API_CORE |API endpoint for the core |gateway
|project_dir |Path to docker compose |admincli
|database |Database name to dump |admincli
|AUTH_API_ME |Authentication API endpoint |gateway

admincli will read a config file from following paths:

Expand Down
63 changes: 63 additions & 0 deletions gateway/auth/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package auth

import (
"encoding/json"
"net/http"

"github.com/gin-gonic/gin"
"github.com/spf13/viper"
)

// User represents a user object.
type User struct {
Id int `json:"id"`
Username string `json:"username"`
Branch Branch `json:"branch"`
Roles []string `json:"roles"`
}

// Branch represents a branch object.
type Branch struct {
Id int `json:"id"`
}

// Authenticate authenticates a user based on the Authorization header.
// It makes a request to the auth service to validate the token and retrieve user information.
func Authenticate(c *gin.Context) bool {
viper.SetDefault("AUTH_API_ME", "/")

authHeader := c.GetHeader("Authorization")

if authHeader == "" || len(authHeader) < 7 || authHeader[0:7] != "Bearer " {
return false
}

token := authHeader[7:]

req, err := http.NewRequest("GET", viper.GetString("AUTH_API_ME"), nil)
if err != nil {
return false
}

req.Header.Set("Authorization", "Bearer "+token)

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return false
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return false
}

var user User
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
return false
}

c.Set("user", user)

return true
}
54 changes: 54 additions & 0 deletions gateway/auth/auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package auth

import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/gin-gonic/gin"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
)

func TestAuthenticate(t *testing.T) {
mockAuthService := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != "Bearer test-token" {
w.WriteHeader(http.StatusUnauthorized)
return
}

user := User{
Id: 1,
Username: "testuser",
Branch: Branch{
Id: 1,
},
Roles: []string{"admin"},
}
json.NewEncoder(w).Encode(user)
}))
defer mockAuthService.Close()

viper.Set("AUTH_API_ME", mockAuthService.URL)

c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("GET", "/", nil)

testCases := []struct {
name string
authorization string
expected bool
}{
{"Missing Authorization header", "", false},
{"Invalid Authorization header", "InvalidToken", false},
{"Valid Authorization header", "Bearer test-token", true},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
c.Request.Header.Set("Authorization", tc.authorization)
assert.Equal(t, tc.expected, Authenticate(c))
})
}
}
131 changes: 131 additions & 0 deletions gateway/cover/cover.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package cover

import (
"fmt"
"image"
"image/jpeg"
"image/png"
"mime/multipart"
"net/http"
"os"
"path/filepath"

"github.com/gin-gonic/gin"

"github.com/nfnt/resize"
)

const uploadsDir = "uploads"

// SaveCover saves the uploaded cover image in different sizes.
func SaveCover(c *gin.Context, imageUUID string) {
imageData, err := c.FormFile("cover")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Image upload required"})
return
}

if err := saveResizedImages(c, imageData, imageUUID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"})
return
}

c.Status(http.StatusOK)
}

func saveResizedImages(c *gin.Context, imageData *multipart.FileHeader, imageUUID string) error {
imagePath, err := saveUploadedImage(c, imageData, imageUUID)
if err != nil {
return fmt.Errorf("failed to save uploaded image: %w", err)
}
defer os.Remove(imagePath)

sizes := []struct {
width uint
suffix string
}{
{400, "l"},
{200, "m"},
{100, "s"},
}

for _, size := range sizes {
resizedImagePath := filepath.Join(uploadsDir, fmt.Sprintf("%s-%s%s", imageUUID, size.suffix, filepath.Ext(imageData.Filename)))

if err := resizeAndSaveImage(imagePath, resizedImagePath, size.width); err != nil {
return fmt.Errorf("failed to resize image: %w", err)
}
}

return nil
}

func saveUploadedImage(c *gin.Context, imageData *multipart.FileHeader, imageUUID string) (string, error) {
imageFilename := fmt.Sprintf("%s%s", imageUUID, filepath.Ext(imageData.Filename))
currentDir, _ := os.Getwd()
uploadsDirPath := filepath.Join(currentDir, uploadsDir)

if err := os.MkdirAll(uploadsDirPath, 0755); err != nil {
return "", fmt.Errorf("failed to create uploads directory")
}

imagePath := filepath.Join(uploadsDirPath, imageFilename)
if err := c.SaveUploadedFile(imageData, imagePath); err != nil {
return "", fmt.Errorf("failed to save image")
}

return imagePath, nil
}

func resizeAndSaveImage(imagePath string, resizedImagePath string, width uint) error {
file, err := os.Open(imagePath)
if err != nil {
return fmt.Errorf("failed to open image")
}
defer file.Close()

img, err := decodeImage(file, imagePath)
if err != nil {
return err
}

resizedImage := resize.Resize(width, 0, img, resize.Lanczos3)

out, err := os.Create(resizedImagePath)
if err != nil {
return fmt.Errorf("failed to create resized image file")
}
defer out.Close()

if err := encodeImage(out, resizedImage, imagePath); err != nil {
return fmt.Errorf("failed to encode resized image")
}

return nil
}

func decodeImage(file *os.File, imagePath string) (image.Image, error) {
ext := filepath.Ext(imagePath)

switch ext {
case ".jpg", ".jpeg":
return jpeg.Decode(file)
case ".png":
return png.Decode(file)
default:
return nil, fmt.Errorf("unsupported image format: %s", ext)
}
}

func encodeImage(out *os.File, resizedImage image.Image, imagePath string) error {
ext := filepath.Ext(imagePath)

switch ext {
case ".jpg", ".jpeg":
return jpeg.Encode(out, resizedImage, nil)
case ".png":
return png.Encode(out, resizedImage)
default:
return fmt.Errorf("unsupported image format: %s", ext)
}
}
62 changes: 62 additions & 0 deletions gateway/cover/cover_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package cover

import (
"bytes"
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"

"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)

func TestSaveCover(t *testing.T) {
gin.SetMode(gin.TestMode)

router := gin.Default()
router.POST("/upload", func(c *gin.Context) {
SaveCover(c, "36ee6d5c-820b-4f0c-9637-73b63dacc2a7")
})

imageData, err := os.ReadFile("test.jpg")
if err != nil {
t.Fatalf("Error reading image file: %v", err)
}

body := new(bytes.Buffer)
writer := multipart.NewWriter(body)
part, _ := writer.CreateFormFile("cover", "test.jpg")
_, _ = io.Copy(part, bytes.NewReader(imageData))
writer.Close()

req, _ := http.NewRequest("POST", "/upload", body)
req.Header.Set("Content-Type", writer.FormDataContentType())

w := httptest.NewRecorder()
router.ServeHTTP(w, req)

assert.Equal(t, http.StatusOK, w.Code)

currentDir, _ := os.Getwd()
expectedFilePaths := []string{
filepath.Join(currentDir, uploadsDir, "36ee6d5c-820b-4f0c-9637-73b63dacc2a7-l.jpg"),
filepath.Join(currentDir, uploadsDir, "36ee6d5c-820b-4f0c-9637-73b63dacc2a7-m.jpg"),
filepath.Join(currentDir, uploadsDir, "36ee6d5c-820b-4f0c-9637-73b63dacc2a7-s.jpg"),
}

for _, expectedFilePath := range expectedFilePaths {
if _, err := os.Stat(expectedFilePath); os.IsNotExist(err) {
t.Errorf("Expected file %s to exist", expectedFilePath)
} else {
os.Remove(expectedFilePath)
}
}

assert.NoFileExists(t, filepath.Join(currentDir, uploadsDir, "36ee6d5c-820b-4f0c-9637-73b63dacc2a7.jpg"))

os.RemoveAll(filepath.Join(currentDir, uploadsDir))
}
Binary file added gateway/cover/test.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 056e813

Please sign in to comment.