def logout(**params): # logout should accept POST only (don't do logout via GET, to avoid pre-fetching issues in browsers) if request.method == "POST": # get the session token from the cookie session_token = request.cookies.get("my-web-app-session") # get the hash of the session token token_hash = hashlib.sha256(str.encode(session_token)).hexdigest() # delete the session token from the User object User.delete_session(user=params["user"], token_hash_five_chars=token_hash[:5]) # prepare the response response = make_response(redirect(url_for("public.main.index"))) # on localhost don't make the cookie secure and http-only (but on production it should be) cookie_secure_httponly = False if not is_local(): cookie_secure_httponly = True # set the session cookie to an empty value (similar to deleting the cookie) response.set_cookie(key="my-web-app-session", value="", secure=cookie_secure_httponly, httponly=cookie_secure_httponly) return response
def validate_magic_login_link(token, **params): if request.method == "GET": success, result = User.validate_magic_login_token(magic_token=token, request=request) if success: # result is session token, store it in a cookie # prepare a response and then store the token in a cookie response = make_response( redirect(url_for("profile.main.sessions_list"))) # on localhost don't make the cookie secure and http-only (but on production it should be) cookie_secure_httponly = False if not is_local(): cookie_secure_httponly = True # store the token in a cookie response.set_cookie(key="my-web-app-session", value=result, secure=cookie_secure_httponly, httponly=cookie_secure_httponly) return response else: # result is an error message return abort(403, description=result)
def send_email(recipient_email, email_template, email_params, email_subject, sender_email=None, sender_name=None, unsubscribe_group=None, attachment_content_b64=None, attachment_filename=None, attachment_filetype=None): if not sender_email: sender_email = os.environ.get("MY_APP_EMAIL") # set this in app.yaml if not sender_name: sender_name = os.environ.get("MY_APP_NAME") # set this in app.yaml # send web app URL data by default to every email template if is_local(): email_params["app_root_url"] = "http://localhost:8080" else: email_params["app_root_url"] = os.environ.get("MY_APP_URL") # set this in app.yaml email_params["my_app_name"] = os.environ.get("MY_APP_NAME") # render the email HTML body email_body = render_template_with_translations(email_template, **email_params) # params sent to the background task payload = {"recipient_email": recipient_email, "email_subject": email_subject, "sender_email": sender_email, "email_body": email_body, "unsubscribe_group": unsubscribe_group, "sender_name": sender_name, "attachment_content_b64": attachment_content_b64, "attachment_filename": attachment_filename, "attachment_filetype": attachment_filetype} run_background_task(relative_path=url_for("tasks.send_email_task.send_email_via_sendgrid"), payload=payload, queue="email", project=os.environ.get("GOOGLE_CLOUD_PROJECT"), location=os.environ.get("MY_GAE_REGION"))
def reset_password_enter_email(**params): if request.method == "GET": return render_template_with_translations( "public/auth/reset_password_enter_email.html", **params) elif request.method == "POST": email_address = request.form.get("reset-password-email") locale = get_locale( ) # get the language that the user currently uses on the website success, message = User.password_reset_link_send( email_address=email_address, locale=locale) if success: # Delete the current session cookie (if it exists) response = make_response( render_template_with_translations( "public/auth/reset_password_link_sent.html", **params)) # on localhost don't make the cookie secure and http-only (but on production it should be) cookie_secure_httponly = False if not is_local(): cookie_secure_httponly = True # set the session cookie value to an empty value which effectively "deletes" it response.set_cookie(key="my-web-app-session", value="", secure=cookie_secure_httponly, httponly=cookie_secure_httponly) return response else: return abort(403, description=message)
def login_via_password(**params): if request.method == "GET": return render_template_with_translations( "public/auth/login_password.html", **params) elif request.method == "POST": email_address = request.form.get("login-email") password = request.form.get("login-password") success, result = User.validate_password_login( email_address=email_address, password=password, request=request) if success: # result is session token, store it in a cookie # prepare a response and then store the token in a cookie response = make_response( redirect(url_for("profile.main.my_details"))) # on localhost don't make the cookie secure and http-only (but on production it should be) cookie_secure_httponly = False if not is_local(): cookie_secure_httponly = True # store the token in a cookie response.set_cookie(key="my-web-app-session", value=result, secure=cookie_secure_httponly, httponly=cookie_secure_httponly) return response else: # result is an error message return abort(403, description=result)
def change_email_link_validate(token, **params): # when user changes their own email address and confirms the change by clicking the link received via email, this # is the handler that does the token validation process if request.method == "GET": success, result = User.validate_change_email_token(token=token, request=request) if success: # result is session token, store it in a cookie # prepare a response and then store the token in a cookie response = make_response( redirect(url_for("profile.main.my_details"))) # on localhost don't make the cookie secure and http-only (but on production it should be) cookie_secure_httponly = False if not is_local(): cookie_secure_httponly = True # store the token in a cookie response.set_cookie(key="my-web-app-session", value=result, secure=cookie_secure_httponly, httponly=cookie_secure_httponly) return response else: # result is an error message return abort(403, description=result)
def _test_set_password_reset_token(cls, user, token): """FOR TESTING PURPOSES ONLY!""" with client.context(): if is_local(): user.password_reset_token_hash = hashlib.sha256( str.encode(token)).hexdigest() user.password_reset_token_expired = datetime.datetime.now( ) + datetime.timedelta(hours=3) user.put()
def _test_mark_email_verified(cls, user): """ FOR TESTING PURPOSES ONLY! :param user: :return: """ with client.context(): if is_local(): user.email_address_verified = True user.put()
def _test_change_deleted_date(cls, user, new_date): """ FOR TESTING PURPOSES ONLY! :param user: :param new_date: :return: """ with client.context(): if is_local(): user.deleted_date = new_date user.put()
def fetch_suspended(cls, email_address_verified=True, limit=None, cursor=None): with client.context(): users, next_cursor, more = cls.query( cls.email_address_verified == email_address_verified, cls.suspended == True).fetch_page(limit, start_cursor=cursor) if is_local(): # this fixes the pagination bug which returns more=True even if less users than limit or if next_cursor # is the same as the cursor. This happens on localhost only. if limit and len(users) < limit: return users, None, False try: return users, next_cursor.urlsafe().decode(), more except AttributeError as e: # if there's no next_cursor, an AttributeError will occur return users, None, False
def run_background_task(relative_path, payload, project=None, queue=None, location=None): if is_local(): if os.environ.get( "TESTING") != "yes": # pytest has issues with running requests requests.post("http://localhost:8080{relative_path}".format( relative_path=relative_path), data=json.dumps(payload).encode(), headers={"Content-type": "application/octet-stream"}) else: # production if not project: project = os.environ.get( "GOOGLE_CLOUD_PROJECT" ) # this is a default environment variable on GAE if not queue: queue = "default" if not location: location = os.environ.get("MY_GAE_REGION") # make sure you have Cloud Tasks API enabled via the Google Cloud Console client = tasks_v2.CloudTasksClient() # Construct the fully qualified queue name. parent = client.queue_path(project, location, queue) task = { 'app_engine_http_request': { 'http_method': 'POST', 'relative_uri': relative_path, 'body': json.dumps(payload).encode(), } } client.create_task(parent, task)
def load_fake_data(): # handler to load fake data for localhost development usage only if is_local(): result, user_1, message = User.create( email_address="user_{}@my.webapp".format(1), admin=False, first_name="Jim", last_name="Jones") result, user_2, message = User.create( email_address="user_{}@my.webapp".format(2), admin=True, first_name="Betty", last_name="Beam") result, user_3, message = User.create( email_address="user_{}@my.webapp".format(3), admin=False, first_name="Cindy", last_name="Crawford") result, user_4, message = User.create( email_address="user_{}@my.webapp".format(4), admin=False, first_name="Damian", last_name="Dante") result, user_5, message = User.create( email_address="user_{}@my.webapp".format(5), admin=False, first_name="Erica", last_name="Enter") result, user_6, message = User.create( email_address="user_{}@my.webapp".format(6), admin=False, first_name="Fatima", last_name="Fowles") result, user_7, message = User.create( email_address="user_{}@my.webapp".format(7), admin=False, first_name="George", last_name="Garrett") result, user_8, message = User.create( email_address="user_{}@my.webapp".format(8), admin=True, first_name="Harriet", last_name="Ham") result, user_9, message = User.create( email_address="user_{}@my.webapp".format(9), admin=False, first_name="Ian", last_name="Ilich") result, user_10, message = User.create( email_address="user_{}@my.webapp".format(10), admin=False, first_name="Jane", last_name="James") result, user_11, message = User.create( email_address="user_{}@my.webapp".format(11), admin=False, first_name="Ken", last_name="Klingon") result, user_12, message = User.create( email_address="user_{}@my.webapp".format(12), admin=False, first_name="Lana", last_name="Lubbards") result, user_13, message = User.create( email_address="user_{}@my.webapp".format(13), admin=False, first_name="Matt", last_name="Morata") result, user_14, message = User.create( email_address="user_{}@my.webapp".format(14), admin=False, first_name="Nika", last_name="Norante") result, user_15, message = User.create( email_address="user_{}@my.webapp".format(15), admin=False, first_name="Omar", last_name="Orange") result, user_16, message = User.create( email_address="user_{}@my.webapp".format(15), admin=False, first_name="Peter", last_name="Pan") # mark most of emails as verified User._test_mark_email_verified(user=user_1) User._test_mark_email_verified(user=user_2) User._test_mark_email_verified(user=user_3) User._test_mark_email_verified(user=user_4) User._test_mark_email_verified(user=user_5) User._test_mark_email_verified(user=user_6) User._test_mark_email_verified(user=user_7) User._test_mark_email_verified(user=user_8) User._test_mark_email_verified(user=user_9) User._test_mark_email_verified(user=user_10) User._test_mark_email_verified(user=user_11) User._test_mark_email_verified(user=user_13) User._test_mark_email_verified(user=user_14) User._test_mark_email_verified(user=user_16) return redirect(url_for("public.main.index"))
methods=["GET"]) app.add_url_rule(rule="/profile/session/delete", endpoint="profile.main.session_delete", view_func=profile_main.session_delete, methods=["POST"]) # PROFILE auth app.add_url_rule(rule="/logout", endpoint="profile.auth.logout", view_func=logout, methods=["POST"]) # ADMIN URLS app.add_url_rule(rule="/admin/users", endpoint="admin.users.users_list", view_func=users.users_list, methods=["GET", "POST"]) app.add_url_rule(rule="/admin/user/<user_id>", endpoint="admin.users.user_details", view_func=users.user_details, methods=["GET"]) # CRON JOBS app.add_url_rule(rule="/cron/remove-deleted-users", view_func=remove_deleted_users_cron, methods=["GET"]) # TASKS app.add_url_rule(rule="/tasks/send-email", endpoint="tasks.send_email_task.send_email_via_sendgrid", view_func=send_email_via_sendgrid, methods=["POST"]) # LOAD FAKE DATA (localhost only!) if is_local(): app.add_url_rule(rule="/load-fake-data", view_func=load_fake_data, methods=["GET"]) if __name__ == '__main__': if is_local(): app.run(port=8080, host="localhost", debug=True) # localhost else: app.run(debug=False) # production
def send_email_via_sendgrid(): """A background task that sends an email via SendGrid.""" data = json.loads(request.get_data(as_text=True)) recipient_email = data.get("recipient_email") sender_email = data.get("sender_email") sender_name = data.get("sender_name") email_subject = data.get("email_subject") email_body = data.get("email_body") unsubscribe_group = data.get("unsubscribe_group") attachment_content_b64 = data.get("attachment_content_b64") attachment_filename = data.get("attachment_filename") attachment_filetype = data.get("attachment_filetype") if is_local(): # localhost (not really sending the email) logging.warning( "SEND EMAIL: Not really sending email because we're on localhost.") logging.warning("Recipient: {}".format(recipient_email)) logging.warning("Sender: {0}, {1}".format(sender_name, sender_email)) logging.warning("Subject: {}".format(email_subject)) logging.warning("Body: {}".format(email_body)) return "{sender_email} {email_subject}".format( sender_email=sender_email, email_subject=email_subject) else: # production (sending the email via SendGrid) if request.headers.get("X-AppEngine-QueueName"): # If the request has this header (X-AppEngine-QueueName), then it really came from Google Cloud Tasks. # Third-party requests that contain headers started with X are stripped of these headers once they hit GAE # servers. That's why no one can fake these headers. # SendGrid setup sg = SendGridAPIClient(api_key=AppSettings.get().sendgrid_api_key) # Set up email message email_message = Mail(from_email=mail.Email(email=sender_email, name=sender_name), to_emails=recipient_email, subject=email_subject, html_content=email_body) if attachment_content_b64 and attachment_content_b64 is not None and attachment_content_b64 != "": attachment = Attachment() attachment.content = attachment_content_b64 attachment.type = "text/{}".format(attachment_filetype) attachment.filename = attachment_filename attachment.disposition = "attachment" email_message.add_attachment(attachment) # Unsubscribe group (ASM) if unsubscribe_group: try: email_message.asm(Asm(group_id=int(unsubscribe_group))) except Exception as e: pass try: response = sg.send(email_message) logging.info(response.status_code) logging.info(response.body) logging.info(response.headers) except Exception as e: logging.error(str(e)) return "true"