/** * Authentication Routes * * Handles login, logout, token refresh, and user info endpoints */ import { Router } from 'express'; import { createToken, verifyToken, decodeToken, getTokenExpiresIn } from '../utils/jwt-utils.js'; export function createAuthRouter({ secret, adminPassword } = {}) { const router = Router(); /** * POST /auth/login * Login with admin password to receive JWT token */ router.post('/login', (req, res) => { const { password } = req.body; // Validate input if (!password) { return res.status(400).json({ error: 'Password is required', code: 'MISSING_PASSWORD' }); } // Validate password if (password !== adminPassword) { return res.status(401).json({ error: 'Invalid password', code: 'INVALID_PASSWORD' }); } try { // Create token with admin permissions const token = createToken( { isAdmin: true, permissions: ['admin', 'gamemaster-edit'], loginTime: new Date().toISOString() }, secret, 7 * 24 * 60 * 60 // 7 days ); res.json({ success: true, token, expiresIn: 7 * 24 * 60 * 60, user: { isAdmin: true, permissions: ['admin', 'gamemaster-edit'] } }); } catch (err) { res.status(500).json({ error: 'Failed to create token', code: 'TOKEN_CREATION_ERROR' }); } }); /** * POST /auth/verify * Verify that a token is valid */ router.post('/verify', (req, res) => { const { token } = req.body; if (!token) { return res.status(400).json({ error: 'Token is required', code: 'MISSING_TOKEN' }); } try { const decoded = verifyToken(token, secret); const expiresIn = getTokenExpiresIn(token); res.json({ valid: true, user: { isAdmin: decoded.isAdmin, permissions: decoded.permissions }, expiresIn: Math.floor(expiresIn / 1000), expiresAt: new Date(Date.now() + expiresIn) }); } catch (err) { return res.status(401).json({ valid: false, error: err.message, code: 'INVALID_TOKEN' }); } }); /** * POST /auth/refresh * Refresh an existing token */ router.post('/refresh', (req, res) => { const { token } = req.body; if (!token) { return res.status(400).json({ error: 'Token is required', code: 'MISSING_TOKEN' }); } try { const decoded = verifyToken(token, secret); // Create new token with same payload but extended expiration const newToken = createToken( { isAdmin: decoded.isAdmin, permissions: decoded.permissions, loginTime: decoded.loginTime }, secret, 7 * 24 * 60 * 60 // 7 days ); res.json({ success: true, token: newToken, expiresIn: 7 * 24 * 60 * 60 }); } catch (err) { return res.status(401).json({ error: err.message, code: 'INVALID_TOKEN' }); } }); /** * GET /auth/user * Get current user info (requires valid token via middleware) */ router.get('/user', (req, res) => { if (!req.user) { return res.status(401).json({ error: 'Not authenticated', code: 'NOT_AUTHENTICATED' }); } res.json({ user: { isAdmin: req.user.isAdmin, permissions: req.user.permissions, loginTime: req.user.loginTime } }); }); /** * POST /auth/logout * Logout (token is invalidated on client side) */ router.post('/logout', (req, res) => { res.json({ success: true, message: 'Logged out successfully' }); }); /** * POST /oauth/token * Exchange OAuth authorization code for access token * Supports multiple providers (Discord, Challonge, etc.) */ router.post('/oauth/token', async (req, res) => { const { code, provider } = req.body; if (!code) { return res.status(400).json({ error: 'Authorization code is required', code: 'MISSING_CODE' }); } if (!provider) { return res.status(400).json({ error: 'Provider is required', code: 'MISSING_PROVIDER' }); } try { // Handle Discord OAuth if (provider === 'discord') { const clientId = process.env.VITE_DISCORD_CLIENT_ID; const clientSecret = process.env.DISCORD_CLIENT_SECRET; const redirectUri = process.env.DISCORD_REDIRECT_URI || process.env.VITE_DISCORD_REDIRECT_URI; console.error('Discord OAuth not configured:', { hasClientId: !!clientId, hasClientSecret: !!clientSecret }); return res.status(500).json({ error: 'Discord OAuth not configured on server', code: 'OAUTH_NOT_CONFIGURED' }); } // Exchange code for token with Discord const tokenResponse = await fetch( 'https://discord.com/api/oauth2/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ client_id: clientId, client_secret: clientSecret, grant_type: 'authorization_code', code: code, redirect_uri: redirectUri }) } ); if (!tokenResponse.ok) { const errorData = await tokenResponse.text(); console.error('Discord token exchange failed:', errorData); return res.status(tokenResponse.status).json({ error: 'Failed to exchange code with Discord', code: 'DISCORD_TOKEN_EXCHANGE_FAILED', details: errorData }); } const tokenData = await tokenResponse.json(); // Return tokens to client return res.json({ access_token: tokenData.access_token, refresh_token: tokenData.refresh_token, token_type: tokenData.token_type, expires_in: tokenData.expires_in, scope: tokenData.scope }); } // Handle Challonge OAuth (if needed in the future) if (provider === 'challonge') { // Challonge uses the existing /api/oauth/token endpoint via oauth-proxy.js return res.status(400).json({ error: 'Use /api/oauth/token for Challonge OAuth', code: 'WRONG_ENDPOINT' }); } // Unknown provider return res.status(400).json({ error: `Unknown provider: ${provider}`, code: 'UNKNOWN_PROVIDER' }); } catch (err) { console.error('OAuth token exchange error:', err); return res.status(500).json({ error: err.message || 'Failed to exchange OAuth code', code: 'TOKEN_EXCHANGE_ERROR' }); } }); return router; }