diff --git a/corehq/apps/domain/auth.py b/corehq/apps/domain/auth.py index 0c2e4f734d62..6d472b2d995d 100644 --- a/corehq/apps/domain/auth.py +++ b/corehq/apps/domain/auth.py @@ -376,6 +376,8 @@ def authenticate(self, request, username, password): domain=couch_user.domain, commcare_user__username=couch_user.username ) + if not link.is_active: + return None return link.commcare_user diff --git a/corehq/apps/user_importer/importer.py b/corehq/apps/user_importer/importer.py index 4974c5d96c64..c8c3974dcd10 100644 --- a/corehq/apps/user_importer/importer.py +++ b/corehq/apps/user_importer/importer.py @@ -51,7 +51,11 @@ InvitationStatus ) from corehq.const import USER_CHANGE_VIA_BULK_IMPORTER -from corehq.toggles import DOMAIN_PERMISSIONS_MIRROR, TABLEAU_USER_SYNCING +from corehq.toggles import ( + DOMAIN_PERMISSIONS_MIRROR, + TABLEAU_USER_SYNCING, + COMMCARE_CONNECT, +) from corehq.apps.sms.util import validate_phone_number from dimagi.utils.logging import notify_error @@ -103,8 +107,12 @@ def check_headers(user_specs, domain, upload_couch_user, is_web_upload=False): "User Syncing is enabled can upload files with 'Tableau Role' and/or 'Tableau Groups' fields." )) + if COMMCARE_CONNECT.enabled(domain): + allowed_headers.add('send_connectid_invite') + illegal_headers = headers - allowed_headers - conditionally_allowed_headers + if is_web_upload: missing_headers = web_required_headers - headers else: @@ -504,6 +512,9 @@ def _parse_username(self): def _parse_password(self): from corehq.apps.user_importer.validation import is_password + if self.column_values["send_connectid_invite"]: + # password login is disabled for connectid + password = '' if self.row.get('password'): password = str(self.row.get('password')) elif self.column_values["send_confirmation_sms"]: @@ -537,10 +548,10 @@ def _process_column_values(self): } for v in ['is_active', 'is_account_confirmed', 'send_confirmation_email', - 'remove_web_user', 'send_confirmation_sms']: + 'remove_web_user', 'send_confirmation_sms', 'send_connectid_invite']: values[v] = spec_value_to_boolean_or_none(self.row, v) - if values["send_confirmation_sms"] and not values["user_id"]: + if (values["send_confirmation_sms"] or values["send_connectid_invite"]) and not values["user_id"]: values["is_account_confirmed"] = False else: values["is_account_confirmed"] = values["is_account_confirmed"] @@ -698,6 +709,9 @@ def _process_web_user(self): send_account_confirmation_if_necessary(self.user) if cv["send_confirmation_sms"]: send_account_confirmation_sms_if_necessary(self.user) + elif cv["send_connectid_invite"]: + from corehq.apps.users.views.mobile.users import deliver_connectid_invite + deliver_connectid_invite(self.user) class WebUserRow(BaseUserRow): diff --git a/corehq/apps/user_importer/validation.py b/corehq/apps/user_importer/validation.py index 0cb2c2bc1ac2..f53380932c5e 100644 --- a/corehq/apps/user_importer/validation.py +++ b/corehq/apps/user_importer/validation.py @@ -67,7 +67,8 @@ def get_user_import_validators(domain_obj, all_specs, is_web_user_import, all_us NewUserPasswordValidator(domain), PasswordValidator(domain) if validate_passwords else noop, GroupValidator(domain, allowed_groups), - ConfirmationSmsValidator(domain) + ConfirmationSmsValidator(domain), + ConnectIdInviteValidator(domain) ] @@ -501,6 +502,10 @@ def validate_spec(self, spec): return self.error_existing_user.format(self.confirmation_sms_header, errors_formatted) +class ConnectIdInviteValidator(ConfirmationSmsValidator): + confirmation_sms_header = "send_connectid_invite" + + class LocationValidator(ImportValidator): error_message_user_access = _("Based on your locations you do not have permission to edit this user or user " "invitation") diff --git a/corehq/apps/users/forms.py b/corehq/apps/users/forms.py index dad316a36dd2..f7865a2fa350 100644 --- a/corehq/apps/users/forms.py +++ b/corehq/apps/users/forms.py @@ -57,6 +57,7 @@ from corehq.pillows.utils import MOBILE_USER_TYPE, WEB_USER_TYPE from corehq.feature_previews import USE_LOCATION_DISPLAY_NAME from corehq.toggles import ( + COMMCARE_CONNECT, TWO_STAGE_USER_PROVISIONING, TWO_STAGE_USER_PROVISIONING_BY_SMS, ) @@ -670,6 +671,13 @@ class NewMobileWorkerForm(forms.Form): ), required=False, ) + account_invite_by_cid = forms.BooleanField( + label=gettext_noop("Invite using ConnectID phone number?"), + help_text=gettext_noop( + "If checked, the user will be sent an SMS to join the project using their ConnectID app." + ), + required=False, + ) phone_number = forms.CharField( required=False, label=gettext_noop("Phone Number"), @@ -768,10 +776,16 @@ def __init__(self, project, request_user, *args, **kwargs): data_bind='value: send_account_confirmation_email', ) - if TWO_STAGE_USER_PROVISIONING_BY_SMS.enabled(self.domain): + # cid => connect-id + provision_by_cid = COMMCARE_CONNECT.enabled(self.domain) + + provision_by_sms = TWO_STAGE_USER_PROVISIONING_BY_SMS.enabled(self.domain) + + if provision_by_sms or provision_by_cid: + varname = 'account_invite_by_cid' if provision_by_cid else 'force_account_confirmation_by_sms' confirm_account_by_sms_field = crispy.Field( - 'force_account_confirmation_by_sms', - data_bind='checked: force_account_confirmation_by_sms', + varname, + data_bind=f'checked: {varname}', ) phone_number_field = crispy.Div( crispy.Field( @@ -867,9 +881,14 @@ def __init__(self, project, request_user, *args, **kwargs): {disabled_email} + && ($root.stagedUser().force_account_confirmation_by_sms() + || $root.stagedUser().account_invite_by_cid) --> {disabled_phone} + + {disabled_cid} +

'''.format( @@ -892,6 +911,11 @@ def __init__(self, project, request_user, *args, **kwargs): "will set their own password on confirming " "their account phone number." ), + disabled_cid = _( + "Setting a password is disabled. The user " + "will be to access by logging into their " + "ConnectID app." + ), short=_("Password must have at least {password_length} characters." ).format(password_length=settings.MINIMUM_PASSWORD_LENGTH) )), diff --git a/corehq/apps/users/migrations/0076_connectiduserlink_is_active.py b/corehq/apps/users/migrations/0076_connectiduserlink_is_active.py new file mode 100644 index 000000000000..afcf7a44e74e --- /dev/null +++ b/corehq/apps/users/migrations/0076_connectiduserlink_is_active.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.14 on 2024-08-12 09:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0075_hqapikey_encrypted_key'), + ] + + operations = [ + migrations.AddField( + model_name='connectiduserlink', + name='is_active', + field=models.BooleanField(default=True), + ), + ] diff --git a/corehq/apps/users/models.py b/corehq/apps/users/models.py index 49c0bb034b4e..ab6afe194d9b 100644 --- a/corehq/apps/users/models.py +++ b/corehq/apps/users/models.py @@ -3315,6 +3315,7 @@ class ConnectIDUserLink(models.Model): connectid_username = models.TextField() commcare_user = models.ForeignKey(User, related_name='connectid_user', on_delete=models.CASCADE) domain = models.TextField() + is_active = models.BooleanField(default=True) class Meta: unique_together = ('domain', 'commcare_user') diff --git a/corehq/apps/users/static/users/js/mobile_workers.js b/corehq/apps/users/static/users/js/mobile_workers.js index 1e130439e1d2..6814e73f5d18 100644 --- a/corehq/apps/users/static/users/js/mobile_workers.js +++ b/corehq/apps/users/static/users/js/mobile_workers.js @@ -72,9 +72,11 @@ hqDefine("users/js/mobile_workers",[ email: '', send_account_confirmation_email: false, force_account_confirmation_by_sms: false, + account_invite_by_cid: false, phone_number: '', is_active: true, is_account_confirmed: true, + is_connect_link_active: null, deactivate_after_date: '', }); @@ -95,9 +97,11 @@ hqDefine("users/js/mobile_workers",[ self.sendConfirmationEmailEnabled = ko.observable(self.force_account_confirmation()); // used by two-stage sms provisioning - self.phoneRequired = ko.observable(self.force_account_confirmation_by_sms()); + self.phoneRequired = ko.observable(self.force_account_confirmation_by_sms() || self.account_invite_by_cid()); - self.passwordEnabled = ko.observable(!(self.force_account_confirmation_by_sms() || self.force_account_confirmation())); + self.passwordEnabled = ko.observable(!( + self.force_account_confirmation_by_sms() || self.force_account_confirmation() || self.account_invite_by_cid()) + ); self.action_error = ko.observable(''); // error when activating/deactivating a user @@ -105,10 +109,7 @@ hqDefine("users/js/mobile_workers",[ return initialPageData.reverse('edit_commcare_user', self.user_id()); }); - self.is_active.subscribe(function (newValue) { - var urlName = newValue ? 'activate_commcare_user' : 'deactivate_commcare_user', - $modal = $('#' + (newValue ? 'activate_' : 'deactivate_') + self.user_id()); - + var toggle_active = function($modal, urlName) { $modal.find(".btn").addSpinnerToButton(); $.ajax({ method: 'POST', @@ -126,6 +127,18 @@ hqDefine("users/js/mobile_workers",[ self.action_error(gettext("Issue communicating with server. Try again.")); }, }); + }; + + self.is_active.subscribe(function (newValue) { + var urlName = newValue ? 'activate_commcare_user' : 'deactivate_commcare_user', + $modal = $('#' + (newValue ? 'activate_' : 'deactivate_') + self.user_id()); + toggle_active($modal, urlName); + }); + + self.is_connect_link_active.subscribe(function (newValue) { + var urlName = newValue ? 'activate_connectid_link' : 'deactivate_connectid_link', + $modal = $('#' + (newValue ? 'activate_connect_link_' : 'deactivate_connect_link_') + self.user_id()); + toggle_active($modal, urlName); }); self.sendConfirmationEmail = function () { @@ -178,6 +191,31 @@ hqDefine("users/js/mobile_workers",[ }); }; + self.sendConnectIDInvite = function () { + var urlName = 'send_connectid_invite'; + var $modal = $('#confirm_' + self.user_id()); + + $modal.find(".btn").addSpinnerToButton(); + $.ajax({ + method: 'POST', + url: initialPageData.reverse(urlName, self.user_id()), + success: function (data) { + $modal.modal('hide'); + if (data.success) { + self.action_error(''); + } else { + self.action_error(data.error); + } + + }, + error: function () { + $modal.modal('hide'); + $modal.find(".btn").removeSpinnerFromButton(); + self.action_error(gettext("Issue communicating with server. Try again.")); + }, + }); + }; + return self; }; @@ -294,7 +332,7 @@ hqDefine("users/js/mobile_workers",[ return self.STATUS.DISABLED; } - if (self.stagedUser().force_account_confirmation_by_sms()) { + if (self.stagedUser().force_account_confirmation_by_sms() || self.stagedUser().account_invite_by_cid()) { return self.STATUS.DISABLED; } @@ -511,7 +549,7 @@ hqDefine("users/js/mobile_workers",[ user.send_account_confirmation_email(false); } }); - user.force_account_confirmation_by_sms.subscribe(function (enabled) { + var handlePhoneRequired = function (enabled) { if (enabled) { // make phone number required user.phoneRequired(true); @@ -525,7 +563,9 @@ hqDefine("users/js/mobile_workers",[ // enable password input user.passwordEnabled(true); } - }); + }; + user.force_account_confirmation_by_sms.subscribe(handlePhoneRequired); + user.account_invite_by_cid.subscribe(handlePhoneRequired); }); self.initializeUser = function () { diff --git a/corehq/apps/users/templates/users/mobile_workers.html b/corehq/apps/users/templates/users/mobile_workers.html index 5ccdb6a03c8b..3ada14853ec1 100644 --- a/corehq/apps/users/templates/users/mobile_workers.html +++ b/corehq/apps/users/templates/users/mobile_workers.html @@ -24,8 +24,11 @@ {% registerurl 'paginate_mobile_workers' domain %} {% registerurl 'activate_commcare_user' domain '---' %} {% registerurl 'deactivate_commcare_user' domain '---' %} + {% registerurl 'activate_connectid_link' domain '---' %} + {% registerurl 'deactivate_connectid_link' domain '---' %} {% registerurl 'send_confirmation_email' domain '---' %} {% registerurl 'send_confirmation_sms' domain '---' %} + {% registerurl 'send_connectid_invite' domain '---' %}
@@ -198,6 +201,7 @@

+ ConnectID @@ -295,6 +299,7 @@

{% trans 'Mobile Workers' %}

+ ConnectID @@ -320,17 +325,21 @@

{% trans 'Mobile Workers' %}

- {% if request|toggle_enabled:"TWO_STAGE_USER_PROVISIONING" %} - {% else %} - - {% endif %} + {% if request|toggle_enabled:"TWO_STAGE_USER_PROVISIONING" %} + {% blocktrans %} + Send Confirmation Email + {% endblocktrans %} + {% elif request|toggle_enabled:"TWO_STAGE_USER_PROVISIONING_BY_SMS" %} + {% blocktrans %} + Send Confirmation SMS + {% endblocktrans %} + {% elif request|toggle_enabled:"COMMCARE_CONNECT" %} + {% blocktrans %} + Send ConnectID Invite + {% endblocktrans %} + {% endif %}

@@ -442,6 +451,104 @@

+ {% if request|toggle_enabled:"COMMCARE_CONNECT" %} +
+ +

+ + +

+
+ +
+ +

+ + +

+
+ + {% endif %} {% if two_stage_user_confirmation %} diff --git a/corehq/apps/users/urls.py b/corehq/apps/users/urls.py index 389acecbee41..90340729e644 100644 --- a/corehq/apps/users/urls.py +++ b/corehq/apps/users/urls.py @@ -59,9 +59,11 @@ UploadCommCareUsers, UserUploadStatusView, activate_commcare_user, + activate_connectid_link, count_commcare_users, count_web_users, deactivate_commcare_user, + deactivate_connectid_link, delete_commcare_user, demo_restore_job_poll, download_commcare_users, @@ -75,6 +77,8 @@ CommCareUserConfirmAccountView, send_confirmation_email, send_confirmation_sms, + send_connectid_invite, + confirm_connectid_user, CommcareUserUploadJobPollView, ClearCommCareUsers, link_connectid_user, @@ -158,6 +162,10 @@ url(r'^commcare/account/(?P[ \w-]+)/groups/$', update_user_groups, name='update_user_groups'), url(r'^commcare/activate/(?P[ \w-]+)/$', activate_commcare_user, name='activate_commcare_user'), url(r'^commcare/deactivate/(?P[ \w-]+)/$', deactivate_commcare_user, name='deactivate_commcare_user'), + url(r'^commcare/activate_connectid_link/(?P[ \w-]+)/$', + activate_connectid_link, name='activate_connectid_link'), + url(r'^commcare/deactivate_connectid_link/(?P[ \w-]+)/$', + deactivate_connectid_link, name='deactivate_connectid_link'), url( r'^commcare/send_confirmation_email/(?P[ \w-]+)/$', send_confirmation_email, @@ -247,6 +255,16 @@ CommCareUserConfirmAccountBySMSView.as_view(), name=CommCareUserConfirmAccountBySMSView.urlname ), + url( + r'^commcare/send_connectid_invite/(?P[ \w-]+)/$', + send_connectid_invite, + name='send_connectid_invite' + ), + url( + r'^commcare/confirm_connectid_user/$', + confirm_connectid_user, + name="confirm_connectid_user" + ), url( r'^commcare/link_connectid_user/$', link_connectid_user, diff --git a/corehq/apps/users/views/mobile/users.py b/corehq/apps/users/views/mobile/users.py index 3b3f8b5ed87b..2c716f79af06 100644 --- a/corehq/apps/users/views/mobile/users.py +++ b/corehq/apps/users/views/mobile/users.py @@ -1,11 +1,14 @@ import io import json import re +import requests import time +from django.conf import settings from django.contrib import messages from django.contrib.humanize.templatetags.humanize import naturaltime from django.core.exceptions import ValidationError +from django.db.models import F from django.http import ( Http404, HttpResponse, @@ -31,7 +34,7 @@ from casexml.apps.phone.models import SyncLogSQL from couchexport.models import Format from couchexport.writers import Excel2007ExportWriter -from dimagi.utils.web import json_response +from dimagi.utils.web import json_response, get_site_domain from dimagi.utils.logging import notify_exception from soil import DownloadBase from soil.exceptions import TaskFailedError @@ -61,7 +64,7 @@ ) from corehq.apps.domain.extension_points import has_custom_clean_password from corehq.apps.domain.models import SMSAccountConfirmationSettings -from corehq.apps.domain.utils import guess_domain_language_for_sms +from corehq.apps.domain.utils import guess_domain_language_for_sms, encrypt_account_confirmation_info from corehq.apps.domain.views.base import DomainViewMixin from corehq.apps.es import FormES from corehq.apps.events.models import ( @@ -86,6 +89,7 @@ MobileWorkerAccountConfirmationForm, ) from corehq.apps.sms.api import send_sms +from corehq.apps.sms.util import clean_phone_number from corehq.apps.user_importer.exceptions import UserUploadError from corehq.apps.users.account_confirmation import ( send_account_confirmation_if_necessary, @@ -157,6 +161,7 @@ from corehq.util.dates import iso_string_to_datetime from corehq.util.jqueryrmi import JSONResponseMixin, allow_remote_invocation from corehq.util.metrics import metrics_counter +from corehq.util.view_utils import absolute_reverse from corehq.util.workbook_json.excel import ( WorkbookJSONError, WorksheetNotFound, @@ -721,9 +726,9 @@ def custom_data(self): @property def two_stage_user_confirmation(self): - return toggles.TWO_STAGE_USER_PROVISIONING.enabled( - self.domain - ) or toggles.TWO_STAGE_USER_PROVISIONING_BY_SMS.enabled(self.domain) + return (toggles.TWO_STAGE_USER_PROVISIONING.enabled(self.domain) + or toggles.TWO_STAGE_USER_PROVISIONING_BY_SMS.enabled(self.domain) + or toggles.COMMCARE_CONNECT.enabled(self.domain)) @property def page_context(self): @@ -799,10 +804,15 @@ def create_mobile_worker(self, in_data): if self.new_mobile_worker_form.cleaned_data['send_account_confirmation_email']: send_account_confirmation_if_necessary(couch_user) + if self.new_mobile_worker_form.cleaned_data['force_account_confirmation_by_sms']: phone_number = self.new_mobile_worker_form.cleaned_data['phone_number'] couch_user.set_default_phone_number(phone_number) send_account_confirmation_sms_if_necessary(couch_user) + if self.new_mobile_worker_form.cleaned_data['account_invite_by_cid']: + phone_number = self.new_mobile_worker_form.cleaned_data['phone_number'] + couch_user.set_default_phone_number(phone_number) + deliver_connectid_invite(couch_user, self.request) plan_limit, user_count = Subscription.get_plan_and_user_count_by_domain(self.domain) check_and_send_limit_email(self.domain, plan_limit, user_count, user_count - 1) @@ -824,14 +834,19 @@ def create_attendee_for_user(self, commcare_user): def _build_commcare_user(self): username = self.new_mobile_worker_form.cleaned_data['username'] - password = self.new_mobile_worker_form.cleaned_data['new_password'] + if self.new_mobile_worker_form.cleaned_data['account_invite_by_cid']: + # Passwordless login using ConnectID + password = '' + else: + password = self.new_mobile_worker_form.cleaned_data['new_password'] first_name = self.new_mobile_worker_form.cleaned_data['first_name'] email = self.new_mobile_worker_form.cleaned_data['email'] last_name = self.new_mobile_worker_form.cleaned_data['last_name'] location_id = self.new_mobile_worker_form.cleaned_data['location_id'] is_account_confirmed = not ( self.new_mobile_worker_form.cleaned_data['force_account_confirmation'] - or self.new_mobile_worker_form.cleaned_data['force_account_confirmation_by_sms']) + or self.new_mobile_worker_form.cleaned_data['force_account_confirmation_by_sms'] + or self.new_mobile_worker_form.cleaned_data['account_invite_by_cid']) role_id = UserRole.commcare_user_default(self.domain).get_id commcare_user = CommCareUser.create( @@ -881,6 +896,7 @@ def _construct_form_data(self, in_data): 'force_account_confirmation': user_data.get('force_account_confirmation'), 'send_account_confirmation_email': user_data.get('send_account_confirmation_email'), 'force_account_confirmation_by_sms': user_data.get('force_account_confirmation_by_sms'), + 'account_invite_by_cid': user_data.get('account_invite_by_cid'), 'phone_number': user_data.get('phone_number'), 'deactivate_after_date': user_data.get('deactivate_after_date'), 'domain': self.domain, @@ -924,6 +940,32 @@ def _modify_user_status(request, domain, user_id, is_active): }) +@require_can_edit_commcare_users +@require_POST +@location_safe +def activate_connectid_link(request, domain, user_id): + return _toggle_connectid_link(request, domain, user_id, True) + + +@require_can_edit_commcare_users +@require_POST +@location_safe +def deactivate_connectid_link(request, domain, user_id): + return _toggle_connectid_link(request, domain, user_id, False) + + +def _toggle_connectid_link(request, domain, user_id, is_active): + if not toggles.COMMCARE_CONNECT.enabled(domain): + return HttpResponseBadRequest() + user = CommCareUser.get_by_user_id(user_id, domain) + connect_link = ConnectIDUserLink.objects.get(commcare_user=user.get_django_user()) + connect_link.is_active = is_active + connect_link.save() + return JsonResponse({ + 'success': True, + }) + + @require_can_edit_commcare_users @require_POST @location_safe @@ -981,6 +1023,22 @@ def _status_string(user_data): else: return _('Pending Confirmation') + def get_connect_links_by_username(users): + if not toggles.COMMCARE_CONNECT.enabled(domain): + return {} + usernames = [ + f"{u['base_username']}@{domain}.commcarehq.org" + for u in users + ] + links = ConnectIDUserLink.objects.filter( + commcare_user__username__in=usernames + ).annotate(commcare_username=F("commcare_user__username")).all() + return { + raw_username(link.commcare_username): link + for link in links + } + + connect_links = get_connect_links_by_username(users) for user in users: date_registered = user.pop('created_on', '') if date_registered: @@ -988,6 +1046,8 @@ def _status_string(user_data): # make sure these are always set and default to true user['is_active'] = user.get('is_active', True) user['is_account_confirmed'] = user.get('is_account_confirmed', True) + connect_link = connect_links.get(user.get('base_username')) + user['is_connect_link_active'] = connect_link.is_active if connect_link else None user.update({ 'username': user.pop('base_username', ''), 'user_id': user.pop('_id'), @@ -1600,6 +1660,69 @@ def link_connectid_user(request, domain): return HttpResponse() +@require_can_edit_commcare_users +@location_safe +def send_connectid_invite(request, domain, user_id): + user = CommCareUser.get_by_user_id(user_id, domain) + if user.domain != domain: + return HttpResponse(status=400) + is_sent = deliver_connectid_invite(user) + if not is_sent: + return HttpResponse(status=400) + return HttpResponse(status=200) + + +def deliver_connectid_invite(user, request=None): + # ConnectID server delivers the invite to user + if user.is_account_confirmed or not user.is_commcare_user(): + if request: + messages.error(request, "The user is already confirmed or is not a mobile user") + return False + + invite_code = encrypt_account_confirmation_info(user) + response = requests.post( + settings.CONNECTID_USERINVITE_URL, + data={ + "phone_number": clean_phone_number(user.default_phone_number), + "callback_url": absolute_reverse("confirm_connectid_user", args=[user.domain]), + "user_domain": user.domain, + "username": user.raw_username, + "invite_code": invite_code, + }, + auth=(settings.CONNECTID_CLIENT_ID, settings.CONNECTID_SECRET_KEY) + ) + response.raise_for_status() + return True + + +@csrf_exempt +@require_POST +def confirm_connectid_user(request, domain): + try: + token = request.POST["token"] + invite_code = request.POST["invite_code"] + invite = json.loads(b64_aes_decrypt(invite_code)) + user_id = invite['user_id'] + expiry_time = invite['time'] + except ValueError: + return HttpResponseBadRequest("Invalid Request") + + # Expiry limit is 3 days + if float(int(time.time()) - expiry_time) > 3 * 24 * 60 * 60: + return JsonResponse(data={"error": "Invite has expired"}) + + user = CommCareUser.get(user_id) + if user.domain != domain: + return HttpResponseBadRequest("Invalid Request") + + connectid_username = get_connectid_userinfo(token) + ConnectIDUserLink.objects.get_or_create( + connectid_username=connectid_username, commcare_user=user.get_django_user(), domain=domain + ) + user.confirm_account(password=None) + return json_response({'success': True}) + + @waf_allow('XSS_BODY') @csrf_exempt @require_POST diff --git a/migrations.lock b/migrations.lock index a79a90b63a89..b32d15efe43b 100644 --- a/migrations.lock +++ b/migrations.lock @@ -1267,6 +1267,7 @@ users 0073_rm_location_from_user_data 0074_alter_sqluserdata_profile 0075_hqapikey_encrypted_key + 0076_connectiduserlink_is_active util 0001_initial 0002_complaintbouncemeta_permanentbouncemeta_transientbounceemail diff --git a/settings.py b/settings.py index 4babe68eba4d..3f01e6ca8141 100755 --- a/settings.py +++ b/settings.py @@ -1145,6 +1145,7 @@ def _pkce_required(client_id): FCM_CREDS = None CONNECTID_USERINFO_URL = 'http://localhost:8080/o/userinfo' +CONNECTID_USERINVITE_URL = 'http://localhost:8080/users/forward_hq_invite' MAX_MOBILE_UCR_LIMIT = 300 # used in corehq.apps.cloudcare.util.should_restrict_web_apps_usage MAX_MOBILE_UCR_SIZE = 100000 # max number of rows allowed when syncing a mobile UCR