features/oauth #6
Merged
astria
merged 2 commits from features/oauth into developpement 4 months ago
16 changed files with 633 additions and 12 deletions
@ -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" }); |
|||
} |
|||
} |
|||
@ -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; |
|||
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 19 MiB |
@ -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> |
|||
); |
|||
} |
|||
@ -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> |
|||
); |
|||
} |
|||
@ -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…
Reference in new issue