import { generateTokenPair } from "../utils/jwt.js"; import { authenticate, verifyRefreshTokenMiddleware, } from "../middleware/auth.js"; import { config } from "../config/index.js"; import Session from "../models/Session.js"; /** * Authentication routes * @param {FastifyInstance} fastify - Fastify instance */ export default async function authRoutes(fastify, options) { // Debug endpoint to check cookies and headers (development only) fastify.get("/debug-cookies", async (request, reply) => { if (config.isProduction) { return reply.status(404).send({ error: "Not found" }); } // Parse the raw cookie header manually to compare const rawCookieHeader = request.headers.cookie || ""; const manualParsedCookies = {}; if (rawCookieHeader) { rawCookieHeader.split(";").forEach((cookie) => { const [name, ...valueParts] = cookie.trim().split("="); if (name) { manualParsedCookies[name] = valueParts.join("="); } }); } return reply.send({ success: true, cookies: request.cookies || {}, manualParsedCookies: manualParsedCookies, rawCookieHeader: rawCookieHeader, headers: { authorization: request.headers.authorization || null, origin: request.headers.origin || null, referer: request.headers.referer || null, cookie: request.headers.cookie ? "Present" : "Missing", host: request.headers.host || null, }, hasAccessToken: !!request.cookies?.accessToken, hasRefreshToken: !!request.cookies?.refreshToken, manualHasAccessToken: !!manualParsedCookies.accessToken, manualHasRefreshToken: !!manualParsedCookies.refreshToken, config: { cookieDomain: config.cookie.domain, cookieSecure: config.cookie.secure, cookieSameSite: config.cookie.sameSite, corsOrigin: config.cors.origin, }, note: "If request.cookies is empty but manualParsedCookies has data, then @fastify/cookie isn't parsing correctly", }); }); // Test endpoint to verify Steam configuration fastify.get("/steam/test", async (request, reply) => { return reply.send({ success: true, steamConfig: { apiKeySet: !!config.steam.apiKey, realm: config.steam.realm, returnURL: config.steam.returnURL, }, message: "Steam authentication is configured. Try /auth/steam to login.", }); }); // Steam login - initiate OAuth flow fastify.get("/steam", async (request, reply) => { try { // Manually construct the Steam OpenID URL const returnURL = encodeURIComponent(config.steam.returnURL); const realm = encodeURIComponent(config.steam.realm); const steamOpenIDURL = `https://steamcommunity.com/openid/login?` + `openid.mode=checkid_setup&` + `openid.ns=http://specs.openid.net/auth/2.0&` + `openid.identity=http://specs.openid.net/auth/2.0/identifier_select&` + `openid.claimed_id=http://specs.openid.net/auth/2.0/identifier_select&` + `openid.return_to=${returnURL}&` + `openid.realm=${realm}`; return reply.redirect(steamOpenIDURL); } catch (error) { console.error("❌ Steam authentication error:", error); return reply.status(500).send({ error: "AuthenticationError", message: "Failed to initiate Steam login", details: error.message, }); } }); // Steam OAuth callback - Manual verification fastify.get("/steam/return", async (request, reply) => { try { const query = request.query; // Verify this is a valid OpenID response if (query["openid.mode"] !== "id_res") { return reply.status(400).send({ error: "AuthenticationError", message: "Invalid OpenID response mode", }); } // Verify the response with Steam const verifyParams = new URLSearchParams(); for (const key in query) { verifyParams.append(key, query[key]); } verifyParams.set("openid.mode", "check_authentication"); const verifyResponse = await fetch( "https://steamcommunity.com/openid/login", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: verifyParams.toString(), } ); const verifyText = await verifyResponse.text(); if (!verifyText.includes("is_valid:true")) { console.error("❌ Steam OpenID verification failed"); return reply.status(401).send({ error: "AuthenticationError", message: "Steam authentication verification failed", }); } // Extract Steam ID from the claimed_id const claimedId = query["openid.claimed_id"]; const steamIdMatch = claimedId.match(/(\d+)$/); if (!steamIdMatch) { return reply.status(400).send({ error: "AuthenticationError", message: "Could not extract Steam ID", }); } const steamId = steamIdMatch[1]; // Get user profile from Steam API const steamApiUrl = `http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=${config.steam.apiKey}&steamids=${steamId}`; const profileResponse = await fetch(steamApiUrl); const profileData = await profileResponse.json(); if ( !profileData.response || !profileData.response.players || profileData.response.players.length === 0 ) { return reply.status(400).send({ error: "AuthenticationError", message: "Could not fetch Steam profile", }); } const profile = profileData.response.players[0]; // Import User model const User = (await import("../models/User.js")).default; // Find or create user let user = await User.findOne({ steamId }); if (user) { // Update existing user user.username = profile.personaname; user.avatar = profile.avatarfull || profile.avatarmedium || profile.avatar; user.communityvisibilitystate = profile.communityvisibilitystate; await user.save(); console.log( `✅ Existing user logged in: ${user.username} (${steamId})` ); } else { // Create new user user = new User({ username: profile.personaname, steamId: steamId, avatar: profile.avatarfull || profile.avatarmedium || profile.avatar, account_creation: profile.timecreated || Math.floor(Date.now() / 1000), communityvisibilitystate: profile.communityvisibilitystate, balance: 0, staffLevel: 0, }); await user.save(); console.log(`✅ New user registered: ${user.username} (${steamId})`); } // Generate JWT tokens const { accessToken, refreshToken } = generateTokenPair(user); // Extract device information const userAgent = request.headers["user-agent"] || "Unknown"; const ip = request.ip || request.headers["x-forwarded-for"] || request.headers["x-real-ip"] || "Unknown"; // Simple device detection let device = "Desktop"; let browser = "Unknown"; let os = "Unknown"; if ( userAgent.includes("Mobile") || userAgent.includes("Android") || userAgent.includes("iPhone") ) { device = "Mobile"; } else if (userAgent.includes("Tablet") || userAgent.includes("iPad")) { device = "Tablet"; } if (userAgent.includes("Chrome")) browser = "Chrome"; else if (userAgent.includes("Firefox")) browser = "Firefox"; else if (userAgent.includes("Safari")) browser = "Safari"; else if (userAgent.includes("Edge")) browser = "Edge"; if (userAgent.includes("Windows")) os = "Windows"; else if (userAgent.includes("Mac")) os = "macOS"; else if (userAgent.includes("Linux")) os = "Linux"; else if (userAgent.includes("Android")) os = "Android"; else if (userAgent.includes("iOS") || userAgent.includes("iPhone")) os = "iOS"; // Create session try { const session = new Session({ userId: user._id, steamId: user.steamId, token: accessToken, refreshToken: refreshToken, ip: ip, userAgent: userAgent, device: device, browser: browser, os: os, location: { country: null, // Can integrate with IP geolocation service city: null, region: null, }, isActive: true, lastActivity: new Date(), expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days }); await session.save(); console.log( `📱 Session created for ${user.username} from ${ip} (${device}/${browser})` ); } catch (error) { console.error("Failed to create session:", error); // Don't fail login if session creation fails } // Set cookies reply .setCookie("accessToken", accessToken, { httpOnly: true, secure: config.cookie.secure, sameSite: config.cookie.sameSite, path: "/", maxAge: 15 * 60, domain: config.cookie.domain, }) .setCookie("refreshToken", refreshToken, { httpOnly: true, secure: config.cookie.secure, sameSite: config.cookie.sameSite, path: "/", maxAge: 7 * 24 * 60 * 60, domain: config.cookie.domain, }); console.log(`✅ User ${user.username} logged in successfully`); // Redirect to frontend return reply.redirect(`${config.cors.origin}/`); } catch (error) { console.error("❌ Steam callback error:", error); return reply.status(500).send({ error: "AuthenticationError", message: "Steam authentication failed", details: error.message, }); } }); // Get current user fastify.get( "/me", { preHandler: authenticate, }, async (request, reply) => { // Remove sensitive data const user = request.user.toObject(); delete user.twoFactor.secret; delete user.email.emailToken; return reply.send({ success: true, user: user, }); } ); // Decode JWT token (for debugging - shows what's in the token) fastify.get("/decode-token", async (request, reply) => { try { let token = null; // Try to get token from Authorization header const authHeader = request.headers.authorization; if (authHeader && authHeader.startsWith("Bearer ")) { token = authHeader.substring(7); } // If not in header, try cookies if (!token && request.cookies && request.cookies.accessToken) { token = request.cookies.accessToken; } if (!token) { return reply.status(400).send({ error: "NoToken", message: "No token provided. Send in Authorization header or cookie.", }); } // Import JWT utilities const { decodeToken } = await import("../utils/jwt.js"); const decoded = decodeToken(token); if (!decoded) { return reply.status(400).send({ error: "InvalidToken", message: "Could not decode token", }); } return reply.send({ success: true, decoded: decoded, message: "This shows what data is stored in your JWT token", }); } catch (error) { return reply.status(500).send({ error: "DecodeError", message: error.message, }); } }); // Refresh access token using refresh token fastify.post( "/refresh", { preHandler: verifyRefreshTokenMiddleware, }, async (request, reply) => { try { const user = request.user; // Generate new token pair const { accessToken, refreshToken } = generateTokenPair(user); // Update existing session with new tokens try { const oldSession = await Session.findOne({ refreshToken: request.refreshToken, userId: user._id, }); if (oldSession) { oldSession.token = accessToken; oldSession.refreshToken = refreshToken; oldSession.lastActivity = new Date(); await oldSession.save(); } } catch (error) { console.error("Failed to update session:", error); } // Set new cookies reply .setCookie("accessToken", accessToken, { httpOnly: true, secure: config.cookie.secure, sameSite: config.cookie.sameSite, path: "/", maxAge: 15 * 60, // 15 minutes domain: config.cookie.domain, }) .setCookie("refreshToken", refreshToken, { httpOnly: true, secure: config.cookie.secure, sameSite: config.cookie.sameSite, path: "/", maxAge: 7 * 24 * 60 * 60, // 7 days domain: config.cookie.domain, }); return reply.send({ success: true, message: "Tokens refreshed successfully", accessToken, refreshToken, }); } catch (error) { console.error("Token refresh error:", error); return reply.status(500).send({ error: "InternalServerError", message: "Failed to refresh tokens", }); } } ); // Logout - clear cookies and invalidate session fastify.post( "/logout", { preHandler: authenticate, }, async (request, reply) => { try { // Deactivate current session try { const session = await Session.findOne({ token: request.token, userId: request.user._id, }); if (session) { await session.deactivate(); } } catch (error) { console.error("Failed to deactivate session:", error); } // Clear cookies reply .clearCookie("accessToken", { path: "/", domain: config.cookie.domain, }) .clearCookie("refreshToken", { path: "/", domain: config.cookie.domain, }); console.log(`👋 User ${request.user.username} logged out`); return reply.send({ success: true, message: "Logged out successfully", }); } catch (error) { console.error("Logout error:", error); return reply.status(500).send({ error: "InternalServerError", message: "Failed to logout", }); } } ); // Verify access token endpoint (useful for checking if token is still valid) fastify.get( "/verify", { preHandler: authenticate, }, async (request, reply) => { return reply.send({ success: true, valid: true, userId: request.user._id, steamId: request.user.steamId, }); } ); }