fastapi-authly 0.1.1__tar.gz → 0.1.3__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.
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/PKG-INFO +29 -22
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/README.md +28 -21
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/README.zh.md +28 -0
- fastapi_authly-0.1.3/docs//345/207/275/346/225/260/345/274/217/347/274/226/347/250/213/344/270/203/346/255/246/345/231/250.md +11 -0
- fastapi_authly-0.1.3/examples/correct_usage.py +74 -0
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/pyproject.toml +2 -1
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/src/fastapi_authly/__about__.py +1 -1
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/src/fastapi_authly/__init__.py +6 -2
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/src/fastapi_authly/auth.py +12 -10
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/src/fastapi_authly/core/__init__.py +2 -1
- fastapi_authly-0.1.3/src/fastapi_authly/core/config.py +98 -0
- fastapi_authly-0.1.3/src/fastapi_authly/deps.py +121 -0
- fastapi_authly-0.1.3/src/fastapi_authly/docs.py +70 -0
- fastapi_authly-0.1.3/src/fastapi_authly/static/scalar/standalone.js +34213 -0
- fastapi_authly-0.1.3/src/fastapi_authly/static/scalar/style.css +1 -0
- fastapi_authly-0.1.3/test.py +177 -0
- fastapi_authly-0.1.1/src/fastapi_authly/core/config.py +0 -58
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/.gitignore +0 -0
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/LICENSE +0 -0
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/coverage.xml +0 -0
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/htmlcov/.gitignore +0 -0
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/htmlcov/class_index.html +0 -0
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/htmlcov/coverage_html_cb_dd2e7eb5.js +0 -0
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/htmlcov/favicon_32_cb_c827f16f.png +0 -0
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/htmlcov/function_index.html +0 -0
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/htmlcov/index.html +0 -0
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/htmlcov/keybd_closed_cb_900cfef5.png +0 -0
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/htmlcov/status.json +0 -0
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/htmlcov/style_cb_9ff733b0.css +0 -0
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/htmlcov/z_af1bec017750c6fc___init___py.html +0 -0
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/htmlcov/z_af1bec017750c6fc_user_py.html +0 -0
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/htmlcov/z_b9d93864b1b0ad6e___about___py.html +0 -0
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/htmlcov/z_b9d93864b1b0ad6e___init___py.html +0 -0
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/htmlcov/z_b9d93864b1b0ad6e_auth_py.html +0 -0
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/htmlcov/z_b9d93864b1b0ad6e_interfaces_py.html +0 -0
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/htmlcov/z_d015ea9b27b0258e___init___py.html +0 -0
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/htmlcov/z_d015ea9b27b0258e_config_py.html +0 -0
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/htmlcov/z_d015ea9b27b0258e_security_py.html +0 -0
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/htmlcov/z_ddd01122054512b0___init___py.html +0 -0
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/htmlcov/z_ddd01122054512b0_tortoise_pg_py.html +0 -0
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/htmlcov/z_feee2d9ae7f7fd96___init___py.html +0 -0
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/htmlcov/z_feee2d9ae7f7fd96_user_py.html +0 -0
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/src/fastapi_authly/contrib/__init__.py +0 -0
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/src/fastapi_authly/contrib/tortoise_pg.py +0 -0
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/src/fastapi_authly/core/security.py +0 -0
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/src/fastapi_authly/interfaces.py +0 -0
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/src/fastapi_authly/models/__init__.py +0 -0
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/src/fastapi_authly/models/user.py +0 -0
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/src/fastapi_authly/schemas/__init__.py +0 -0
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/src/fastapi_authly/schemas/user.py +0 -0
- {fastapi_authly-0.1.1 → fastapi_authly-0.1.3}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-authly
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: A modular authentication system for FastAPI with OAuth2, JWT, and password recovery
|
|
5
5
|
Project-URL: Homepage, https://github.com/yourusername/fastapi-auth-module
|
|
6
6
|
Project-URL: Documentation, https://yourusername.github.io/fastapi-auth-module/
|
|
@@ -109,6 +109,10 @@ deps = AuthDependencyConfig(user_repository=TortoiseUserRepository())
|
|
|
109
109
|
|
|
110
110
|
auth_router = create_auth_router(config=config, dependencies=deps)
|
|
111
111
|
app.include_router(auth_router)
|
|
112
|
+
|
|
113
|
+
# Optional: Setup Scalar API documentation (static resources included, no manual setup needed)
|
|
114
|
+
from fastapi_authly import setup_scalar_docs
|
|
115
|
+
setup_scalar_docs(app, docs_url="/docs", static_url="/static")
|
|
112
116
|
```
|
|
113
117
|
|
|
114
118
|
### Advanced Usage (custom implementations)
|
|
@@ -135,6 +139,30 @@ deps = AuthDependencyConfig(
|
|
|
135
139
|
auth_router = create_auth_router(config=config, dependencies=deps)
|
|
136
140
|
```
|
|
137
141
|
|
|
142
|
+
## 📚 API Documentation
|
|
143
|
+
|
|
144
|
+
`fastapi-authly` includes built-in Scalar API documentation support with all necessary static resources:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from fastapi import FastAPI
|
|
148
|
+
from fastapi_authly import setup_scalar_docs
|
|
149
|
+
|
|
150
|
+
app = FastAPI(title="My API")
|
|
151
|
+
|
|
152
|
+
# One line to enable Scalar documentation
|
|
153
|
+
# Automatically mounts static files to /static and creates docs page at /docs
|
|
154
|
+
setup_scalar_docs(app)
|
|
155
|
+
|
|
156
|
+
# Custom configuration
|
|
157
|
+
setup_scalar_docs(
|
|
158
|
+
app,
|
|
159
|
+
docs_url="/api-docs", # Custom docs URL
|
|
160
|
+
static_url="/assets", # Custom static files prefix
|
|
161
|
+
title="Custom API Docs", # Custom title
|
|
162
|
+
openapi_url="/openapi.json" # Custom OpenAPI schema URL
|
|
163
|
+
)
|
|
164
|
+
```
|
|
165
|
+
|
|
138
166
|
## 📋 API Endpoints
|
|
139
167
|
|
|
140
168
|
### Authentication
|
|
@@ -216,26 +244,6 @@ deps = AuthDependencyConfig(user_repository=TortoiseUserRepository())
|
|
|
216
244
|
app.include_router(create_auth_router(config=config, dependencies=deps))
|
|
217
245
|
```
|
|
218
246
|
|
|
219
|
-
## 🧪 Testing
|
|
220
|
-
|
|
221
|
-
```bash
|
|
222
|
-
# Install test dependencies
|
|
223
|
-
uv pip install -e ".[test]"
|
|
224
|
-
|
|
225
|
-
# Run tests
|
|
226
|
-
uv run pytest
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
## 📦 Build & Publish
|
|
230
|
-
|
|
231
|
-
```bash
|
|
232
|
-
# Build
|
|
233
|
-
uv build
|
|
234
|
-
|
|
235
|
-
# Publish to PyPI (set UV_PUBLISH_TOKEN or pass --token)
|
|
236
|
-
uv publish --token pypi-xxxxx
|
|
237
|
-
```
|
|
238
|
-
|
|
239
247
|
## 🤝 Contributing
|
|
240
248
|
|
|
241
249
|
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
@@ -261,6 +269,5 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
|
261
269
|
|
|
262
270
|
If you have any questions or need help:
|
|
263
271
|
|
|
264
|
-
- 📧 Email: your.email@example.com
|
|
265
272
|
- 💬 GitHub Issues: [Create an issue](https://github.com/yourusername/fastapi-authly/issues)
|
|
266
273
|
- 📖 Documentation: [Read the docs](https://yourusername.github.io/fastapi-authly/)
|
|
@@ -57,6 +57,10 @@ deps = AuthDependencyConfig(user_repository=TortoiseUserRepository())
|
|
|
57
57
|
|
|
58
58
|
auth_router = create_auth_router(config=config, dependencies=deps)
|
|
59
59
|
app.include_router(auth_router)
|
|
60
|
+
|
|
61
|
+
# Optional: Setup Scalar API documentation (static resources included, no manual setup needed)
|
|
62
|
+
from fastapi_authly import setup_scalar_docs
|
|
63
|
+
setup_scalar_docs(app, docs_url="/docs", static_url="/static")
|
|
60
64
|
```
|
|
61
65
|
|
|
62
66
|
### Advanced Usage (custom implementations)
|
|
@@ -83,6 +87,30 @@ deps = AuthDependencyConfig(
|
|
|
83
87
|
auth_router = create_auth_router(config=config, dependencies=deps)
|
|
84
88
|
```
|
|
85
89
|
|
|
90
|
+
## 📚 API Documentation
|
|
91
|
+
|
|
92
|
+
`fastapi-authly` includes built-in Scalar API documentation support with all necessary static resources:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from fastapi import FastAPI
|
|
96
|
+
from fastapi_authly import setup_scalar_docs
|
|
97
|
+
|
|
98
|
+
app = FastAPI(title="My API")
|
|
99
|
+
|
|
100
|
+
# One line to enable Scalar documentation
|
|
101
|
+
# Automatically mounts static files to /static and creates docs page at /docs
|
|
102
|
+
setup_scalar_docs(app)
|
|
103
|
+
|
|
104
|
+
# Custom configuration
|
|
105
|
+
setup_scalar_docs(
|
|
106
|
+
app,
|
|
107
|
+
docs_url="/api-docs", # Custom docs URL
|
|
108
|
+
static_url="/assets", # Custom static files prefix
|
|
109
|
+
title="Custom API Docs", # Custom title
|
|
110
|
+
openapi_url="/openapi.json" # Custom OpenAPI schema URL
|
|
111
|
+
)
|
|
112
|
+
```
|
|
113
|
+
|
|
86
114
|
## 📋 API Endpoints
|
|
87
115
|
|
|
88
116
|
### Authentication
|
|
@@ -164,26 +192,6 @@ deps = AuthDependencyConfig(user_repository=TortoiseUserRepository())
|
|
|
164
192
|
app.include_router(create_auth_router(config=config, dependencies=deps))
|
|
165
193
|
```
|
|
166
194
|
|
|
167
|
-
## 🧪 Testing
|
|
168
|
-
|
|
169
|
-
```bash
|
|
170
|
-
# Install test dependencies
|
|
171
|
-
uv pip install -e ".[test]"
|
|
172
|
-
|
|
173
|
-
# Run tests
|
|
174
|
-
uv run pytest
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
## 📦 Build & Publish
|
|
178
|
-
|
|
179
|
-
```bash
|
|
180
|
-
# Build
|
|
181
|
-
uv build
|
|
182
|
-
|
|
183
|
-
# Publish to PyPI (set UV_PUBLISH_TOKEN or pass --token)
|
|
184
|
-
uv publish --token pypi-xxxxx
|
|
185
|
-
```
|
|
186
|
-
|
|
187
195
|
## 🤝 Contributing
|
|
188
196
|
|
|
189
197
|
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
@@ -209,6 +217,5 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
|
209
217
|
|
|
210
218
|
If you have any questions or need help:
|
|
211
219
|
|
|
212
|
-
- 📧 Email: your.email@example.com
|
|
213
220
|
- 💬 GitHub Issues: [Create an issue](https://github.com/yourusername/fastapi-authly/issues)
|
|
214
221
|
- 📖 Documentation: [Read the docs](https://yourusername.github.io/fastapi-authly/)
|
|
@@ -41,6 +41,10 @@ register_tortoise(
|
|
|
41
41
|
config = AuthConfig(token_url="login")
|
|
42
42
|
deps = AuthDependencyConfig(user_repository=TortoiseUserRepository())
|
|
43
43
|
app.include_router(create_auth_router(config=config, dependencies=deps))
|
|
44
|
+
|
|
45
|
+
# 可选:设置 Scalar API 文档(内置静态资源,无需手动配置)
|
|
46
|
+
from fastapi_authly import setup_scalar_docs
|
|
47
|
+
setup_scalar_docs(app, docs_url="/docs", static_url="/static")
|
|
44
48
|
```
|
|
45
49
|
|
|
46
50
|
## 🔌 自定义实现示例
|
|
@@ -65,6 +69,30 @@ deps = AuthDependencyConfig(user_repository=MyRepo(), mailer=MyMailer())
|
|
|
65
69
|
router = create_auth_router(config=config, dependencies=deps)
|
|
66
70
|
```
|
|
67
71
|
|
|
72
|
+
## 📚 API 文档功能
|
|
73
|
+
|
|
74
|
+
`fastapi-authly` 内置了 Scalar API 文档支持,包含所有必要的静态资源,无需手动配置:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from fastapi import FastAPI
|
|
78
|
+
from fastapi_authly import setup_scalar_docs
|
|
79
|
+
|
|
80
|
+
app = FastAPI(title="My API")
|
|
81
|
+
|
|
82
|
+
# 一行代码启用 Scalar 文档
|
|
83
|
+
# 自动挂载静态文件到 /static,创建文档页面到 /docs
|
|
84
|
+
setup_scalar_docs(app)
|
|
85
|
+
|
|
86
|
+
# 自定义配置
|
|
87
|
+
setup_scalar_docs(
|
|
88
|
+
app,
|
|
89
|
+
docs_url="/api-docs", # 自定义文档 URL
|
|
90
|
+
static_url="/assets", # 自定义静态文件前缀
|
|
91
|
+
title="Custom API Docs", # 自定义标题
|
|
92
|
+
openapi_url="/openapi.json" # 自定义 OpenAPI schema URL
|
|
93
|
+
)
|
|
94
|
+
```
|
|
95
|
+
|
|
68
96
|
## 📋 主要接口
|
|
69
97
|
|
|
70
98
|
- `POST /auth/login`:登录,返回 access_token(可选 refresh_token)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
正确的使用方式示例
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from fastapi import FastAPI
|
|
6
|
+
from tortoise.contrib.fastapi import register_tortoise
|
|
7
|
+
from fastapi_authly import (
|
|
8
|
+
AuthConfig,
|
|
9
|
+
AuthDependencyConfig,
|
|
10
|
+
create_auth_router,
|
|
11
|
+
JwtConfig,
|
|
12
|
+
setup_scalar_docs
|
|
13
|
+
)
|
|
14
|
+
from fastapi_authly.contrib.tortoise_pg import TortoiseUserRepository
|
|
15
|
+
|
|
16
|
+
app = FastAPI(title="FastAPI Authly 测试应用")
|
|
17
|
+
|
|
18
|
+
# 初始化 Tortoise + Postgres
|
|
19
|
+
register_tortoise(
|
|
20
|
+
app,
|
|
21
|
+
db_url="postgres://user:password@localhost:5432/testdb",
|
|
22
|
+
modules={"models": ["fastapi_authly.models.user"]},
|
|
23
|
+
generate_schemas=True,
|
|
24
|
+
add_exception_handlers=True,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# 方式 1: 使用 JwtConfig 实例(推荐)
|
|
28
|
+
config = AuthConfig(
|
|
29
|
+
jwt=JwtConfig(
|
|
30
|
+
secret_key="your-secret-key-change-in-production",
|
|
31
|
+
algorithm="HS256",
|
|
32
|
+
access_token_expires_minutes=30,
|
|
33
|
+
refresh_token_expire_days=7,
|
|
34
|
+
),
|
|
35
|
+
router_prefix="/auth",
|
|
36
|
+
token_url="login",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# 方式 2: 使用字典更新(也可以)
|
|
40
|
+
# config = AuthConfig(
|
|
41
|
+
# jwt={
|
|
42
|
+
# "secret_key": "your-secret-key-change-in-production",
|
|
43
|
+
# "algorithm": "HS256",
|
|
44
|
+
# "access_token_expires_minutes": 30,
|
|
45
|
+
# },
|
|
46
|
+
# router_prefix="/auth",
|
|
47
|
+
# token_url="login",
|
|
48
|
+
# )
|
|
49
|
+
|
|
50
|
+
# 方式 3: 使用默认配置,然后通过环境变量覆盖
|
|
51
|
+
# 设置环境变量: AUTH_JWT__SECRET_KEY=your-secret-key
|
|
52
|
+
# config = AuthConfig(
|
|
53
|
+
# router_prefix="/auth",
|
|
54
|
+
# token_url="login",
|
|
55
|
+
# )
|
|
56
|
+
|
|
57
|
+
deps = AuthDependencyConfig(
|
|
58
|
+
user_repository=TortoiseUserRepository(),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# 注册认证路由
|
|
62
|
+
auth_router = create_auth_router(config=config, dependencies=deps)
|
|
63
|
+
app.include_router(auth_router)
|
|
64
|
+
|
|
65
|
+
# 设置 Scalar 文档
|
|
66
|
+
setup_scalar_docs(app, docs_url="/docs", static_url="/static")
|
|
67
|
+
|
|
68
|
+
@app.get("/")
|
|
69
|
+
async def root():
|
|
70
|
+
return {"message": "FastAPI Authly 测试应用已启动"}
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
import uvicorn
|
|
74
|
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "fastapi-authly"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.3"
|
|
8
8
|
description = "A modular authentication system for FastAPI with OAuth2, JWT, and password recovery"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -75,6 +75,7 @@ path = "src/fastapi_authly/__about__.py"
|
|
|
75
75
|
|
|
76
76
|
[tool.hatch.build.targets.wheel]
|
|
77
77
|
packages = ["src/fastapi_authly"]
|
|
78
|
+
# 静态文件在包目录内,会自动包含,无需额外配置
|
|
78
79
|
|
|
79
80
|
[tool.hatch.build.targets.sdist]
|
|
80
81
|
exclude = [
|
|
@@ -7,23 +7,27 @@ This package provides a complete authentication solution with:
|
|
|
7
7
|
- Password recovery
|
|
8
8
|
- User management
|
|
9
9
|
- Modular and configurable design
|
|
10
|
+
- Built-in Scalar API documentation
|
|
10
11
|
"""
|
|
11
12
|
|
|
12
13
|
from .auth import AuthModule, create_auth_router
|
|
13
|
-
from .core import AuthConfig, AuthDependencyConfig, BcryptPasswordHasher, JWTTokenService
|
|
14
|
+
from .core import AuthConfig, AuthDependencyConfig, JwtConfig, BcryptPasswordHasher, JWTTokenService
|
|
15
|
+
from .docs import setup_scalar_docs
|
|
14
16
|
from .interfaces import Mailer, PasswordHasher, TokenService, UserRepository
|
|
15
17
|
from .schemas.user import UserBase, UserCreate, UserUpdate, UserPublic, Token, TokenData
|
|
16
18
|
|
|
17
|
-
__version__ = "0.1.
|
|
19
|
+
__version__ = "0.1.1"
|
|
18
20
|
|
|
19
21
|
__all__ = [
|
|
20
22
|
# Main classes
|
|
21
23
|
"AuthModule",
|
|
22
24
|
"AuthConfig",
|
|
23
25
|
"AuthDependencyConfig",
|
|
26
|
+
"JwtConfig",
|
|
24
27
|
|
|
25
28
|
# Core functions
|
|
26
29
|
"create_auth_router",
|
|
30
|
+
"setup_scalar_docs",
|
|
27
31
|
|
|
28
32
|
# Security utilities
|
|
29
33
|
"BcryptPasswordHasher",
|
|
@@ -7,7 +7,7 @@ from fastapi.security import OAuth2PasswordBearer
|
|
|
7
7
|
from jose import JWTError
|
|
8
8
|
from pydantic import EmailStr
|
|
9
9
|
|
|
10
|
-
from .core.config import AuthConfig, AuthDependencyConfig
|
|
10
|
+
from .core.config import AuthConfig, AuthDependencyConfig, _config
|
|
11
11
|
from .core.security import BcryptPasswordHasher, JWTTokenService
|
|
12
12
|
from .interfaces import Mailer, PasswordHasher, TokenService, UserRepository
|
|
13
13
|
from .schemas.user import (
|
|
@@ -24,11 +24,9 @@ from fastapi_authly.contrib.tortoise_pg import TortoiseUserRepository
|
|
|
24
24
|
|
|
25
25
|
def _merge_config(config: Optional[AuthConfig]) -> AuthConfig:
|
|
26
26
|
"""Environment-driven config, overridden by user-supplied config when provided."""
|
|
27
|
-
env_config = AuthConfig()
|
|
28
27
|
if config is None:
|
|
29
|
-
return
|
|
30
|
-
|
|
31
|
-
return env_config.model_copy(update=config.model_dump(exclude_none=True))
|
|
28
|
+
return _config
|
|
29
|
+
return _config.model_copy(update=config.model_dump(exclude_none=True))
|
|
32
30
|
|
|
33
31
|
|
|
34
32
|
class AuthModule:
|
|
@@ -64,9 +62,13 @@ class AuthModule:
|
|
|
64
62
|
self.router = APIRouter(
|
|
65
63
|
prefix=self.config.router_prefix, tags=self.config.router_tags
|
|
66
64
|
)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
# token 提取依赖:默认使用 OAuth2PasswordBearer,可通过 AuthDependencyConfig.token_dependency 覆盖
|
|
66
|
+
self.token_dependency = (
|
|
67
|
+
self.dependencies.token_dependency
|
|
68
|
+
or OAuth2PasswordBearer(
|
|
69
|
+
tokenUrl=f"{self.config.router_prefix.rstrip('/')}/{self.config.token_url}".strip(
|
|
70
|
+
"/"
|
|
71
|
+
)
|
|
70
72
|
)
|
|
71
73
|
)
|
|
72
74
|
|
|
@@ -120,7 +122,7 @@ class AuthModule:
|
|
|
120
122
|
return Token(access_token=access_token, refresh_token=refresh_token)
|
|
121
123
|
|
|
122
124
|
@self.router.post("/token/verify")
|
|
123
|
-
async def verify_token(token: str = Depends(self.
|
|
125
|
+
async def verify_token(token: str = Depends(self.token_dependency)) -> Dict[str, Any]:
|
|
124
126
|
try:
|
|
125
127
|
payload = self.token_service.decode_token(token)
|
|
126
128
|
token_data = TokenData(**payload)
|
|
@@ -142,7 +144,7 @@ class AuthModule:
|
|
|
142
144
|
return await repo.to_public(created)
|
|
143
145
|
|
|
144
146
|
@self.router.get("/me", response_model=UserPublic)
|
|
145
|
-
async def get_current_user(token: str = Depends(self.
|
|
147
|
+
async def get_current_user(token: str = Depends(self.token_dependency)) -> UserPublic:
|
|
146
148
|
repo = self._ensure_user_repo()
|
|
147
149
|
try:
|
|
148
150
|
payload = self.token_service.decode_token(token, verify_type="access")
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"""Core functionality for fastapi-authly."""
|
|
2
2
|
|
|
3
|
-
from .config import AuthConfig, AuthDependencyConfig
|
|
3
|
+
from .config import AuthConfig, AuthDependencyConfig, JwtConfig
|
|
4
4
|
from .security import BcryptPasswordHasher, JWTTokenService
|
|
5
5
|
|
|
6
6
|
__all__ = [
|
|
7
7
|
"AuthConfig",
|
|
8
8
|
"AuthDependencyConfig",
|
|
9
|
+
"JwtConfig",
|
|
9
10
|
"BcryptPasswordHasher",
|
|
10
11
|
"JWTTokenService",
|
|
11
12
|
]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Configuration settings for fastapi-authly."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional, Any
|
|
4
|
+
from datetime import timedelta
|
|
5
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
6
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
7
|
+
|
|
8
|
+
class JwtConfig(BaseModel):
|
|
9
|
+
secret_key: str = "xxx"
|
|
10
|
+
algorithm: str = "HS256"
|
|
11
|
+
scheme: str = "JWT"
|
|
12
|
+
token_expires_time: str = "1200" # seconds, legacy name
|
|
13
|
+
access_token_expires_minutes: int | None = None
|
|
14
|
+
refresh_token_expires_days: int = 7
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def access_expires(self) -> timedelta:
|
|
18
|
+
"""Preferred access token expiry."""
|
|
19
|
+
if self.access_token_expires_minutes is not None:
|
|
20
|
+
return timedelta(minutes=self.access_token_expires_minutes)
|
|
21
|
+
# fallback to legacy seconds-based setting
|
|
22
|
+
return timedelta(seconds=int(self.token_expires_time))
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def refresh_expires(self) -> timedelta:
|
|
26
|
+
return timedelta(days=self.refresh_token_expires_days)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AuthConfig(BaseSettings):
|
|
32
|
+
"""
|
|
33
|
+
Runtime configuration loaded from environment variables with ``AUTH_`` prefix.
|
|
34
|
+
|
|
35
|
+
Host applications can still pass an ``AuthConfig`` instance directly;
|
|
36
|
+
in that case, the passed values override the environment values.
|
|
37
|
+
"""
|
|
38
|
+
jwt: JwtConfig = JwtConfig()
|
|
39
|
+
|
|
40
|
+
router_prefix: str = "/auth"
|
|
41
|
+
router_tags: List[str] = Field(default_factory=lambda: ["authentication"])
|
|
42
|
+
|
|
43
|
+
enable_password_recovery: bool = True
|
|
44
|
+
enable_user_registration: bool = True
|
|
45
|
+
enable_token_refresh: bool = True
|
|
46
|
+
enable_html_content: bool = True
|
|
47
|
+
|
|
48
|
+
email_from: str = "noreply@example.com"
|
|
49
|
+
email_from_name: str = "Auth System"
|
|
50
|
+
password_reset_url_template: str = (
|
|
51
|
+
"https://yourapp.com/reset-password?token={token}"
|
|
52
|
+
)
|
|
53
|
+
verification_url_template: str = (
|
|
54
|
+
"https://yourapp.com/verify-email?token={token}"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
model_config = SettingsConfigDict(
|
|
58
|
+
env_prefix="AUTH_",
|
|
59
|
+
extra="ignore",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# 属性访问器:直接访问 jwt 中的属性,兼容现有代码
|
|
63
|
+
@property
|
|
64
|
+
def secret_key(self) -> str:
|
|
65
|
+
"""JWT secret key"""
|
|
66
|
+
return self.jwt.secret_key
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def algorithm(self) -> str:
|
|
70
|
+
"""JWT algorithm"""
|
|
71
|
+
return self.jwt.algorithm
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def access_token_expire_minutes(self) -> Optional[int]:
|
|
75
|
+
"""Access token expiration in minutes (compatible with auth.py)"""
|
|
76
|
+
return self.jwt.access_token_expires_minutes
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def refresh_token_expire_days(self) -> int:
|
|
80
|
+
"""Refresh token expiration in days"""
|
|
81
|
+
return self.jwt.refresh_token_expire_days
|
|
82
|
+
|
|
83
|
+
_config = AuthConfig()
|
|
84
|
+
|
|
85
|
+
class AuthDependencyConfig(BaseModel):
|
|
86
|
+
"""
|
|
87
|
+
Optional dependency injection container.
|
|
88
|
+
|
|
89
|
+
Host projects can supply custom implementations for the overridable hooks.
|
|
90
|
+
"""
|
|
91
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
92
|
+
|
|
93
|
+
user_repository: Optional[Any] = None
|
|
94
|
+
password_hasher: Optional[Any] = None
|
|
95
|
+
token_service: Optional[Any] = None
|
|
96
|
+
mailer: Optional[Any] = None
|
|
97
|
+
# 可选:自定义 token 提取依赖(例如自定义 Bearer/JWT 解析)
|
|
98
|
+
token_dependency: Optional[Any] = None
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from typing import Annotated, Optional, List
|
|
2
|
+
from fastapi import Request, HTTPException, status, Depends
|
|
3
|
+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
4
|
+
from fastapi.security.utils import get_authorization_scheme_param
|
|
5
|
+
import jwt
|
|
6
|
+
from jwt.exceptions import InvalidTokenError
|
|
7
|
+
from pydantic import BaseModel, ValidationError
|
|
8
|
+
from .core.config import _config
|
|
9
|
+
from .models.user import User
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FlexibleHTTPBearer(HTTPBearer):
|
|
13
|
+
"""支持自定义 scheme 的 HTTP Bearer"""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
*,
|
|
18
|
+
accepted_schemes: Optional[List[str]] = None,
|
|
19
|
+
scheme_name: Optional[str] = None,
|
|
20
|
+
auto_error: bool = True,
|
|
21
|
+
):
|
|
22
|
+
self.accepted_schemes_lower = [
|
|
23
|
+
s.lower() for s in (accepted_schemes or ["bearer", "jwt"])
|
|
24
|
+
]
|
|
25
|
+
super().__init__(
|
|
26
|
+
scheme_name=scheme_name or "Bearer/JWT",
|
|
27
|
+
auto_error=auto_error,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
async def __call__(
|
|
31
|
+
self, request: Request
|
|
32
|
+
) -> Optional[HTTPAuthorizationCredentials]:
|
|
33
|
+
authorization = request.headers.get("Authorization")
|
|
34
|
+
scheme, credentials = get_authorization_scheme_param(authorization)
|
|
35
|
+
|
|
36
|
+
if not (authorization and scheme and credentials):
|
|
37
|
+
if self.auto_error:
|
|
38
|
+
raise HTTPException(
|
|
39
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
40
|
+
detail="Not authenticated",
|
|
41
|
+
)
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
if scheme.lower() not in self.accepted_schemes_lower:
|
|
45
|
+
if self.auto_error:
|
|
46
|
+
raise HTTPException(
|
|
47
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
48
|
+
detail=f"Invalid auth scheme. Accepted: {', '.join(self.accepted_schemes_lower)}",
|
|
49
|
+
)
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
jwt_scheme = FlexibleHTTPBearer(
|
|
56
|
+
accepted_schemes=["bearer", "jwt"], # 可以改成 ["bearer"] 或 ["jwt"]
|
|
57
|
+
auto_error=True
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
class TokenPayload(BaseModel):
|
|
61
|
+
"""
|
|
62
|
+
JWT 载荷模型
|
|
63
|
+
- sub: 用户 ID
|
|
64
|
+
- type: 令牌类型(access/refresh),旧 token 可能缺少
|
|
65
|
+
- exp/nbf 等字段由 jwt 库自行校验
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
sub: int
|
|
69
|
+
type: str | None = None
|
|
70
|
+
|
|
71
|
+
async def get_token(
|
|
72
|
+
credentials: HTTPAuthorizationCredentials = Depends(jwt_scheme),
|
|
73
|
+
) -> str:
|
|
74
|
+
if credentials is None:
|
|
75
|
+
raise HTTPException(
|
|
76
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
77
|
+
detail="Not authenticated",
|
|
78
|
+
)
|
|
79
|
+
scheme = credentials.scheme # "Bearer" / "JWT"
|
|
80
|
+
token = credentials.credentials
|
|
81
|
+
|
|
82
|
+
if scheme.lower() not in ("bearer", "jwt"):
|
|
83
|
+
raise HTTPException(
|
|
84
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
85
|
+
detail=f"Invalid auth scheme. Expected 'Bearer' or 'JWT', got '{scheme}'",
|
|
86
|
+
)
|
|
87
|
+
return token
|
|
88
|
+
|
|
89
|
+
TokenDep = Annotated[str, Depends(get_token)]
|
|
90
|
+
|
|
91
|
+
async def get_current_user(token: TokenDep) -> User:
|
|
92
|
+
try:
|
|
93
|
+
payload = jwt.decode(
|
|
94
|
+
token,
|
|
95
|
+
_config.jwt.secret_key,
|
|
96
|
+
algorithms=[_config.jwt.algorithm],
|
|
97
|
+
)
|
|
98
|
+
token_data = TokenPayload(**payload)
|
|
99
|
+
if token_data.type and token_data.type != "access":
|
|
100
|
+
raise InvalidTokenError("Invalid token type")
|
|
101
|
+
except (InvalidTokenError, ValidationError):
|
|
102
|
+
raise HTTPException(
|
|
103
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
104
|
+
detail="Could not validate credentials",
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
user = await User.get_or_none(id=token_data.sub)
|
|
108
|
+
if not user:
|
|
109
|
+
raise HTTPException(status_code=404, detail="User not found")
|
|
110
|
+
if not user.is_active:
|
|
111
|
+
raise HTTPException(status_code=400, detail="Inactive user")
|
|
112
|
+
return user
|
|
113
|
+
|
|
114
|
+
CurrentUser = Annotated[User, Depends(get_current_user)]
|
|
115
|
+
|
|
116
|
+
def get_current_active_superuser(current_user: CurrentUser) -> User:
|
|
117
|
+
if not current_user.is_superuser:
|
|
118
|
+
raise HTTPException(
|
|
119
|
+
status_code=403, detail="The user doesn't have enough privileges"
|
|
120
|
+
)
|
|
121
|
+
return current_user
|