the37lab-authlib 0.1.1757062387__tar.gz → 0.1.1758264497__tar.gz

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.

Files changed (15) hide show
  1. {the37lab_authlib-0.1.1757062387 → the37lab_authlib-0.1.1758264497}/PKG-INFO +1 -1
  2. {the37lab_authlib-0.1.1757062387 → the37lab_authlib-0.1.1758264497}/pyproject.toml +1 -1
  3. {the37lab_authlib-0.1.1757062387 → the37lab_authlib-0.1.1758264497}/src/the37lab_authlib/auth.py +291 -0
  4. {the37lab_authlib-0.1.1757062387 → the37lab_authlib-0.1.1758264497}/src/the37lab_authlib.egg-info/PKG-INFO +1 -1
  5. {the37lab_authlib-0.1.1757062387 → the37lab_authlib-0.1.1758264497}/README.md +0 -0
  6. {the37lab_authlib-0.1.1757062387 → the37lab_authlib-0.1.1758264497}/setup.cfg +0 -0
  7. {the37lab_authlib-0.1.1757062387 → the37lab_authlib-0.1.1758264497}/src/the37lab_authlib/__init__.py +0 -0
  8. {the37lab_authlib-0.1.1757062387 → the37lab_authlib-0.1.1758264497}/src/the37lab_authlib/db.py +0 -0
  9. {the37lab_authlib-0.1.1757062387 → the37lab_authlib-0.1.1758264497}/src/the37lab_authlib/decorators.py +0 -0
  10. {the37lab_authlib-0.1.1757062387 → the37lab_authlib-0.1.1758264497}/src/the37lab_authlib/exceptions.py +0 -0
  11. {the37lab_authlib-0.1.1757062387 → the37lab_authlib-0.1.1758264497}/src/the37lab_authlib/models.py +0 -0
  12. {the37lab_authlib-0.1.1757062387 → the37lab_authlib-0.1.1758264497}/src/the37lab_authlib.egg-info/SOURCES.txt +0 -0
  13. {the37lab_authlib-0.1.1757062387 → the37lab_authlib-0.1.1758264497}/src/the37lab_authlib.egg-info/dependency_links.txt +0 -0
  14. {the37lab_authlib-0.1.1757062387 → the37lab_authlib-0.1.1758264497}/src/the37lab_authlib.egg-info/requires.txt +0 -0
  15. {the37lab_authlib-0.1.1757062387 → the37lab_authlib-0.1.1758264497}/src/the37lab_authlib.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: the37lab_authlib
3
- Version: 0.1.1757062387
3
+ Version: 0.1.1758264497
4
4
  Summary: Python SDK for the Authlib
5
5
  Author-email: the37lab <info@the37lab.com>
6
6
  Classifier: Programming Language :: Python :: 3
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "the37lab_authlib"
7
- version = "0.1.1757062387"
7
+ version = "0.1.1758264497"
8
8
  description = "Python SDK for the Authlib"
9
9
  authors = [{name = "the37lab", email = "info@the37lab.com"}]
10
10
  dependencies = ["flask", "psycopg2-binary", "pyjwt", "python-dotenv", "requests", "authlib", "bcrypt", "msal"]
@@ -488,6 +488,291 @@ class AuthManager:
488
488
  roles = cur.fetchall()
489
489
  return jsonify(roles)
490
490
 
491
+ # Admin endpoints - require administrator role
492
+ @bp.route('/admin/users', methods=['GET'])
493
+ def admin_get_users():
494
+ self._require_admin_role()
495
+ with self.db.get_cursor() as cur:
496
+ cur.execute("""
497
+ SELECT u.*,
498
+ COALESCE(array_agg(r.name) FILTER (WHERE r.name IS NOT NULL), '{}') as roles
499
+ FROM users u
500
+ LEFT JOIN user_roles ur ON ur.user_id = u.id
501
+ LEFT JOIN roles r ON ur.role_id = r.id
502
+ GROUP BY u.id, u.username, u.email, u.real_name, u.created_at, u.updated_at
503
+ ORDER BY u.created_at DESC
504
+ """)
505
+ users = cur.fetchall()
506
+ return jsonify(users)
507
+
508
+ @bp.route('/admin/users', methods=['POST'])
509
+ def admin_create_user():
510
+ self._require_admin_role()
511
+ data = request.get_json()
512
+
513
+ # Validate required fields
514
+ required_fields = ['username', 'email', 'real_name', 'password']
515
+ for field in required_fields:
516
+ if not data.get(field):
517
+ raise AuthError(f'{field} is required', 400)
518
+
519
+ # Hash the password
520
+ salt = bcrypt.gensalt()
521
+ password_hash = bcrypt.hashpw(data['password'].encode('utf-8'), salt)
522
+
523
+ with self.db.get_cursor() as cur:
524
+ # Check if username or email already exists
525
+ cur.execute("SELECT id FROM users WHERE username = %s OR email = %s",
526
+ (data['username'], data['email']))
527
+ if cur.fetchone():
528
+ raise AuthError('Username or email already exists', 400)
529
+
530
+ # Create user
531
+ cur.execute("""
532
+ INSERT INTO users (username, email, real_name, password_hash, created_at, updated_at)
533
+ VALUES (%s, %s, %s, %s, %s, %s)
534
+ RETURNING id
535
+ """, (data['username'], data['email'], data['real_name'],
536
+ password_hash.decode('utf-8'), datetime.utcnow(), datetime.utcnow()))
537
+ user_id = cur.fetchone()['id']
538
+
539
+ # Assign roles if provided
540
+ if data.get('roles'):
541
+ for role_name in data['roles']:
542
+ cur.execute("SELECT id FROM roles WHERE name = %s", (role_name,))
543
+ role = cur.fetchone()
544
+ if role:
545
+ cur.execute("""
546
+ INSERT INTO user_roles (user_id, role_id)
547
+ VALUES (%s, %s)
548
+ ON CONFLICT (user_id, role_id) DO NOTHING
549
+ """, (user_id, role['id']))
550
+
551
+ return jsonify({'id': user_id}), 201
552
+
553
+ @bp.route('/admin/users/<user_id>', methods=['PUT'])
554
+ def admin_update_user(user_id):
555
+ self._require_admin_role()
556
+ data = request.get_json()
557
+
558
+ with self.db.get_cursor() as cur:
559
+ # Check if user exists
560
+ cur.execute("SELECT id FROM users WHERE id = %s", (user_id,))
561
+ if not cur.fetchone():
562
+ raise AuthError('User not found', 404)
563
+
564
+ # Update user fields
565
+ update_fields = []
566
+ update_values = []
567
+
568
+ if 'username' in data:
569
+ update_fields.append('username = %s')
570
+ update_values.append(data['username'])
571
+ if 'email' in data:
572
+ update_fields.append('email = %s')
573
+ update_values.append(data['email'])
574
+ if 'real_name' in data:
575
+ update_fields.append('real_name = %s')
576
+ update_values.append(data['real_name'])
577
+ if 'password' in data:
578
+ salt = bcrypt.gensalt()
579
+ password_hash = bcrypt.hashpw(data['password'].encode('utf-8'), salt)
580
+ update_fields.append('password_hash = %s')
581
+ update_values.append(password_hash.decode('utf-8'))
582
+
583
+ if update_fields:
584
+ update_fields.append('updated_at = %s')
585
+ update_values.append(datetime.utcnow())
586
+ update_values.append(user_id)
587
+
588
+ cur.execute(f"""
589
+ UPDATE users
590
+ SET {', '.join(update_fields)}
591
+ WHERE id = %s
592
+ """, update_values)
593
+
594
+ # Update roles if provided
595
+ if 'roles' in data:
596
+ # Remove existing roles
597
+ cur.execute("DELETE FROM user_roles WHERE user_id = %s", (user_id,))
598
+
599
+ # Add new roles
600
+ for role_name in data['roles']:
601
+ cur.execute("SELECT id FROM roles WHERE name = %s", (role_name,))
602
+ role = cur.fetchone()
603
+ if role:
604
+ cur.execute("""
605
+ INSERT INTO user_roles (user_id, role_id)
606
+ VALUES (%s, %s)
607
+ """, (user_id, role['id']))
608
+
609
+ return jsonify({'success': True})
610
+
611
+ @bp.route('/admin/users/<user_id>', methods=['DELETE'])
612
+ def admin_delete_user(user_id):
613
+ self._require_admin_role()
614
+
615
+ with self.db.get_cursor() as cur:
616
+ # Check if user exists
617
+ cur.execute("SELECT id FROM users WHERE id = %s", (user_id,))
618
+ if not cur.fetchone():
619
+ raise AuthError('User not found', 404)
620
+
621
+ # Delete user (cascade will handle related records)
622
+ cur.execute("DELETE FROM users WHERE id = %s", (user_id,))
623
+
624
+ return jsonify({'success': True})
625
+
626
+ @bp.route('/admin/roles', methods=['GET'])
627
+ def admin_get_roles():
628
+ self._require_admin_role()
629
+ with self.db.get_cursor() as cur:
630
+ cur.execute("SELECT * FROM roles ORDER BY name")
631
+ roles = cur.fetchall()
632
+ return jsonify(roles)
633
+
634
+ @bp.route('/admin/roles', methods=['POST'])
635
+ def admin_create_role():
636
+ self._require_admin_role()
637
+ data = request.get_json()
638
+
639
+ if not data.get('name'):
640
+ raise AuthError('Role name is required', 400)
641
+
642
+ with self.db.get_cursor() as cur:
643
+ # Check if role already exists
644
+ cur.execute("SELECT id FROM roles WHERE name = %s", (data['name'],))
645
+ if cur.fetchone():
646
+ raise AuthError('Role already exists', 400)
647
+
648
+ cur.execute("""
649
+ INSERT INTO roles (name, description, created_at)
650
+ VALUES (%s, %s, %s)
651
+ RETURNING id
652
+ """, (data['name'], data.get('description', ''), datetime.utcnow()))
653
+ role_id = cur.fetchone()['id']
654
+
655
+ return jsonify({'id': role_id}), 201
656
+
657
+ @bp.route('/admin/roles/<role_id>', methods=['PUT'])
658
+ def admin_update_role(role_id):
659
+ self._require_admin_role()
660
+ data = request.get_json()
661
+
662
+ with self.db.get_cursor() as cur:
663
+ # Check if role exists
664
+ cur.execute("SELECT id FROM roles WHERE id = %s", (role_id,))
665
+ if not cur.fetchone():
666
+ raise AuthError('Role not found', 404)
667
+
668
+ update_fields = []
669
+ update_values = []
670
+
671
+ if 'name' in data:
672
+ update_fields.append('name = %s')
673
+ update_values.append(data['name'])
674
+ if 'description' in data:
675
+ update_fields.append('description = %s')
676
+ update_values.append(data['description'])
677
+
678
+ if update_fields:
679
+ update_values.append(role_id)
680
+ cur.execute(f"""
681
+ UPDATE roles
682
+ SET {', '.join(update_fields)}
683
+ WHERE id = %s
684
+ """, update_values)
685
+
686
+ return jsonify({'success': True})
687
+
688
+ @bp.route('/admin/roles/<role_id>', methods=['DELETE'])
689
+ def admin_delete_role(role_id):
690
+ self._require_admin_role()
691
+
692
+ with self.db.get_cursor() as cur:
693
+ # Check if role exists
694
+ cur.execute("SELECT id FROM roles WHERE id = %s", (role_id,))
695
+ if not cur.fetchone():
696
+ raise AuthError('Role not found', 404)
697
+
698
+ # Check if role is assigned to any users
699
+ cur.execute("SELECT COUNT(*) as count FROM user_roles WHERE role_id = %s", (role_id,))
700
+ count = cur.fetchone()['count']
701
+ if count > 0:
702
+ raise AuthError('Cannot delete role that is assigned to users', 400)
703
+
704
+ cur.execute("DELETE FROM roles WHERE id = %s", (role_id,))
705
+
706
+ return jsonify({'success': True})
707
+
708
+ @bp.route('/admin/api-tokens', methods=['GET'])
709
+ def admin_get_all_tokens():
710
+ self._require_admin_role()
711
+ with self.db.get_cursor() as cur:
712
+ cur.execute("""
713
+ SELECT t.*, u.username, u.email
714
+ FROM api_tokens t
715
+ JOIN users u ON t.user_id = u.id
716
+ ORDER BY t.created_at DESC
717
+ """)
718
+ tokens = cur.fetchall()
719
+ return jsonify(tokens)
720
+
721
+ @bp.route('/admin/api-tokens', methods=['POST'])
722
+ def admin_create_token():
723
+ self._require_admin_role()
724
+ data = request.get_json()
725
+
726
+ if not data.get('user_id') or not data.get('name'):
727
+ raise AuthError('user_id and name are required', 400)
728
+
729
+ expires_in_days = data.get('expires_in_days')
730
+ token = self.create_api_token(data['user_id'], data['name'], expires_in_days)
731
+
732
+ return jsonify({
733
+ 'id': token.id,
734
+ 'name': token.name,
735
+ 'token': token.get_full_token(),
736
+ 'created_at': token.created_at,
737
+ 'expires_at': token.expires_at
738
+ }), 201
739
+
740
+ @bp.route('/admin/api-tokens/<token_id>', methods=['DELETE'])
741
+ def admin_delete_token(token_id):
742
+ self._require_admin_role()
743
+
744
+ with self.db.get_cursor() as cur:
745
+ cur.execute("DELETE FROM api_tokens WHERE id = %s", (token_id,))
746
+ if cur.rowcount == 0:
747
+ raise AuthError('Token not found', 404)
748
+
749
+ return jsonify({'success': True})
750
+
751
+ @bp.route('/admin/invite', methods=['POST'])
752
+ def admin_send_invitation():
753
+ self._require_admin_role()
754
+ data = request.get_json()
755
+
756
+ if not data.get('email'):
757
+ raise AuthError('Email is required', 400)
758
+
759
+ # Check if user already exists
760
+ with self.db.get_cursor() as cur:
761
+ cur.execute("SELECT id FROM users WHERE email = %s", (data['email'],))
762
+ if cur.fetchone():
763
+ raise AuthError('User with this email already exists', 400)
764
+
765
+ # Send invitation email (placeholder - implement actual email sending)
766
+ invitation_token = str(uuid.uuid4())
767
+
768
+ # Store invitation in database (you might want to create an invitations table)
769
+ # For now, we'll just return success
770
+ return jsonify({
771
+ 'success': True,
772
+ 'message': f'Invitation sent to {data["email"]}',
773
+ 'invitation_token': invitation_token
774
+ })
775
+
491
776
  return bp
492
777
 
493
778
  def validate_token(self, token):
@@ -616,6 +901,12 @@ class AuthManager:
616
901
  def get_current_user(self):
617
902
  return self._authenticate_request()
618
903
 
904
+ def _require_admin_role(self):
905
+ """Require the current user to have administrator role."""
906
+ user = g.requesting_user
907
+ if not user or 'administrator' not in user.get('roles', []):
908
+ raise AuthError('Administrator role required', 403)
909
+
619
910
  def get_user_api_tokens(self, user_id):
620
911
  """Get all API tokens for a user."""
621
912
  with self.db.get_cursor() as cur:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: the37lab_authlib
3
- Version: 0.1.1757062387
3
+ Version: 0.1.1758264497
4
4
  Summary: Python SDK for the Authlib
5
5
  Author-email: the37lab <info@the37lab.com>
6
6
  Classifier: Programming Language :: Python :: 3