Skip to content

Commit a2e09e6

Browse files
committed
Add session management utilities
Introduces utilities for managing user sessions, including session creation, activity tracking, device and location parsing, suspicious activity detection, and formatting for display. This module provides foundational tools for handling user sessions across devices.
1 parent 3aed97f commit a2e09e6

1 file changed

Lines changed: 242 additions & 0 deletions

File tree

lib/utils/session-manager.ts

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
/**
2+
* Session Management Utilities
3+
* Track and manage user sessions across devices
4+
*/
5+
6+
export interface UserSession {
7+
id: string
8+
userId: string
9+
deviceInfo: {
10+
userAgent: string
11+
browser: string
12+
os: string
13+
device: string
14+
}
15+
location: {
16+
ip: string
17+
country?: string
18+
city?: string
19+
}
20+
createdAt: string
21+
lastActivity: string
22+
expiresAt: string
23+
isActive: boolean
24+
isCurrent: boolean
25+
}
26+
27+
/**
28+
* Parse user agent to extract device information
29+
*/
30+
export function parseUserAgent(userAgent: string): {
31+
browser: string
32+
os: string
33+
device: string
34+
} {
35+
// Simple parser - in production, use a library like ua-parser-js
36+
const browser = detectBrowser(userAgent)
37+
const os = detectOS(userAgent)
38+
const device = detectDevice(userAgent)
39+
40+
return { browser, os, device }
41+
}
42+
43+
function detectBrowser(ua: string): string {
44+
if (ua.includes('Firefox')) return 'Firefox'
45+
if (ua.includes('Edg')) return 'Edge'
46+
if (ua.includes('Chrome')) return 'Chrome'
47+
if (ua.includes('Safari')) return 'Safari'
48+
if (ua.includes('Opera')) return 'Opera'
49+
return 'Unknown Browser'
50+
}
51+
52+
function detectOS(ua: string): string {
53+
if (ua.includes('Windows')) return 'Windows'
54+
if (ua.includes('Mac OS X')) return 'macOS'
55+
if (ua.includes('Linux')) return 'Linux'
56+
if (ua.includes('Android')) return 'Android'
57+
if (ua.includes('iOS')) return 'iOS'
58+
return 'Unknown OS'
59+
}
60+
61+
function detectDevice(ua: string): string {
62+
if (ua.includes('Mobile')) return 'Mobile'
63+
if (ua.includes('Tablet')) return 'Tablet'
64+
return 'Desktop'
65+
}
66+
67+
/**
68+
* Get location from IP address (placeholder)
69+
* In production, integrate with a geolocation API
70+
*/
71+
export async function getLocationFromIP(ip: string): Promise<{
72+
country?: string
73+
city?: string
74+
}> {
75+
// TODO: Integrate with IP geolocation API (e.g., ipapi.co, ip-api.com)
76+
// For now, return placeholder
77+
return {
78+
country: 'Unknown',
79+
city: 'Unknown',
80+
}
81+
}
82+
83+
/**
84+
* Create session record
85+
*/
86+
export function createSession(
87+
userId: string,
88+
userAgent: string,
89+
ip: string,
90+
expiresIn: number = 7 * 24 * 60 * 60 * 1000 // 7 days default
91+
): Omit<UserSession, 'location'> {
92+
const now = new Date().toISOString()
93+
const deviceInfo = parseUserAgent(userAgent)
94+
95+
return {
96+
id: generateSessionId(),
97+
userId,
98+
deviceInfo: {
99+
userAgent,
100+
...deviceInfo,
101+
},
102+
location: {
103+
ip,
104+
},
105+
createdAt: now,
106+
lastActivity: now,
107+
expiresAt: new Date(Date.now() + expiresIn).toISOString(),
108+
isActive: true,
109+
isCurrent: true,
110+
}
111+
}
112+
113+
/**
114+
* Generate unique session ID
115+
*/
116+
function generateSessionId(): string {
117+
return `sess_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
118+
}
119+
120+
/**
121+
* Check if session is expired
122+
*/
123+
export function isSessionExpired(session: UserSession): boolean {
124+
return new Date(session.expiresAt) < new Date()
125+
}
126+
127+
/**
128+
* Update session activity
129+
*/
130+
export function updateSessionActivity(session: UserSession): UserSession {
131+
return {
132+
...session,
133+
lastActivity: new Date().toISOString(),
134+
}
135+
}
136+
137+
/**
138+
* Invalidate session
139+
*/
140+
export function invalidateSession(session: UserSession): UserSession {
141+
return {
142+
...session,
143+
isActive: false,
144+
expiresAt: new Date().toISOString(),
145+
}
146+
}
147+
148+
/**
149+
* Check for suspicious session activity
150+
*/
151+
export function detectSuspiciousActivity(
152+
currentSession: UserSession,
153+
previousSession: UserSession
154+
): {
155+
suspicious: boolean
156+
reasons: string[]
157+
} {
158+
const reasons: string[] = []
159+
160+
// Check for impossible travel
161+
if (
162+
currentSession.location.country &&
163+
previousSession.location.country &&
164+
currentSession.location.country !== previousSession.location.country
165+
) {
166+
const timeDiff =
167+
new Date(currentSession.createdAt).getTime() -
168+
new Date(previousSession.lastActivity).getTime()
169+
170+
// If country changed in less than 1 hour, flag as suspicious
171+
if (timeDiff < 60 * 60 * 1000) {
172+
reasons.push('Impossible travel detected')
173+
}
174+
}
175+
176+
// Check for device change
177+
if (currentSession.deviceInfo.device !== previousSession.deviceInfo.device) {
178+
reasons.push('New device detected')
179+
}
180+
181+
// Check for OS change
182+
if (currentSession.deviceInfo.os !== previousSession.deviceInfo.os) {
183+
reasons.push('Different operating system')
184+
}
185+
186+
return {
187+
suspicious: reasons.length > 0,
188+
reasons,
189+
}
190+
}
191+
192+
/**
193+
* Format session for display
194+
*/
195+
export function formatSessionDisplay(session: UserSession): {
196+
title: string
197+
subtitle: string
198+
icon: string
199+
} {
200+
const { deviceInfo, location } = session
201+
const locationStr = [location.city, location.country]
202+
.filter(Boolean)
203+
.join(', ') || 'Unknown location'
204+
205+
return {
206+
title: `${deviceInfo.browser} on ${deviceInfo.os}`,
207+
subtitle: `${locationStr} • Last active ${formatRelativeTime(session.lastActivity)}`,
208+
icon: getDeviceIcon(deviceInfo.device),
209+
}
210+
}
211+
212+
/**
213+
* Get device icon name
214+
*/
215+
function getDeviceIcon(device: string): string {
216+
switch (device.toLowerCase()) {
217+
case 'mobile':
218+
return 'smartphone'
219+
case 'tablet':
220+
return 'tablet'
221+
default:
222+
return 'desktop'
223+
}
224+
}
225+
226+
/**
227+
* Format relative time
228+
*/
229+
function formatRelativeTime(timestamp: string): string {
230+
const now = Date.now()
231+
const then = new Date(timestamp).getTime()
232+
const diff = now - then
233+
234+
const minutes = Math.floor(diff / 60000)
235+
const hours = Math.floor(diff / 3600000)
236+
const days = Math.floor(diff / 86400000)
237+
238+
if (minutes < 1) return 'just now'
239+
if (minutes < 60) return `${minutes}m ago`
240+
if (hours < 24) return `${hours}h ago`
241+
return `${days}d ago`
242+
}

0 commit comments

Comments
 (0)