"""Prestodoctor OAuth on Authomatic implementation with data import and mapping to internal database.""" from argparse import Namespace import time from authomatic.core import json_qs_parser from authomatic.providers.oauth2 import OAuth2 from websauna.system.model import now from websauna.system.user.social import EmailSocialLoginMapper, \ NotSatisfiedWithData from trees.models import UserMedia __author__ = "Mikko Ohtamaa <mikko@opensourcehacker.com>" __license__ = "AGPL" class PrestodoctorAuthomatic(OAuth2): """Prestodoctor Authomatic OAuth2 implementation. * Docs: https://github.com/PrestoDoctor/omniauth-prestodoctor/blob/master/lib/omniauth/strategies/prestodoctor.rb """ user_authorization_url = 'https://prestodoctor.com/oauth/authorize' access_token_url = 'https://prestodoctor.com/oauth/token' user_info_url = 'https://prestodoctor.com/api/v1/user' info_base_url = "https://prestodoctor.com/api/v1/user" #: Comes from config file user_info_scope = [] def _x_scope_parser(self, scope): """Space separated scopes""" return 'user_info recommendation photo_id' def _update_or_create_user(self, data, credentials=None, content=None): """Fetch user info from Prestodoctor specific endpoints.""" super(PrestodoctorAuthomatic, self)._update_or_create_user(data, credentials, content) self.user.base_data = self.access(self.info_base_url, content_parser=json_qs_parser).data # Recommendation data might be empty if the user has not done medical evaluation yet self.user.recommendation_data = self.access(self.info_base_url + "/recommendation", content_parser=json_qs_parser).data or {} self.user.photo_data = self.access(self.info_base_url + "/photo_id", content_parser=json_qs_parser).data or {} return self.user class PrestodoctorMapper(EmailSocialLoginMapper): """Map Prestodoctor external users to our database.""" def import_social_media_user(self, user): """Convert incoming Authomatic object to info dictionary.""" # Merge all separate data sources to a single dictionary info = user.base_data.copy() info["photo_id"] = user.photo_data.copy() info["recommendation"] = user.recommendation_data.copy() return info def update_first_login_social_data(self, user, data): """Update user full name on the first login only.""" super(PrestodoctorMapper, self).update_first_login_social_data(user, data) user.full_name = data["first_name"] + " " + data["last_name"] def update_full_presto_data(self, user, info:Namespace): """Download copy of Prestodoctor photo files to local server. Set user's medical license verified if it looks good. """ user.license_initial_upload_completed_at = now() # Trust Prestodoctor licenses if they are not expired if info.recommendation.expires > time.time(): user.license_verified_by = None user.license_verified_at = now() user.presto_license_number = info.recommendation.id_num user.medical_license_upload_completed_at = now() user.license_initial_upload_completed_at = now() # Download copy of government issued id so the driver can check this is the right person driving_license = UserMedia.fetch_from_url(self.registry, info.photo_id.url, user=user) driving_license.approved_by = None driving_license.approved_at = now() driving_license.store_bbb_copy(self.registry, "driving") # Backwards compatibility # Set a marker for tests so we know we don't do this operation twice user.user_data["social"]["prestodoctor"]["full_data_updated_at"] = now().isoformat() def update_every_login_social_data(self, user, data): """Update user data every time they relogin.""" # If the user has been using our system before, get the current active prestodoctor recommendation issued date last_known_recommendation_issued = user.user_data["social"].get("prestodoctor", {}).get("recommendation", {}).get("issued", None) super(PrestodoctorMapper, self).update_every_login_social_data(user, data) # Convert data to dotted notation for saving wrists below # http://stackoverflow.com/a/16279578/315168 info = Namespace(**data) info.address = Namespace(**info.address) info.photo_id = Namespace(**info.photo_id) info.recommendation = Namespace(**info.recommendation) # Map Presto fields to our fields mappings = { "dob": info.dob, "photo_url": info.photo, "country": "US", "zipcode": info.address.zip5, "zip4": info.address.zip4, "gender": None, "first_name": info.first_name, "last_name": info.last_name, "full_name": info.first_name + " " + info.last_name, "city": info.address.city, "state": info.address.state, "postal_code": info.address.zip5, "address": info.address.address1, "apartment": info.address.address2, "external_data_updated": now().isoformat() } # TODO: Set user medical license verified if Presto gives us its number # Update internal structure. Only override existing value if we have data from Presto. # E.g. phone number might be missing, but we have it, so we don't want to replace existing phone number with empty string. for key, value in mappings.items(): if value: user.user_data[key] = value if user.user_data["social"]["prestodoctor"]["recommendation"].get("issued") != last_known_recommendation_issued: # The prestodoctor evaluation issue has changed, do the heavy data update self.update_full_presto_data(user, info) def capture_social_media_user(self, request, result): """Extract social media information from the Authomatic login result in order to associate the user account.""" # Should not happen assert not result.error email = result.user.base_data.get("email") if not email: # We cannot login if the Facebook doesnt' give us email as we use it for the user mapping # This can also happen when you have not configured Facebook app properly in the developers.facebook.com raise NotSatisfiedWithData("Email address is needed in order to user this service and we could not get one from your social media provider. Please try to sign up with your email instead.") user = self.get_or_create_user_by_social_medial_email(request, result.user) return user #: This map is to satisfy Authomatic module loader PROVIDER_ID_MAP = [PrestodoctorAuthomatic]