the37lab-authlib 0.1.1749279322__py3-none-any.whl → 0.1.1749553514__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of the37lab-authlib might be problematic. Click here for more details.

the37lab_authlib/auth.py CHANGED
@@ -1,520 +1,513 @@
1
- import inspect
2
- from flask import Blueprint, request, jsonify, current_app, url_for, redirect
3
- import jwt
4
- from datetime import datetime, timedelta
5
- from .db import Database
6
- from .models import User, Role, ApiToken
7
- from .exceptions import AuthError
8
- import uuid
9
- import requests
10
- import bcrypt
11
- import logging
12
- import os
13
- from functools import wraps
14
-
15
- logging.basicConfig(level=logging.DEBUG)
16
- logger = logging.getLogger(__name__)
17
-
18
- def handle_auth_errors(f):
19
- @wraps(f)
20
- def decorated(*args, **kwargs):
21
- try:
22
- return f(*args, **kwargs)
23
- except AuthError as e:
24
- response = jsonify(e.to_dict())
25
- response.status_code = e.status_code
26
- return response
27
- return decorated
28
-
29
- class AuthManager:
30
- def __init__(self, app=None, db_dsn=None, jwt_secret=None, oauth_config=None, id_type='integer'):
31
- logger.info("INITIALIZING AUTHMANAGER {} - {} - {}".format(db_dsn, jwt_secret, not app))
32
- self.db = Database(db_dsn, id_type=id_type) if db_dsn else None
33
- self.jwt_secret = jwt_secret
34
- logger.debug(f"Initializing AuthManager with JWT secret: {jwt_secret[:5]}..." if jwt_secret else "No JWT secret provided")
35
- self.oauth_config = oauth_config or {}
36
-
37
- if app:
38
- self.init_app(app)
39
-
40
- def _extract_token_from_header(self):
41
- auth_header = request.headers.get('Authorization')
42
- if not auth_header:
43
- raise AuthError('No authorization header', 401)
44
-
45
- try:
46
- scheme, token = auth_header.split()
47
- if scheme.lower() != 'bearer':
48
- raise AuthError('Invalid authorization scheme', 401)
49
-
50
- if not token:
51
- raise AuthError('No token provided', 401)
52
-
53
- return token
54
- except ValueError:
55
- raise AuthError('Invalid authorization header format', 401)
56
-
57
- def _validate_api_token(self, api_token):
58
- try:
59
- parsed = ApiToken.parse_token(api_token)
60
- with self.db.get_cursor() as cur:
61
- # First get the API token record
62
- cur.execute("""
63
- SELECT t.*, u.* FROM api_tokens t
64
- JOIN users u ON t.user_id = u.id
65
- WHERE t.id = %s
66
- """, (parsed['id'],))
67
- result = cur.fetchone()
68
- if not result:
69
- raise AuthError('Invalid API token')
70
-
71
- # Verify the nonce
72
- if not bcrypt.checkpw(parsed['nonce'].encode('utf-8'), result['token'].encode('utf-8')):
73
- raise AuthError('Invalid API token')
74
-
75
- # Check if token is expired
76
- if result['expires_at'] and result['expires_at'] < datetime.utcnow():
77
- raise AuthError('API token has expired')
78
-
79
- # Update last used timestamp
80
- cur.execute("""
81
- UPDATE api_tokens
82
- SET last_used_at = %s
83
- WHERE id = %s
84
- """, (datetime.utcnow(), parsed['id']))
85
-
86
- # Fetch roles
87
- cur.execute("""
88
- SELECT r.name FROM roles r
89
- JOIN user_roles ur ON ur.role_id = r.id
90
- WHERE ur.user_id = %s
91
- """, (result['user_id'],))
92
- roles = [row['name'] for row in cur.fetchall()]
93
-
94
- # Construct user object
95
- return {
96
- 'id': result['user_id'],
97
- 'username': result['username'],
98
- 'email': result['email'],
99
- 'real_name': result['real_name'],
100
- 'roles': roles
101
- }
102
- except ValueError:
103
- raise AuthError('Invalid token format')
104
-
105
- def _authenticate_request(self):
106
- auth_header = request.headers.get('Authorization')
107
- api_token = request.headers.get('X-API-Token')
108
-
109
- if auth_header and auth_header.startswith('Bearer '):
110
- # JWT authentication
111
- token = self._extract_token_from_header()
112
- return self.validate_token(token)
113
- elif api_token:
114
- # API token authentication
115
- return self._validate_api_token(api_token)
116
- else:
117
- raise AuthError('No authentication provided', 401)
118
-
119
- def require_auth(self, f):
120
- @wraps(f)
121
- def decorated(*args, **kwargs):
122
- user = self._authenticate_request()
123
- sig = inspect.signature(f)
124
- if 'requesting_user' in sig.parameters:
125
- kwargs['requesting_user'] = user
126
-
127
- return f(*args, **kwargs)
128
- return decorated
129
-
130
- def init_app(self, app):
131
- app.auth_manager = self
132
- app.register_blueprint(self.create_blueprint())
133
-
134
- def create_blueprint(self):
135
- bp = Blueprint('auth', __name__, url_prefix='/v1/users')
136
-
137
- @bp.route('/login', methods=['POST'])
138
- @handle_auth_errors
139
- def login():
140
- data = request.get_json()
141
- username = data.get('username')
142
- password = data.get('password')
143
-
144
- if not username or not password:
145
- raise AuthError('Username and password required', 400)
146
-
147
- with self.db.get_cursor() as cur:
148
- cur.execute("SELECT * FROM users WHERE username = %s", (username,))
149
- user = cur.fetchone()
150
-
151
- if not user or not self._verify_password(password, user['password_hash']):
152
- raise AuthError('Invalid username or password', 401)
153
-
154
- # Fetch roles
155
- cur.execute("""
156
- SELECT r.name FROM roles r
157
- JOIN user_roles ur ON ur.role_id = r.id
158
- WHERE ur.user_id = %s
159
- """, (user['id'],))
160
- roles = [row['name'] for row in cur.fetchall()]
161
- user['roles'] = roles
162
-
163
- token = self._create_token(user)
164
- refresh_token = self._create_refresh_token(user)
165
-
166
- return jsonify({
167
- 'token': token,
168
- 'refresh_token': refresh_token,
169
- 'user': user
170
- })
171
-
172
- @bp.route('/login/oauth', methods=['POST'])
173
- @handle_auth_errors
174
- def oauth_login():
175
- provider = request.json.get('provider')
176
- if provider not in self.oauth_config:
177
- raise AuthError('Invalid OAuth provider', 400)
178
-
179
- redirect_uri = url_for('auth.oauth_callback', _external=True)
180
- logger.info("REDIRECT URI: {}".format(redirect_uri))
181
- redirect_uri = redirect_uri.replace("http://", "https://")
182
- return jsonify({
183
- 'redirect_url': self._get_oauth_url(provider, redirect_uri)
184
- })
185
-
186
- @bp.route('/login/oauth2callback')
187
- @handle_auth_errors
188
- def oauth_callback():
189
- code = request.args.get('code')
190
- provider = request.args.get('state')
191
-
192
- if not code or not provider:
193
- raise AuthError('Invalid OAuth callback', 400)
194
-
195
- user_info = self._get_oauth_user_info(provider, code)
196
- token = self._create_token(user_info)
197
- refresh_token = self._create_refresh_token(user_info)
198
-
199
- # Redirect to frontend with tokens
200
- frontend_url = os.getenv('FRONTEND_URL', 'http://localhost:5173')
201
- return redirect(f"{frontend_url}/oauth-callback?token={token}&refresh_token={refresh_token}")
202
-
203
- @bp.route('/login/profile')
204
- @handle_auth_errors
205
- def profile():
206
- token = request.headers.get('Authorization', '').split(' ')[-1]
207
- user = self.validate_token(token)
208
- return jsonify(user)
209
-
210
- @bp.route('/api-tokens', methods=['GET'])
211
- @handle_auth_errors
212
- @self.require_auth
213
- def get_tokens(requesting_user):
214
- tokens = self.get_user_api_tokens(requesting_user['id'])
215
- return jsonify(tokens)
216
-
217
- @bp.route('/api-tokens', methods=['POST'])
218
- @handle_auth_errors
219
- @self.require_auth
220
- def create_token(requesting_user):
221
- name = request.json.get('name')
222
- expires_in_days = request.json.get('expires_in_days')
223
- if not name:
224
- raise AuthError('Token name is required', 400)
225
- api_token = self.create_api_token(requesting_user['id'], name, expires_in_days)
226
- return jsonify({
227
- 'id': api_token.id,
228
- 'name': api_token.name,
229
- 'token': api_token.get_full_token(),
230
- 'created_at': api_token.created_at,
231
- 'expires_at': api_token.expires_at
232
- })
233
-
234
- @bp.route('/token-refresh', methods=['POST'])
235
- @handle_auth_errors
236
- def refresh_token():
237
- refresh_token = request.json.get('refresh_token')
238
- if not refresh_token:
239
- raise AuthError('No refresh token provided', 400)
240
-
241
- try:
242
- payload = jwt.decode(refresh_token, self.jwt_secret, algorithms=['HS256'])
243
- user_id = payload['sub']
244
-
245
- with self.db.get_cursor() as cur:
246
- cur.execute("SELECT * FROM users WHERE id = %s", (user_id,))
247
- user = cur.fetchone()
248
-
249
- if not user:
250
- raise AuthError('User not found', 404)
251
-
252
- return jsonify({
253
- 'token': self._create_token(user),
254
- 'refresh_token': self._create_refresh_token(user)
255
- })
256
- except jwt.InvalidTokenError:
257
- raise AuthError('Invalid refresh token', 401)
258
-
259
- @bp.route('/api-tokens', methods=['POST'])
260
- @handle_auth_errors
261
- @self.require_auth
262
- def create_api_token(requesting_user):
263
- name = request.json.get('name')
264
- if not name:
265
- raise AuthError('Token name required', 400)
266
-
267
- token = self.create_api_token(requesting_user['id'], name)
268
- return jsonify({'token': token.token})
269
-
270
- @bp.route('/api-tokens/validate', methods=['GET'])
271
- @handle_auth_errors
272
- @self.require_auth
273
- def validate_api_token(requesting_user):
274
- token = request.json.get('token')
275
- if not token:
276
- raise AuthError('No API token provided', 401)
277
- token = ApiToken.parse_token_id(token)
278
-
279
- with self.db.get_cursor() as cur:
280
- cur.execute("""
281
- SELECT * FROM api_tokens
282
- WHERE user_id = %s AND id = %s
283
- """, (requesting_user['id'], token))
284
- api_token = cur.fetchone()
285
-
286
- if not api_token:
287
- raise AuthError('Invalid API token', 401)
288
-
289
- # Check if token is expired
290
- if api_token['expires_at'] and api_token['expires_at'] < datetime.utcnow():
291
- raise AuthError('API token has expired', 401)
292
-
293
- # Update last used timestamp
294
- with self.db.get_cursor() as cur:
295
- cur.execute("""
296
- UPDATE api_tokens
297
- SET last_used_at = %s
298
- WHERE id = %s
299
- """, (datetime.utcnow(), api_token['id']))
300
-
301
- return jsonify({'valid': True})
302
-
303
- @bp.route('/api-tokens', methods=['DELETE'])
304
- @handle_auth_errors
305
- @self.require_auth
306
- def delete_api_token(requesting_user):
307
- token = request.json.get('token')
308
- if not token:
309
- raise AuthError('Token required', 400)
310
- token = ApiToken.parse_token_id(token)
311
-
312
- with self.db.get_cursor() as cur:
313
- cur.execute("""
314
- DELETE FROM api_tokens
315
- WHERE user_id = %s AND id = %s
316
- RETURNING id
317
- """, (requesting_user['id'], token))
318
- deleted_id = cur.fetchone()
319
- if not deleted_id:
320
- raise ValueError('Token not found or already deleted')
321
-
322
- return jsonify({'deleted': True})
323
-
324
- @bp.route('/register', methods=['POST'])
325
- @handle_auth_errors
326
- def register():
327
- data = request.get_json()
328
-
329
- # Hash the password
330
- password = data.get('password')
331
- if not password:
332
- raise AuthError('Password is required', 400)
333
-
334
- salt = bcrypt.gensalt()
335
- password_hash = bcrypt.hashpw(password.encode('utf-8'), salt)
336
-
337
- user = User(
338
- username=data['username'],
339
- email=data['email'],
340
- real_name=data['real_name'],
341
- roles=data.get('roles', []),
342
- id_generator=self.db.get_id_generator()
343
- )
344
-
345
- with self.db.get_cursor() as cur:
346
- if user.id is None:
347
- cur.execute("""
348
- INSERT INTO users (username, email, real_name, password_hash, created_at, updated_at)
349
- VALUES (%s, %s, %s, %s, %s, %s)
350
- RETURNING id
351
- """, (user.username, user.email, user.real_name, password_hash.decode('utf-8'),
352
- user.created_at, user.updated_at))
353
- user.id = cur.fetchone()['id']
354
- else:
355
- cur.execute("""
356
- INSERT INTO users (id, username, email, real_name, password_hash, created_at, updated_at)
357
- VALUES (%s, %s, %s, %s, %s, %s, %s)
358
- """, (user.id, user.username, user.email, user.real_name, password_hash.decode('utf-8'),
359
- user.created_at, user.updated_at))
360
-
361
- return jsonify({'id': user.id}), 201
362
-
363
- @bp.route('/roles', methods=['GET'])
364
- @handle_auth_errors
365
- def get_roles():
366
- with self.db.get_cursor() as cur:
367
- cur.execute("SELECT * FROM roles")
368
- roles = cur.fetchall()
369
- return jsonify(roles)
370
-
371
- return bp
372
-
373
- def validate_token(self, token):
374
- try:
375
- logger.debug(f"Validating token: {token}")
376
- payload = jwt.decode(token, self.jwt_secret, algorithms=['HS256'])
377
- logger.debug(f"Token payload: {payload}")
378
- user_id = int(payload['sub']) # Convert string ID back to integer
379
-
380
- with self.db.get_cursor() as cur:
381
- cur.execute("SELECT * FROM users WHERE id = %s", (user_id,))
382
- user = cur.fetchone()
383
- if not user:
384
- logger.error(f"User not found for ID: {user_id}")
385
- raise AuthError('User not found', 404)
386
- # Fetch roles
387
- cur.execute("""
388
- SELECT r.name FROM roles r
389
- JOIN user_roles ur ON ur.role_id = r.id
390
- WHERE ur.user_id = %s
391
- """, (user_id,))
392
- roles = [row['name'] for row in cur.fetchall()]
393
- user['roles'] = roles
394
-
395
- return user
396
- except jwt.InvalidTokenError as e:
397
- logger.error(f"Invalid token error: {str(e)}")
398
- raise AuthError('Invalid token', 401)
399
- except Exception as e:
400
- logger.error(f"Unexpected error during token validation: {str(e)}")
401
- raise AuthError(str(e), 500)
402
-
403
- def get_current_user(self):
404
- return self._authenticate_request()
405
-
406
- def get_user_api_tokens(self, user_id):
407
- """Get all API tokens for a user."""
408
- with self.db.get_cursor() as cur:
409
- cur.execute("""
410
- SELECT id, name, created_at, expires_at, last_used_at
411
- FROM api_tokens
412
- WHERE user_id = %s
413
- ORDER BY created_at DESC
414
- """, (user_id,))
415
- return cur.fetchall()
416
-
417
- def create_api_token(self, user_id, name, expires_in_days=None):
418
- """Create a new API token for a user."""
419
- token = ApiToken(user_id, name, expires_in_days)
420
-
421
- with self.db.get_cursor() as cur:
422
- cur.execute("""
423
- INSERT INTO api_tokens (id, user_id, name, token, created_at, expires_at)
424
- VALUES (%s, %s, %s, %s, %s, %s)
425
- """, (token.id, token.user_id, token.name, token.token, token.created_at, token.expires_at))
426
- return token
427
-
428
- def _create_token(self, user):
429
- payload = {
430
- 'sub': str(user['id']),
431
- 'exp': datetime.utcnow() + timedelta(hours=1),
432
- 'iat': datetime.utcnow()
433
- }
434
- logger.debug(f"Creating token with payload: {payload}")
435
- token = jwt.encode(payload, self.jwt_secret, algorithm='HS256')
436
- logger.debug(f"Created token: {token}")
437
- return token
438
-
439
- def _create_refresh_token(self, user):
440
- payload = {
441
- 'sub': str(user['id']),
442
- 'exp': datetime.utcnow() + timedelta(days=30),
443
- 'iat': datetime.utcnow()
444
- }
445
- return jwt.encode(payload, self.jwt_secret, algorithm='HS256')
446
-
447
- def _verify_password(self, password, password_hash):
448
- return bcrypt.checkpw(password.encode('utf-8'), password_hash.encode('utf-8'))
449
-
450
- def _get_oauth_url(self, provider, redirect_uri):
451
- if provider == 'google':
452
- client_id = self.oauth_config['google']['client_id']
453
- scope = 'openid email profile'
454
- state = provider # Pass provider as state for callback
455
- return f'https://accounts.google.com/o/oauth2/v2/auth?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code&scope={scope}&state={state}'
456
- raise AuthError('Invalid OAuth provider')
457
-
458
- def _get_oauth_user_info(self, provider, code):
459
- if provider == 'google':
460
- client_id = self.oauth_config['google']['client_id']
461
- client_secret = self.oauth_config['google']['client_secret']
462
- redirect_uri = url_for('auth.oauth_callback', _external=True)
463
- redirect_uri = redirect_uri.replace("http://", "https://")
464
-
465
- # Exchange code for tokens
466
- token_url = 'https://oauth2.googleapis.com/token'
467
- token_data = {
468
- 'client_id': client_id,
469
- 'client_secret': client_secret,
470
- 'code': code,
471
- 'grant_type': 'authorization_code',
472
- 'redirect_uri': redirect_uri
473
- }
474
- token_response = requests.post(token_url, data=token_data)
475
- logger.info("TOKEN RESPONSE: {} {} {} [[[{}]]]".format(token_response.text, token_response.status_code, token_response.headers, token_data))
476
- token_response.raise_for_status()
477
- tokens = token_response.json()
478
-
479
- # Get user info
480
- userinfo_url = 'https://www.googleapis.com/oauth2/v3/userinfo'
481
- userinfo_response = requests.get(
482
- userinfo_url,
483
- headers={'Authorization': f"Bearer {tokens['access_token']}"}
484
- )
485
- userinfo_response.raise_for_status()
486
- userinfo = userinfo_response.json()
487
-
488
- # Create or update user
489
- with self.db.get_cursor() as cur:
490
- cur.execute("SELECT * FROM users WHERE email = %s", (userinfo['email'],))
491
- user = cur.fetchone()
492
-
493
- if not user:
494
- # Create new user
495
- user = User(
496
- username=userinfo['email'],
497
- email=userinfo['email'],
498
- real_name=userinfo.get('name', userinfo['email']),
499
- id_generator=self.db.get_id_generator()
500
- )
501
- cur.execute("""
502
- INSERT INTO users (username, email, real_name, created_at, updated_at)
503
- VALUES (%s, %s, %s, %s, %s)
504
- RETURNING id
505
- """, (user.username, user.email, user.real_name,
506
- user.created_at, user.updated_at))
507
- user.id = cur.fetchone()['id']
508
- user = {'id': user.id, 'username': user.username, 'email': user.email,
509
- 'real_name': user.real_name, 'roles': []}
510
- else:
511
- # Update existing user
512
- cur.execute("""
513
- UPDATE users
514
- SET real_name = %s, updated_at = %s
515
- WHERE email = %s
516
- """, (userinfo.get('name', userinfo['email']), datetime.utcnow(), userinfo['email']))
517
- user['real_name'] = userinfo.get('name', userinfo['email'])
518
-
519
- return user
1
+ import inspect
2
+ from flask import Blueprint, request, jsonify, current_app, url_for, redirect
3
+ import jwt
4
+ from datetime import datetime, timedelta
5
+ from .db import Database
6
+ from .models import User, Role, ApiToken
7
+ from .exceptions import AuthError
8
+ import uuid
9
+ import requests
10
+ import bcrypt
11
+ import logging
12
+ import os
13
+ from functools import wraps
14
+
15
+ logging.basicConfig(level=logging.DEBUG)
16
+ logger = logging.getLogger(__name__)
17
+
18
+ def handle_auth_errors(f):
19
+ @wraps(f)
20
+ def decorated(*args, **kwargs):
21
+ try:
22
+ return f(*args, **kwargs)
23
+ except AuthError as e:
24
+ response = jsonify(e.to_dict())
25
+ response.status_code = e.status_code
26
+ return response
27
+ return decorated
28
+
29
+ class AuthManager:
30
+ def __init__(self, app=None, db_dsn=None, jwt_secret=None, oauth_config=None, id_type='integer'):
31
+ logger.info("INITIALIZING AUTHMANAGER {} - {} - {}".format(db_dsn, jwt_secret, not app))
32
+ self.db = Database(db_dsn, id_type=id_type) if db_dsn else None
33
+ self.jwt_secret = jwt_secret
34
+ logger.debug(f"Initializing AuthManager with JWT secret: {jwt_secret[:5]}..." if jwt_secret else "No JWT secret provided")
35
+ self.oauth_config = oauth_config or {}
36
+
37
+ if app:
38
+ self.init_app(app)
39
+
40
+ def _extract_token_from_header(self):
41
+ auth = request.authorization
42
+ if not auth or not auth.token:
43
+ raise AuthError('No authorization header or token', 401)
44
+
45
+ if auth.type.lower() != 'bearer':
46
+ raise AuthError('Invalid authorization scheme', 401)
47
+
48
+ return auth.token
49
+
50
+ def _validate_api_token(self, api_token):
51
+ try:
52
+ parsed = ApiToken.parse_token(api_token)
53
+ with self.db.get_cursor() as cur:
54
+ # First get the API token record
55
+ cur.execute("""
56
+ SELECT t.*, u.* FROM api_tokens t
57
+ JOIN users u ON t.user_id = u.id
58
+ WHERE t.id = %s
59
+ """, (parsed['id'],))
60
+ result = cur.fetchone()
61
+ if not result:
62
+ raise AuthError('Invalid API token')
63
+
64
+ # Verify the nonce
65
+ if not bcrypt.checkpw(parsed['nonce'].encode('utf-8'), result['token'].encode('utf-8')):
66
+ raise AuthError('Invalid API token')
67
+
68
+ # Check if token is expired
69
+ if result['expires_at'] and result['expires_at'] < datetime.utcnow():
70
+ raise AuthError('API token has expired')
71
+
72
+ # Update last used timestamp
73
+ cur.execute("""
74
+ UPDATE api_tokens
75
+ SET last_used_at = %s
76
+ WHERE id = %s
77
+ """, (datetime.utcnow(), parsed['id']))
78
+
79
+ # Fetch roles
80
+ cur.execute("""
81
+ SELECT r.name FROM roles r
82
+ JOIN user_roles ur ON ur.role_id = r.id
83
+ WHERE ur.user_id = %s
84
+ """, (result['user_id'],))
85
+ roles = [row['name'] for row in cur.fetchall()]
86
+
87
+ # Construct user object
88
+ return {
89
+ 'id': result['user_id'],
90
+ 'username': result['username'],
91
+ 'email': result['email'],
92
+ 'real_name': result['real_name'],
93
+ 'roles': roles
94
+ }
95
+ except ValueError:
96
+ raise AuthError('Invalid token format')
97
+
98
+ def _authenticate_request(self):
99
+ auth_header = request.headers.get('Authorization')
100
+ api_token = request.headers.get('X-API-Token')
101
+
102
+ if auth_header and auth_header.startswith('Bearer '):
103
+ # JWT authentication
104
+ token = self._extract_token_from_header()
105
+ return self.validate_token(token)
106
+ elif api_token:
107
+ # API token authentication
108
+ return self._validate_api_token(api_token)
109
+ else:
110
+ raise AuthError('No authentication provided', 401)
111
+
112
+ def require_auth(self, f):
113
+ @wraps(f)
114
+ def decorated(*args, **kwargs):
115
+ user = self._authenticate_request()
116
+ sig = inspect.signature(f)
117
+ if 'requesting_user' in sig.parameters:
118
+ kwargs['requesting_user'] = user
119
+
120
+ return f(*args, **kwargs)
121
+ return decorated
122
+
123
+ def init_app(self, app):
124
+ app.auth_manager = self
125
+ app.register_blueprint(self.create_blueprint())
126
+
127
+ def create_blueprint(self):
128
+ bp = Blueprint('auth', __name__, url_prefix='/v1/users')
129
+
130
+ @bp.route('/login', methods=['POST'])
131
+ @handle_auth_errors
132
+ def login():
133
+ data = request.get_json()
134
+ username = data.get('username')
135
+ password = data.get('password')
136
+
137
+ if not username or not password:
138
+ raise AuthError('Username and password required', 400)
139
+
140
+ with self.db.get_cursor() as cur:
141
+ cur.execute("SELECT * FROM users WHERE username = %s", (username,))
142
+ user = cur.fetchone()
143
+
144
+ if not user or not self._verify_password(password, user['password_hash']):
145
+ raise AuthError('Invalid username or password', 401)
146
+
147
+ # Fetch roles
148
+ cur.execute("""
149
+ SELECT r.name FROM roles r
150
+ JOIN user_roles ur ON ur.role_id = r.id
151
+ WHERE ur.user_id = %s
152
+ """, (user['id'],))
153
+ roles = [row['name'] for row in cur.fetchall()]
154
+ user['roles'] = roles
155
+
156
+ token = self._create_token(user)
157
+ refresh_token = self._create_refresh_token(user)
158
+
159
+ return jsonify({
160
+ 'token': token,
161
+ 'refresh_token': refresh_token,
162
+ 'user': user
163
+ })
164
+
165
+ @bp.route('/login/oauth', methods=['POST'])
166
+ @handle_auth_errors
167
+ def oauth_login():
168
+ provider = request.json.get('provider')
169
+ if provider not in self.oauth_config:
170
+ raise AuthError('Invalid OAuth provider', 400)
171
+
172
+ redirect_uri = url_for('auth.oauth_callback', _external=True)
173
+ logger.info("REDIRECT URI: {}".format(redirect_uri))
174
+ redirect_uri = redirect_uri.replace("http://", "https://")
175
+ return jsonify({
176
+ 'redirect_url': self._get_oauth_url(provider, redirect_uri)
177
+ })
178
+
179
+ @bp.route('/login/oauth2callback')
180
+ @handle_auth_errors
181
+ def oauth_callback():
182
+ code = request.args.get('code')
183
+ provider = request.args.get('state')
184
+
185
+ if not code or not provider:
186
+ raise AuthError('Invalid OAuth callback', 400)
187
+
188
+ user_info = self._get_oauth_user_info(provider, code)
189
+ token = self._create_token(user_info)
190
+ refresh_token = self._create_refresh_token(user_info)
191
+
192
+ # Redirect to frontend with tokens
193
+ frontend_url = os.getenv('FRONTEND_URL', 'http://localhost:5173')
194
+ return redirect(f"{frontend_url}/oauth-callback?token={token}&refresh_token={refresh_token}")
195
+
196
+ @bp.route('/login/profile')
197
+ @handle_auth_errors
198
+ def profile():
199
+ token = request.headers.get('Authorization', '').split(' ')[-1]
200
+ user = self.validate_token(token)
201
+ return jsonify(user)
202
+
203
+ @bp.route('/api-tokens', methods=['GET'])
204
+ @handle_auth_errors
205
+ @self.require_auth
206
+ def get_tokens(requesting_user):
207
+ tokens = self.get_user_api_tokens(requesting_user['id'])
208
+ return jsonify(tokens)
209
+
210
+ @bp.route('/api-tokens', methods=['POST'])
211
+ @handle_auth_errors
212
+ @self.require_auth
213
+ def create_token(requesting_user):
214
+ name = request.json.get('name')
215
+ expires_in_days = request.json.get('expires_in_days')
216
+ if not name:
217
+ raise AuthError('Token name is required', 400)
218
+ api_token = self.create_api_token(requesting_user['id'], name, expires_in_days)
219
+ return jsonify({
220
+ 'id': api_token.id,
221
+ 'name': api_token.name,
222
+ 'token': api_token.get_full_token(),
223
+ 'created_at': api_token.created_at,
224
+ 'expires_at': api_token.expires_at
225
+ })
226
+
227
+ @bp.route('/token-refresh', methods=['POST'])
228
+ @handle_auth_errors
229
+ def refresh_token():
230
+ refresh_token = request.json.get('refresh_token')
231
+ if not refresh_token:
232
+ raise AuthError('No refresh token provided', 400)
233
+
234
+ try:
235
+ payload = jwt.decode(refresh_token, self.jwt_secret, algorithms=['HS256'])
236
+ user_id = payload['sub']
237
+
238
+ with self.db.get_cursor() as cur:
239
+ cur.execute("SELECT * FROM users WHERE id = %s", (user_id,))
240
+ user = cur.fetchone()
241
+
242
+ if not user:
243
+ raise AuthError('User not found', 404)
244
+
245
+ return jsonify({
246
+ 'token': self._create_token(user),
247
+ 'refresh_token': self._create_refresh_token(user)
248
+ })
249
+ except jwt.InvalidTokenError:
250
+ raise AuthError('Invalid refresh token', 401)
251
+
252
+ @bp.route('/api-tokens', methods=['POST'])
253
+ @handle_auth_errors
254
+ @self.require_auth
255
+ def create_api_token(requesting_user):
256
+ name = request.json.get('name')
257
+ if not name:
258
+ raise AuthError('Token name required', 400)
259
+
260
+ token = self.create_api_token(requesting_user['id'], name)
261
+ return jsonify({'token': token.token})
262
+
263
+ @bp.route('/api-tokens/validate', methods=['GET'])
264
+ @handle_auth_errors
265
+ @self.require_auth
266
+ def validate_api_token(requesting_user):
267
+ token = request.json.get('token')
268
+ if not token:
269
+ raise AuthError('No API token provided', 401)
270
+ token = ApiToken.parse_token_id(token)
271
+
272
+ with self.db.get_cursor() as cur:
273
+ cur.execute("""
274
+ SELECT * FROM api_tokens
275
+ WHERE user_id = %s AND id = %s
276
+ """, (requesting_user['id'], token))
277
+ api_token = cur.fetchone()
278
+
279
+ if not api_token:
280
+ raise AuthError('Invalid API token', 401)
281
+
282
+ # Check if token is expired
283
+ if api_token['expires_at'] and api_token['expires_at'] < datetime.utcnow():
284
+ raise AuthError('API token has expired', 401)
285
+
286
+ # Update last used timestamp
287
+ with self.db.get_cursor() as cur:
288
+ cur.execute("""
289
+ UPDATE api_tokens
290
+ SET last_used_at = %s
291
+ WHERE id = %s
292
+ """, (datetime.utcnow(), api_token['id']))
293
+
294
+ return jsonify({'valid': True})
295
+
296
+ @bp.route('/api-tokens', methods=['DELETE'])
297
+ @handle_auth_errors
298
+ @self.require_auth
299
+ def delete_api_token(requesting_user):
300
+ token = request.json.get('token')
301
+ if not token:
302
+ raise AuthError('Token required', 400)
303
+ token = ApiToken.parse_token_id(token)
304
+
305
+ with self.db.get_cursor() as cur:
306
+ cur.execute("""
307
+ DELETE FROM api_tokens
308
+ WHERE user_id = %s AND id = %s
309
+ RETURNING id
310
+ """, (requesting_user['id'], token))
311
+ deleted_id = cur.fetchone()
312
+ if not deleted_id:
313
+ raise ValueError('Token not found or already deleted')
314
+
315
+ return jsonify({'deleted': True})
316
+
317
+ @bp.route('/register', methods=['POST'])
318
+ @handle_auth_errors
319
+ def register():
320
+ data = request.get_json()
321
+
322
+ # Hash the password
323
+ password = data.get('password')
324
+ if not password:
325
+ raise AuthError('Password is required', 400)
326
+
327
+ salt = bcrypt.gensalt()
328
+ password_hash = bcrypt.hashpw(password.encode('utf-8'), salt)
329
+
330
+ user = User(
331
+ username=data['username'],
332
+ email=data['email'],
333
+ real_name=data['real_name'],
334
+ roles=data.get('roles', []),
335
+ id_generator=self.db.get_id_generator()
336
+ )
337
+
338
+ with self.db.get_cursor() as cur:
339
+ if user.id is None:
340
+ cur.execute("""
341
+ INSERT INTO users (username, email, real_name, password_hash, created_at, updated_at)
342
+ VALUES (%s, %s, %s, %s, %s, %s)
343
+ RETURNING id
344
+ """, (user.username, user.email, user.real_name, password_hash.decode('utf-8'),
345
+ user.created_at, user.updated_at))
346
+ user.id = cur.fetchone()['id']
347
+ else:
348
+ cur.execute("""
349
+ INSERT INTO users (id, username, email, real_name, password_hash, created_at, updated_at)
350
+ VALUES (%s, %s, %s, %s, %s, %s, %s)
351
+ """, (user.id, user.username, user.email, user.real_name, password_hash.decode('utf-8'),
352
+ user.created_at, user.updated_at))
353
+
354
+ return jsonify({'id': user.id}), 201
355
+
356
+ @bp.route('/roles', methods=['GET'])
357
+ @handle_auth_errors
358
+ def get_roles():
359
+ with self.db.get_cursor() as cur:
360
+ cur.execute("SELECT * FROM roles")
361
+ roles = cur.fetchall()
362
+ return jsonify(roles)
363
+
364
+ return bp
365
+
366
+ def validate_token(self, token):
367
+ try:
368
+ logger.debug(f"Validating token: {token}")
369
+ payload = jwt.decode(token, self.jwt_secret, algorithms=['HS256'])
370
+ logger.debug(f"Token payload: {payload}")
371
+ user_id = int(payload['sub']) # Convert string ID back to integer
372
+
373
+ with self.db.get_cursor() as cur:
374
+ cur.execute("SELECT * FROM users WHERE id = %s", (user_id,))
375
+ user = cur.fetchone()
376
+ if not user:
377
+ logger.error(f"User not found for ID: {user_id}")
378
+ raise AuthError('User not found', 404)
379
+ # Fetch roles
380
+ cur.execute("""
381
+ SELECT r.name FROM roles r
382
+ JOIN user_roles ur ON ur.role_id = r.id
383
+ WHERE ur.user_id = %s
384
+ """, (user_id,))
385
+ roles = [row['name'] for row in cur.fetchall()]
386
+ user['roles'] = roles
387
+
388
+ return user
389
+ except jwt.InvalidTokenError as e:
390
+ logger.error(f"Invalid token error: {str(e)}")
391
+ raise AuthError('Invalid token', 401)
392
+ except Exception as e:
393
+ logger.error(f"Unexpected error during token validation: {str(e)}")
394
+ raise AuthError(str(e), 500)
395
+
396
+ def get_current_user(self):
397
+ return self._authenticate_request()
398
+
399
+ def get_user_api_tokens(self, user_id):
400
+ """Get all API tokens for a user."""
401
+ with self.db.get_cursor() as cur:
402
+ cur.execute("""
403
+ SELECT id, name, created_at, expires_at, last_used_at
404
+ FROM api_tokens
405
+ WHERE user_id = %s
406
+ ORDER BY created_at DESC
407
+ """, (user_id,))
408
+ return cur.fetchall()
409
+
410
+ def create_api_token(self, user_id, name, expires_in_days=None):
411
+ """Create a new API token for a user."""
412
+ token = ApiToken(user_id, name, expires_in_days)
413
+
414
+ with self.db.get_cursor() as cur:
415
+ cur.execute("""
416
+ INSERT INTO api_tokens (id, user_id, name, token, created_at, expires_at)
417
+ VALUES (%s, %s, %s, %s, %s, %s)
418
+ """, (token.id, token.user_id, token.name, token.token, token.created_at, token.expires_at))
419
+ return token
420
+
421
+ def _create_token(self, user):
422
+ payload = {
423
+ 'sub': str(user['id']),
424
+ 'exp': datetime.utcnow() + timedelta(hours=1),
425
+ 'iat': datetime.utcnow()
426
+ }
427
+ logger.debug(f"Creating token with payload: {payload}")
428
+ token = jwt.encode(payload, self.jwt_secret, algorithm='HS256')
429
+ logger.debug(f"Created token: {token}")
430
+ return token
431
+
432
+ def _create_refresh_token(self, user):
433
+ payload = {
434
+ 'sub': str(user['id']),
435
+ 'exp': datetime.utcnow() + timedelta(days=30),
436
+ 'iat': datetime.utcnow()
437
+ }
438
+ return jwt.encode(payload, self.jwt_secret, algorithm='HS256')
439
+
440
+ def _verify_password(self, password, password_hash):
441
+ return bcrypt.checkpw(password.encode('utf-8'), password_hash.encode('utf-8'))
442
+
443
+ def _get_oauth_url(self, provider, redirect_uri):
444
+ if provider == 'google':
445
+ client_id = self.oauth_config['google']['client_id']
446
+ scope = 'openid email profile'
447
+ state = provider # Pass provider as state for callback
448
+ return f'https://accounts.google.com/o/oauth2/v2/auth?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code&scope={scope}&state={state}'
449
+ raise AuthError('Invalid OAuth provider')
450
+
451
+ def _get_oauth_user_info(self, provider, code):
452
+ if provider == 'google':
453
+ client_id = self.oauth_config['google']['client_id']
454
+ client_secret = self.oauth_config['google']['client_secret']
455
+ redirect_uri = url_for('auth.oauth_callback', _external=True)
456
+ redirect_uri = redirect_uri.replace("http://", "https://")
457
+
458
+ # Exchange code for tokens
459
+ token_url = 'https://oauth2.googleapis.com/token'
460
+ token_data = {
461
+ 'client_id': client_id,
462
+ 'client_secret': client_secret,
463
+ 'code': code,
464
+ 'grant_type': 'authorization_code',
465
+ 'redirect_uri': redirect_uri
466
+ }
467
+ token_response = requests.post(token_url, data=token_data)
468
+ logger.info("TOKEN RESPONSE: {} {} {} [[[{}]]]".format(token_response.text, token_response.status_code, token_response.headers, token_data))
469
+ token_response.raise_for_status()
470
+ tokens = token_response.json()
471
+
472
+ # Get user info
473
+ userinfo_url = 'https://www.googleapis.com/oauth2/v3/userinfo'
474
+ userinfo_response = requests.get(
475
+ userinfo_url,
476
+ headers={'Authorization': f"Bearer {tokens['access_token']}"}
477
+ )
478
+ userinfo_response.raise_for_status()
479
+ userinfo = userinfo_response.json()
480
+
481
+ # Create or update user
482
+ with self.db.get_cursor() as cur:
483
+ cur.execute("SELECT * FROM users WHERE email = %s", (userinfo['email'],))
484
+ user = cur.fetchone()
485
+
486
+ if not user:
487
+ # Create new user
488
+ user = User(
489
+ username=userinfo['email'],
490
+ email=userinfo['email'],
491
+ real_name=userinfo.get('name', userinfo['email']),
492
+ id_generator=self.db.get_id_generator()
493
+ )
494
+ cur.execute("""
495
+ INSERT INTO users (username, email, real_name, created_at, updated_at)
496
+ VALUES (%s, %s, %s, %s, %s)
497
+ RETURNING id
498
+ """, (user.username, user.email, user.real_name,
499
+ user.created_at, user.updated_at))
500
+ user.id = cur.fetchone()['id']
501
+ user = {'id': user.id, 'username': user.username, 'email': user.email,
502
+ 'real_name': user.real_name, 'roles': []}
503
+ else:
504
+ # Update existing user
505
+ cur.execute("""
506
+ UPDATE users
507
+ SET real_name = %s, updated_at = %s
508
+ WHERE email = %s
509
+ """, (userinfo.get('name', userinfo['email']), datetime.utcnow(), userinfo['email']))
510
+ user['real_name'] = userinfo.get('name', userinfo['email'])
511
+
512
+ return user
520
513
  raise AuthError('Invalid OAuth provider')