Compare commits

..

7 commits

Author SHA1 Message Date
grngxd
b3057e185c delete route 2025-06-08 19:43:57 +02:00
grngxd
ea86ebc6a3 fix file retrieval 2025-06-08 19:43:57 +02:00
grngxd
56a907689e j 2025-06-08 19:43:57 +02:00
grngxd
f947cba89d sharing base64 file data 2025-06-08 19:43:57 +02:00
grngxd
c4f7f79cfd skibidi 2025-06-08 19:43:57 +02:00
grngxd
f2f1c28c71 add to db 2025-06-08 19:43:57 +02:00
grngxd
7733556c76 add auth to /uploads 2025-06-08 19:43:57 +02:00
10 changed files with 190 additions and 39 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
.env
tmp
*.db
imgs

2
go.mod
View file

@ -19,6 +19,7 @@ require (
github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.4 // indirect
@ -29,6 +30,7 @@ require (
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lithammer/shortuuid/v4 v4.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.28 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect

4
go.sum
View file

@ -31,6 +31,8 @@ github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVI
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
@ -60,6 +62,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lithammer/shortuuid/v4 v4.2.0 h1:LMFOzVB3996a7b8aBuEXxqOBflbfPQAiVzkIcHO0h8c=
github.com/lithammer/shortuuid/v4 v4.2.0/go.mod h1:D5noHZ2oFw/YaKCfGy0YxyE7M0wMbezmMjPdhyEFe6Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=

View file

@ -7,6 +7,6 @@ import (
func Register(cfg *types.StereoConfig) {
api := cfg.Router.Group("/api")
routes.RegisterUploadRoutes(cfg, api)
routes.RegisterFileRoutes(cfg, api)
routes.RegisterAuthRoutes(cfg, api)
}

View file

@ -0,0 +1,154 @@
package routes
import (
"encoding/base64"
"os"
"path/filepath"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"stereo.cat/backend/internal/auth"
"stereo.cat/backend/internal/types"
)
func RegisterFileRoutes(cfg *types.StereoConfig, api *gin.RouterGroup) {
api.POST("/upload", auth.JwtMiddleware(cfg.JWTSecret), func(c *gin.Context) {
claims := c.MustGet("claims").(jwt.MapClaims)
user := claims["user"].(auth.User)
uid := user.ID
if uid == "" {
c.JSON(401, gin.H{"error": "unauthorized"})
return
}
file, err := c.FormFile("file")
if err != nil {
c.JSON(400, gin.H{"error": "file is required"})
return
}
filePath := filepath.Join(cfg.ImagePath, uid, file.Filename)
if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {
c.JSON(500, gin.H{"error": "failed to create directory"})
return
}
if err := c.SaveUploadedFile(file, filePath); err != nil {
c.JSON(500, gin.H{"error": "failed to save file"})
return
}
b64, err := convertToBase64(filePath)
if err != nil {
}
fileMeta := types.File{
ID: uid + "_" + file.Filename,
Path: filePath,
Owner: uid,
CreatedAt: time.Now(),
Base64: b64,
}
if err := cfg.Database.Create(&fileMeta).Error; err != nil {
c.JSON(500, gin.H{"error": "failed to save file metadata"})
return
}
c.JSON(200, gin.H{"message": "file uploaded successfully", "file_id": fileMeta.ID})
})
api.DELETE("/delete", auth.JwtMiddleware(cfg.JWTSecret), func(c *gin.Context) {
claims := c.MustGet("claims").(jwt.MapClaims)
user := claims["user"].(auth.User)
uid := user.ID
if uid == "" {
c.JSON(401, gin.H{"error": "unauthorized"})
return
}
var response struct {
FileID string `json:"file_id" binding:"required"`
}
if err := c.ShouldBindJSON(&response); err != nil {
c.JSON(400, gin.H{"error": "file_id is required"})
return
}
resfID := response.FileID
if resfID == "" {
c.JSON(400, gin.H{"error": "file_id cannot be empty"})
return
}
parts := strings.SplitN(resfID, "_", 2)
if len(parts) != 2 {
c.JSON(400, gin.H{"error": "invalid file_id format"})
return
}
fileID, filename := parts[0], parts[1]
if fileID != uid {
c.JSON(403, gin.H{"error": "you can only delete your own files"})
return
}
filePath := filepath.Join(cfg.ImagePath, uid, filename)
if err := os.Remove(filePath); err != nil {
c.JSON(500, gin.H{"error": "failed to delete file"})
return
}
if err := cfg.Database.Where("id = ?", resfID).Delete(&types.File{}).Error; err != nil {
c.JSON(500, gin.H{"error": "failed to delete file metadata"})
return
}
c.JSON(200, gin.H{"message": "file deleted successfully"})
})
api.GET("/:name", func(c *gin.Context) {
name := c.Param("name")
parts := strings.SplitN(name, "_", 2)
if len(parts) != 2 {
c.JSON(400, gin.H{"error": "invalid file name"})
return
}
uid, filename := parts[0], parts[1]
path := filepath.Join(cfg.ImagePath, uid, filename)
if _, err := os.Stat(path); err != nil {
c.JSON(404, gin.H{"error": "file not found"})
return
}
c.File(path)
})
api.GET("/list", auth.JwtMiddleware(cfg.JWTSecret), func(c *gin.Context) {
claims := c.MustGet("claims").(jwt.MapClaims)
user := claims["user"].(auth.User)
var files []types.File
if err := cfg.Database.Where("owner = ?", user.ID).Find(&files).Error; err != nil {
c.JSON(500, gin.H{"error": "failed to retrieve files"})
return
}
c.JSON(200, files)
})
}
func convertToBase64(filePath string) (string, error) {
file, err := os.ReadFile(filePath)
if err != nil {
return "", err
}
b64 := base64.StdEncoding.EncodeToString(file)
return b64, nil
}

View file

@ -1,30 +0,0 @@
package routes
import (
"path/filepath"
"github.com/gin-gonic/gin"
"stereo.cat/backend/internal/types"
)
func RegisterUploadRoutes(cfg *types.StereoConfig, api *gin.RouterGroup) {
api.POST("/upload", func(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.JSON(400, gin.H{"error": "file is required"})
return
}
filePath := filepath.Join(cfg.ImagePath, file.Filename)
if err := c.SaveUploadedFile(file, filePath); err != nil {
c.JSON(500, gin.H{"error": "failed to save file"})
return
}
})
api.GET("/:name", func(c *gin.Context) {
name := c.Param("name")
path := filepath.Join(cfg.ImagePath, name)
c.File(path)
})
}

View file

@ -11,9 +11,9 @@ import (
)
func GenerateJWT(key string, user User, expiryTimestamp uint64) (string, error) {
claims := jwt.MapClaims{
"user": user,
"exp": expiryTimestamp,
claims := Claims{
User: user,
Exp: expiryTimestamp,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

View file

@ -1,6 +1,10 @@
package auth
import "time"
import (
"time"
"github.com/golang-jwt/jwt/v5"
)
type TokenResponse struct {
AccessToken string `json:"access_token"`
@ -28,3 +32,9 @@ type ExchangeCodeRequest struct {
Code string `json:"code"`
RedirectUri string `json:"redirect_uri"`
}
type Claims struct {
User User `json:"user"`
Exp uint64 `json:"exp"`
jwt.RegisteredClaims
}

View file

@ -1,6 +1,8 @@
package types
import (
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"stereo.cat/backend/internal/auth/client"
@ -16,6 +18,14 @@ type StereoConfig struct {
ImagePath string
Router *gin.Engine
Client client.Client
Database *gorm.DB
JWTSecret string
Database *gorm.DB
JWTSecret string
}
type File struct {
ID string `gorm:"primaryKey"`
Path string `gorm:"not null;index"`
Owner string `gorm:"not null;index"`
CreatedAt time.Time `gorm:"autoCreateTime"`
Base64 string `gorm:"type:text"`
}

View file

@ -83,9 +83,9 @@ func main() {
panic(errors.New("Invalid database type was specified."))
}
c.Database.AutoMigrate(&auth.User{})
c.Database.AutoMigrate(&auth.User{}, &types.File{})
api.Register(&c)
fmt.Printf("Running on port %s\n", getEnv("PORT", "8080"))
c.Router.Run()
}