Skip to content

Commit 83953b2

Browse files
committed
Extract get user auth token service
1 parent 81dd7b4 commit 83953b2

File tree

4 files changed

+109
-26
lines changed

4 files changed

+109
-26
lines changed

futuramaapi/routers/rest/tokens/api.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
from typing import Annotated
22

33
from fastapi import APIRouter, Depends, status
4+
from fastapi.security import OAuth2PasswordRequestForm
5+
from pydantic import SecretStr
46

57
from futuramaapi.routers.exceptions import UnauthorizedResponse
6-
from futuramaapi.routers.rest.users.schemas import User
8+
from futuramaapi.routers.services.tokens.get_auth_user_token import (
9+
GetAuthUserTokenResponse,
10+
GetAuthUserTokenService,
11+
)
712

8-
from .dependencies import from_form_data, refresh_token
13+
from .dependencies import refresh_token
914
from .schemas import UserToken
1015

1116
router: APIRouter = APIRouter(
@@ -21,12 +26,15 @@
2126
"model": UnauthorizedResponse,
2227
},
2328
},
24-
response_model=UserToken,
25-
name="user_token_auth",
29+
response_model=GetAuthUserTokenResponse,
30+
name="get_user_auth_token",
2631
)
27-
async def token_auth_user(
28-
user: Annotated[User, Depends(from_form_data)],
29-
) -> UserToken:
32+
async def get_user_auth_token(
33+
form_data: Annotated[
34+
OAuth2PasswordRequestForm,
35+
Depends(),
36+
],
37+
) -> GetAuthUserTokenResponse:
3038
"""Authenticate user.
3139
3240
JSON Web Token (JWT) authentication is a popular method for securing web applications and APIs.
@@ -35,7 +43,11 @@ async def token_auth_user(
3543
3644
Use a token in a response to get secured stored data of your user.
3745
"""
38-
return UserToken.from_user(user)
46+
service: GetAuthUserTokenService = GetAuthUserTokenService(
47+
username=form_data.username,
48+
password=SecretStr(form_data.password),
49+
)
50+
return await service()
3951

4052

4153
@router.post(

futuramaapi/routers/rest/tokens/dependencies.py

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,13 @@
11
from typing import Annotated
22

33
from fastapi import Depends, HTTPException, status
4-
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
5-
from sqlalchemy.ext.asyncio.session import AsyncSession
6-
7-
from futuramaapi.repositories.session import get_async_session
8-
from futuramaapi.routers.exceptions import ModelNotFoundError
9-
from futuramaapi.routers.rest.users.schemas import User, UserPasswordError
4+
from fastapi.security import OAuth2PasswordBearer
105

116
from .schemas import DecodedTokenError, UserToken, UserTokenRefreshRequest
127

138
_oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/tokens/users/auth")
149

1510

16-
async def from_form_data(
17-
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
18-
session: AsyncSession = Depends(get_async_session), # noqa: B008
19-
) -> User:
20-
try:
21-
user: User = await User.auth(session, form_data.username, form_data.password)
22-
except (ModelNotFoundError, UserPasswordError):
23-
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) from None
24-
25-
return user
26-
27-
2811
def refresh_token(
2912
token: Annotated[str, Depends(_oauth2_scheme)],
3013
data: UserTokenRefreshRequest,

futuramaapi/routers/services/tokens/__init__.py

Whitespace-only changes.
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import uuid
2+
from datetime import UTC, datetime, timedelta
3+
from typing import Any, ClassVar, Self
4+
5+
import jwt
6+
from fastapi import HTTPException, status
7+
from pydantic import Field, SecretStr
8+
from sqlalchemy import Result, Select, select
9+
from sqlalchemy.exc import NoResultFound
10+
11+
from futuramaapi.core import settings
12+
from futuramaapi.helpers.pydantic import BaseModel
13+
from futuramaapi.repositories.models import UserModel
14+
from futuramaapi.routers.services import BaseSessionService
15+
16+
17+
class GetAuthUserTokenResponse(BaseModel):
18+
access_token: str = Field(
19+
alias="access_token",
20+
description="Keep in mind, that the field is not in a camel case. That's the standard.",
21+
)
22+
refresh_token: str = Field(
23+
alias="refresh_token",
24+
description="Keep in mind, that the field is not in a camel case. That's the standard.",
25+
)
26+
27+
_default_access_seconds: ClassVar[int] = 15 * 60
28+
_default_refresh_seconds: ClassVar[int] = 5 * 24 * 60 * 60
29+
30+
@classmethod
31+
def from_user_model(
32+
cls,
33+
user: UserModel,
34+
/,
35+
*,
36+
algorithm="HS256",
37+
) -> Self:
38+
return cls(
39+
access_token=jwt.encode(
40+
{
41+
"exp": datetime.now(UTC) + timedelta(seconds=cls._default_access_seconds),
42+
"nonce": uuid.uuid4().hex,
43+
"type": "access",
44+
"user": {
45+
"id": user.id,
46+
},
47+
},
48+
settings.secret_key.get_secret_value(),
49+
algorithm=algorithm,
50+
),
51+
refresh_token=jwt.encode(
52+
{
53+
"exp": datetime.now(UTC) + timedelta(seconds=cls._default_refresh_seconds),
54+
"nonce": uuid.uuid4().hex,
55+
"type": "refresh",
56+
"user": {
57+
"id": user.id,
58+
},
59+
},
60+
settings.secret_key.get_secret_value(),
61+
algorithm=algorithm,
62+
),
63+
)
64+
65+
66+
class GetAuthUserTokenService(BaseSessionService[GetAuthUserTokenResponse]):
67+
username: str
68+
password: SecretStr
69+
70+
@property
71+
def __user_statement(self) -> Select[tuple[UserModel]]:
72+
return select(UserModel).where(UserModel.username == self.username)
73+
74+
async def _get_user(self) -> UserModel:
75+
result: Result[tuple[Any]] = await self.session.execute(self.__user_statement)
76+
try:
77+
return result.scalars().one()
78+
except NoResultFound:
79+
raise HTTPException(
80+
status_code=status.HTTP_401_UNAUTHORIZED,
81+
) from None
82+
83+
async def process(self, *args, **kwargs) -> GetAuthUserTokenResponse:
84+
user: UserModel = await self._get_user()
85+
if not self.hasher.verify(self.password.get_secret_value(), user.password):
86+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
87+
88+
return GetAuthUserTokenResponse.from_user_model(user)

0 commit comments

Comments
 (0)