♻️ Use paperclip to store avatar and more

This commit is contained in:
2024-05-18 17:24:14 +08:00
parent ebef35a619
commit fd5bbd732f
17 changed files with 185 additions and 206 deletions

View File

@ -5,7 +5,7 @@ import (
"gorm.io/gorm"
)
var DatabaseAutoActionRange = []any{
var AutoMaintainRange = []any{
&models.Account{},
&models.AuthFactor{},
&models.AccountProfile{},
@ -23,7 +23,7 @@ var DatabaseAutoActionRange = []any{
}
func RunMigration(source *gorm.DB) error {
if err := source.AutoMigrate(DatabaseAutoActionRange...); err != nil {
if err := source.AutoMigrate(AutoMaintainRange...); err != nil {
return err
}

View File

@ -30,8 +30,8 @@ func (v *Server) Authenticate(_ context.Context, in *proto.AuthRequest) (*proto.
Name: user.Name,
Nick: user.Nick,
Email: user.GetPrimaryEmail().Content,
Avatar: fmt.Sprintf("https://%s/api/avatar/%s", viper.GetString("domain"), user.Avatar),
Banner: fmt.Sprintf("https://%s/api/avatar/%s", viper.GetString("domain"), user.Banner),
Avatar: fmt.Sprintf("%s/api/attachments/%s", viper.GetString("paperclip.endpoint"), user.Avatar),
Banner: fmt.Sprintf("%s/api/attachments/%s", viper.GetString("paperclip.endpoint"), user.Banner),
Description: &user.Description,
},
}, nil

21
pkg/grpc/client.go Normal file
View File

@ -0,0 +1,21 @@
package grpc
import (
pcpb "git.solsynth.dev/hydrogen/paperclip/pkg/grpc/proto"
"github.com/spf13/viper"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
var Attachments pcpb.AttachmentsClient
func ConnectPaperclip() error {
addr := viper.GetString("paperclip.grpc_endpoint")
if conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials())); err != nil {
return err
} else {
Attachments = pcpb.NewAttachmentsClient(conn)
}
return nil
}

View File

@ -1,11 +1,9 @@
package models
import (
"path/filepath"
"time"
"github.com/samber/lo"
"github.com/spf13/viper"
"gorm.io/datatypes"
)
@ -47,16 +45,6 @@ func (v Account) GetPrimaryEmail() AccountContact {
return val
}
func (v Account) GetAvatarPath() string {
basepath := viper.GetString("content")
return filepath.Join(basepath, v.Avatar)
}
func (v Account) GetBannerPath() string {
basepath := viper.GetString("content")
return filepath.Join(basepath, v.Banner)
}
type AccountContactType = int8
const (

View File

@ -38,7 +38,7 @@ func getUserinfo(c *fiber.Ctx) error {
resp["preferred_username"] = data.Nick
if len(data.Avatar) > 0 {
resp["picture"] = fmt.Sprintf("https://%s/api/avatar/%s", viper.GetString("domain"), data.Avatar)
resp["picture"] = fmt.Sprintf("%s/api/attachments/%s", viper.GetString("paperclip.endpoint"), data.Avatar)
}
return c.JSON(resp)

View File

@ -1,50 +1,34 @@
package server
import (
"os"
"path/filepath"
"context"
"fmt"
pcpb "git.solsynth.dev/hydrogen/paperclip/pkg/grpc/proto"
"git.solsynth.dev/hydrogen/passport/pkg/database"
"git.solsynth.dev/hydrogen/passport/pkg/grpc"
"git.solsynth.dev/hydrogen/passport/pkg/models"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"github.com/spf13/viper"
"github.com/samber/lo"
)
func getAvatar(c *fiber.Ctx) error {
id := c.Params("avatarId")
basepath := viper.GetString("content")
return c.SendFile(filepath.Join(basepath, id))
}
func setAvatar(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
file, err := c.FormFile("avatar")
if err != nil {
return err
var data struct {
AttachmentID string `json:"attachment"`
}
var previous string
if len(user.Avatar) > 0 {
previous = user.GetAvatarPath()
if _, err := grpc.Attachments.CheckAttachmentExists(context.Background(), &pcpb.AttachmentLookupRequest{
Uuid: &data.AttachmentID,
Usage: lo.ToPtr("p.avatar"),
}); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("avatar was not found in repository: %v", err))
}
user.Avatar = uuid.NewString()
user.Avatar = data.AttachmentID
if err := c.SaveFile(file, user.GetAvatarPath()); err != nil {
return err
} else {
database.C.Save(&user)
// Clean up
if len(previous) > 0 {
basepath := viper.GetString("content")
filepath := filepath.Join(basepath, previous)
if info, err := os.Stat(filepath); err == nil && !info.IsDir() {
os.Remove(filepath)
}
}
if err := database.C.Save(&user).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return c.SendStatus(fiber.StatusOK)
@ -52,31 +36,21 @@ func setAvatar(c *fiber.Ctx) error {
func setBanner(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
file, err := c.FormFile("banner")
if err != nil {
return err
var data struct {
AttachmentID string `json:"attachment"`
}
var previous string
if len(user.Banner) > 0 {
previous = user.GetBannerPath()
if _, err := grpc.Attachments.CheckAttachmentExists(context.Background(), &pcpb.AttachmentLookupRequest{
Uuid: &data.AttachmentID,
Usage: lo.ToPtr("p.banner"),
}); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("banner was not found in repository: %v", err))
}
user.Banner = uuid.NewString()
user.Banner = data.AttachmentID
if err := c.SaveFile(file, user.GetBannerPath()); err != nil {
return err
} else {
database.C.Save(&user)
// Clean up
if len(previous) > 0 {
basepath := viper.GetString("content")
filepath := filepath.Join(basepath, previous)
if info, err := os.Stat(filepath); err == nil && !info.IsDir() {
os.Remove(filepath)
}
}
if err := database.C.Save(&user).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return c.SendStatus(fiber.StatusOK)

View File

@ -66,8 +66,6 @@ func NewServer() {
api := A.Group("/api").Name("API")
{
api.Get("/avatar/:avatarId", getAvatar)
notify := api.Group("/notifications").Name("Notifications API")
{
notify.Get("/", authMiddleware, getNotifications)

View File

@ -8,6 +8,7 @@ import (
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/html"
"github.com/gomarkdown/markdown/parser"
"github.com/spf13/viper"
"github.com/sujit-baniya/flash"
"html/template"
"time"
@ -44,5 +45,7 @@ func selfUserinfoPage(c *fiber.Ctx) error {
"birthday_at": birthday,
"personal_page": template.HTML(markdown.Render(doc, renderer)),
"userinfo": data,
"avatar": fmt.Sprintf("%s/api/attachments/%s", viper.GetString("paperclip.endpoint"), data.Avatar),
"banner": fmt.Sprintf("%s/api/attachments/%s", viper.GetString("paperclip.endpoint"), data.Banner),
}, "views/layouts/user-center")
}

View File

@ -8,6 +8,7 @@ import (
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/html"
"github.com/gomarkdown/markdown/parser"
"github.com/spf13/viper"
"github.com/sujit-baniya/flash"
"html/template"
"time"
@ -44,5 +45,7 @@ func otherUserinfoPage(c *fiber.Ctx) error {
"birthday_at": birthday,
"personal_page": template.HTML(markdown.Render(doc, renderer)),
"userinfo": data,
"avatar": fmt.Sprintf("%s/api/attachments/%s", viper.GetString("paperclip.endpoint"), data.Avatar),
"banner": fmt.Sprintf("%s/api/attachments/%s", viper.GetString("paperclip.endpoint"), data.Banner),
}, "views/layouts/user-center")
}

View File

@ -11,7 +11,7 @@ func DoAutoDatabaseCleanup() {
log.Debug().Time("deadline", deadline).Msg("Now cleaning up entire database...")
var count int64
for _, model := range database.DatabaseAutoActionRange {
for _, model := range database.AutoMaintainRange {
tx := database.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...")

View File

@ -5,13 +5,13 @@
<div class="banner-container">
{{if gt (len .userinfo.Banner) 0}}
<img src="/api/avatar/{{.userinfo.Banner}}" alt="Banner" class="banner">
<img src="{{.banner}}" alt="Banner" class="banner">
{{end}}
</div>
<div class="left-part name-card">
{{if gt (len .userinfo.Avatar) 0}}
<img src="/api/avatar/{{.userinfo.Avatar}}" alt="Avatar" class="avatar">
<img src="{{.avatar}}" alt="Avatar" class="avatar">
{{else}}
<div class="avatar empty">
<span class="material-symbols-outlined">account_circle</span>

View File

@ -5,13 +5,13 @@
<div class="banner-container">
{{if gt (len .userinfo.Banner) 0}}
<img src="/api/avatar/{{.userinfo.Banner}}" alt="Banner" class="banner">
<img src="{{.banner}}" alt="Banner" class="banner">
{{end}}
</div>
<div class="left-part name-card">
{{if gt (len .userinfo.Avatar) 0}}
<img src="/api/avatar/{{.userinfo.Avatar}}" alt="Avatar" class="avatar">
<img src="{{.avatar}}" alt="Avatar" class="avatar">
{{else}}
<div class="avatar empty">
<span class="material-symbols-outlined">account_circle</span>

View File

@ -9,33 +9,7 @@
<div class="responsive-title-gap"></div>
<div class="personalize-actions">
<md-filled-tonal-button class="personalize-action" data-target="avatar">
Edit Avatar
<span slot="icon" class="material-symbols-outlined">account_circle</span>
</md-filled-tonal-button>
<input
hidden
id="avatar-input"
class="block-field"
name="avatar"
type="file"
accept="image/*"
placeholder="Avatar"
>
<md-filled-tonal-button class="personalize-action" data-target="banner">
Edit Banner
<span slot="icon" class="material-symbols-outlined">background_replace</span>
</md-filled-tonal-button>
<input
hidden
id="banner-input"
class="block-field"
name="banner"
type="file"
accept="image/*"
placeholder="Banner"
>
<span>We doesn't support edit avatar / banner through Hydrogen.Passport web yet. Go try our Solian App!</span>
</div>
<form class="action-form" action="/users/me/personalize" method="POST">
@ -133,35 +107,4 @@
font-size: 20px;
margin-bottom: 2px;
}
</style>
<script>
document.querySelectorAll(".personalize-action").forEach((element) => {
element.addEventListener("click", (_) => {
document.getElementById(`${element.getAttribute("data-target")}-input`).click();
})
})
document.getElementById("avatar-input").addEventListener("input", (evt) => {
if (!evt.target.files) return
const data = new FormData();
data.set("avatar", evt.target.files[0])
fetch("/api/users/me/avatar", {
method: "PUT",
body: data,
}).then(() => {
location.href = "/users/me"
})
})
document.getElementById("banner-input").addEventListener("input", (evt) => {
if (!evt.target.files) return
const data = new FormData();
data.set("banner", evt.target.files[0])
fetch("/api/users/me/banner", {
method: "PUT",
body: data,
}).then(() => {
location.href = "/users/me"
})
})
</script>
</style>