Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,10 @@ FRONTEND_URL=http://localhost:5173
# NODE_ENV=production (for production deployments)
# NODE_ENV=development (default for local)
NODE_ENV=development

# Adzuna Job API (free tier — register at https://developer.adzuna.com)
# No credit card required. Get App ID + API Key after signing up.
ADZUNA_APP_ID=your_adzuna_app_id
ADZUNA_API_KEY=your_adzuna_api_key
# Country code for job listings: "in"=India, "us"=USA, "gb"=UK, "au"=Australia etc.
ADZUNA_COUNTRY=in
4 changes: 4 additions & 0 deletions backend/config/validateEnv.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ const requiredEnvVars = Object.freeze([
"GEMINI_API_KEY",
]);

// Optional integrations — the server boots fine without these, but the
// dependent feature is disabled until they are provided.
// ADZUNA_APP_ID / ADZUNA_API_KEY → "Jobs for You" (see controllers/jobController.js)

const validateEnv = () => {
const missingVars = [];

Expand Down
99 changes: 99 additions & 0 deletions backend/controllers/jobController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
const axios = require("axios");
const Session = require("../models/Session");
const JobCache = require("../models/JobCache");

const ADZUNA_APP_ID = process.env.ADZUNA_APP_ID;
const ADZUNA_API_KEY = process.env.ADZUNA_API_KEY;
const ADZUNA_COUNTRY = process.env.ADZUNA_COUNTRY || "in";
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;

// The Jobs feature is optional: without Adzuna credentials it stays dormant
// instead of crashing the server or spamming failed API calls.
const isAdzunaConfigured = () => Boolean(ADZUNA_APP_ID && ADZUNA_API_KEY);

async function fetchFromAdzuna(role, country = ADZUNA_COUNTRY) {
const url = `https://api.adzuna.com/v1/api/jobs/${country}/search/1`;
const { data } = await axios.get(url, {
params: {
app_id: ADZUNA_APP_ID,
app_key: ADZUNA_API_KEY,
what: role,
results_per_page: 10,
},
});
return (data.results || []).map((j) => ({
id: j.id,
title: j.title,
company: j.company?.display_name || "Unknown",
location: j.location?.display_name || "Remote",
salary_min: j.salary_min || null,
salary_max: j.salary_max || null,
description: j.description || "",
redirect_url: j.redirect_url,
created: j.created,
}));
}

exports.getJobs = async (req, res) => {
try {
if (!isAdzunaConfigured()) {
return res.json({
jobs: [],
role: "",
source: "disabled",
message:
"Job listings are not configured on this server. Set ADZUNA_APP_ID and ADZUNA_API_KEY to enable them.",
});
}

const userId = req.user._id;

const latestSession = await Session.findOne({ user: userId })
.sort({ createdAt: -1 })
.select("role");

const role = latestSession?.role || req.query.role || "software engineer";
const country = req.query.country || ADZUNA_COUNTRY;
const cacheKey = `${role.toLowerCase()}|${country}`;

const cached = await JobCache.findOne({ cacheKey });
if (cached && Date.now() - cached.fetchedAt.getTime() < CACHE_TTL_MS) {
return res.json({ jobs: cached.jobs, role, source: "cache" });
}

const jobs = await fetchFromAdzuna(role, country);

await JobCache.findOneAndUpdate(
{ cacheKey },
{ jobs, fetchedAt: new Date() },
{ upsert: true, new: true }
);

return res.json({ jobs, role, source: "api" });
} catch (err) {
console.error("[Jobs] getJobs error:", err.message);
res.status(500).json({ message: "Failed to fetch jobs", error: err.message });
}
};

exports.refreshJobCache = async () => {
if (!isAdzunaConfigured()) return;
try {
const roles = await Session.distinct("role", {
createdAt: { $gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) },
});

for (const role of roles) {
const cacheKey = `${role.toLowerCase()}|${ADZUNA_COUNTRY}`;
const jobs = await fetchFromAdzuna(role);
await JobCache.findOneAndUpdate(
{ cacheKey },
{ jobs, fetchedAt: new Date() },
{ upsert: true, new: true }
);
console.log(`[JobCron] Refreshed cache for: ${role}`);
}
} catch (err) {
console.error("[JobCron] Refresh failed:", err.message);
}
};
21 changes: 21 additions & 0 deletions backend/models/JobCache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const mongoose = require("mongoose");

const JobCacheSchema = new mongoose.Schema({
cacheKey: { type: String, required: true, unique: true },
jobs: [
{
id: String,
title: String,
company: String,
location: String,
salary_min: Number,
salary_max: Number,
description: String,
redirect_url: String,
created: String,
},
],
fetchedAt: { type: Date, default: Date.now },
});

module.exports = mongoose.model("JobCache", JobCacheSchema);
8 changes: 8 additions & 0 deletions backend/routes/jobRoutes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const express = require("express");
const router = express.Router();
const { protect } = require("../middlewares/authMiddleware");
const { getJobs } = require("../controllers/jobController");

router.get("/", protect, getJobs);

module.exports = router;
10 changes: 10 additions & 0 deletions backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const questionRoutes = require("./routes/questionRoutes");
const aiRoutes = require("./routes/aiRoutes");
const resumeRoutes = require("./routes/resumeRoutes");
const aptitudeQuestionsRoutes = require("./routes/AptitudeQuestions.js");
const jobRoutes = require("./routes/jobRoutes");
const { generalLimiter, aiLimiter } = require("./middlewares/rateLimiter");
const { generalHeaders, sensitiveRouteHeaders } = require("./middlewares/securityHeaders");
// Remove ES Module import for cors. Use CommonJS require below.
Expand Down Expand Up @@ -129,6 +130,7 @@ app.use(
);

app.use("/api/books", generalLimiter, booksRoutes);
app.use("/api/jobs", generalLimiter, jobRoutes);

//Serve uploads folder
app.use("/uploads", express.static(path.join(__dirname, "uploads"), {}));
Expand All @@ -140,6 +142,14 @@ app.get("/api/test", (req, res) => {

// Remove duplicate CORS middleware (already set above)

// Daily job cache refresh — warm on boot, then every 24 hours.
// Only runs when Adzuna is configured; otherwise refreshJobCache() no-ops.
if (process.env.ADZUNA_APP_ID && process.env.ADZUNA_API_KEY) {
const { refreshJobCache } = require("./controllers/jobController");
refreshJobCache();
setInterval(refreshJobCache, 24 * 60 * 60 * 1000);
}

// Start Server
const PORT = process.env.PORT || 5000;
const server = app.listen(PORT, "0.0.0.0", () => {
Expand Down
11 changes: 11 additions & 0 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import RepositoryHive from "./pages/OpenSource/RepositoryHive";
import OSSBlog from "./pages/OpenSource/OSSBlog";
import OpenSourceEvents from "./pages/OpenSource/OpenSourceEvents";
import NotesBooks from "./pages/NotesBooks/NotesBooks";
import JobsForYou from "./pages/Jobs/JobsForYou";
import HelpSupport from "./pages/Support/HelpSupport";
import Settings from "./pages/Settings/Settings";
import NotFound from "./pages/NotFound";
Expand Down Expand Up @@ -314,6 +315,16 @@ const App = () => {
</ProtectedRoute>
}
/>
<Route
path="/jobs"
element={
<ProtectedRoute>
<PageTransition>
<JobsForYou />
</PageTransition>
</ProtectedRoute>
}
/>
<Route
path="/terms-and-conditions"
element={
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/components/Layouts/Sidebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Bot,
BrainCircuit,
Briefcase,
BriefcaseBusiness,
Code2,
Target,
Settings,
Expand Down Expand Up @@ -99,6 +100,19 @@ const Sidebar = () => {
},
],
},
{
id: "jobs",
title: "Jobs",
isHeader: true,
items: [
{
id: "jobs-for-you",
title: "Jobs for You",
path: "/jobs",
icon: BriefcaseBusiness,
},
],
},
{
id: "project",
title: "Project",
Expand Down
Loading
Loading