feat: profile picture fetching through guard service
This commit is contained in:
@ -46,7 +46,14 @@ func (s *APIServer) Run() error {
|
|||||||
|
|
||||||
router.Route("/api/v1", func(r chi.Router) {
|
router.Route("/api/v1", func(r chi.Router) {
|
||||||
am := imiddleware.New(s.cfg)
|
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 := user.NewUserHandler(s.repo, s.storage)
|
||||||
userHandler.RegisterRoutes(r)
|
userHandler.RegisterRoutes(r)
|
||||||
|
@ -34,6 +34,10 @@ func (fs *FileStorage) PutObject(ctx context.Context, bucketName string, objectN
|
|||||||
return fs.client.PutObject(ctx, bucketName, objectName, reader, size, opts)
|
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 {
|
func (fs *FileStorage) EndpointURL() *url.URL {
|
||||||
return fs.client.EndpointURL()
|
return fs.client.EndpointURL()
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@ -34,6 +36,7 @@ func NewUserHandler(repo *repository.Queries, minio *storage.FileStorage) *UserH
|
|||||||
func (h *UserHandler) RegisterRoutes(api chi.Router) {
|
func (h *UserHandler) RegisterRoutes(api chi.Router) {
|
||||||
api.Post("/register", h.register)
|
api.Post("/register", h.register)
|
||||||
api.Put("/avatar", h.uploadAvatar)
|
api.Put("/avatar", h.uploadAvatar)
|
||||||
|
api.Get("/avatar/{avatar}", h.getAvatar)
|
||||||
}
|
}
|
||||||
|
|
||||||
type RegisterParams struct {
|
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) {
|
func (h *UserHandler) uploadAvatar(w http.ResponseWriter, r *http.Request) {
|
||||||
userId, ok := util.GetRequestUserId(r.Context())
|
userId, ok := util.GetRequestUserId(r.Context())
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -134,11 +159,9 @@ func (h *UserHandler) uploadAvatar(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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{
|
if err := h.repo.UpdateProfilePicture(r.Context(), repository.UpdateProfilePictureParams{
|
||||||
ProfilePicture: pgtype.Text{
|
ProfilePicture: pgtype.Text{
|
||||||
String: imageURL,
|
String: uploadInfo.Key,
|
||||||
Valid: true,
|
Valid: true,
|
||||||
},
|
},
|
||||||
ID: user.ID,
|
ID: user.ID,
|
||||||
@ -148,14 +171,14 @@ func (h *UserHandler) uploadAvatar(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Response struct {
|
type Response struct {
|
||||||
URL string `json:"url"`
|
AvatarID string `json:"url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
encoder := json.NewEncoder(w)
|
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)
|
web.Error(w, "failed to write response", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import Avatar from "@/feature/Avatar";
|
||||||
import { useAuth } from "@/store/auth";
|
import { useAuth } from "@/store/auth";
|
||||||
import { User } from "lucide-react";
|
|
||||||
import { type FC } from "react";
|
import { type FC } from "react";
|
||||||
|
|
||||||
const Home: FC = () => {
|
const Home: FC = () => {
|
||||||
@ -10,15 +10,7 @@ const Home: FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center gap-2 p-7">
|
<div className="flex flex-col items-center gap-2 p-7">
|
||||||
<div className="w-24 h-24 sm:w-36 sm:h-36 overflow-hidden rounded-full flex items-center justify-center bg-gray-300">
|
<div className="w-24 h-24 sm:w-36 sm:h-36 overflow-hidden rounded-full flex items-center justify-center bg-gray-300">
|
||||||
{profile?.profile_picture ? (
|
<Avatar iconSize={64} />
|
||||||
<img
|
|
||||||
className="w-full h-full object-cover"
|
|
||||||
src={profile.profile_picture}
|
|
||||||
alt="profile pic"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<User size={64} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<h1 className="dark:text-gray-200 text-gray-800 text-2xl select-none">
|
<h1 className="dark:text-gray-200 text-gray-800 text-2xl select-none">
|
||||||
Welcome, {profile?.full_name}
|
Welcome, {profile?.full_name}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import Avatar from "@/feature/Avatar";
|
||||||
import { ChevronRight } from "lucide-react";
|
import { ChevronRight } from "lucide-react";
|
||||||
import { type FC } from "react";
|
import { type FC } from "react";
|
||||||
|
|
||||||
@ -35,11 +36,7 @@ const PersonalInfo: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="w-16 h-16 overflow-hidden rounded-full dark:bg-gray-400 bg-gray-700">
|
<div className="w-16 h-16 overflow-hidden rounded-full dark:bg-gray-400 bg-gray-700">
|
||||||
<img
|
<Avatar iconSize={12} />
|
||||||
className="w-full h-full"
|
|
||||||
src="http://192.168.178.69:9000/guard-storage/profile_eff00028-2d9e-458d-8944-677855edc147_1748099702417601900.jpg"
|
|
||||||
alt="profile pic"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { type LocalAccount } from "@/repository/account";
|
import { type LocalAccount } from "@/repository/account";
|
||||||
import { useAuth } from "@/store/auth";
|
import { useAuth } from "@/store/auth";
|
||||||
import { CirclePlus, User } from "lucide-react";
|
import { CirclePlus } from "lucide-react";
|
||||||
import { useCallback, type FC } from "react";
|
import { useCallback, type FC } from "react";
|
||||||
import { Link, useLocation } from "react-router";
|
import { Link, useLocation } from "react-router";
|
||||||
|
import Avatar from "../Avatar";
|
||||||
|
|
||||||
const AccountList: FC = () => {
|
const AccountList: FC = () => {
|
||||||
const accounts = useAuth((state) => state.accounts);
|
const accounts = useAuth((state) => state.accounts);
|
||||||
@ -27,17 +28,7 @@ const AccountList: FC = () => {
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div className="rounded-full w-10 h-10 overflow-hidden bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-gray-200 mr-3 ring ring-gray-400 dark:ring dark:ring-gray-500">
|
<div className="rounded-full w-10 h-10 overflow-hidden bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-gray-200 mr-3 ring ring-gray-400 dark:ring dark:ring-gray-500">
|
||||||
{account.profilePicture ? (
|
<Avatar iconSize={8} avatarId={account.profilePicture ?? null} />
|
||||||
<img
|
|
||||||
src={account.profilePicture}
|
|
||||||
className="w-full h-full flex-1 object-cover"
|
|
||||||
alt="profile"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="w-full h-full flex items-center justify-center">
|
|
||||||
<User />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
|
@ -1,22 +1,28 @@
|
|||||||
import { useAuth } from "@/store/auth";
|
import { useAuth } from "@/store/auth";
|
||||||
import { User } from "lucide-react";
|
import { User } from "lucide-react";
|
||||||
import type { FC } from "react";
|
import { useMemo, type FC } from "react";
|
||||||
|
|
||||||
export interface AvatarProps {
|
export interface AvatarProps {
|
||||||
iconSize?: number;
|
iconSize?: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
avatarId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Avatar: FC<AvatarProps> = ({ iconSize = 32, className }) => {
|
const Avatar: FC<AvatarProps> = ({ iconSize = 32, className, avatarId }) => {
|
||||||
const profile = useAuth((state) => state.profile);
|
const profile = useAuth((state) => state.profile);
|
||||||
|
|
||||||
|
const avatar = useMemo(
|
||||||
|
() => (avatarId !== undefined ? avatarId : profile?.profile_picture),
|
||||||
|
[avatarId, profile?.profile_picture],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`overflow-hidden bg-gray-100 rounded-full ring ring-gray-400 dark:ring dark:ring-gray-500 flex items-center justify-center ${className}`}
|
className={`overflow-hidden bg-gray-100 rounded-full ring ring-gray-400 dark:ring dark:ring-gray-500 flex items-center justify-center ${className}`}
|
||||||
>
|
>
|
||||||
{profile?.profile_picture ? (
|
{avatar ? (
|
||||||
<img
|
<img
|
||||||
src={profile?.profile_picture?.toString()}
|
src={`/api/v1/avatar/${avatar?.toString()}`}
|
||||||
className="w-full h-full flex-1 object-cover"
|
className="w-full h-full flex-1 object-cover"
|
||||||
alt="profile"
|
alt="profile"
|
||||||
/>
|
/>
|
||||||
|
Reference in New Issue
Block a user