← back to Watches
api/auth/github.js
280 lines
/**
* GitHub OAuth Authentication Handler
* Implements GitHub Sign-In for the Omega Watch Platform
*/
import { v4 as uuidv4 } from 'uuid';
import pool from '../../database/connection.js';
// GitHub OAuth configuration
const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID;
const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET;
const GITHUB_REDIRECT_URI = process.env.GITHUB_REDIRECT_URI || 'http://45.61.58.125:7600/api/auth/github/callback';
// GitHub OAuth URLs
const GITHUB_AUTH_URL = 'https://github.com/login/oauth/authorize';
const GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token';
const GITHUB_USER_URL = 'https://api.github.com/user';
const GITHUB_EMAILS_URL = 'https://api.github.com/user/emails';
/**
* Generate GitHub OAuth authorization URL
* @param {string} state - CSRF protection state token
* @returns {string} Authorization URL
*/
export function getGitHubAuthUrl(state) {
const params = new URLSearchParams({
client_id: GITHUB_CLIENT_ID,
redirect_uri: GITHUB_REDIRECT_URI,
scope: 'read:user user:email',
state: state,
allow_signup: 'true'
});
return `${GITHUB_AUTH_URL}?${params.toString()}`;
}
/**
* Exchange authorization code for access token
* @param {string} code - Authorization code from GitHub
* @returns {string} Access token
*/
export async function exchangeCodeForToken(code) {
const response = await fetch(GITHUB_TOKEN_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
client_id: GITHUB_CLIENT_ID,
client_secret: GITHUB_CLIENT_SECRET,
code: code,
redirect_uri: GITHUB_REDIRECT_URI
})
});
if (!response.ok) {
throw new Error('Failed to exchange GitHub code');
}
const data = await response.json();
if (data.error) {
throw new Error(data.error_description || data.error);
}
return data.access_token;
}
/**
* Get user info from GitHub
* @param {string} accessToken - GitHub access token
* @returns {object} User info
*/
export async function getGitHubUser(accessToken) {
const response = await fetch(GITHUB_USER_URL, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Accept': 'application/vnd.github.v3+json',
'User-Agent': 'OmegaWatchPlatform'
}
});
if (!response.ok) {
throw new Error('Failed to get GitHub user');
}
return response.json();
}
/**
* Get user's primary email from GitHub
* @param {string} accessToken - GitHub access token
* @returns {string|null} Primary email or null
*/
export async function getGitHubEmail(accessToken) {
const response = await fetch(GITHUB_EMAILS_URL, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Accept': 'application/vnd.github.v3+json',
'User-Agent': 'OmegaWatchPlatform'
}
});
if (!response.ok) {
return null;
}
const emails = await response.json();
// Find primary verified email
const primary = emails.find(e => e.primary && e.verified);
if (primary) return primary.email;
// Fallback to any verified email
const verified = emails.find(e => e.verified);
if (verified) return verified.email;
// Last resort: any email
return emails[0]?.email || null;
}
/**
* Find or create user from GitHub profile
* @param {object} githubUser - GitHub user profile
* @param {string} email - User's email
* @returns {object} User record
*/
export async function findOrCreateGitHubUser(githubUser, email) {
const { id: githubId, login, name, avatar_url } = githubUser;
// First, try to find existing user by GitHub ID
let result = await pool.query(
`SELECT * FROM users WHERE provider = 'github' AND provider_id = $1`,
[githubId.toString()]
);
if (result.rows.length > 0) {
// Update last login and avatar
await pool.query(
`UPDATE users
SET last_login_at = NOW(), login_count = login_count + 1, avatar_url = $1
WHERE id = $2`,
[avatar_url, result.rows[0].id]
);
return result.rows[0];
}
// Check if email already exists
if (email) {
result = await pool.query(
`SELECT * FROM users WHERE email = $1`,
[email]
);
if (result.rows.length > 0) {
// Link GitHub account to existing user
const existingUser = result.rows[0];
await pool.query(
`UPDATE users
SET provider = 'github', provider_id = $1, avatar_url = $2,
last_login_at = NOW(), login_count = login_count + 1,
email_verified = true
WHERE id = $3`,
[githubId.toString(), avatar_url, existingUser.id]
);
return { ...existingUser, provider: 'github', provider_id: githubId.toString() };
}
}
// Create new user
const userName = name || login;
const userEmail = email || `${login}@github.local`; // Fallback if no email
const newUser = await pool.query(
`INSERT INTO users (email, name, avatar_url, provider, provider_id, email_verified)
VALUES ($1, $2, $3, 'github', $4, $5)
RETURNING *`,
[userEmail, userName, avatar_url, githubId.toString(), !!email]
);
return newUser.rows[0];
}
/**
* Create session for user
* @param {object} user - User record
* @param {object} req - Express request
* @returns {object} Session tokens
*/
export async function createSession(user, req) {
const sessionToken = uuidv4();
const refreshToken = uuidv4();
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
const refreshExpiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
const userAgent = req.headers['user-agent'] || '';
const ipAddress = req.ip || req.connection?.remoteAddress;
await pool.query(
`INSERT INTO user_sessions
(user_id, session_token, refresh_token, user_agent, ip_address, device_type, expires_at, refresh_expires_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
[user.id, sessionToken, refreshToken, userAgent, ipAddress, 'desktop', expiresAt, refreshExpiresAt]
);
return { sessionToken, refreshToken, expiresAt };
}
/**
* Express route handler for initiating GitHub OAuth
*/
export function initiateGitHubAuth(req, res) {
const state = uuidv4();
res.cookie('oauth_state', state, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 10 * 60 * 1000
});
const authUrl = getGitHubAuthUrl(state);
res.redirect(authUrl);
}
/**
* Express route handler for GitHub OAuth callback
*/
export async function handleGitHubCallback(req, res) {
try {
const { code, state } = req.query;
const storedState = req.cookies?.oauth_state;
if (!state || state !== storedState) {
return res.status(400).json({ error: 'Invalid state parameter' });
}
res.clearCookie('oauth_state');
// Exchange code for token
const accessToken = await exchangeCodeForToken(code);
// Get user info
const githubUser = await getGitHubUser(accessToken);
// Get email (might be private)
const email = githubUser.email || await getGitHubEmail(accessToken);
// Find or create user
const user = await findOrCreateGitHubUser(githubUser, email);
// Create session
const session = await createSession(user, req);
res.cookie('session_token', session.sessionToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 7 * 24 * 60 * 60 * 1000,
sameSite: 'lax'
});
res.redirect('/?auth=success');
} catch (error) {
console.error('GitHub OAuth error:', error);
res.redirect('/?auth=error&message=' + encodeURIComponent(error.message));
}
}
export default {
getGitHubAuthUrl,
exchangeCodeForToken,
getGitHubUser,
getGitHubEmail,
findOrCreateGitHubUser,
createSession,
initiateGitHubAuth,
handleGitHubCallback
};