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