auth2fa 0.1.0__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.
auth2fa-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 horrible-gh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ recursive-include auth2fa *.sql
2
+ include README.md
3
+ include LICENSE
auth2fa-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.4
2
+ Name: auth2fa
3
+ Version: 0.1.0
4
+ Summary: TOTP-based Two-Factor Authentication Library for Python
5
+ Author-email: horrible-gh <shinjpn1@gmail.com>
6
+ Project-URL: Homepage, https://github.com/horrible-gh/auth2fa
7
+ Project-URL: Bug Tracker, https://github.com/horrible-gh/auth2fa/issues
8
+ Keywords: auth2fa,2fa,totp,authentication,otp,two-factor
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: pyotp
19
+ Requires-Dist: qrcode
20
+ Requires-Dist: Pillow
21
+ Provides-Extra: sql
22
+ Requires-Dist: sqloader; extra == "sql"
23
+ Dynamic: license-file
24
+
25
+ # sqloader
26
+
27
+ A lightweight Python utility for managing SQL migrations and loading SQL from JSON or .sql files.
28
+ Supports common relational databases and is designed for simple, clean integration with any Python backend (e.g., FastAPI).
29
+
30
+ ---
31
+
32
+ ## Installation
33
+
34
+ ```powershell
35
+ pip install sqloader
36
+ ```
37
+
38
+ ## Features
39
+
40
+ - ✅ Easy database migration management
41
+ - ✅ Load SQL queries from `.json` or `.sql` files
42
+ - ✅ Supports MySQL and SQLite
43
+ - ✅ Clean API for integration
44
+ - ✅ Lightweight and dependency-minimized
45
+
46
+ ## Quickstart
47
+
48
+ ```python
49
+ from sqloader.init import database_init
50
+
51
+ config = {
52
+ "type": "mysql",
53
+ "mysql": {
54
+ "host": "localhost",
55
+ "port": 3306,
56
+ "user": "root",
57
+ "password": "pass",
58
+ "database": "mydb"
59
+ },
60
+ "service": {
61
+ "sqloder": "res/sql/sqloader/mysql"
62
+ },
63
+ "migration": {
64
+ "auto_migration": True,
65
+ "migration_path": "res/sql/migration/mysql"
66
+ },
67
+ }
68
+
69
+ db, sqloader, migrator = database_init(config)
70
+
71
+ # Example usage
72
+ query = sqloader.load_sql("user_info", "user.get_user_by_id")
73
+ result = db.fetch_one(query, ['abc', 123])
74
+
75
+ ```
76
+
77
+ ## SQL Loading Behavior
78
+
79
+ - If the value in the .json file ends with .sql, the referenced file will be loaded from the same directory.
80
+ - Otherwise, the value is treated as a raw SQL string.
81
+
82
+ Example JSON file user.json:
83
+
84
+
85
+ ```json
86
+ {
87
+ "user": {
88
+ "get_user_by_id": "SELECT * FROM users WHERE id = %s",
89
+ "get_all_users": "user_all.sql"
90
+ },
91
+ "get_etc": "SELECT * FROM etc"
92
+ }
93
+ ```
@@ -0,0 +1,69 @@
1
+ # sqloader
2
+
3
+ A lightweight Python utility for managing SQL migrations and loading SQL from JSON or .sql files.
4
+ Supports common relational databases and is designed for simple, clean integration with any Python backend (e.g., FastAPI).
5
+
6
+ ---
7
+
8
+ ## Installation
9
+
10
+ ```powershell
11
+ pip install sqloader
12
+ ```
13
+
14
+ ## Features
15
+
16
+ - ✅ Easy database migration management
17
+ - ✅ Load SQL queries from `.json` or `.sql` files
18
+ - ✅ Supports MySQL and SQLite
19
+ - ✅ Clean API for integration
20
+ - ✅ Lightweight and dependency-minimized
21
+
22
+ ## Quickstart
23
+
24
+ ```python
25
+ from sqloader.init import database_init
26
+
27
+ config = {
28
+ "type": "mysql",
29
+ "mysql": {
30
+ "host": "localhost",
31
+ "port": 3306,
32
+ "user": "root",
33
+ "password": "pass",
34
+ "database": "mydb"
35
+ },
36
+ "service": {
37
+ "sqloder": "res/sql/sqloader/mysql"
38
+ },
39
+ "migration": {
40
+ "auto_migration": True,
41
+ "migration_path": "res/sql/migration/mysql"
42
+ },
43
+ }
44
+
45
+ db, sqloader, migrator = database_init(config)
46
+
47
+ # Example usage
48
+ query = sqloader.load_sql("user_info", "user.get_user_by_id")
49
+ result = db.fetch_one(query, ['abc', 123])
50
+
51
+ ```
52
+
53
+ ## SQL Loading Behavior
54
+
55
+ - If the value in the .json file ends with .sql, the referenced file will be loaded from the same directory.
56
+ - Otherwise, the value is treated as a raw SQL string.
57
+
58
+ Example JSON file user.json:
59
+
60
+
61
+ ```json
62
+ {
63
+ "user": {
64
+ "get_user_by_id": "SELECT * FROM users WHERE id = %s",
65
+ "get_all_users": "user_all.sql"
66
+ },
67
+ "get_etc": "SELECT * FROM etc"
68
+ }
69
+ ```
@@ -0,0 +1,19 @@
1
+ """
2
+ auth2fa - TOTP-based Two-Factor Authentication Library
3
+
4
+ A flexible and easy-to-use Python library for implementing TOTP-based
5
+ two-factor authentication with support for multiple storage backends.
6
+ """
7
+
8
+ from .core import TwoFactorAuth
9
+ from .storage.base import BaseStorage
10
+ from .storage.json_storage import JSONStorage
11
+ from .storage.sql_storage import SQLStorage
12
+
13
+ __version__ = '0.1.0'
14
+ __all__ = [
15
+ 'TwoFactorAuth',
16
+ 'BaseStorage',
17
+ 'JSONStorage',
18
+ 'SQLStorage'
19
+ ]
@@ -0,0 +1,222 @@
1
+ """Core TOTP authentication logic for auth2fa."""
2
+ import base64
3
+ import pyotp
4
+ import qrcode
5
+ from io import BytesIO
6
+ from datetime import datetime
7
+ from typing import Optional, Dict
8
+ from .storage.base import BaseStorage
9
+ from .storage.json_storage import JSONStorage
10
+ from .storage.sql_storage import SQLStorage
11
+ from .recovery import generate_recovery_codes, verify_recovery_code
12
+
13
+
14
+ class TwoFactorAuth:
15
+ """Main class for TOTP-based two-factor authentication."""
16
+
17
+ def __init__(self, issuer: str = "MyApp", storage_path: Optional[str] = None, sq=None):
18
+ """
19
+ Initialize TwoFactorAuth.
20
+
21
+ Args:
22
+ issuer: Name of the application/service for TOTP
23
+ storage_path: Path to JSON storage file (optional)
24
+ sq: sqloader instance for SQL storage (optional)
25
+
26
+ Initialization priority:
27
+ 1. If sq is provided → SQLStorage
28
+ 2. If storage_path is provided → JSONStorage with custom path
29
+ 3. Otherwise → JSONStorage with default path ("./totp_data.json")
30
+ """
31
+ self.issuer = issuer
32
+
33
+ # Determine storage backend based on priority
34
+ if sq is not None:
35
+ self.storage = SQLStorage(sq)
36
+ elif storage_path is not None:
37
+ self.storage = JSONStorage(storage_path)
38
+ else:
39
+ self.storage = JSONStorage("./totp_data.json")
40
+
41
+ def setup(self, user_id: str, username: str = "") -> Dict:
42
+ """
43
+ Set up TOTP for a user.
44
+
45
+ Args:
46
+ user_id: Unique identifier for the user
47
+ username: Display name for the account (defaults to empty string)
48
+
49
+ Returns:
50
+ Dictionary containing:
51
+ {
52
+ "secret": "BASE32SECRET",
53
+ "qr_uri": "otpauth://totp/Issuer:username?secret=...&issuer=...",
54
+ "qr_image": "base64 encoded PNG QR image",
55
+ "recovery_codes": ["A3F8K2M1", ...]
56
+ }
57
+ """
58
+ user_id = str(user_id) # Ensure user_id is string
59
+
60
+ if self.storage.exists(user_id):
61
+ raise ValueError(f"TOTP already configured for user: {user_id}")
62
+
63
+ # Generate secret and recovery codes
64
+ secret = pyotp.random_base32()
65
+ recovery_codes = generate_recovery_codes(count=8, length=8)
66
+
67
+ # Generate provisioning URI for QR code
68
+ account_name = username if username else user_id
69
+ totp = pyotp.TOTP(secret)
70
+ qr_uri = totp.provisioning_uri(
71
+ name=account_name,
72
+ issuer_name=self.issuer
73
+ )
74
+
75
+ # Generate QR code image
76
+ qr = qrcode.QRCode(version=1, box_size=10, border=5)
77
+ qr.add_data(qr_uri)
78
+ qr.make(fit=True)
79
+
80
+ img = qr.make_image(fill_color="black", back_color="white")
81
+ buffer = BytesIO()
82
+ img.save(buffer, format='PNG')
83
+ qr_image_bytes = buffer.getvalue()
84
+ qr_image_base64 = base64.b64encode(qr_image_bytes).decode('utf-8')
85
+
86
+ # Save to storage (not enabled yet)
87
+ data = {
88
+ 'secret': secret,
89
+ 'enabled': False,
90
+ 'recovery_codes': recovery_codes,
91
+ 'created_at': datetime.now().isoformat()
92
+ }
93
+ self.storage.save(user_id, data)
94
+
95
+ return {
96
+ "secret": secret,
97
+ "qr_uri": qr_uri,
98
+ "qr_image": qr_image_base64,
99
+ "recovery_codes": recovery_codes
100
+ }
101
+
102
+ def activate(self, user_id: str, code: str) -> bool:
103
+ """
104
+ Activate TOTP after user verifies the code from their app.
105
+
106
+ Args:
107
+ user_id: Unique identifier for the user
108
+ code: TOTP code from authenticator app
109
+
110
+ Returns:
111
+ True if activation successful, False if code is invalid
112
+ """
113
+ user_id = str(user_id) # Ensure user_id is string
114
+
115
+ data = self.storage.get(user_id)
116
+ if not data:
117
+ raise ValueError(f"TOTP not configured for user: {user_id}")
118
+
119
+ if data['enabled']:
120
+ raise ValueError(f"TOTP already activated for user: {user_id}")
121
+
122
+ # Verify the token
123
+ secret = data['secret']
124
+ totp = pyotp.TOTP(secret)
125
+ if totp.verify(code, valid_window=1):
126
+ data['enabled'] = True
127
+ self.storage.save(user_id, data)
128
+ return True
129
+
130
+ return False
131
+
132
+ def verify(self, user_id: str, code: str) -> bool:
133
+ """
134
+ Verify TOTP code during login.
135
+
136
+ Args:
137
+ user_id: Unique identifier for the user
138
+ code: TOTP code or recovery code
139
+
140
+ Returns:
141
+ True if verification successful, False otherwise
142
+
143
+ Behavior:
144
+ - Unconfigured user → True (pass through)
145
+ - TOTP code verification (valid_window=1)
146
+ - If TOTP fails, try recovery code
147
+ - Recovery code used → automatically removed
148
+ """
149
+ user_id = str(user_id) # Ensure user_id is string
150
+
151
+ data = self.storage.get(user_id)
152
+
153
+ # Unconfigured user → pass through
154
+ if not data:
155
+ return True
156
+
157
+ # Try TOTP code first
158
+ secret = data['secret']
159
+ totp = pyotp.TOTP(secret)
160
+ if totp.verify(code, valid_window=1):
161
+ return True
162
+
163
+ # TOTP failed, try recovery code
164
+ recovery_codes = data.get('recovery_codes', [])
165
+ is_valid, updated_codes = verify_recovery_code(recovery_codes, code)
166
+
167
+ if is_valid:
168
+ # Update storage with used code removed
169
+ data['recovery_codes'] = updated_codes
170
+ self.storage.save(user_id, data)
171
+ return True
172
+
173
+ return False
174
+
175
+ def disable(self, user_id: str) -> None:
176
+ """
177
+ Disable and remove TOTP configuration for a user.
178
+
179
+ Args:
180
+ user_id: Unique identifier for the user
181
+ """
182
+ user_id = str(user_id) # Ensure user_id is string
183
+ self.storage.delete(user_id)
184
+
185
+ def is_enabled(self, user_id: str) -> bool:
186
+ """
187
+ Check if TOTP is enabled for a user.
188
+
189
+ Args:
190
+ user_id: Unique identifier for the user
191
+
192
+ Returns:
193
+ True if TOTP is enabled, False otherwise
194
+ """
195
+ user_id = str(user_id) # Ensure user_id is string
196
+
197
+ data = self.storage.get(user_id)
198
+ if not data:
199
+ return False
200
+ return data.get('enabled', False)
201
+
202
+ def regenerate_recovery_codes(self, user_id: str) -> list:
203
+ """
204
+ Regenerate recovery codes for a user.
205
+
206
+ Args:
207
+ user_id: Unique identifier for the user
208
+
209
+ Returns:
210
+ List of new recovery codes
211
+ """
212
+ user_id = str(user_id) # Ensure user_id is string
213
+
214
+ data = self.storage.get(user_id)
215
+ if not data:
216
+ raise ValueError(f"TOTP not configured for user: {user_id}")
217
+
218
+ new_codes = generate_recovery_codes(count=8, length=8)
219
+ data['recovery_codes'] = new_codes
220
+ self.storage.save(user_id, data)
221
+
222
+ return new_codes
@@ -0,0 +1,54 @@
1
+ """Recovery code generation and verification for auth2fa."""
2
+ import secrets
3
+ import string
4
+ from typing import List, Tuple
5
+
6
+
7
+ def generate_recovery_codes(count: int = 8, length: int = 8) -> List[str]:
8
+ """
9
+ Generate a list of random recovery codes.
10
+
11
+ Args:
12
+ count: Number of recovery codes to generate (default: 8)
13
+ length: Length of each recovery code (default: 8)
14
+
15
+ Returns:
16
+ List of recovery codes in format like ['A3F8K2M1', 'B7D2N4P9', ...]
17
+ """
18
+ codes = []
19
+ # Use alphanumeric characters excluding similar-looking ones (0, O, I, 1)
20
+ alphabet = string.ascii_uppercase + string.digits
21
+ alphabet = alphabet.replace('0', '').replace('O', '').replace('I', '').replace('1', '')
22
+
23
+ for _ in range(count):
24
+ code = ''.join(secrets.choice(alphabet) for _ in range(length))
25
+ codes.append(code)
26
+
27
+ return codes
28
+
29
+
30
+ def verify_recovery_code(stored_codes: List[str], input_code: str) -> Tuple[bool, List[str]]:
31
+ """
32
+ Verify if a recovery code is valid and return updated code list.
33
+
34
+ Args:
35
+ stored_codes: List of valid recovery codes
36
+ input_code: The recovery code to verify
37
+
38
+ Returns:
39
+ Tuple of (is_valid, updated_codes):
40
+ - (True, codes with used code removed) if valid
41
+ - (False, original codes) if invalid
42
+
43
+ Recovery codes are single-use — successful verification removes the code.
44
+ """
45
+ # Case-insensitive comparison
46
+ input_upper = input_code.upper()
47
+ codes_upper = [rc.upper() for rc in stored_codes]
48
+
49
+ if input_upper in codes_upper:
50
+ # Remove the used code
51
+ updated_codes = [rc for rc in stored_codes if rc.upper() != input_upper]
52
+ return (True, updated_codes)
53
+ else:
54
+ return (False, stored_codes)
@@ -0,0 +1,13 @@
1
+ -- Create TOTP authentication table
2
+ CREATE TABLE IF NOT EXISTS totp_auth (
3
+ user_id VARCHAR(255) PRIMARY KEY,
4
+ secret VARCHAR(255) NOT NULL,
5
+ enabled BOOLEAN DEFAULT FALSE,
6
+ recovery_codes TEXT,
7
+ created_at DATETIME NOT NULL,
8
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
9
+ );
10
+
11
+ -- Create index for faster lookups
12
+ CREATE INDEX IF NOT EXISTS idx_totp_auth_user_id ON totp_auth(user_id);
13
+ CREATE INDEX IF NOT EXISTS idx_totp_auth_enabled ON totp_auth(enabled);
@@ -0,0 +1,2 @@
1
+ -- name: delete
2
+ DELETE FROM totp_auth WHERE user_id = :user_id;
@@ -0,0 +1,7 @@
1
+ -- name: insert
2
+ INSERT INTO totp_auth (user_id, secret, enabled, recovery_codes, created_at)
3
+ VALUES (:user_id, :secret, :enabled, :recovery_codes, :created_at)
4
+ ON CONFLICT (user_id) DO UPDATE
5
+ SET secret = EXCLUDED.secret,
6
+ enabled = EXCLUDED.enabled,
7
+ recovery_codes = EXCLUDED.recovery_codes;
@@ -0,0 +1,2 @@
1
+ -- name: select_by_user
2
+ SELECT * FROM totp_auth WHERE user_id = :user_id;
@@ -0,0 +1,2 @@
1
+ -- name: update
2
+ UPDATE totp_auth SET enabled = :enabled, recovery_codes = :recovery_codes WHERE user_id = :user_id;
@@ -0,0 +1,6 @@
1
+ """Storage modules for auth2fa."""
2
+ from .base import BaseStorage
3
+ from .json_storage import JSONStorage
4
+ from .sql_storage import SQLStorage
5
+
6
+ __all__ = ['BaseStorage', 'JSONStorage', 'SQLStorage']
@@ -0,0 +1,60 @@
1
+ """Base storage interface for auth2fa."""
2
+ from abc import ABC, abstractmethod
3
+ from typing import Optional
4
+
5
+
6
+ class BaseStorage(ABC):
7
+ """Abstract base class for TOTP storage implementations."""
8
+
9
+ @abstractmethod
10
+ def save(self, user_id: str, data: dict) -> None:
11
+ """
12
+ Save or update TOTP data for a user.
13
+
14
+ Args:
15
+ user_id: Unique identifier for the user
16
+ data: Dictionary containing TOTP data with structure:
17
+ {
18
+ "secret": "BASE32SECRET",
19
+ "enabled": False,
20
+ "recovery_codes": ["A3F8K2M1", "B7D2N4P9", ...],
21
+ "created_at": "2026-02-11T18:00:00"
22
+ }
23
+ """
24
+ pass
25
+
26
+ @abstractmethod
27
+ def get(self, user_id: str) -> Optional[dict]:
28
+ """
29
+ Retrieve TOTP data for a user.
30
+
31
+ Args:
32
+ user_id: Unique identifier for the user
33
+
34
+ Returns:
35
+ Dictionary containing TOTP data, or None if not found
36
+ """
37
+ pass
38
+
39
+ @abstractmethod
40
+ def delete(self, user_id: str) -> None:
41
+ """
42
+ Delete TOTP data for a user.
43
+
44
+ Args:
45
+ user_id: Unique identifier for the user
46
+ """
47
+ pass
48
+
49
+ @abstractmethod
50
+ def exists(self, user_id: str) -> bool:
51
+ """
52
+ Check if TOTP is configured for a user.
53
+
54
+ Args:
55
+ user_id: Unique identifier for the user
56
+
57
+ Returns:
58
+ True if TOTP data exists, False otherwise
59
+ """
60
+ pass
@@ -0,0 +1,131 @@
1
+ """JSON file-based storage implementation for auth2fa."""
2
+ import json
3
+ import time
4
+ from pathlib import Path
5
+ from typing import Optional
6
+ from .base import BaseStorage
7
+
8
+
9
+ class JSONStorage(BaseStorage):
10
+ """JSON file-based storage implementation with file locking."""
11
+
12
+ def __init__(self, storage_path: str = "./totp_data.json"):
13
+ """
14
+ Initialize JSON storage.
15
+
16
+ Args:
17
+ storage_path: Path to the JSON file for storing TOTP data
18
+ """
19
+ self.file_path = Path(storage_path)
20
+ self.lock_path = Path(str(self.file_path) + ".lock")
21
+ self._ensure_file_exists()
22
+
23
+ def _ensure_file_exists(self) -> None:
24
+ """Create the JSON file if it doesn't exist."""
25
+ if not self.file_path.exists():
26
+ self.file_path.parent.mkdir(parents=True, exist_ok=True)
27
+ self._write_data({})
28
+
29
+ def _acquire_lock(self, timeout: int = 5) -> None:
30
+ """
31
+ Acquire file lock using lock file.
32
+
33
+ Args:
34
+ timeout: Maximum time to wait for lock in seconds
35
+ """
36
+ start_time = time.time()
37
+ while self.lock_path.exists():
38
+ if time.time() - start_time > timeout:
39
+ raise TimeoutError("Failed to acquire file lock")
40
+ time.sleep(0.01)
41
+
42
+ # Create lock file
43
+ self.lock_path.touch()
44
+
45
+ def _release_lock(self) -> None:
46
+ """Release file lock by removing lock file."""
47
+ if self.lock_path.exists():
48
+ self.lock_path.unlink()
49
+
50
+ def _read_data(self) -> dict:
51
+ """Read all data from the JSON file."""
52
+ try:
53
+ with open(self.file_path, 'r', encoding='utf-8') as f:
54
+ return json.load(f)
55
+ except (json.JSONDecodeError, FileNotFoundError):
56
+ return {}
57
+
58
+ def _write_data(self, data: dict) -> None:
59
+ """Write all data to the JSON file."""
60
+ with open(self.file_path, 'w', encoding='utf-8') as f:
61
+ json.dump(data, f, indent=2, ensure_ascii=False)
62
+
63
+ def save(self, user_id: str, data: dict) -> None:
64
+ """
65
+ Save or update TOTP data for a user.
66
+
67
+ Args:
68
+ user_id: Unique identifier for the user (converted to str)
69
+ data: Dictionary containing TOTP data
70
+ """
71
+ user_id = str(user_id) # Ensure user_id is string
72
+ try:
73
+ self._acquire_lock()
74
+ all_data = self._read_data()
75
+ all_data[user_id] = data
76
+ self._write_data(all_data)
77
+ finally:
78
+ self._release_lock()
79
+
80
+ def get(self, user_id: str) -> Optional[dict]:
81
+ """
82
+ Retrieve TOTP data for a user.
83
+
84
+ Args:
85
+ user_id: Unique identifier for the user (converted to str)
86
+
87
+ Returns:
88
+ Dictionary containing TOTP data, or None if not found
89
+ """
90
+ user_id = str(user_id) # Ensure user_id is string
91
+ try:
92
+ self._acquire_lock()
93
+ all_data = self._read_data()
94
+ return all_data.get(user_id)
95
+ finally:
96
+ self._release_lock()
97
+
98
+ def delete(self, user_id: str) -> None:
99
+ """
100
+ Delete TOTP data for a user.
101
+
102
+ Args:
103
+ user_id: Unique identifier for the user (converted to str)
104
+ """
105
+ user_id = str(user_id) # Ensure user_id is string
106
+ try:
107
+ self._acquire_lock()
108
+ all_data = self._read_data()
109
+ if user_id in all_data:
110
+ del all_data[user_id]
111
+ self._write_data(all_data)
112
+ finally:
113
+ self._release_lock()
114
+
115
+ def exists(self, user_id: str) -> bool:
116
+ """
117
+ Check if TOTP is configured for a user.
118
+
119
+ Args:
120
+ user_id: Unique identifier for the user (converted to str)
121
+
122
+ Returns:
123
+ True if TOTP data exists, False otherwise
124
+ """
125
+ user_id = str(user_id) # Ensure user_id is string
126
+ try:
127
+ self._acquire_lock()
128
+ all_data = self._read_data()
129
+ return user_id in all_data
130
+ finally:
131
+ self._release_lock()
@@ -0,0 +1,105 @@
1
+ """SQL database storage implementation using sqloader."""
2
+ import json
3
+ from typing import Optional
4
+ from .base import BaseStorage
5
+
6
+
7
+ class SQLStorage(BaseStorage):
8
+ """SQL database storage implementation using sqloader."""
9
+
10
+ def __init__(self, sq, table_prefix: str = "totp_auth"):
11
+ """
12
+ Initialize SQL storage.
13
+
14
+ Args:
15
+ sq: sqloader instance (SQLite3 or MySQL)
16
+ table_prefix: SQL file directory name (default: totp_auth)
17
+ """
18
+ self.db = sq
19
+ self.table_prefix = table_prefix
20
+ self._ensure_table_exists()
21
+
22
+ def _ensure_table_exists(self) -> None:
23
+ """Create the TOTP table if it doesn't exist."""
24
+ # sqloader will execute the create_table.sql file
25
+ self.db.execute(f"{self.table_prefix}/create_table")
26
+
27
+ def save(self, user_id: str, data: dict) -> None:
28
+ """
29
+ Save or update TOTP data for a user.
30
+
31
+ Args:
32
+ user_id: Unique identifier for the user (converted to str)
33
+ data: Dictionary containing TOTP data
34
+ """
35
+ user_id = str(user_id) # Ensure user_id is string
36
+
37
+ # Use INSERT with ON CONFLICT (UPSERT)
38
+ self.db.execute(
39
+ f"{self.table_prefix}/insert",
40
+ user_id=user_id,
41
+ secret=data.get('secret'),
42
+ enabled=data.get('enabled', False),
43
+ recovery_codes=json.dumps(data.get('recovery_codes', [])),
44
+ created_at=data.get('created_at')
45
+ )
46
+
47
+ def get(self, user_id: str) -> Optional[dict]:
48
+ """
49
+ Retrieve TOTP data for a user.
50
+
51
+ Args:
52
+ user_id: Unique identifier for the user (converted to str)
53
+
54
+ Returns:
55
+ Dictionary containing TOTP data, or None if not found
56
+ """
57
+ user_id = str(user_id) # Ensure user_id is string
58
+
59
+ result = self.db.execute(
60
+ f"{self.table_prefix}/select_by_user",
61
+ user_id=user_id
62
+ )
63
+
64
+ if not result:
65
+ return None
66
+
67
+ row = result[0]
68
+ return {
69
+ 'secret': row.get('secret'),
70
+ 'enabled': bool(row.get('enabled')),
71
+ 'recovery_codes': json.loads(row.get('recovery_codes', '[]')),
72
+ 'created_at': row.get('created_at')
73
+ }
74
+
75
+ def delete(self, user_id: str) -> None:
76
+ """
77
+ Delete TOTP data for a user.
78
+
79
+ Args:
80
+ user_id: Unique identifier for the user (converted to str)
81
+ """
82
+ user_id = str(user_id) # Ensure user_id is string
83
+
84
+ self.db.execute(
85
+ f"{self.table_prefix}/delete",
86
+ user_id=user_id
87
+ )
88
+
89
+ def exists(self, user_id: str) -> bool:
90
+ """
91
+ Check if TOTP is configured for a user.
92
+
93
+ Args:
94
+ user_id: Unique identifier for the user (converted to str)
95
+
96
+ Returns:
97
+ True if TOTP data exists, False otherwise
98
+ """
99
+ user_id = str(user_id) # Ensure user_id is string
100
+
101
+ result = self.db.execute(
102
+ f"{self.table_prefix}/select_by_user",
103
+ user_id=user_id
104
+ )
105
+ return len(result) > 0
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.4
2
+ Name: auth2fa
3
+ Version: 0.1.0
4
+ Summary: TOTP-based Two-Factor Authentication Library for Python
5
+ Author-email: horrible-gh <shinjpn1@gmail.com>
6
+ Project-URL: Homepage, https://github.com/horrible-gh/auth2fa
7
+ Project-URL: Bug Tracker, https://github.com/horrible-gh/auth2fa/issues
8
+ Keywords: auth2fa,2fa,totp,authentication,otp,two-factor
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: pyotp
19
+ Requires-Dist: qrcode
20
+ Requires-Dist: Pillow
21
+ Provides-Extra: sql
22
+ Requires-Dist: sqloader; extra == "sql"
23
+ Dynamic: license-file
24
+
25
+ # sqloader
26
+
27
+ A lightweight Python utility for managing SQL migrations and loading SQL from JSON or .sql files.
28
+ Supports common relational databases and is designed for simple, clean integration with any Python backend (e.g., FastAPI).
29
+
30
+ ---
31
+
32
+ ## Installation
33
+
34
+ ```powershell
35
+ pip install sqloader
36
+ ```
37
+
38
+ ## Features
39
+
40
+ - ✅ Easy database migration management
41
+ - ✅ Load SQL queries from `.json` or `.sql` files
42
+ - ✅ Supports MySQL and SQLite
43
+ - ✅ Clean API for integration
44
+ - ✅ Lightweight and dependency-minimized
45
+
46
+ ## Quickstart
47
+
48
+ ```python
49
+ from sqloader.init import database_init
50
+
51
+ config = {
52
+ "type": "mysql",
53
+ "mysql": {
54
+ "host": "localhost",
55
+ "port": 3306,
56
+ "user": "root",
57
+ "password": "pass",
58
+ "database": "mydb"
59
+ },
60
+ "service": {
61
+ "sqloder": "res/sql/sqloader/mysql"
62
+ },
63
+ "migration": {
64
+ "auto_migration": True,
65
+ "migration_path": "res/sql/migration/mysql"
66
+ },
67
+ }
68
+
69
+ db, sqloader, migrator = database_init(config)
70
+
71
+ # Example usage
72
+ query = sqloader.load_sql("user_info", "user.get_user_by_id")
73
+ result = db.fetch_one(query, ['abc', 123])
74
+
75
+ ```
76
+
77
+ ## SQL Loading Behavior
78
+
79
+ - If the value in the .json file ends with .sql, the referenced file will be loaded from the same directory.
80
+ - Otherwise, the value is treated as a raw SQL string.
81
+
82
+ Example JSON file user.json:
83
+
84
+
85
+ ```json
86
+ {
87
+ "user": {
88
+ "get_user_by_id": "SELECT * FROM users WHERE id = %s",
89
+ "get_all_users": "user_all.sql"
90
+ },
91
+ "get_etc": "SELECT * FROM etc"
92
+ }
93
+ ```
@@ -0,0 +1,22 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ pyproject.toml
5
+ setup.py
6
+ auth2fa/__init__.py
7
+ auth2fa/core.py
8
+ auth2fa/recovery.py
9
+ auth2fa.egg-info/PKG-INFO
10
+ auth2fa.egg-info/SOURCES.txt
11
+ auth2fa.egg-info/dependency_links.txt
12
+ auth2fa.egg-info/requires.txt
13
+ auth2fa.egg-info/top_level.txt
14
+ auth2fa/sql/totp_auth/create_table.sql
15
+ auth2fa/sql/totp_auth/delete.sql
16
+ auth2fa/sql/totp_auth/insert.sql
17
+ auth2fa/sql/totp_auth/select_by_user.sql
18
+ auth2fa/sql/totp_auth/update.sql
19
+ auth2fa/storage/__init__.py
20
+ auth2fa/storage/base.py
21
+ auth2fa/storage/json_storage.py
22
+ auth2fa/storage/sql_storage.py
@@ -0,0 +1,6 @@
1
+ pyotp
2
+ qrcode
3
+ Pillow
4
+
5
+ [sql]
6
+ sqloader
@@ -0,0 +1 @@
1
+ auth2fa
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "auth2fa"
7
+ version = "0.1.0"
8
+ authors = [
9
+ { name="horrible-gh", email="shinjpn1@gmail.com" },
10
+ ]
11
+ description = "TOTP-based Two-Factor Authentication Library for Python"
12
+ readme = "README.md"
13
+ requires-python = ">=3.10"
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.10",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Operating System :: OS Independent",
21
+ ]
22
+ keywords = [
23
+ "auth2fa", "2fa", "totp", "authentication", "otp", "two-factor"
24
+ ]
25
+ dependencies = [
26
+ "pyotp",
27
+ "qrcode",
28
+ "Pillow"
29
+ ]
30
+
31
+ [project.optional-dependencies]
32
+ sql = ["sqloader"]
33
+
34
+ [project.urls]
35
+ "Homepage" = "https://github.com/horrible-gh/auth2fa"
36
+ "Bug Tracker" = "https://github.com/horrible-gh/auth2fa/issues"
37
+
38
+ [tool.setuptools.package-data]
39
+ auth2fa = ["sql/**/*.sql"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
auth2fa-0.1.0/setup.py ADDED
@@ -0,0 +1,18 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="auth2fa",
5
+ version="0.1.0",
6
+ packages=find_packages(),
7
+ package_data={
8
+ "auth2fa": ["sql/**/*.sql"],
9
+ },
10
+ install_requires=[
11
+ "pyotp",
12
+ "qrcode",
13
+ "Pillow",
14
+ ],
15
+ extras_require={
16
+ "sql": ["sqloader"],
17
+ },
18
+ )