🎨 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,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"),
})
}