Compare commits
46 commits
master
...
feature/in
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bd28168a5 | ||
|
|
e2abe509d0 | ||
|
|
f7e1e618a4 | ||
|
|
0b3de762d8 | ||
|
|
ac29d8369f | ||
|
|
78fb1338e8 | ||
|
|
80020e61e9 | ||
|
|
17a1aa6c5d | ||
|
|
6f0a2fdd41 | ||
|
|
c8ffc71e1e | ||
|
|
ad0f9bc1c3 | ||
|
|
003132a838 | ||
|
|
ddcbf4ca90 | ||
|
|
ca686c28aa | ||
|
|
3102ed9cc7 | ||
|
|
97bd114837 | ||
|
|
fa8cebdef5 | ||
|
|
8f8e3bd585 | ||
|
|
692cf2223c | ||
|
|
ffbf63a15d | ||
|
|
01d79c06da | ||
|
|
07a0052624 | ||
|
|
9d6d51451b | ||
|
|
cf36a8dd63 | ||
|
|
1071ef012d | ||
|
|
cb32993d5b | ||
|
|
e3ca7adb08 | ||
|
|
3bab80711b | ||
|
|
21c488eeba | ||
|
|
35daf83b92 | ||
|
|
57ce71062d | ||
|
|
c9449db0bb | ||
|
|
8f1bd9e962 | ||
|
|
63cec006bc | ||
|
|
8942f0fff2 | ||
|
|
f9c3448b86 | ||
|
|
03201fe9c0 | ||
|
|
cf48f6aa7d | ||
|
|
565b7e547c | ||
|
|
5c5f1d06ae | ||
|
|
1b77fdf39d | ||
|
|
8cd779e10e | ||
|
|
0f84aea193 | ||
|
|
ddb5e6f976 | ||
|
|
3440a66f6d | ||
|
|
797c40b9a0 |
13 changed files with 392 additions and 0 deletions
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Node modules
|
||||
node_modules/
|
||||
|
||||
# Dependency lock files
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
|
||||
# Logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# VS Code settings
|
||||
.vscode/
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
build/
|
||||
38
README.md
Normal file
38
README.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# CVGen Project
|
||||
|
||||
## Overview
|
||||
CVGen is a Node.js application that connects to the LinkedIn API to retrieve user profile information. The application is designed to display this information on a blank page without the need for HTML or CSS.
|
||||
|
||||
## Project Structure
|
||||
```
|
||||
cvgen
|
||||
├── controllers
|
||||
│ └── linkedin.js # Handles LinkedIn API connection and user profile retrieval
|
||||
├── routes
|
||||
│ ├── api.routes.js # Defines API routes for LinkedIn authentication and data fetching
|
||||
│ └── front.routes.js # Defines front-end routes (currently not utilized)
|
||||
├── views # Currently empty, not needed for this project
|
||||
├── public
|
||||
│ └── img # Contains images (not utilized in this project)
|
||||
├── app.js # Entry point of the application, sets up server and routes
|
||||
├── package.json # Configuration file for npm, lists project dependencies
|
||||
└── README.md # Documentation for the project
|
||||
```
|
||||
|
||||
## Setup Instructions
|
||||
1. Clone the repository to your local machine.
|
||||
2. Navigate to the project directory.
|
||||
3. Run `npm install` to install the required dependencies.
|
||||
4. Configure your LinkedIn API credentials in the `linkedin.js` controller.
|
||||
5. Start the application by running `node app.js`.
|
||||
|
||||
## Usage
|
||||
- The application will connect to the LinkedIn API and retrieve user profile information.
|
||||
- The retrieved information will be displayed on a blank page.
|
||||
|
||||
## Dependencies
|
||||
- Express: A web framework for Node.js.
|
||||
- LinkedIn API libraries: For connecting and interacting with the LinkedIn API.
|
||||
|
||||
## License
|
||||
This project is licensed under the MIT License.
|
||||
26
app.js
Normal file
26
app.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
const express = require("express");
|
||||
const bodyParser = require("body-parser");
|
||||
const session = require("express-session");
|
||||
const apiRoutes = require("./routes/api.routes");
|
||||
const frontRoutes = require("./routes/front.routes");
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 4200;
|
||||
|
||||
// Middleware
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
app.use(session({
|
||||
secret: "your_secret_key", // change ce secret !
|
||||
resave: false,
|
||||
saveUninitialized: true,
|
||||
}));
|
||||
|
||||
// Routes
|
||||
app.use("/api", apiRoutes);
|
||||
app.use("/", frontRoutes);
|
||||
|
||||
// Start the server
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server is running on http://localhost:${PORT}`);
|
||||
});
|
||||
204
controllers/linkedin.js
Normal file
204
controllers/linkedin.js
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
// filepath: c:\Users\amaizy\Desktop\cvgen\controllers\linkedin.js
|
||||
|
||||
const axios = require("axios");
|
||||
const querystring = require("querystring");
|
||||
const { chromium } = require("playwright");
|
||||
|
||||
const clientId = "780w7gsy8eysmj";
|
||||
const clientSecret = "WPL_AP1.w6OTTkAndAdT3PYF.UZEcwQ==";
|
||||
const redirectUri = "http://localhost:4200/api/auth/linkedin/callback";
|
||||
const scope = "openid profile email";
|
||||
|
||||
const email = "amaury@maizy.net";
|
||||
const password = "2Qh*fJrp+l7M6g>8P~}/S$Bc2Yvf&-vd";
|
||||
|
||||
let accessToken = "";
|
||||
|
||||
const authenticateUser = (req, res) => {
|
||||
const authUrl = `https://www.linkedin.com/oauth/v2/authorization?${querystring.stringify(
|
||||
{
|
||||
response_type: "code",
|
||||
client_id: clientId,
|
||||
redirect_uri: redirectUri,
|
||||
scope: scope,
|
||||
}
|
||||
)}`;
|
||||
res.redirect(authUrl);
|
||||
};
|
||||
|
||||
const handleCallback = async (req, res) => {
|
||||
const { code } = req.query;
|
||||
|
||||
try {
|
||||
const tokenResponse = await axios.post(
|
||||
"https://www.linkedin.com/oauth/v2/accessToken",
|
||||
querystring.stringify({
|
||||
grant_type: "authorization_code",
|
||||
code: code,
|
||||
redirect_uri: redirectUri,
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
})
|
||||
);
|
||||
|
||||
const accessToken = tokenResponse.data.access_token;
|
||||
|
||||
// Stocke dans la session
|
||||
req.session.user = {
|
||||
accessToken,
|
||||
};
|
||||
|
||||
// Redirige vers /profile après authentification
|
||||
res.redirect("/profile");
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"LinkedIn token error:",
|
||||
error.response ? error.response.data : error.message
|
||||
);
|
||||
res.status(500).send("Error retrieving access token.");
|
||||
}
|
||||
};
|
||||
|
||||
const getUserProfile = async (req, res) => {
|
||||
try {
|
||||
const token = req.session?.user?.accessToken;
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: "Aucun accessToken en session." });
|
||||
}
|
||||
|
||||
const { data } = await axios.get("https://api.linkedin.com/v2/userinfo", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
// Enregistre les infos utiles en session
|
||||
req.session.user.nom = data.given_name + " " + data.family_name;
|
||||
req.session.user.img = data.picture;
|
||||
req.session.user.email = data.email;
|
||||
|
||||
// Redirige vers /me après authentification
|
||||
res.redirect("/me");
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"LinkedIn OIDC error:",
|
||||
error.response?.data || error.message
|
||||
);
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: "Erreur lors de la récupération du profil." });
|
||||
}
|
||||
};
|
||||
|
||||
const scrapeLinkedInProfile = async (user) => {
|
||||
const UserImg = user.img;
|
||||
const UserEmail = user.email;
|
||||
const profileUrl = user.linkedinUrl;
|
||||
|
||||
console.log("Scraping LinkedIn profile:", profileUrl);
|
||||
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
args: ["--no-sandbox", "--disable-setuid-sandbox"],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
try {
|
||||
// === 1. Login ===
|
||||
await page.goto("https://www.linkedin.com/login", {
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: 60000,
|
||||
});
|
||||
console.log("Login page loaded");
|
||||
await page.screenshot({
|
||||
path: "public/error/step1_login.png",
|
||||
fullPage: true,
|
||||
});
|
||||
|
||||
await page.fill("#username", email, { delay: 50 });
|
||||
await page.fill("#password", password, { delay: 50 });
|
||||
|
||||
await Promise.all([
|
||||
page.click('[type="submit"]'),
|
||||
page.waitForNavigation({ waitUntil: "domcontentloaded", timeout: 60000 }),
|
||||
]);
|
||||
console.log("Logged in");
|
||||
await page.screenshot({
|
||||
path: "public/error/step2_logged_in.png",
|
||||
fullPage: true,
|
||||
});
|
||||
|
||||
// === 2. Aller sur le profil ===
|
||||
await page.goto(profileUrl, {
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: 60000,
|
||||
});
|
||||
await page.waitForSelector("h1", { timeout: 60000 });
|
||||
console.log("Profile page loaded");
|
||||
await page.screenshot({
|
||||
path: "public/error/step3_profile_loaded.png",
|
||||
fullPage: true,
|
||||
});
|
||||
|
||||
// Scroll pour charger contenu lazy
|
||||
await page.evaluate(() => window.scrollBy(0, window.innerHeight));
|
||||
await page.waitForTimeout(2000);
|
||||
await page.screenshot({
|
||||
path: "public/error/step4_scrolled.png",
|
||||
fullPage: true,
|
||||
});
|
||||
|
||||
// === 3. Extraire les infos ===
|
||||
const profileData = await page.evaluate(() => {
|
||||
const getText = (selector) =>
|
||||
document.querySelector(selector)?.innerText || null;
|
||||
const getAllText = (selector) =>
|
||||
Array.from(document.querySelectorAll(selector)).map((el) =>
|
||||
el.innerText.trim()
|
||||
);
|
||||
|
||||
return {
|
||||
name: getText("h1"),
|
||||
headline: getText("div.text-body-medium.break-words"),
|
||||
location: getText(
|
||||
"span.text-body-small.inline.t-black--light.break-words"
|
||||
),
|
||||
// about: getText(""),
|
||||
experiences: getAllText(
|
||||
"div.KbuWagYZEALUPOYGtkFczhgTiHHNRLZKhdLlK > ul li"
|
||||
),
|
||||
// education: getAllText(""),
|
||||
// skills: getAllText(""),
|
||||
};
|
||||
});
|
||||
|
||||
// Ajoute les infos supplémentaires avant de retourner
|
||||
profileData.img = UserImg || null;
|
||||
profileData.email = UserEmail || null;
|
||||
profileData.linkedinUrl = profileUrl || null;
|
||||
|
||||
await page.screenshot({
|
||||
path: "public/error/step5_data_extracted.png",
|
||||
fullPage: true,
|
||||
});
|
||||
|
||||
console.log("Data extracted:", profileData);
|
||||
|
||||
await browser.close();
|
||||
return profileData;
|
||||
} catch (err) {
|
||||
console.error("❌ Erreur pendant le scraping:", err.message);
|
||||
await page.screenshot({ path: "public/error/error.png", fullPage: true });
|
||||
await browser.close();
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
authenticateUser,
|
||||
handleCallback,
|
||||
getUserProfile,
|
||||
scrapeLinkedInProfile,
|
||||
};
|
||||
22
package.json
Normal file
22
package.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "cvgen",
|
||||
"version": "1.0.0",
|
||||
"description": "A project to connect to the LinkedIn API and retrieve user profile information.",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"start": "node app.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.11.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"express-session": "^1.17.1",
|
||||
"playwright": "^1.55.0"
|
||||
},
|
||||
"author": "ExostFlash",
|
||||
"license": "ISC",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "ssh://git@git.lehub.tf:2222/MecDu.Dev/cvgen.git"
|
||||
}
|
||||
}
|
||||
BIN
public/error/error.png
Normal file
BIN
public/error/error.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 907 KiB |
BIN
public/error/step1_login.png
Normal file
BIN
public/error/step1_login.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
BIN
public/error/step2_logged_in.png
Normal file
BIN
public/error/step2_logged_in.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 636 KiB |
BIN
public/error/step3_profile_loaded.png
Normal file
BIN
public/error/step3_profile_loaded.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 888 KiB |
BIN
public/error/step4_scrolled.png
Normal file
BIN
public/error/step4_scrolled.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 899 KiB |
BIN
public/error/step5_data_extracted.png
Normal file
BIN
public/error/step5_data_extracted.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 906 KiB |
23
routes/api.routes.js
Normal file
23
routes/api.routes.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// filepath: c:\Users\amaizy\Desktop\cvgen\routes\api.routes.js
|
||||
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const { authenticateUser, handleCallback } = require("../controllers/linkedin");
|
||||
|
||||
// Route for LinkedIn authentication
|
||||
router.get("/auth/linkedin", authenticateUser);
|
||||
|
||||
// Callback route for LinkedIn OAuth2
|
||||
router.get("/auth/linkedin/callback", handleCallback);
|
||||
|
||||
// Traitement du formulaire d'URL LinkedIn
|
||||
router.post("/me/link", express.urlencoded({ extended: true }), (req, res) => {
|
||||
if (!req.session.user) return res.redirect("/");
|
||||
req.session.user.linkedinUrl = req.body.linkedinUrl;
|
||||
res.send(`
|
||||
<p>URL LinkedIn enregistrée : <a href="${req.body.linkedinUrl}" target="_blank">${req.body.linkedinUrl}</a></p>
|
||||
<a href="/me">Retour</a>
|
||||
`);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
52
routes/front.routes.js
Normal file
52
routes/front.routes.js
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
// filepath: c:\Users\amaizy\Desktop\cvgen\routes\front.routes.js
|
||||
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const {
|
||||
getUserProfile,
|
||||
scrapeLinkedInProfile,
|
||||
} = require("../controllers/linkedin");
|
||||
|
||||
// Page d'accueil
|
||||
router.get("/", (req, res) => {
|
||||
const user = req.session.user;
|
||||
if (user) {
|
||||
return res.redirect("/me");
|
||||
}
|
||||
|
||||
res.send(`
|
||||
<h1>Welcome to the CV Generator Home Page!</h1>
|
||||
<a href="/api/auth/linkedin">
|
||||
<button>Connexion LinkedIn</button>
|
||||
</a>
|
||||
`);
|
||||
});
|
||||
|
||||
// Route pour afficher le profil utilisateur (API)
|
||||
router.get("/profile", getUserProfile);
|
||||
|
||||
// Route protégée /me
|
||||
router.get("/me", async (req, res) => {
|
||||
const user = req.session.user;
|
||||
if (!user) {
|
||||
return res.redirect("/");
|
||||
}
|
||||
|
||||
if (!user.linkedinUrl) {
|
||||
return res.send(`
|
||||
<h2>Bienvenue, ${user.nom} !</h2>
|
||||
<form method="POST" action="/api/me/link">
|
||||
<label for="linkedinUrl">Votre URL LinkedIn :</label>
|
||||
<input type="url" id="linkedinUrl" name="linkedinUrl" placeholder="https://www.linkedin.com/in/votre-profil" required>
|
||||
<button type="submit">Enregistrer</button>
|
||||
</form>
|
||||
`);
|
||||
}
|
||||
|
||||
const profile = await scrapeLinkedInProfile(user);
|
||||
|
||||
res.send(profile);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Loading…
Add table
Reference in a new issue