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
16 changes: 6 additions & 10 deletions backend/controllers/authController.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,6 @@ const registerUser = async (req, res) => {
return res.status(400).json({ success: false, message: "A user with this email already exists." });
}

// hash password
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(password, salt);

// Split name into first and last names for defaults
const nameParts = name.trim().split(/\s+/);
const firstName = nameParts[0] || "";
Expand All @@ -62,7 +58,7 @@ const registerUser = async (req, res) => {
const user = await User.create({
name,
email,
password: hashedPassword,
password: password,
profileImageUrl,
firstName,
lastName,
Expand Down Expand Up @@ -116,7 +112,7 @@ const loginUser = async (req, res) => {
}

// Verify password against stored hash
const isMatch = await bcrypt.compare(password, user.password);
const isMatch = await user.isValidPassword(password);
if (!isMatch) {
return res.status(401).json({ success: false, message: "Invalid email or password provided." });
}
Expand Down Expand Up @@ -146,7 +142,8 @@ const loginUser = async (req, res) => {
refreshToken,
});
} catch (error) {
res.status(500).json({ success: false, message: "Internal server error occurred", error: error.message });
console.log(error)
res.status(500).json({ success: false, message: "Internal server error occurred", error });
}
};

Expand Down Expand Up @@ -457,14 +454,13 @@ const changePassword = async (req, res) => {
}

// Compare original password
const isMatch = await bcrypt.compare(originalPassword, user.password);
const isMatch = await user.isValidPassword(originalPassword);
if (!isMatch) {
return res.status(400).json({ success: false, message: "Incorrect original password" });
}

// Hash new password
const salt = await bcrypt.genSalt(10);
user.password = await bcrypt.hash(newPassword, salt);
user.password = newPassword;
await user.save();

res.json({ success: true, message: "Password updated successfully" });
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);
18 changes: 18 additions & 0 deletions backend/models/User.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const mongoose = require("mongoose");
const bcrypt = require("bcryptjs");

const UserSchema = new mongoose.Schema(
{
Expand Down Expand Up @@ -59,4 +60,21 @@ const UserSchema = new mongoose.Schema(
{ timestamps: true }
);

// Hash password automatically on save, and when there is no update in password then the password remains same and saves precious computation of hasing password

UserSchema.pre('save', async function() {
if (!this.isModified('password')) {
return;
}

const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
});

UserSchema.methods.isValidPassword = async function(candidatePassword) {
// Compare candidate password with the stored hash
return await bcrypt.compare(candidatePassword, this.password);
};


module.exports = mongoose.model("User", UserSchema);
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
14 changes: 13 additions & 1 deletion frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Toaster } from "react-hot-toast";
import { AnimatePresence } from "framer-motion";
import PageTransition from "./components/animations/PageTransition";
import ErrorBoundary from "./components/ErrorBoundary";

import InteractiveCursor from './components/InteractiveCursor';
import Login from "./pages/Auth/Login";
import SignUp from "./pages/Auth/SignUp";
import VerifyEmail from "./pages/Auth/verifyEmail";
Expand All @@ -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 All @@ -56,6 +57,7 @@ const App = () => {
<UserProvider>
<ErrorBoundary>
<div className="min-h-screen bg-[var(--color-background)] text-[var(--color-text-dark)] transition-colors duration-300">
<InteractiveCursor />
<Router>
<AnimatePresence mode="wait">
<Routes>
Expand Down Expand Up @@ -314,6 +316,16 @@ const App = () => {
</ProtectedRoute>
}
/>
<Route
path="/jobs"
element={
<ProtectedRoute>
<PageTransition>
<JobsForYou />
</PageTransition>
</ProtectedRoute>
}
/>
<Route
path="/terms-and-conditions"
element={
Expand Down
Loading