Browse Source

Added login github

features/oauth
astria 4 months ago
parent
commit
e3572e16f3
  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. 73
      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. 59
      frontend/src/pages/LoginSuccess.jsx
  13. 3
      frontend/src/pages/Register.jsx
  14. 7
      frontend/src/routes/routes.jsx
  15. 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 { try {
let query = `CREATE TABLE IF NOT EXISTS users ( let query = `CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL, email VARCHAR(255),
username VARCHAR(255) NOT NULL, username VARCHAR(255) NOT NULL,
password VARCHAR(255) NOT NULL, password VARCHAR(255),
picture 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); await client.query(query);
@ -126,8 +129,21 @@ export async function initDb() {
)`; )`;
await client.query(query); 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) { } catch (e) {
console.error("Error initializing database:", e); console.error("Error initializing database:", e);
} finally {
client.end();
} }
} }

73
backend/logs/access.log

@ -8357,3 +8357,76 @@
[2025-08-20 19:34:43.088] [undefined] PUT(/:id/tags): try to add tags to video 20 [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.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-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

165
backend/package-lock.json

@ -14,11 +14,14 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^17.0.1", "dotenv": "^17.0.1",
"express": "^5.1.0", "express": "^5.1.0",
"express-session": "^1.18.2",
"express-validator": "^7.2.1", "express-validator": "^7.2.1",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"moment": "^2.30.1", "moment": "^2.30.1",
"multer": "^2.0.1", "multer": "^2.0.1",
"nodemailer": "^7.0.5", "nodemailer": "^7.0.5",
"passport": "^0.7.0",
"passport-github2": "^0.1.12",
"pg": "^8.16.3" "pg": "^8.16.3"
}, },
"devDependencies": { "devDependencies": {
@ -1069,6 +1072,15 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/bcrypt": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
@ -1925,6 +1937,46 @@
"url": "https://opencollective.com/express" "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": { "node_modules/express-validator": {
"version": "7.2.1", "version": "7.2.1",
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.1.tgz", "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.1.tgz",
@ -3153,6 +3205,12 @@
"node": ">=0.10.0" "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": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@ -3186,6 +3244,15 @@
"node": ">= 0.8" "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": { "node_modules/once": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@ -3243,6 +3310,63 @@
"node": ">= 0.8" "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": { "node_modules/path-exists": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@ -3306,6 +3430,11 @@
"node": ">= 14.16" "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": { "node_modules/pg": {
"version": "8.16.3", "version": "8.16.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
@ -3534,6 +3663,15 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/randombytes": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@ -4292,6 +4430,24 @@
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"license": "MIT" "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": { "node_modules/undefsafe": {
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
@ -4321,6 +4477,15 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT" "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": { "node_modules/validator": {
"version": "13.12.0", "version": "13.12.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz",

3
backend/package.json

@ -18,11 +18,14 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^17.0.1", "dotenv": "^17.0.1",
"express": "^5.1.0", "express": "^5.1.0",
"express-session": "^1.18.2",
"express-validator": "^7.2.1", "express-validator": "^7.2.1",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"moment": "^2.30.1", "moment": "^2.30.1",
"multer": "^2.0.1", "multer": "^2.0.1",
"nodemailer": "^7.0.5", "nodemailer": "^7.0.5",
"passport": "^0.7.0",
"passport-github2": "^0.1.12",
"pg": "^8.16.3" "pg": "^8.16.3"
}, },
"devDependencies": { "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 {initDb} from "./app/utils/database.js";
import MediaRoutes from "./app/routes/media.routes.js"; import MediaRoutes from "./app/routes/media.routes.js";
import SearchRoute from "./app/routes/search.route.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(); console.clear();
dotenv.config(); dotenv.config();
@ -23,6 +27,39 @@ app.use(express.urlencoded({extended: true, limit: '500mb'}));
app.use(express.json({limit: '500mb'})); app.use(express.json({limit: '500mb'}));
app.use(cors()) 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 // ROUTES
app.use("/api/users/", UserRoute); app.use("/api/users/", UserRoute);
app.use("/api/channels/", ChannelRoute); app.use("/api/channels/", ChannelRoute);
@ -32,6 +69,7 @@ app.use("/api/playlists", PlaylistRoute);
app.use("/api/recommendations", RecommendationRoute); app.use("/api/recommendations", RecommendationRoute);
app.use("/api/media", MediaRoutes); app.use("/api/media", MediaRoutes);
app.use("/api/search", SearchRoute); app.use("/api/search", SearchRoute);
app.use("/api/oauth", OAuthRoute);
const port = process.env.PORT; const port = process.env.PORT;

2
docker-compose.yaml

@ -18,6 +18,8 @@ services:
PORT: ${BACKEND_PORT} PORT: ${BACKEND_PORT}
GMAIL_USER: ${GMAIL_USER} GMAIL_USER: ${GMAIL_USER}
GMAIL_PASSWORD: ${GMAIL_PASSWORD} GMAIL_PASSWORD: ${GMAIL_PASSWORD}
GITHUB_ID: ${GITHUB_ID}
GITHUB_SECRET: ${GITHUB_SECRET}
volumes: volumes:
- ./backend/logs:/var/log/freetube - ./backend/logs:/var/log/freetube
- ./backend:/app - ./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(); const AuthContext = createContext();
@ -25,7 +25,7 @@ export const AuthProvider = ({ children }) => {
setLoading(false); setLoading(false);
}, []); }, []);
const login = async (username, password) => { const login = useCallback(async (username, password) => {
try { try {
const response = await fetch('/api/users/login', { const response = await fetch('/api/users/login', {
method: 'POST', method: 'POST',
@ -50,9 +50,9 @@ export const AuthProvider = ({ children }) => {
} catch (error) { } catch (error) {
throw error; throw error;
} }
}; }, []);
const register = async (email, username, password, profileImage) => { const register = useCallback(async (email, username, password, profileImage) => {
try { try {
const formData = new FormData(); const formData = new FormData();
formData.append('email', email); formData.append('email', email);
@ -81,22 +81,31 @@ export const AuthProvider = ({ children }) => {
} catch (error) { } catch (error) {
throw 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('token');
localStorage.removeItem('user'); localStorage.removeItem('user');
setUser(null); setUser(null);
}; }, []);
const getAuthHeaders = () => { const getAuthHeaders = useCallback(() => {
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
return token ? { Authorization: `Bearer ${token}` } : {}; return token ? { Authorization: `Bearer ${token}` } : {};
}; }, []);
const value = { const value = {
user, user,
login, login,
loginWithOAuth,
register, register,
logout, logout,
getAuthHeaders, getAuthHeaders,

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 Navbar from '../components/Navbar';
import EmailVerificationModal from '../modals/EmailVerificationModal'; import EmailVerificationModal from '../modals/EmailVerificationModal';
import { verifyEmail } from '../services/user.service'; import { verifyEmail } from '../services/user.service';
import GitHubLoginButton from '../components/GitHubLoginButton';
export default function Register() { export default function Register() {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
@ -236,6 +237,8 @@ export default function Register() {
</button> </button>
</form> </form>
<GitHubLoginButton/>
<div className="mt-6 text-center"> <div className="mt-6 text-center">
<p className="text-gray-600"> <p className="text-gray-600">
Déjà un compte ?{' '} 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 Search from "../pages/Search.jsx";
import Channel from "../pages/Channel.jsx"; import Channel from "../pages/Channel.jsx";
import Playlist from "../pages/Playlist.jsx"; import Playlist from "../pages/Playlist.jsx";
import LoginSuccess from '../pages/LoginSuccess.jsx'
const routes = [ const routes = [
{ path: "/", element: <Home/> }, { path: "/", element: <Home/> },
@ -88,6 +89,12 @@ const routes = [
<Playlist/> <Playlist/>
</ProtectedRoute> </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