plain.oauth 0.19.1__py3-none-any.whl → 0.21.0__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.
plain/oauth/README.md CHANGED
@@ -89,10 +89,12 @@ class ExampleOAuthProvider(OAuthProvider):
89
89
  data = response.json()
90
90
  return OAuthUser(
91
91
  # The provider ID is required
92
- id=data["id"],
93
- # And you can populate any of your User model fields with additional kwargs
94
- email=data["email"],
95
- username=data["username"],
92
+ provider_id=data["id"],
93
+ # Populate your User model fields using the user_model_fields dict
94
+ user_model_fields={
95
+ "email": data["email"],
96
+ "username": data["username"],
97
+ },
96
98
  )
97
99
  ```
98
100
 
plain/oauth/exceptions.py CHANGED
@@ -1,12 +1,16 @@
1
1
  class OAuthError(Exception):
2
2
  """Base class for OAuth errors"""
3
3
 
4
- pass
4
+ message = "An error occurred during the OAuth process."
5
+
6
+
7
+ class OAuthStateMissingError(OAuthError):
8
+ message = "The state parameter is missing. Please try again."
5
9
 
6
10
 
7
11
  class OAuthStateMismatchError(OAuthError):
8
- pass
12
+ message = "The state parameter did not match. Please try again."
9
13
 
10
14
 
11
15
  class OAuthUserAlreadyExistsError(OAuthError):
12
- pass
16
+ message = "A user already exists with this email address. Please log in first and then connect this OAuth provider to the existing account."
plain/oauth/models.py CHANGED
@@ -76,7 +76,7 @@ class OAuthConnection(models.Model):
76
76
  self.refresh_token_expires_at = oauth_token.refresh_token_expires_at
77
77
 
78
78
  def set_user_fields(self, oauth_user: "OAuthUser"):
79
- self.provider_user_id = oauth_user.id
79
+ self.provider_user_id = oauth_user.provider_id
80
80
 
81
81
  def access_token_expired(self) -> bool:
82
82
  return (
@@ -97,7 +97,7 @@ class OAuthConnection(models.Model):
97
97
  try:
98
98
  connection = cls.objects.get(
99
99
  provider_key=provider_key,
100
- provider_user_id=oauth_user.id,
100
+ provider_user_id=oauth_user.provider_id,
101
101
  )
102
102
  connection.set_token_fields(oauth_token)
103
103
  connection.save()
@@ -137,7 +137,7 @@ class OAuthConnection(models.Model):
137
137
  connection = cls.objects.get(
138
138
  user=user,
139
139
  provider_key=provider_key,
140
- provider_user_id=oauth_user.id,
140
+ provider_user_id=oauth_user.provider_id,
141
141
  )
142
142
  except cls.DoesNotExist:
143
143
  # Create our own instance (not using get_or_create)
@@ -145,7 +145,7 @@ class OAuthConnection(models.Model):
145
145
  connection = cls(
146
146
  user=user,
147
147
  provider_key=provider_key,
148
- provider_user_id=oauth_user.id,
148
+ provider_user_id=oauth_user.provider_id,
149
149
  )
150
150
 
151
151
  connection.set_user_fields(oauth_user)
plain/oauth/providers.py CHANGED
@@ -7,10 +7,11 @@ from plain.auth import login as auth_login
7
7
  from plain.http import HttpRequest, Response, ResponseRedirect
8
8
  from plain.runtime import settings
9
9
  from plain.urls import reverse
10
+ from plain.utils.cache import add_never_cache_headers
10
11
  from plain.utils.crypto import get_random_string
11
12
  from plain.utils.module_loading import import_string
12
13
 
13
- from .exceptions import OAuthError, OAuthStateMismatchError
14
+ from .exceptions import OAuthError, OAuthStateMismatchError, OAuthStateMissingError
14
15
  from .models import OAuthConnection
15
16
 
16
17
  SESSION_STATE_KEY = "plainoauth_state"
@@ -23,8 +24,8 @@ class OAuthToken:
23
24
  *,
24
25
  access_token: str,
25
26
  refresh_token: str = "",
26
- access_token_expires_at: datetime.datetime = None,
27
- refresh_token_expires_at: datetime.datetime = None,
27
+ access_token_expires_at: datetime.datetime | None = None,
28
+ refresh_token_expires_at: datetime.datetime | None = None,
28
29
  ):
29
30
  self.access_token = access_token
30
31
  self.refresh_token = refresh_token
@@ -33,16 +34,16 @@ class OAuthToken:
33
34
 
34
35
 
35
36
  class OAuthUser:
36
- def __init__(self, *, id: str, **user_model_fields: dict):
37
- self.id = id # ID on the provider's system
38
- self.user_model_fields = user_model_fields
37
+ def __init__(self, *, provider_id: str, user_model_fields: dict | None = None):
38
+ self.provider_id = provider_id # ID on the provider's system
39
+ self.user_model_fields = user_model_fields or {}
39
40
 
40
41
  def __str__(self):
41
42
  if "email" in self.user_model_fields:
42
43
  return self.user_model_fields["email"]
43
44
  if "username" in self.user_model_fields:
44
45
  return self.user_model_fields["username"]
45
- return str(self.id)
46
+ return str(self.provider_id)
46
47
 
47
48
 
48
49
  class OAuthProvider:
@@ -105,7 +106,11 @@ class OAuthProvider:
105
106
  if error := request.query_params.get("error"):
106
107
  raise OAuthError(error)
107
108
 
108
- state = request.query_params["state"]
109
+ try:
110
+ state = request.query_params["state"]
111
+ except KeyError as e:
112
+ raise OAuthStateMissingError() from e
113
+
109
114
  expected_state = request.session.pop(SESSION_STATE_KEY)
110
115
  request.session.save() # Make sure the pop is saved (won't save on an exception)
111
116
  if not secrets.compare_digest(state, expected_state):
@@ -130,7 +135,7 @@ class OAuthProvider:
130
135
  # Sort authorization params for consistency
131
136
  sorted_authorization_params = sorted(authorization_params.items())
132
137
  redirect_url = authorization_url + "?" + urlencode(sorted_authorization_params)
133
- return ResponseRedirect(redirect_url)
138
+ return self.get_redirect_response(redirect_url)
134
139
 
135
140
  def handle_connect_request(
136
141
  self, *, request: HttpRequest, redirect_to: str = ""
@@ -144,7 +149,7 @@ class OAuthProvider:
144
149
  )
145
150
  connection.delete()
146
151
  redirect_url = self.get_disconnect_redirect_url(request=request)
147
- return ResponseRedirect(redirect_url)
152
+ return self.get_redirect_response(redirect_url)
148
153
 
149
154
  def handle_callback_request(self, *, request: HttpRequest) -> Response:
150
155
  self.check_request_state(request=request)
@@ -174,9 +179,9 @@ class OAuthProvider:
174
179
  self.login(request=request, user=user)
175
180
 
176
181
  redirect_url = self.get_login_redirect_url(request=request)
177
- return ResponseRedirect(redirect_url)
182
+ return self.get_redirect_response(redirect_url)
178
183
 
179
- def login(self, *, request: HttpRequest, user: Any) -> Response:
184
+ def login(self, *, request: HttpRequest, user: Any) -> None:
180
185
  auth_login(request=request, user=user)
181
186
 
182
187
  def get_login_redirect_url(self, *, request: HttpRequest) -> str:
@@ -185,6 +190,15 @@ class OAuthProvider:
185
190
  def get_disconnect_redirect_url(self, *, request: HttpRequest) -> str:
186
191
  return request.data.get("next", "/")
187
192
 
193
+ def get_redirect_response(self, redirect_url: str) -> Response:
194
+ """
195
+ Returns a redirect response to the given URL.
196
+ This is a utility method to ensure consistent redirect handling.
197
+ """
198
+ response = ResponseRedirect(redirect_url)
199
+ add_never_cache_headers(response)
200
+ return response
201
+
188
202
 
189
203
  def get_oauth_provider_instance(*, provider_key: str) -> OAuthProvider:
190
204
  OAUTH_LOGIN_PROVIDERS = getattr(settings, "OAUTH_LOGIN_PROVIDERS", {})
@@ -0,0 +1,8 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block content %}
4
+ <div class="text-center">
5
+ <h1 class="text-lg">OAuth Error</h1>
6
+ <p>{{ oauth_error.message }}</p>
7
+ </div>
8
+ {% endblock %}
plain/oauth/views.py CHANGED
@@ -1,14 +1,11 @@
1
1
  import logging
2
2
 
3
3
  from plain.auth.views import AuthViewMixin
4
- from plain.http import ResponseBadRequest, ResponseRedirect
5
- from plain.templates import Template
6
- from plain.views import View
4
+ from plain.http import ResponseRedirect
5
+ from plain.views import TemplateView, View
7
6
 
8
7
  from .exceptions import (
9
8
  OAuthError,
10
- OAuthStateMismatchError,
11
- OAuthUserAlreadyExistsError,
12
9
  )
13
10
  from .providers import get_oauth_provider_instance
14
11
 
@@ -26,39 +23,30 @@ class OAuthLoginView(View):
26
23
  return provider_instance.handle_login_request(request=request)
27
24
 
28
25
 
29
- class OAuthCallbackView(View):
26
+ class OAuthCallbackView(TemplateView):
30
27
  """
31
28
  The callback view is used for signup, login, and connect.
32
29
  """
33
30
 
31
+ template_name = "oauth/callback.html"
32
+
34
33
  def get(self):
35
- request = self.request
36
34
  provider = self.url_kwargs["provider"]
37
35
  provider_instance = get_oauth_provider_instance(provider_key=provider)
38
36
  try:
39
- return provider_instance.handle_callback_request(request=request)
40
- except OAuthUserAlreadyExistsError:
41
- template = Template("oauth/error.html")
42
- return ResponseBadRequest(
43
- template.render(
44
- {
45
- "oauth_error": "A user already exists with this email address. Please log in first and then connect this OAuth provider to the existing account."
46
- }
47
- )
48
- )
49
- except OAuthStateMismatchError:
50
- template = Template("oauth/error.html")
51
- return ResponseBadRequest(
52
- template.render(
53
- {
54
- "oauth_error": "The state parameter did not match. Please try again."
55
- }
56
- )
57
- )
37
+ return provider_instance.handle_callback_request(request=self.request)
58
38
  except OAuthError as e:
59
39
  logger.exception("OAuth error")
60
- template = Template("oauth/error.html")
61
- return ResponseBadRequest(template.render({"oauth_error": str(e)}))
40
+ self.oauth_error = e
41
+
42
+ response = super().get()
43
+ response.status_code = 400
44
+ return response
45
+
46
+ def get_template_context(self) -> dict:
47
+ context = super().get_template_context()
48
+ context["oauth_error"] = getattr(self, "oauth_error", None)
49
+ return context
62
50
 
63
51
 
64
52
  class OAuthConnectView(AuthViewMixin, View):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain.oauth
3
- Version: 0.19.1
3
+ Version: 0.21.0
4
4
  Summary: OAuth login and API access for Plain.
5
5
  Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
6
  License-Expression: BSD-3-Clause
@@ -103,10 +103,12 @@ class ExampleOAuthProvider(OAuthProvider):
103
103
  data = response.json()
104
104
  return OAuthUser(
105
105
  # The provider ID is required
106
- id=data["id"],
107
- # And you can populate any of your User model fields with additional kwargs
108
- email=data["email"],
109
- username=data["username"],
106
+ provider_id=data["id"],
107
+ # Populate your User model fields using the user_model_fields dict
108
+ user_model_fields={
109
+ "email": data["email"],
110
+ "username": data["username"],
111
+ },
110
112
  )
111
113
  ```
112
114
 
@@ -1,13 +1,13 @@
1
- plain/oauth/README.md,sha256=9_l9lOSndGBG31mD3oteT8UJMeonuCKdBreLQhUO70E,9900
1
+ plain/oauth/README.md,sha256=N8getAUUhLX6P4mg-5P0V41gk5EKBGb8v904OfP2Z9E,9961
2
2
  plain/oauth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  plain/oauth/admin.py,sha256=rqrGRRUVxOlG9wiuXtndIyQxd1VT67cvFy6qWo7LF-Q,1278
4
4
  plain/oauth/config.py,sha256=0Q4IILBKQbIaxqeL9WRTH5Cka-BO3c3SOj1AdQIAJgc,167
5
5
  plain/oauth/default_settings.py,sha256=dlN1J9vSOjjxPNLp-0qe-cLTqwM4E69ZAx_8lpxMhaM,28
6
- plain/oauth/exceptions.py,sha256=TMGtIGkK3_J4rsEy1oPCmia7BnRSK8N8RMZm4_pNelA,189
7
- plain/oauth/models.py,sha256=ceSw9AKZMXffE4ZKZLxIbOlSyndTCHL22GCuSF3Hpmk,6880
8
- plain/oauth/providers.py,sha256=M8r_XkKgyCz8YkLyFyNOt35EHThDvwggcDlUoC7ZYwo,7187
6
+ plain/oauth/exceptions.py,sha256=yoZsq8XgzstuwbE2ihoet0nzpw_sVZgDrwUauh6hhUs,546
7
+ plain/oauth/models.py,sha256=_DS8Mv-dn_a9EO63aNb8oV8jxXUgHkoytrMkBritVhQ,6916
8
+ plain/oauth/providers.py,sha256=YDftJUMyyfXgsDCkyTNxGwrbwXAowL0Hg6KrwrAN5S0,7793
9
9
  plain/oauth/urls.py,sha256=FYzpQwhvZdcat8n3f7RyA-1Q21finKb8JEyakSOjXXg,696
10
- plain/oauth/views.py,sha256=dVlFYGsEusD2lr24eVRLVfHfEEreg_1nByepiIOoWLk,3047
10
+ plain/oauth/views.py,sha256=J2NCa37YediBTi82CfRlmsb45hFT6gWN6zMaFHhsDMM,2410
11
11
  plain/oauth/migrations/0001_initial.py,sha256=B9Finbn7ijEIUbkDy_B7UsKQLfMWaXd0Kx3oZrUENWc,1753
12
12
  plain/oauth/migrations/0002_alter_oauthconnection_options_and_more.py,sha256=3Mb0IU9KDRQfog0PjVbzuNv_AxCs7UVHnA0F263AKNo,581
13
13
  plain/oauth/migrations/0003_alter_oauthconnection_access_token_and_more.py,sha256=FyLfwxc2pRzF-CbdRFQRRSQTOCxc9l1womgStygm_lo,629
@@ -16,8 +16,8 @@ plain/oauth/migrations/0005_alter_oauthconnection_unique_together_and_more.py,sh
16
16
  plain/oauth/migrations/0006_remove_oauthconnection_unique_oauth_provider_user_id_and_more.py,sha256=UjWRezZoAzjVS70oCRQ38k_w6YJtcd_uSYT4Kc3H1pg,713
17
17
  plain/oauth/migrations/0007_alter_oauthconnection_provider_key_and_more.py,sha256=B_LW6xG1o_uA13tqUs0KniXl1JBNbQu4wMh2pW8rq5I,675
18
18
  plain/oauth/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
- plain/oauth/templates/oauth/error.html,sha256=xkkWw57sZ3fz4dDfH30SVtq3okJNnJmrSRPfGxrxjh8,108
20
- plain_oauth-0.19.1.dist-info/METADATA,sha256=Iemik0OPAMnSfBFMTCdu6qmDNFQlrgrZhtJiAVmH994,10304
21
- plain_oauth-0.19.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
22
- plain_oauth-0.19.1.dist-info/licenses/LICENSE,sha256=cvKM3OlqHx3ijD6e34zsSUkPvzl-ya3Dd63A6EHL94U,1500
23
- plain_oauth-0.19.1.dist-info/RECORD,,
19
+ plain/oauth/templates/oauth/callback.html,sha256=4CJG0oAN0xYjw2IPkjaL7B4hwlf9um9LI4CTu50E-yE,173
20
+ plain_oauth-0.21.0.dist-info/METADATA,sha256=mdMZjbi7Tp69z0XeUxO2jDUEdLQpZcWqT7dseHafO0Y,10365
21
+ plain_oauth-0.21.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
22
+ plain_oauth-0.21.0.dist-info/licenses/LICENSE,sha256=cvKM3OlqHx3ijD6e34zsSUkPvzl-ya3Dd63A6EHL94U,1500
23
+ plain_oauth-0.21.0.dist-info/RECORD,,
@@ -1,6 +0,0 @@
1
- {% extends "base.html" %}
2
-
3
- {% block content %}
4
- <h1>OAuth Error</h1>
5
- <p>{{ oauth_error }}</p>
6
- {% endblock %}