joashsam commited on
Commit
beec165
·
verified ·
1 Parent(s): 8007730

Create session_manager.py

Browse files
Files changed (1) hide show
  1. session_manager.py +249 -0
session_manager.py ADDED
@@ -0,0 +1,249 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ session_manager.py - Session Management for User Authentication
3
+ Handles session token generation, validation, and expiry
4
+ """
5
+
6
+ import uuid
7
+ import hashlib
8
+ import secrets
9
+ from datetime import datetime, timedelta
10
+ import logging
11
+ from typing import Optional, Dict, Any
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # ===========================================
16
+ # CONFIGURATION
17
+ # ===========================================
18
+
19
+ # Session expiry (30 days)
20
+ SESSION_EXPIRY_DAYS = 30
21
+
22
+ # In-memory session store (for demo - use database in production)
23
+ # Structure: {session_token: {'user_id': user_id, 'expires_at': timestamp, 'created_at': timestamp}}
24
+ _session_store: Dict[str, Dict[str, Any]] = {}
25
+
26
+
27
+ # ===========================================
28
+ # SESSION MANAGEMENT
29
+ # ===========================================
30
+
31
+ def generate_session_token() -> str:
32
+ """
33
+ Generate a secure random session token.
34
+ """
35
+ # Use secrets for cryptographically secure random token
36
+ return secrets.token_urlsafe(32)
37
+
38
+
39
+ def create_session(user_id: str) -> Dict[str, Any]:
40
+ """
41
+ Create a new session for a user.
42
+ Returns session token and expiry information.
43
+ """
44
+ session_token = generate_session_token()
45
+ expires_at = datetime.now() + timedelta(days=SESSION_EXPIRY_DAYS)
46
+
47
+ _session_store[session_token] = {
48
+ 'user_id': user_id,
49
+ 'expires_at': expires_at.timestamp(),
50
+ 'created_at': datetime.now().timestamp(),
51
+ 'last_accessed': datetime.now().timestamp()
52
+ }
53
+
54
+ logger.debug(f"Created session for user {user_id}")
55
+
56
+ return {
57
+ 'session_token': session_token,
58
+ 'expires_at': expires_at.isoformat(),
59
+ 'expires_in_days': SESSION_EXPIRY_DAYS
60
+ }
61
+
62
+
63
+ def validate_session(session_token: str) -> Optional[str]:
64
+ """
65
+ Validate a session token and return user_id if valid.
66
+ Also refreshes the session expiry if within refresh window.
67
+ """
68
+ if session_token not in _session_store:
69
+ return None
70
+
71
+ session_data = _session_store[session_token]
72
+ expires_at = datetime.fromtimestamp(session_data['expires_at'])
73
+
74
+ # Check if expired
75
+ if datetime.now() > expires_at:
76
+ # Session expired, remove it
77
+ delete_session(session_token)
78
+ logger.debug(f"Session expired: {session_token[:16]}...")
79
+ return None
80
+
81
+ # Update last accessed time
82
+ session_data['last_accessed'] = datetime.now().timestamp()
83
+
84
+ # Refresh session if within 7 days of expiry (rolling expiry)
85
+ # This implements the 30-day rolling requirement
86
+ days_remaining = (expires_at - datetime.now()).days
87
+ if days_remaining < 7:
88
+ # Extend by another 30 days
89
+ new_expiry = datetime.now() + timedelta(days=SESSION_EXPIRY_DAYS)
90
+ session_data['expires_at'] = new_expiry.timestamp()
91
+ logger.debug(f"Refreshed session for user {session_data['user_id']}")
92
+
93
+ return session_data['user_id']
94
+
95
+
96
+ def delete_session(session_token: str) -> bool:
97
+ """
98
+ Delete a session (logout).
99
+ """
100
+ if session_token in _session_store:
101
+ user_id = _session_store[session_token]['user_id']
102
+ del _session_store[session_token]
103
+ logger.debug(f"Deleted session for user {user_id}")
104
+ return True
105
+ return False
106
+
107
+
108
+ def delete_all_user_sessions(user_id: str) -> int:
109
+ """
110
+ Delete all sessions for a user (e.g., when changing password).
111
+ Returns number of sessions deleted.
112
+ """
113
+ deleted = 0
114
+ tokens_to_delete = []
115
+
116
+ for token, data in _session_store.items():
117
+ if data['user_id'] == user_id:
118
+ tokens_to_delete.append(token)
119
+
120
+ for token in tokens_to_delete:
121
+ del _session_store[token]
122
+ deleted += 1
123
+
124
+ if deleted > 0:
125
+ logger.info(f"Deleted {deleted} sessions for user {user_id}")
126
+
127
+ return deleted
128
+
129
+
130
+ def get_session_info(session_token: str) -> Optional[Dict[str, Any]]:
131
+ """
132
+ Get information about a session (without validating).
133
+ """
134
+ if session_token not in _session_store:
135
+ return None
136
+
137
+ data = _session_store[session_token]
138
+ return {
139
+ 'user_id': data['user_id'],
140
+ 'expires_at': datetime.fromtimestamp(data['expires_at']).isoformat(),
141
+ 'created_at': datetime.fromtimestamp(data['created_at']).isoformat(),
142
+ 'last_accessed': datetime.fromtimestamp(data['last_accessed']).isoformat()
143
+ }
144
+
145
+
146
+ def get_active_session_count(user_id: str) -> int:
147
+ """
148
+ Get the number of active sessions for a user.
149
+ """
150
+ count = 0
151
+ for token, data in _session_store.items():
152
+ if data['user_id'] == user_id:
153
+ # Check if still valid
154
+ expires_at = datetime.fromtimestamp(data['expires_at'])
155
+ if datetime.now() <= expires_at:
156
+ count += 1
157
+ return count
158
+
159
+
160
+ def cleanup_expired_sessions() -> int:
161
+ """
162
+ Remove all expired sessions from memory.
163
+ Returns number of sessions removed.
164
+ """
165
+ now = datetime.now()
166
+ expired = []
167
+
168
+ for token, data in _session_store.items():
169
+ expires_at = datetime.fromtimestamp(data['expires_at'])
170
+ if now > expires_at:
171
+ expired.append(token)
172
+
173
+ for token in expired:
174
+ del _session_store[token]
175
+
176
+ if expired:
177
+ logger.debug(f"Cleaned up {len(expired)} expired sessions")
178
+
179
+ return len(expired)
180
+
181
+
182
+ # ===========================================
183
+ # PASSWORD HASHING (for user management)
184
+ # ===========================================
185
+
186
+ def hash_password(password: str, salt: str = None) -> tuple:
187
+ """
188
+ Hash a password with salt.
189
+ Returns (hashed_password, salt)
190
+ """
191
+ if salt is None:
192
+ salt = secrets.token_hex(16)
193
+
194
+ # Use PBKDF2 for secure password hashing
195
+ hashed = hashlib.pbkdf2_hmac(
196
+ 'sha256',
197
+ password.encode('utf-8'),
198
+ salt.encode('utf-8'),
199
+ 100000 # Number of iterations
200
+ ).hex()
201
+
202
+ return hashed, salt
203
+
204
+
205
+ def verify_password(password: str, hashed_password: str, salt: str) -> bool:
206
+ """
207
+ Verify a password against its hash and salt.
208
+ """
209
+ test_hash, _ = hash_password(password, salt)
210
+ return test_hash == hashed_password
211
+
212
+
213
+ # ===========================================
214
+ # USER MANAGEMENT HELPERS
215
+ # ===========================================
216
+
217
+ def generate_user_id() -> str:
218
+ """
219
+ Generate a unique user ID.
220
+ """
221
+ return str(uuid.uuid4())
222
+
223
+
224
+ def is_valid_email(email: str) -> bool:
225
+ """
226
+ Basic email validation.
227
+ """
228
+ import re
229
+ pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
230
+ return re.match(pattern, email) is not None
231
+
232
+
233
+ def is_valid_username(username: str) -> bool:
234
+ """
235
+ Validate username (alphanumeric + underscore, 3-30 chars).
236
+ """
237
+ import re
238
+ pattern = r'^[a-zA-Z0-9_]{3,30}$'
239
+ return re.match(pattern, username) is not None
240
+
241
+
242
+ # ===========================================
243
+ # INITIALIZATION
244
+ # ===========================================
245
+
246
+ # Run cleanup on module load (optional)
247
+ cleanup_expired_sessions()
248
+
249
+ logger.info("Session manager initialized")