From d9ca1ce2b4b3e27316a2023725ea73856bdadbd8 Mon Sep 17 00:00:00 2001 From: LandaMm Date: Thu, 29 May 2025 19:51:15 +0200 Subject: [PATCH] feat: profile picture fetching through guard service --- cmd/hspguard/api/api.go | 9 ++++- internal/storage/mod.go | 4 +++ internal/user/routes.go | 33 ++++++++++++++++--- web/src/components/Home/Tabs/Home.tsx | 12 ++----- web/src/components/Home/Tabs/PersonalInfo.tsx | 7 ++-- web/src/feature/AccountList/index.tsx | 15 ++------- web/src/feature/Avatar/index.tsx | 14 +++++--- 7 files changed, 57 insertions(+), 37 deletions(-) diff --git a/cmd/hspguard/api/api.go b/cmd/hspguard/api/api.go index a86aa02..ad47482 100644 --- a/cmd/hspguard/api/api.go +++ b/cmd/hspguard/api/api.go @@ -46,7 +46,14 @@ func (s *APIServer) Run() error { router.Route("/api/v1", func(r chi.Router) { am := imiddleware.New(s.cfg) - r.Use(imiddleware.WithSkipper(am.Runner, "/api/v1/auth/login", "/api/v1/register", "/api/v1/auth/refresh", "/api/v1/oauth/token")) + r.Use(imiddleware.WithSkipper( + am.Runner, + "/api/v1/auth/login", + "/api/v1/register", + "/api/v1/auth/refresh", + "/api/v1/oauth/token", + "/api/v1/avatar", + )) userHandler := user.NewUserHandler(s.repo, s.storage) userHandler.RegisterRoutes(r) diff --git a/internal/storage/mod.go b/internal/storage/mod.go index ac50219..377e794 100644 --- a/internal/storage/mod.go +++ b/internal/storage/mod.go @@ -34,6 +34,10 @@ func (fs *FileStorage) PutObject(ctx context.Context, bucketName string, objectN return fs.client.PutObject(ctx, bucketName, objectName, reader, size, opts) } +func (fs *FileStorage) GetObject(ctx context.Context, bucketName string, objectName string, opts minio.GetObjectOptions) (*minio.Object, error) { + return fs.client.GetObject(ctx, bucketName, objectName, opts) +} + func (fs *FileStorage) EndpointURL() *url.URL { return fs.client.EndpointURL() } diff --git a/internal/user/routes.go b/internal/user/routes.go index b46c7a4..72b3044 100644 --- a/internal/user/routes.go +++ b/internal/user/routes.go @@ -4,6 +4,8 @@ import ( "context" "encoding/json" "fmt" + "io" + "log" "net/http" "path/filepath" "strings" @@ -34,6 +36,7 @@ func NewUserHandler(repo *repository.Queries, minio *storage.FileStorage) *UserH func (h *UserHandler) RegisterRoutes(api chi.Router) { api.Post("/register", h.register) api.Put("/avatar", h.uploadAvatar) + api.Get("/avatar/{avatar}", h.getAvatar) } type RegisterParams struct { @@ -93,6 +96,28 @@ func (h *UserHandler) register(w http.ResponseWriter, r *http.Request) { } } +func (h *UserHandler) getAvatar(w http.ResponseWriter, r *http.Request) { + avatarObject := chi.URLParam(r, "avatar") + + object, err := h.minio.GetObject(r.Context(), "guard-storage", avatarObject, minio.GetObjectOptions{}) + if err != nil { + web.Error(w, "avatar not found", http.StatusNotFound) + return + } + defer object.Close() + + stat, err := object.Stat() + if err != nil { + log.Printf("ERR: failed to get object stats: %v\n", err) + web.Error(w, "failed to get avatar", http.StatusNotFound) + return + } + + w.Header().Set("Content-Type", stat.ContentType) + w.WriteHeader(http.StatusOK) + io.Copy(w, object) +} + func (h *UserHandler) uploadAvatar(w http.ResponseWriter, r *http.Request) { userId, ok := util.GetRequestUserId(r.Context()) if !ok { @@ -134,11 +159,9 @@ func (h *UserHandler) uploadAvatar(w http.ResponseWriter, r *http.Request) { return } - imageURL := fmt.Sprintf("http://%s/%s/%s", h.minio.EndpointURL().Host, "guard-storage", uploadInfo.Key) - if err := h.repo.UpdateProfilePicture(r.Context(), repository.UpdateProfilePictureParams{ ProfilePicture: pgtype.Text{ - String: imageURL, + String: uploadInfo.Key, Valid: true, }, ID: user.ID, @@ -148,14 +171,14 @@ func (h *UserHandler) uploadAvatar(w http.ResponseWriter, r *http.Request) { } type Response struct { - URL string `json:"url"` + AvatarID string `json:"url"` } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) encoder := json.NewEncoder(w) - if err := encoder.Encode(Response{URL: imageURL}); err != nil { + if err := encoder.Encode(Response{AvatarID: uploadInfo.Key}); err != nil { web.Error(w, "failed to write response", http.StatusInternalServerError) } } diff --git a/web/src/components/Home/Tabs/Home.tsx b/web/src/components/Home/Tabs/Home.tsx index 2ca56fc..437abad 100644 --- a/web/src/components/Home/Tabs/Home.tsx +++ b/web/src/components/Home/Tabs/Home.tsx @@ -1,6 +1,6 @@ import { Button } from "@/components/ui/button"; +import Avatar from "@/feature/Avatar"; import { useAuth } from "@/store/auth"; -import { User } from "lucide-react"; import { type FC } from "react"; const Home: FC = () => { @@ -10,15 +10,7 @@ const Home: FC = () => { return (
- {profile?.profile_picture ? ( - profile pic - ) : ( - - )} +

Welcome, {profile?.full_name} diff --git a/web/src/components/Home/Tabs/PersonalInfo.tsx b/web/src/components/Home/Tabs/PersonalInfo.tsx index 74002c8..223f998 100644 --- a/web/src/components/Home/Tabs/PersonalInfo.tsx +++ b/web/src/components/Home/Tabs/PersonalInfo.tsx @@ -1,3 +1,4 @@ +import Avatar from "@/feature/Avatar"; import { ChevronRight } from "lucide-react"; import { type FC } from "react"; @@ -35,11 +36,7 @@ const PersonalInfo: FC = () => {

- profile pic +
diff --git a/web/src/feature/AccountList/index.tsx b/web/src/feature/AccountList/index.tsx index 9703358..d245ffb 100644 --- a/web/src/feature/AccountList/index.tsx +++ b/web/src/feature/AccountList/index.tsx @@ -1,8 +1,9 @@ import { type LocalAccount } from "@/repository/account"; import { useAuth } from "@/store/auth"; -import { CirclePlus, User } from "lucide-react"; +import { CirclePlus } from "lucide-react"; import { useCallback, type FC } from "react"; import { Link, useLocation } from "react-router"; +import Avatar from "../Avatar"; const AccountList: FC = () => { const accounts = useAuth((state) => state.accounts); @@ -27,17 +28,7 @@ const AccountList: FC = () => { >
- {account.profilePicture ? ( - profile - ) : ( -
- -
- )} +
diff --git a/web/src/feature/Avatar/index.tsx b/web/src/feature/Avatar/index.tsx index f225e51..04a6e30 100644 --- a/web/src/feature/Avatar/index.tsx +++ b/web/src/feature/Avatar/index.tsx @@ -1,22 +1,28 @@ import { useAuth } from "@/store/auth"; import { User } from "lucide-react"; -import type { FC } from "react"; +import { useMemo, type FC } from "react"; export interface AvatarProps { iconSize?: number; className?: string; + avatarId?: string; } -const Avatar: FC = ({ iconSize = 32, className }) => { +const Avatar: FC = ({ iconSize = 32, className, avatarId }) => { const profile = useAuth((state) => state.profile); + const avatar = useMemo( + () => (avatarId !== undefined ? avatarId : profile?.profile_picture), + [avatarId, profile?.profile_picture], + ); + return (
- {profile?.profile_picture ? ( + {avatar ? ( profile