feat: add Google and GitHub OAuth login endpoints
This commit is contained in:
parent
1f5ae79f11
commit
09ebd18b1e
132
server.js
132
server.js
|
|
@ -782,6 +782,138 @@ function buildRouter() {
|
||||||
res.json({ ok: true, userId: req.userId, authType: req.authType });
|
res.json({ ok: true, userId: req.userId, authType: req.authType });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Auth: Google OAuth ───────────────────────────────────────────
|
||||||
|
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID || '';
|
||||||
|
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET || '';
|
||||||
|
const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID || '';
|
||||||
|
const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET || '';
|
||||||
|
const OAUTH_BASE_URL = process.env.MAGIC_LINK_BASE_URL || 'http://localhost:3000';
|
||||||
|
|
||||||
|
// GET /api/auth/oauth/google — redirect to Google
|
||||||
|
r.get('/auth/oauth/google', (req, res) => {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
client_id: GOOGLE_CLIENT_ID,
|
||||||
|
redirect_uri: `${process.env.API_PUBLIC_URL || 'https://api.soar-enrich.com'}/brain/api/auth/oauth/google/callback`,
|
||||||
|
response_type: 'code',
|
||||||
|
scope: 'openid email profile',
|
||||||
|
access_type: 'offline',
|
||||||
|
prompt: 'select_account',
|
||||||
|
});
|
||||||
|
res.redirect(`https://accounts.google.com/o/oauth2/v2/auth?${params}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/auth/oauth/google/callback
|
||||||
|
r.get('/auth/oauth/google/callback', async (req, res) => {
|
||||||
|
const { code } = req.query;
|
||||||
|
if (!code) return res.redirect(`${OAUTH_BASE_URL}/login?error=no_code`);
|
||||||
|
try {
|
||||||
|
// Exchange code for tokens
|
||||||
|
const tokenRes = await fetch('https://oauth2.googleapis.com/token', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: new URLSearchParams({
|
||||||
|
code,
|
||||||
|
client_id: GOOGLE_CLIENT_ID,
|
||||||
|
client_secret: GOOGLE_CLIENT_SECRET,
|
||||||
|
redirect_uri: `${process.env.API_PUBLIC_URL || 'https://api.soar-enrich.com'}/brain/api/auth/oauth/google/callback`,
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const tokenData = await tokenRes.json();
|
||||||
|
if (!tokenData.access_token) throw new Error('No access token');
|
||||||
|
|
||||||
|
// Get user info
|
||||||
|
const userRes = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
|
||||||
|
headers: { Authorization: `Bearer ${tokenData.access_token}` },
|
||||||
|
});
|
||||||
|
const userInfo = await userRes.json();
|
||||||
|
const email = userInfo.email?.toLowerCase();
|
||||||
|
if (!email) throw new Error('No email from Google');
|
||||||
|
|
||||||
|
// Find or create user
|
||||||
|
const existing = await pool.query(
|
||||||
|
`SELECT id FROM users WHERE email = $1`, [email]
|
||||||
|
);
|
||||||
|
let userId;
|
||||||
|
if (existing.rows.length > 0) {
|
||||||
|
userId = existing.rows[0].id;
|
||||||
|
} else {
|
||||||
|
const newUser = await pool.query(
|
||||||
|
`INSERT INTO users (email, created_at) VALUES ($1, NOW()) RETURNING id`, [email]
|
||||||
|
);
|
||||||
|
userId = newUser.rows[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = await createSessionJWT(userId);
|
||||||
|
res.redirect(`${OAUTH_BASE_URL}/auth/verify?token=${token}&type=oauth`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[OAuth Google]', e);
|
||||||
|
res.redirect(`${OAUTH_BASE_URL}/login?error=google_failed`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Auth: GitHub OAuth ───────────────────────────────────────────
|
||||||
|
|
||||||
|
// GET /api/auth/oauth/github — redirect to GitHub
|
||||||
|
r.get('/auth/oauth/github', (req, res) => {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
client_id: GITHUB_CLIENT_ID,
|
||||||
|
redirect_uri: `${process.env.API_PUBLIC_URL || 'https://api.soar-enrich.com'}/brain/api/auth/oauth/github/callback`,
|
||||||
|
scope: 'user:email',
|
||||||
|
});
|
||||||
|
res.redirect(`https://github.com/login/oauth/authorize?${params}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/auth/oauth/github/callback
|
||||||
|
r.get('/auth/oauth/github/callback', async (req, res) => {
|
||||||
|
const { code } = req.query;
|
||||||
|
if (!code) return res.redirect(`${OAUTH_BASE_URL}/login?error=no_code`);
|
||||||
|
try {
|
||||||
|
// Exchange code for token
|
||||||
|
const tokenRes = await fetch('https://github.com/login/oauth/access_token', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
client_id: GITHUB_CLIENT_ID,
|
||||||
|
client_secret: GITHUB_CLIENT_SECRET,
|
||||||
|
code,
|
||||||
|
redirect_uri: `${process.env.API_PUBLIC_URL || 'https://api.soar-enrich.com'}/brain/api/auth/oauth/github/callback`,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const tokenData = await tokenRes.json();
|
||||||
|
if (!tokenData.access_token) throw new Error('No access token');
|
||||||
|
|
||||||
|
// Get user emails
|
||||||
|
const emailRes = await fetch('https://api.github.com/user/emails', {
|
||||||
|
headers: { Authorization: `Bearer ${tokenData.access_token}`, 'User-Agent': 'Posimai' },
|
||||||
|
});
|
||||||
|
const emails = await emailRes.json();
|
||||||
|
const primary = emails.find((e) => e.primary && e.verified);
|
||||||
|
const email = primary?.email?.toLowerCase();
|
||||||
|
if (!email) throw new Error('No verified email from GitHub');
|
||||||
|
|
||||||
|
// Find or create user
|
||||||
|
const existing = await pool.query(
|
||||||
|
`SELECT id FROM users WHERE email = $1`, [email]
|
||||||
|
);
|
||||||
|
let userId;
|
||||||
|
if (existing.rows.length > 0) {
|
||||||
|
userId = existing.rows[0].id;
|
||||||
|
} else {
|
||||||
|
const newUser = await pool.query(
|
||||||
|
`INSERT INTO users (email, created_at) VALUES ($1, NOW()) RETURNING id`, [email]
|
||||||
|
);
|
||||||
|
userId = newUser.rows[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = await createSessionJWT(userId);
|
||||||
|
res.redirect(`${OAUTH_BASE_URL}/auth/verify?token=${token}&type=oauth`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[OAuth GitHub]', e);
|
||||||
|
res.redirect(`${OAUTH_BASE_URL}/login?error=github_failed`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// DELETE /api/auth/session — logout (revoke session in DB)
|
// DELETE /api/auth/session — logout (revoke session in DB)
|
||||||
r.delete('/auth/session', authMiddleware, async (req, res) => {
|
r.delete('/auth/session', authMiddleware, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue