Skip to content

Commit 9004f35

Browse files
authored
Merge pull request #30 from NextStepFinalProject/NXD-27-Edit-feed-page
Nxd 27 edit feed page
2 parents 517b2df + 0c439de commit 9004f35

31 files changed

+5963
-2168
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ web_modules/
7979
.env.test.local
8080
.env.production.local
8181
.env.local
82+
.env*
8283

8384
# parcel-bundler cache (https://parceljs.org/)
8485
.cache

nextstep-backend/src/app.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ app.use(authenticateToken.unless({
6262
{ url: '/comment', methods: ['GET'] },
6363
{ url: '/post', methods: ['GET'] }, // Allow GET to /post
6464
{ url: /^\/resource\/image\/[^\/]+$/, methods: ['GET'] }, // Allow GET to /resource/image/{anything}
65+
{ url: /^\/resource\/file\/[^\/]+$/, methods: ['GET'] }, // Allow GET to /resource/image/{anything}
6566
]
6667
}));
6768

nextstep-backend/src/config/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export const config = {
1515
refresh_token_secret: () => process.env.REFRESH_TOKEN_SECRET || 'secret'
1616
},
1717
resources: {
18+
filesDirectoryPath: () => 'resources/files',
19+
fileMaxSize: () => 10 * 1024 * 1024, // Max file size: 10MB
1820
imagesDirectoryPath: () => 'resources/images',
1921
imageMaxSize: () => 10 * 1024 * 1024, // Max file size: 10MB
2022
resumesDirectoryPath: () => 'resources/resumes',

nextstep-backend/src/controllers/resources_controller.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Request, Response } from 'express';
22
import { config } from '../config/config';
33
import fs from 'fs';
44
import path from 'path';
5-
import { uploadResume, uploadImage } from '../services/resources_service';
5+
import { uploadResume, uploadImage, uploadFile } from '../services/resources_service';
66
import multer from 'multer';
77
import {CustomRequest} from "types/customRequest";
88
import {updateUserById} from "../services/users_service";
@@ -37,6 +37,19 @@ const createImageResource = async (req: Request, res: Response) => {
3737
}
3838
};
3939

40+
const createFileResource = async (req: Request, res: Response) => {
41+
try {
42+
const filename = await uploadFile(req);
43+
return res.status(201).send(filename);
44+
} catch (error) {
45+
if (error instanceof multer.MulterError || error instanceof TypeError) {
46+
return res.status(400).send({ message: error.message });
47+
} else {
48+
handleError(error, res);
49+
}
50+
}
51+
};
52+
4053
const getImageResource = async (req: Request, res: Response) => {
4154
try {
4255
const { filename } = req.params;
@@ -52,9 +65,27 @@ const getImageResource = async (req: Request, res: Response) => {
5265
}
5366
};
5467

68+
const getFileResource = async (req: Request, res: Response) => {
69+
try {
70+
const { filename } = req.params;
71+
const filePath = path.resolve(config.resources.filesDirectoryPath(), filename);
72+
73+
if (!fs.existsSync(filePath)) {
74+
return res.status(404).send('File not found');
75+
}
76+
77+
// res.sendFile(filePath);
78+
return res.download(filePath);
79+
} catch (error) {
80+
handleError(error, res);
81+
}
82+
};
83+
5584
const createResumeResource = async (req: Request, res: Response) => {
5685
try {
57-
const resumeFilename = await uploadResume(req);
86+
const resumeFilename = await uploadResume(req);
87+
88+
5889
return res.status(201).send(resumeFilename);
5990
} catch (error) {
6091
if (error instanceof multer.MulterError || error instanceof TypeError) {
@@ -83,7 +114,9 @@ const getResumeResource = async (req: Request, res: Response) => {
83114
export default {
84115
createUserImageResource,
85116
createImageResource,
117+
createFileResource,
86118
getImageResource,
119+
getFileResource,
87120
getResumeResource,
88121
createResumeResource
89122
};

nextstep-backend/src/controllers/resume_controller.ts

Lines changed: 82 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,20 @@ import { Request, Response } from 'express';
22
import { config } from '../config/config';
33
import fs from 'fs';
44
import path from 'path';
5-
import { scoreResume, streamScoreResume, parseResumeFields } from '../services/resume_service';
5+
import { scoreResume, streamScoreResume, parseResumeFields,
6+
saveParsedResume, getResumeByOwner, updateResume } from '../services/resume_service';
67
import multer from 'multer';
8+
import {getResumeBuffer, resumeExists} from '../services/resources_service';
79
import { CustomRequest } from "types/customRequest";
810
import { handleError } from "../utils/handle_error";
911

12+
// Simple in-memory cache: { [key: string]: { scoreAndFeedback, timestamp } }
13+
const resumeScoreCache: Record<string, { data: any, timestamp: number }> = {};
14+
const CACHE_TTL_MS = 24* 60 * 60 * 1000; // 24 hour
15+
16+
const getCacheKey = (filename: string, jobDescription?: string) =>
17+
`${filename}::${jobDescription || ''}`;
18+
1019
const getResumeScore = async (req: Request, res: Response) => {
1120
try {
1221
const { filename } = req.params;
@@ -17,7 +26,14 @@ const getResumeScore = async (req: Request, res: Response) => {
1726
return res.status(404).send('Resume not found');
1827
}
1928

29+
const cacheKey = getCacheKey(filename, jobDescription);
30+
const cached = resumeScoreCache[cacheKey];
31+
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
32+
return res.status(200).send(cached.data);
33+
}
34+
2035
const scoreAndFeedback = await scoreResume(resumePath, jobDescription);
36+
resumeScoreCache[cacheKey] = { data: scoreAndFeedback, timestamp: Date.now() };
2137
return res.status(200).send(scoreAndFeedback);
2238
} catch (error) {
2339
if (error instanceof TypeError) {
@@ -28,7 +44,7 @@ const getResumeScore = async (req: Request, res: Response) => {
2844
}
2945
};
3046

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

57+
const cacheKey = getCacheKey(filename, jobDescription);
58+
const cached = resumeScoreCache[cacheKey];
59+
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
60+
// Stream cached result as SSE
61+
res.setHeader('Content-Type', 'text/event-stream');
62+
res.setHeader('Cache-Control', 'no-cache');
63+
res.setHeader('Connection', 'keep-alive');
64+
res.write(`data: ${JSON.stringify({ ...cached.data, done: true })}\n\n`);
65+
res.end();
66+
return;
67+
}
68+
4169
// Set headers for SSE
4270
res.setHeader('Content-Type', 'text/event-stream');
4371
res.setHeader('Cache-Control', 'no-cache');
@@ -49,17 +77,24 @@ const getStreamResumeScore = async (req: Request, res: Response) => {
4977
});
5078

5179
// Stream the response
52-
const score = await streamScoreResume(
80+
let fullChunk = '';
81+
const [score, fullText] = await streamScoreResume(
5382
resumePath,
5483
jobDescription,
5584
(chunk) => {
85+
fullChunk += chunk;
5686
res.write(`data: ${JSON.stringify({ chunk })}\n\n`);
5787
}
5888
);
5989

6090
// Send the final score
6191
res.write(`data: ${JSON.stringify({ score, done: true })}\n\n`);
6292
res.end();
93+
94+
// Optionally cache the result (score and fullText)
95+
resumeScoreCache[cacheKey] = { data: { score, fullText }, timestamp: Date.now() };
96+
await updateResume(req.user.id, jobDescription, fullText, score);
97+
6398
} catch (error) {
6499
if (error instanceof TypeError) {
65100
return res.status(400).send(error.message);
@@ -69,17 +104,57 @@ const getStreamResumeScore = async (req: Request, res: Response) => {
69104
}
70105
};
71106

72-
const parseResume = async (req: Request, res: Response) => {
107+
108+
const parseResume = async (req: CustomRequest, res: Response) => {
73109
try {
74-
if (!req.file) {
110+
if (!req.body.resumefileName) {
75111
return res.status(400).json({ error: 'No resume file uploaded' });
76112
}
77-
const parsed = await parseResumeFields(req.file.buffer, req.file.originalname);
113+
else if (!resumeExists(req.body.resumefileName)) {
114+
return res.status(400).json({ error: 'No resume file uploaded' });
115+
}
116+
117+
const resumeFilename = req.body.resumefileName;
118+
const parsed = await parseResumeFields(getResumeBuffer(req.body.resumefileName), resumeFilename);
119+
const resumeData = await saveParsedResume(parsed, req.user.id, resumeFilename, req.body.originfilename);
120+
78121
return res.status(200).json(parsed);
79122
} catch (err: any) {
80123
console.error('Error parsing resume:', err);
81124
return handleError(err, res);
82125
}
83126
};
84127

85-
export default { parseResume, getResumeScore, getStreamResumeScore };
128+
const getResume = async (req: CustomRequest, res: Response) => {
129+
try {
130+
const ownerId = req.user.id;
131+
const resume = await getResumeByOwner(ownerId);
132+
133+
if (!resume) {
134+
return res.status(404).json({ error: 'Resume not found' });
135+
}
136+
137+
return res.status(200).json(resume);
138+
} catch (error) {
139+
console.error('Error retrieving resume:', error);
140+
return handleError(error, res);
141+
}
142+
}
143+
144+
145+
const getResumeData = async (req: CustomRequest, res: Response) => {
146+
try {
147+
const ownerId = req.user.id;
148+
// Get the optional version parameter from query string
149+
const version = req.query.version ? parseInt(req.query.version as string) : undefined;
150+
const resume = await getResumeByOwner(ownerId, version);
151+
152+
return res.status(200).json(resume);
153+
} catch (error) {
154+
console.error('Error retrieving resume data:', error);
155+
return handleError(error, res);
156+
}
157+
};
158+
159+
export default { parseResume, getResumeScore,
160+
getStreamResumeScore, getResumeData, getResume };

nextstep-backend/src/controllers/users_controller.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,25 @@ export const getUserById = async (req: Request, res: Response): Promise<void> =>
3636
}
3737

3838

39+
export const updateUserProfile = async (req: Request, res: Response) => {
40+
const { aboutMe, skills, selectedRole } = req.body;
41+
42+
if (!req.params.id) {
43+
return res.status(400).json({ error: 'User ID is required' });
44+
}
45+
46+
try {
47+
const updatedUser = await usersService.updateUserProfile(req.params.id, aboutMe, skills, selectedRole);
48+
if (!updatedUser) {
49+
return res.status(404).json({ error: 'User not found' });
50+
}
51+
52+
res.status(200).json(updatedUser);
53+
} catch (error) {
54+
handleError(error, res);
55+
}
56+
};
57+
3958
export const updateUserById = async (req: Request, res: Response): Promise<void> => {
4059
try {
4160
const user = await usersService.updateUserById(req.params.id, req.body);
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import mongoose, { Schema } from 'mongoose';
2+
import {ResumeData} from "types/resume_types";
3+
4+
const ResumeSchema = new Schema({
5+
owner: { type: Schema.Types.ObjectId, ref: 'User', required: true },
6+
version: { type: Number, required: true },
7+
rawContentLink: { type: String, required: true },
8+
parsedData: {
9+
type: {
10+
fileName: { type: String, required: false },
11+
aboutMe: { type: String, required: false },
12+
skills: { type: [String], required: false },
13+
roleMatch: { type: String, required: false },
14+
experience: { type: [String], required: false },
15+
jobDescription: { type: String, required: false },
16+
feedback: { type: String, required: false },
17+
score: { type: Number, required: false },
18+
},
19+
required: false
20+
},
21+
createdAt: { type: Date, default: Date.now }
22+
}, { versionKey: false });
23+
24+
25+
ResumeSchema.set('toJSON', {
26+
transform: (doc, ret): ResumeData => {
27+
return {
28+
id: ret._id,
29+
owner: ret.owner._id.toString(),
30+
createdAt: ret.createdAt,
31+
updatedAt: ret.updatedAt,
32+
version: ret.version,
33+
rawContentLink: ret.rawContentLink,
34+
parsedData: ret.parsedData
35+
};
36+
}
37+
});
38+
39+
export const ResumeModel = mongoose.model('Resume', ResumeSchema);

nextstep-backend/src/models/user_model.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@ const userSchema: Schema = new Schema({
2323
type: Date,
2424
default: Date.now
2525
},
26+
aboutMe: {
27+
type: String,
28+
default: ""
29+
},
30+
skills: {
31+
type: [String],
32+
default: []
33+
},
34+
selectedRole: {
35+
type: String,
36+
default: ""
37+
},
2638
authProvider: {
2739
type: String
2840
}
@@ -37,7 +49,10 @@ userSchema.set('toJSON', {
3749
password: ret.password as string,
3850
imageFilename: ret?.imageFilename as string | undefined,
3951
createdAt: ret.createdAt ? ret.createdAt.toISOString() : undefined,
40-
updatedAt: ret.updatedAt ? ret.updatedAt.toISOString() : undefined
52+
updatedAt: ret.updatedAt ? ret.updatedAt.toISOString() : undefined,
53+
aboutMe: ret?.aboutMe as string | undefined,
54+
skills: ret?.skills as string[] | undefined,
55+
selectedRole: ret?.selectedRole as string | undefined
4156
};
4257
}
4358
});

nextstep-backend/src/routes/resources_routes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ const router = express.Router();
77
router.post('/image/user', (req: Request, res: Response) => Resource.createUserImageResource(req as CustomRequest, res));
88

99
router.post('/image', Resource.createImageResource);
10+
router.post('/file', Resource.createFileResource);
1011

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

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

nextstep-backend/src/routes/resume_routes.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,22 @@ import express, { Request, Response } from 'express';
22
import Resume from '../controllers/resume_controller';
33
import { CustomRequest } from "types/customRequest";
44
import multer from 'multer';
5+
import * as commentsController from "../controllers/comments_controller";
56

67
const upload = multer();
78

89
const router = express.Router();
910

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

12-
router.get('/streamScore/:filename', Resume.getStreamResumeScore);
13+
router.get('/streamScore/:filename', (req: Request, res: Response) => Resume.getStreamResumeScore(req as CustomRequest, res));
14+
15+
router.post('/parseResume', upload.single('resume'), (req: Request, res: Response) => Resume.parseResume(req as CustomRequest, res));
16+
17+
// TODO - Use it in the frontend after the parse and upload resume
18+
router.get('/resumeData/:version', (req: Request, res: Response) => Resume.getResumeData(req as CustomRequest, res))
19+
20+
router.get('/', (req: Request, res: Response) => Resume.getResume(req as CustomRequest, res))
1321

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

1623
export default router;

0 commit comments

Comments
 (0)