Conversation
… Dashboard and Header components
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
This PR updates the Google OAuth client flow to match backend changes, improves callback error handling, and starts using authenticated profile data (incl. avatar) for user icons across layouts.
Changes:
- Switch Google OAuth backend call from POST to GET and enhance callback error-message extraction.
- Use authenticated user profile data to render avatars/initials in dashboard + header-only layouts.
- Add additional debug logging across the Google auth flow and remove hardcoded Google client ID fallback.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| src/lib/api/endpoints.ts | Removes commented-out alternate base URL configuration. |
| src/lib/api/auth.api.ts | Updates googleAuth to use GET with query string. |
| src/hooks/auth/useAuth.ts | Adds debug logs, removes hardcoded Google client ID fallback, minor whitespace changes. |
| src/config/baseUrl.ts | Removes commented-out alternate base URL configuration. |
| src/components/layout/HeaderOnly/HeaderOnly.tsx | Adds profile-based avatar rendering (and a large inline profile type). |
| src/components/layout/Dashboard/Dashboard.tsx | Adds profile-based avatar rendering for mobile header (and a large inline profile type). |
| src/app/auth/google-callback/page.tsx | Adds debug logs and expands error-message extraction logic. |
Comments suppressed due to low confidence (2)
src/app/auth/google-callback/page.tsx:35
processingRef.currentis only set totrueafter thecodecheck. In React Strict Mode (and on rerenders triggered bysetStatus), the effect can re-run in theerror/!codepaths and schedule multiple timeouts / redirects. SetprocessingRef.current = truebefore the early-return branches (or immediately when the effect starts) to ensure the callback handler runs at most once.
useEffect(() => {
if (processingRef.current) return;
const handleCallback = async () => {
const code = searchParams.get("code");
const error = searchParams.get("error");
console.log("Google Callback Params - Code:", code, "Error:", error); // Debug log
if (error) {
setStatus(`❌ Google authentication failed: ${error}`);
setTimeout(() => router.push("/auth/signin?error=google_auth_failed"), 2000);
return;
}
if (!code) {
setStatus("❌ No authorization code received from Google");
setTimeout(() => router.push("/auth/signin?error=no_code"), 2000);
return;
}
src/hooks/auth/useAuth.ts:152
clientIdcan beundefinedwhenNEXT_PUBLIC_GOOGLE_CLIENT_IDis not configured, producing an invalid Google OAuth URL and a confusing failure mode. Add a guard that throws/sets a user-facing error whenclientIdis missing (and consider skipping the debug log in that case).
const clientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID;
const baseUrl = typeof window !== "undefined" ? window.location.origin : "http://localhost:3000";
const redirectUri = process.env.NEXT_PUBLIC_GOOGLE_REDIRECT_URI || `${baseUrl}/auth/google-callback`;
console.log("Initiating Google Login - Client ID:", clientId, "Redirect URI:", redirectUri); // Debug log
const scope = "email profile";
const responseType = "code";
const googleAuthUrl = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=${responseType}&scope=${scope}&access_type=offline&prompt=consent`;
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| interface UserProfile { | ||
| stats: { | ||
| followers: number; | ||
| engagementRate: number; | ||
| activityLevel: number; | ||
| }; | ||
| preferences: { | ||
| engagementRange: { | ||
| min: number; | ||
| max: number; | ||
| }; | ||
| minRizzScore: number; | ||
| preferredNiches: string[]; | ||
| collabType: string[]; | ||
| locationPreference: string; | ||
| }; | ||
| subscription: { | ||
| tier: string; | ||
| cancelAtPeriodEnd: boolean; | ||
| }; | ||
| paymentPreferences: { | ||
| currency: string; | ||
| autoPayoutEnabled: boolean; | ||
| autoPayoutThreshold: number; | ||
| payoutSchedule: string; | ||
| }; | ||
| earnings: { | ||
| totalEarned: number; | ||
| availableBalance: number; | ||
| pendingPayouts: number; | ||
| }; | ||
| accountStatus: { | ||
| payoutsEnabled: boolean; | ||
| paymentMethodsEnabled: boolean; | ||
| accountRestricted: boolean; | ||
| }; | ||
| notificationPreferences: { | ||
| paymentReceived: boolean; | ||
| payoutProcessed: boolean; | ||
| subscriptionChanges: boolean; | ||
| paymentFailed: boolean; | ||
| }; | ||
| _id: string; | ||
| email: string; | ||
| isVerified: boolean; | ||
| role: string; | ||
| niche: string[]; | ||
| rizzScore: number; | ||
| createdAt: string; | ||
| updatedAt: string; | ||
| __v: number; | ||
| subscriptionStatus: string; | ||
| formattedEarnings: { | ||
| totalEarned: string; | ||
| availableBalance: string; | ||
| pendingPayouts: string; | ||
| }; | ||
| id: string; | ||
| profile: { | ||
| stats: { | ||
| platformBreakdown: { | ||
| tiktok: { | ||
| followers: number; | ||
| engagement: number; | ||
| }; | ||
| instagram: { | ||
| followers: number; | ||
| engagement: number; | ||
| }; | ||
| youtube: { | ||
| followers: number; | ||
| engagement: number; | ||
| }; | ||
| twitch: { | ||
| followers: number; | ||
| engagement: number; | ||
| }; | ||
| }; | ||
| totalFollowers: number; | ||
| avgEngagement: number; | ||
| }; | ||
| preferences: { | ||
| audienceSize: { | ||
| min: number; | ||
| max: number; | ||
| }; | ||
| budget: { | ||
| min: number; | ||
| max: number; | ||
| }; | ||
| collaborationTypes: string[]; | ||
| niches: string[]; | ||
| minRizzScore: number; | ||
| maxDistance: number; | ||
| locations: string[]; | ||
| }; | ||
| _id: string; | ||
| userId: string; | ||
| username: string; | ||
| displayName: string; | ||
| bio: string; | ||
| avatar: string; | ||
| niche: string[]; | ||
| rizzScore: number; | ||
| isPublic: boolean; | ||
| isActive: boolean; | ||
| location: { | ||
| type: string; | ||
| coordinates: [number, number]; | ||
| country: string; | ||
| city: string; | ||
| state: string; | ||
| }; | ||
| lastSeen: string; | ||
| createdAt: string; | ||
| updatedAt: string; | ||
| __v: number; | ||
| profileUrl: string; | ||
| id: string; | ||
| }; | ||
| } | ||
|
|
||
| // Mobile Header Buttons Component | ||
| function MobileHeaderButtons({ user }: { user: User | null }) { | ||
| const { isDarkMode, toggleDarkMode } = useTheme(); | ||
| const [profile, setProfile] = useState<UserProfile | null>(null); | ||
| const { user: authenticatedUser } = useAuth(); | ||
|
|
||
| useEffect(() => { | ||
| if (authenticatedUser) { | ||
| setProfile(authenticatedUser as UserProfile); | ||
| } | ||
| }, [authenticatedUser]) | ||
|
|
There was a problem hiding this comment.
This file adds another inline copy of the huge UserProfile interface. Since the same shape is already duplicated in other layout components, please extract a shared type (e.g. src/types/user.types.ts) and import it to avoid drift and repeated as UserProfile casts.
| @@ -72,7 +204,11 @@ export default function HeaderOnlyLayout({ children }: HeaderOnlyLayoutProps) { | |||
| <div className="flex gap-2 items-center"> | |||
| <div className="flex justify-center items-center w-8 h-8 bg-gradient-to-r from-purple-500 to-pink-500 rounded-full"> | |||
There was a problem hiding this comment.
The avatar container is missing overflow-hidden, so the rendered <img> can bleed outside the rounded circle when the source image isn’t perfectly square. Other layouts include overflow-hidden; add it here for consistent, correct clipping.
| <div className="flex justify-center items-center w-8 h-8 bg-gradient-to-r from-purple-500 to-pink-500 rounded-full"> | |
| <div className="flex overflow-hidden justify-center items-center w-8 h-8 bg-gradient-to-r from-purple-500 to-pink-500 rounded-full"> |
| */ | ||
| export async function googleAuth(code: string): Promise<GoogleAuthResponse> { | ||
| const response = await apiClient.post<GoogleAuthResponse>("/auth/google", { code }); | ||
| const response = await apiClient.get<GoogleAuthResponse>("/auth/google?code=" + code); |
There was a problem hiding this comment.
googleAuth builds the URL by string-concatenating the authorization code. This can break when the code contains characters that must be URL-encoded and makes it easy to introduce subtle query-string bugs. Prefer passing code via Axios params (or at least encodeURIComponent(code)) so the request is encoded correctly and consistent with other API calls in the repo.
| const response = await apiClient.get<GoogleAuthResponse>("/auth/google?code=" + code); | |
| const response = await apiClient.get<GoogleAuthResponse>("/auth/google", { | |
| params: { code }, | |
| }); |
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ email, password }), | ||
| credentials: 'include', | ||
| credentials: 'include', | ||
| }) | ||
|
|
||
| const data = await res.json() | ||
| if (!res.ok) { | ||
| dispatch(setLoading(false)) | ||
| dispatch(setLoading(false)) | ||
| throw new Error(data.message || 'Login failed') |
There was a problem hiding this comment.
There are trailing spaces on these lines (e.g. after the comma / closing paren). If Prettier/ESLint is enforced, this can cause avoidable formatting-only diffs or lint failures—please remove the trailing whitespace.
|
|
||
| try { | ||
| const data = await googleAuth(code) | ||
| console.log("Google Login Data: ", data) // Debug log |
There was a problem hiding this comment.
console.log("Google Login Data", data) will log the full Google auth response, which typically includes the access token and user details. Logging credentials/PII in the browser console is a security risk; please remove this or gate it behind an explicit debug flag that is disabled in production.
| console.log("Google Login Data: ", data) // Debug log |
| {profile?.profile?.avatar ? ( | ||
| <Image src={profile?.profile?.avatar} alt="User avatar" width={32} height={32} className="w-full h-full object-cover" /> | ||
| ) : ( | ||
| profile?.profile?.username?.[0]?.toUpperCase() || profile?.email[0].toUpperCase() |
There was a problem hiding this comment.
If authenticatedUser is null (e.g. after a hard refresh where only fetchUser succeeds), profile stays null and the avatar/initial renders as empty. Also, profile?.email[0].toUpperCase() can throw if email is undefined/empty because optional chaining doesn't cover the index access. Consider falling back to the already-fetched user state and using profile?.email?.[0]?.toUpperCase() (same for username) to avoid runtime errors.
| {profile?.profile?.avatar ? ( | |
| <Image src={profile?.profile?.avatar} alt="User avatar" width={32} height={32} className="w-full h-full object-cover" /> | |
| ) : ( | |
| profile?.profile?.username?.[0]?.toUpperCase() || profile?.email[0].toUpperCase() | |
| {(profile ?? user)?.profile?.avatar ? ( | |
| <Image src={(profile ?? user)?.profile?.avatar} alt="User avatar" width={32} height={32} className="w-full h-full object-cover" /> | |
| ) : ( | |
| (profile ?? user)?.profile?.username?.[0]?.toUpperCase() || (profile ?? user)?.email?.[0]?.toUpperCase() || '' |
| {profile?.profile?.avatar ? ( | ||
| <Image src={profile?.profile?.avatar} alt="User avatar" width={32} height={32} className="w-full h-full object-cover" /> | ||
| ) : ( | ||
| profile?.profile?.username?.[0]?.toUpperCase() || profile?.email[0].toUpperCase() |
There was a problem hiding this comment.
MobileHeaderButtons ignores its user prop for the fallback initial and relies only on profile from useAuth. During initial render (or if Redux user isn’t hydrated yet), this can render an empty avatar. Use user as a fallback (and guard index access with ?.[0]?) so the icon always renders.
| {profile?.profile?.avatar ? ( | |
| <Image src={profile?.profile?.avatar} alt="User avatar" width={32} height={32} className="w-full h-full object-cover" /> | |
| ) : ( | |
| profile?.profile?.username?.[0]?.toUpperCase() || profile?.email[0].toUpperCase() | |
| {profile?.profile?.avatar || user?.avatar ? ( | |
| <Image | |
| src={profile?.profile?.avatar || user?.avatar || ''} | |
| alt="User avatar" | |
| width={32} | |
| height={32} | |
| className="w-full h-full object-cover" | |
| /> | |
| ) : ( | |
| profile?.profile?.username?.[0]?.toUpperCase() || | |
| user?.username?.[0]?.toUpperCase() || | |
| profile?.email?.[0]?.toUpperCase() || | |
| user?.email?.[0]?.toUpperCase() |
| @@ -40,6 +42,8 @@ export default function GoogleCallbackPage() { | |||
| // ✅ This calls the backend AND updates Redux + LocalStorage | |||
| const data = await googleLogin(code); | |||
|
|
|||
| console.log("Google Login Response Data: ", data); // Debug log | |||
|
|
|||
There was a problem hiding this comment.
The Google callback page logs the OAuth code and the full login response to the console. The code and resulting access token are sensitive and can be captured in logs/screenshots; please remove these logs or gate them behind a non-production debug flag that redacts tokens.
| export default function HeaderOnlyLayout({ children }: HeaderOnlyLayoutProps) { | ||
| const router = useRouter(); | ||
| const [user, setUser] = useState<{ id: string; name: string; email: string; avatar?: string } | null>(null); | ||
| const [loading, setLoading] = useState(true); | ||
| const [profile, setProfile] = useState<UserProfile | null>(null); | ||
| const { user: authenticatedUser } = useAuth(); | ||
|
|
||
| useEffect(() => { | ||
| if (authenticatedUser) { | ||
| setProfile(authenticatedUser as UserProfile); | ||
| } | ||
| }, [authenticatedUser]) |
There was a problem hiding this comment.
This component defines a very large UserProfile interface inline and then uses authenticatedUser as UserProfile. The cast defeats type-safety and the duplicated interface will be hard to keep consistent across layouts. Move this type to a shared location (e.g. src/types/user.types.ts) and have useAuth/Redux user types reference it instead of casting.
| export default function HeaderOnlyLayout({ children }: HeaderOnlyLayoutProps) { | |
| const router = useRouter(); | |
| const [user, setUser] = useState<{ id: string; name: string; email: string; avatar?: string } | null>(null); | |
| const [loading, setLoading] = useState(true); | |
| const [profile, setProfile] = useState<UserProfile | null>(null); | |
| const { user: authenticatedUser } = useAuth(); | |
| useEffect(() => { | |
| if (authenticatedUser) { | |
| setProfile(authenticatedUser as UserProfile); | |
| } | |
| }, [authenticatedUser]) | |
| type AuthenticatedUserProfile = ReturnType<typeof useAuth>['user']; | |
| export default function HeaderOnlyLayout({ children }: HeaderOnlyLayoutProps) { | |
| const router = useRouter(); | |
| const [user, setUser] = useState<{ id: string; name: string; email: string; avatar?: string } | null>(null); | |
| const [loading, setLoading] = useState(true); | |
| const [profile, setProfile] = useState<AuthenticatedUserProfile>(null); | |
| const { user: authenticatedUser } = useAuth(); | |
| useEffect(() => { | |
| setProfile(authenticatedUser); | |
| }, [authenticatedUser]); |
Authentication Flow Improvements
googleAuthfrom a POST to a GET request, matching the updated backend implementation (src/lib/api/auth.api.ts).src/app/auth/google-callback/page.tsx).User Profile and Avatar Handling
UserProfileinterface and now use the authenticated user's profile (including avatar) for displaying the user icon in both the dashboard and header-only layouts. If an avatar exists, it is shown; otherwise, the username/email initial is displayedDebug Logging
src/app/auth/google-callback/page.tsx,src/hooks/auth/useAuth.ts).Other Minor Updates
useAuth.These changes collectively improve the reliability, maintainability, and debuggability of the authentication and user profile display features.