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.
- {the37lab_authlib-0.1.1757062387 → the37lab_authlib-0.1.1758264497}/PKG-INFO +1 -1
- {the37lab_authlib-0.1.1757062387 → the37lab_authlib-0.1.1758264497}/pyproject.toml +1 -1
- {the37lab_authlib-0.1.1757062387 → the37lab_authlib-0.1.1758264497}/src/the37lab_authlib/auth.py +291 -0
- {the37lab_authlib-0.1.1757062387 → the37lab_authlib-0.1.1758264497}/src/the37lab_authlib.egg-info/PKG-INFO +1 -1
- {the37lab_authlib-0.1.1757062387 → the37lab_authlib-0.1.1758264497}/README.md +0 -0
- {the37lab_authlib-0.1.1757062387 → the37lab_authlib-0.1.1758264497}/setup.cfg +0 -0
- {the37lab_authlib-0.1.1757062387 → the37lab_authlib-0.1.1758264497}/src/the37lab_authlib/__init__.py +0 -0
- {the37lab_authlib-0.1.1757062387 → the37lab_authlib-0.1.1758264497}/src/the37lab_authlib/db.py +0 -0
- {the37lab_authlib-0.1.1757062387 → the37lab_authlib-0.1.1758264497}/src/the37lab_authlib/decorators.py +0 -0
- {the37lab_authlib-0.1.1757062387 → the37lab_authlib-0.1.1758264497}/src/the37lab_authlib/exceptions.py +0 -0
- {the37lab_authlib-0.1.1757062387 → the37lab_authlib-0.1.1758264497}/src/the37lab_authlib/models.py +0 -0
- {the37lab_authlib-0.1.1757062387 → the37lab_authlib-0.1.1758264497}/src/the37lab_authlib.egg-info/SOURCES.txt +0 -0
- {the37lab_authlib-0.1.1757062387 → the37lab_authlib-0.1.1758264497}/src/the37lab_authlib.egg-info/dependency_links.txt +0 -0
- {the37lab_authlib-0.1.1757062387 → the37lab_authlib-0.1.1758264497}/src/the37lab_authlib.egg-info/requires.txt +0 -0
- {the37lab_authlib-0.1.1757062387 → the37lab_authlib-0.1.1758264497}/src/the37lab_authlib.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "the37lab_authlib"
|
|
7
|
-
version = "0.1.
|
|
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"]
|
{the37lab_authlib-0.1.1757062387 → the37lab_authlib-0.1.1758264497}/src/the37lab_authlib/auth.py
RENAMED
|
@@ -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:
|
|
File without changes
|
|
File without changes
|
{the37lab_authlib-0.1.1757062387 → the37lab_authlib-0.1.1758264497}/src/the37lab_authlib/__init__.py
RENAMED
|
File without changes
|
{the37lab_authlib-0.1.1757062387 → the37lab_authlib-0.1.1758264497}/src/the37lab_authlib/db.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{the37lab_authlib-0.1.1757062387 → the37lab_authlib-0.1.1758264497}/src/the37lab_authlib/models.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|