fastapi-rtk 0.2.27__py3-none-any.whl → 1.0.13__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.
- fastapi_rtk/__init__.py +39 -35
- fastapi_rtk/_version.py +1 -0
- fastapi_rtk/api/model_rest_api.py +476 -221
- fastapi_rtk/auth/auth.py +0 -9
- fastapi_rtk/backends/generic/__init__.py +6 -0
- fastapi_rtk/backends/generic/column.py +21 -12
- fastapi_rtk/backends/generic/db.py +42 -7
- fastapi_rtk/backends/generic/filters.py +21 -16
- fastapi_rtk/backends/generic/interface.py +14 -8
- fastapi_rtk/backends/generic/model.py +19 -11
- fastapi_rtk/backends/sqla/__init__.py +1 -0
- fastapi_rtk/backends/sqla/db.py +77 -17
- fastapi_rtk/backends/sqla/extensions/audit/audit.py +401 -189
- fastapi_rtk/backends/sqla/extensions/geoalchemy2/filters.py +15 -12
- fastapi_rtk/backends/sqla/filters.py +50 -21
- fastapi_rtk/backends/sqla/interface.py +96 -34
- fastapi_rtk/backends/sqla/model.py +56 -39
- fastapi_rtk/bases/__init__.py +20 -0
- fastapi_rtk/bases/db.py +94 -7
- fastapi_rtk/bases/file_manager.py +47 -3
- fastapi_rtk/bases/filter.py +22 -0
- fastapi_rtk/bases/interface.py +49 -5
- fastapi_rtk/bases/model.py +3 -0
- fastapi_rtk/bases/session.py +2 -0
- fastapi_rtk/cli/cli.py +62 -9
- fastapi_rtk/cli/commands/__init__.py +23 -0
- fastapi_rtk/cli/{db.py → commands/db/__init__.py} +107 -50
- fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/env.py +2 -3
- fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/env.py +10 -9
- fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/script.py.mako +3 -1
- fastapi_rtk/cli/{export.py → commands/export.py} +12 -10
- fastapi_rtk/cli/{security.py → commands/security.py} +73 -7
- fastapi_rtk/cli/commands/translate.py +299 -0
- fastapi_rtk/cli/decorators.py +9 -4
- fastapi_rtk/cli/utils.py +46 -0
- fastapi_rtk/config.py +41 -1
- fastapi_rtk/const.py +29 -1
- fastapi_rtk/db.py +76 -40
- fastapi_rtk/decorators.py +1 -1
- fastapi_rtk/dependencies.py +134 -62
- fastapi_rtk/exceptions.py +51 -1
- fastapi_rtk/fastapi_react_toolkit.py +186 -171
- fastapi_rtk/file_managers/file_manager.py +8 -6
- fastapi_rtk/file_managers/s3_file_manager.py +69 -33
- fastapi_rtk/globals.py +22 -12
- fastapi_rtk/lang/__init__.py +3 -0
- fastapi_rtk/lang/babel/__init__.py +4 -0
- fastapi_rtk/lang/babel/cli.py +40 -0
- fastapi_rtk/lang/babel/config.py +17 -0
- fastapi_rtk/lang/babel.cfg +1 -0
- fastapi_rtk/lang/lazy_text.py +120 -0
- fastapi_rtk/lang/messages.pot +238 -0
- fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.mo +0 -0
- fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.po +248 -0
- fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.mo +0 -0
- fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.po +244 -0
- fastapi_rtk/manager.py +355 -37
- fastapi_rtk/mixins.py +12 -0
- fastapi_rtk/routers.py +208 -72
- fastapi_rtk/schemas.py +142 -39
- fastapi_rtk/security/sqla/apis.py +39 -13
- fastapi_rtk/security/sqla/models.py +8 -23
- fastapi_rtk/security/sqla/security_manager.py +369 -11
- fastapi_rtk/setting.py +446 -88
- fastapi_rtk/types.py +94 -27
- fastapi_rtk/utils/__init__.py +8 -0
- fastapi_rtk/utils/async_task_runner.py +286 -61
- fastapi_rtk/utils/csv_json_converter.py +243 -40
- fastapi_rtk/utils/hooks.py +34 -0
- fastapi_rtk/utils/merge_schema.py +3 -3
- fastapi_rtk/utils/multiple_async_contexts.py +21 -0
- fastapi_rtk/utils/pydantic.py +46 -1
- fastapi_rtk/utils/run_utils.py +31 -1
- fastapi_rtk/utils/self_dependencies.py +1 -1
- fastapi_rtk/utils/use_default_when_none.py +1 -1
- fastapi_rtk/version.py +6 -1
- fastapi_rtk-1.0.13.dist-info/METADATA +28 -0
- fastapi_rtk-1.0.13.dist-info/RECORD +133 -0
- {fastapi_rtk-0.2.27.dist-info → fastapi_rtk-1.0.13.dist-info}/WHEEL +1 -2
- fastapi_rtk/backends/gremlinpython/__init__.py +0 -108
- fastapi_rtk/backends/gremlinpython/column.py +0 -208
- fastapi_rtk/backends/gremlinpython/db.py +0 -228
- fastapi_rtk/backends/gremlinpython/exceptions.py +0 -34
- fastapi_rtk/backends/gremlinpython/filters.py +0 -461
- fastapi_rtk/backends/gremlinpython/interface.py +0 -734
- fastapi_rtk/backends/gremlinpython/model.py +0 -364
- fastapi_rtk/backends/gremlinpython/session.py +0 -23
- fastapi_rtk/cli/commands.py +0 -295
- fastapi_rtk-0.2.27.dist-info/METADATA +0 -23
- fastapi_rtk-0.2.27.dist-info/RECORD +0 -126
- fastapi_rtk-0.2.27.dist-info/top_level.txt +0 -1
- /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/README +0 -0
- /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/alembic.ini.mako +0 -0
- /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/script.py.mako +0 -0
- /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/README +0 -0
- /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/alembic.ini.mako +0 -0
- {fastapi_rtk-0.2.27.dist-info → fastapi_rtk-1.0.13.dist-info}/entry_points.txt +0 -0
- {fastapi_rtk-0.2.27.dist-info → fastapi_rtk-1.0.13.dist-info}/licenses/LICENSE +0 -0
fastapi_rtk/routers.py
CHANGED
|
@@ -1,21 +1,29 @@
|
|
|
1
|
-
|
|
1
|
+
import typing
|
|
2
2
|
|
|
3
3
|
import jwt
|
|
4
|
+
import pydantic
|
|
4
5
|
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
|
|
5
6
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
|
6
7
|
from fastapi.security import OAuth2PasswordRequestForm
|
|
7
8
|
from fastapi_users import models, schemas
|
|
8
9
|
from fastapi_users.authentication import AuthenticationBackend, Authenticator, Strategy
|
|
9
10
|
from fastapi_users.exceptions import UserAlreadyExists
|
|
10
|
-
from fastapi_users.jwt import SecretType, decode_jwt
|
|
11
|
-
from fastapi_users.manager import
|
|
11
|
+
from fastapi_users.jwt import SecretType, decode_jwt
|
|
12
|
+
from fastapi_users.manager import UserManagerDependency
|
|
12
13
|
from fastapi_users.openapi import OpenAPIResponseType
|
|
13
14
|
from fastapi_users.router.common import ErrorCode, ErrorModel
|
|
14
|
-
from fastapi_users.router.oauth import STATE_TOKEN_AUDIENCE
|
|
15
|
-
from httpx_oauth.integrations.fastapi import
|
|
15
|
+
from fastapi_users.router.oauth import STATE_TOKEN_AUDIENCE, generate_state_token
|
|
16
|
+
from httpx_oauth.integrations.fastapi import (
|
|
17
|
+
OAuth2AuthorizeCallback as BaseOAuth2AuthorizeCallback,
|
|
18
|
+
)
|
|
19
|
+
from httpx_oauth.integrations.fastapi import OAuth2AuthorizeCallbackError
|
|
16
20
|
from httpx_oauth.oauth2 import BaseOAuth2, OAuth2Token
|
|
17
21
|
|
|
22
|
+
from .const import AuthType
|
|
23
|
+
from .const import ErrorCode as _ErrorCode
|
|
18
24
|
from .globals import g
|
|
25
|
+
from .manager import UserManager
|
|
26
|
+
from .utils import smart_run
|
|
19
27
|
|
|
20
28
|
__all__ = ["get_oauth_router", "get_oauth_associate_router", "get_auth_router"]
|
|
21
29
|
|
|
@@ -32,49 +40,124 @@ HTML_SUCCESS_RESPONSE = """
|
|
|
32
40
|
</html>
|
|
33
41
|
"""
|
|
34
42
|
|
|
35
|
-
POPUP_DICT = {}
|
|
36
43
|
|
|
44
|
+
class OAuth2AuthorizeCallback(BaseOAuth2AuthorizeCallback):
|
|
45
|
+
"""
|
|
46
|
+
Modified version of the `OAuth2AuthorizeCallback` from `httpx_oauth.integrations.fastapi`.
|
|
47
|
+
- Adds `state_secret` parameter to support for decoding `state` JWT to get `redirect_url`.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
state_secret: SecretType | None
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
client,
|
|
55
|
+
route_name=None,
|
|
56
|
+
redirect_url=None,
|
|
57
|
+
state_secret: SecretType | None = None,
|
|
58
|
+
):
|
|
59
|
+
self.state_secret = state_secret
|
|
60
|
+
# Use `redirect_url` from `state`
|
|
61
|
+
if self.state_secret:
|
|
62
|
+
route_name = None
|
|
63
|
+
redirect_url = ""
|
|
64
|
+
super().__init__(client, route_name, redirect_url)
|
|
65
|
+
|
|
66
|
+
async def __call__(
|
|
67
|
+
self,
|
|
68
|
+
request: Request,
|
|
69
|
+
code: typing.Optional[str] = None,
|
|
70
|
+
code_verifier: typing.Optional[str] = None,
|
|
71
|
+
state: typing.Optional[str] = None,
|
|
72
|
+
error: typing.Optional[str] = None,
|
|
73
|
+
):
|
|
74
|
+
original_redirect_url = self.redirect_url
|
|
75
|
+
if self.state_secret:
|
|
76
|
+
# Try to decode JWT
|
|
77
|
+
try:
|
|
78
|
+
state_data = decode_jwt(
|
|
79
|
+
state, self.state_secret, [STATE_TOKEN_AUDIENCE]
|
|
80
|
+
)
|
|
81
|
+
request.state.state_data = state_data
|
|
82
|
+
self.redirect_url = state_data.get(
|
|
83
|
+
"redirect_url", original_redirect_url
|
|
84
|
+
)
|
|
85
|
+
except jwt.PyJWTError:
|
|
86
|
+
raise OAuth2AuthorizeCallbackError(
|
|
87
|
+
status.HTTP_400_BAD_REQUEST,
|
|
88
|
+
"Invalid state token",
|
|
89
|
+
)
|
|
37
90
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return generate_jwt(data, secret, lifetime_seconds)
|
|
91
|
+
try:
|
|
92
|
+
return await super().__call__(request, code, code_verifier, state, error)
|
|
93
|
+
finally:
|
|
94
|
+
self.redirect_url = original_redirect_url
|
|
43
95
|
|
|
44
96
|
|
|
45
97
|
def get_oauth_router(
|
|
46
98
|
oauth_client: BaseOAuth2,
|
|
47
|
-
backend: AuthenticationBackend,
|
|
99
|
+
backend: AuthenticationBackend[models.UP, models.ID],
|
|
48
100
|
get_user_manager: UserManagerDependency[models.UP, models.ID],
|
|
49
101
|
state_secret: SecretType,
|
|
50
|
-
redirect_url: Optional[str] = None,
|
|
102
|
+
redirect_url: typing.Optional[str] = None,
|
|
103
|
+
redirect_url_factory: typing.Optional[
|
|
104
|
+
typing.Callable[[Request, list[str]], str]
|
|
105
|
+
] = None,
|
|
106
|
+
redirect_url_after_callback: typing.Optional[str] = None,
|
|
51
107
|
associate_by_email: bool = False,
|
|
52
108
|
is_verified_by_default: bool = False,
|
|
53
|
-
**kwargs:
|
|
109
|
+
**kwargs: dict[str, typing.Any],
|
|
54
110
|
) -> APIRouter:
|
|
55
111
|
"""
|
|
56
|
-
Modified version of the
|
|
57
|
-
- `
|
|
58
|
-
-
|
|
112
|
+
Modified version of the `get_oauth_router` from `fastapi_users.router.oauth`.
|
|
113
|
+
- Updates the `login` to redirect to `authorization_url` directly instead of returning a JSONResponse with key `authorization_url`.
|
|
114
|
+
- Adds `redirect_url_factory` to allow dynamic generation of the redirect URL.
|
|
115
|
+
- Adds `redirect_url_after_callback` to redirect user to the application after OAuth callback.
|
|
116
|
+
- Adds `**kwargs` to provide additional arguments for the user manager's `oauth_callback` method.
|
|
59
117
|
|
|
60
118
|
Generate a router with the OAuth routes.
|
|
61
119
|
"""
|
|
62
120
|
router = APIRouter()
|
|
63
121
|
callback_route_name = f"oauth:{oauth_client.name}.{backend.name}.callback"
|
|
64
122
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
123
|
+
if redirect_url is not None:
|
|
124
|
+
oauth2_authorize_callback = OAuth2AuthorizeCallback(
|
|
125
|
+
oauth_client,
|
|
126
|
+
redirect_url=redirect_url,
|
|
127
|
+
)
|
|
128
|
+
elif redirect_url_factory is not None:
|
|
129
|
+
oauth2_authorize_callback = OAuth2AuthorizeCallback(
|
|
130
|
+
oauth_client,
|
|
131
|
+
state_secret=state_secret,
|
|
132
|
+
)
|
|
133
|
+
else:
|
|
134
|
+
oauth2_authorize_callback = OAuth2AuthorizeCallback(
|
|
135
|
+
oauth_client,
|
|
136
|
+
route_name=callback_route_name,
|
|
137
|
+
)
|
|
69
138
|
|
|
70
139
|
@router.get(
|
|
71
140
|
f"/login/{oauth_client.name}",
|
|
72
141
|
name=f"oauth:{oauth_client.name}.{backend.name}.authorize",
|
|
73
142
|
)
|
|
74
|
-
async def authorize(request: Request, scopes:
|
|
75
|
-
|
|
143
|
+
async def authorize(request: Request, scopes: list[str] = Query(None)):
|
|
144
|
+
if redirect_url is not None:
|
|
145
|
+
authorize_redirect_url = redirect_url
|
|
146
|
+
elif redirect_url_factory is not None:
|
|
147
|
+
authorize_redirect_url = await smart_run(
|
|
148
|
+
redirect_url_factory, request, scopes
|
|
149
|
+
)
|
|
150
|
+
else:
|
|
151
|
+
authorize_redirect_url = str(request.url_for(callback_route_name))
|
|
76
152
|
|
|
77
|
-
state_data
|
|
153
|
+
state_data = dict[str, str]()
|
|
154
|
+
if redirect_url_factory:
|
|
155
|
+
state_data["redirect_url"] = authorize_redirect_url
|
|
156
|
+
if (
|
|
157
|
+
request.query_params.get("popup") == "1"
|
|
158
|
+
or request.query_params.get("popup") == "true"
|
|
159
|
+
):
|
|
160
|
+
state_data["popup"] = "true"
|
|
78
161
|
state = generate_state_token(state_data, state_secret)
|
|
79
162
|
authorization_url = await oauth_client.get_authorization_url(
|
|
80
163
|
authorize_redirect_url,
|
|
@@ -82,13 +165,6 @@ def get_oauth_router(
|
|
|
82
165
|
scopes,
|
|
83
166
|
)
|
|
84
167
|
|
|
85
|
-
if (
|
|
86
|
-
request.query_params.get("popup") == "1"
|
|
87
|
-
or request.query_params.get("popup") == "true"
|
|
88
|
-
):
|
|
89
|
-
# Store the popup in POPUP_DICT
|
|
90
|
-
POPUP_DICT[state] = True
|
|
91
|
-
|
|
92
168
|
return RedirectResponse(authorization_url)
|
|
93
169
|
|
|
94
170
|
@router.get(
|
|
@@ -117,10 +193,10 @@ def get_oauth_router(
|
|
|
117
193
|
)
|
|
118
194
|
async def callback(
|
|
119
195
|
request: Request,
|
|
120
|
-
access_token_state:
|
|
196
|
+
access_token_state: tuple[OAuth2Token, str] = Depends(
|
|
121
197
|
oauth2_authorize_callback
|
|
122
198
|
),
|
|
123
|
-
user_manager:
|
|
199
|
+
user_manager: UserManager = Depends(get_user_manager),
|
|
124
200
|
strategy: Strategy[models.UP, models.ID] = Depends(backend.get_strategy),
|
|
125
201
|
):
|
|
126
202
|
token, state = access_token_state
|
|
@@ -134,10 +210,13 @@ def get_oauth_router(
|
|
|
134
210
|
detail=ErrorCode.OAUTH_NOT_AVAILABLE_EMAIL,
|
|
135
211
|
)
|
|
136
212
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
213
|
+
# Try to use existing state data from request.state
|
|
214
|
+
state_data = getattr(request.state, "state_data", None)
|
|
215
|
+
if state_data is None:
|
|
216
|
+
try:
|
|
217
|
+
state_data = decode_jwt(state, state_secret, [STATE_TOKEN_AUDIENCE])
|
|
218
|
+
except jwt.DecodeError:
|
|
219
|
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
|
|
141
220
|
|
|
142
221
|
try:
|
|
143
222
|
user = await user_manager.oauth_callback(
|
|
@@ -169,17 +248,17 @@ def get_oauth_router(
|
|
|
169
248
|
response = await backend.login(strategy, user)
|
|
170
249
|
await user_manager.on_after_login(user, request, response)
|
|
171
250
|
|
|
172
|
-
if
|
|
173
|
-
|
|
174
|
-
POPUP_DICT.pop(state)
|
|
175
|
-
|
|
251
|
+
# Check if the request is from a popup
|
|
252
|
+
if state_data.get("popup") == "true":
|
|
176
253
|
return HTMLResponse(
|
|
177
254
|
content=HTML_SUCCESS_RESPONSE,
|
|
178
255
|
headers=response.headers,
|
|
179
256
|
)
|
|
180
257
|
|
|
181
|
-
if
|
|
182
|
-
return RedirectResponse(
|
|
258
|
+
if redirect_url_after_callback:
|
|
259
|
+
return RedirectResponse(
|
|
260
|
+
redirect_url_after_callback, headers=response.headers
|
|
261
|
+
)
|
|
183
262
|
|
|
184
263
|
return response
|
|
185
264
|
|
|
@@ -188,17 +267,24 @@ def get_oauth_router(
|
|
|
188
267
|
|
|
189
268
|
def get_oauth_associate_router(
|
|
190
269
|
oauth_client: BaseOAuth2,
|
|
191
|
-
authenticator: Authenticator,
|
|
270
|
+
authenticator: Authenticator[models.UP, models.ID],
|
|
192
271
|
get_user_manager: UserManagerDependency[models.UP, models.ID],
|
|
193
|
-
user_schema:
|
|
272
|
+
user_schema: type[schemas.U],
|
|
194
273
|
state_secret: SecretType,
|
|
195
|
-
redirect_url: Optional[str] = None,
|
|
274
|
+
redirect_url: typing.Optional[str] = None,
|
|
275
|
+
redirect_url_factory: typing.Optional[
|
|
276
|
+
typing.Callable[[Request, list[str]], str]
|
|
277
|
+
] = None,
|
|
278
|
+
redirect_url_after_callback: typing.Optional[str] = None,
|
|
196
279
|
requires_verification: bool = False,
|
|
197
280
|
) -> APIRouter:
|
|
198
281
|
"""
|
|
199
|
-
|
|
282
|
+
Modified version of the `get_oauth_associate_router` from `fastapi_users.router.oauth`.
|
|
283
|
+
- Updates the `login` to redirect to `authorization_url` directly instead of returning a JSONResponse with key `authorization_url`.
|
|
284
|
+
- Adds `redirect_url_factory` to allow dynamic generation of the redirect URL.
|
|
285
|
+
- Adds `redirect_url_after_callback` to redirect user to the application after OAuth callback.
|
|
200
286
|
|
|
201
|
-
|
|
287
|
+
Generate a router with the OAuth routes to associate an authenticated user.
|
|
202
288
|
"""
|
|
203
289
|
router = APIRouter()
|
|
204
290
|
|
|
@@ -208,10 +294,21 @@ def get_oauth_associate_router(
|
|
|
208
294
|
|
|
209
295
|
callback_route_name = f"oauth-associate:{oauth_client.name}.callback"
|
|
210
296
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
297
|
+
if redirect_url is not None:
|
|
298
|
+
oauth2_authorize_callback = OAuth2AuthorizeCallback(
|
|
299
|
+
oauth_client,
|
|
300
|
+
redirect_url=redirect_url,
|
|
301
|
+
)
|
|
302
|
+
elif redirect_url_factory is not None:
|
|
303
|
+
oauth2_authorize_callback = OAuth2AuthorizeCallback(
|
|
304
|
+
oauth_client,
|
|
305
|
+
state_secret=state_secret,
|
|
306
|
+
)
|
|
307
|
+
else:
|
|
308
|
+
oauth2_authorize_callback = OAuth2AuthorizeCallback(
|
|
309
|
+
oauth_client,
|
|
310
|
+
route_name=callback_route_name,
|
|
311
|
+
)
|
|
215
312
|
|
|
216
313
|
@router.get(
|
|
217
314
|
f"/login/{oauth_client.name}",
|
|
@@ -219,12 +316,21 @@ def get_oauth_associate_router(
|
|
|
219
316
|
)
|
|
220
317
|
async def authorize(
|
|
221
318
|
request: Request,
|
|
222
|
-
scopes:
|
|
319
|
+
scopes: list[str] = Query(None),
|
|
223
320
|
user: models.UP = Depends(get_current_active_user),
|
|
224
321
|
):
|
|
225
|
-
|
|
322
|
+
if redirect_url is not None:
|
|
323
|
+
authorize_redirect_url = redirect_url
|
|
324
|
+
elif redirect_url_factory is not None:
|
|
325
|
+
authorize_redirect_url = await smart_run(
|
|
326
|
+
redirect_url_factory, request, scopes
|
|
327
|
+
)
|
|
328
|
+
else:
|
|
329
|
+
authorize_redirect_url = str(request.url_for(callback_route_name))
|
|
226
330
|
|
|
227
|
-
state_data
|
|
331
|
+
state_data = {"sub": str(user.id)}
|
|
332
|
+
if redirect_url_factory:
|
|
333
|
+
state_data["redirect_url"] = authorize_redirect_url
|
|
228
334
|
state = generate_state_token(state_data, state_secret)
|
|
229
335
|
authorization_url = await oauth_client.get_authorization_url(
|
|
230
336
|
authorize_redirect_url,
|
|
@@ -257,10 +363,10 @@ def get_oauth_associate_router(
|
|
|
257
363
|
async def callback(
|
|
258
364
|
request: Request,
|
|
259
365
|
user: models.UP = Depends(get_current_active_user),
|
|
260
|
-
access_token_state:
|
|
366
|
+
access_token_state: tuple[OAuth2Token, str] = Depends(
|
|
261
367
|
oauth2_authorize_callback
|
|
262
368
|
),
|
|
263
|
-
user_manager:
|
|
369
|
+
user_manager: UserManager = Depends(get_user_manager),
|
|
264
370
|
):
|
|
265
371
|
token, state = access_token_state
|
|
266
372
|
account_id, account_email = await oauth_client.get_id_email(
|
|
@@ -273,10 +379,13 @@ def get_oauth_associate_router(
|
|
|
273
379
|
detail=ErrorCode.OAUTH_NOT_AVAILABLE_EMAIL,
|
|
274
380
|
)
|
|
275
381
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
382
|
+
# Try to use existing state data from request.state
|
|
383
|
+
state_data = getattr(request.state, "state_data", None)
|
|
384
|
+
if state_data is None:
|
|
385
|
+
try:
|
|
386
|
+
state_data = decode_jwt(state, state_secret, [STATE_TOKEN_AUDIENCE])
|
|
387
|
+
except jwt.DecodeError:
|
|
388
|
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
|
|
280
389
|
|
|
281
390
|
if state_data["sub"] != str(user.id):
|
|
282
391
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
|
|
@@ -292,8 +401,8 @@ def get_oauth_associate_router(
|
|
|
292
401
|
request,
|
|
293
402
|
)
|
|
294
403
|
|
|
295
|
-
if
|
|
296
|
-
return RedirectResponse(
|
|
404
|
+
if redirect_url_after_callback:
|
|
405
|
+
return RedirectResponse(redirect_url_after_callback)
|
|
297
406
|
|
|
298
407
|
return schemas.model_validate(user_schema, user)
|
|
299
408
|
|
|
@@ -301,14 +410,18 @@ def get_oauth_associate_router(
|
|
|
301
410
|
|
|
302
411
|
|
|
303
412
|
def get_auth_router(
|
|
304
|
-
backend: AuthenticationBackend,
|
|
413
|
+
backend: AuthenticationBackend[models.UP, models.ID],
|
|
305
414
|
get_user_manager: UserManagerDependency[models.UP, models.ID],
|
|
306
|
-
authenticator: Authenticator,
|
|
415
|
+
authenticator: Authenticator[models.UP, models.ID],
|
|
307
416
|
requires_verification: bool = False,
|
|
417
|
+
auth_type: str | None = None,
|
|
308
418
|
) -> APIRouter:
|
|
309
|
-
"""
|
|
419
|
+
"""
|
|
420
|
+
Modified version of the `get_auth_router` from `fastapi_users.router.auth`.
|
|
421
|
+
- Sets `g.user` to `None` after logout.
|
|
422
|
+
- Adds `auth_type` parameter to set various authentication methods like `ldap`.
|
|
310
423
|
|
|
311
|
-
|
|
424
|
+
Generate a router with login/logout routes for an authentication backend.
|
|
312
425
|
"""
|
|
313
426
|
router = APIRouter()
|
|
314
427
|
get_current_user_token = authenticator.current_user_token(
|
|
@@ -344,10 +457,13 @@ def get_auth_router(
|
|
|
344
457
|
async def login(
|
|
345
458
|
request: Request,
|
|
346
459
|
credentials: OAuth2PasswordRequestForm = Depends(),
|
|
347
|
-
user_manager:
|
|
460
|
+
user_manager: UserManager = Depends(get_user_manager),
|
|
348
461
|
strategy: Strategy[models.UP, models.ID] = Depends(backend.get_strategy),
|
|
349
462
|
):
|
|
350
|
-
|
|
463
|
+
if auth_type == AuthType.LDAP:
|
|
464
|
+
user = await user_manager.authenticate_ldap(credentials)
|
|
465
|
+
else:
|
|
466
|
+
user = await user_manager.authenticate(credentials)
|
|
351
467
|
|
|
352
468
|
if user is None or not user.is_active:
|
|
353
469
|
raise HTTPException(
|
|
@@ -363,10 +479,30 @@ def get_auth_router(
|
|
|
363
479
|
await user_manager.on_after_login(user, request, response)
|
|
364
480
|
return response
|
|
365
481
|
|
|
482
|
+
class LoginBody(pydantic.BaseModel):
|
|
483
|
+
username: str
|
|
484
|
+
password: str
|
|
485
|
+
|
|
486
|
+
@router.post(
|
|
487
|
+
"/login-json",
|
|
488
|
+
name=f"auth:{backend.name}.login-json",
|
|
489
|
+
responses=login_responses,
|
|
490
|
+
)
|
|
491
|
+
async def login_with_json(
|
|
492
|
+
request: Request,
|
|
493
|
+
credentials: LoginBody,
|
|
494
|
+
user_manager: UserManager = Depends(get_user_manager),
|
|
495
|
+
strategy: Strategy[models.UP, models.ID] = Depends(backend.get_strategy),
|
|
496
|
+
):
|
|
497
|
+
credentials = OAuth2PasswordRequestForm(
|
|
498
|
+
username=credentials.username, password=credentials.password
|
|
499
|
+
)
|
|
500
|
+
return await login(request, credentials, user_manager, strategy)
|
|
501
|
+
|
|
366
502
|
logout_responses: OpenAPIResponseType = {
|
|
367
503
|
**{
|
|
368
504
|
status.HTTP_401_UNAUTHORIZED: {
|
|
369
|
-
"description":
|
|
505
|
+
"description": _ErrorCode.GET_USER_MISSING_TOKEN_OR_INACTIVE_USER
|
|
370
506
|
}
|
|
371
507
|
},
|
|
372
508
|
**backend.transport.get_openapi_logout_responses_success(),
|
|
@@ -377,8 +513,8 @@ def get_auth_router(
|
|
|
377
513
|
)
|
|
378
514
|
async def logout(
|
|
379
515
|
request: Request,
|
|
380
|
-
user_token:
|
|
381
|
-
user_manager:
|
|
516
|
+
user_token: tuple[models.UP, str] = Depends(get_current_user_token),
|
|
517
|
+
user_manager: UserManager = Depends(get_user_manager),
|
|
382
518
|
strategy: Strategy[models.UP, models.ID] = Depends(backend.get_strategy),
|
|
383
519
|
):
|
|
384
520
|
user, token = user_token
|