Skip to content

Commit 9e100ce

Browse files
authored
Merge pull request #3 from NextStepFinalProject/NXD-6
Nxd 6 - Add ATS Resume Scoring To The Backend
2 parents e97b066 + 46d64b9 commit 9e100ce

35 files changed

Lines changed: 1637 additions & 201 deletions

nextstep-backend/package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
},
2121
"homepage": "https://github.com/NextStepFinalProject/NextStep#readme",
2222
"dependencies": {
23+
"@types/pdf-parse": "^1.1.5",
24+
"axios": "^1.8.3",
2325
"bcrypt": "^5.1.1",
2426
"body-parser": "^1.20.3",
2527
"cors": "^2.8.5",
@@ -34,13 +36,15 @@
3436
"jest-junit": "^16.0.0",
3537
"js-yaml": "^4.1.0",
3638
"jsonwebtoken": "^9.0.2",
39+
"mammoth": "^1.9.0",
3740
"mongoose": "^8.8.2",
3841
"multer": "^1.4.5-lts.1",
42+
"office-text-extractor": "^3.0.3",
43+
"pdf-lib": "^1.17.1",
3944
"socket.io": "^4.8.1",
4045
"supertest": "^7.0.0",
4146
"swagger-jsdoc": "^6.2.8",
42-
"swagger-ui-express": "^5.0.1",
43-
"axios": "^1.8.3"
47+
"swagger-ui-express": "^5.0.1"
4448
},
4549
"devDependencies": {
4650
"@babel/preset-env": "^7.26.0",

nextstep-backend/src/app.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {config} from "./config/config";
1414
import validateUser from "./middleware/validateUser";
1515
import loadOpenApiFile from "./openapi/openapi_loader";
1616
import resource_routes from './routes/resources_routes';
17-
17+
import resume_routes from './routes/resume_routes';
1818

1919
const specs = swaggerJsdoc(options);
2020

@@ -43,10 +43,8 @@ app.use(bodyParser.json());
4343
app.use(removeUndefinedOrEmptyFields);
4444
app.use(bodyParser.urlencoded({ extended: true }));
4545

46-
4746
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(loadOpenApiFile() as JsonObject));
4847

49-
5048
// Add Authentication for all routes except the ones listed below
5149
app.use(authenticateToken.unless({
5250
path: [
@@ -68,14 +66,13 @@ app.use(authenticateToken.unless({
6866
// To block queries without Authentication
6967
app.use(authenticateTokenForParams);
7068

71-
72-
7369
app.use('/auth', authRoutes);
7470
app.use('/comment', commentsRoutes);
7571
app.use('/post', postsRoutes);
7672
app.use("/user/:id", validateUser);
7773
app.use('/user', usersRoutes);
7874
app.use('/resource', resource_routes);
7975
app.use('/room', roomsRoutes);
76+
app.use('/resume', resume_routes);
8077

8178
export { app, corsOptions };

nextstep-backend/src/config/config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ export const config = {
1616
},
1717
resources: {
1818
imagesDirectoryPath: () => 'resources/images',
19-
imageMaxSize: () => 10 * 1024 * 1024 // Max file size: 10MB
19+
imageMaxSize: () => 10 * 1024 * 1024, // Max file size: 10MB
20+
resumesDirectoryPath: () => 'resources/resumes',
21+
resumeMaxSize: () => 5 * 1024 * 1024 // Max file size: 5MB
2022
},
2123
chatAi: {
2224
api_url: () => process.env.CHAT_AI_API_URL || 'https://openrouter.ai/api/v1/chat/completions',

nextstep-backend/src/controllers/resources_controller.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@ import { Request, Response } from 'express';
22
import { config } from '../config/config';
33
import fs from 'fs';
44
import path from 'path';
5-
import { uploadImage } from '../services/resources_service';
5+
import { uploadResume, uploadImage } from '../services/resources_service';
66
import multer from 'multer';
77
import {CustomRequest} from "types/customRequest";
88
import {updateUserById} from "../services/users_service";
99
import {handleError} from "../utils/handle_error";
1010

11-
1211
const createUserImageResource = async (req: CustomRequest, res: Response) => {
1312
try {
1413
const imageFilename = await uploadImage(req);
@@ -49,4 +48,38 @@ const getImageResource = async (req: Request, res: Response) => {
4948
}
5049
};
5150

52-
export default { createUserImageResource, createImageResource, getImageResource };
51+
const createResumeResource = async (req: Request, res: Response) => {
52+
try {
53+
const resumeFilename = await uploadResume(req);
54+
return res.status(201).send(resumeFilename);
55+
} catch (error) {
56+
if (error instanceof multer.MulterError || error instanceof TypeError) {
57+
return res.status(400).send(error.message);
58+
} else {
59+
handleError(error, res);
60+
}
61+
}
62+
};
63+
64+
const getResumeResource = async (req: Request, res: Response) => {
65+
try {
66+
const { filename } = req.params;
67+
const resumePath = path.resolve(config.resources.resumesDirectoryPath(), filename);
68+
69+
if (!fs.existsSync(resumePath)) {
70+
return res.status(404).send('Resume not found');
71+
}
72+
73+
res.sendFile(resumePath);
74+
} catch (error) {
75+
handleError(error, res);
76+
}
77+
};
78+
79+
export default {
80+
createUserImageResource,
81+
createImageResource,
82+
getImageResource,
83+
getResumeResource,
84+
createResumeResource
85+
};
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { Request, Response } from 'express';
2+
import { config } from '../config/config';
3+
import fs from 'fs';
4+
import path from 'path';
5+
import { scoreResume, streamScoreResume } from '../services/resume_service';
6+
import multer from 'multer';
7+
import { CustomRequest } from "types/customRequest";
8+
import { handleError } from "../utils/handle_error";
9+
10+
const getResumeScore = async (req: Request, res: Response) => {
11+
try {
12+
const { filename } = req.params;
13+
const resumePath = path.resolve(config.resources.resumesDirectoryPath(), filename);
14+
const jobDescription = req.query.jobDescription as string;
15+
16+
if (!fs.existsSync(resumePath)) {
17+
return res.status(404).send('Resume not found');
18+
}
19+
20+
const scoreAndFeedback = await scoreResume(resumePath, jobDescription);
21+
return res.status(200).send(scoreAndFeedback);
22+
} catch (error) {
23+
if (error instanceof TypeError) {
24+
return res.status(400).send(error.message);
25+
} else {
26+
handleError(error, res);
27+
}
28+
}
29+
};
30+
31+
const getStreamResumeScore = async (req: Request, res: Response) => {
32+
try {
33+
const { filename } = req.params;
34+
const resumePath = path.resolve(config.resources.resumesDirectoryPath(), filename);
35+
const jobDescription = req.query.jobDescription as string;
36+
37+
if (!fs.existsSync(resumePath)) {
38+
return res.status(404).send('Resume not found');
39+
}
40+
41+
// Set headers for SSE
42+
res.setHeader('Content-Type', 'text/event-stream');
43+
res.setHeader('Cache-Control', 'no-cache');
44+
res.setHeader('Connection', 'keep-alive');
45+
46+
// Handle client disconnect
47+
req.on('close', () => {
48+
res.end();
49+
});
50+
51+
// Stream the response
52+
const score = await streamScoreResume(
53+
resumePath,
54+
jobDescription,
55+
(chunk) => {
56+
res.write(`data: ${JSON.stringify({ chunk })}\n\n`);
57+
}
58+
);
59+
60+
// Send the final score
61+
res.write(`data: ${JSON.stringify({ score, done: true })}\n\n`);
62+
res.end();
63+
} catch (error) {
64+
if (error instanceof TypeError) {
65+
return res.status(400).send(error.message);
66+
} else {
67+
handleError(error, res);
68+
}
69+
}
70+
};
71+
72+
export default {
73+
getResumeScore,
74+
getStreamResumeScore
75+
};

nextstep-backend/src/middleware/auth.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,16 @@ const getTokenFromHeader = (req: CustomRequest): string | undefined => {
1111
return authHeader?.split(' ')[1];
1212
}
1313

14+
const getTokenFromQueryParams = (req: CustomRequest): string | undefined => {
15+
return req.query.accessToken as string;
16+
}
17+
1418
const authenticateTokenHandler: any & { unless: typeof unless } = async (req: CustomRequest, res: Response, next: NextFunction, ignoreExpiration = false): Promise<void> => {
15-
const token = getTokenFromHeader(req);
19+
let token = getTokenFromHeader(req);
20+
if (!token) {
21+
// If the token was not found in the headers, fall back to the query params.
22+
token = getTokenFromQueryParams(req);
23+
}
1624

1725
if (!token) {
1826
res.status(401).json({ message: 'Access token required' });

0 commit comments

Comments
 (0)