Files
TurboTrades/routes/auth.js
2026-01-10 04:57:43 +00:00

498 lines
15 KiB
JavaScript

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,
});
}
);
}