Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
342a348
Changed some structure
LiavTB May 23, 2025
3d98656
saved to the parsed resume in the db
LiavTB Jun 1, 2025
c1bcbfd
fix types errors
LiavTB Jun 1, 2025
e94ba1f
upload the resume using the resource upload route
LiavTB Jun 1, 2025
5b278d8
Merge branch 'master' into NXD-13
Lina0Elman Jun 7, 2025
5044047
added get resume by owner & added filename to resume model
Lina0Elman Jun 7, 2025
da0c68d
added changes to support resume page integration with server
Lina0Elman Jun 7, 2025
c6b26c4
Merge branch 'NXD-13-add-changes' into NXD-27-Edit-feed-page
Lina0Elman Jun 10, 2025
ca89dda
trying to improve the design
Lina0Elman Jun 11, 2025
f7a84e9
added fixes to dashboard
Lina0Elman Jun 12, 2025
9d6be0b
changed linkedin jobs
Lina0Elman Jun 13, 2025
38f0ec7
changed design
Lina0Elman Jun 13, 2025
a2deca9
added files for resume in resources
Lina0Elman Jun 14, 2025
880fe5a
added profile image to dashboard
Lina0Elman Jun 14, 2025
9711092
fixed files uploading
Lina0Elman Jul 5, 2025
2818ee5
fixed profile image fetching
Lina0Elman Jul 5, 2025
e800d4f
added design changes
Lina0Elman Jul 5, 2025
a54d0b7
removed linkedin from main dashboard & added missing from quiz page
Lina0Elman Jul 11, 2025
374dd48
reverted to previous quiz page
Lina0Elman Jul 11, 2025
94ae64a
Merge branch 'master' into NXD-27-Edit-feed-page
Lina0Elman Jul 17, 2025
8d88326
fixed build issues unnecessary imports
Lina0Elman Jul 17, 2025
1ed5852
added sync resume with main dashboard
Lina0Elman Jul 17, 2025
e374be9
added aboutme, skills and role to backend
Lina0Elman Aug 8, 2025
1735592
added small fix
Lina0Elman Aug 8, 2025
ffd6df5
fixed quiz response not parsing correctly
Lina0Elman Aug 19, 2025
dc69b19
fixed issue text box dissapearing
Lina0Elman Aug 19, 2025
0c439de
Merge branch 'master' into NXD-27-Edit-feed-page
taljacob2 Aug 19, 2025
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ web_modules/
.env.test.local
.env.production.local
.env.local
.env*

# parcel-bundler cache (https://parceljs.org/)
.cache
Expand Down
1 change: 1 addition & 0 deletions nextstep-backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ app.use(authenticateToken.unless({
{ url: '/comment', methods: ['GET'] },
{ url: '/post', methods: ['GET'] }, // Allow GET to /post
{ url: /^\/resource\/image\/[^\/]+$/, methods: ['GET'] }, // Allow GET to /resource/image/{anything}
{ url: /^\/resource\/file\/[^\/]+$/, methods: ['GET'] }, // Allow GET to /resource/image/{anything}
]
}));

Expand Down
2 changes: 2 additions & 0 deletions nextstep-backend/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export const config = {
refresh_token_secret: () => process.env.REFRESH_TOKEN_SECRET || 'secret'
},
resources: {
filesDirectoryPath: () => 'resources/files',
fileMaxSize: () => 10 * 1024 * 1024, // Max file size: 10MB
imagesDirectoryPath: () => 'resources/images',
imageMaxSize: () => 10 * 1024 * 1024, // Max file size: 10MB
resumesDirectoryPath: () => 'resources/resumes',
Expand Down
37 changes: 35 additions & 2 deletions nextstep-backend/src/controllers/resources_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Request, Response } from 'express';
import { config } from '../config/config';
import fs from 'fs';
import path from 'path';
import { uploadResume, uploadImage } from '../services/resources_service';
import { uploadResume, uploadImage, uploadFile } from '../services/resources_service';
import multer from 'multer';
import {CustomRequest} from "types/customRequest";
import {updateUserById} from "../services/users_service";
Expand Down Expand Up @@ -37,6 +37,19 @@ const createImageResource = async (req: Request, res: Response) => {
}
};

const createFileResource = async (req: Request, res: Response) => {
try {
const filename = await uploadFile(req);
return res.status(201).send(filename);
} catch (error) {
if (error instanceof multer.MulterError || error instanceof TypeError) {
return res.status(400).send({ message: error.message });
} else {
handleError(error, res);
}
}
};

const getImageResource = async (req: Request, res: Response) => {
try {
const { filename } = req.params;
Expand All @@ -52,9 +65,27 @@ const getImageResource = async (req: Request, res: Response) => {
}
};

const getFileResource = async (req: Request, res: Response) => {
try {
const { filename } = req.params;
const filePath = path.resolve(config.resources.filesDirectoryPath(), filename);

if (!fs.existsSync(filePath)) {
return res.status(404).send('File not found');
}

// res.sendFile(filePath);
return res.download(filePath);
} catch (error) {
handleError(error, res);
}
};

const createResumeResource = async (req: Request, res: Response) => {
try {
const resumeFilename = await uploadResume(req);
const resumeFilename = await uploadResume(req);


return res.status(201).send(resumeFilename);
} catch (error) {
if (error instanceof multer.MulterError || error instanceof TypeError) {
Expand Down Expand Up @@ -83,7 +114,9 @@ const getResumeResource = async (req: Request, res: Response) => {
export default {
createUserImageResource,
createImageResource,
createFileResource,
getImageResource,
getFileResource,
getResumeResource,
createResumeResource
};
89 changes: 82 additions & 7 deletions nextstep-backend/src/controllers/resume_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,20 @@ import { Request, Response } from 'express';
import { config } from '../config/config';
import fs from 'fs';
import path from 'path';
import { scoreResume, streamScoreResume, parseResumeFields } from '../services/resume_service';
import { scoreResume, streamScoreResume, parseResumeFields,
saveParsedResume, getResumeByOwner, updateResume } from '../services/resume_service';
import multer from 'multer';
import {getResumeBuffer, resumeExists} from '../services/resources_service';
import { CustomRequest } from "types/customRequest";
import { handleError } from "../utils/handle_error";

// Simple in-memory cache: { [key: string]: { scoreAndFeedback, timestamp } }
const resumeScoreCache: Record<string, { data: any, timestamp: number }> = {};
const CACHE_TTL_MS = 24* 60 * 60 * 1000; // 24 hour

const getCacheKey = (filename: string, jobDescription?: string) =>
`${filename}::${jobDescription || ''}`;

const getResumeScore = async (req: Request, res: Response) => {
try {
const { filename } = req.params;
Expand All @@ -17,7 +26,14 @@ const getResumeScore = async (req: Request, res: Response) => {
return res.status(404).send('Resume not found');
}

const cacheKey = getCacheKey(filename, jobDescription);
const cached = resumeScoreCache[cacheKey];
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
return res.status(200).send(cached.data);
}

const scoreAndFeedback = await scoreResume(resumePath, jobDescription);
resumeScoreCache[cacheKey] = { data: scoreAndFeedback, timestamp: Date.now() };
return res.status(200).send(scoreAndFeedback);
} catch (error) {
if (error instanceof TypeError) {
Expand All @@ -28,7 +44,7 @@ const getResumeScore = async (req: Request, res: Response) => {
}
};

const getStreamResumeScore = async (req: Request, res: Response) => {
const getStreamResumeScore = async (req: CustomRequest, res: Response) => {
try {
const { filename } = req.params;
const resumePath = path.resolve(config.resources.resumesDirectoryPath(), filename);
Expand All @@ -38,6 +54,18 @@ const getStreamResumeScore = async (req: Request, res: Response) => {
return res.status(404).send('Resume not found');
}

const cacheKey = getCacheKey(filename, jobDescription);
const cached = resumeScoreCache[cacheKey];
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
// Stream cached result as SSE
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.write(`data: ${JSON.stringify({ ...cached.data, done: true })}\n\n`);
res.end();
return;
}

// Set headers for SSE
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
Expand All @@ -49,17 +77,24 @@ const getStreamResumeScore = async (req: Request, res: Response) => {
});

// Stream the response
const score = await streamScoreResume(
let fullChunk = '';
const [score, fullText] = await streamScoreResume(
resumePath,
jobDescription,
(chunk) => {
fullChunk += chunk;
res.write(`data: ${JSON.stringify({ chunk })}\n\n`);
}
);

// Send the final score
res.write(`data: ${JSON.stringify({ score, done: true })}\n\n`);
res.end();

// Optionally cache the result (score and fullText)
resumeScoreCache[cacheKey] = { data: { score, fullText }, timestamp: Date.now() };
await updateResume(req.user.id, jobDescription, fullText, score);

} catch (error) {
if (error instanceof TypeError) {
return res.status(400).send(error.message);
Expand All @@ -69,17 +104,57 @@ const getStreamResumeScore = async (req: Request, res: Response) => {
}
};

const parseResume = async (req: Request, res: Response) => {

const parseResume = async (req: CustomRequest, res: Response) => {
try {
if (!req.file) {
if (!req.body.resumefileName) {
return res.status(400).json({ error: 'No resume file uploaded' });
}
const parsed = await parseResumeFields(req.file.buffer, req.file.originalname);
else if (!resumeExists(req.body.resumefileName)) {
return res.status(400).json({ error: 'No resume file uploaded' });
}

const resumeFilename = req.body.resumefileName;
const parsed = await parseResumeFields(getResumeBuffer(req.body.resumefileName), resumeFilename);
const resumeData = await saveParsedResume(parsed, req.user.id, resumeFilename, req.body.originfilename);

return res.status(200).json(parsed);
} catch (err: any) {
console.error('Error parsing resume:', err);
return handleError(err, res);
}
};

export default { parseResume, getResumeScore, getStreamResumeScore };
const getResume = async (req: CustomRequest, res: Response) => {
try {
const ownerId = req.user.id;
const resume = await getResumeByOwner(ownerId);

if (!resume) {
return res.status(404).json({ error: 'Resume not found' });
}

return res.status(200).json(resume);
} catch (error) {
console.error('Error retrieving resume:', error);
return handleError(error, res);
}
}


const getResumeData = async (req: CustomRequest, res: Response) => {
try {
const ownerId = req.user.id;
// Get the optional version parameter from query string
const version = req.query.version ? parseInt(req.query.version as string) : undefined;
const resume = await getResumeByOwner(ownerId, version);

return res.status(200).json(resume);
} catch (error) {
console.error('Error retrieving resume data:', error);
return handleError(error, res);
}
};

export default { parseResume, getResumeScore,
getStreamResumeScore, getResumeData, getResume };
19 changes: 19 additions & 0 deletions nextstep-backend/src/controllers/users_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,25 @@ export const getUserById = async (req: Request, res: Response): Promise<void> =>
}


export const updateUserProfile = async (req: Request, res: Response) => {
const { aboutMe, skills, selectedRole } = req.body;

if (!req.params.id) {
return res.status(400).json({ error: 'User ID is required' });
}

try {
const updatedUser = await usersService.updateUserProfile(req.params.id, aboutMe, skills, selectedRole);
if (!updatedUser) {
return res.status(404).json({ error: 'User not found' });
}

res.status(200).json(updatedUser);
} catch (error) {
handleError(error, res);
}
};

export const updateUserById = async (req: Request, res: Response): Promise<void> => {
try {
const user = await usersService.updateUserById(req.params.id, req.body);
Expand Down
39 changes: 39 additions & 0 deletions nextstep-backend/src/models/resume_model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import mongoose, { Schema } from 'mongoose';
import {ResumeData} from "types/resume_types";

const ResumeSchema = new Schema({
owner: { type: Schema.Types.ObjectId, ref: 'User', required: true },
version: { type: Number, required: true },
rawContentLink: { type: String, required: true },
parsedData: {
type: {
fileName: { type: String, required: false },
aboutMe: { type: String, required: false },
skills: { type: [String], required: false },
roleMatch: { type: String, required: false },
experience: { type: [String], required: false },
jobDescription: { type: String, required: false },
feedback: { type: String, required: false },
score: { type: Number, required: false },
},
required: false
},
createdAt: { type: Date, default: Date.now }
}, { versionKey: false });


ResumeSchema.set('toJSON', {
transform: (doc, ret): ResumeData => {
return {
id: ret._id,
owner: ret.owner._id.toString(),
createdAt: ret.createdAt,
updatedAt: ret.updatedAt,
version: ret.version,
rawContentLink: ret.rawContentLink,
parsedData: ret.parsedData
};
}
});

export const ResumeModel = mongoose.model('Resume', ResumeSchema);
17 changes: 16 additions & 1 deletion nextstep-backend/src/models/user_model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ const userSchema: Schema = new Schema({
type: Date,
default: Date.now
},
aboutMe: {
type: String,
default: ""
},
skills: {
type: [String],
default: []
},
selectedRole: {
type: String,
default: ""
},
authProvider: {
type: String
}
Expand All @@ -37,7 +49,10 @@ userSchema.set('toJSON', {
password: ret.password as string,
imageFilename: ret?.imageFilename as string | undefined,
createdAt: ret.createdAt ? ret.createdAt.toISOString() : undefined,
updatedAt: ret.updatedAt ? ret.updatedAt.toISOString() : undefined
updatedAt: ret.updatedAt ? ret.updatedAt.toISOString() : undefined,
aboutMe: ret?.aboutMe as string | undefined,
skills: ret?.skills as string[] | undefined,
selectedRole: ret?.selectedRole as string | undefined
};
}
});
Expand Down
2 changes: 2 additions & 0 deletions nextstep-backend/src/routes/resources_routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ const router = express.Router();
router.post('/image/user', (req: Request, res: Response) => Resource.createUserImageResource(req as CustomRequest, res));

router.post('/image', Resource.createImageResource);
router.post('/file', Resource.createFileResource);

router.get('/image/:filename', Resource.getImageResource);
router.get('/file/:filename', Resource.getFileResource);

router.post('/resume', Resource.createResumeResource);

Expand Down
11 changes: 9 additions & 2 deletions nextstep-backend/src/routes/resume_routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,22 @@ import express, { Request, Response } from 'express';
import Resume from '../controllers/resume_controller';
import { CustomRequest } from "types/customRequest";
import multer from 'multer';
import * as commentsController from "../controllers/comments_controller";

const upload = multer();

const router = express.Router();

router.get('/score/:filename', Resume.getResumeScore);

router.get('/streamScore/:filename', Resume.getStreamResumeScore);
router.get('/streamScore/:filename', (req: Request, res: Response) => Resume.getStreamResumeScore(req as CustomRequest, res));

router.post('/parseResume', upload.single('resume'), (req: Request, res: Response) => Resume.parseResume(req as CustomRequest, res));

// TODO - Use it in the frontend after the parse and upload resume
router.get('/resumeData/:version', (req: Request, res: Response) => Resume.getResumeData(req as CustomRequest, res))

router.get('/', (req: Request, res: Response) => Resume.getResume(req as CustomRequest, res))

router.post('/parseResume', upload.single('resume'), Resume.parseResume);

export default router;
2 changes: 1 addition & 1 deletion nextstep-backend/src/routes/users_routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ router.delete('/:id', validateUserId, handleValidationErrors, (req: Request, res

router.patch('/:id', validateUserId, validateUserDataOptional, handleValidationErrors, (req: Request, res: Response) => usersController.updateUserById(req, res));

// router.post('/', validateUserRegister, handleValidationErrors, (req: Request, res: Response) => usersController.createUser(req, res));
router.put('/:id', validateUserId, handleValidationErrors, (req: Request, res: Response) => usersController.updateUserProfile(req, res));


export default router;
Loading
Loading