def delete_api_key(request, *args, **argv): #pylint: disable=unused-argument """Deletes a user's API key passed through an authorization header, e.g. `Authorization: API-key xyz`. Args: request (HTTPRequest): A request to get the user's API key. """ user_claims = authenticate_request(request) uid = user_claims['uid'] post_data = loads(request.body.decode('utf-8')) # FIXME: Get the name of the desired key to delete. # Delete the key from the users API keys. # Remove the key HMAC by created_at time. # authorization = request.META['HTTP_AUTHORIZATION'] # api_key = authorization.split(' ')[-1] # app_secret = get_document('admin/api')['app_secret_key'] # code = sha256_hmac(app_secret, api_key) # key_data = get_document(f'admin/api/api_key_hmacs/{code}') # uid = key_data['uid'] # delete_document(f'admin/api/api_key_hmacs/{code}') # delete_document(f'users/{uid}/api_key_hmacs/{code}') # return JsonResponse({'status': 'success'}) create_log(f'users/{uid}/logs', user_claims, 'Deleted API key.', 'api_key', 'api_key_delete', [{'deleted_at': datetime.now().isoformat()}]) message = 'Delete API key not yet implemented, will be implemented shortly.' return JsonResponse({'error': True, 'message': message})
def get_signature(request, *args, **argv): #pylint: disable=unused-argument """Get a user's signature given their pin, using a stored hash of the `pin:uid`. Args: request (HTTPRequest): A request to get the user's session. Returns: (JsonResponse): A JSON response containing the user's claims. """ user_claims = authenticate_request(request) uid = user_claims['uid'] post_data = loads(request.body.decode('utf-8')) pin = post_data['pin'] message = f'{pin}:{uid}' app_secret = get_document('admin/api')['app_secret_key'] app_salt = get_document('admin/api')['app_salt'] code = sha256_hmac(app_secret, message + app_salt) verified_claims = get_document(f'admin/api/pin_hmacs/{code}') if not verified_claims: return JsonResponse({'error': True, 'message': 'Invalid pin.'}) elif verified_claims.get('uid') == uid: signature_data = get_document(f'users/{uid}/user_settings/signature') return JsonResponse({ 'success': True, 'message': 'User verified.', 'signature_url': signature_data['signature_url'], 'signature_created_at': signature_data['signature_created_at'] }) else: return JsonResponse({'error': True, 'message': 'Invalid pin.'})
def download_analyses_data(request): """Download analyses data.""" # Define data points. # Optional: Store allowed data points in Firebase? data_points = [ 'analysis_id', 'analytes', 'color', 'name', 'singular', 'units', ] # Get the data file to download if the user is signed in, # otherwise return an error. collection = 'public/data/analyses' claims = authenticate_request(request) temp_name, filename = download_dataset(claims, collection, data_points) if filename is None: response = { 'success': False, 'message': 'Authentication required for suggestion.' } return JsonResponse(response) # Return the file to download. return FileResponse(open(temp_name, 'rb'), filename=filename)
def get_user_subscriptions(request): """Get a user's subscriptions.""" claims = authenticate_request(request) try: user_id = claims['uid'] subscriptions = get_document(f'subscribers/{user_id}') response = {'success': True, 'data': subscriptions} return Response(response, content_type='application/json') except KeyError: response = {'success': False, 'message': 'Invalid authentication.'} return Response(response, content_type='application/json')
def send_message(request): """Send a message from the website to the Cannlytics admin through email. The user must provide an `email`, `subject`, and `message` in their POST. """ try: request.POST['math_input'] == request.POST['math_total'] except KeyError: return JsonResponse({ 'success': False, 'message': 'Bugger off.' }, status=401) claims = authenticate_request(request) uid = claims.get('uid', 'User not signed in.') user_email = claims.get('email', 'Unknown') name = request.POST.get('name', claims.get('name', 'Unknown')) try: subject = request.POST['subject'] message = request.POST['message'] sender = request.POST['email'] except: message = 'An `email`, `subject`, and `message` are required.' return JsonResponse({'success': False, 'message': message}, status=400) recipients = LIST_OF_EMAIL_RECIPIENTS template = 'New message from the Cannlytics website:' \ '\n\n{0}\n\nUser: {0}\nUser Email: {0}\n\nFrom,\n{0}' text = template.format(message, uid, user_email, name) send_mail( subject=subject.strip(), message=text, from_email=sender, recipient_list=recipients, fail_silently=False, ) create_log( ref='logs/website/email', claims=claims, action=f'User ({user_email}) sent the staff an email.', log_type='email', key='send_message', changes={ 'message': message, 'name': name, 'subject': subject, 'uid': uid }, ) response = { 'success': True, 'message': 'Message sent to the Cannlytics staff.' } return JsonResponse(response)
def get_api_key_hmacs(request, *args, **argv): #pylint: disable=unused-argument """Get a user's API key HMAC information. Args: request (HTTPRequest): A request to get the user's HMAC information. Returns: (JsonResponse): A JSON response containing the API key HMAC information in a `data` field. """ user_claims = authenticate_request(request) uid = user_claims['uid'] query = {'key': 'uid', 'operation': '==', 'value': uid} docs = get_collection('admin/api/api_key_hmacs', filters=[query]) return JsonResponse({'status': 'success', 'data': docs})
def download_lab_data(request): """Download lab data.""" # Define data points. # Optional: Store allowed data points in Firebase? data_points = [ 'id', 'name', 'trade_name', 'license', 'license_url', 'license_issue_date', 'license_expiration_date', 'status', 'street', 'city', 'county', 'state', 'zip', 'description', 'formatted_address', 'timezone', 'longitude', 'latitude', 'capacity', 'square_feet', 'brand_color', 'favicon', 'email', 'phone', 'website', 'linkedin', 'image_url', 'opening_hours', 'analyses', ] # Get the data file to download if the user is signed in, # otherwise return an error. collection = 'public/data/labs' claims = authenticate_request(request) temp_name, filename = download_dataset(claims, collection, data_points) if filename is None: response = { 'success': False, 'message': 'Authentication required for suggestion.' } return JsonResponse(response) # Return the file to download. return FileResponse(open(temp_name, 'rb'), filename=filename)
def suggest_edit(request): """Send a data edit suggestion to the staff. The user must be signed into their account to suggest an edit.""" claims = authenticate_request(request) try: uid = claims['uid'] user_email = claims['email'] name = claims.get('name', 'Unknown') except KeyError: response = { 'success': False, 'message': 'Authentication required for suggestion.' } return JsonResponse(response) subject = request.POST.get('subject', 'Cannlytics Website Data Edit Recommendation') recipients = LIST_OF_EMAIL_RECIPIENTS suggestion = request.POST['suggestion'] message = json.dumps(suggestion, sort_keys=True, indent=4) template = 'New data edit recommendation from the Cannlytics website:' \ '\n\n{0}\n\nUser: {0}\nUser Email: {0}\n\nFrom,\n{0}' text = template.format(message, uid, user_email, name) send_mail( subject=subject.strip(), message=text, from_email=user_email, recipient_list=recipients, fail_silently=False, ) create_log( ref='logs/website/suggestions', claims=claims, action= f'User ({user_email}) suggested a data edit to the staff in an email.', log_type='email', key='suggest_edit', changes={ 'message': message, 'name': name, 'subject': subject, 'uid': uid }, ) response = { 'success': True, 'message': 'Data edit suggestion sent to the staff.' } return JsonResponse(response)
def delete_user_pin(request, *args, **argv): #pylint: disable=unused-argument """Delete all pins for a given user, removing the data stored with their hash. Args: request (HTTPRequest): A request to get the user's session. Returns: (JsonResponse): A JSON response containing the API key in an `api_key` field. """ user_claims = authenticate_request(request) uid = user_claims['uid'] query = {'key': 'uid', 'operation': '==', 'value': uid} existing_pins = get_collection('admin/api/pin_hmacs', filters=[query]) for pin in existing_pins: code = pin['hmac'] delete_document(f'admin/api/pin_hmacs/{code}') delete_field(f'users/{uid}', 'pin_created_at') create_log(f'users/{uid}/logs', user_claims, 'Deleted pin.', 'pin', 'pin_delete', [{'deleted_at': datetime.now().isoformat()}]) return JsonResponse({'success': True, 'message': 'User pin deleted.'})
def delete_signature(request, *args, **argv): #pylint: disable=unused-argument """Delete a user's signature. Args: request (HTTPRequest): A request to get the user's session. Returns: (JsonResponse): A JSON response containing the user's claims. """ user_claims = authenticate_request(request) uid = user_claims['uid'] entry = { 'signature_created_at': '', 'signature_url': '', 'signature_ref': '', } delete_file(BUCKET_NAME, f'users/{uid}/user_settings/signature.png') update_document(f'users/{uid}', entry) update_document(f'users/{uid}/user_settings/signature', entry) create_log(f'users/{uid}/logs', user_claims, 'Deleted signature.', 'signature', 'signature_delete', [{'deleted_at': datetime.now().isoformat()}]) return JsonResponse({'success': True, 'message': 'Signature deleted.'})
def get_user_data(request: Any, context: dict) -> dict: """Get user-specific context. Args: request (HTTPRequest): A request to check for a user session. context (dict): Existing page context. Returns context (dict): Page context updated with any user-specific context. """ claims = authenticate_request(request) try: uid = claims['uid'] query = {'key': 'team', 'operation': 'array_contains', 'value': uid} organizations = get_collection('organizations', filters=[query]) user_data = get_document(f'users/{uid}') context['organizations'] = organizations context['user'] = {**claims, **user_data} except KeyError: context['organizations'] = [] context['user'] = {} return context
def logout(request): """Functional view to remove a user session.""" claims = authenticate_request(request) try: uid = claims['uid'] update_document(f'users/{uid}', {'signed_in': False}) create_log(ref=f'users/{uid}/logs', claims=claims, action='Signed out.', log_type='auth', key='logout') revoke_refresh_tokens(claims['sub']) response = JsonResponse({'success': True}, status=200) response['Set-Cookie'] = '__session=None; Path=/' request.session['__session'] = '' return response except KeyError: response = JsonResponse({'success': False}, status=205) response['Set-Cookie'] = '__session=None; Path=/' request.session['__session'] = '' return response
def verify_user_pin(request, *args, **argv): #pylint: disable=unused-argument """Verify a pin for a given user, using a stored hash of the `pin:uid`. Args: request (HTTPRequest): A request to get the user's session. Returns: (JsonResponse): A JSON response containing the user's claims. """ user_claims = authenticate_request(request) uid = user_claims['uid'] post_data = loads(request.body.decode('utf-8')) pin = post_data['pin'] message = f'{pin}:{uid}' app_secret = get_document('admin/api')['app_secret_key'] app_salt = get_document('admin/api')['app_salt'] code = sha256_hmac(app_secret, message + app_salt) verified_claims = get_document(f'admin/api/pin_hmacs/{code}') if verified_claims.get('uid') == uid: token = create_custom_token(uid, claims={'pin_verified': True}) return JsonResponse({'success': True, 'message': 'User verified.', 'token': token}) else: return JsonResponse({'error': True, 'message': 'Invalid pin.'})
def create_api_key(request, *args, **argv): #pylint: disable=unused-argument """Mint an API key for a user, granting programmatic use at the same level of permission as the user. Args: request (HTTPRequest): A request to get the user's session. Returns: (JsonResponse): A JSON response containing the API key in an `api_key` field. """ user_claims = authenticate_request(request) uid = user_claims['uid'] api_key = token_urlsafe(48) app_secret = get_document('admin/api')['app_secret_key'] app_salt = get_document('admin/api')['app_salt'] code = sha256_hmac(app_secret, api_key + app_salt) post_data = loads(request.body.decode('utf-8')) now = datetime.now() expiration_at = post_data['expiration_at'] try: expiration_at = datetime.fromisoformat(expiration_at) except: expiration_at = datetime.strptime(expiration_at, '%m/%d/%Y') if expiration_at - now > timedelta(365): expiration_at = now + timedelta(365) key_data = { 'created_at': now.isoformat(), 'expiration_at': expiration_at.isoformat(), 'name': post_data['name'], 'permissions': post_data['permissions'], 'uid': uid, 'user_email': user_claims['email'], 'user_name': user_claims.get('name', 'No Name'), } update_document(f'admin/api/api_key_hmacs/{code}', key_data) update_document(f'users/{uid}/api_key_hmacs/{code}', key_data) create_log(f'users/{uid}/logs', user_claims, 'Created API key.', 'api_key', 'api_key_create', [key_data]) return JsonResponse({'status': 'success', 'api_key': api_key})
def create_signature(request, *args, **argv): #pylint: disable=unused-argument """Save a signature for a user, given their pin. Args: request (HTTPRequest): A request to get the user's session. Returns: (JsonResponse): A JSON response with a success message. """ user_claims = authenticate_request(request) uid = user_claims['uid'] post_data = loads(request.body.decode('utf-8')) data_url = post_data['data_url'] ref = f'admin/auth/{uid}/user_settings/signature.png' upload_file(BUCKET_NAME, ref, data_url=data_url) url = get_file_url(ref, bucket_name=BUCKET_NAME) signature_created_at = datetime.now().isoformat() signature_data = { 'signature_created_at': signature_created_at, 'signature_url': url, 'signature_ref': ref, } update_document(f'admin/auth/{uid}/user_settings', signature_data) update_document(f'users/{uid}/user_settings/signature', signature_data) create_log(f'users/{uid}/logs', user_claims, 'Created signature.', 'signature', 'signature_create', [{'created_at': signature_created_at}]) return JsonResponse({'success': True, 'message': 'Signature saved.', 'signature_url': url})
def create_user_pin(request, *args, **argv): #pylint: disable=unused-argument """Using a pin for a given user, create and store a hash of the `pin:uid`. Args: request (HTTPRequest): A request to get the user's session. Returns: (JsonResponse): A JSON response with a success message. """ user_claims = authenticate_request(request) uid = user_claims['uid'] post_data = loads(request.body.decode('utf-8')) pin = post_data['pin'] message = f'{pin}:{uid}' app_secret = get_document('admin/api')['app_secret_key'] app_salt = get_document('admin/api')['app_salt'] code = sha256_hmac(app_secret, message + app_salt) post_data = loads(request.body.decode('utf-8')) now = datetime.now() # Optional: Add expiration to pins user_claims['hmac'] = code delete_user_pin(request) update_document(f'admin/api/pin_hmacs/{code}', user_claims) update_document(f'users/{uid}', {'pin_created_at': now.isoformat() }) create_log(f'users/{uid}/logs', user_claims, 'Created pin.', 'pin', 'pin_create', [{'created_at': now}]) return JsonResponse({'success': True, 'message': 'Pin successfully created.'})
def download_regulation_data(request): """Download regulation data.""" # Define data points. # Optional: Store allowed data points in Firebase? data_points = [ 'state', 'state_name', 'traceability_system', 'adult_use', 'adult_use_permitted', 'adult_use_permitted_source', 'medicinal', 'medicinal_permitted', 'medicinal_permitted_source', 'state_sales_tax', 'state_excise_tax', 'state_local_tax', 'tax_rate_url', 'sources', ] # Get the data file to download if the user is signed in, # otherwise return an error. collection = 'public/data/regulations' claims = authenticate_request(request) temp_name, filename = download_dataset(claims, collection, data_points) if filename is None: response = { 'success': False, 'message': 'Authentication required for suggestion.' } return JsonResponse(response) # Return the file to download. return FileResponse(open(temp_name, 'rb'), filename=filename)
def subscribe(request): """Subscribe a user to newsletters, sending them a notification with the ability to unsubscribe. Creates a Cannlytics account and sends a welcome email if the user does not have an account yet. """ # Ensure that the user has a valid email. data = loads(request.body) try: user_email = data['email'] validate_email(user_email) except ValidationError: response = { 'success': False, 'message': 'Invalid email in request body.' } return JsonResponse(response) # Create a promo code that can be used to download data. promo_code = get_promo_code(8) add_to_array('promos/data', 'promo_codes', promo_code) # Record the subscription in Firestore. now = datetime.now() iso_time = now.isoformat() data['created_at'] = iso_time data['updated_at'] = iso_time data['promo_code'] = promo_code update_document(f'subscribers/{user_email}', data) # Save the user's subscription. plan_name = data['plan_name'] try: claims = authenticate_request(request) uid = claims['uid'] user_data = {'support': True} if plan_name == 'newsletter': user_data['newsletter'] = True else: user_data[f'{plan_name}_subscription_id'] = data['id'] update_document(f'users/{uid}', user_data) except KeyError: pass # Create an account if one does not exist. # Optional: Load messages from state? try: name = (data.get('first_name', '') + data.get('last_name', '')).strip() _, password = create_user(name, user_email) message = f'Congratulations,\n\nYou can now login to the Cannlytics console (https://console.cannlytics.com) with the following credentials.\n\nEmail: {user_email}\nPassword: {password}\n\nAlways here to help,\nThe Cannlytics Team' #pylint: disable=line-too-long subject = 'Welcome to the Cannlytics Platform' except: message = f'Congratulations,\n\nYou are now subscribed to Cannlytics.\n\nPlease stay tuned for more material or email {DEFAULT_FROM_EMAIL} to begin.\n\nAlways here to help,\nThe Cannlytics Team' #pylint: disable=line-too-long subject = 'Welcome to the Cannlytics Newsletter' # Send a welcome / thank you email. # (Optional: Use HTML template.) # template_url = 'website/emails/newsletter_subscription_thank_you.html' send_mail( subject=subject, message=message, from_email=DEFAULT_FROM_EMAIL, recipient_list=[user_email, DEFAULT_FROM_EMAIL], fail_silently=False, # html_message = render_to_string(template_url, {'context': 'values'}) ) # Create an activity log. create_log( ref='logs/website/subscriptions', claims=claims, action=f'User ({user_email}) subscribed to {plan_name}.', log_type='subscription', key='subscribe', changes=data, ) # Return a success message. response = {'success': True, 'message': 'User successfully subscribed.'} return JsonResponse(response)
def unsubscribe(request): """Unsubscribe a user from a PayPal subscription.""" # Authenticate the user. claims = authenticate_request(request) try: uid = claims['uid'] user_email = claims['email'] except KeyError: response = {'success': False, 'message': 'Unable to authenticate.'} return JsonResponse(response) # Get the subscription they wish to unsubscribe from. data = loads(request.body) plan_name = data['plan_name'] # Unsubscribe the user with the PayPal SDK. try: project_id = os.environ['GOOGLE_CLOUD_PROJECT'] payload = access_secret_version(project_id, 'paypal', 'latest') paypal_secrets = loads(payload) paypal_client_id = paypal_secrets['client_id'] paypal_secret = paypal_secrets['secret'] paypal_access_token = get_paypal_access_token(paypal_client_id, paypal_secret) user_data = get_document(f'users/{uid}') subscription_id = user_data[f'{plan_name}_subscription_id'] cancel_paypal_subscription(paypal_access_token, subscription_id) updated_user_data = {'support': False} updated_user_data[f'{plan_name}_subscription_id'] = '' update_document(f'users/{uid}', updated_user_data) except: subscription_id = 'Unidentified' # Notify the staff. staff_message = """Confirm that the following subscription has been canceled: User: {} Email: {} Plan: {} Subscription ID: {} """.format(uid, user_email, plan_name, subscription_id) send_mail( subject='User unsubscribed from a PayPal subscription.', message=staff_message, from_email=DEFAULT_FROM_EMAIL, recipient_list=[DEFAULT_FROM_EMAIL], fail_silently=False, ) # Create an activity log. create_log( ref='logs/website/subscriptions', claims=claims, action=f'User ({user_email}) unsubscribed from {plan_name}.', log_type='subscription', key='subscribe', changes=data, ) # Return a success message. message = 'Successfully unsubscribed from subscription.' response = {'success': True, 'message': message} return JsonResponse(response)
def buy_data(request): """Facilitate the purchase of a dataset on the data market.""" # Ensure that the user has a valid email. data = loads(request.body) try: user_email = data['email'] validate_email(user_email) except ValidationError: response = { 'success': False, 'message': 'Invalid email in request body.' } return JsonResponse(response) # Check if the payment ID is valid. # FIXME: Make this required. try: payment_id = data['payment_id'] print('Checking payment ID:', payment_id) project_id = os.environ['GOOGLE_CLOUD_PROJECT'] payload = access_secret_version(project_id, 'paypal', 'latest') paypal_secrets = loads(payload) paypal_client_id = paypal_secrets['client_id'] paypal_secret = paypal_secrets['secret'] paypal_access_token = get_paypal_access_token(paypal_client_id, paypal_secret) payment = get_paypal_payment(paypal_access_token, payment_id) assert payment['id'] == payment_id print('Payment ID matched captured payment ID.') except: pass # Future work: Ensure that the user has a .edu email for student discount? # Get the dataset zipped folder. dataset = data['dataset'] file_name = dataset['file_name'] file_ref = dataset['file_ref'] download_url = get_file_url(file_ref) # Optional: Allow for specifying suffix options. short_url = create_short_url(FIREBASE_API_KEY, download_url, FIREBASE_PROJECT_ID) data['download_url'] = download_url data['short_url'] = short_url # Keep track of a user's downloaded data if the user is signed in. now = datetime.now() iso_time = now.isoformat() data['created_at'] = iso_time data['updated_at'] = iso_time try: claims = authenticate_request(request) uid = claims['uid'] update_document(f'users/{uid}/datasets', {**data, **{'uid': uid}}) except KeyError: pass # Optional: Read the email template from storage? # Optional: Use HTML template. # Optional: Load messages from state? # # template_url = 'website/emails/newsletter_subscription_thank_you.html' # Optional: Actually attach the datafile (too large a file problem?) # Email the data to the user. message = f'Congratulations on your new dataset,\n\nYou can access your data with the following link:\n\n{short_url}\n\nYou can monitor the market for new datasets.\n\nAlways here to help,\nThe Cannlytics Team' #pylint: disable=line-too-long subject = 'Dataset Purchased - Your Dataset is Attached' send_mail( subject=subject, message=message, from_email=DEFAULT_FROM_EMAIL, recipient_list=[user_email, DEFAULT_FROM_EMAIL], fail_silently=False, # html_message = render_to_string(template_url, {'context': 'values'}) ) # Create an activity log. create_log( ref='logs/website/payments', claims=claims, action=f'User ({user_email}) bought a dataset.', log_type='market', key='buy_data', changes=data, ) # Return the file to download. return FileResponse(open(download_url, 'rb'), filename=file_name)
def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) # Get video statistics. video_stats = get_document('public/videos') total_videos = video_stats['total'] # Get data for a specific video if a video ID is specified. video_id = self.kwargs.get('video_id', '') if video_id: context['video_data'] = get_document( f'public/videos/video_data/{video_id}') # Get more videos. more_videos = [] try: while len(more_videos) < 3: random_number = randint(1, total_videos) if random_number == context['video_data']['number']: continue random_video = get_collection('public/videos/video_data', limit=1, order_by='number', desc=True, start_at={ 'key': 'number', 'value': random_number }) more_videos = [*more_videos, *random_video] except: pass # Get recent videos. try: context['recent_videos'] = get_collection( 'public/videos/video_data', limit=3, order_by='number', desc=True, start_at={ 'key': 'number', 'value': total_videos + 1 }) context['more_videos'] = more_videos except: pass # Look-up if user has a subscription for premium videos. if context.get('premium'): claims = authenticate_request(self.request) try: uid = claims.get('uid') context['user_id'] = uid user_data = get_document(f'users/{uid}') premium_subscription = user_data.get( 'premium_subscription_id') context['premium_subscription'] = premium_subscription except KeyError: pass # Return context for a specific video. return context # If there is no singular video specified, then paginate videos. limit = 9 page = self.request.GET.get('page', 1) start_at = 1 + total_videos - (int(page) - 1) * limit context['page_index'] = range(ceil(total_videos / 10)) context['last_page'] = str(context['page_index'][-1] + 1) context['video_archive'] = get_collection('public/videos/video_data', limit=limit, order_by='number', desc=True, start_at={ 'key': 'number', 'value': start_at }) return context