101 lines
3.3 KiB
JavaScript
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;
|