Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
9d4f73b
fix: relax role sanitization from exact allow-list to length/sanitiza…
ionfwsrijan Aug 8, 2026
d7f5850
fix: validate country field input
sashatakpere Aug 9, 2026
e2d6a03
fix: accept object completedTopics in saveProgress so progress persists
vedant7007 Aug 9, 2026
200d7c2
refine: validate completedTopics values are booleans (match record co…
vedant7007 Aug 11, 2026
3ac006d
feat : added AI interview question keyword extractor
tmdeveloper007 Aug 12, 2026
3a5b7b9
fix: CVE-2026-55685 security vulnerability
anupamme Aug 12, 2026
00c06a8
fix: validate country names in profile
sashatakpere Aug 12, 2026
9deda32
fix: support unicode country names
sashatakpere Aug 12, 2026
3c2ea83
fix: normalize unicode country input
sashatakpere Aug 12, 2026
6cf82bb
fix: enable copy rewrite functionality
kumud-05 Aug 12, 2026
373ed1d
test : added unit tests for SM-2 spaced repetition algorithm
tmdeveloper007 Aug 13, 2026
61cb6eb
feat: setup SRS Flashcards UI MVP
anshika1179 Aug 13, 2026
d6326c7
Merge pull request #1714 from sashatakpere/fix-country-field-validation
KaranUnique Aug 13, 2026
28ab461
Merge pull request #1921 from tmdeveloper007/feat/interview-kw-extrac…
KaranUnique Aug 13, 2026
c90c5a6
Merge pull request #1933 from tmdeveloper007/#1930
KaranUnique Aug 13, 2026
bec04fc
fix: correct react-router override to 7.18.0 (CVE-2026-55685)
anupamme Aug 13, 2026
37ec5a8
feat: unify streak tracking across sessions, flashcards, and sheet pr…
Torqued-codes Aug 13, 2026
908f14b
Apply code changes: @anupamme Address coderabbit suggestions...
anupamme Aug 13, 2026
920ff50
fix: handle clipboard copy failures
kumud-05 Aug 13, 2026
b7f09b9
feat: add AI answer improvement versioning
jainiksha Aug 13, 2026
7a9e02f
feat: add AI interview practice gap detector
jainiksha Aug 13, 2026
72c99bb
feat: add AI question restatement coach
jainiksha Aug 13, 2026
ccc2087
feat: add AI practice consistency forecast
jainiksha Aug 13, 2026
f5f3968
feat: add AI skill difficulty matrix
jainiksha Aug 13, 2026
c6f2527
feat: add AI adaptive review mode
jainiksha Aug 13, 2026
033b0dd
feat: add AI answer opening to conclusion flow analyzer
jainiksha Aug 13, 2026
517e500
feat: add AI topic mastery decay monitor
jainiksha Aug 13, 2026
96113d7
Merge pull request #2001 from jainiksha/feature/ai-topic-mastery-deca…
KaranUnique Aug 13, 2026
6476ade
Merge pull request #1983 from jainiksha/feature/ai-practice-gap-detector
KaranUnique Aug 13, 2026
9256967
Merge pull request #1995 from jainiksha/feature/ai-answer-flow-analyzer
KaranUnique Aug 13, 2026
3d53b15
feat: add AI solution constraint stress test
jainiksha Aug 13, 2026
dc88b0d
Merge pull request #2050 from jainiksha/feature/ai-solution-constrain…
KaranUnique Aug 13, 2026
af986c2
Create pr_raise.yml
itsdakshjain Aug 13, 2026
56e2438
Create closes_comment.yml
itsdakshjain Aug 13, 2026
f001399
Merge pull request #1923 from anupamme/fix-repo-preppilot-cve-2026-55…
KaranUnique Aug 14, 2026
1c5d196
Merge pull request #2098 from itsdakshjain/patch-2
KaranUnique Aug 14, 2026
c2d1d99
Merge pull request #2100 from itsdakshjain/issue-close
KaranUnique Aug 14, 2026
0ccc769
resolved dependency conflicts on router dependencies
KaranUnique Aug 14, 2026
2135705
Merge pull request #2015 from Torqued-codes/main
KaranUnique Aug 14, 2026
9fb2c2a
Merge pull request #1940 from anshika1179/feat/srs-flashcards-ui
KaranUnique Aug 14, 2026
43c71ca
fix(backend): scope interview experience idempotency dedup to author
openhands-agent Aug 14, 2026
31ba7df
fix: add max length guard to notes summary filename
bindusreeseetha Aug 14, 2026
f9122cb
test: mock streak tracker in progress controller tests
bindusreeseetha Aug 14, 2026
fd2df28
Merge pull request #1925 from kumud-05/fix/copy-rewrite-button
KaranUnique Aug 14, 2026
07c5fef
Add streak milestone achievements/badges (1/3/7/14/30-day streaks)
Torqued-codes Aug 14, 2026
2375739
fixed the err
Torqued-codes Aug 14, 2026
39b2a0a
Merge pull request #1645 from ionfwsrijan/fix/1626-role-allow-list-to…
KaranUnique Aug 15, 2026
bbfd057
Merge pull request #1729 from vedant7007/fix/sheet-progress-object-shape
KaranUnique Aug 15, 2026
044ab74
Merge pull request #2166 from saidai-bhuvanesh/fix/interview-experien…
KaranUnique Aug 15, 2026
edd146f
Merge pull request #2172 from bindusreeseetha/fix/notes-summary-filen…
KaranUnique Aug 15, 2026
0e7409e
Resolve merge conflict: keep streakTrackerMock with {newlyUnlocked: […
Torqued-codes Aug 15, 2026
dd7f773
Merge branch 'main' into main
Torqued-codes Aug 15, 2026
2cef6d3
fix: resolve account registration login issue
sam-dwdkr404 Aug 15, 2026
f8c07e4
Merge pull request #2250 from Torqued-codes/main
KaranUnique Aug 16, 2026
93d2983
Merge pull request #2259 from sam-dwdkr404/fix/account-registration-v…
KaranUnique Aug 16, 2026
ffb1258
resolved signup issue
KaranUnique Aug 16, 2026
50e0406
solve the problem
dpi26 Aug 18, 2026
2fec3b0
Analyze the resume
dpi26 Aug 18, 2026
4e81c9a
resolved signup issue
KaranUnique Aug 19, 2026
60a352e
Merge pull request #2263 from dpi26/feature/my-change
KaranUnique Aug 19, 2026
5983c61
commented email verification on signup flow for testing purpose
KaranUnique Aug 19, 2026
8139899
Merge branch 'main' of https://github.com/Canopus-Labs/PrepPilot
KaranUnique Aug 19, 2026
d5a27b1
Feature : Added Hamburger menu for NavItems in Mobile/Tablet view in …
Subham269 Aug 21, 2026
3aac69b
fix: add missing Google Calendar backend routes
dpi26 Aug 21, 2026
33086ac
Solved
dpi26 Aug 21, 2026
fcdc013
error solved
dpi26 Aug 21, 2026
1cb3078
Merge pull request #2267 from dpi26/feature/my-change
KaranUnique Aug 22, 2026
0473ff9
Fix: Added Login button to all screens of all sizes and made public r…
Subham269 Aug 26, 2026
04e77ef
Merge pull request #2266 from Subham269/feature/hamburger-menu-addition
KaranUnique Aug 28, 2026
ba30b50
feat(resume): add deterministic ATS keyword-match endpoint
vedant7007 Aug 28, 2026
420348a
Merge pull request #2269 from vedant7007/build/ats-match
KaranUnique Aug 29, 2026
bf7f648
feat(company-prep): add company interview prep board
karan-chaos Aug 29, 2026
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
37 changes: 37 additions & 0 deletions .github/workflows/closes_comment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Issue Closed Comment

on:
issues:
types: [closed]

permissions:
issues: write

jobs:
comment-on-close:
runs-on: ubuntu-latest

steps:
- name: Post Closing Comment
uses: actions/github-script@v7

with:
github-token: ${{ secrets.GITHUB_TOKEN }}

script: |
const issue = context.payload.issue;
const creator = issue.user.login;
const repoName = context.repo.repo;

const commentBody = `### Issue Closed\n\n` +
`Thank you, @${creator}, for taking the time to report or engage with this issue in **${repoName}**! 🛡️\n\n` +
`Your input helps us manage our project roadmap effectively.\n\n` +
`If you discover new edge cases or have ideas for enhancements, feel free to open a new issue. Happy coding! 🌟`;

await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: commentBody
});

35 changes: 35 additions & 0 deletions .github/workflows/pr_raise.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Auto Comment on PR

on:
pull_request_target:
types: [opened]

permissions:
pull-requests: write

jobs:
comment:
runs-on: ubuntu-latest

steps:
- name: Add Comment to Pull Request
uses: actions/github-script@v7

with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const prNumber = context.payload.pull_request.number;
const contributor = context.payload.pull_request.user.login;
const commentBody = `Thank you for submitting your pull request, @${contributor}! 🙌
We'll review it as soon as possible.
If there are any specific instructions or feedback regarding your PR, we'll provide them here.
Thanks again for your contribution to our project! 😊`;

await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: commentBody
});


81 changes: 81 additions & 0 deletions backend/Input_validators/ValidateCompanyPrep.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
const z = require("zod");

const STATUSES = ["researching", "preparing", "applied", "interviewing", "offer", "rejected", "withdrawn"];

const validateCreateCompanyPrep = (req, res, next) => {
const schema = z.object({
companyName: z.string().min(1, "Company name required").max(100),
role: z.string().min(1, "Role required").max(100),
status: z.enum(STATUSES).default("researching"),
applicationUrl: z.string().max(500).optional(),
salaryRange: z.string().max(80).optional(),
location: z.string().max(100).optional(),
readinessScore: z.number().min(0).max(100).optional(),
focusTopics: z.array(z.string().max(40)).max(8).optional(),
processNotes: z.string().max(2000).optional(),
priority: z.enum(["low", "medium", "high"]).optional(),
deadline: z.string().datetime().optional(),
});

const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ success: false, message: result.error.errors.map((e) => e.message).join(", ") });
}
req.validatedBody = result.data;
next();
};

const validateUpdateCompanyPrep = (req, res, next) => {
const schema = z.object({
companyName: z.string().min(1).max(100).optional(),
role: z.string().min(1).max(100).optional(),
status: z.enum(STATUSES).optional(),
applicationUrl: z.string().max(500).optional(),
salaryRange: z.string().max(80).optional(),
location: z.string().max(100).optional(),
readinessScore: z.number().min(0).max(100).optional(),
focusTopics: z.array(z.string().max(40)).max(8).optional(),
processNotes: z.string().max(2000).optional(),
priority: z.enum(["low", "medium", "high"]).optional(),
deadline: z.string().datetime().nullable().optional(),
});

const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ success: false, message: result.error.errors.map((e) => e.message).join(", ") });
}
req.validatedBody = result.data;
next();
};

const validateAddQuestion = (req, res, next) => {
const schema = z.object({
question: z.string().min(1, "Question required").max(500),
difficulty: z.enum(["Easy", "Medium", "Hard"]).default("Medium"),
notes: z.string().max(300).optional(),
});

const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ success: false, message: result.error.errors.map((e) => e.message).join(", ") });
}
req.validatedBody = result.data;
next();
};

const validateAddRound = (req, res, next) => {
const schema = z.object({
name: z.string().min(1, "Round name required").max(80),
description: z.string().max(300).optional(),
scheduledAt: z.string().datetime().optional(),
});

const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ success: false, message: result.error.errors.map((e) => e.message).join(", ") });
}
req.validatedBody = result.data;
next();
};

module.exports = { validateCreateCompanyPrep, validateUpdateCompanyPrep, validateAddQuestion, validateAddRound };
2 changes: 1 addition & 1 deletion backend/Input_validators/ValidateNotesSummary.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const aiOutputSchema = z.object({


const saveNotesSummarySchema = z.object({
fileName: z.string().min(1).max(200),
fileName: z.string().min(1).max(200, "File name cannot exceed 200 characters"),
sourceType: z.enum(["upload", "platform"]),
sourceUrl: z.string().url().optional().nullable(),
pageCount: z.number().int().nonnegative().optional().default(0),
Expand Down
1 change: 1 addition & 0 deletions backend/constants/achievements.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const VALID_ACHIEVEMENTS = new Set([
"DSA Master",
"3-Day Streak",
"7-Day Streak",
"14-Day Streak",
"30-Day Streak",
]);

Expand Down
17 changes: 3 additions & 14 deletions backend/controllers/achievementController.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,14 @@
const User = require('../models/User');
const { VALID_ACHIEVEMENTS } = require('../constants/achievements');
const { resetStreakIfMissed } = require('../utils/streakTracker');

exports.getAchievements = async (req, res) => {
try {
const user = await User.findById(req.user._id).select('unlockedAchievements lastPracticeDate currentStreak');
if (!user) return res.status(404).json({ success: false, error: 'User not found' });

// Reset streak to 0 if one or more calendar days were missed
if (user.lastPracticeDate && user.currentStreak > 0) {
const now = new Date();
const d1 = new Date(user.lastPracticeDate);
const utc1 = Date.UTC(d1.getUTCFullYear(), d1.getUTCMonth(), d1.getUTCDate());
const utc2 = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
const diffDays = Math.floor((utc2 - utc1) / (1000 * 60 * 60 * 24));
if (diffDays > 1) {
user.currentStreak = 0;
await user.save();
}
if (resetStreakIfMissed(user)) {
await user.save();
}

res.json({ success: true, unlockedAchievements: user.unlockedAchievements });
Expand All @@ -35,7 +27,6 @@ exports.saveAchievements = async (req, res) => {
});
}

// Reject any ID not in the server-side allowlist
const unknown = unlockedAchievements.filter((id) => !VALID_ACHIEVEMENTS.has(id));

if (unknown.length > 0) {
Expand All @@ -46,8 +37,6 @@ exports.saveAchievements = async (req, res) => {
}

try {
// $addToSet is idempotent and additive-only — it never removes
// achievements the user already earned, and never duplicates.
await User.findByIdAndUpdate(
req.user._id,
{ $addToSet: { unlockedAchievements: { $each: unlockedAchievements } } },
Expand Down
101 changes: 45 additions & 56 deletions backend/controllers/authController.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ const jwt = require("jsonwebtoken");
const crypto = require("crypto");
const { sendVerificationEmail } = require("../utils/sendEmail");
const { validatePassword } = require('../utils/passwordPolicy');
const { isValidCountry } = require("../utils/nameCountry");
const { resetStreakIfMissed } = require("../utils/streakTracker");

// Models for cascade deletion on account delete
const Session = require("../models/Session");
Expand Down Expand Up @@ -97,27 +99,13 @@ const registerUser = async (req, res) => {

const userExists = await User.findOne({ email: cleanEmail });
if (userExists) {
if (!userExists.isEmailVerified) {
const rawToken = crypto.randomBytes(32).toString("hex");
userExists.emailVerificationToken = crypto.createHash("sha256").update(rawToken).digest("hex");
userExists.emailVerificationExpires = new Date(Date.now() + 24 * 60 * 60 * 1000);
await userExists.save();
try {
const verificationUrl = `${process.env.FRONTEND_URL}/verify-email?token=${rawToken}`;
await sendVerificationEmail(userExists.email, verificationUrl);
} catch (err) {
console.error("Failed to resend verification email on re-registration:", err);
}
}
return res.status(201).json({
success: true,
message: "If this email is not already registered, your account has been created. Please check your email to verify your account before logging in.",
// Always return the same ambiguous message to prevent email enumeration
return res.status(409).json({
success: false,
message: "An account with this email already exists. Please log in.",
});
}

// Hash raw password with bcrypt before DB creation (#757)
const hashedPassword = await bcrypt.hash(password, PASSWORD_SALT_ROUNDS);

// Split name into first and last names for defaults
const nameParts = cleanName.split(/\s+/);
const firstName = nameParts[0] || "";
Expand All @@ -126,14 +114,12 @@ const registerUser = async (req, res) => {
// Generate default unique PrepPilot ID
const defaultPrepPilotId = cleanEmail.split("@")[0] + Math.floor(1000 + Math.random() * 9000);

const rawToken = crypto.randomBytes(32).toString("hex");
const emailVerificationToken = crypto.createHash("sha256").update(rawToken).digest("hex");
const emailVerificationExpires = new Date(Date.now() + 24 * 60 * 60 * 1000);

// Email verification is currently disabled — accounts are active immediately on creation.
// To re-enable: set isEmailVerified to false, generate a token, and call sendVerificationEmail.
const user = await User.create({
name: cleanName,
email: cleanEmail,
password: hashedPassword,
password,
profileImageUrl,
firstName,
lastName,
Expand All @@ -147,21 +133,29 @@ const registerUser = async (req, res) => {
socials: { github: "", linkedin: "", twitter: "", portfolio: "" }
},
platformPreferences: { theme: "light", notificationsEnabled: true },
isEmailVerified: false,
emailVerificationToken,
emailVerificationExpires,
isEmailVerified: true, // verification disabled — users can log in immediately
emailVerificationToken: null,
emailVerificationExpires: null,
});

try {
const verificationUrl = `${process.env.FRONTEND_URL}/verify-email?token=${rawToken}`;
await sendVerificationEmail(user.email, verificationUrl);
} catch (err) {
console.error("Failed to send initial verification email:", err);
}
// Issue tokens immediately so the user is logged in right after signup
const accessToken = generateAccessToken(user._id, user.tokenVersion);
const refreshToken = generateRefreshToken(user._id);

user.refreshTokenHash = await bcrypt.hash(refreshToken, REFRESH_TOKEN_SALT_ROUNDS);
user.refreshTokenExpiresAt = new Date(Date.now() + REFRESH_TOKEN_MAX_AGE_MS);
await user.save();

res.cookie("refreshToken", refreshToken, getRefreshCookieOptions());

return res.status(201).json({
success: true,
message: "If this email is not already registered, your account has been created. Please check your email to verify your account before logging in.",
message: "Account created successfully.",
accessToken,
_id: user._id,
name: user.name,
email: user.email,
profileImageUrl: user.profileImageUrl,
});
} catch (error) {
console.error("Register error:", error);
Expand Down Expand Up @@ -192,14 +186,6 @@ const loginUser = async (req, res) => {
return res.status(401).json({ success: false, message: "Invalid email or password provided." });
}

// Block login until email is verified
if (!user.isEmailVerified) {
return res.status(403).json({
success: false,
message: "Please verify your email before logging in. Check your inbox for the verification link.",
});
}

const accessToken = generateAccessToken(user._id, user.tokenVersion);
const refreshToken = generateRefreshToken(user._id);

Expand Down Expand Up @@ -413,17 +399,8 @@ const getUserProfile = async (req, res) => {
return res.status(404).json({ success: false, message: "Requested user profile not found" });
}

// Reset streak to 0 if one or more calendar days were missed
if (user.lastPracticeDate && user.currentStreak > 0) {
const now = new Date();
const d1 = new Date(user.lastPracticeDate);
const utc1 = Date.UTC(d1.getUTCFullYear(), d1.getUTCMonth(), d1.getUTCDate());
const utc2 = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
const diffDays = Math.floor((utc2 - utc1) / (1000 * 60 * 60 * 24));
if (diffDays > 1) {
user.currentStreak = 0;
await user.save();
}
if (resetStreakIfMissed(user)) {
await user.save();
}

res.json(user);
Expand All @@ -433,6 +410,7 @@ const getUserProfile = async (req, res) => {
}
};


/**
* Update the user profile settings.
* @route PUT /api/auth/profile
Expand All @@ -459,13 +437,24 @@ const updateUserProfile = async (req, res) => {
}

// Update fields if they are sent in request
// Update fields if they are sent in request
if (firstName !== undefined) user.firstName = firstName;
if (lastName !== undefined) user.lastName = lastName;
if (bio !== undefined) user.bio = bio;
if (country !== undefined) user.country = country;

if (country !== undefined) {
if (country.trim() !== "" && !isValidCountry(country)) {
return res.status(400).json({
success: false,
message: "Please enter a valid country name.",
});
}

user.country = country.trim();
}

if (profileImageUrl !== undefined) user.profileImageUrl = profileImageUrl;
if (visibility !== undefined) user.visibility = visibility;

// Sync name based on firstName and lastName
if (firstName !== undefined || lastName !== undefined) {
const fName = firstName !== undefined ? firstName : user.firstName;
Expand Down Expand Up @@ -558,8 +547,8 @@ const changePassword = async (req, res) => {
return res.status(400).json({ success: false, message: "Incorrect original password" });
}

// Hash new password before saving (#757)
user.password = await bcrypt.hash(newPassword, PASSWORD_SALT_ROUNDS);
// Set new password (User.js pre save hook hashes it automatically)
user.password = newPassword;

// Fix #759: Revoke active refresh tokens in database & increment tokenVersion for access tokens
user.refreshTokenHash = null;
Expand Down
Loading
Loading