Files
memory-infrastructure-palace/code/websites/pokedex.online/server/routes/auth.js

341 lines
8.6 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.DISCORD_REDIRECT_URI ||
process.env.VITE_DISCORD_REDIRECT_URI;
if (!clientId || !clientSecret || !redirectUri) {
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'
});
}
});
/**
* GET /auth/discord/profile
* Fetch Discord user profile using the stored Discord token
* Requires: Authorization header with Discord access token
*/
router.get('/discord/profile', async (req, res) => {
try {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
error: 'Missing or invalid authorization header',
code: 'MISSING_AUTH'
});
}
const token = authHeader.substring('Bearer '.length);
// Fetch user profile from Discord API
const response = await fetch('https://discord.com/api/users/@me', {
headers: {
Authorization: `Bearer ${token}`
}
});
if (!response.ok) {
console.error('Discord API error:', response.status);
return res.status(response.status).json({
error: 'Failed to fetch Discord profile',
code: 'DISCORD_API_ERROR'
});
}
const userData = await response.json();
// Return user data
res.json({
user: {
id: userData.id,
username: userData.username,
global_name: userData.global_name,
discriminator: userData.discriminator,
avatar: userData.avatar,
email: userData.email,
verified: userData.verified
}
});
} catch (err) {
console.error('Failed to fetch Discord profile:', err);
return res.status(500).json({
error: 'Failed to fetch Discord profile',
code: 'PROFILE_FETCH_ERROR'
});
}
});
return router;
}