git/routes/discord.js

101 lines
3.3 KiB
JavaScript

import { Router } from 'express';
import { v4 as uuidv4 } from 'uuid';
import db from '../db.js';
import { generateToken } from '../middleware/auth.js';
const router = Router();
const DISCORD_API = 'https://discord.com/api';
const DISCORD_CLIENT_ID = process.env.DISCORD_CLIENT_ID;
const DISCORD_CLIENT_SECRET = process.env.DISCORD_CLIENT_SECRET;
const DISCORD_REDIRECT_URI = process.env.DISCORD_REDIRECT_URI;
const WHITELIST = new Set([
'1207017997173137481'
]);
function getFrontendUrl() {
return process.env.FRONTEND_URL || 'http://localhost:3000';
}
router.get('/discord', (req, res) => {
const next = req.query.next || '/';
const safeNext = next.startsWith('/') && !next.startsWith('//') ? next : '/';
const params = new URLSearchParams({
client_id: DISCORD_CLIENT_ID,
redirect_uri: DISCORD_REDIRECT_URI,
response_type: 'code',
scope: 'identify',
state: safeNext,
});
res.redirect(`${DISCORD_API}/oauth2/authorize?${params.toString()}`);
});
router.get('/discord/callback', async (req, res) => {
const { code, state, error } = req.query;
if (error) {
return res.redirect(`${getFrontendUrl()}/login?error=${encodeURIComponent(error)}`);
}
if (!code || !state) {
return res.redirect(`${getFrontendUrl()}/login?error=missing_code_or_state`);
}
try {
const tokenRes = await fetch(`${DISCORD_API}/oauth2/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: DISCORD_CLIENT_ID,
client_secret: DISCORD_CLIENT_SECRET,
grant_type: 'authorization_code',
code,
redirect_uri: DISCORD_REDIRECT_URI,
}).toString(),
});
if (!tokenRes.ok) {
const text = await tokenRes.text();
throw new Error(`Discord token exchange failed: ${tokenRes.status} ${text}`);
}
const token = await tokenRes.json();
const userRes = await fetch(`${DISCORD_API}/users/@me`, {
headers: { Authorization: `Bearer ${token.access_token}` },
});
if (!userRes.ok) {
const text = await userRes.text();
throw new Error(`Discord user fetch failed: ${userRes.status} ${text}`);
}
const discordUser = await userRes.json();
if (!WHITELIST.has(discordUser.id)) {
return res.redirect(`${getFrontendUrl()}/login?error=not_allowed`);
}
const existing = db.prepare('SELECT id FROM users WHERE discord_id = ?').get(discordUser.id);
let userId;
if (existing) {
userId = existing.id;
db.prepare(
"UPDATE users SET username = ?, global_name = ?, avatar = ?, updated_at = datetime('now') WHERE id = ?"
).run(discordUser.username, discordUser.global_name, discordUser.avatar, userId);
} else {
userId = uuidv4();
db.prepare(
'INSERT INTO users (id, username, global_name, avatar, discord_id) VALUES (?, ?, ?, ?, ?)'
).run(userId, discordUser.username, discordUser.global_name, discordUser.avatar, discordUser.id);
}
const jwt = generateToken(userId);
const redirectTo = state.startsWith('/') ? state : '/';
res.redirect(`${getFrontendUrl()}${redirectTo}?token=${jwt}`);
} catch (e) {
const msg = e instanceof Error ? e.message : 'unknown error';
res.redirect(`${getFrontendUrl()}/login?error=${encodeURIComponent(msg.slice(0, 200))}`);
}
});
export default router;