import { UmbrellaDualAuth } from './dual-auth.js';
import { UmbrellaApiClient } from './api-client.js';
import { debugLog } from './utils/debug-logger.js';
export interface UserSession {
id: string;
username: string;
auth: UmbrellaDualAuth;
apiClient: UmbrellaApiClient;
isAuthenticated: boolean;
lastActivity: Date;
createdAt: Date;
authHeaders?: any;
accountKey?: string; // Current account key from auth
// Cached accounts data from /users/plain-sub-users?IsAccount=true
accountsData?: any[]; // Array of accounts with cloudTypeId
// Cached user data from /users/plain-sub-users
userData?: {
user_type: number;
accounts?: any[];
customerDivisions?: any;
is_reseller_mode?: number;
};
}
export interface UserCredentials {
username: string;
password: string;
}
export class UserSessionManager {
private sessions: Map<string, UserSession> = new Map();
private sessionTimeoutMs: number;
private cleanupIntervalMs: number;
private cleanupTimer?: NodeJS.Timeout;
private baseURL: string;
private frontendBaseURL: string;
private currentOAuthSessionId?: string; // Track the current OAuth session for this request
constructor(
baseURL: string,
frontendBaseURL?: string,
sessionTimeoutMs: number = 24 * 60 * 60 * 1000, // 24 hours
cleanupIntervalMs: number = 5 * 60 * 1000 // 5 minutes
) {
this.baseURL = baseURL;
this.frontendBaseURL = frontendBaseURL || baseURL;
this.sessionTimeoutMs = sessionTimeoutMs;
this.cleanupIntervalMs = cleanupIntervalMs;
this.startCleanupTimer();
}
/**
* @deprecated Regular authentication is deprecated. Use OAuth flow instead.
* This method is kept for backwards compatibility but will be removed.
*/
async authenticateUser(credentials: UserCredentials, sessionId?: string): Promise<{ success: boolean; sessionId: string; error?: string }> {
console.error('[AUTH] Warning: Regular authentication is deprecated. Please use OAuth authentication.');
return {
success: false,
sessionId: sessionId || this.generateSessionId(credentials.username),
error: 'Regular authentication is disabled. Please use OAuth authentication.'
};
}
/**
* Set authentication from a verified OAuth token (for HTTPS transport)
*/
async setAuthFromToken(tokenData: {
Authorization: string; // This contains the actual Umbrella API authorization token
userEmail: string;
clientId: string;
userManagementInfo?: { isKeycloak: boolean; authMethod: string }; // Auth method from JWT token
}): Promise<{ success: boolean; sessionId: string }> {
try {
// Use clientId as session identifier for multi-tenant support
const sessionId = `oauth-${tokenData.clientId}`;
// Create auth and API client instances
const auth = new UmbrellaDualAuth(this.baseURL);
const apiClient = new UmbrellaApiClient(this.baseURL, this.frontendBaseURL);
// IMPORTANT: tokenData.Authorization contains the actual Umbrella API authorization
// (not the OAuth Bearer token - that was already verified and extracted)
// This is the real Cognito JWT token for Umbrella API
auth.setPreAuthenticatedTokens({
Authorization: tokenData.Authorization // This is the actual Umbrella API auth token
});
// Set the userManagementInfo from the JWT token (critical for correct API key building)
if (tokenData.userManagementInfo) {
(auth as any).userManagementInfo = tokenData.userManagementInfo;
console.error(`[AUTH] Set userManagementInfo from token: ${JSON.stringify(tokenData.userManagementInfo)}`);
}
const authHeaders = {
Authorization: tokenData.Authorization // Use the actual Umbrella API auth token
};
// Set auth headers on API client
apiClient.setAuthToken(authHeaders);
apiClient.setAuth(auth);
// Create or update session
const session: UserSession = {
id: sessionId,
username: tokenData.userEmail,
auth,
apiClient,
isAuthenticated: true,
lastActivity: new Date(),
createdAt: this.sessions.get(sessionId)?.createdAt || new Date(),
authHeaders
};
this.sessions.set(sessionId, session);
// DEFERRED: User data will be fetched lazily when first needed by a tool
// This prevents timeout issues during MCP initialization for accounts with many sub-users
console.error(`[AUTH] OAuth - User authenticated - data will be fetched on first tool use`)
// Set this as the current OAuth session for this request
this.currentOAuthSessionId = sessionId;
if (process.env.DEBUG === 'true') {
console.error(`[AUTH] OAuth session created for ${tokenData.userEmail} (tenant: ${tokenData.clientId})`);
}
return {
success: true,
sessionId
};
} catch (error: any) {
console.error(`[AUTH] Failed to set auth from token: ${error.message}`);
return {
success: false,
sessionId: ''
};
}
}
/**
* Get user session by session ID
*/
getUserSession(sessionId: string): UserSession | null {
const session = this.sessions.get(sessionId);
if (!session) {
return null;
}
// Check if session has expired
if (this.isSessionExpired(session)) {
this.removeSession(sessionId);
return null;
}
// Update last activity
session.lastActivity = new Date();
return session;
}
/**
* @deprecated Get user session by username (for backwards compatibility)
*/
getUserSessionByUsername(username: string): UserSession | null {
// OAuth sessions don't use username-based session IDs
return null;
}
/**
* Remove a user session (logout)
*/
removeSession(sessionId: string): boolean {
const session = this.sessions.get(sessionId);
if (session) {
console.error(`[SESSION] Removing session for user ${session.username} (session: ${sessionId})`);
this.sessions.delete(sessionId);
return true;
}
return false;
}
/**
* @deprecated Remove user session by username
*/
removeUserSession(username: string): boolean {
// OAuth sessions don't use username-based session IDs
return false;
}
/**
* Get all active sessions (for admin/debugging)
*/
getActiveSessions(): Array<{ id: string; username: string; lastActivity: Date; isAuthenticated: boolean }> {
const activeSessions: Array<{ id: string; username: string; lastActivity: Date; isAuthenticated: boolean }> = [];
for (const [id, session] of this.sessions.entries()) {
if (!this.isSessionExpired(session)) {
activeSessions.push({
id,
username: session.username,
lastActivity: session.lastActivity,
isAuthenticated: session.isAuthenticated
});
}
}
return activeSessions;
}
/**
* Get the current active session (for single-user mode)
*/
getCurrentSession(): UserSession | null {
// If we have a current OAuth session ID, return that session first
if (this.currentOAuthSessionId) {
const oauthSession = this.sessions.get(this.currentOAuthSessionId);
if (oauthSession && oauthSession.isAuthenticated && !this.isSessionExpired(oauthSession)) {
oauthSession.lastActivity = new Date();
return oauthSession;
}
}
// Otherwise, return the first active session
for (const [id, session] of this.sessions.entries()) {
if (session.isAuthenticated && !this.isSessionExpired(session)) {
session.lastActivity = new Date();
return session;
}
}
return null;
}
/**
* Clear the current OAuth session ID (called after request completes)
*/
clearCurrentOAuthSession(): void {
this.currentOAuthSessionId = undefined;
}
/**
* Generate consistent session ID from username
*/
private generateSessionId(username: string): string {
// Create a consistent session ID based on username
// This allows the same user to have the same session ID across restarts
return `session_${Buffer.from(username).toString('base64').replace(/[^a-zA-Z0-9]/g, '')}`;
}
/**
* Check if a session has expired
*/
private isSessionExpired(session: UserSession): boolean {
const now = new Date();
const timeSinceLastActivity = now.getTime() - session.lastActivity.getTime();
return timeSinceLastActivity > this.sessionTimeoutMs;
}
/**
* Start the cleanup timer to remove expired sessions
*/
private startCleanupTimer(): void {
this.cleanupTimer = setInterval(() => {
this.cleanupExpiredSessions();
}, this.cleanupIntervalMs);
}
/**
* Clean up expired sessions
*/
private cleanupExpiredSessions(): void {
const now = new Date();
const expiredSessions: string[] = [];
for (const [id, session] of this.sessions.entries()) {
if (this.isSessionExpired(session)) {
expiredSessions.push(id);
}
}
if (expiredSessions.length > 0) {
console.error(`[CLEANUP] Cleaning up ${expiredSessions.length} expired sessions`);
expiredSessions.forEach(id => {
const session = this.sessions.get(id);
if (session) {
console.error(` - Expired: ${session.username} (inactive for ${Math.round((now.getTime() - session.lastActivity.getTime()) / 1000 / 60)} minutes)`);
}
this.sessions.delete(id);
});
}
}
/**
* Stop the cleanup timer (for graceful shutdown)
*/
shutdown(): void {
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
this.cleanupTimer = undefined;
}
console.error(`[SHUTDOWN] Shutting down UserSessionManager with ${this.sessions.size} active sessions`);
this.sessions.clear();
}
/**
* Get session statistics
*/
getStats(): { totalSessions: number; activeSessions: number; expiredSessions: number } {
let activeSessions = 0;
let expiredSessions = 0;
for (const session of this.sessions.values()) {
if (this.isSessionExpired(session)) {
expiredSessions++;
} else {
activeSessions++;
}
}
return {
totalSessions: this.sessions.size,
activeSessions,
expiredSessions
};
}
}