-
Notifications
You must be signed in to change notification settings - Fork 306
/
serializers.py
288 lines (243 loc) · 10.8 KB
/
serializers.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
from allauth.socialaccount.providers.oauth2.client import OAuth2Error
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError as DjangoValidationError
from django.db import IntegrityError
from django.http import HttpRequest, HttpResponseBadRequest
from django.urls.exceptions import NoReverseMatch
from django.utils.translation import gettext_lazy as _
from requests.exceptions import HTTPError
from rest_framework import serializers
from rest_framework.reverse import reverse
try:
from allauth.account import app_settings as allauth_account_settings
from allauth.account.adapter import get_adapter
from allauth.account.utils import setup_user_email
from allauth.socialaccount.helpers import complete_social_login
from allauth.socialaccount.models import SocialAccount, EmailAddress
from allauth.socialaccount.providers.base import AuthProcess
from allauth.utils import get_username_max_length
except ImportError:
raise ImportError('allauth needs to be added to INSTALLED_APPS.')
class SocialAccountSerializer(serializers.ModelSerializer):
"""
serialize allauth SocialAccounts for use with a REST API
"""
class Meta:
model = SocialAccount
fields = (
'id',
'provider',
'uid',
'last_login',
'date_joined',
)
class SocialLoginSerializer(serializers.Serializer):
access_token = serializers.CharField(required=False, allow_blank=True)
code = serializers.CharField(required=False, allow_blank=True)
id_token = serializers.CharField(required=False, allow_blank=True)
def _get_request(self):
request = self.context.get('request')
if not isinstance(request, HttpRequest):
request = request._request
return request
def get_social_login(self, adapter, app, token, response):
"""
:param adapter: allauth.socialaccount Adapter subclass.
Usually OAuthAdapter or Auth2Adapter
:param app: `allauth.socialaccount.SocialApp` instance
:param token: `allauth.socialaccount.SocialToken` instance
:param response: Provider's response for OAuth1. Not used in the
:returns: A populated instance of the
`allauth.socialaccount.SocialLoginView` instance
"""
request = self._get_request()
social_login = adapter.complete_login(request, app, token, response=response)
social_login.token = token
return social_login
def set_callback_url(self, view, adapter_class):
# first set url from view
self.callback_url = getattr(view, 'callback_url', None)
if not self.callback_url:
# auto generate base on adapter and request
try:
self.callback_url = reverse(
viewname=adapter_class.provider_id + '_callback',
request=self._get_request(),
)
except NoReverseMatch:
raise serializers.ValidationError(
_('Define callback_url in view'),
)
def validate(self, attrs):
view = self.context.get('view')
request = self._get_request()
if not view:
raise serializers.ValidationError(
_('View is not defined, pass it as a context variable'),
)
adapter_class = getattr(view, 'adapter_class', None)
if not adapter_class:
raise serializers.ValidationError(_('Define adapter_class in view'))
adapter = adapter_class(request)
app = adapter.get_provider().app
# More info on code vs access_token
# http://stackoverflow.com/questions/8666316/facebook-oauth-2-0-code-and-token
access_token = attrs.get('access_token')
code = attrs.get('code')
# Case 1: We received the access_token
if access_token:
tokens_to_parse = {'access_token': access_token}
token = access_token
# For sign in with apple
id_token = attrs.get('id_token')
if id_token:
tokens_to_parse['id_token'] = id_token
# Case 2: We received the authorization code
elif code:
self.set_callback_url(view=view, adapter_class=adapter_class)
self.client_class = getattr(view, 'client_class', None)
if not self.client_class:
raise serializers.ValidationError(
_('Define client_class in view'),
)
provider = adapter.get_provider()
scope = provider.get_scope(request)
client = self.client_class(
request,
app.client_id,
app.secret,
adapter.access_token_method,
adapter.access_token_url,
self.callback_url,
scope,
scope_delimiter=adapter.scope_delimiter,
headers=adapter.headers,
basic_auth=adapter.basic_auth,
)
try:
token = client.get_access_token(code)
except OAuth2Error as ex:
raise serializers.ValidationError(
_('Failed to exchange code for access token')
) from ex
access_token = token['access_token']
tokens_to_parse = {'access_token': access_token}
# If available we add additional data to the dictionary
for key in ['refresh_token', 'id_token', adapter.expires_in_key]:
if key in token:
tokens_to_parse[key] = token[key]
else:
raise serializers.ValidationError(
_('Incorrect input. access_token or code is required.'),
)
social_token = adapter.parse_token(tokens_to_parse)
social_token.app = app
try:
if adapter.provider_id == 'google' and not code:
login = self.get_social_login(adapter, app, social_token, response={'id_token': id_token})
else:
login = self.get_social_login(adapter, app, social_token, token)
ret = complete_social_login(request, login)
except HTTPError:
raise serializers.ValidationError(_('Incorrect value'))
if isinstance(ret, HttpResponseBadRequest):
raise serializers.ValidationError(ret.content)
if not login.is_existing:
# We have an account already signed up in a different flow
# with the same email address: raise an exception.
# This needs to be handled in the frontend. We can not just
# link up the accounts due to security constraints
if allauth_account_settings.UNIQUE_EMAIL:
# Do we have an account already with this email address?
account_exists = get_user_model().objects.filter(
email=login.user.email,
).exists()
if account_exists:
raise serializers.ValidationError(
_('User is already registered with this e-mail address.'),
)
login.lookup()
try:
login.save(request, connect=True)
except IntegrityError as ex:
raise serializers.ValidationError(
_('User is already registered with this e-mail address.'),
) from ex
self.post_signup(login, attrs)
attrs['user'] = login.account.user
return attrs
def post_signup(self, login, attrs):
"""
Inject behavior when the user signs up with a social account.
:param login: The social login instance being registered.
:type login: allauth.socialaccount.models.SocialLogin
:param attrs: The attributes of the serializer.
:type attrs: dict
"""
pass
class SocialConnectMixin:
def get_social_login(self, *args, **kwargs):
"""
Set the social login process state to connect rather than login
Refer to the implementation of get_social_login in base class and to the
allauth.socialaccount.helpers module complete_social_login function.
"""
social_login = super().get_social_login(*args, **kwargs)
social_login.state['process'] = AuthProcess.CONNECT
return social_login
class SocialConnectSerializer(SocialConnectMixin, SocialLoginSerializer):
pass
class RegisterSerializer(serializers.Serializer):
username = serializers.CharField(
max_length=get_username_max_length(),
min_length=allauth_account_settings.USERNAME_MIN_LENGTH,
required=allauth_account_settings.USERNAME_REQUIRED,
)
email = serializers.EmailField(required=allauth_account_settings.EMAIL_REQUIRED)
password1 = serializers.CharField(write_only=True)
password2 = serializers.CharField(write_only=True)
def validate_username(self, username):
username = get_adapter().clean_username(username)
return username
def validate_email(self, email):
email = get_adapter().clean_email(email)
if allauth_account_settings.UNIQUE_EMAIL:
if email and EmailAddress.objects.is_verified(email):
raise serializers.ValidationError(
_('A user is already registered with this e-mail address.'),
)
return email
def validate_password1(self, password):
return get_adapter().clean_password(password)
def validate(self, data):
if data['password1'] != data['password2']:
raise serializers.ValidationError(_("The two password fields didn't match."))
return data
def custom_signup(self, request, user):
pass
def get_cleaned_data(self):
return {
'username': self.validated_data.get('username', ''),
'password1': self.validated_data.get('password1', ''),
'email': self.validated_data.get('email', ''),
}
def save(self, request):
adapter = get_adapter()
user = adapter.new_user(request)
self.cleaned_data = self.get_cleaned_data()
user = adapter.save_user(request, user, self, commit=False)
if "password1" in self.cleaned_data:
try:
adapter.clean_password(self.cleaned_data['password1'], user=user)
except DjangoValidationError as exc:
raise serializers.ValidationError(
detail=serializers.as_serializer_error(exc)
)
user.save()
self.custom_signup(request, user)
setup_user_email(request, user, [])
return user
class VerifyEmailSerializer(serializers.Serializer):
key = serializers.CharField(write_only=True)
class ResendEmailVerificationSerializer(serializers.Serializer):
email = serializers.EmailField(required=allauth_account_settings.EMAIL_REQUIRED)