feat(profile): add round avatar with upload and cropping
- backend: user_avatar_objects binding (PG+SQLite), GC preserves avatar blobs, avatar service (EXIF-stripping WebP 128/512), public avatar read route - web: avatar upload/crop on profile, round avatar in header/members/comments/author - android: avatar upload/crop, round avatar on profile and albums header - openspec: add-user-avatar proposal/design/specs/tasks
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
||||
assets "galo"
|
||||
"galo/internal/albums"
|
||||
"galo/internal/auth"
|
||||
"galo/internal/avatar"
|
||||
"galo/internal/config"
|
||||
"galo/internal/httpx"
|
||||
"galo/internal/members"
|
||||
@@ -38,6 +39,8 @@ type Server struct {
|
||||
|
||||
auth *auth.Service
|
||||
|
||||
avatar *avatar.Service
|
||||
|
||||
membersSvc *members.Service
|
||||
|
||||
photos *photos.Service
|
||||
@@ -71,6 +74,8 @@ func New(cfg *config.Config, repos store.Repos, ping func(ctx context.Context) e
|
||||
photosSvc.SetTempDir(cfg.Video.TempDir)
|
||||
photosWorker.SetVideo(videoTools, cfg.Video.TranscodeConcurrency)
|
||||
|
||||
avatarSvc := avatar.NewService(repos.Photos, repos.Auth, st)
|
||||
|
||||
socialSvc := social.NewService(repos.Social, albumsSvc, repos.Photos, hub, notifSvc)
|
||||
|
||||
shareSvc := sharelinks.NewService(repos.ShareLinks, albumsSvc)
|
||||
@@ -83,6 +88,7 @@ func New(cfg *config.Config, repos store.Repos, ping func(ctx context.Context) e
|
||||
hub: hub,
|
||||
albums: albumsSvc,
|
||||
auth: authSvc,
|
||||
avatar: avatarSvc,
|
||||
membersSvc: membersSvc,
|
||||
photos: photosSvc,
|
||||
worker: photosWorker,
|
||||
@@ -106,6 +112,7 @@ func (s *Server) Router() *gin.Engine {
|
||||
|
||||
authHandler := auth.NewHandler(s.auth)
|
||||
albumsHandler := albums.NewHandler(s.albums)
|
||||
avatarHandler := avatar.NewHandler(s.avatar)
|
||||
membersHandler := members.NewHandler(s.membersSvc)
|
||||
photosHandler := photos.NewHandler(s.photos)
|
||||
socialHandler := social.NewHandler(s.social)
|
||||
@@ -116,6 +123,11 @@ func (s *Server) Router() *gin.Engine {
|
||||
api.GET("/health", s.health)
|
||||
api.GET("/objects/*key", s.serveObject)
|
||||
api.HEAD("/objects/*key", s.serveObject)
|
||||
// Avatars are read by <img> tags and image loaders, which cannot send an
|
||||
// Authorization header, so the read route is public. Upload and removal
|
||||
// stay behind authentication. The URL carries an opaque per-generation
|
||||
// version segment, so the path is not guessable from a user id alone.
|
||||
api.GET("/avatars/:userID/:version", avatarHandler.Serve)
|
||||
|
||||
api.POST("/auth/register", authHandler.Register)
|
||||
api.POST("/auth/login", authHandler.Login)
|
||||
@@ -134,6 +146,8 @@ func (s *Server) Router() *gin.Engine {
|
||||
|
||||
authed.GET("/auth/me", authHandler.Me)
|
||||
authed.PATCH("/auth/me", authHandler.UpdateMe)
|
||||
authed.POST("/auth/me/avatar", avatarHandler.Upload)
|
||||
authed.DELETE("/auth/me/avatar", avatarHandler.Remove)
|
||||
authed.POST("/auth/me/password", authHandler.ChangePassword)
|
||||
authed.POST("/auth/me/identifiers", authHandler.AddIdentifier)
|
||||
authed.DELETE("/auth/me", authHandler.DeleteMe)
|
||||
|
||||
Reference in New Issue
Block a user