feat: Add GET /media/sign and GET /users/by-handle/:handle endpoints
Both were wired in Flutter but missing from Go routes: - GET /media/sign?path=X — resolves R2 relative keys to full URLs - GET /users/by-handle/:handle — profile lookup for capsule invite flow Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
afdc0f3f1c
commit
6e2de2cd9d
|
|
@ -398,6 +398,7 @@ func main() {
|
|||
|
||||
// Media routes
|
||||
authorized.POST("/upload", mediaHandler.Upload)
|
||||
authorized.GET("/media/sign", mediaHandler.GetSignedMediaURL)
|
||||
|
||||
// Search & Discover routes
|
||||
discoverHandler := handlers.NewDiscoverHandler(userRepo, postRepo, tagRepo, categoryRepo, assetService)
|
||||
|
|
@ -409,6 +410,9 @@ func main() {
|
|||
authorized.POST("/hashtags/:name/follow", discoverHandler.FollowHashtag)
|
||||
authorized.DELETE("/hashtags/:name/follow", discoverHandler.UnfollowHashtag)
|
||||
|
||||
// User by-handle lookup (used by capsule invite to resolve public keys)
|
||||
authorized.GET("/users/by-handle/:handle", userHandler.GetUserByHandle)
|
||||
|
||||
// Follow System (unique routes only — followers/following covered by users group above)
|
||||
followHandler := handlers.NewFollowHandler(dbPool)
|
||||
authorized.POST("/users/:userId/unfollow", followHandler.UnfollowUser)
|
||||
|
|
|
|||
|
|
@ -208,6 +208,32 @@ func (h *MediaHandler) putObjectS3(c *gin.Context, body io.ReadSeeker, contentLe
|
|||
return key, nil
|
||||
}
|
||||
|
||||
// GetSignedMediaURL resolves a relative R2 path to a fully-qualified URL.
|
||||
// Flutter calls GET /media/sign?path=<key> for any path that was stored as a relative key.
|
||||
func (h *MediaHandler) GetSignedMediaURL(c *gin.Context) {
|
||||
path := c.Query("path")
|
||||
if path == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "path query parameter is required"})
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(path, "http") {
|
||||
c.JSON(http.StatusOK, gin.H{"url": path})
|
||||
return
|
||||
}
|
||||
domain := h.publicDomain
|
||||
if strings.Contains(path, "videos/") {
|
||||
domain = h.videoDomain
|
||||
}
|
||||
if domain == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"url": path})
|
||||
return
|
||||
}
|
||||
if !strings.HasPrefix(domain, "http") {
|
||||
domain = "https://" + domain
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"url": fmt.Sprintf("%s/%s", domain, path)})
|
||||
}
|
||||
|
||||
func (h *MediaHandler) putObjectR2API(c *gin.Context, fileBytes []byte, contentType string, bucket string, key string, publicDomain string) (string, error) {
|
||||
if h.accountID == "" || h.apiToken == "" {
|
||||
return "", fmt.Errorf("R2 API credentials missing")
|
||||
|
|
|
|||
|
|
@ -663,6 +663,22 @@ func (h *UserHandler) BulkBlockUsers(c *gin.Context) {
|
|||
})
|
||||
}
|
||||
|
||||
// GetUserByHandle resolves a public profile by @handle.
|
||||
// Used by the capsule invite flow so the client can look up a user's public key before encrypting.
|
||||
func (h *UserHandler) GetUserByHandle(c *gin.Context) {
|
||||
handle := strings.TrimPrefix(strings.TrimSpace(c.Param("handle")), "@")
|
||||
if handle == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "handle is required"})
|
||||
return
|
||||
}
|
||||
profile, err := h.repo.GetProfileByHandle(c.Request.Context(), handle)
|
||||
if err != nil || profile == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, profile)
|
||||
}
|
||||
|
||||
// ExportData streams user data as JSON for portability/GDPR compliance
|
||||
func (h *UserHandler) ExportData(c *gin.Context) {
|
||||
userID, _ := c.Get("user_id")
|
||||
|
|
|
|||
Loading…
Reference in a new issue