🎨 Update project structure
This commit is contained in:
190
pkg/internal/server/attachments_api.go
Normal file
190
pkg/internal/server/attachments_api.go
Normal 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)
|
||||
}
|
||||
}
|
50
pkg/internal/server/auth.go
Normal file
50
pkg/internal/server/auth.go
Normal 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())
|
||||
}
|
||||
}
|
69
pkg/internal/server/startup.go
Normal file
69
pkg/internal/server/startup.go
Normal 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...")
|
||||
}
|
||||
}
|
18
pkg/internal/server/utils.go
Normal file
18
pkg/internal/server/utils.go
Normal 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
|
||||
}
|
28
pkg/internal/server/well_known_api.go
Normal file
28
pkg/internal/server/well_known_api.go
Normal 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"),
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user