File size: 7,031 Bytes
beec165
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
"""
session_manager.py - Session Management for User Authentication
Handles session token generation, validation, and expiry
"""

import uuid
import hashlib
import secrets
from datetime import datetime, timedelta
import logging
from typing import Optional, Dict, Any

logger = logging.getLogger(__name__)

# ===========================================
# CONFIGURATION
# ===========================================

# Session expiry (30 days)
SESSION_EXPIRY_DAYS = 30

# In-memory session store (for demo - use database in production)
# Structure: {session_token: {'user_id': user_id, 'expires_at': timestamp, 'created_at': timestamp}}
_session_store: Dict[str, Dict[str, Any]] = {}


# ===========================================
# SESSION MANAGEMENT
# ===========================================

def generate_session_token() -> str:
    """
    Generate a secure random session token.
    """
    # Use secrets for cryptographically secure random token
    return secrets.token_urlsafe(32)


def create_session(user_id: str) -> Dict[str, Any]:
    """
    Create a new session for a user.
    Returns session token and expiry information.
    """
    session_token = generate_session_token()
    expires_at = datetime.now() + timedelta(days=SESSION_EXPIRY_DAYS)
    
    _session_store[session_token] = {
        'user_id': user_id,
        'expires_at': expires_at.timestamp(),
        'created_at': datetime.now().timestamp(),
        'last_accessed': datetime.now().timestamp()
    }
    
    logger.debug(f"Created session for user {user_id}")
    
    return {
        'session_token': session_token,
        'expires_at': expires_at.isoformat(),
        'expires_in_days': SESSION_EXPIRY_DAYS
    }


def validate_session(session_token: str) -> Optional[str]:
    """
    Validate a session token and return user_id if valid.
    Also refreshes the session expiry if within refresh window.
    """
    if session_token not in _session_store:
        return None
    
    session_data = _session_store[session_token]
    expires_at = datetime.fromtimestamp(session_data['expires_at'])
    
    # Check if expired
    if datetime.now() > expires_at:
        # Session expired, remove it
        delete_session(session_token)
        logger.debug(f"Session expired: {session_token[:16]}...")
        return None
    
    # Update last accessed time
    session_data['last_accessed'] = datetime.now().timestamp()
    
    # Refresh session if within 7 days of expiry (rolling expiry)
    # This implements the 30-day rolling requirement
    days_remaining = (expires_at - datetime.now()).days
    if days_remaining < 7:
        # Extend by another 30 days
        new_expiry = datetime.now() + timedelta(days=SESSION_EXPIRY_DAYS)
        session_data['expires_at'] = new_expiry.timestamp()
        logger.debug(f"Refreshed session for user {session_data['user_id']}")
    
    return session_data['user_id']


def delete_session(session_token: str) -> bool:
    """
    Delete a session (logout).
    """
    if session_token in _session_store:
        user_id = _session_store[session_token]['user_id']
        del _session_store[session_token]
        logger.debug(f"Deleted session for user {user_id}")
        return True
    return False


def delete_all_user_sessions(user_id: str) -> int:
    """
    Delete all sessions for a user (e.g., when changing password).
    Returns number of sessions deleted.
    """
    deleted = 0
    tokens_to_delete = []
    
    for token, data in _session_store.items():
        if data['user_id'] == user_id:
            tokens_to_delete.append(token)
    
    for token in tokens_to_delete:
        del _session_store[token]
        deleted += 1
    
    if deleted > 0:
        logger.info(f"Deleted {deleted} sessions for user {user_id}")
    
    return deleted


def get_session_info(session_token: str) -> Optional[Dict[str, Any]]:
    """
    Get information about a session (without validating).
    """
    if session_token not in _session_store:
        return None
    
    data = _session_store[session_token]
    return {
        'user_id': data['user_id'],
        'expires_at': datetime.fromtimestamp(data['expires_at']).isoformat(),
        'created_at': datetime.fromtimestamp(data['created_at']).isoformat(),
        'last_accessed': datetime.fromtimestamp(data['last_accessed']).isoformat()
    }


def get_active_session_count(user_id: str) -> int:
    """
    Get the number of active sessions for a user.
    """
    count = 0
    for token, data in _session_store.items():
        if data['user_id'] == user_id:
            # Check if still valid
            expires_at = datetime.fromtimestamp(data['expires_at'])
            if datetime.now() <= expires_at:
                count += 1
    return count


def cleanup_expired_sessions() -> int:
    """
    Remove all expired sessions from memory.
    Returns number of sessions removed.
    """
    now = datetime.now()
    expired = []
    
    for token, data in _session_store.items():
        expires_at = datetime.fromtimestamp(data['expires_at'])
        if now > expires_at:
            expired.append(token)
    
    for token in expired:
        del _session_store[token]
    
    if expired:
        logger.debug(f"Cleaned up {len(expired)} expired sessions")
    
    return len(expired)


# ===========================================
# PASSWORD HASHING (for user management)
# ===========================================

def hash_password(password: str, salt: str = None) -> tuple:
    """
    Hash a password with salt.
    Returns (hashed_password, salt)
    """
    if salt is None:
        salt = secrets.token_hex(16)
    
    # Use PBKDF2 for secure password hashing
    hashed = hashlib.pbkdf2_hmac(
        'sha256',
        password.encode('utf-8'),
        salt.encode('utf-8'),
        100000  # Number of iterations
    ).hex()
    
    return hashed, salt


def verify_password(password: str, hashed_password: str, salt: str) -> bool:
    """
    Verify a password against its hash and salt.
    """
    test_hash, _ = hash_password(password, salt)
    return test_hash == hashed_password


# ===========================================
# USER MANAGEMENT HELPERS
# ===========================================

def generate_user_id() -> str:
    """
    Generate a unique user ID.
    """
    return str(uuid.uuid4())


def is_valid_email(email: str) -> bool:
    """
    Basic email validation.
    """
    import re
    pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    return re.match(pattern, email) is not None


def is_valid_username(username: str) -> bool:
    """
    Validate username (alphanumeric + underscore, 3-30 chars).
    """
    import re
    pattern = r'^[a-zA-Z0-9_]{3,30}$'
    return re.match(pattern, username) is not None


# ===========================================
# INITIALIZATION
# ===========================================

# Run cleanup on module load (optional)
cleanup_expired_sessions()

logger.info("Session manager initialized")