the37lab-authlib 0.1.1758263039__py3-none-any.whl → 0.1.1758266466__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/db.py CHANGED
@@ -1,91 +1,91 @@
1
- import psycopg2
2
- from psycopg2.extras import RealDictCursor
3
- from psycopg2 import pool
4
- from contextlib import contextmanager
5
- from .models import UUIDGenerator, IntegerGenerator
6
-
7
- class Database:
8
- def __init__(self, dsn, id_type='uuid', min_conn=1, max_conn=10):
9
- self.dsn = dsn
10
- self.id_generator = UUIDGenerator() if id_type == 'uuid' else IntegerGenerator()
11
- self.id_type = id_type
12
- self.min_conn = min_conn
13
- self.max_conn = max_conn
14
- self._pool = None
15
- self._init_pool()
16
- self._init_db()
17
-
18
- def _init_pool(self):
19
- self._pool = pool.ThreadedConnectionPool(
20
- self.min_conn,
21
- self.max_conn,
22
- self.dsn,
23
- cursor_factory=RealDictCursor
24
- )
25
-
26
- def _init_db(self):
27
- with self.get_connection() as conn:
28
- with conn.cursor() as cur:
29
- # Create users table with configurable ID type
30
- cur.execute(f"""
31
- CREATE TABLE IF NOT EXISTS users (
32
- id {self._get_id_type()} PRIMARY KEY,
33
- username VARCHAR(255) UNIQUE NOT NULL,
34
- email VARCHAR(255) UNIQUE NOT NULL,
35
- real_name VARCHAR(255) NOT NULL,
36
- password_hash VARCHAR(255),
37
- created_at TIMESTAMP NOT NULL,
38
- updated_at TIMESTAMP NOT NULL
39
- );
40
-
41
- CREATE TABLE IF NOT EXISTS roles (
42
- id {self._get_id_type()} PRIMARY KEY,
43
- name VARCHAR(255) UNIQUE NOT NULL,
44
- description TEXT,
45
- created_at TIMESTAMP NOT NULL
46
- );
47
-
48
- CREATE TABLE IF NOT EXISTS user_roles (
49
- user_id {self._get_id_type()} REFERENCES users(id),
50
- role_id {self._get_id_type()} REFERENCES roles(id),
51
- PRIMARY KEY (user_id, role_id)
52
- );
53
-
54
- CREATE TABLE IF NOT EXISTS api_tokens (
55
- id VARCHAR(8) PRIMARY KEY,
56
- user_id INTEGER REFERENCES users(id),
57
- name VARCHAR(255) NOT NULL,
58
- token VARCHAR(255) NOT NULL,
59
- created_at TIMESTAMP NOT NULL,
60
- expires_at TIMESTAMP,
61
- last_used_at TIMESTAMP
62
- );
63
- """)
64
-
65
- def _get_id_type(self):
66
- return 'UUID' if self.id_type == 'uuid' else 'SERIAL'
67
-
68
- @contextmanager
69
- def get_connection(self):
70
- conn = self._pool.getconn()
71
- try:
72
- yield conn
73
- conn.commit()
74
- except Exception:
75
- conn.rollback()
76
- raise
77
- finally:
78
- self._pool.putconn(conn)
79
-
80
- @contextmanager
81
- def get_cursor(self):
82
- with self.get_connection() as conn:
83
- with conn.cursor() as cur:
84
- yield cur
85
-
86
- def get_id_generator(self):
87
- return self.id_generator
88
-
89
- def close(self):
90
- if self._pool:
1
+ import psycopg2
2
+ from psycopg2.extras import RealDictCursor
3
+ from psycopg2 import pool
4
+ from contextlib import contextmanager
5
+ from .models import UUIDGenerator, IntegerGenerator
6
+
7
+ class Database:
8
+ def __init__(self, dsn, id_type='uuid', min_conn=1, max_conn=10):
9
+ self.dsn = dsn
10
+ self.id_generator = UUIDGenerator() if id_type == 'uuid' else IntegerGenerator()
11
+ self.id_type = id_type
12
+ self.min_conn = min_conn
13
+ self.max_conn = max_conn
14
+ self._pool = None
15
+ self._init_pool()
16
+ self._init_db()
17
+
18
+ def _init_pool(self):
19
+ self._pool = pool.ThreadedConnectionPool(
20
+ self.min_conn,
21
+ self.max_conn,
22
+ self.dsn,
23
+ cursor_factory=RealDictCursor
24
+ )
25
+
26
+ def _init_db(self):
27
+ with self.get_connection() as conn:
28
+ with conn.cursor() as cur:
29
+ # Create users table with configurable ID type
30
+ cur.execute(f"""
31
+ CREATE TABLE IF NOT EXISTS users (
32
+ id {self._get_id_type()} PRIMARY KEY,
33
+ username VARCHAR(255) UNIQUE NOT NULL,
34
+ email VARCHAR(255) UNIQUE NOT NULL,
35
+ real_name VARCHAR(255) NOT NULL,
36
+ password_hash VARCHAR(255),
37
+ created_at TIMESTAMP NOT NULL,
38
+ updated_at TIMESTAMP NOT NULL
39
+ );
40
+
41
+ CREATE TABLE IF NOT EXISTS roles (
42
+ id {self._get_id_type()} PRIMARY KEY,
43
+ name VARCHAR(255) UNIQUE NOT NULL,
44
+ description TEXT,
45
+ created_at TIMESTAMP NOT NULL
46
+ );
47
+
48
+ CREATE TABLE IF NOT EXISTS user_roles (
49
+ user_id {self._get_id_type()} REFERENCES users(id),
50
+ role_id {self._get_id_type()} REFERENCES roles(id),
51
+ PRIMARY KEY (user_id, role_id)
52
+ );
53
+
54
+ CREATE TABLE IF NOT EXISTS api_tokens (
55
+ id VARCHAR(8) PRIMARY KEY,
56
+ user_id INTEGER REFERENCES users(id),
57
+ name VARCHAR(255) NOT NULL,
58
+ token VARCHAR(255) NOT NULL,
59
+ created_at TIMESTAMP NOT NULL,
60
+ expires_at TIMESTAMP,
61
+ last_used_at TIMESTAMP
62
+ );
63
+ """)
64
+
65
+ def _get_id_type(self):
66
+ return 'UUID' if self.id_type == 'uuid' else 'SERIAL'
67
+
68
+ @contextmanager
69
+ def get_connection(self):
70
+ conn = self._pool.getconn()
71
+ try:
72
+ yield conn
73
+ conn.commit()
74
+ except Exception:
75
+ conn.rollback()
76
+ raise
77
+ finally:
78
+ self._pool.putconn(conn)
79
+
80
+ @contextmanager
81
+ def get_cursor(self):
82
+ with self.get_connection() as conn:
83
+ with conn.cursor() as cur:
84
+ yield cur
85
+
86
+ def get_id_generator(self):
87
+ return self.id_generator
88
+
89
+ def close(self):
90
+ if self._pool:
91
91
  self._pool.closeall()
@@ -1,33 +1,33 @@
1
- from functools import wraps
2
- from flask import request, current_app, jsonify
3
- from .exceptions import AuthError
4
-
5
- def require_auth(roles=None):
6
- def decorator(f):
7
- @wraps(f)
8
- def decorated(*args, **kwargs):
9
- if request.method == 'OPTIONS':
10
- return f(*args, **kwargs)
11
- try:
12
- # Get the require_auth decorator from AuthManager
13
- user = current_app.auth_manager.get_current_user()
14
- if not user:
15
- raise AuthError('User not authenticated', 401)
16
-
17
- auth_decorator = current_app.auth_manager.require_auth
18
-
19
- # Apply the AuthManager's decorator and get the result
20
- decorated_func = auth_decorator(f)
21
-
22
- # Check roles if specified
23
- if roles and not any(role in user['roles'] for role in roles):
24
- raise AuthError('Insufficient permissions', 403)
25
-
26
- # Now execute the function
27
- return decorated_func(*args, **kwargs)
28
- except AuthError as e:
29
- response = jsonify(e.to_dict())
30
- response.status_code = e.status_code
31
- return response
32
- return decorated
1
+ from functools import wraps
2
+ from flask import request, current_app, jsonify
3
+ from .exceptions import AuthError
4
+
5
+ def require_auth(roles=None):
6
+ def decorator(f):
7
+ @wraps(f)
8
+ def decorated(*args, **kwargs):
9
+ if request.method == 'OPTIONS':
10
+ return f(*args, **kwargs)
11
+ try:
12
+ # Get the require_auth decorator from AuthManager
13
+ user = current_app.auth_manager.get_current_user()
14
+ if not user:
15
+ raise AuthError('User not authenticated', 401)
16
+
17
+ auth_decorator = current_app.auth_manager.require_auth
18
+
19
+ # Apply the AuthManager's decorator and get the result
20
+ decorated_func = auth_decorator(f)
21
+
22
+ # Check roles if specified
23
+ if roles and not any(role in user['roles'] for role in roles):
24
+ raise AuthError('Insufficient permissions', 403)
25
+
26
+ # Now execute the function
27
+ return decorated_func(*args, **kwargs)
28
+ except AuthError as e:
29
+ response = jsonify(e.to_dict())
30
+ response.status_code = e.status_code
31
+ return response
32
+ return decorated
33
33
  return decorator
@@ -1,11 +1,11 @@
1
- class AuthError(Exception):
2
- def __init__(self, message, status_code=401):
3
- self.message = message
4
- self.status_code = status_code
5
- super().__init__(self.message)
6
-
7
- def to_dict(self):
8
- return {
9
- 'error': self.message,
10
- 'status_code': self.status_code
1
+ class AuthError(Exception):
2
+ def __init__(self, message, status_code=401):
3
+ self.message = message
4
+ self.status_code = status_code
5
+ super().__init__(self.message)
6
+
7
+ def to_dict(self):
8
+ return {
9
+ 'error': self.message,
10
+ 'status_code': self.status_code
11
11
  }
@@ -1,95 +1,95 @@
1
- from datetime import datetime, timedelta
2
- import uuid
3
- import bcrypt
4
- import secrets
5
- import string
6
- from abc import ABC, abstractmethod
7
-
8
- def generate_random_string(length, alphabet=string.ascii_letters + string.digits):
9
- """Generate a random string of specified length using the given alphabet."""
10
- return ''.join(secrets.choice(alphabet) for _ in range(length))
11
-
12
- class IDGenerator(ABC):
13
- @abstractmethod
14
- def generate(self):
15
- pass
16
-
17
- class UUIDGenerator(IDGenerator):
18
- def generate(self):
19
- return str(uuid.uuid4())
20
-
21
- class IntegerGenerator(IDGenerator):
22
- def generate(self):
23
- return None # Let the database handle ID generation with SERIAL
24
-
25
- class User:
26
- def __init__(self, username, email, real_name, roles=None, id_generator=None):
27
- self.id = id_generator.generate() if id_generator else str(uuid.uuid4())
28
- if self.id is None: # Let database handle ID generation
29
- self.id = None
30
- self.username = username
31
- self.email = email
32
- self.real_name = real_name
33
- self.roles = roles or []
34
- self.created_at = datetime.utcnow()
35
- self.updated_at = datetime.utcnow()
36
-
37
- class Role:
38
- def __init__(self, name, description=None, id_generator=None):
39
- self.id = id_generator.generate() if id_generator else str(uuid.uuid4())
40
- self.name = name
41
- self.description = description
42
- self.created_at = datetime.utcnow()
43
-
44
- class ApiToken:
45
- def __init__(self, user_id, name, expires_in_days=None):
46
- self.id = generate_random_string(8) # 8 character ID
47
- self.user_id = user_id
48
- self.name = name
49
- self.nonce = generate_random_string(32) # 32 character nonce
50
- self.token = self._hash_nonce(self.nonce) # Hash the nonce
51
- self.created_at = datetime.utcnow()
52
- self.expires_at = datetime.utcnow() + timedelta(days=expires_in_days) if expires_in_days else None
53
- self.last_used_at = None
54
-
55
- def _hash_nonce(self, nonce):
56
- """Hash the nonce using bcrypt."""
57
- salt = bcrypt.gensalt()
58
- return bcrypt.hashpw(nonce.encode('utf-8'), salt).decode('utf-8')
59
-
60
- def get_full_token(self):
61
- """Get the full token string in the format api_IDNONCE."""
62
- return f"api_{self.id}{self.nonce}"
63
-
64
- @staticmethod
65
- def parse_token(token_string):
66
- """Parse a token string into its components."""
67
- if not token_string.startswith('api_'):
68
- raise ValueError('Invalid token format')
69
-
70
- token_string = token_string[4:] # Remove 'api_' prefix
71
- if len(token_string) != 40: # 8 (id) + 32 (nonce)
72
- raise ValueError('Invalid token length')
73
-
74
- return {
75
- 'id': token_string[:8],
76
- 'nonce': token_string[8:]
77
- }
78
-
79
- @staticmethod
80
- def parse_token_id(token_string):
81
- if len(token_string) == 8:
82
- return token_string
83
- if not token_string.startswith('api_'):
84
- raise ValueError('Invalid token format')
85
- return token_string[4:][:8]
86
-
87
- def verify_token(self, token_string):
88
- """Verify if a token string matches this token."""
89
- try:
90
- parsed = self.parse_token(token_string)
91
- if parsed['id'] != self.id:
92
- return False
93
- return bcrypt.checkpw(parsed['nonce'].encode('utf-8'), self.token.encode('utf-8'))
94
- except ValueError:
1
+ from datetime import datetime, timedelta
2
+ import uuid
3
+ import bcrypt
4
+ import secrets
5
+ import string
6
+ from abc import ABC, abstractmethod
7
+
8
+ def generate_random_string(length, alphabet=string.ascii_letters + string.digits):
9
+ """Generate a random string of specified length using the given alphabet."""
10
+ return ''.join(secrets.choice(alphabet) for _ in range(length))
11
+
12
+ class IDGenerator(ABC):
13
+ @abstractmethod
14
+ def generate(self):
15
+ pass
16
+
17
+ class UUIDGenerator(IDGenerator):
18
+ def generate(self):
19
+ return str(uuid.uuid4())
20
+
21
+ class IntegerGenerator(IDGenerator):
22
+ def generate(self):
23
+ return None # Let the database handle ID generation with SERIAL
24
+
25
+ class User:
26
+ def __init__(self, username, email, real_name, roles=None, id_generator=None):
27
+ self.id = id_generator.generate() if id_generator else str(uuid.uuid4())
28
+ if self.id is None: # Let database handle ID generation
29
+ self.id = None
30
+ self.username = username
31
+ self.email = email
32
+ self.real_name = real_name
33
+ self.roles = roles or []
34
+ self.created_at = datetime.utcnow()
35
+ self.updated_at = datetime.utcnow()
36
+
37
+ class Role:
38
+ def __init__(self, name, description=None, id_generator=None):
39
+ self.id = id_generator.generate() if id_generator else str(uuid.uuid4())
40
+ self.name = name
41
+ self.description = description
42
+ self.created_at = datetime.utcnow()
43
+
44
+ class ApiToken:
45
+ def __init__(self, user_id, name, expires_in_days=None):
46
+ self.id = generate_random_string(8) # 8 character ID
47
+ self.user_id = user_id
48
+ self.name = name
49
+ self.nonce = generate_random_string(32) # 32 character nonce
50
+ self.token = self._hash_nonce(self.nonce) # Hash the nonce
51
+ self.created_at = datetime.utcnow()
52
+ self.expires_at = datetime.utcnow() + timedelta(days=expires_in_days) if expires_in_days else None
53
+ self.last_used_at = None
54
+
55
+ def _hash_nonce(self, nonce):
56
+ """Hash the nonce using bcrypt."""
57
+ salt = bcrypt.gensalt()
58
+ return bcrypt.hashpw(nonce.encode('utf-8'), salt).decode('utf-8')
59
+
60
+ def get_full_token(self):
61
+ """Get the full token string in the format api_IDNONCE."""
62
+ return f"api_{self.id}{self.nonce}"
63
+
64
+ @staticmethod
65
+ def parse_token(token_string):
66
+ """Parse a token string into its components."""
67
+ if not token_string.startswith('api_'):
68
+ raise ValueError('Invalid token format')
69
+
70
+ token_string = token_string[4:] # Remove 'api_' prefix
71
+ if len(token_string) != 40: # 8 (id) + 32 (nonce)
72
+ raise ValueError('Invalid token length')
73
+
74
+ return {
75
+ 'id': token_string[:8],
76
+ 'nonce': token_string[8:]
77
+ }
78
+
79
+ @staticmethod
80
+ def parse_token_id(token_string):
81
+ if len(token_string) == 8:
82
+ return token_string
83
+ if not token_string.startswith('api_'):
84
+ raise ValueError('Invalid token format')
85
+ return token_string[4:][:8]
86
+
87
+ def verify_token(self, token_string):
88
+ """Verify if a token string matches this token."""
89
+ try:
90
+ parsed = self.parse_token(token_string)
91
+ if parsed['id'] != self.id:
92
+ return False
93
+ return bcrypt.checkpw(parsed['nonce'].encode('utf-8'), self.token.encode('utf-8'))
94
+ except ValueError:
95
95
  return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: the37lab_authlib
3
- Version: 0.1.1758263039
3
+ Version: 0.1.1758266466
4
4
  Summary: Python SDK for the Authlib
5
5
  Author-email: the37lab <info@the37lab.com>
6
6
  Classifier: Programming Language :: Python :: 3
@@ -0,0 +1,10 @@
1
+ the37lab_authlib/__init__.py,sha256=cFVTWL-0YIMqwOMVy1P8mOt_bQODJp-L9bfp2QQ8CTo,132
2
+ the37lab_authlib/auth.py,sha256=3igW3vyWXY6Ec4A6WuE0btYZcIcnv0TfweRHQdEDcwQ,49336
3
+ the37lab_authlib/db.py,sha256=cmnmykKvq6V5e-D0HGiRN4DjFBOGB-SL1HpFjR5uyCw,3162
4
+ the37lab_authlib/decorators.py,sha256=L-gJUUwDUT2JXTptQ6XEey1LkI5RprbqzEfArWI7F8Y,1305
5
+ the37lab_authlib/exceptions.py,sha256=mdplK5sKNtagPAzSGq5NGsrQ4r-k03DKJBKx6myWwZc,317
6
+ the37lab_authlib/models.py,sha256=-PlvQlHGIsSdrH0H9Cdh_vTPlltGV8G1Z1mmGQvAg9Y,3422
7
+ the37lab_authlib-0.1.1758266466.dist-info/METADATA,sha256=Za_SVsmAMryV_x8vOOjByTjRpgWUVFZ0zveBORf3Dqw,8339
8
+ the37lab_authlib-0.1.1758266466.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ the37lab_authlib-0.1.1758266466.dist-info/top_level.txt,sha256=6Jmxw4UeLrhfJXgRKbXWY4OhxRSaMs0dKKhNCGWWSwc,17
10
+ the37lab_authlib-0.1.1758266466.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- the37lab_authlib/__init__.py,sha256=QxIyIyb-b2C91a9vSE05cFov-MFwprBnPLUTCz1rAGo,136
2
- the37lab_authlib/auth.py,sha256=3coNtCHg6sMIsTuWXNe8Ni147jmypCB_x-28AYq0ZMw,50484
3
- the37lab_authlib/db.py,sha256=w8OcWvFWA-MbWals--l64qx_iDQYxv85wKGT52GtNVo,3252
4
- the37lab_authlib/decorators.py,sha256=hCd6XSEREgeQ8XGJzlmKD-K8sQYbK7WgZCQKdY3m7ng,1337
5
- the37lab_authlib/exceptions.py,sha256=ONA64ktHAuj4w0ur4xUeWZQQmfZw9hHo4che1Bi-M3s,327
6
- the37lab_authlib/models.py,sha256=9-9ndGq-o9VGjHF8VvgMHvjhYOEapfOkocfjpqEFHY4,3516
7
- the37lab_authlib-0.1.1758263039.dist-info/METADATA,sha256=r4jwSFSiHkOrGQ8HJxvhDxcs0UzfILy_NS50Qwy93eU,8339
8
- the37lab_authlib-0.1.1758263039.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
- the37lab_authlib-0.1.1758263039.dist-info/top_level.txt,sha256=6Jmxw4UeLrhfJXgRKbXWY4OhxRSaMs0dKKhNCGWWSwc,17
10
- the37lab_authlib-0.1.1758263039.dist-info/RECORD,,