Skip to content

Commit f6a64c6

Browse files
committed
Extract 401 error from service layer
1 parent d362a1d commit f6a64c6

File tree

6 files changed

+58
-27
lines changed

6 files changed

+58
-27
lines changed

futuramaapi/apps/fastapi.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import mimetypes
22
from collections.abc import AsyncGenerator
33
from contextlib import asynccontextmanager
4-
from typing import TYPE_CHECKING, Any, ClassVar, Literal, Self
4+
from typing import TYPE_CHECKING, Any, ClassVar, Literal, NamedTuple, Self
55

66
import sentry_sdk
7-
from fastapi import FastAPI
7+
from fastapi import FastAPI, Request, Response, status
8+
from fastapi.responses import JSONResponse
89
from fastapi.staticfiles import StaticFiles
910
from fastapi_pagination import add_pagination
1011
from starlette.routing import Host, Mount, Route, WebSocketRoute
@@ -23,6 +24,11 @@
2324
mimetypes.add_type("image/webp", ".webp")
2425

2526

27+
class _ExceptionValue(NamedTuple):
28+
status_code: int
29+
default_message: str
30+
31+
2632
class FuturamaAPI(FastAPI):
2733
BOTS_FORBIDDEN_URLS: ClassVar[tuple[str, ...]] = (
2834
"/favicon.ico",
@@ -92,12 +98,36 @@ def _setup_static(self) -> None:
9298
name="static",
9399
)
94100

101+
def _exception_handler(self, _: Request, exc) -> Response:
102+
from futuramaapi.routers.services import ServiceError, UnauthorizedError # noqa: PLC0415
103+
104+
exception_to_value: dict[type[ServiceError], _ExceptionValue] = {
105+
UnauthorizedError: _ExceptionValue(
106+
status_code=status.HTTP_401_UNAUTHORIZED,
107+
default_message="Unauthorized",
108+
),
109+
}
110+
111+
exc_value = exception_to_value[type(exc)]
112+
return JSONResponse(
113+
status_code=exc_value.status_code,
114+
content={
115+
"detail": str(exc) or exc_value.default_message,
116+
},
117+
)
118+
119+
def _setup_exceptions(self) -> None:
120+
from futuramaapi.routers.services import ServiceError # noqa: PLC0415
121+
122+
self.add_exception_handler(ServiceError, self._exception_handler)
123+
95124
def setup(self) -> None:
96125
super().setup()
97126

98127
self._setup_middlewares()
99128
self._setup_routers()
100129
self._setup_static()
130+
self._setup_exceptions()
101131

102132
add_pagination(self)
103133

futuramaapi/routers/services/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
BaseService,
33
BaseSessionService,
44
BaseUserAuthenticatedService,
5+
ServiceError,
6+
UnauthorizedError,
57
)
68
from ._base_template import BaseTemplateService
79

@@ -10,4 +12,6 @@
1012
"BaseSessionService",
1113
"BaseTemplateService",
1214
"BaseUserAuthenticatedService",
15+
"ServiceError",
16+
"UnauthorizedError",
1317
]

futuramaapi/routers/services/_base.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from typing import Any, TypeVar
44

55
import jwt
6-
from fastapi import HTTPException, status
76
from fastapi_pagination import Page
87
from jwt import ExpiredSignatureError, InvalidSignatureError, InvalidTokenError
98
from sqlalchemy import Select, select
@@ -21,6 +20,14 @@
2120
)
2221

2322

23+
class ServiceError(Exception):
24+
"""Service Error."""
25+
26+
27+
class UnauthorizedError(ServiceError):
28+
"""Unauthorized Error."""
29+
30+
2431
class BaseService[TResponse](BaseModel, ABC):
2532
context: dict[str, Any] | None = None
2633

@@ -79,10 +86,10 @@ def __get_decoded_token(
7986
algorithms=[algorithm],
8087
)
8188
except (ExpiredSignatureError, InvalidSignatureError, InvalidTokenError):
82-
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) from None
89+
raise UnauthorizedError() from None
8390

8491
if decoded_token["type"] != "access":
85-
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) from None
92+
raise UnauthorizedError() from None
8693

8794
return decoded_token
8895

@@ -95,7 +102,7 @@ async def __set_user(self) -> None:
95102
try:
96103
self._user = (await self.session.execute(self.__get_user_statement)).scalars().one()
97104
except NoResultFound:
98-
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) from None
105+
raise UnauthorizedError() from None
99106

100107
async def __call__(self, *args, **kwargs) -> TResponse:
101108
async with session_manager.session() as session:

futuramaapi/routers/services/tokens/get_auth_user_token.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,14 @@
33
from typing import Any, ClassVar, Self
44

55
import jwt
6-
from fastapi import HTTPException, status
76
from pydantic import Field, SecretStr
87
from sqlalchemy import Result, Select, select
98
from sqlalchemy.exc import NoResultFound
109

1110
from futuramaapi.core import settings
1211
from futuramaapi.helpers.pydantic import BaseModel
1312
from futuramaapi.repositories.models import UserModel
14-
from futuramaapi.routers.services import BaseSessionService
13+
from futuramaapi.routers.services import BaseSessionService, UnauthorizedError
1514

1615

1716
class GetAuthUserTokenResponse(BaseModel):
@@ -76,13 +75,11 @@ async def _get_user(self) -> UserModel:
7675
try:
7776
return result.scalars().one()
7877
except NoResultFound:
79-
raise HTTPException(
80-
status_code=status.HTTP_401_UNAUTHORIZED,
81-
) from None
78+
raise UnauthorizedError() from None
8279

8380
async def process(self, *args, **kwargs) -> GetAuthUserTokenResponse:
8481
user: UserModel = await self._get_user()
8582
if not self.hasher.verify(self.password.get_secret_value(), user.password):
86-
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
83+
raise UnauthorizedError()
8784

8885
return GetAuthUserTokenResponse.from_user_model(user)

futuramaapi/routers/services/tokens/get_refreshed_auth_user_token.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from typing import Any, ClassVar
22

33
import jwt
4-
from fastapi import HTTPException, status
54
from jwt import ExpiredSignatureError, InvalidSignatureError, InvalidTokenError
65
from pydantic import Field
76
from sqlalchemy import Result, Select, select
@@ -10,7 +9,7 @@
109
from futuramaapi.core import settings
1110
from futuramaapi.helpers.pydantic import BaseModel
1211
from futuramaapi.repositories.models import UserModel
13-
from futuramaapi.routers.services import BaseSessionService
12+
from futuramaapi.routers.services import BaseSessionService, UnauthorizedError
1413

1514
from .get_auth_user_token import GetAuthUserTokenResponse
1615

@@ -38,7 +37,7 @@ async def _get_user(self, decoded_token: dict[str, Any], /) -> UserModel:
3837
try:
3938
user: UserModel = result.scalars().one()
4039
except NoResultFound:
41-
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) from None
40+
raise UnauthorizedError() from None
4241

4342
return user
4443

@@ -50,10 +49,10 @@ async def process(self, *args, **kwargs) -> GetRefreshedAuthUserTokenResponse:
5049
algorithms=[self.algorithm],
5150
)
5251
except (ExpiredSignatureError, InvalidSignatureError, InvalidTokenError):
53-
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) from None
52+
raise UnauthorizedError() from None
5453

5554
if decoded_token["type"] != "refresh":
56-
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
55+
raise UnauthorizedError()
5756

5857
user: UserModel = await self._get_user(decoded_token)
5958

futuramaapi/routers/services/users/activate_signature_user.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
from typing import Any, ClassVar
22

33
import jwt
4-
from fastapi import HTTPException, Request, status
4+
from fastapi import Request, status
55
from jwt import ExpiredSignatureError, InvalidSignatureError, InvalidTokenError
66
from sqlalchemy import Result, Select, Update, select, update
77
from starlette.responses import RedirectResponse
88

99
from futuramaapi.core import settings
1010
from futuramaapi.repositories.models import UserModel
11-
from futuramaapi.routers.services import BaseSessionService
11+
from futuramaapi.routers.services import BaseSessionService, UnauthorizedError
1212

1313

1414
class TokenDecodeError(Exception):
@@ -59,18 +59,12 @@ async def process(self, *args, **kwargs) -> RedirectResponse:
5959
try:
6060
token_: dict[str, Any] = self._get_decoded_token()
6161
except TokenDecodeError:
62-
raise HTTPException(
63-
status_code=status.HTTP_401_UNAUTHORIZED,
64-
detail="Token expired or invalid.",
65-
) from None
62+
raise UnauthorizedError("Token expired or invalid.") from None
6663

6764
user: UserModel = await self._get_current_user(token_["user"]["id"])
6865

6966
if user.is_confirmed:
70-
raise HTTPException(
71-
status_code=status.HTTP_401_UNAUTHORIZED,
72-
detail="User already activated.",
73-
)
67+
raise UnauthorizedError("User already activated.")
7468

7569
await self.session.execute(self.__get_update_user_statement(user.id))
7670
await self.session.commit()

0 commit comments

Comments
 (0)