fix: Issue #898 - allow anonymous public profile viewing and redesign…#918
fix: Issue #898 - allow anonymous public profile viewing and redesign…#918dotrwt wants to merge 1 commit into
Conversation
…iewing and redesign layout
|
Hi @dotrwt, thanks for contributing to InternHack! 🎉 I have automatically:
Our workflows will now analyze your changes to classify:
Tip Ensure your PR description references the issue it resolves (e.g. Happy coding! 🚀 |
📝 WalkthroughWalkthroughThis PR fixes public shareable student profile URLs by removing auth requirements on the backend and refactoring the client UI. The server now allows unauthenticated access to public profiles while enforcing private-profile restrictions; the client adds error handling for private profiles and refactors the profile display with reusable components. ChangesPublic Student Profile Access & Display
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
client/src/module/student/profile/PublicProfilePage.tsx (3)
289-294: ⚡ Quick winReplace generic avatar icon fallback with first-letter initial.
Fallback currently renders a generic
Usericon instead of the required initial-in-neutral box.Suggested fix
- ) : ( - <User className="w-12 h-12 text-stone-400 dark:text-stone-500" /> - )} + ) : ( + <span className="text-3xl font-bold text-stone-700 dark:text-stone-200"> + {profile.name?.trim()?.charAt(0).toUpperCase() || "U"} + </span> + )}As per coding guidelines, "Use company avatars with first-letter initial in neutral box, not generic icon."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@client/src/module/student/profile/PublicProfilePage.tsx` around lines 289 - 294, The fallback currently renders a generic <User /> icon when profile.profilePic is absent; replace that with a first-letter initial in a neutral box by deriving the initial from profile.name (e.g., const initial = profile.name?.trim()?.[0]?.toUpperCase() || '?') and rendering a styled <span> (matching the surrounding avatar container: centered, same font-size, neutral background/text color classes) instead of <User />; keep the existing img onError handler and ensure the fallback element includes an accessible label/alt via aria-label or title (e.g., `aria-label={`Profile initial ${initial}`}`) so screen readers get the initial.
263-270: ⚡ Quick winUse the shared
Buttoncomponent for the new Back action.This new interactive control is implemented as a native
motion.buttoninstead of the reusableButtonAPI.Suggested direction
- <motion.button + <motion.div initial={{ opacity: 0, x: -10 }} animate={{ opacity: 1, x: 0 }} - onClick={() => navigate(-1)} - className="flex items-center gap-2 text-sm text-stone-500 dark:text-stone-400 hover:text-stone-900 dark:hover:text-white mb-6 transition-colors font-medium border border-stone-200/80 dark:border-stone-800/80 bg-white/70 dark:bg-stone-900/50 backdrop-blur-xs px-3.5 py-1.5 rounded-xl shadow-2xs hover:scale-[1.01]" > - <ArrowLeft className="w-4 h-4" /> Back - </motion.button> + <Button + variant="secondary" + onClick={() => navigate(-1)} + className="mb-6" + > + <ArrowLeft className="w-4 h-4" /> Back + </Button> + </motion.div>As per coding guidelines, "Use the reusable
Buttoncomponent fromclient/src/components/ui/button.tsxfor all new buttons."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@client/src/module/student/profile/PublicProfilePage.tsx` around lines 263 - 270, Replace the native motion.button with the shared Button component: import Button from client/src/components/ui/button.tsx and use motion(Button) (e.g., const MotionButton = motion(Button) or inline motion(Button)) so you can keep the initial/animate props, preserve onClick={() => navigate(-1)}, the ArrowLeft child, and the existing className. Ensure you update imports (Button and motion from 'framer-motion'), remove the direct motion.button usage, and render <MotionButton ...>Back</MotionButton> to maintain animation and reuse the Button API.
186-188: ⚡ Quick winAvoid gradient background on the lock icon container.
The lock icon wrapper uses a gradient (
bg-linear-to-br ...) instead of a flat/neutral icon background.As per coding guidelines, "Do not use gradient backgrounds on icons, use flat color or bare colored icons instead."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@client/src/module/student/profile/PublicProfilePage.tsx` around lines 186 - 188, The lock icon container currently applies a gradient via the classes "bg-linear-to-br from-amber-400 to-orange-500" around the inner white circle; change this to a flat/neutral background by removing the gradient classes and using a single flat color utility (e.g., replace with a neutral/amber solid like "bg-amber-100" and an appropriate dark mode class such as "dark:bg-stone-800") while keeping the existing structure (the outer div with classes "w-16 h-16 rounded-2xl p-[1px] shadow-lg mb-6" and the inner container and <Lock /> icon) so the icon background is flat per guidelines.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@client/src/module/student/profile/PublicProfilePage.tsx`:
- Around line 252-258: The canonicalUrl passed to the SEO component is using the
private route pattern currently (`/student/profile/${profile.id}`) which doesn't
match the public route; update the canonicalUrl prop in PublicProfilePage (the
SEO usage) to point to the public route pattern used in App
(`/student/profile/public/${profile.id}`) so crawlers and previews use the
correct public profile URL.
In `@server/src/module/auth/auth.controller.ts`:
- Around line 145-147: The controller currently returns profile JSON without
cache headers; update the auth controller around the req.user /
this.authService.getPublicProfile call to set Cache-Control: no-store for
private views—detect using the returned profile's privacy flag (e.g.,
profile.isPrivate or profile.private) or by checking if viewer (req.user) is the
owner/privileged role, and call res.setHeader('Cache-Control', 'no-store') (or
res.set) before returning res.status(200).json({ profile }) so private profile
responses are not cached by browsers/proxies.
In `@server/src/module/auth/auth.service.ts`:
- Around line 520-522: The cached public profile may be returned even if the
user just toggled privacy; change the early-return logic so you only return
cache when the profile is still public: after reading cached via
cacheGet(cacheKey) verify the cached object's isProfilePublic flag (or re-check
the current isProfilePublic state from the authoritative source) and only return
the cached value when isProfilePublic is true; otherwise continue and fetch
fresh data. Also ensure cache entries are written only for public profiles and
note that updateProfile currently invalidates profile:public:${userId} after DB
and signing work—keep that invalidation but prevent returning stale private data
by guarding the cached return with the isProfilePublic check.
---
Nitpick comments:
In `@client/src/module/student/profile/PublicProfilePage.tsx`:
- Around line 289-294: The fallback currently renders a generic <User /> icon
when profile.profilePic is absent; replace that with a first-letter initial in a
neutral box by deriving the initial from profile.name (e.g., const initial =
profile.name?.trim()?.[0]?.toUpperCase() || '?') and rendering a styled <span>
(matching the surrounding avatar container: centered, same font-size, neutral
background/text color classes) instead of <User />; keep the existing img
onError handler and ensure the fallback element includes an accessible label/alt
via aria-label or title (e.g., `aria-label={`Profile initial ${initial}`}`) so
screen readers get the initial.
- Around line 263-270: Replace the native motion.button with the shared Button
component: import Button from client/src/components/ui/button.tsx and use
motion(Button) (e.g., const MotionButton = motion(Button) or inline
motion(Button)) so you can keep the initial/animate props, preserve onClick={()
=> navigate(-1)}, the ArrowLeft child, and the existing className. Ensure you
update imports (Button and motion from 'framer-motion'), remove the direct
motion.button usage, and render <MotionButton ...>Back</MotionButton> to
maintain animation and reuse the Button API.
- Around line 186-188: The lock icon container currently applies a gradient via
the classes "bg-linear-to-br from-amber-400 to-orange-500" around the inner
white circle; change this to a flat/neutral background by removing the gradient
classes and using a single flat color utility (e.g., replace with a
neutral/amber solid like "bg-amber-100" and an appropriate dark mode class such
as "dark:bg-stone-800") while keeping the existing structure (the outer div with
classes "w-16 h-16 rounded-2xl p-[1px] shadow-lg mb-6" and the inner container
and <Lock /> icon) so the icon background is flat per guidelines.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 70346d14-e3a9-4e23-ab70-d8e23c6b3445
📒 Files selected for processing (5)
client/src/App.tsxclient/src/module/student/profile/PublicProfilePage.tsxserver/src/module/auth/auth.controller.tsserver/src/module/auth/auth.routes.tsserver/src/module/auth/auth.service.ts
| <SEO | ||
| title={`${profile.name} — InternHack Profile`} | ||
| description={`${profile.name}'s skills: ${profile.skills.slice(0, 5).join(", ")}${profile.skills.length > 5 ? " and more" : ""}. ${profile.bio ? profile.bio.slice(0, 100) : "View their projects, achievements, and verified skills on InternHack."}`} | ||
| ogImage={profile.profilePic || undefined} | ||
| ogType="profile" | ||
| canonicalUrl={`https://internhack.xyz/student/profile/${profile.id}`} | ||
| /> | ||
|
|
||
| {/* Back button */} | ||
| <motion.button | ||
| initial={{ opacity: 0, x: -10 }} | ||
| animate={{ opacity: 1, x: 0 }} | ||
| onClick={() => navigate(-1)} | ||
| className="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white mb-6 transition-colors" | ||
| > | ||
| <ArrowLeft className="w-4 h-4" /> Back | ||
| </motion.button> | ||
|
|
||
| {/* ── Hero Card with Cover Image ── */} | ||
| <motion.div custom={0} variants={fadeInUp} initial="hidden" animate="visible" | ||
| className="bg-white dark:bg-gray-900 rounded-2xl border border-gray-100 dark:border-gray-800 overflow-hidden mb-6"> | ||
| {/* Cover / Banner */} | ||
| <div className="h-36 relative"> | ||
| {profile.coverImage ? ( | ||
| <img src={profile.coverImage} alt="" className="w-full h-full object-cover" /> | ||
| ) : ( | ||
| <div className="w-full h-full bg-linear-to-br from-indigo-500 via-violet-500 to-purple-500"> | ||
| <div className="absolute inset-0 opacity-15" style={{ backgroundImage: "radial-gradient(circle at 1px 1px, rgba(255,255,255,0.3) 1px, transparent 0)", backgroundSize: "20px 20px" }} /> | ||
| </div> | ||
| )} | ||
| </div> | ||
| title={`${profile.name} — InternHack Profile`} | ||
| description={`${profile.name}'s skills: ${profile.skills.slice(0, 5).join(", ")}${profile.skills.length > 5 ? " and more" : ""}. ${profile.bio ? profile.bio.slice(0, 100) : "View their projects, achievements, and verified skills on InternHack."}`} | ||
| ogImage={profile.profilePic || undefined} | ||
| ogType="profile" | ||
| canonicalUrl={`https://internhack.xyz/student/profile/${profile.id}`} | ||
| /> |
There was a problem hiding this comment.
Fix canonical URL to the actual public profile route.
Line 257 currently sets canonical to /student/profile/${profile.id}, but the public route added in App is /student/profile/public/:id. This produces a wrong canonical for crawlers and shared previews.
Suggested fix
- canonicalUrl={`https://internhack.xyz/student/profile/${profile.id}`}
+ canonicalUrl={`https://internhack.xyz/student/profile/public/${profile.id}`}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <SEO | |
| title={`${profile.name} — InternHack Profile`} | |
| description={`${profile.name}'s skills: ${profile.skills.slice(0, 5).join(", ")}${profile.skills.length > 5 ? " and more" : ""}. ${profile.bio ? profile.bio.slice(0, 100) : "View their projects, achievements, and verified skills on InternHack."}`} | |
| ogImage={profile.profilePic || undefined} | |
| ogType="profile" | |
| canonicalUrl={`https://internhack.xyz/student/profile/${profile.id}`} | |
| /> | |
| {/* Back button */} | |
| <motion.button | |
| initial={{ opacity: 0, x: -10 }} | |
| animate={{ opacity: 1, x: 0 }} | |
| onClick={() => navigate(-1)} | |
| className="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white mb-6 transition-colors" | |
| > | |
| <ArrowLeft className="w-4 h-4" /> Back | |
| </motion.button> | |
| {/* ── Hero Card with Cover Image ── */} | |
| <motion.div custom={0} variants={fadeInUp} initial="hidden" animate="visible" | |
| className="bg-white dark:bg-gray-900 rounded-2xl border border-gray-100 dark:border-gray-800 overflow-hidden mb-6"> | |
| {/* Cover / Banner */} | |
| <div className="h-36 relative"> | |
| {profile.coverImage ? ( | |
| <img src={profile.coverImage} alt="" className="w-full h-full object-cover" /> | |
| ) : ( | |
| <div className="w-full h-full bg-linear-to-br from-indigo-500 via-violet-500 to-purple-500"> | |
| <div className="absolute inset-0 opacity-15" style={{ backgroundImage: "radial-gradient(circle at 1px 1px, rgba(255,255,255,0.3) 1px, transparent 0)", backgroundSize: "20px 20px" }} /> | |
| </div> | |
| )} | |
| </div> | |
| title={`${profile.name} — InternHack Profile`} | |
| description={`${profile.name}'s skills: ${profile.skills.slice(0, 5).join(", ")}${profile.skills.length > 5 ? " and more" : ""}. ${profile.bio ? profile.bio.slice(0, 100) : "View their projects, achievements, and verified skills on InternHack."}`} | |
| ogImage={profile.profilePic || undefined} | |
| ogType="profile" | |
| canonicalUrl={`https://internhack.xyz/student/profile/${profile.id}`} | |
| /> | |
| <SEO | |
| title={`${profile.name} — InternHack Profile`} | |
| description={`${profile.name}'s skills: ${profile.skills.slice(0, 5).join(", ")}${profile.skills.length > 5 ? " and more" : ""}. ${profile.bio ? profile.bio.slice(0, 100) : "View their projects, achievements, and verified skills on InternHack."}`} | |
| ogImage={profile.profilePic || undefined} | |
| ogType="profile" | |
| canonicalUrl={`https://internhack.xyz/student/profile/public/${profile.id}`} | |
| /> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@client/src/module/student/profile/PublicProfilePage.tsx` around lines 252 -
258, The canonicalUrl passed to the SEO component is using the private route
pattern currently (`/student/profile/${profile.id}`) which doesn't match the
public route; update the canonicalUrl prop in PublicProfilePage (the SEO usage)
to point to the public route pattern used in App
(`/student/profile/public/${profile.id}`) so crawlers and previews use the
correct public profile URL.
| const viewer = req.user ? { id: req.user.id, role: req.user.role } : undefined; | ||
| const profile = await this.authService.getPublicProfile(id, viewer); | ||
| return res.status(200).json({ profile }); |
There was a problem hiding this comment.
Private 200 responses still need explicit no-store headers.
This endpoint now serves anonymous public data and authenticated private data from the same URL. When an owner, recruiter, or admin gets a 200 for a private profile, the response is still left to browser/proxy cache defaults, so the "disable caching for private profile data" objective is incomplete.
🛡️ Suggested fix
const viewer = req.user ? { id: req.user.id, role: req.user.role } : undefined;
const profile = await this.authService.getPublicProfile(id, viewer);
+ if (!profile.isProfilePublic) {
+ res.set("Cache-Control", "private, no-store, max-age=0");
+ }
return res.status(200).json({ profile });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const viewer = req.user ? { id: req.user.id, role: req.user.role } : undefined; | |
| const profile = await this.authService.getPublicProfile(id, viewer); | |
| return res.status(200).json({ profile }); | |
| const viewer = req.user ? { id: req.user.id, role: req.user.role } : undefined; | |
| const profile = await this.authService.getPublicProfile(id, viewer); | |
| if (!profile.isProfilePublic) { | |
| res.set("Cache-Control", "private, no-store, max-age=0"); | |
| } | |
| return res.status(200).json({ profile }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@server/src/module/auth/auth.controller.ts` around lines 145 - 147, The
controller currently returns profile JSON without cache headers; update the auth
controller around the req.user / this.authService.getPublicProfile call to set
Cache-Control: no-store for private views—detect using the returned profile's
privacy flag (e.g., profile.isPrivate or profile.private) or by checking if
viewer (req.user) is the owner/privileged role, and call
res.setHeader('Cache-Control', 'no-store') (or res.set) before returning
res.status(200).json({ profile }) so private profile responses are not cached by
browsers/proxies.
| const cacheKey = `profile:public:${userId}`; | ||
| const cached = await cacheGet(cacheKey); | ||
| if (cached) return cached as never; |
There was a problem hiding this comment.
Stale public cache can outlive a privacy toggle.
Line 522 returns the cached profile before the new isProfilePublic check runs. In this same file, updateProfile only deletes profile:public:${userId} after the database write and the awaited URL-signing work, so a profile that was just made private can still leak from cache during that window.
🔐 Suggested fix
const user = await prisma.user.update({
where: { id: userId },
data: updateData,
select: this.profileSelect,
});
+ // Bust the public cache before any further awaited work so privacy
+ // changes take effect immediately.
+ await cacheDel(`profile:public:${userId}`);
+
// Check profile_complete badge (fire-and-forget)
badgeService.checkAndAwardBadges(userId, "profile_complete").catch(() => {});
@@
// Bust cached profile so next GET /auth/me returns fresh data
await cacheDel(`profile:me:${userId}`);
- await cacheDel(`profile:public:${userId}`);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const cacheKey = `profile:public:${userId}`; | |
| const cached = await cacheGet(cacheKey); | |
| if (cached) return cached as never; | |
| const user = await prisma.user.update({ | |
| where: { id: userId }, | |
| data: updateData, | |
| select: this.profileSelect, | |
| }); | |
| // Bust the public cache before any further awaited work so privacy | |
| // changes take effect immediately. | |
| await cacheDel(`profile:public:${userId}`); | |
| // Check profile_complete badge (fire-and-forget) | |
| badgeService.checkAndAwardBadges(userId, "profile_complete").catch(() => {}); | |
| // ... other code ... | |
| // Bust cached profile so next GET /auth/me returns fresh data | |
| await cacheDel(`profile:me:${userId}`); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@server/src/module/auth/auth.service.ts` around lines 520 - 522, The cached
public profile may be returned even if the user just toggled privacy; change the
early-return logic so you only return cache when the profile is still public:
after reading cached via cacheGet(cacheKey) verify the cached object's
isProfilePublic flag (or re-check the current isProfilePublic state from the
authoritative source) and only return the cached value when isProfilePublic is
true; otherwise continue and fetch fresh data. Also ensure cache entries are
written only for public profiles and note that updateProfile currently
invalidates profile:public:${userId} after DB and signing work—keep that
invalidation but prevent returning stale private data by guarding the cached
return with the isProfilePublic check.
Pull Request
Description
This pull request resolves issue #898 by introducing anonymous viewing of public profiles, implementing elegant fallback/empty states for incomplete profiles, and polishing layout responsiveness and visual continuity.
Key Changes:
authMiddlewarewithoptionalAuthMiddlewareonGET /api/auth/profile/:idto allow guest access.403 Forbidden("Profile is private") if the requested profile is private. Profile owners, Admins, and Recruiters bypass this block.PublicProfilePage.tsxwith dynamic glassmorphism aesthetics, visual continuity, globalNavbar, and globalFootercomponents.StatCard,ProjectCard,AchievementCard) usingReact.memoto optimize rendering performance.sm:grid-cols-3orsm:grid-cols-4) depending on whether the ATS Score is available.Related Issue
Fixes #898
Type of Change
Testing
npm run buildandnpx tsc --noEmitboth complete with zero errors).Screenshots
Please refer to the embedded images for desktop layouts:


Checklist
.env, credentials, ornode_modulescommittedSummary by CodeRabbit
Release Notes
New Features
/student/profile/public/:idUI/UX Improvements
Other