feat: add Google and GitHub OAuth login endpoints

This commit is contained in:
posimai 2026-04-04 17:25:26 +09:00
parent 1f5ae79f11
commit 09ebd18b1e
1 changed files with 132 additions and 0 deletions

132
server.js
View File

@ -782,6 +782,138 @@ function buildRouter() {
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)
r.delete('/auth/session', authMiddleware, async (req, res) => {
try {