🎨 Update project structure

This commit is contained in:
2024-06-16 23:24:54 +08:00
parent a1aa418496
commit 05a59113c9
28 changed files with 63 additions and 27 deletions

View File

@ -0,0 +1,21 @@
package database
import (
"git.solsynth.dev/hydrogen/paperclip/pkg/models"
"gorm.io/gorm"
)
var AutoMaintainRange = []any{
&models.Account{},
&models.Attachment{},
}
func RunMigration(source *gorm.DB) error {
if err := source.AutoMigrate(
AutoMaintainRange...,
); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,28 @@
package database
import (
"github.com/rs/zerolog/log"
"github.com/samber/lo"
"github.com/spf13/viper"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
)
var C *gorm.DB
func NewSource() error {
var err error
dialector := postgres.Open(viper.GetString("database.dsn"))
C, err = gorm.Open(dialector, &gorm.Config{NamingStrategy: schema.NamingStrategy{
TablePrefix: viper.GetString("database.prefix"),
}, Logger: logger.New(&log.Logger, logger.Config{
Colorful: true,
IgnoreRecordNotFoundError: true,
LogLevel: lo.Ternary(viper.GetBool("debug.database"), logger.Info, logger.Silent),
})})
return err
}

View File

@ -0,0 +1,70 @@
package grpc
import (
"context"
"fmt"
"git.solsynth.dev/hydrogen/paperclip/pkg/internal/database"
"git.solsynth.dev/hydrogen/paperclip/pkg/proto"
"google.golang.org/protobuf/types/known/emptypb"
"git.solsynth.dev/hydrogen/paperclip/pkg/models"
jsoniter "github.com/json-iterator/go"
)
func (v *Server) GetAttachment(ctx context.Context, request *proto.AttachmentLookupRequest) (*proto.Attachment, error) {
var attachment models.Attachment
tx := database.C.Model(&models.Attachment{})
if request.Id != nil {
tx = tx.Where("id = ?", request.GetId())
}
if request.Uuid != nil {
tx = tx.Where("uuid = ?", request.GetUuid())
}
if request.Usage != nil {
tx = tx.Where("usage = ?", request.GetUsage())
}
if err := tx.First(&attachment).Error; err != nil {
return nil, err
}
rawMetadata, _ := jsoniter.Marshal(attachment.Metadata)
return &proto.Attachment{
Id: uint64(attachment.ID),
Uuid: attachment.Uuid,
Size: attachment.Size,
Name: attachment.Name,
Alt: attachment.Alternative,
Usage: attachment.Usage,
Mimetype: attachment.MimeType,
Hash: attachment.HashCode,
Destination: attachment.Destination,
Metadata: rawMetadata,
IsMature: attachment.IsMature,
AccountId: uint64(attachment.AccountID),
}, nil
}
func (v *Server) CheckAttachmentExists(ctx context.Context, request *proto.AttachmentLookupRequest) (*emptypb.Empty, error) {
tx := database.C.Model(&models.Attachment{})
if request.Id != nil {
tx = tx.Where("id = ?", request.GetId())
}
if request.Uuid != nil {
tx = tx.Where("uuid = ?", request.GetUuid())
}
if request.Usage != nil {
tx = tx.Where("usage = ?", request.GetUsage())
}
var count int64
if err := tx.Model(&models.Attachment{}).Count(&count).Error; err != nil {
return nil, err
} else if count == 0 {
return nil, fmt.Errorf("record not found")
}
return &emptypb.Empty{}, nil
}

View File

@ -0,0 +1,22 @@
package grpc
import (
idpb "git.solsynth.dev/hydrogen/passport/pkg/grpc/proto"
"google.golang.org/grpc/credentials/insecure"
"github.com/spf13/viper"
"google.golang.org/grpc"
)
var Auth idpb.AuthClient
func ConnectPassport() error {
addr := viper.GetString("passport.grpc_endpoint")
if conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials())); err != nil {
return err
} else {
Auth = idpb.NewAuthClient(conn)
}
return nil
}

View File

@ -0,0 +1,29 @@
package grpc
import (
"git.solsynth.dev/hydrogen/paperclip/pkg/proto"
"net"
"github.com/spf13/viper"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
type Server struct {
proto.UnimplementedAttachmentsServer
}
func StartGrpc() error {
listen, err := net.Listen("tcp", viper.GetString("grpc_bind"))
if err != nil {
return err
}
server := grpc.NewServer()
proto.RegisterAttachmentsServer(server, &Server{})
reflection.Register(server)
return server.Serve(listen)
}

5
pkg/internal/meta.go Normal file
View File

@ -0,0 +1,5 @@
package pkg
const (
AppVersion = "1.0.0"
)

View File

@ -0,0 +1,18 @@
package models
// Account profiles basically fetched from Hydrogen.Passport
// But cache at here for better usage
// At the same time this model can make relations between local models
type Account struct {
BaseModel
Name string `json:"name"`
Nick string `json:"nick"`
Avatar string `json:"avatar"`
Banner string `json:"banner"`
Description string `json:"description"`
EmailAddress string `json:"email_address"`
PowerLevel int `json:"power_level"`
Attachments []Attachment `json:"attachments"`
ExternalID uint `json:"external_id"`
}

View File

@ -0,0 +1,22 @@
package models
import "gorm.io/datatypes"
type Attachment struct {
BaseModel
Uuid string `json:"uuid"`
Size int64 `json:"size"`
Name string `json:"name"`
Alternative string `json:"alt"`
Usage string `json:"usage"`
MimeType string `json:"mimetype"`
HashCode string `json:"hash"`
Destination string `json:"destination"`
Metadata datatypes.JSONMap `json:"metadata"`
IsMature bool `json:"is_mature"`
Account Account `json:"account"`
AccountID uint `json:"account_id"`
}

View File

@ -0,0 +1,17 @@
package models
import (
"time"
"gorm.io/datatypes"
"gorm.io/gorm"
)
type JSONMap = datatypes.JSONType[map[string]any]
type BaseModel struct {
ID uint `json:"id" gorm:"primaryKey"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
}

View File

@ -0,0 +1,27 @@
package models
const (
DestinationTypeLocal = "local"
DestinationTypeS3 = "s3"
)
type BaseDestination struct {
Type string `json:"type"`
}
type LocalDestination struct {
BaseDestination
Path string `json:"path"`
}
type S3Destination struct {
BaseDestination
Path string `json:"path"`
Bucket string `json:"bucket"`
Endpoint string `json:"endpoint"`
SecretID string `json:"secret_id"`
SecretKey string `json:"secret_key"`
EnableSSL bool `json:"enable_ssl"`
}

View File

@ -0,0 +1,4 @@
package models
type MediaMetadata struct {
}

View File

@ -0,0 +1,190 @@
package server
import (
"context"
"fmt"
"git.solsynth.dev/hydrogen/paperclip/pkg/internal/database"
"git.solsynth.dev/hydrogen/paperclip/pkg/internal/grpc"
"net/url"
"path/filepath"
"git.solsynth.dev/hydrogen/passport/pkg/grpc/proto"
"git.solsynth.dev/hydrogen/paperclip/pkg/models"
"git.solsynth.dev/hydrogen/paperclip/pkg/services"
"github.com/gofiber/fiber/v2"
jsoniter "github.com/json-iterator/go"
"github.com/samber/lo"
"github.com/spf13/viper"
)
func openAttachment(c *fiber.Ctx) error {
id, _ := c.ParamsInt("id", 0)
metadata, err := services.GetAttachmentByID(uint(id))
if err != nil {
return fiber.NewError(fiber.StatusNotFound)
}
destMap := viper.GetStringMap("destinations")
dest, destOk := destMap[metadata.Destination]
if !destOk {
return fiber.NewError(fiber.StatusInternalServerError, "invalid destination: destination configuration was not found")
}
var destParsed models.BaseDestination
rawDest, _ := jsoniter.Marshal(dest)
_ = jsoniter.Unmarshal(rawDest, &destParsed)
switch destParsed.Type {
case models.DestinationTypeLocal:
var destConfigured models.LocalDestination
_ = jsoniter.Unmarshal(rawDest, &destConfigured)
if len(metadata.MimeType) > 0 {
c.Set(fiber.HeaderContentType, metadata.MimeType)
}
return c.SendFile(filepath.Join(destConfigured.Path, metadata.Uuid), false)
case models.DestinationTypeS3:
var destConfigured models.S3Destination
_ = jsoniter.Unmarshal(rawDest, &destConfigured)
protocol := lo.Ternary(destConfigured.EnableSSL, "https", "http")
return c.Redirect(fmt.Sprintf(
"%s://%s.%s/%s",
protocol,
destConfigured.Bucket,
destConfigured.Endpoint,
url.QueryEscape(filepath.Join(destConfigured.Path, metadata.Uuid)),
))
default:
return fmt.Errorf("invalid destination: unsupported protocol %s", destParsed.Type)
}
}
func getAttachmentMeta(c *fiber.Ctx) error {
id, _ := c.ParamsInt("id")
metadata, err := services.GetAttachmentByID(uint(id))
if err != nil {
return fiber.NewError(fiber.StatusNotFound)
}
return c.JSON(metadata)
}
func createAttachment(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
destName := c.Query("destination", viper.GetString("preferred_destination"))
hash := c.FormValue("hash")
if len(hash) != 64 {
return fiber.NewError(fiber.StatusBadRequest, "please provide a sha-256 hash code, length should be 64 characters")
}
usage := c.FormValue("usage")
if !lo.Contains(viper.GetStringSlice("accepts_usage"), usage) {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("disallowed usage: %s", usage))
}
file, err := c.FormFile("file")
if err != nil {
return err
}
requiredPerm, _ := jsoniter.Marshal(file.Size)
if result, err := grpc.Auth.CheckPerm(context.Background(), &proto.CheckPermRequest{
Token: c.Locals("token").(string),
Key: "CreatePaperclipAttachments",
Value: requiredPerm,
}); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("failed to check permission: %v", err))
} else if !result.GetIsValid() {
return fiber.NewError(
fiber.StatusForbidden,
fmt.Sprintf("requires permission CreatePaperclipAttachments equals or greater than %d", file.Size),
)
}
usermeta := make(map[string]any)
_ = jsoniter.UnmarshalFromString(c.FormValue("metadata"), &usermeta)
tx := database.C.Begin()
metadata, linked, err := services.NewAttachmentMetadata(tx, user, file, models.Attachment{
Usage: usage,
HashCode: hash,
Alternative: c.FormValue("alt"),
MimeType: c.FormValue("mimetype"),
Metadata: usermeta,
IsMature: len(c.FormValue("mature")) > 0,
Destination: destName,
})
if err != nil {
tx.Rollback()
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
if !linked {
if err := services.UploadFile(destName, c, file, metadata); err != nil {
tx.Rollback()
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
}
tx.Commit()
return c.JSON(metadata)
}
func updateAttachmentMeta(c *fiber.Ctx) error {
id, _ := c.ParamsInt("id", 0)
user := c.Locals("principal").(models.Account)
var data struct {
Alternative string `json:"alt"`
Usage string `json:"usage"`
Metadata map[string]any `json:"metadata"`
IsMature bool `json:"is_mature"`
}
if err := BindAndValidate(c, &data); err != nil {
return err
}
var attachment models.Attachment
if err := database.C.Where(models.Attachment{
BaseModel: models.BaseModel{ID: uint(id)},
AccountID: user.ID,
}).First(&attachment).Error; err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
attachment.Alternative = data.Alternative
attachment.Usage = data.Usage
attachment.Metadata = data.Metadata
attachment.IsMature = data.IsMature
if err := database.C.Save(&attachment).Error; err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(attachment)
}
func deleteAttachment(c *fiber.Ctx) error {
id, _ := c.ParamsInt("id", 0)
user := c.Locals("principal").(models.Account)
attachment, err := services.GetAttachmentByID(uint(id))
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
} else if attachment.AccountID != user.ID {
return fiber.NewError(fiber.StatusNotFound, "record not created by you")
}
if err := services.DeleteAttachment(attachment); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
} else {
return c.SendStatus(fiber.StatusOK)
}
}

View File

@ -0,0 +1,50 @@
package server
import (
"git.solsynth.dev/hydrogen/paperclip/pkg/services"
"github.com/gofiber/fiber/v2"
"strings"
)
func authMiddleware(c *fiber.Ctx) error {
var token string
if cookie := c.Cookies(services.CookieAccessKey); len(cookie) > 0 {
token = cookie
}
if header := c.Get(fiber.HeaderAuthorization); len(header) > 0 {
tk := strings.Replace(header, "Bearer", "", 1)
token = strings.TrimSpace(tk)
}
c.Locals("token", token)
if err := authFunc(c); err != nil {
return err
}
return c.Next()
}
func authFunc(c *fiber.Ctx, overrides ...string) error {
var token string
if len(overrides) > 0 {
token = overrides[0]
} else {
if tk, ok := c.Locals("token").(string); !ok {
return fiber.NewError(fiber.StatusUnauthorized)
} else {
token = tk
}
}
rtk := c.Cookies(services.CookieRefreshKey)
if user, atk, rtk, err := services.Authenticate(token, rtk); err == nil {
if atk != token {
services.SetJwtCookieSet(c, atk, rtk)
}
c.Locals("principal", user)
return nil
} else {
return fiber.NewError(fiber.StatusUnauthorized, err.Error())
}
}

View File

@ -0,0 +1,69 @@
package server
import (
"strings"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/idempotency"
"github.com/gofiber/fiber/v2/middleware/logger"
jsoniter "github.com/json-iterator/go"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
)
var A *fiber.App
func NewServer() {
A = fiber.New(fiber.Config{
DisableStartupMessage: true,
EnableIPValidation: true,
ServerHeader: "Hydrogen.Paperclip",
AppName: "Hydrogen.Paperclip",
ProxyHeader: fiber.HeaderXForwardedFor,
JSONEncoder: jsoniter.ConfigCompatibleWithStandardLibrary.Marshal,
JSONDecoder: jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal,
BodyLimit: 512 * 1024 * 1024 * 1024, // 512 TiB
EnablePrintRoutes: viper.GetBool("debug.print_routes"),
})
A.Use(idempotency.New())
A.Use(cors.New(cors.Config{
AllowCredentials: true,
AllowMethods: strings.Join([]string{
fiber.MethodGet,
fiber.MethodPost,
fiber.MethodHead,
fiber.MethodOptions,
fiber.MethodPut,
fiber.MethodDelete,
fiber.MethodPatch,
}, ","),
AllowOriginsFunc: func(origin string) bool {
return true
},
}))
A.Use(logger.New(logger.Config{
Format: "${status} | ${latency} | ${method} ${path}\n",
Output: log.Logger,
}))
A.Get("/.well-known", getMetadata)
A.Get("/.well-known/destinations", getDestinations)
api := A.Group("/api").Name("API")
{
api.Get("/attachments/:id/meta", getAttachmentMeta)
api.Get("/attachments/:id", openAttachment)
api.Post("/attachments", authMiddleware, createAttachment)
api.Put("/attachments/:id", authMiddleware, updateAttachmentMeta)
api.Delete("/attachments/:id", authMiddleware, deleteAttachment)
}
}
func Listen() {
if err := A.Listen(viper.GetString("bind")); err != nil {
log.Fatal().Err(err).Msg("An error occurred when starting server...")
}
}

View File

@ -0,0 +1,18 @@
package server
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
)
var validation = validator.New(validator.WithRequiredStructEnabled())
func BindAndValidate(c *fiber.Ctx, out any) error {
if err := c.BodyParser(out); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else if err := validation.Struct(out); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return nil
}

View File

@ -0,0 +1,28 @@
package server
import (
"github.com/gofiber/fiber/v2"
"github.com/spf13/viper"
)
func getMetadata(c *fiber.Ctx) error {
return c.JSON(fiber.Map{
"name": viper.GetString("name"),
"domain": viper.GetString("domain"),
"components": fiber.Map{
"passport": viper.GetString("passport.endpoint"),
},
})
}
func getDestinations(c *fiber.Ctx) error {
var data []string
for key := range viper.GetStringMap("destinations") {
data = append(data, key)
}
return c.JSON(fiber.Map{
"data": data,
"preferred": viper.GetString("preferred_destination"),
})
}

View File

@ -0,0 +1,113 @@
package services
import (
"fmt"
"git.solsynth.dev/hydrogen/paperclip/pkg/internal/database"
"mime"
"mime/multipart"
"net/http"
"path/filepath"
"git.solsynth.dev/hydrogen/paperclip/pkg/models"
"github.com/google/uuid"
"gorm.io/gorm"
)
func GetAttachmentByID(id uint) (models.Attachment, error) {
var attachment models.Attachment
if err := database.C.Where(models.Attachment{
BaseModel: models.BaseModel{ID: id},
}).First(&attachment).Error; err != nil {
return attachment, err
}
return attachment, nil
}
func GetAttachmentByUUID(id string) (models.Attachment, error) {
var attachment models.Attachment
if err := database.C.Where(models.Attachment{
Uuid: id,
}).First(&attachment).Error; err != nil {
return attachment, err
}
return attachment, nil
}
func GetAttachmentByHash(hash string) (models.Attachment, error) {
var attachment models.Attachment
if err := database.C.Where(models.Attachment{
HashCode: hash,
}).First(&attachment).Error; err != nil {
return attachment, err
}
return attachment, nil
}
func NewAttachmentMetadata(tx *gorm.DB, user models.Account, file *multipart.FileHeader, attachment models.Attachment) (models.Attachment, bool, error) {
linked := false
exists, pickupErr := GetAttachmentByHash(attachment.HashCode)
if pickupErr == nil {
linked = true
exists.Alternative = attachment.Alternative
exists.Usage = attachment.Usage
exists.Metadata = attachment.Metadata
attachment = exists
attachment.ID = 0
attachment.AccountID = user.ID
} else {
// Upload the new file
attachment.Uuid = uuid.NewString()
attachment.Size = file.Size
attachment.Name = file.Filename
attachment.AccountID = user.ID
// If user didn't provide file mimetype manually, we gotta to detect it
if len(attachment.MimeType) == 0 {
if ext := filepath.Ext(attachment.Name); len(ext) > 0 {
// Detect mimetype by file extensions
attachment.MimeType = mime.TypeByExtension(ext)
} else {
// Detect mimetype by file header
// This method as a fallback method, because this isn't pretty accurate
header, err := file.Open()
if err != nil {
return attachment, false, fmt.Errorf("failed to read file header: %v", err)
}
defer header.Close()
fileHeader := make([]byte, 512)
_, err = header.Read(fileHeader)
if err != nil {
return attachment, false, err
}
attachment.MimeType = http.DetectContentType(fileHeader)
}
}
}
if err := tx.Save(&attachment).Error; err != nil {
return attachment, linked, fmt.Errorf("failed to save attachment record: %v", err)
}
return attachment, linked, nil
}
func DeleteAttachment(item models.Attachment) error {
var dupeCount int64
if err := database.C.
Where(&models.Attachment{HashCode: item.HashCode}).
Model(&models.Attachment{}).
Count(&dupeCount).Error; err != nil {
dupeCount = -1
}
if err := database.C.Delete(&item).Error; err != nil {
return err
}
if dupeCount != -1 && dupeCount <= 1 {
return DeleteFile(item)
}
return nil
}

View File

@ -0,0 +1,76 @@
package services
import (
"context"
"errors"
"fmt"
"git.solsynth.dev/hydrogen/paperclip/pkg/internal/database"
"git.solsynth.dev/hydrogen/paperclip/pkg/internal/grpc"
"reflect"
"time"
"git.solsynth.dev/hydrogen/paperclip/pkg/models"
"git.solsynth.dev/hydrogen/passport/pkg/grpc/proto"
"gorm.io/gorm"
)
func LinkAccount(userinfo *proto.Userinfo) (models.Account, error) {
var account models.Account
if userinfo == nil {
return account, fmt.Errorf("remote userinfo was not found")
}
if err := database.C.Where(&models.Account{
ExternalID: uint(userinfo.Id),
}).First(&account).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
account = models.Account{
Name: userinfo.Name,
Nick: userinfo.Nick,
Avatar: userinfo.Avatar,
Banner: userinfo.Banner,
Description: userinfo.GetDescription(),
EmailAddress: userinfo.Email,
PowerLevel: 0,
ExternalID: uint(userinfo.Id),
}
return account, database.C.Save(&account).Error
}
return account, err
}
prev := account
account.Name = userinfo.Name
account.Nick = userinfo.Nick
account.Avatar = userinfo.Avatar
account.Banner = userinfo.Banner
account.Description = userinfo.GetDescription()
account.EmailAddress = userinfo.Email
var err error
if !reflect.DeepEqual(prev, account) {
err = database.C.Save(&account).Error
}
return account, err
}
func Authenticate(atk, rtk string) (models.Account, string, string, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
var err error
var user models.Account
reply, err := grpc.Auth.Authenticate(ctx, &proto.AuthRequest{
AccessToken: atk,
RefreshToken: &rtk,
})
if err != nil {
return user, reply.GetAccessToken(), reply.GetRefreshToken(), err
} else if !reply.IsValid {
return user, reply.GetAccessToken(), reply.GetRefreshToken(), fmt.Errorf("invalid authorization context")
}
user, err = LinkAccount(reply.Userinfo)
return user, reply.GetAccessToken(), reply.GetRefreshToken(), err
}

View File

@ -0,0 +1,24 @@
package services
import (
database2 "git.solsynth.dev/hydrogen/paperclip/pkg/internal/database"
"time"
"github.com/rs/zerolog/log"
)
func DoAutoDatabaseCleanup() {
deadline := time.Now().Add(60 * time.Minute)
log.Debug().Time("deadline", deadline).Msg("Now cleaning up entire database...")
var count int64
for _, model := range database2.AutoMaintainRange {
tx := database2.C.Unscoped().Delete(model, "deleted_at >= ?", deadline)
if tx.Error != nil {
log.Error().Err(tx.Error).Msg("An error occurred when running auth context cleanup...")
}
count += tx.RowsAffected
}
log.Debug().Int64("affected", count).Msg("Clean up entire database accomplished.")
}

View File

@ -0,0 +1,81 @@
package services
import (
"fmt"
"github.com/gofiber/fiber/v2"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/spf13/viper"
)
type PayloadClaims struct {
jwt.RegisteredClaims
Type string `json:"typ"`
}
const (
JwtAccessType = "access"
JwtRefreshType = "refresh"
)
const (
CookieAccessKey = "passport_auth_key"
CookieRefreshKey = "passport_refresh_key"
)
func EncodeJwt(id string, typ, sub string, aud []string, exp time.Time) (string, error) {
tk := jwt.NewWithClaims(jwt.SigningMethodHS512, PayloadClaims{
jwt.RegisteredClaims{
Subject: sub,
Audience: aud,
Issuer: fmt.Sprintf("https://%s", viper.GetString("domain")),
ExpiresAt: jwt.NewNumericDate(exp),
NotBefore: jwt.NewNumericDate(time.Now()),
IssuedAt: jwt.NewNumericDate(time.Now()),
ID: id,
},
typ,
})
return tk.SignedString([]byte(viper.GetString("secret")))
}
func DecodeJwt(str string) (PayloadClaims, error) {
var claims PayloadClaims
tk, err := jwt.ParseWithClaims(str, &claims, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(viper.GetString("secret")), nil
})
if err != nil {
return claims, err
}
if data, ok := tk.Claims.(*PayloadClaims); ok {
return *data, nil
} else {
return claims, fmt.Errorf("unexpected token payload: not payload claims type")
}
}
func SetJwtCookieSet(c *fiber.Ctx, access, refresh string) {
c.Cookie(&fiber.Cookie{
Name: CookieAccessKey,
Value: access,
Domain: viper.GetString("security.cookie_domain"),
SameSite: viper.GetString("security.cookie_samesite"),
Expires: time.Now().Add(60 * time.Minute),
Path: "/",
})
c.Cookie(&fiber.Cookie{
Name: CookieRefreshKey,
Value: refresh,
Domain: viper.GetString("security.cookie_domain"),
SameSite: viper.GetString("security.cookie_samesite"),
Expires: time.Now().Add(24 * 30 * time.Hour),
Path: "/",
})
}

View File

@ -0,0 +1,61 @@
package services
import (
"context"
"fmt"
"os"
"path/filepath"
"git.solsynth.dev/hydrogen/paperclip/pkg/models"
jsoniter "github.com/json-iterator/go"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/spf13/viper"
)
func DeleteFile(meta models.Attachment) error {
destMap := viper.GetStringMap("destinations")
dest, destOk := destMap[meta.Destination]
if !destOk {
return fmt.Errorf("invalid destination: destination configuration was not found")
}
var destParsed models.BaseDestination
rawDest, _ := jsoniter.Marshal(dest)
_ = jsoniter.Unmarshal(rawDest, &destParsed)
switch destParsed.Type {
case models.DestinationTypeLocal:
var destConfigured models.LocalDestination
_ = jsoniter.Unmarshal(rawDest, &destConfigured)
return DeleteFileFromLocal(destConfigured, meta)
case models.DestinationTypeS3:
var destConfigured models.S3Destination
_ = jsoniter.Unmarshal(rawDest, &destConfigured)
return DeleteFileFromS3(destConfigured, meta)
default:
return fmt.Errorf("invalid destination: unsupported protocol %s", destParsed.Type)
}
}
func DeleteFileFromLocal(config models.LocalDestination, meta models.Attachment) error {
fullpath := filepath.Join(config.Path, meta.Uuid)
return os.Remove(fullpath)
}
func DeleteFileFromS3(config models.S3Destination, meta models.Attachment) error {
client, err := minio.New(config.Endpoint, &minio.Options{
Creds: credentials.NewStaticV4(config.SecretID, config.SecretKey, ""),
Secure: config.EnableSSL,
})
if err != nil {
return fmt.Errorf("unable to configure s3 client: %v", err)
}
err = client.RemoveObject(context.Background(), config.Bucket, filepath.Join(config.Path, meta.Uuid), minio.RemoveObjectOptions{})
if err != nil {
return fmt.Errorf("unable to upload file to s3: %v", err)
}
return nil
}

View File

@ -0,0 +1,76 @@
package services
import (
"bytes"
"context"
"fmt"
"io"
"mime/multipart"
"path/filepath"
"git.solsynth.dev/hydrogen/paperclip/pkg/models"
"github.com/gofiber/fiber/v2"
jsoniter "github.com/json-iterator/go"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/spf13/viper"
)
func UploadFile(destName string, ctx *fiber.Ctx, file *multipart.FileHeader, meta models.Attachment) error {
destMap := viper.GetStringMap("destinations")
dest, destOk := destMap[destName]
if !destOk {
return fmt.Errorf("invalid destination: destination configuration was not found")
}
var destParsed models.BaseDestination
rawDest, _ := jsoniter.Marshal(dest)
_ = jsoniter.Unmarshal(rawDest, &destParsed)
switch destParsed.Type {
case models.DestinationTypeLocal:
var destConfigured models.LocalDestination
_ = jsoniter.Unmarshal(rawDest, &destConfigured)
return UploadFileToLocal(destConfigured, ctx, file, meta)
case models.DestinationTypeS3:
var destConfigured models.S3Destination
_ = jsoniter.Unmarshal(rawDest, &destConfigured)
return UploadFileToS3(destConfigured, file, meta)
default:
return fmt.Errorf("invalid destination: unsupported protocol %s", destParsed.Type)
}
}
func UploadFileToLocal(config models.LocalDestination, ctx *fiber.Ctx, file *multipart.FileHeader, meta models.Attachment) error {
return ctx.SaveFile(file, filepath.Join(config.Path, meta.Uuid))
}
func UploadFileToS3(config models.S3Destination, file *multipart.FileHeader, meta models.Attachment) error {
header, err := file.Open()
if err != nil {
return fmt.Errorf("read upload file: %v", err)
}
defer header.Close()
buffer := bytes.NewBuffer(nil)
if _, err := io.Copy(buffer, header); err != nil {
return fmt.Errorf("create io reader for upload file: %v", err)
}
client, err := minio.New(config.Endpoint, &minio.Options{
Creds: credentials.NewStaticV4(config.SecretID, config.SecretKey, ""),
Secure: config.EnableSSL,
})
if err != nil {
return fmt.Errorf("unable to configure s3 client: %v", err)
}
_, err = client.PutObject(context.Background(), config.Bucket, filepath.Join(config.Path, meta.Uuid), buffer, -1, minio.PutObjectOptions{
ContentType: meta.MimeType,
})
if err != nil {
return fmt.Errorf("unable to upload file to s3: %v", err)
}
return nil
}