← back to Dear Bubbe Nextjs

lib/user-session.ts

137 lines

/**
 * User Session Management for Bubbe.ai
 * 
 * Handles user plans and feature access without disrupting existing chat flow
 * This is a parallel system that doesn't interfere with current Bubbe behavior
 */

import { PlanId, FeatureKey, checkFeatureAccess } from './bubbe-pricing';

// In-memory session storage (replace with database in production)
const userSessions = new Map<string, UserSession>();

export interface UserSession {
  userId: string;
  plan: PlanId;
  email?: string;
  subscribedAt?: Date;
  features: FeatureKey[];
  ipAddress?: string;
  location?: string;
}

/**
 * Get or create a user session
 * Default plan is "basic" (free)
 */
export function getUserSession(userId: string): UserSession {
  if (!userSessions.has(userId)) {
    userSessions.set(userId, {
      userId,
      plan: 'basic',
      features: [],
      subscribedAt: new Date()
    });
  }
  return userSessions.get(userId)!;
}

/**
 * Update user's subscription plan
 */
export function updateUserPlan(userId: string, plan: PlanId): UserSession {
  const session = getUserSession(userId);
  session.plan = plan;
  session.subscribedAt = new Date();
  userSessions.set(userId, session);
  return session;
}

/**
 * Check if user can access a specific feature
 */
export function canUserAccessFeature(
  userId: string, 
  feature: FeatureKey
): { allowed: boolean; bubbeResponse?: string } {
  const session = getUserSession(userId);
  return checkFeatureAccess(session.plan, feature);
}

/**
 * Get user ID from request headers or create one
 * Uses IP address as fallback for user identification
 */
export function getUserIdFromRequest(req: any): string {
  // Try to get from cookie first
  const cookieUserId = req.cookies?.userId;
  if (cookieUserId) return cookieUserId;
  
  // Try to get from header
  const headerUserId = req.headers?.['x-user-id'];
  if (headerUserId) return headerUserId;
  
  // Fallback to IP-based ID
  const ip = req.headers?.['x-forwarded-for'] || 
             req.connection?.remoteAddress || 
             'anonymous';
  
  return `user_${ip.replace(/[.:]/g, '_')}`;
}

/**
 * Middleware helper to check feature access
 * Returns either the allowed response or a Bubbe rejection
 */
export function withFeatureCheck(
  feature: FeatureKey,
  handler: (req: any, res: any) => Promise<any>
) {
  return async (req: any, res: any) => {
    const userId = getUserIdFromRequest(req);
    const access = canUserAccessFeature(userId, feature);
    
    if (!access.allowed) {
      // Return Bubbe-style rejection
      return res.status(402).json({
        error: 'feature_locked',
        message: access.bubbeResponse,
        requiredFeature: feature,
        currentPlan: getUserSession(userId).plan
      });
    }
    
    // Feature allowed, proceed with handler
    return handler(req, res);
  };
}

/**
 * Track feature usage for analytics
 */
export function trackFeatureUsage(userId: string, feature: FeatureKey) {
  const timestamp = new Date().toISOString();
  console.log(`[Feature Usage] User: ${userId}, Feature: ${feature}, Time: ${timestamp}`);
  // In production, send to analytics service
}

/**
 * Get all active sessions (for admin dashboard)
 */
export function getAllSessions(): UserSession[] {
  return Array.from(userSessions.values());
}

/**
 * Clear inactive sessions (housekeeping)
 */
export function clearInactiveSessions(inactiveDays: number = 30) {
  const cutoff = new Date();
  cutoff.setDate(cutoff.getDate() - inactiveDays);
  
  for (const [userId, session] of userSessions.entries()) {
    if (session.subscribedAt && session.subscribedAt < cutoff) {
      userSessions.delete(userId);
    }
  }
}