Coverage for src/lexigram/auth/web/middleware/response_handler.py: 34%

29 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-25 12:26 +0800

1from __future__ import annotations 

2 

3from typing import Any 

4 

5from lexigram.auth import constants as const 

6 

7 

8class AuthResponseHandler: 

9 """Handles authentication-related HTTP responses.""" 

10 

11 @staticmethod 

12 async def unauthorized_response( 

13 message: str = "Authentication required", 

14 request: Any = None, 

15 response_factory: Any | None = None, 

16 ) -> Any: 

17 """Return 401 Unauthorized response or redirect to login.""" 

18 rf = response_factory or await _get_response_factory(request) 

19 

20 if request and rf: 

21 # Check if client accepts HTML 

22 accept = request.headers.get("Accept", "") 

23 if "text/html" in accept: 

24 login_url = getattr(request, "login_url", "/login") 

25 return rf.redirect( 

26 f"{login_url}?next={request.url.path}", 

27 status_code=302, 

28 ) 

29 

30 if rf: 

31 return rf.json( 

32 status_code=401, 

33 content={"error": "unauthorized", "message": message}, 

34 headers={"WWW-Authenticate": f'{const.DEFAULT_TOKEN_TYPE} realm="api"'}, 

35 ) 

36 

37 raise RuntimeError( 

38 "ResponseFactoryProtocol not available for unauthorized_response — ensure DI container is configured", 

39 ) 

40 

41 @staticmethod 

42 async def forbidden_response( 

43 message: str = "Insufficient permissions", 

44 request: Any | None = None, 

45 response_factory: Any | None = None, 

46 ) -> Any: 

47 """Return 403 Forbidden response.""" 

48 rf = response_factory or await _get_response_factory(request) 

49 if rf: 

50 return rf.json( 

51 status_code=403, 

52 content={"error": "forbidden", "message": message}, 

53 ) 

54 

55 raise RuntimeError( 

56 "ResponseFactoryProtocol not available for forbidden_response — ensure DI container is configured", 

57 ) 

58 

59 

60async def _get_response_factory(context: Any | None = None) -> Any: 

61 """Resolve `ResponseFactoryProtocol` from context or global container.""" 

62 from lexigram.contracts.web import ResponseFactoryProtocol 

63 from lexigram.di.resolution.context import get_resolver 

64 

65 resolver = get_resolver(context) 

66 if not resolver: 

67 return None 

68 

69 return await resolver.resolve_optional(ResponseFactoryProtocol) 

70 

71 

72__all__ = [ 

73 "AuthResponseHandler", 

74]