diff --git a/CodeChallenge/api/eb.py b/CodeChallenge/api/eb.py index f2d3c14..9ea0c3f 100644 --- a/CodeChallenge/api/eb.py +++ b/CodeChallenge/api/eb.py @@ -1,4 +1,10 @@ -from flask import Blueprint +from hmac import compare_digest + +from flask import Blueprint, request, current_app, render_template +from flask_mail import Message + +from .. import core +from ..mail import mail bp = Blueprint("awsebapi", __name__, url_prefix="/api/v1/eb") @@ -7,3 +13,32 @@ @bp.route("/health", methods=["GET"]) def eb_health_check(): return "OK", 200 + + +# POST request from an AWS Lambda function once per day +# any daily tasks should be placed here +@bp.route("/worker", methods=["POST"]) +def worker(): + try: + password = request.json["password"] + except (TypeError, KeyError): + return "", 400 + + if not compare_digest(password, + current_app.config["WORKER_PASSWORD"]): + return "", 401 + + # send daily reminder emails only while challenge is active + + if core.day_number() >= 1 and not core.challenge_ended(): + msg = Message("New code challenge question is unlocked!", + sender=current_app.config["MAIL_DEFAULT_SENDER"], + recipients=[current_app.config["MG_LIST"]]) + + msg.html = render_template("challenge_daily_email.html", + name="%recipient_fname%") + msg.extra_headers = {"List-Unsubscribe": "%unsubscribe_email%"} + + mail.send(msg) + + return "", 200 diff --git a/CodeChallenge/api/users.py b/CodeChallenge/api/users.py index a18557e..cb7f754 100644 --- a/CodeChallenge/api/users.py +++ b/CodeChallenge/api/users.py @@ -134,14 +134,33 @@ def register(): f"{new_u.studentfirstname} {new_u.studentlastname}", data=mg_vars) - msg = Message("Welcome Pilgrim! You have accepted the Code Challenge", - sender=current_app.config["MAIL_DEFAULT_SENDER"], - recipients=[new_u.parent_email]) + rcpts = [new_u.parent_email] + if new_u.student_email: + rcpts.append(new_u.student_email) + + # account confirmation email + # only contains login/password + confirm_email = Message("Your Code Challenge Account", + sender=current_app.config["MAIL_DEFAULT_SENDER"], + recipients=rcpts) name = new_u.studentfirstname or new_u.parentfirstname - msg.html = render_template("challenge_account_confirm.html", - name=name) - msg.extra_headers = {"List-Unsubscribe": "%unsubscribe_email%"} - mail.send(msg) + confirm_email.html = render_template("challenge_account_confirm.html", + name=name, + username=new_u.username, + password=password) + confirm_email.extra_headers = {"List-Unsubscribe": "%unsubscribe_email%"} + + # welcome email + # more in depth + welcome_email = Message("Welcome Pilgrim! You have accepted the Code Challenge", + sender=current_app.config["MAIL_DEFAULT_SENDER"], + recipients=rcpts) + welcome_email.html = render_template("challenge_welcome.html", name=name) + welcome_email.extra_headers = {"List-Unsubscribe": "%unsubscribe_email%"} + + # send emails + mail.send(confirm_email) + mail.send(welcome_email) return jsonify({"status": "success"}) @@ -176,29 +195,33 @@ def forgot_password(): return jsonify(status="error", reason="no account with that email"), 400 - for user in users: + multiple_accounts = len(users) > 1 + + for user in users: # type: Users token = password_reset_token(user) - msg = Message(subject="Password Reset", - body="You are receiving this message because a password " - "reset request has been issued for your account. If you " - "did not make this request, you can ignore this email. " - "To reset your password, use this link within 24 hours. " - f"\n\n{current_app.config['EXTERNAL_URL']}/reset-password/{token}" - f"\n\nAccount Username: {user.username}", - recipients=[user.parent_email]) + + rcpts = [user.parent_email] + if user.student_email: + rcpts.append(user.student_email) + + if user.studentfirstname: + name = user.studentfirstname + else: + name = user.username + + msg = Message(subject="Reset your Code Challenge password", + html=render_template("challenge_password_reset.html", + name=name, + token=token, + multiple=multiple_accounts), + recipients=rcpts) if current_app.config.get("TESTING", False): msg.extra_headers = {"X-Password-Reset-Token": token} - if len(users) > 1: - msg.body += "\n\nNOTICE: Your email address matched multiple " \ - "student accounts. Double check to make sure you " \ - "are resetting the intended account, as an email " \ - "was sent for all matching accounts." - mail.send(msg) - return jsonify(status="success", reason="password reset email sent", multiple=len(users) > 1) + return jsonify(status="success", reason="password reset email sent", multiple=multiple_accounts) @bp.route("/reset-password", methods=["POST"]) diff --git a/CodeChallenge/config.py b/CodeChallenge/config.py index 6bfca97..1434492 100644 --- a/CodeChallenge/config.py +++ b/CodeChallenge/config.py @@ -29,6 +29,7 @@ class DefaultConfig: MAIL_SUPPRESS_SEND = True MG_PRIVATE_KEY = os.getenv("MG_PRIVATE_KEY") MG_LIST = "codechallenge@school.codewizardshq.com" + WORKER_PASSWORD = os.getenv("WORKER_PASSWORD") # no trailing / EXTERNAL_URL = "https://challenge.codewizardshq.com" diff --git a/CodeChallenge/templates/challenge_account_confirm.html b/CodeChallenge/templates/challenge_account_confirm.html index 2dcffd7..c7c516d 100644 --- a/CodeChallenge/templates/challenge_account_confirm.html +++ b/CodeChallenge/templates/challenge_account_confirm.html @@ -3,9 +3,9 @@ -CodeWizardsHQ Code Challenge Welcome Email +CodeWizardsHQ Code Challenge Daily Email + + + + + + + + + + +
+
+ + + + + + + + + + +
+ +
+ + + + + + + +
+

{{name}}, today's code challenge question is unlocked!

+
+

Continue your quest by logging in.

+

Good luck and safe travels!

+
+ ANSWER TODAY'S QUESTION +
+

Sign in to play
+ Challenge Details
+ Prizes
+ Frequently Asked Questions

+
+
+ Share With Friends +
+ + + +
+ +
+
+ + + diff --git a/CodeChallenge/templates/challenge_password_reset.html b/CodeChallenge/templates/challenge_password_reset.html new file mode 100644 index 0000000..9f79cad --- /dev/null +++ b/CodeChallenge/templates/challenge_password_reset.html @@ -0,0 +1,333 @@ + + + + + +CodeWizardsHQ Code Challenge Password Reset + + + + + + + + + + + +
+
+ + + + + + + +
+ +
+ + + + + + + +
+

{{name}}, reset your Code Challenge password.

+
+

Reset Your Password

+
+

We received a request to reset your password. If you didn't ask to change your passsword, your password is still safe and you can delete this message.

+ {% if multiple %} +
+

+ Your email address matched multiple student accounts. Double check to make sure you are resetting the intended account, as an email was sent for all matching accounts +

+ {% endif %} +
+

Sign in to play
+ Challenge Details
+ Prizes
+ Frequently Asked Questions

+
+
+
+
+ + + diff --git a/CodeChallenge/templates/challenge_welcome.html b/CodeChallenge/templates/challenge_welcome.html new file mode 100644 index 0000000..e3aa5ab --- /dev/null +++ b/CodeChallenge/templates/challenge_welcome.html @@ -0,0 +1,337 @@ + + + + + +CodeWizardsHQ Code Challenge Welcome Email + + + + + + + + + + + +
+
+ + + + + + + + + + +
+ +
+ + + + + + + +
+

Welcome to Defeat the Dragon, {{name}}!

+
+

Your mission is to defeat the evil dragon who has invaded CWHQ land. Only the bravest and brightest kid coders, like you, are prepared for this quest.

+

Good luck and safe travels!

+
+ START YOUR JOURNEY +
+

Sign in to play
+ Challenge Details
+ Prizes
+ Frequently Asked Questions

+
+
+ Share With Friends +
+ + + +
+
+
+ + + diff --git a/tests/test_auth.py b/tests/test_auth.py index b498ef1..7924b4c 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -16,8 +16,9 @@ def test_registration_success(client): retval = register(client, "sam@codewizardshq.com", "cwhqsam", "supersecurepassword", "Sam", "Hoffman") assert retval.get_json()["status"] == "success" - assert len(outbox) == 1 - assert "Sheldon, you've accepted" in outbox[0].html + assert len(outbox) == 2 + assert "Sheldon, your account has been created" in outbox[0].html + assert "Welcome to Defeat the Dragon, Sheldon!" in outbox[1].html def test_registration_failure_invalid_password(client):