Skip to content

Commit 5413b85

Browse files
[ENG-9128] [Post-Release] P57 - [Contributors] Unregistered user’s “Set Password” link opens a blank page (#11367)
* Add claim user api endpoint * Update ConfirmClaimUser endpoint to use 'guid' instead of 'pid' in payload and URL
1 parent 05484eb commit 5413b85

File tree

4 files changed

+178
-0
lines changed

4 files changed

+178
-0
lines changed

api/users/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
re_path(r'^(?P<user_id>\w+)/addons/(?P<provider>\w+)/accounts/$', views.UserAddonAccountList.as_view(), name=views.UserAddonAccountList.view_name),
1515
re_path(r'^(?P<user_id>\w+)/addons/(?P<provider>\w+)/accounts/(?P<account_id>\w+)/$', views.UserAddonAccountDetail.as_view(), name=views.UserAddonAccountDetail.view_name),
1616
re_path(r'^(?P<user_id>\w+)/claim/$', views.ClaimUser.as_view(), name=views.ClaimUser.view_name),
17+
re_path(r'^(?P<user_id>\w+)/confirm_claim/$', views.ConfirmClaimUser.as_view(), name=views.ConfirmClaimUser.view_name),
1718
re_path(r'^(?P<user_id>\w+)/confirm/$', views.ConfirmEmailView.as_view(), name=views.ConfirmEmailView.view_name),
1819
re_path(r'^(?P<user_id>\w+)/sanction_response/$', views.SanctionResponseView.as_view(), name=views.SanctionResponseView.view_name),
1920
re_path(r'^(?P<user_id>\w+)/draft_registrations/$', views.UserDraftRegistrations.as_view(), name=views.UserDraftRegistrations.view_name),

api/users/views.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,16 @@
100100
OSFUser,
101101
Email,
102102
Tag,
103+
PreprintProvider,
103104
)
104105
from osf.utils.tokens import TokenHandler
105106
from osf.utils.tokens.handlers import sanction_handler
106107
from website import mails, settings, language
107108
from website.project.views.contributor import send_claim_email, send_claim_registered_email
108109
from website.util.metrics import CampaignClaimedTags, CampaignSourceTags
109110
from framework.auth import exceptions
111+
from website.project.views.contributor import _add_related_claimed_tag_to_user
112+
from website.util import api_v2_url
110113

111114

112115
class UserMixin:
@@ -1008,6 +1011,86 @@ def post(self, request, *args, **kwargs):
10081011

10091012
return Response(status=status.HTTP_204_NO_CONTENT)
10101013

1014+
class ConfirmClaimUser(JSONAPIBaseView, generics.CreateAPIView, UserMixin):
1015+
1016+
view_category = 'users'
1017+
view_name = 'confirm-claim-user'
1018+
1019+
def verify_claim_token(self, user, token, pid):
1020+
"""View helper that checks that a claim token for a given user and node ID
1021+
is valid. If not valid, throws an error with custom error messages.
1022+
"""
1023+
# if token is invalid, throw an error
1024+
if not user.verify_claim_token(token=token, project_id=pid):
1025+
if user.is_registered:
1026+
raise ValidationError('User has already been claimed.')
1027+
else:
1028+
return False
1029+
return True
1030+
1031+
def post(self, request, *args, **kwargs):
1032+
"""
1033+
View for setting the password for a claimed user.
1034+
Sets the user's password.
1035+
HTTP Method: POST
1036+
**URL:** /v2/users/<user_id>/confirm_claim/
1037+
**Body (JSON):**
1038+
{
1039+
"data": {
1040+
"type": "users",
1041+
"attributes": {
1042+
"guid": "node/preprint guid",
1043+
"token": "token",
1044+
"password": "password",
1045+
"accepted_terms_of_service": bool
1046+
}
1047+
}
1048+
}
1049+
"""
1050+
1051+
uid = kwargs['user_id']
1052+
token = request.data.get('token')
1053+
guid = request.data.get('guid')
1054+
password = request.data.get('password')
1055+
accepted_terms_of_service = request.data.get('accepted_terms_of_service', False)
1056+
user = OSFUser.load(uid)
1057+
1058+
# If unregistered user is not in database, or url bears an invalid token raise HTTP 400 error
1059+
if not user or not self.verify_claim_token(user, token, guid):
1060+
raise ValidationError('Claim user does not exists, the token in the URL is invalid or has expired.')
1061+
1062+
# If user is logged in, need to use 'confirm_claim_registered' view
1063+
if request.user.is_authenticated:
1064+
raise ValidationError('You are already logged in. Please log out before trying to claim a user via this view.')
1065+
1066+
unclaimed_record = user.unclaimed_records[guid]
1067+
user.fullname = unclaimed_record['name']
1068+
user.update_guessed_names()
1069+
username = unclaimed_record.get('claimer_email') or unclaimed_record.get('email')
1070+
1071+
user.register(username=username, password=password, accepted_terms_of_service=accepted_terms_of_service)
1072+
# Clear unclaimed records
1073+
user.unclaimed_records = {}
1074+
user.verification_key = generate_verification_key()
1075+
user.save()
1076+
provider = PreprintProvider.load(guid)
1077+
redirect_url = None
1078+
if provider:
1079+
redirect_url = api_v2_url('auth_login', next=provider.landing_url, _absolute=True)
1080+
else:
1081+
# Add related claimed tags to user
1082+
_add_related_claimed_tag_to_user(guid, user)
1083+
redirect_url = api_v2_url('resolve_guid', guid=guid, _absolute=True)
1084+
1085+
data = {
1086+
'firstname': user.given_name,
1087+
'email': username if username else '',
1088+
'fullname': user.fullname,
1089+
'osf_contact_email': settings.OSF_CONTACT_EMAIL,
1090+
'next': redirect_url,
1091+
}
1092+
1093+
return Response(status=status.HTTP_200_OK, data=data)
10111094

10121095
class ConfirmEmailView(generics.CreateAPIView):
10131096
"""

api_tests/base/test_views.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
)
2020
from api.users.views import (
2121
ClaimUser,
22+
ConfirmClaimUser,
2223
ResetPassword,
2324
ExternalLoginConfirmEmailView,
2425
ExternalLogin,
@@ -60,6 +61,7 @@ class TestApiBaseViews(ApiTestCase):
6061
def setUp(self):
6162
super().setUp()
6263
self.EXCLUDED_VIEWS = [
64+
ConfirmClaimUser,
6365
ClaimUser,
6466
CopyFileMetadataView,
6567
CountedAuthUsageView,

api_tests/users/views/test_user_claim.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,3 +232,95 @@ def test_claim_auth_success(self, app, url, claimer, unreg_user, project, mock_s
232232
)
233233
assert res.status_code == 204
234234
assert mock_send_grid.call_count == 2
235+
236+
237+
@pytest.mark.django_db
238+
class TestConfirmClaimUser:
239+
240+
@pytest.fixture()
241+
def referrer(self):
242+
return AuthUserFactory()
243+
244+
@pytest.fixture()
245+
def project(self, referrer):
246+
return ProjectFactory(creator=referrer)
247+
248+
@pytest.fixture()
249+
def preprint(self, referrer, project):
250+
return PreprintFactory(creator=referrer, project=project)
251+
252+
@pytest.fixture()
253+
def wrong_preprint(self, referrer):
254+
return PreprintFactory(creator=referrer)
255+
256+
@pytest.fixture()
257+
def unreg_user(self, referrer, project):
258+
return project.add_unregistered_contributor(
259+
'David Davidson',
260+
'david@david.son',
261+
auth=Auth(referrer),
262+
save=True
263+
)
264+
265+
@pytest.fixture()
266+
def url(self):
267+
return f'/{API_BASE}users/{{}}/confirm_claim/'
268+
269+
def payload(self, **kwargs):
270+
payload = {
271+
'data': {
272+
'attributes': {}
273+
}
274+
}
275+
_id = kwargs.pop('id', None)
276+
if _id:
277+
payload['data']['id'] = _id
278+
if kwargs:
279+
payload['data']['attributes'] = kwargs
280+
return payload
281+
282+
def test_confirm_claim(self, app, url, unreg_user, project, wrong_preprint):
283+
_url = url.format(unreg_user._id)
284+
unclaimed_record = unreg_user.get_unclaimed_record(project._id)
285+
token = unclaimed_record['token']
286+
payload = self.payload(guid=project._id, password='password1234', accepted_terms_of_service=True, token=token)
287+
res = app.post_json_api(
288+
_url,
289+
payload,
290+
)
291+
assert res.status_code == 200
292+
unreg_user.reload()
293+
assert unreg_user.is_registered
294+
295+
def test_confirm_claim_wrong_pid(self, app, url, unreg_user, project, wrong_preprint):
296+
_url = url.format(unreg_user._id)
297+
token = 'someinvalidtoken'
298+
payload = self.payload(guid=wrong_preprint._id, password='password1234', accepted_terms_of_service=True, token=token)
299+
res = app.post_json_api(
300+
_url,
301+
payload,
302+
expect_errors=True
303+
)
304+
assert res.status_code == 400
305+
assert res.json['errors'][0]['detail'] == 'Claim user does not exists, the token in the URL is invalid or has expired.'
306+
307+
def test_claimed_user(self, app, url, unreg_user, project, wrong_preprint):
308+
_url = url.format(unreg_user._id)
309+
unclaimed_record = unreg_user.get_unclaimed_record(project._id)
310+
token = unclaimed_record['token']
311+
payload = self.payload(guid=project._id, password='password1234', accepted_terms_of_service=True, token=token)
312+
res = app.post_json_api(
313+
_url,
314+
payload,
315+
)
316+
assert res.status_code == 200
317+
unreg_user.reload()
318+
assert unreg_user.is_registered
319+
320+
res = app.post_json_api(
321+
_url,
322+
payload,
323+
expect_errors=True
324+
)
325+
assert res.status_code == 400
326+
assert res.json['errors'][0]['detail'] == 'User has already been claimed.'

0 commit comments

Comments
 (0)