Compare commits
No commits in common. "feature/initialize" and "master" have entirely different histories.
feature/in
...
master
13 changed files with 0 additions and 392 deletions
27
.gitignore
vendored
27
.gitignore
vendored
|
|
@ -1,27 +0,0 @@
|
||||||
# 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
38
README.md
|
|
@ -1,38 +0,0 @@
|
||||||
# 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
26
app.js
|
|
@ -1,26 +0,0 @@
|
||||||
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}`);
|
|
||||||
});
|
|
||||||
|
|
@ -1,204 +0,0 @@
|
||||||
// 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
22
package.json
|
|
@ -1,22 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 907 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 32 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 636 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 888 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 899 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 906 KiB |
|
|
@ -1,23 +0,0 @@
|
||||||
// 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;
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
// 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