Coverage for /Users/davegaeddert/Developer/dropseed/plain/plain-auth/plain/auth/views.py: 87%

52 statements  

« prev     ^ index     » next       coverage.py v7.6.9, created at 2024-12-23 11:16 -0600

1from urllib.parse import urlparse, urlunparse 

2 

3from plain.exceptions import PermissionDenied 

4from plain.http import ( 

5 Http404, 

6 QueryDict, 

7 Response, 

8 ResponseRedirect, 

9) 

10from plain.runtime import settings 

11from plain.urls import reverse 

12from plain.views import View 

13 

14from .sessions import logout 

15from .utils import resolve_url 

16 

17 

18class LoginRequired(Exception): 

19 def __init__(self, login_url=None, redirect_field_name="next"): 

20 self.login_url = login_url or settings.AUTH_LOGIN_URL 

21 self.redirect_field_name = redirect_field_name 

22 

23 

24class AuthViewMixin: 

25 login_required = True 

26 staff_required = False 

27 login_url = None 

28 

29 def check_auth(self) -> None: 

30 """ 

31 Raises either LoginRequired or PermissionDenied. 

32 - LoginRequired can specify a login_url and redirect_field_name 

33 - PermissionDenied can specify a message 

34 """ 

35 

36 if not hasattr(self, "request"): 

37 raise AttributeError( 

38 "AuthViewMixin requires the request attribute to be set." 

39 ) 

40 

41 if self.login_required and not self.request.user: 

42 raise LoginRequired(login_url=self.login_url) 

43 

44 if impersonator := getattr(self.request, "impersonator", None): 

45 # Impersonators should be able to view staff pages while impersonating. 

46 # There's probably never a case where an impersonator isn't staff, but it can be configured. 

47 if self.staff_required and not impersonator.is_staff: 

48 raise PermissionDenied( 

49 "You do not have permission to access this page." 

50 ) 

51 elif self.staff_required and not self.request.user.is_staff: 

52 # Show a 404 so we don't expose staff urls to non-staff users 

53 raise Http404() 

54 

55 def get_response(self) -> Response: 

56 if not hasattr(self, "request"): 

57 raise AttributeError( 

58 "AuthViewMixin requires the request attribute to be set." 

59 ) 

60 

61 try: 

62 self.check_auth() 

63 except LoginRequired as e: 

64 # Ideally this could be handled elsewhere... like PermissionDenied 

65 # also seems like this code is used multiple places anyway... 

66 # could be easier to get redirect query param 

67 path = self.request.build_absolute_uri() 

68 resolved_login_url = reverse(e.login_url) 

69 # If the login url is the same scheme and net location then use the 

70 # path as the "next" url. 

71 login_scheme, login_netloc = urlparse(resolved_login_url)[:2] 

72 current_scheme, current_netloc = urlparse(path)[:2] 

73 if (not login_scheme or login_scheme == current_scheme) and ( 

74 not login_netloc or login_netloc == current_netloc 

75 ): 

76 path = self.request.get_full_path() 

77 return redirect_to_login( 

78 path, 

79 resolved_login_url, 

80 e.redirect_field_name, 

81 ) 

82 

83 return super().get_response() # type: ignore 

84 

85 

86class LogoutView(View): 

87 def post(self): 

88 logout(self.request) 

89 return ResponseRedirect("/") 

90 

91 

92def redirect_to_login(next, login_url=None, redirect_field_name="next"): 

93 """ 

94 Redirect the user to the login page, passing the given 'next' page. 

95 """ 

96 resolved_url = resolve_url(login_url or settings.AUTH_LOGIN_URL) 

97 

98 login_url_parts = list(urlparse(resolved_url)) 

99 if redirect_field_name: 

100 querystring = QueryDict(login_url_parts[4], mutable=True) 

101 querystring[redirect_field_name] = next 

102 login_url_parts[4] = querystring.urlencode(safe="/") 

103 

104 return ResponseRedirect(urlunparse(login_url_parts))