Skip to content

Commit c56658e

Browse files
authored
Fixes: #302 (#389)
Added user avatar bubble referring the already written api for screenshot and the backend code of User.
1 parent d362c9e commit c56658e

File tree

6 files changed

+140
-37
lines changed

6 files changed

+140
-37
lines changed

frontend/__tests__/Navbar/Navbar.test.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,4 +151,29 @@ describe("GlobalNavbar", () => {
151151
await user.click(brandLogo);
152152
expect(mockPush).toHaveBeenCalledWith("/");
153153
});
154+
155+
it("renders default avatar when authorized and no profileImage", () => {
156+
(useAuth as MockedFunction<typeof useAuth>).mockReturnValue({
157+
status: AuthStatus.Authorized,
158+
userId: 1,
159+
profileImage: "",
160+
});
161+
render(<GlobalNavbar />);
162+
const avatar = screen.getByAltText("Profile") as HTMLImageElement;
163+
expect(avatar).toBeInTheDocument();
164+
expect(avatar.src).toContain("/img_avatar.png");
165+
});
166+
167+
it("renders user avatar when authorized and profileImage exists", () => {
168+
(useAuth as MockedFunction<typeof useAuth>).mockReturnValue({
169+
status: AuthStatus.Authorized,
170+
userId: 1,
171+
profileImage: "avatars/1.png",
172+
});
173+
render(<GlobalNavbar />);
174+
const avatar = screen.getByAltText("Profile") as HTMLImageElement;
175+
expect(avatar).toBeInTheDocument();
176+
expect(avatar.src).toContain("/api/user/avatar?userId=1");
177+
});
178+
154179
});

frontend/components/Navbar/Navbar.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const GlobalNavbar: React.FC = () => {
1515

1616
const router = useRouter();
1717
function authButton() {
18-
if (userAuth == AuthStatus.Unauthorized || userAuth === undefined) {
18+
if (userAuth.status === AuthStatus.Unauthorized || userAuth.status === undefined) {
1919
return (
2020
<ButtonGroup>
2121
<Button
@@ -69,20 +69,33 @@ const GlobalNavbar: React.FC = () => {
6969
/>
7070
FindFirst
7171
</Navbar.Brand>
72-
{userAuth === AuthStatus.Authorized ? <Searchbar /> : null}
72+
{userAuth.status === AuthStatus.Authorized ? <Searchbar /> : null}
7373
<div className={`btn-group ${navbarView.navBtns}`}>
74-
{userAuth === AuthStatus.Authorized ? (
74+
{userAuth.status === AuthStatus.Authorized ? (
7575
<ImportModal
7676
file={undefined}
7777
show={false}
7878
data-testid="import-modal"
7979
/>
8080
) : null}
81-
{userAuth === AuthStatus.Authorized ? (
81+
{userAuth.status === AuthStatus.Authorized ? (
8282
<Export data-testid="export-component" />
8383
) : null}
8484
<LightDarkToggle />
8585
{authButton()}
86+
{userAuth.status === AuthStatus.Authorized && (
87+
<Image
88+
src={
89+
userAuth.profileImage && userAuth.profileImage.trim() !== ""
90+
? `/api/user/avatar?userId=${userAuth.userId}`
91+
: "/img_avatar.png"
92+
}
93+
alt="Profile"
94+
width={36}
95+
height={36}
96+
className="rounded-circle ms-6"
97+
/>
98+
)}
8699
</div>
87100
</Container>
88101
</Navbar>

frontend/components/UseAuth.tsx

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,37 @@
1-
import authService, { AuthObserver, AuthStatus } from "@services/auth.service";
1+
"use client";
22
import { useEffect, useState } from "react";
3+
import authService, { AuthStatus } from "@services/auth.service";
34

4-
export default function UseAuth() {
5-
const [authorized, setAuthorized] = useState<AuthStatus>();
5+
interface UserAuth {
6+
status: AuthStatus;
7+
userId?: number | null;
8+
profileImage?: string | null;
9+
}
10+
11+
export default function useAuth(): UserAuth {
12+
const [auth, setAuth] = useState<UserAuth>({
13+
status: AuthStatus.Unauthorized,
14+
userId: null,
15+
profileImage: null,
16+
});
17+
18+
useEffect(() => {
19+
async function checkAuth() {
20+
const status = authService.getAuthorized();
21+
let userId: number | null = null;
22+
let profileImage: string | null = null;
623

7-
const onAuthUpdated: AuthObserver = (authState: AuthStatus) => {
8-
setAuthorized(authState);
9-
};
24+
if (status === AuthStatus.Authorized) {
25+
const user = authService.getUser(); // your backend returns { id, profileImage }
26+
userId = user?.id || null;
27+
profileImage = user?.profileImage || null;
28+
}
1029

11-
useEffect(() => {
12-
authService.attach(onAuthUpdated);
13-
return () => authService.detach(onAuthUpdated);
14-
}, []);
30+
setAuth({ status, userId, profileImage });
31+
}
1532

16-
useEffect(() => {
17-
setAuthorized(authService.getAuthorized());
18-
}, []);
33+
checkAuth();
34+
}, []);
1935

20-
return authorized;
36+
return auth;
2137
}

frontend/pages/api/user/avatar.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { NextApiRequest, NextApiResponse } from "next";
2+
import axios from "axios";
3+
import fs from "fs";
4+
import path from "path";
5+
6+
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
7+
const { userId } = req.query;
8+
9+
if (!userId) {
10+
return res.status(400).json({ error: "Missing userId" });
11+
}
12+
13+
try {
14+
const backendUrl = `${process.env.NEXT_PUBLIC_SERVER_URL}/user/profile-picture?userId=${userId}`;
15+
16+
const response = await axios.get(backendUrl, {
17+
responseType: "arraybuffer",
18+
});
19+
20+
res.setHeader("Content-Type", response.headers["content-type"] || "image/png");
21+
res.setHeader("Cache-Control", "public, max-age=3600");
22+
res.end(Buffer.from(response.data), "binary");
23+
24+
} catch (err: any) {
25+
if (err.response?.status === 404) {
26+
// fallback to default avatar
27+
const fallbackPath = path.join(process.cwd(), "public", "img_avatar.png");
28+
const imageBuffer = fs.readFileSync(fallbackPath);
29+
res.setHeader("Content-Type", "image/png");
30+
res.setHeader("Cache-Control", "public, max-age=3600");
31+
res.end(imageBuffer);
32+
return;
33+
}
34+
res.status(err.response?.status || 500).json({ error: "Unable to fetch avatar" });
35+
}
36+
}

frontend/public/img_avatar.png

8.04 KB
Loading

frontend/services/auth.service.tsx

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import axios from "axios";
33
import { Credentials } from "../app/account/login/page";
44
import userApi from "@api/userApi";
55
export interface User {
6-
username: string;
7-
refreshToken: string;
6+
id: number;
7+
username: string;
8+
refreshToken: string;
9+
profileImage?: string | null;
810
}
911

1012
const SIGNIN_URL = process.env.NEXT_PUBLIC_SERVER_URL + "/user/signin";
@@ -46,19 +48,28 @@ class AuthService {
4648

4749
// Gets the user info implicitly using the cookie
4850
// and sets the user info.
49-
public async getUserInfoOauth2(): Promise<User | null> {
50-
let cookieuser = (await userApi.userInfo() as unknown) as User;
51-
if (cookieuser) {
52-
this.setUser(cookieuser);
51+
public async getUserInfoOauth2(): Promise<User | null> {
52+
const backendUser = (await userApi.userInfo()) as any; // the response from backend
53+
54+
if (backendUser) {
55+
const user: User = {
56+
id: backendUser.userId,
57+
username: backendUser.username,
58+
refreshToken: "",
59+
profileImage: backendUser.userPhoto || null,
60+
};
61+
62+
this.setUser(user);
63+
localStorage.setItem("user", JSON.stringify(user));
64+
this.authorizedState = AuthStatus.Authorized;
65+
this.notify(this.authorizedState);
66+
67+
return user;
68+
}
69+
return null;
5370
}
54-
localStorage.setItem("user", JSON.stringify(cookieuser));
55-
this.authorizedState = AuthStatus.Authorized;
56-
this.notify(this.authorizedState);
57-
58-
return cookieuser;
59-
}
6071

61-
public getAuthorized(): AuthStatus {
72+
public getAuthorized(): AuthStatus {
6273
return this.getUser() ? AuthStatus.Authorized : AuthStatus.Unauthorized;
6374
}
6475

@@ -75,12 +86,14 @@ class AuthService {
7586
})
7687
.then((response) => {
7788
if (response.status == 200) {
78-
let signedinUser: User = {
79-
username: credentials.username,
80-
refreshToken: response.data.refreshToken,
81-
};
82-
localStorage.setItem("user", JSON.stringify(signedinUser));
83-
this.user = signedinUser;
89+
let signedInUser: User = {
90+
id: response.data.id,
91+
username: credentials.username,
92+
refreshToken: response.data.refreshToken,
93+
profileImage: response.data.profileImage || null,
94+
};
95+
localStorage.setItem("user", JSON.stringify(signedInUser));
96+
this.user = signedInUser;
8497
this.authorizedState = AuthStatus.Authorized;
8598
this.notify(this.authorizedState);
8699
success = true;

0 commit comments

Comments
 (0)