Browse Source

Merge pull request 'features/oauth' (#6) from features/oauth into developpement

Reviewed-on: #6
features/trending
astria 4 months ago
parent
commit
cd791aac71
  1. 159
      backend/app/controllers/oauth.controller.js
  2. 15
      backend/app/routes/oauth.route.js
  3. BIN
      backend/app/uploads/profiles/astria.png
  4. 22
      backend/app/utils/database.js
  5. 83
      backend/logs/access.log
  6. 165
      backend/package-lock.json
  7. 3
      backend/package.json
  8. 38
      backend/server.js
  9. 2
      docker-compose.yaml
  10. 23
      frontend/src/components/GitHubLoginButton.jsx
  11. 27
      frontend/src/contexts/AuthContext.jsx
  12. 3
      frontend/src/pages/Login.jsx
  13. 59
      frontend/src/pages/LoginSuccess.jsx
  14. 3
      frontend/src/pages/Register.jsx
  15. 7
      frontend/src/routes/routes.jsx
  16. 36
      frontend/src/services/oauthService.js

159
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" });
}
}

15
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;

BIN
backend/app/uploads/profiles/astria.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 19 MiB

22
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();
}
}

83
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

165
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",

3
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": {

38
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;

2
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

23
frontend/src/components/GitHubLoginButton.jsx

@ -0,0 +1,23 @@
import oauthService from '../services/oauthService';
export default function GitHubLoginButton({ className = "" }) {
const handleGitHubLogin = () => {
oauthService.loginWithGitHub();
};
return (
<button
onClick={handleGitHubLogin}
className={`flex items-center justify-center w-full px-4 py-2 text-sm font-medium text-white bg-gray-800 border border-transparent rounded-md hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-colors ${className}`}
>
<svg className="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z"
clipRule="evenodd"
/>
</svg>
Continuer avec GitHub
</button>
);
}

27
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 logout = () => {
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 = 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,

3
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() {
</div>
</div>
<button
type="submit"
disabled={loading}
@ -111,6 +113,7 @@ export default function Login() {
>
{loading ? 'Connexion...' : 'Se connecter'}
</button>
<GitHubLoginButton className="mt-1 cursor-pointer" />
</form>
<div className="mt-6 text-center">

59
frontend/src/pages/LoginSuccess.jsx

@ -0,0 +1,59 @@
import { useEffect, useRef } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
export default function LoginSuccess() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const { loginWithOAuth } = useAuth();
const hasProcessed = useRef(false);
useEffect(() => {
// Prevent multiple executions
if (hasProcessed.current) return;
const token = searchParams.get('token');
const userParam = searchParams.get('user');
console.log('Processing OAuth callback:', { token: !!token, userParam: !!userParam });
if (token && userParam) {
try {
hasProcessed.current = true;
const userData = JSON.parse(decodeURIComponent(userParam));
console.log('Parsed user data:', userData);
// Use the OAuth login method to update auth context
loginWithOAuth(userData, token);
// Small delay before navigation to ensure state is updated
setTimeout(() => {
navigate('/', { replace: true });
}, 100);
} catch (error) {
console.error('Error processing OAuth login:', error);
hasProcessed.current = true;
setTimeout(() => {
navigate('/login?error=invalid_data', { replace: true });
}, 100);
}
} else {
console.log('Missing token or user data');
hasProcessed.current = true;
setTimeout(() => {
navigate('/login?error=missing_data', { replace: true });
}, 100);
}
}, []); // Remove dependencies to prevent re-runs
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="text-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-500 mx-auto"></div>
<p className="mt-4 text-gray-600">Finalisation de la connexion...</p>
</div>
</div>
);
}

3
frontend/src/pages/Register.jsx

@ -4,6 +4,7 @@ import { useAuth } from '../contexts/AuthContext';
import Navbar from '../components/Navbar';
import EmailVerificationModal from '../modals/EmailVerificationModal';
import { verifyEmail } from '../services/user.service';
import GitHubLoginButton from '../components/GitHubLoginButton';
export default function Register() {
const [formData, setFormData] = useState({
@ -236,6 +237,8 @@ export default function Register() {
</button>
</form>
<GitHubLoginButton className="mt-4 cursor-pointer" />
<div className="mt-6 text-center">
<p className="text-gray-600">
Déjà un compte ?{' '}

7
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: <Home/> },
@ -88,6 +89,12 @@ const routes = [
<Playlist/>
</ProtectedRoute>
)
},
{
path: "/login/success",
element: (
<LoginSuccess/>
)
}
]

36
frontend/src/services/oauthService.js

@ -0,0 +1,36 @@
const API_BASE_URL = import.meta.env.VITE_API_URL || 'https://localhost/api';
export const oauthService = {
// Start GitHub OAuth flow
loginWithGitHub() {
window.location.href = `${API_BASE_URL}/oauth/github`;
},
// Get current user info using token
async getCurrentUser(token) {
const response = await fetch(`${API_BASE_URL}/oauth/me`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error('Failed to get user info');
}
return response.json();
},
// Verify if token is still valid
async verifyToken(token) {
try {
const userInfo = await this.getCurrentUser(token);
return userInfo;
} catch (error) {
return null;
}
}
};
export default oauthService;
Loading…
Cancel
Save