diff --git a/code/websites/pokedex.online/server/routes/auth.js b/code/websites/pokedex.online/server/routes/auth.js new file mode 100644 index 0000000..143d0c1 --- /dev/null +++ b/code/websites/pokedex.online/server/routes/auth.js @@ -0,0 +1,175 @@ +/** + * 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' + }); + }); + + return router; +}