-
-
{profile.name}
- {jobStatusInfo && (
-
{jobStatusInfo.label}
+
+ {/* Back button */}
+ 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]"
+ >
+ Back
+
+
+ {/* ── Hero Card with Cover Image ── */}
+
+ {/* Cover / Banner */}
+
+ {profile.coverImage ? (
+

+ ) : (
+
+ )}
+
+
+ {/* Profile Info Layout (Responsive Flex/Grid) */}
+
+
+
+ {profile.profilePic ? (
+

{ e.currentTarget.style.display = "none"; }} />
+ ) : (
+
+ )}
+
+
+
+
{profile.name}
+ {jobStatusInfo && (
+
+ {jobStatusInfo.label}
+
+ )}
+
+ {(profile.designation || profile.company) && (
+
+ {profile.designation}{profile.designation && profile.company ? " at " : ""}{profile.company}
+
)}
- {(profile.designation || profile.company) && (
-
- {profile.designation}{profile.designation && profile.company ? " at " : ""}{profile.company}
-
+
+
+ {profile.bio &&
{profile.bio}
}
+
+ {/* Contact & Info Grid */}
+
+
{profile.email}
+ {profile.contactNo &&
{profile.contactNo}}
+ {profile.college && (
+
+ {profile.college}
+ {profile.graduationYear && ({profile.graduationYear})}
+
)}
+ {profile.location &&
{profile.location}}
+ {profile.createdAt &&
Joined {formatDate(profile.createdAt)}}
-
- {profile.bio && {profile.bio}
}
-
- {/* Contact & Info Row */}
-
-
{profile.email}
- {profile.contactNo &&
{profile.contactNo}}
- {profile.college && (
-
- {profile.college}
- {profile.graduationYear && ({profile.graduationYear})}
-
+ {/* Social Links */}
+ {(profile.linkedinUrl || profile.githubUrl || profile.portfolioUrl) && (
+
)}
- {profile.location &&
{profile.location}}
- {profile.createdAt &&
Joined {formatDate(profile.createdAt)}}
+
- {/* Social Links */}
- {(profile.linkedinUrl || profile.githubUrl || profile.portfolioUrl) && (
-
+ {/* ── Stats Cards Row ── */}
+
+ {profile.bestAtsScore !== null && (
+ = 80 ? "text-emerald-600 dark:text-emerald-400" : profile.bestAtsScore >= 60 ? "text-amber-600 dark:text-amber-400" : "text-red-500 dark:text-red-400"}
+ />
)}
-
-
-
- {/* ── Stats Row ── */}
-
- {profile.bestAtsScore !== null && (
-
-
= 80 ? "text-emerald-600" : profile.bestAtsScore >= 60 ? "text-amber-600" : "text-red-500"}`}>{profile.bestAtsScore}
-
Best ATS Score
-
- )}
-
-
{profile.verifiedSkills.length}
-
Verified Skills
-
-
-
{profile.projects.length}
-
Projects
-
-
-
{profile.resumes.length}
-
Resumes
-
-
-
- {/* ── Content Grid ── */}
-
- {/* Left Column */}
-
- {/* Skills */}
- {profile.skills.length > 0 && (
+
+
+
+
+
+ {/* ── Content Grid (Responsive Layout) ── */}
+
+ {/* Left Column */}
+
+ {/* Skills */}
-
+ className="bg-white/70 dark:bg-stone-900/75 backdrop-blur-md border border-stone-200/80 dark:border-stone-800/80 p-5 rounded-2xl shadow-xs">
+
Skills
-
- {profile.skills.map((skill) => {
- const v = verifiedMap.get(skill.toLowerCase());
- return (
-
- {v && }
- {skill}
- {v && {v.score}%}
-
- );
- })}
-
+ {profile.skills.length > 0 ? (
+
+ {profile.skills.map((skill) => {
+ const v = verifiedMap.get(skill.toLowerCase());
+ return (
+
+ {v && }
+ {skill}
+ {v && ({v.score}%)}
+
+ );
+ })}
+
+ ) : (
+
+
+
No skills listed yet
+
+ )}
- )}
- {/* Resumes */}
- {profile.resumes.length > 0 && (
+ {/* Resumes */}
-
+ className="bg-white/70 dark:bg-stone-900/75 backdrop-blur-md border border-stone-200/80 dark:border-stone-800/80 p-5 rounded-2xl shadow-xs">
+
Resumes
-
+ {profile.resumes.length > 0 ? (
+
+ ) : (
+
+
+
No resumes uploaded yet
+
+ )}
- )}
- {/* Badges */}
-
-
-
-
-
-
-
-
+ {/* Badges */}
+
+
+
- {/* Right Column */}
-
- {/* Coding Activity */}
- {profile.githubUrl && (
-
-
+
+
- )}
+
+
+ {/* Right Column */}
+
+ {/* Coding Activity */}
+ {profile.githubUrl ? (
+
+
+
+ ) : (
+
+
+ Coding Activity
+
+
+
+
Link a GitHub profile to display repositories and contributions
+
+
+ )}
- {/* Projects */}
- {profile.projects.length > 0 && (
+ {/* Projects */}
-
- {/* GSSoC '26: Updated title to Featured Projects */}
+ className="bg-white/70 dark:bg-stone-900/75 backdrop-blur-md border border-stone-200/80 dark:border-stone-800/80 p-6 rounded-2xl shadow-xs">
+
Featured Projects
-
- {profile.projects.map((p) => (
-
-
-
{p.title}
- {p.builtAt && {p.builtAt}}
-
-
{p.description}
- {p.techStack.length > 0 && (
-
- {p.techStack.map((t, i) => (
- {t}
- ))}
-
- )}
- {(p.liveUrl || p.repoUrl) && (
-
- {p.liveUrl &&
Live}
- {p.repoUrl &&
Code}
-
- )}
-
- ))}
-
+ {profile.projects.length > 0 ? (
+
+ {profile.projects.map((p) => (
+
+ ))}
+
+ ) : (
+
+
+
No featured projects uploaded yet
+
+ )}
- )}
- {/* Achievements */}
- {profile.achievements.length > 0 && (
+ {/* Achievements */}
-
+ className="bg-white/70 dark:bg-stone-900/75 backdrop-blur-md border border-stone-200/80 dark:border-stone-800/80 p-6 rounded-2xl shadow-xs">
+
Achievements & Leadership
-
- {profile.achievements.map((a) => (
-
-
-
-
-
-
{a.title}
-
{a.description}
- {a.date &&
{a.date}
}
-
-
- ))}
-
+ {profile.achievements.length > 0 ? (
+
+ {profile.achievements.map((a) => (
+
+ ))}
+
+ ) : (
+
+
+
No achievements or leadership roles listed yet
+
+ )}
- )}
+
-
+
+
+
);
}
diff --git a/server/src/module/auth/auth.controller.ts b/server/src/module/auth/auth.controller.ts
index 850d001a..33cd82d8 100644
--- a/server/src/module/auth/auth.controller.ts
+++ b/server/src/module/auth/auth.controller.ts
@@ -137,23 +137,22 @@ export class AuthController {
async getPublicProfile(req: Request, res: Response) {
try {
- if (!req.user) {
- return res.status(401).json({ message: "Authentication required" });
- }
- if (req.user.role !== "RECRUITER" && req.user.role !== "ADMIN") {
- return res.status(403).json({ message: "Not authorized" });
- }
-
const id = Number(req.params["id"]);
if (!id || isNaN(id)) {
return res.status(400).json({ message: "Invalid user ID" });
}
- const profile = await this.authService.getPublicProfile(id);
+ 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 });
} catch (error) {
- if (error instanceof Error && error.message === "User not found") {
- return res.status(404).json({ message: error.message });
+ if (error instanceof Error) {
+ if (error.message === "User not found") {
+ return res.status(404).json({ message: error.message });
+ }
+ if (error.message === "Profile is private") {
+ return res.status(403).json({ message: error.message, isPrivate: true });
+ }
}
console.error(error);
return res.status(500).json({ message: "Internal Server Error" });
diff --git a/server/src/module/auth/auth.routes.ts b/server/src/module/auth/auth.routes.ts
index 7f1c5d6d..645dd790 100644
--- a/server/src/module/auth/auth.routes.ts
+++ b/server/src/module/auth/auth.routes.ts
@@ -1,7 +1,7 @@
import { Router } from "express";
import { AuthController } from "./auth.controller.js";
import { AuthService } from "./auth.service.js";
-import { authMiddleware } from "../../middleware/auth.middleware.js";
+import { authMiddleware, optionalAuthMiddleware } from "../../middleware/auth.middleware.js";
import { usageLimit } from "../../middleware/usage-limit.middleware.js";
import rateLimit from "express-rate-limit";
import { createRateLimitStore } from "../../utils/rate-limit-store.js";
@@ -60,4 +60,4 @@ authRouter.get("/me", authMiddleware, (req, res) => authController.getProfile(re
authRouter.put("/me", authMiddleware, validateBody(updateProfileSchema), (req, res) => authController.updateProfile(req, res));
authRouter.post("/import-github", authMiddleware, validateBody(importGitHubSchema), (req, res) => authController.importGitHub(req, res));
authRouter.get("/github-stats", authMiddleware, usageLimit("GITHUB_STATS"), (req, res) => authController.getGitHubStats(req, res));
-authRouter.get("/profile/:id", authMiddleware, (req, res) => authController.getPublicProfile(req, res));
+authRouter.get("/profile/:id", optionalAuthMiddleware, (req, res) => authController.getPublicProfile(req, res));
diff --git a/server/src/module/auth/auth.service.ts b/server/src/module/auth/auth.service.ts
index c15108f4..fb88489a 100644
--- a/server/src/module/auth/auth.service.ts
+++ b/server/src/module/auth/auth.service.ts
@@ -1,4 +1,4 @@
-import crypto from "crypto";
+import crypto from "crypto";
import { OAuth2Client } from "google-auth-library";
import { prisma } from "../../database/db.js";
import { hashPassword, comparePassword } from "../../utils/password.utils.js";
@@ -516,15 +516,16 @@ export class AuthService {
return user;
}
- async getPublicProfile(userId: number) {
+ async getPublicProfile(userId: number, viewer?: { id: number; role: string }) {
const cacheKey = `profile:public:${userId}`;
const cached = await cacheGet(cacheKey);
if (cached) return cached as never;
const user = await prisma.user.findUnique({
- where: { id: userId, role: "STUDENT", isProfilePublic: true },
+ where: { id: userId, role: "STUDENT" },
select: {
...this.profileSelect,
+ isProfilePublic: true,
verifiedSkills: {
select: { skillName: true, score: true, verifiedAt: true },
},
@@ -540,6 +541,17 @@ export class AuthService {
throw new Error("User not found");
}
+ if (!user.isProfilePublic) {
+ const isAuthorized = viewer && (
+ viewer.role === "ADMIN" ||
+ viewer.role === "RECRUITER" ||
+ viewer.id === userId
+ );
+ if (!isAuthorized) {
+ throw new Error("Profile is private");
+ }
+ }
+
const { atsScores, ...rest } = user;
if (rest.resumes.length > 0) {
(rest as Record
).resumes = await signUrls(rest.resumes);
@@ -554,7 +566,11 @@ export class AuthService {
...rest,
bestAtsScore: atsScores[0]?.overallScore ?? null,
};
- await cacheSet(cacheKey, result, PROFILE_TTL);
+
+ if (user.isProfilePublic) {
+ await cacheSet(cacheKey, result, PROFILE_TTL);
+ }
+
return result;
}