284 lines
6.9 KiB
JavaScript
284 lines
6.9 KiB
JavaScript
/**
|
|
* 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.VITE_DISCORD_REDIRECT_URI;
|
|
|
|
if (!clientId || !clientSecret) {
|
|
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;
|
|
}
|