4 changed files with 277 additions and 1 deletions
@ -0,0 +1,36 @@ |
|||||||
|
import jwt from 'jsonwebtoken'; |
||||||
|
import { Response, NextFunction } from 'express'; |
||||||
|
import { AuthRequest } from '../types/index.js'; |
||||||
|
import { env } from '../config/env.js'; |
||||||
|
|
||||||
|
/** |
||||||
|
* Optional auth middleware - sets user info if token exists but doesn't fail if missing |
||||||
|
* Useful for endpoints that work for both authenticated and anonymous users |
||||||
|
*/ |
||||||
|
export function optionalAuthMiddleware( |
||||||
|
req: AuthRequest, |
||||||
|
res: Response, |
||||||
|
next: NextFunction |
||||||
|
) { |
||||||
|
// Check for token in Authorization header or cookie
|
||||||
|
const token = req.cookies.auth_token ||
|
||||||
|
req.headers.authorization?.replace('Bearer ', ''); |
||||||
|
|
||||||
|
if (token) { |
||||||
|
try { |
||||||
|
const decoded = jwt.verify(token, env.jwtSecret) as { |
||||||
|
userId: number; |
||||||
|
username: string; |
||||||
|
}; |
||||||
|
|
||||||
|
req.userId = decoded.userId; |
||||||
|
req.username = decoded.username; |
||||||
|
} catch (error) { |
||||||
|
// Invalid token - just continue without user info
|
||||||
|
// Don't set userId/username
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Always continue - this middleware never fails
|
||||||
|
next(); |
||||||
|
} |
||||||
@ -0,0 +1,142 @@ |
|||||||
|
/** |
||||||
|
* Connection Tracker Service |
||||||
|
* Tracks active user connections via heartbeat mechanism |
||||||
|
*/ |
||||||
|
|
||||||
|
interface Connection { |
||||||
|
sessionId: string; |
||||||
|
userId?: number; |
||||||
|
username?: string; |
||||||
|
route?: string; |
||||||
|
videoTitle?: string; |
||||||
|
videoChannel?: string; |
||||||
|
timeUsed?: number; // minutes
|
||||||
|
dailyLimit?: number; // minutes
|
||||||
|
lastHeartbeat: number; |
||||||
|
connectedAt: number; |
||||||
|
} |
||||||
|
|
||||||
|
class ConnectionTracker { |
||||||
|
private connections: Map<string, Connection> = new Map(); |
||||||
|
private readonly HEARTBEAT_TIMEOUT = 60 * 1000; // 60 seconds - consider connection dead if no heartbeat
|
||||||
|
private cleanupInterval: NodeJS.Timeout | null = null; |
||||||
|
|
||||||
|
constructor() { |
||||||
|
// Clean up stale connections every 30 seconds
|
||||||
|
this.cleanupInterval = setInterval(() => { |
||||||
|
this.cleanupStaleConnections(); |
||||||
|
}, 30 * 1000); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Register or update a connection heartbeat |
||||||
|
*/ |
||||||
|
heartbeat(sessionId: string, userId?: number, username?: string, route?: string, videoTitle?: string, videoChannel?: string, timeUsed?: number, dailyLimit?: number): void { |
||||||
|
const now = Date.now(); |
||||||
|
const existing = this.connections.get(sessionId); |
||||||
|
|
||||||
|
if (existing) { |
||||||
|
// Update existing connection
|
||||||
|
existing.lastHeartbeat = now; |
||||||
|
if (userId !== undefined) existing.userId = userId; |
||||||
|
if (username !== undefined) existing.username = username; |
||||||
|
if (route !== undefined) existing.route = route; |
||||||
|
if (videoTitle !== undefined) existing.videoTitle = videoTitle; |
||||||
|
if (videoChannel !== undefined) existing.videoChannel = videoChannel; |
||||||
|
if (timeUsed !== undefined) existing.timeUsed = timeUsed; |
||||||
|
if (dailyLimit !== undefined) existing.dailyLimit = dailyLimit; |
||||||
|
} else { |
||||||
|
// New connection
|
||||||
|
this.connections.set(sessionId, { |
||||||
|
sessionId, |
||||||
|
userId, |
||||||
|
username, |
||||||
|
route, |
||||||
|
videoTitle, |
||||||
|
videoChannel, |
||||||
|
timeUsed, |
||||||
|
dailyLimit, |
||||||
|
lastHeartbeat: now, |
||||||
|
connectedAt: now |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Remove a connection |
||||||
|
*/ |
||||||
|
disconnect(sessionId: string): void { |
||||||
|
this.connections.delete(sessionId); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Get count of active connections |
||||||
|
*/ |
||||||
|
getConnectionCount(): number { |
||||||
|
this.cleanupStaleConnections(); |
||||||
|
return this.connections.size; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Get all active connections (for admin/debugging) |
||||||
|
*/ |
||||||
|
getConnections(): Connection[] { |
||||||
|
this.cleanupStaleConnections(); |
||||||
|
return Array.from(this.connections.values()); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Get connection stats |
||||||
|
*/ |
||||||
|
getStats(): { |
||||||
|
total: number; |
||||||
|
authenticated: number; |
||||||
|
anonymous: number; |
||||||
|
} { |
||||||
|
this.cleanupStaleConnections(); |
||||||
|
const connections = Array.from(this.connections.values()); |
||||||
|
const authenticated = connections.filter(c => c.userId !== undefined).length; |
||||||
|
|
||||||
|
return { |
||||||
|
total: connections.length, |
||||||
|
authenticated, |
||||||
|
anonymous: connections.length - authenticated |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Remove connections that haven't sent a heartbeat recently |
||||||
|
*/ |
||||||
|
private cleanupStaleConnections(): void { |
||||||
|
const now = Date.now(); |
||||||
|
const stale: string[] = []; |
||||||
|
|
||||||
|
this.connections.forEach((conn, sessionId) => { |
||||||
|
if (now - conn.lastHeartbeat > this.HEARTBEAT_TIMEOUT) { |
||||||
|
stale.push(sessionId); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
stale.forEach(sessionId => { |
||||||
|
this.connections.delete(sessionId); |
||||||
|
}); |
||||||
|
|
||||||
|
if (stale.length > 0) { |
||||||
|
console.log(`[ConnectionTracker] Cleaned up ${stale.length} stale connection(s)`); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Cleanup on shutdown |
||||||
|
*/ |
||||||
|
destroy(): void { |
||||||
|
if (this.cleanupInterval) { |
||||||
|
clearInterval(this.cleanupInterval); |
||||||
|
this.cleanupInterval = null; |
||||||
|
} |
||||||
|
this.connections.clear(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Singleton instance
|
||||||
|
export const connectionTracker = new ConnectionTracker(); |
||||||
Loading…
Reference in new issue