fastlib-py 0.4.3__tar.gz → 0.4.4__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.
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/PKG-INFO +3 -1
- fastlib_py-0.4.4/fastlib/security.py +177 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib_py.egg-info/PKG-INFO +3 -1
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib_py.egg-info/requires.txt +2 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/pyproject.toml +3 -1
- fastlib_py-0.4.3/fastlib/security.py +0 -290
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/LICENSE +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/README.md +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/__init__.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/cache/__init__.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/cache/_memony_cache.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/cache/_redis_cache.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/cache/manager.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/config/__init__.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/config/_database_config.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/config/_log_config.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/config/_security_config.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/config/_server_config.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/config/base.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/config/decorator.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/config/loader.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/config/manager.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/config/registry.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/config/utils.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/constants.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/contextvars.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/db_engine.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/enums.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/exception/__init__.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/exception/base.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/exception/exception_handler.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/exception/exception_manager.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/i18n/__init__.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/i18n/manager.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/i18n/messages.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/i18n/types.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/i18n/utils.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/logging/__init__.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/logging/handlers.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/mapper/__init__.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/mapper/base_mapper.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/mapper/impl/__init__.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/mapper/impl/base_mapper_impl.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/middleware/__init__.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/middleware/db_session.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/middleware/i18n.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/middleware/jwt.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/migrate.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/openapi.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/py.typed +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/request.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/response.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/router.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/schema.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/service/__init__.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/service/base_service.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/service/impl/__init__.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/service/impl/base_service_impl.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/stream/__init__.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/stream/enums.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/stream/event.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/stream/handler.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/stream/schema.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/stream/sse.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/utils/__init__.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/utils/excel_util.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/utils/file_util.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/utils/http_util.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/utils/ip_util.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/utils/security_util.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/utils/snowflake_util.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/utils/str_util.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib/utils/validate_util.py +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib_py.egg-info/SOURCES.txt +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib_py.egg-info/dependency_links.txt +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/fastlib_py.egg-info/top_level.txt +0 -0
- {fastlib_py-0.4.3 → fastlib_py-0.4.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastlib-py
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.4
|
|
4
4
|
Summary: Fastlib, high performance, easy to use, fast to code, ready for production
|
|
5
5
|
Author-email: tyvekZhang <tyvekzhang@gmail.com>
|
|
6
6
|
Maintainer-email: tyvekZhang <tyvekzhang@gmail.com>
|
|
@@ -21,11 +21,13 @@ Description-Content-Type: text/markdown
|
|
|
21
21
|
License-File: LICENSE
|
|
22
22
|
Requires-Dist: aiosqlite>=0.21
|
|
23
23
|
Requires-Dist: alembic>=1.15.2
|
|
24
|
+
Requires-Dist: bcrypt==4.3.0
|
|
24
25
|
Requires-Dist: fastapi>=0.121.0
|
|
25
26
|
Requires-Dist: httpx>=0.28.1
|
|
26
27
|
Requires-Dist: loguru>=0.7.3
|
|
27
28
|
Requires-Dist: openpyxl>=3.1.5
|
|
28
29
|
Requires-Dist: pandas>=2.2.3
|
|
30
|
+
Requires-Dist: passlib[bcrypt]>=1.7.4
|
|
29
31
|
Requires-Dist: pyjwt[crypto]>=2.10.1
|
|
30
32
|
Requires-Dist: python-multipart>=0.0.20
|
|
31
33
|
Requires-Dist: sqlmodel>=0.0.24
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
|
+
from typing import Any, Optional, Callable, Union
|
|
4
|
+
|
|
5
|
+
from fastapi import Depends, HTTPException, status
|
|
6
|
+
from fastapi.security import OAuth2PasswordBearer
|
|
7
|
+
import jwt
|
|
8
|
+
from jwt.exceptions import InvalidTokenError
|
|
9
|
+
from passlib.context import CryptContext
|
|
10
|
+
|
|
11
|
+
from fastlib.config import ConfigManager
|
|
12
|
+
from fastlib.schema import CurrentUser
|
|
13
|
+
|
|
14
|
+
security_config = ConfigManager.get_security_config()
|
|
15
|
+
server_config = ConfigManager.get_server_config()
|
|
16
|
+
|
|
17
|
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def decode_jwt_token(token: str) -> dict[str, Any]:
|
|
21
|
+
"""Decode and validate JWT token.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
token: JWT token string
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Decoded token payload
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
HTTPException: If token is invalid or expired
|
|
31
|
+
"""
|
|
32
|
+
try:
|
|
33
|
+
return jwt.decode(
|
|
34
|
+
token,
|
|
35
|
+
security_config.secret_key,
|
|
36
|
+
algorithms=[security_config.algorithm],
|
|
37
|
+
)
|
|
38
|
+
except InvalidTokenError as err:
|
|
39
|
+
raise HTTPException(
|
|
40
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
41
|
+
detail="Token has expired. Please log in again.",
|
|
42
|
+
) from err
|
|
43
|
+
except Exception as err:
|
|
44
|
+
raise HTTPException(
|
|
45
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
46
|
+
detail="Could not validate credentials.",
|
|
47
|
+
) from err
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_oauth2_scheme() -> OAuth2PasswordBearer:
|
|
51
|
+
oauth2_scheme = OAuth2PasswordBearer(
|
|
52
|
+
tokenUrl=f"{server_config.api_prefix}/v1/auth/auth:signInWithEmailAndPassword"
|
|
53
|
+
)
|
|
54
|
+
return oauth2_scheme
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_current_user() -> Callable[[], CurrentUser]:
|
|
58
|
+
"""
|
|
59
|
+
Acquire current info through access_token
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
CurrentUser instance
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def current_user(
|
|
66
|
+
access_token: str = Depends(get_oauth2_scheme()),
|
|
67
|
+
) -> CurrentUser:
|
|
68
|
+
if not security_config.enable:
|
|
69
|
+
user_id = 9
|
|
70
|
+
return CurrentUser(user_id=user_id)
|
|
71
|
+
try:
|
|
72
|
+
user_id = get_user_id(access_token)
|
|
73
|
+
except InvalidTokenError as err:
|
|
74
|
+
raise HTTPException(
|
|
75
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
76
|
+
detail="Your token has expired. Please log in again.",
|
|
77
|
+
) from err
|
|
78
|
+
except Exception:
|
|
79
|
+
raise HTTPException(
|
|
80
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
81
|
+
detail="Error when decoding the token. Please check your request.",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return CurrentUser(user_id=user_id)
|
|
85
|
+
|
|
86
|
+
return current_user
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def create_token(
|
|
90
|
+
subject: Optional[Union[str, int]] = None,
|
|
91
|
+
expires_delta: Optional[timedelta] = None,
|
|
92
|
+
token_type: Optional[str] = None,
|
|
93
|
+
) -> str:
|
|
94
|
+
"""Create new JWT token.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
subject: Token subject (usually user ID)
|
|
98
|
+
expires_delta: Optional timedelta for token expiration
|
|
99
|
+
token_type: Token type identifier
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Encoded JWT string
|
|
103
|
+
"""
|
|
104
|
+
expire = datetime.utcnow() + (
|
|
105
|
+
expires_delta or timedelta(minutes=security_config.refresh_token_expire_minutes)
|
|
106
|
+
)
|
|
107
|
+
to_encode = {"exp": expire, "sub": str(subject), "type": token_type}
|
|
108
|
+
return jwt.encode(
|
|
109
|
+
to_encode,
|
|
110
|
+
security_config.secret_key,
|
|
111
|
+
algorithm=security_config.algorithm,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
|
116
|
+
"""Verify password against hashed version.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
plain_password: Input password
|
|
120
|
+
hashed_password: Stored hashed password
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
True if passwords match
|
|
124
|
+
"""
|
|
125
|
+
return pwd_context.verify(plain_password, hashed_password)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def get_password_hash(password: str) -> str:
|
|
129
|
+
"""Generate password hash.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
password: Plain text password
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Hashed password string
|
|
136
|
+
"""
|
|
137
|
+
return pwd_context.hash(password)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def validate_token(token: str) -> bool:
|
|
141
|
+
"""Check if token is valid and not expired.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
token: JWT token to validate
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
True if token is valid
|
|
148
|
+
|
|
149
|
+
Raises:
|
|
150
|
+
HTTPException: If token is invalid
|
|
151
|
+
"""
|
|
152
|
+
try:
|
|
153
|
+
payload = decode_jwt_token(token)
|
|
154
|
+
if datetime.fromtimestamp(payload["exp"]) < datetime.utcnow():
|
|
155
|
+
raise HTTPException(
|
|
156
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
157
|
+
detail="Token has expired",
|
|
158
|
+
)
|
|
159
|
+
return True
|
|
160
|
+
except InvalidTokenError:
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def get_user_id(token: str) -> int:
|
|
165
|
+
"""Extract user ID from JWT token.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
token: JWT token string
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
User ID extracted from the token
|
|
172
|
+
|
|
173
|
+
Raises:
|
|
174
|
+
HTTPException: If token is invalid or expired
|
|
175
|
+
"""
|
|
176
|
+
payload = decode_jwt_token(token)
|
|
177
|
+
return int(payload["sub"])
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastlib-py
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.4
|
|
4
4
|
Summary: Fastlib, high performance, easy to use, fast to code, ready for production
|
|
5
5
|
Author-email: tyvekZhang <tyvekzhang@gmail.com>
|
|
6
6
|
Maintainer-email: tyvekZhang <tyvekzhang@gmail.com>
|
|
@@ -21,11 +21,13 @@ Description-Content-Type: text/markdown
|
|
|
21
21
|
License-File: LICENSE
|
|
22
22
|
Requires-Dist: aiosqlite>=0.21
|
|
23
23
|
Requires-Dist: alembic>=1.15.2
|
|
24
|
+
Requires-Dist: bcrypt==4.3.0
|
|
24
25
|
Requires-Dist: fastapi>=0.121.0
|
|
25
26
|
Requires-Dist: httpx>=0.28.1
|
|
26
27
|
Requires-Dist: loguru>=0.7.3
|
|
27
28
|
Requires-Dist: openpyxl>=3.1.5
|
|
28
29
|
Requires-Dist: pandas>=2.2.3
|
|
30
|
+
Requires-Dist: passlib[bcrypt]>=1.7.4
|
|
29
31
|
Requires-Dist: pyjwt[crypto]>=2.10.1
|
|
30
32
|
Requires-Dist: python-multipart>=0.0.20
|
|
31
33
|
Requires-Dist: sqlmodel>=0.0.24
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "fastlib-py"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.4"
|
|
4
4
|
description = "Fastlib, high performance, easy to use, fast to code, ready for production"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
keywords = ["fastapi", "library", "sqlmodel", "web"]
|
|
@@ -22,11 +22,13 @@ classifiers = [
|
|
|
22
22
|
dependencies = [
|
|
23
23
|
"aiosqlite>=0.21",
|
|
24
24
|
"alembic>=1.15.2",
|
|
25
|
+
"bcrypt==4.3.0",
|
|
25
26
|
"fastapi>=0.121.0",
|
|
26
27
|
"httpx>=0.28.1",
|
|
27
28
|
"loguru>=0.7.3",
|
|
28
29
|
"openpyxl>=3.1.5",
|
|
29
30
|
"pandas>=2.2.3",
|
|
31
|
+
"passlib[bcrypt]>=1.7.4",
|
|
30
32
|
"pyjwt[crypto]>=2.10.1",
|
|
31
33
|
"python-multipart>=0.0.20",
|
|
32
34
|
"sqlmodel>=0.0.24",
|
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
# SPDX-License-Identifier: MIT
|
|
2
|
-
import base64
|
|
3
|
-
import hashlib
|
|
4
|
-
import secrets
|
|
5
|
-
from collections.abc import Callable
|
|
6
|
-
from datetime import datetime, timedelta
|
|
7
|
-
from typing import Any
|
|
8
|
-
|
|
9
|
-
import jwt
|
|
10
|
-
from fastapi import Depends, HTTPException, status
|
|
11
|
-
from fastapi.security import OAuth2PasswordBearer
|
|
12
|
-
from jwt.exceptions import InvalidTokenError
|
|
13
|
-
|
|
14
|
-
from fastlib.config.manager import ConfigManager
|
|
15
|
-
from fastlib.schema import CurrentUser
|
|
16
|
-
|
|
17
|
-
# Configuration
|
|
18
|
-
security_config = ConfigManager.get_security_config()
|
|
19
|
-
server_config = ConfigManager.get_server_config()
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def decode_jwt_token(token: str) -> dict[str, Any]:
|
|
23
|
-
"""Decode and validate JWT token.
|
|
24
|
-
|
|
25
|
-
Args:
|
|
26
|
-
token: JWT token string
|
|
27
|
-
|
|
28
|
-
Returns:
|
|
29
|
-
Decoded token payload
|
|
30
|
-
|
|
31
|
-
Raises:
|
|
32
|
-
HTTPException: If token is invalid or expired
|
|
33
|
-
"""
|
|
34
|
-
try:
|
|
35
|
-
return jwt.decode(
|
|
36
|
-
token,
|
|
37
|
-
security_config.secret_key,
|
|
38
|
-
algorithms=[security_config.algorithm],
|
|
39
|
-
)
|
|
40
|
-
except InvalidTokenError as err:
|
|
41
|
-
raise HTTPException(
|
|
42
|
-
status_code=status.HTTP_403_FORBIDDEN,
|
|
43
|
-
detail="Token has expired. Please log in again.",
|
|
44
|
-
) from err
|
|
45
|
-
except Exception as err:
|
|
46
|
-
raise HTTPException(
|
|
47
|
-
status_code=status.HTTP_403_FORBIDDEN,
|
|
48
|
-
detail="Could not validate credentials.",
|
|
49
|
-
) from err
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def get_oauth2_scheme() -> OAuth2PasswordBearer:
|
|
53
|
-
"""Create OAuth2 password bearer scheme.
|
|
54
|
-
|
|
55
|
-
Returns:
|
|
56
|
-
OAuth2PasswordBearer instance
|
|
57
|
-
"""
|
|
58
|
-
oauth2_scheme = OAuth2PasswordBearer(
|
|
59
|
-
tokenUrl=f"{server_config.api_prefix}/v1/auth/auth:signInWithEmailAndPassword"
|
|
60
|
-
)
|
|
61
|
-
return oauth2_scheme
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def get_current_user() -> Callable[[], CurrentUser]:
|
|
65
|
-
"""Get current user from access token.
|
|
66
|
-
|
|
67
|
-
Returns:
|
|
68
|
-
CurrentUser instance
|
|
69
|
-
"""
|
|
70
|
-
|
|
71
|
-
def current_user(
|
|
72
|
-
access_token: str = Depends(get_oauth2_scheme()),
|
|
73
|
-
) -> CurrentUser:
|
|
74
|
-
if not security_config.enable:
|
|
75
|
-
user_id = 9
|
|
76
|
-
return CurrentUser(user_id=user_id)
|
|
77
|
-
try:
|
|
78
|
-
user_id = get_user_id(access_token)
|
|
79
|
-
except InvalidTokenError as err:
|
|
80
|
-
raise HTTPException(
|
|
81
|
-
status_code=status.HTTP_403_FORBIDDEN,
|
|
82
|
-
detail="Your token has expired. Please log in again.",
|
|
83
|
-
) from err
|
|
84
|
-
except Exception as err:
|
|
85
|
-
raise HTTPException(
|
|
86
|
-
status_code=status.HTTP_403_FORBIDDEN,
|
|
87
|
-
detail="Error when decoding the token. Please check your request.",
|
|
88
|
-
) from err
|
|
89
|
-
|
|
90
|
-
return CurrentUser(user_id=user_id)
|
|
91
|
-
|
|
92
|
-
return current_user
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def create_token(
|
|
96
|
-
subject: str | int | None = None,
|
|
97
|
-
expires_delta: timedelta | None = None,
|
|
98
|
-
token_type: str | None = None,
|
|
99
|
-
) -> str:
|
|
100
|
-
"""Create new JWT token.
|
|
101
|
-
|
|
102
|
-
Args:
|
|
103
|
-
subject: Token subject (usually user ID)
|
|
104
|
-
expires_delta: Optional timedelta for token expiration
|
|
105
|
-
token_type: Token type identifier
|
|
106
|
-
|
|
107
|
-
Returns:
|
|
108
|
-
Encoded JWT string
|
|
109
|
-
"""
|
|
110
|
-
expire = datetime.utcnow() + (
|
|
111
|
-
expires_delta or timedelta(minutes=security_config.refresh_token_expire_minutes)
|
|
112
|
-
)
|
|
113
|
-
to_encode = {"exp": expire, "sub": str(subject), "type": token_type}
|
|
114
|
-
return jwt.encode(
|
|
115
|
-
to_encode,
|
|
116
|
-
security_config.secret_key,
|
|
117
|
-
algorithm=security_config.algorithm,
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def generate_salt(length: int = 16) -> str:
|
|
122
|
-
"""Generate a cryptographically secure random salt.
|
|
123
|
-
|
|
124
|
-
Args:
|
|
125
|
-
length: Length of the salt in bytes
|
|
126
|
-
|
|
127
|
-
Returns:
|
|
128
|
-
Base64 encoded salt string
|
|
129
|
-
"""
|
|
130
|
-
return base64.b64encode(secrets.token_bytes(length)).decode("utf-8")
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def hash_password_sha256(password: str, salt: str | None = None) -> tuple[str, str]:
|
|
134
|
-
"""Hash password using SHA-256 with salt.
|
|
135
|
-
|
|
136
|
-
Args:
|
|
137
|
-
password: Plain text password to hash
|
|
138
|
-
salt: Salt for hashing. If None, generates random salt.
|
|
139
|
-
|
|
140
|
-
Returns:
|
|
141
|
-
Tuple of (hashed_password, salt)
|
|
142
|
-
"""
|
|
143
|
-
if salt is None:
|
|
144
|
-
salt = generate_salt()
|
|
145
|
-
|
|
146
|
-
# Decode base64 salt
|
|
147
|
-
salt_bytes = base64.b64decode(salt.encode("utf-8"))
|
|
148
|
-
|
|
149
|
-
# Create hash with salt and password
|
|
150
|
-
hash_obj = hashlib.sha256()
|
|
151
|
-
hash_obj.update(salt_bytes + password.encode("utf-8"))
|
|
152
|
-
|
|
153
|
-
# Multiple iterations for increased security (optional)
|
|
154
|
-
for _ in range(1000): # 1000 iterations
|
|
155
|
-
hash_obj = hashlib.sha256(
|
|
156
|
-
hash_obj.digest() + salt_bytes + password.encode("utf-8")
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
hashed_password = base64.b64encode(hash_obj.digest()).decode("utf-8")
|
|
160
|
-
return hashed_password, salt
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
def verify_password(
|
|
164
|
-
plain_password: str, hashed_password: str, salt: str | None = None
|
|
165
|
-
) -> bool:
|
|
166
|
-
"""Verify password against hashed version using SHA-256.
|
|
167
|
-
|
|
168
|
-
Args:
|
|
169
|
-
plain_password: Input password to verify
|
|
170
|
-
hashed_password: Stored hashed password
|
|
171
|
-
salt: Salt used for the original hash. If None, uses a default salt.
|
|
172
|
-
|
|
173
|
-
Returns:
|
|
174
|
-
True if passwords match
|
|
175
|
-
"""
|
|
176
|
-
try:
|
|
177
|
-
# Use default salt if none provided (for backward compatibility)
|
|
178
|
-
if salt is None:
|
|
179
|
-
# Default salt - in production, this should be stored with the hash
|
|
180
|
-
salt = "default_salt_placeholder"
|
|
181
|
-
|
|
182
|
-
# Recalculate hash with the same salt and algorithm
|
|
183
|
-
calculated_hash, _ = hash_password_sha256(plain_password, salt)
|
|
184
|
-
|
|
185
|
-
# Use timing-safe comparison function
|
|
186
|
-
return secrets.compare_digest(calculated_hash, hashed_password)
|
|
187
|
-
except Exception:
|
|
188
|
-
return False
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
def get_password_hash(password: str) -> tuple[str, str]:
|
|
192
|
-
"""Generate password hash using SHA-256.
|
|
193
|
-
|
|
194
|
-
Args:
|
|
195
|
-
password: Plain text password
|
|
196
|
-
|
|
197
|
-
Returns:
|
|
198
|
-
Tuple of (hashed_password, salt)
|
|
199
|
-
"""
|
|
200
|
-
return hash_password_sha256(password)
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
def validate_token(token: str) -> bool:
|
|
204
|
-
"""Check if token is valid and not expired.
|
|
205
|
-
|
|
206
|
-
Args:
|
|
207
|
-
token: JWT token to validate
|
|
208
|
-
|
|
209
|
-
Returns:
|
|
210
|
-
True if token is valid
|
|
211
|
-
|
|
212
|
-
Raises:
|
|
213
|
-
HTTPException: If token is invalid
|
|
214
|
-
"""
|
|
215
|
-
try:
|
|
216
|
-
payload = decode_jwt_token(token)
|
|
217
|
-
if datetime.fromtimestamp(payload["exp"]) < datetime.utcnow():
|
|
218
|
-
raise HTTPException(
|
|
219
|
-
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
220
|
-
detail="Token has expired",
|
|
221
|
-
)
|
|
222
|
-
return True
|
|
223
|
-
except InvalidTokenError:
|
|
224
|
-
return False
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
def get_user_id(token: str) -> int:
|
|
228
|
-
"""Extract user ID from JWT token.
|
|
229
|
-
|
|
230
|
-
Args:
|
|
231
|
-
token: JWT token string
|
|
232
|
-
|
|
233
|
-
Returns:
|
|
234
|
-
User ID extracted from the token
|
|
235
|
-
|
|
236
|
-
Raises:
|
|
237
|
-
HTTPException: If token is invalid or expired
|
|
238
|
-
"""
|
|
239
|
-
payload = decode_jwt_token(token)
|
|
240
|
-
return int(payload["sub"])
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
# Backward compatibility wrapper functions
|
|
244
|
-
def verify_password_compat(plain_password: str, hashed_password: str) -> bool:
|
|
245
|
-
"""Compatibility version of password verification with default salt.
|
|
246
|
-
|
|
247
|
-
Args:
|
|
248
|
-
plain_password: Input password to verify
|
|
249
|
-
hashed_password: Stored hashed password
|
|
250
|
-
|
|
251
|
-
Returns:
|
|
252
|
-
True if passwords match
|
|
253
|
-
"""
|
|
254
|
-
return verify_password(plain_password, hashed_password, salt=None)
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
def get_password_hash_compat(password: str) -> str:
|
|
258
|
-
"""Compatibility version of password hash function.
|
|
259
|
-
|
|
260
|
-
Args:
|
|
261
|
-
password: Plain text password
|
|
262
|
-
|
|
263
|
-
Returns:
|
|
264
|
-
Formatted string containing hash and salt (hash:salt)
|
|
265
|
-
"""
|
|
266
|
-
hashed_password, salt = get_password_hash(password)
|
|
267
|
-
return f"{hashed_password}:{salt}"
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
def parse_hashed_password(hashed_data: str) -> tuple[str, str]:
|
|
271
|
-
"""Parse hashed password data in format 'hash:salt'.
|
|
272
|
-
|
|
273
|
-
Args:
|
|
274
|
-
hashed_data: String containing hash and salt separated by colon
|
|
275
|
-
|
|
276
|
-
Returns:
|
|
277
|
-
Tuple of (hashed_password, salt)
|
|
278
|
-
|
|
279
|
-
Raises:
|
|
280
|
-
ValueError: If format is invalid
|
|
281
|
-
"""
|
|
282
|
-
if ":" not in hashed_data:
|
|
283
|
-
# If no salt is present, use default salt
|
|
284
|
-
return hashed_data, "default_salt_placeholder"
|
|
285
|
-
|
|
286
|
-
parts = hashed_data.split(":", 1)
|
|
287
|
-
if len(parts) != 2:
|
|
288
|
-
raise ValueError("Invalid hashed password format")
|
|
289
|
-
|
|
290
|
-
return parts[0], parts[1]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|