diff --git a/backend/app/controllers/oauth.controller.js b/backend/app/controllers/oauth.controller.js
new file mode 100644
index 0000000..f6613f6
--- /dev/null
+++ b/backend/app/controllers/oauth.controller.js
@@ -0,0 +1,159 @@
+import passport from "passport";
+import { Strategy as GitHubStrategy } from "passport-github2";
+import { getClient } from "../utils/database.js";
+import jwt from "jsonwebtoken";
+
+export async function login(req, res) {
+ passport.authenticate("github", { scope: ["user:email"] })(req, res);
+}
+
+export async function callback(req, res, next) {
+ passport.authenticate("github", { failureRedirect: "https://localhost/login?error=oauth_failed" }, async (err, user) => {
+ if (err) {
+ console.error("OAuth error:", err);
+ return res.redirect("https://localhost/login?error=oauth_error");
+ }
+
+ if (!user) {
+ return res.redirect("https://localhost/login?error=oauth_cancelled");
+ }
+
+ try {
+ // Extract user information from GitHub profile
+ const githubId = user.id;
+ const username = user.username || user.login;
+ const email = user.emails[0].value || null;
+ const avatarUrl = user.photos && user.photos[0] ? user.photos[0].value : null;
+ const displayName = user.displayName || username;
+
+ console.log(user);
+
+ console.log("GitHub user info:", {
+ githubId,
+ username,
+ email,
+ displayName,
+ avatarUrl
+ });
+
+ const client = await getClient();
+
+ // Check if user already exists by GitHub ID
+ let existingUserQuery = `SELECT * FROM users WHERE github_id = $1`;
+ let existingUserResult = await client.query(existingUserQuery, [githubId]);
+
+ let dbUser;
+
+ if (existingUserResult.rows.length > 0) {
+ // User exists, update their information
+ dbUser = existingUserResult.rows[0];
+
+ const updateQuery = `UPDATE users SET
+ username = $1,
+ email = $2,
+ picture = $3,
+ is_verified = true,
+ updated_at = NOW()
+ WHERE github_id = $4
+ RETURNING id, username, email, picture`;
+
+ const updateResult = await client.query(updateQuery, [
+ username,
+ email,
+ avatarUrl || "/api/media/profile/default.png",
+ githubId
+ ]);
+
+ dbUser = updateResult.rows[0];
+ } else {
+ // Check if username already exists
+ const usernameCheck = await client.query(`SELECT id FROM users WHERE username = $1`, [username]);
+ let finalUsername = username;
+
+ if (usernameCheck.rows.length > 0) {
+ // Username exists, append GitHub ID to make it unique
+ finalUsername = `${username}_gh${githubId}`;
+ }
+
+ // Create new user
+ const insertQuery = `INSERT INTO users (
+ username,
+ email,
+ picture,
+ github_id,
+ password,
+ is_verified
+ ) VALUES ($1, $2, $3, $4, $5, $6)
+ RETURNING id, username, email, picture`;
+
+ const insertResult = await client.query(insertQuery, [
+ finalUsername,
+ email,
+ avatarUrl || "/api/media/profile/default.png",
+ githubId,
+ null, // No password for OAuth users
+ true // OAuth users are automatically verified
+ ]);
+
+ dbUser = insertResult.rows[0];
+
+ // Create default playlist for new user
+ const playlistQuery = `INSERT INTO playlists (name, owner) VALUES ('A regarder plus tard', $1)`;
+ await client.query(playlistQuery, [dbUser.id]);
+ }
+
+ client.end();
+
+ // Generate JWT token
+ const payload = {
+ id: dbUser.id,
+ username: dbUser.username,
+ };
+
+ const token = jwt.sign(payload, process.env.JWT_SECRET);
+
+ // Redirect to frontend with token and user info
+ const userData = encodeURIComponent(JSON.stringify({
+ id: dbUser.id,
+ username: dbUser.username,
+ email: dbUser.email,
+ picture: dbUser.picture
+ }));
+
+ res.redirect(`https://localhost/login/success?token=${token}&user=${userData}`);
+
+ } catch (error) {
+ console.error("Error processing GitHub OAuth callback:", error);
+ res.redirect("https://localhost/login?error=processing_error");
+ }
+ })(req, res, next);
+}
+
+export async function getUserInfo(req, res) {
+ try {
+ // This endpoint can be used to get current user info from token
+ const token = req.headers.authorization?.split(" ")[1];
+
+ if (!token) {
+ return res.status(401).json({ error: "No token provided" });
+ }
+
+ const decoded = jwt.verify(token, process.env.JWT_SECRET);
+ const client = await getClient();
+
+ const query = `SELECT id, username, email, picture, github_id, is_verified FROM users WHERE id = $1`;
+ const result = await client.query(query, [decoded.id]);
+
+ if (!result.rows[0]) {
+ client.end();
+ return res.status(404).json({ error: "User not found" });
+ }
+
+ client.end();
+ res.status(200).json({ user: result.rows[0] });
+
+ } catch (error) {
+ console.error("Error getting user info:", error);
+ res.status(500).json({ error: "Internal server error" });
+ }
+}
\ No newline at end of file
diff --git a/backend/app/routes/oauth.route.js b/backend/app/routes/oauth.route.js
new file mode 100644
index 0000000..2b2d2b3
--- /dev/null
+++ b/backend/app/routes/oauth.route.js
@@ -0,0 +1,15 @@
+import {Router} from 'express';
+import { callback, login, getUserInfo } from '../controllers/oauth.controller.js';
+import { isTokenValid } from '../middlewares/jwt.middleware.js';
+import { addLogger } from '../middlewares/logger.middleware.js';
+
+const router = Router();
+
+router.get('/github', login)
+
+router.get('/callback', callback)
+
+// Get current user info from token
+router.get('/me', [addLogger, isTokenValid], getUserInfo)
+
+export default router;
\ No newline at end of file
diff --git a/backend/app/uploads/profiles/astria.png b/backend/app/uploads/profiles/astria.png
index 388cdbe..f1a0670 100644
Binary files a/backend/app/uploads/profiles/astria.png and b/backend/app/uploads/profiles/astria.png differ
diff --git a/backend/app/utils/database.js b/backend/app/utils/database.js
index 315335f..ad85431 100644
--- a/backend/app/utils/database.js
+++ b/backend/app/utils/database.js
@@ -20,11 +20,14 @@ export async function initDb() {
try {
let query = `CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
- email VARCHAR(255) NOT NULL,
+ email VARCHAR(255),
username VARCHAR(255) NOT NULL,
- password VARCHAR(255) NOT NULL,
+ password VARCHAR(255),
picture VARCHAR(255),
- is_verified BOOLEAN NOT NULL DEFAULT FALSE
+ is_verified BOOLEAN NOT NULL DEFAULT FALSE,
+ github_id VARCHAR(255) UNIQUE,
+ created_at TIMESTAMP NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);`;
await client.query(query);
@@ -126,8 +129,21 @@ export async function initDb() {
)`;
await client.query(query);
+ // Add GitHub OAuth columns if they don't exist
+ try {
+ await client.query(`ALTER TABLE users ADD COLUMN IF NOT EXISTS github_id VARCHAR(255) UNIQUE`);
+ await client.query(`ALTER TABLE users ADD COLUMN IF NOT EXISTS created_at TIMESTAMP DEFAULT NOW()`);
+ await client.query(`ALTER TABLE users ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP DEFAULT NOW()`);
+ await client.query(`ALTER TABLE users ALTER COLUMN email DROP NOT NULL`);
+ await client.query(`ALTER TABLE users ALTER COLUMN password DROP NOT NULL`);
+ } catch (e) {
+ console.log("OAuth columns already exist or error adding them:", e.message);
+ }
+
} catch (e) {
console.error("Error initializing database:", e);
+ } finally {
+ client.end();
}
}
\ No newline at end of file
diff --git a/backend/logs/access.log b/backend/logs/access.log
index 2b06713..d76fa21 100644
--- a/backend/logs/access.log
+++ b/backend/logs/access.log
@@ -8357,3 +8357,86 @@
[2025-08-20 19:34:43.088] [undefined] PUT(/:id/tags): try to add tags to video 20
[2025-08-20 19:34:43.096] [undefined] PUT(/:id/tags): Tag csgo already exists for video 20 with status 200
[2025-08-20 19:34:43.100] [undefined] PUT(/:id/tags): successfully added tags to video 20 with status 200
+[2025-08-22 11:41:01.973] [undefined] GET(/:id/history): try to retrieve history of user 1
+[2025-08-22 11:41:01.980] [undefined] GET(/:id/history): failed to retrieve history of user 1 because it doesn't exist with status 404
+[2025-08-22 11:41:01.985] [undefined] GET(/:id/channel): try to retrieve channel of user 1
+[2025-08-22 11:41:01.991] [undefined] GET(/:id/channel): successfully retrieved channel of user 1 with status 200
+[2025-08-22 11:41:02.003] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
+[2025-08-22 11:41:04.343] [undefined] GET(/:id): try to get channel with id 1
+[2025-08-22 11:41:04.355] [undefined] GET(/:id/stats): try to get stats
+[2025-08-22 11:41:04.361] [undefined] GET(/:id): Successfully get channel with id 1 with status 200
+[2025-08-22 11:41:04.371] [undefined] GET(/:id/stats): Successfully get stats with status 200
+[2025-08-22 11:41:12.948] [undefined] POST(/login): try to login with username 'astria'
+[2025-08-22 11:41:13.006] [undefined] POST(/login): failed to login with status 401
+[2025-08-22 11:41:19.893] [undefined] POST(/login): try to login with username 'astria'
+[2025-08-22 11:41:19.950] [undefined] POST(/login): Successfully logged in with status 200
+[2025-08-22 16:21:17.273] [undefined] GET(/:id/channel): try to retrieve channel of user 4
+[2025-08-22 16:21:17.277] [undefined] GET(/:id/channel): failed to retrieve channel of user 4 because it doesn't exist with status 404
+[2025-08-22 16:21:17.282] [undefined] GET(/:id/history): try to retrieve history of user 4
+[2025-08-22 16:21:17.286] [undefined] GET(/:id/history): failed to retrieve history of user 4 because it doesn't exist with status 404
+[2025-08-22 16:21:17.294] [undefined] GET(/user/:id): Playlists retrieved for user with id 4 with status 200
+[2025-08-22 16:23:00.680] [undefined] POST(/login): failed due to invalid values with status 400
+[2025-08-22 16:23:08.955] [undefined] POST(/login): failed due to invalid values with status 400
+[2025-08-22 16:24:16.438] [undefined] POST(/login): failed due to invalid values with status 400
+[2025-08-22 16:33:03.791] [undefined] POST(/login): failed due to invalid values with status 400
+[2025-08-22 16:33:18.532] [undefined] POST(/login): failed due to invalid values with status 400
+[2025-08-22 16:33:23.707] [undefined] GET(/:id/channel): try to retrieve channel of user 4
+[2025-08-22 16:33:23.712] [undefined] GET(/:id/channel): failed to retrieve channel of user 4 because it doesn't exist with status 404
+[2025-08-22 16:33:23.718] [undefined] GET(/:id/history): try to retrieve history of user 4
+[2025-08-22 16:33:23.722] [undefined] GET(/:id/history): failed to retrieve history of user 4 because it doesn't exist with status 404
+[2025-08-22 16:33:23.735] [undefined] GET(/user/:id): Playlists retrieved for user with id 4 with status 200
+[2025-08-22 16:33:44.131] [undefined] POST(/login): failed due to invalid values with status 400
+[2025-08-22 16:34:21.678] [undefined] POST(/login): failed due to invalid values with status 400
+[2025-08-22 16:34:50.238] [undefined] POST(/login): failed due to invalid values with status 400
+[2025-08-22 16:35:02.341] [undefined] GET(/:id/channel): try to retrieve channel of user 4
+[2025-08-22 16:35:02.345] [undefined] GET(/:id/channel): failed to retrieve channel of user 4 because it doesn't exist with status 404
+[2025-08-22 16:35:02.353] [undefined] GET(/:id/history): try to retrieve history of user 4
+[2025-08-22 16:35:02.357] [undefined] GET(/:id/history): failed to retrieve history of user 4 because it doesn't exist with status 404
+[2025-08-22 16:35:02.368] [undefined] GET(/user/:id): Playlists retrieved for user with id 4 with status 200
+[2025-08-22 16:35:08.368] [undefined] POST(/): try to create new channel with owner 4 and name github
+[2025-08-22 16:35:08.371] [undefined] POST(/): Successfully created new channel with name github with status 200
+[2025-08-22 16:35:08.389] [undefined] GET(/:id/channel): try to retrieve channel of user 4
+[2025-08-22 16:35:08.393] [undefined] GET(/:id/channel): successfully retrieved channel of user 4 with status 200
+[2025-08-22 16:35:09.953] [undefined] GET(/:id): try to get channel with id 2
+[2025-08-22 16:35:09.966] [undefined] GET(/:id/stats): try to get stats
+[2025-08-22 16:35:09.970] [undefined] GET(/:id): Successfully get channel with id 2 with status 200
+[2025-08-22 16:35:09.977] [undefined] GET(/:id/stats): Successfully get stats with status 200
+[2025-08-22 16:35:14.046] [undefined] GET(/:id/history): try to retrieve history of user 4
+[2025-08-22 16:35:14.049] [undefined] GET(/:id/channel): try to retrieve channel of user 4
+[2025-08-22 16:35:14.057] [undefined] GET(/:id/history): failed to retrieve history of user 4 because it doesn't exist with status 404
+[2025-08-22 16:35:14.061] [undefined] GET(/:id/channel): successfully retrieved channel of user 4 with status 200
+[2025-08-22 16:35:14.066] [undefined] GET(/user/:id): Playlists retrieved for user with id 4 with status 200
+[2025-08-22 16:35:49.944] [undefined] GET(/:id/channel): try to retrieve channel of user 4
+[2025-08-22 16:35:49.948] [undefined] GET(/:id/channel): successfully retrieved channel of user 4 with status 200
+[2025-08-22 16:35:49.951] [undefined] GET(/:id/history): try to retrieve history of user 4
+[2025-08-22 16:35:49.955] [undefined] GET(/:id/history): failed to retrieve history of user 4 because it doesn't exist with status 404
+[2025-08-22 16:35:49.964] [undefined] GET(/user/:id): Playlists retrieved for user with id 4 with status 200
+[2025-08-22 16:38:32.507] [undefined] GET(/:id/channel): try to retrieve channel of user 4
+[2025-08-22 16:38:32.511] [undefined] GET(/:id/channel): successfully retrieved channel of user 4 with status 200
+[2025-08-22 16:38:32.519] [undefined] GET(/:id/history): try to retrieve history of user 4
+[2025-08-22 16:38:32.525] [undefined] GET(/:id/history): failed to retrieve history of user 4 because it doesn't exist with status 404
+[2025-08-22 16:38:32.535] [undefined] GET(/user/:id): Playlists retrieved for user with id 4 with status 200
+[2025-08-22 16:38:34.090] [undefined] GET(/:id): try to get channel with id 2
+[2025-08-22 16:38:34.101] [undefined] GET(/:id/stats): try to get stats
+[2025-08-22 16:38:34.104] [undefined] GET(/:id): Successfully get channel with id 2 with status 200
+[2025-08-22 16:38:34.111] [undefined] GET(/:id/stats): Successfully get stats with status 200
+[2025-08-22 16:38:35.245] [undefined] GET(/:id/channel): try to retrieve channel of user 4
+[2025-08-22 16:38:35.248] [undefined] GET(/:id/channel): successfully retrieved channel of user 4 with status 200
+[2025-08-22 16:38:39.372] [undefined] GET(/search): try to search user by username a
+[2025-08-22 16:38:39.376] [undefined] GET(/search): successfully found user with username a with status 200
+[2025-08-22 16:38:43.731] [undefined] GET(/search): try to search user by username as
+[2025-08-22 16:38:43.738] [undefined] GET(/search): successfully found user with username as with status 200
+[2025-08-22 16:38:44.209] [undefined] GET(/search): try to search user by username a
+[2025-08-22 16:38:44.212] [undefined] GET(/search): successfully found user with username a with status 200
+[2025-08-22 16:38:45.263] [undefined] GET(/search): try to search user by username s
+[2025-08-22 16:38:45.267] [undefined] GET(/search): successfully found user with username s with status 200
+[2025-08-22 17:16:45.647] [undefined] GET(/:id/channel): try to retrieve channel of user 1
+[2025-08-22 17:16:45.652] [undefined] GET(/:id/channel): failed to retrieve channel of user 1 because it doesn't exist with status 404
+[2025-08-22 17:16:45.657] [undefined] GET(/:id/history): try to retrieve history of user 1
+[2025-08-22 17:16:45.662] [undefined] GET(/:id/history): failed to retrieve history of user 1 because it doesn't exist with status 404
+[2025-08-22 17:16:45.673] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
+[2025-08-22 17:20:25.391] [undefined] GET(/:id/channel): try to retrieve channel of user 1
+[2025-08-22 17:20:25.395] [undefined] GET(/:id/channel): failed to retrieve channel of user 1 because it doesn't exist with status 404
+[2025-08-22 17:20:25.400] [undefined] GET(/:id/history): try to retrieve history of user 1
+[2025-08-22 17:20:25.407] [undefined] GET(/:id/history): failed to retrieve history of user 1 because it doesn't exist with status 404
+[2025-08-22 17:20:25.418] [undefined] GET(/user/:id): Playlists retrieved for user with id 1 with status 200
diff --git a/backend/package-lock.json b/backend/package-lock.json
index faa30e7..a7ab9bf 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -14,11 +14,14 @@
"cors": "^2.8.5",
"dotenv": "^17.0.1",
"express": "^5.1.0",
+ "express-session": "^1.18.2",
"express-validator": "^7.2.1",
"jsonwebtoken": "^9.0.2",
"moment": "^2.30.1",
"multer": "^2.0.1",
"nodemailer": "^7.0.5",
+ "passport": "^0.7.0",
+ "passport-github2": "^0.1.12",
"pg": "^8.16.3"
},
"devDependencies": {
@@ -1069,6 +1072,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/base64url": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
+ "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/bcrypt": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
@@ -1925,6 +1937,46 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/express-session": {
+ "version": "1.18.2",
+ "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz",
+ "integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "0.7.2",
+ "cookie-signature": "1.0.7",
+ "debug": "2.6.9",
+ "depd": "~2.0.0",
+ "on-headers": "~1.1.0",
+ "parseurl": "~1.3.3",
+ "safe-buffer": "5.2.1",
+ "uid-safe": "~2.1.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/express-session/node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+ "license": "MIT"
+ },
+ "node_modules/express-session/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/express-session/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
"node_modules/express-validator": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.1.tgz",
@@ -3153,6 +3205,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/oauth": {
+ "version": "0.10.2",
+ "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz",
+ "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==",
+ "license": "MIT"
+ },
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -3186,6 +3244,15 @@
"node": ">= 0.8"
}
},
+ "node_modules/on-headers": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
+ "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -3243,6 +3310,63 @@
"node": ">= 0.8"
}
},
+ "node_modules/passport": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz",
+ "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "passport-strategy": "1.x.x",
+ "pause": "0.0.1",
+ "utils-merge": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/jaredhanson"
+ }
+ },
+ "node_modules/passport-github2": {
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/passport-github2/-/passport-github2-0.1.12.tgz",
+ "integrity": "sha512-3nPUCc7ttF/3HSP/k9sAXjz3SkGv5Nki84I05kSQPo01Jqq1NzJACgMblCK0fGcv9pKCG/KXU3AJRDGLqHLoIw==",
+ "dependencies": {
+ "passport-oauth2": "1.x.x"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/passport-oauth2": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz",
+ "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==",
+ "license": "MIT",
+ "dependencies": {
+ "base64url": "3.x.x",
+ "oauth": "0.10.x",
+ "passport-strategy": "1.x.x",
+ "uid2": "0.0.x",
+ "utils-merge": "1.x.x"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/jaredhanson"
+ }
+ },
+ "node_modules/passport-strategy": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
+ "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -3306,6 +3430,11 @@
"node": ">= 14.16"
}
},
+ "node_modules/pause": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
+ "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
+ },
"node_modules/pg": {
"version": "8.16.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
@@ -3534,6 +3663,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/random-bytes": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
+ "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -4292,6 +4430,24 @@
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"license": "MIT"
},
+ "node_modules/uid-safe": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
+ "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
+ "license": "MIT",
+ "dependencies": {
+ "random-bytes": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/uid2": {
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz",
+ "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==",
+ "license": "MIT"
+ },
"node_modules/undefsafe": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
@@ -4321,6 +4477,15 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
"node_modules/validator": {
"version": "13.12.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz",
diff --git a/backend/package.json b/backend/package.json
index 28e7f10..c7247bd 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -18,11 +18,14 @@
"cors": "^2.8.5",
"dotenv": "^17.0.1",
"express": "^5.1.0",
+ "express-session": "^1.18.2",
"express-validator": "^7.2.1",
"jsonwebtoken": "^9.0.2",
"moment": "^2.30.1",
"multer": "^2.0.1",
"nodemailer": "^7.0.5",
+ "passport": "^0.7.0",
+ "passport-github2": "^0.1.12",
"pg": "^8.16.3"
},
"devDependencies": {
diff --git a/backend/server.js b/backend/server.js
index 6140769..ffd5df4 100644
--- a/backend/server.js
+++ b/backend/server.js
@@ -11,6 +11,10 @@ import PlaylistRoute from "./app/routes/playlist.route.js";
import {initDb} from "./app/utils/database.js";
import MediaRoutes from "./app/routes/media.routes.js";
import SearchRoute from "./app/routes/search.route.js";
+import OAuthRoute from "./app/routes/oauth.route.js";
+import session from "express-session";
+import passport from "passport";
+import { Strategy as GitHubStrategy } from "passport-github2";
console.clear();
dotenv.config();
@@ -23,6 +27,39 @@ app.use(express.urlencoded({extended: true, limit: '500mb'}));
app.use(express.json({limit: '500mb'}));
app.use(cors())
+app.use(session({
+ secret: "your-secret",
+ resave: false,
+ saveUninitialized: false,
+}));
+
+// --- Passport setup ---
+app.use(passport.initialize());
+app.use(passport.session());
+
+// Serialize user -> session
+passport.serializeUser((user, done) => {
+ done(null, user);
+});
+
+// Deserialize session -> user
+passport.deserializeUser((obj, done) => {
+ done(null, obj);
+});
+
+// --- GitHub Strategy ---
+ passport.use(new GitHubStrategy({
+ clientID: process.env.GITHUB_ID,
+ clientSecret: process.env.GITHUB_SECRET,
+ callbackURL: "https://localhost/api/oauth/callback",
+ scope: ["user:email"]
+ },
+ (accessToken, refreshToken, profile, done) => {
+
+ return done(null, profile);
+ }
+ ));
+
// ROUTES
app.use("/api/users/", UserRoute);
app.use("/api/channels/", ChannelRoute);
@@ -32,6 +69,7 @@ app.use("/api/playlists", PlaylistRoute);
app.use("/api/recommendations", RecommendationRoute);
app.use("/api/media", MediaRoutes);
app.use("/api/search", SearchRoute);
+app.use("/api/oauth", OAuthRoute);
const port = process.env.PORT;
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 66550d8..1f2014d 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -18,6 +18,8 @@ services:
PORT: ${BACKEND_PORT}
GMAIL_USER: ${GMAIL_USER}
GMAIL_PASSWORD: ${GMAIL_PASSWORD}
+ GITHUB_ID: ${GITHUB_ID}
+ GITHUB_SECRET: ${GITHUB_SECRET}
volumes:
- ./backend/logs:/var/log/freetube
- ./backend:/app
diff --git a/frontend/src/components/GitHubLoginButton.jsx b/frontend/src/components/GitHubLoginButton.jsx
new file mode 100644
index 0000000..466d102
--- /dev/null
+++ b/frontend/src/components/GitHubLoginButton.jsx
@@ -0,0 +1,23 @@
+import oauthService from '../services/oauthService';
+
+export default function GitHubLoginButton({ className = "" }) {
+ const handleGitHubLogin = () => {
+ oauthService.loginWithGitHub();
+ };
+
+ return (
+
+ );
+}
diff --git a/frontend/src/contexts/AuthContext.jsx b/frontend/src/contexts/AuthContext.jsx
index 2bbf860..a12365f 100644
--- a/frontend/src/contexts/AuthContext.jsx
+++ b/frontend/src/contexts/AuthContext.jsx
@@ -1,4 +1,4 @@
-import React, { createContext, useContext, useState, useEffect } from 'react';
+import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
const AuthContext = createContext();
@@ -25,7 +25,7 @@ export const AuthProvider = ({ children }) => {
setLoading(false);
}, []);
- const login = async (username, password) => {
+ const login = useCallback(async (username, password) => {
try {
const response = await fetch('/api/users/login', {
method: 'POST',
@@ -50,9 +50,9 @@ export const AuthProvider = ({ children }) => {
} catch (error) {
throw error;
}
- };
+ }, []);
- const register = async (email, username, password, profileImage) => {
+ const register = useCallback(async (email, username, password, profileImage) => {
try {
const formData = new FormData();
formData.append('email', email);
@@ -81,22 +81,31 @@ export const AuthProvider = ({ children }) => {
} catch (error) {
throw error;
}
- };
+ }, []);
+
+ const loginWithOAuth = useCallback((userData, token) => {
+ console.log('OAuth login called with:', userData);
+ // Store token and user data
+ localStorage.setItem('token', token);
+ localStorage.setItem('user', JSON.stringify(userData));
+ setUser(userData);
+ }, []);
- const logout = () => {
+ const logout = useCallback(() => {
localStorage.removeItem('token');
localStorage.removeItem('user');
setUser(null);
- };
+ }, []);
- const getAuthHeaders = () => {
+ const getAuthHeaders = useCallback(() => {
const token = localStorage.getItem('token');
return token ? { Authorization: `Bearer ${token}` } : {};
- };
+ }, []);
const value = {
user,
login,
+ loginWithOAuth,
register,
logout,
getAuthHeaders,
diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx
index d0d5851..9e13cb5 100644
--- a/frontend/src/pages/Login.jsx
+++ b/frontend/src/pages/Login.jsx
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import Navbar from '../components/Navbar';
+import GitHubLoginButton from '../components/GitHubLoginButton';
export default function Login() {
const [formData, setFormData] = useState({
@@ -104,6 +105,7 @@ export default function Login() {
+
+
Finalisation de la connexion...
+
Déjà un compte ?{' '}
diff --git a/frontend/src/routes/routes.jsx b/frontend/src/routes/routes.jsx
index 28d1f6a..0cde932 100644
--- a/frontend/src/routes/routes.jsx
+++ b/frontend/src/routes/routes.jsx
@@ -10,6 +10,7 @@ import AddVideo from "../pages/AddVideo.jsx";
import Search from "../pages/Search.jsx";
import Channel from "../pages/Channel.jsx";
import Playlist from "../pages/Playlist.jsx";
+import LoginSuccess from '../pages/LoginSuccess.jsx'
const routes = [
{ path: "/", element: