245 lines
7.2 KiB
JavaScript
245 lines
7.2 KiB
JavaScript
import { Router } from 'express';
|
|
import jwt from 'jsonwebtoken';
|
|
import { db, VALID_ROLES, initDiscordUsers } from '../db/init.js';
|
|
import { authenticateToken, requireRole } from '../middleware/auth.js';
|
|
import { getDiscordAuthUrl, exchangeCode, getDiscordUser, getGuildMember, getGuildMemberships, getUserRole, getUserRoleFromMemberships } from '../services/discord.js';
|
|
|
|
const router = Router();
|
|
|
|
// Initialize Discord users table
|
|
initDiscordUsers();
|
|
|
|
// ===== Guest Login =====
|
|
|
|
// Create guest token (view-only, expires in 24h)
|
|
router.post('/guest', (req, res) => {
|
|
const guestId = 'guest_' + Date.now() + '_' + Math.random().toString(36).substring(2, 9);
|
|
|
|
const token = jwt.sign(
|
|
{
|
|
id: guestId,
|
|
username: 'Gast',
|
|
role: 'guest',
|
|
isGuest: true
|
|
},
|
|
process.env.JWT_SECRET,
|
|
{ expiresIn: '24h' }
|
|
);
|
|
|
|
res.json({ token });
|
|
});
|
|
|
|
// ===== Discord OAuth2 =====
|
|
|
|
// Start Discord OAuth2 flow
|
|
router.get('/discord', (req, res) => {
|
|
res.redirect(getDiscordAuthUrl());
|
|
});
|
|
|
|
// Discord OAuth2 callback
|
|
router.get('/discord/callback', async (req, res) => {
|
|
const { code, error } = req.query;
|
|
|
|
// Redirect URL for frontend
|
|
const frontendUrl = process.env.FRONTEND_URL || 'https://gsm.dimension47.de';
|
|
|
|
if (error) {
|
|
return res.redirect(`${frontendUrl}/login?error=discord_denied`);
|
|
}
|
|
|
|
if (!code) {
|
|
return res.redirect(`${frontendUrl}/login?error=no_code`);
|
|
}
|
|
|
|
try {
|
|
// Exchange code for access token
|
|
const tokenData = await exchangeCode(code);
|
|
|
|
// Get Discord user info
|
|
const discordUser = await getDiscordUser(tokenData.access_token);
|
|
|
|
// Check if user is in any of the configured guilds
|
|
const memberships = await getGuildMemberships(discordUser.id);
|
|
|
|
if (!memberships) {
|
|
return res.redirect(`${frontendUrl}/login?error=not_in_guild`);
|
|
}
|
|
|
|
// Determine role based on Discord roles (highest role from all servers)
|
|
const role = getUserRoleFromMemberships(memberships);
|
|
|
|
// Use first membership for display name
|
|
const member = memberships[0].member;
|
|
|
|
// Get display name (nickname or username)
|
|
const displayName = member.nick || discordUser.global_name || discordUser.username;
|
|
|
|
// Upsert user in database
|
|
const existingUser = db.prepare('SELECT * FROM discord_users WHERE discord_id = ?').get(discordUser.id);
|
|
|
|
let userId;
|
|
if (existingUser) {
|
|
// Update existing user
|
|
db.prepare(`
|
|
UPDATE discord_users
|
|
SET username = ?, discriminator = ?, avatar = ?, role = ?, updated_at = CURRENT_TIMESTAMP
|
|
WHERE discord_id = ?
|
|
`).run(displayName, discordUser.discriminator || '0', discordUser.avatar, role, discordUser.id);
|
|
userId = existingUser.id;
|
|
} else {
|
|
// Create new user
|
|
const result = db.prepare(`
|
|
INSERT INTO discord_users (discord_id, username, discriminator, avatar, role)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
`).run(discordUser.id, displayName, discordUser.discriminator || '0', discordUser.avatar, role);
|
|
userId = result.lastInsertRowid;
|
|
}
|
|
|
|
// Create JWT token
|
|
const token = jwt.sign(
|
|
{
|
|
id: userId,
|
|
discordId: discordUser.id,
|
|
username: displayName,
|
|
role,
|
|
avatar: discordUser.avatar
|
|
},
|
|
process.env.JWT_SECRET,
|
|
{ expiresIn: '7d' }
|
|
);
|
|
|
|
// Redirect to frontend with token
|
|
res.redirect(`${frontendUrl}/auth/callback?token=${token}`);
|
|
|
|
} catch (err) {
|
|
console.error('Discord OAuth error:', err);
|
|
res.redirect(`${frontendUrl}/login?error=oauth_failed`);
|
|
}
|
|
});
|
|
|
|
// Get current user info
|
|
router.get('/me', authenticateToken, (req, res) => {
|
|
// Check if it's a guest user
|
|
if (req.user.isGuest) {
|
|
return res.json({
|
|
id: req.user.id,
|
|
username: req.user.username,
|
|
role: req.user.role,
|
|
isGuest: true
|
|
});
|
|
}
|
|
|
|
// Check if it's a Discord user
|
|
if (req.user.discordId) {
|
|
const user = db.prepare('SELECT id, discord_id, username, avatar, role FROM discord_users WHERE id = ?').get(req.user.id);
|
|
if (!user) {
|
|
return res.status(404).json({ error: 'User not found' });
|
|
}
|
|
return res.json({
|
|
id: user.id,
|
|
discordId: user.discord_id,
|
|
username: user.username,
|
|
avatar: user.avatar,
|
|
role: user.role
|
|
});
|
|
}
|
|
|
|
// Fallback for old users (shouldn't happen after migration)
|
|
const user = db.prepare('SELECT id, username, role FROM users WHERE id = ?').get(req.user.id);
|
|
if (!user) {
|
|
return res.status(404).json({ error: 'User not found' });
|
|
}
|
|
res.json({ id: user.id, username: user.username, role: user.role });
|
|
});
|
|
|
|
// Refresh user role from Discord (useful if roles changed)
|
|
router.post('/refresh-role', authenticateToken, async (req, res) => {
|
|
if (!req.user.discordId) {
|
|
return res.status(400).json({ error: 'Not a Discord user' });
|
|
}
|
|
|
|
try {
|
|
const memberships = await getGuildMemberships(req.user.discordId);
|
|
|
|
if (!memberships) {
|
|
return res.status(403).json({ error: 'No longer in any guild' });
|
|
}
|
|
|
|
const newRole = getUserRoleFromMemberships(memberships);
|
|
|
|
db.prepare('UPDATE discord_users SET role = ?, updated_at = CURRENT_TIMESTAMP WHERE discord_id = ?')
|
|
.run(newRole, req.user.discordId);
|
|
|
|
// Generate new token with updated role
|
|
const token = jwt.sign(
|
|
{
|
|
id: req.user.id,
|
|
discordId: req.user.discordId,
|
|
username: req.user.username,
|
|
role: newRole,
|
|
avatar: req.user.avatar
|
|
},
|
|
process.env.JWT_SECRET,
|
|
{ expiresIn: '7d' }
|
|
);
|
|
|
|
res.json({ token, role: newRole });
|
|
} catch (err) {
|
|
console.error('Failed to refresh role:', err);
|
|
res.status(500).json({ error: 'Failed to refresh role' });
|
|
}
|
|
});
|
|
|
|
// ===== User Management (superadmin only) =====
|
|
|
|
// Get all Discord users
|
|
router.get('/users', authenticateToken, requireRole('superadmin'), (req, res) => {
|
|
const users = db.prepare(`
|
|
SELECT id, discord_id, username, avatar, role, created_at, updated_at
|
|
FROM discord_users
|
|
ORDER BY created_at DESC
|
|
`).all();
|
|
res.json(users);
|
|
});
|
|
|
|
// Update user role (override Discord role)
|
|
router.patch('/users/:id/role', authenticateToken, requireRole('superadmin'), (req, res) => {
|
|
const userId = parseInt(req.params.id);
|
|
const { role } = req.body;
|
|
|
|
if (!VALID_ROLES.includes(role)) {
|
|
return res.status(400).json({ error: 'Invalid role' });
|
|
}
|
|
|
|
if (userId === req.user.id) {
|
|
return res.status(400).json({ error: 'Cannot change your own role' });
|
|
}
|
|
|
|
const user = db.prepare('SELECT id FROM discord_users WHERE id = ?').get(userId);
|
|
if (!user) {
|
|
return res.status(404).json({ error: 'User not found' });
|
|
}
|
|
|
|
db.prepare('UPDATE discord_users SET role = ? WHERE id = ?').run(role, userId);
|
|
res.json({ message: 'Role updated' });
|
|
});
|
|
|
|
// Delete user
|
|
router.delete('/users/:id', authenticateToken, requireRole('superadmin'), (req, res) => {
|
|
const userId = parseInt(req.params.id);
|
|
|
|
if (userId === req.user.id) {
|
|
return res.status(400).json({ error: 'Cannot delete yourself' });
|
|
}
|
|
|
|
const user = db.prepare('SELECT id FROM discord_users WHERE id = ?').get(userId);
|
|
if (!user) {
|
|
return res.status(404).json({ error: 'User not found' });
|
|
}
|
|
|
|
db.prepare('DELETE FROM discord_users WHERE id = ?').run(userId);
|
|
res.json({ message: 'User deleted' });
|
|
});
|
|
|
|
export default router;
|