diff --git a/start b/start index 8296cd8..ff1a855 100755 --- a/start +++ b/start @@ -18,7 +18,6 @@ if [[ "$1" == "prod" ]]; then gunicorn -b 0.0.0.0:80 -b 0.0.0.0:443 "undercover:create_app()" else echo "Starting local dev server..." - export FLASK_ENV=development export FLASK_APP=undercover - flask run --host=0.0.0.0 + flask --debug run --host=0.0.0.0 fi diff --git a/undercover/db.py b/undercover/db.py index e1bc625..b0b85f5 100644 --- a/undercover/db.py +++ b/undercover/db.py @@ -1,4 +1,7 @@ import os +import sys +import threading +import types from dataclasses import dataclass from datetime import datetime, timedelta from typing import Optional @@ -35,14 +38,38 @@ db_user = os.environ.get('UNDERCOVER_POSTGRES_USER') db_available = host and db_name and port and db_user and os.environ.get('UNDERCOVER_POSTGRES_PASSWORD') +if db_available: + def connect(): + return psycopg.connect( + host=host, + dbname=db_name, + port=port, + user=db_user, + password=os.environ.get('UNDERCOVER_POSTGRES_PASSWORD')) +else: + sys.stderr.write('Database login not configured: DB access is disabled.\n') + sys.stderr.write(' To enable, ensure UNDERCOVER_POSTGRES_{HOST,DBNAME,PORT,USER,PASSWORD} are set.\n') -def connect(): - return psycopg.connect( - host=host, - dbname=db_name, - port=port, - user=db_user, - password=os.environ.get('UNDERCOVER_POSTGRES_PASSWORD')) + def connect(): + return MockConnection() + + class MockConnection: + mock_cursor = types.SimpleNamespace() + mock_cursor.execute = lambda *a: () + mock_cursor.fetchone = lambda *a: None + mock_cursor.fetchall = lambda *a: [] + + def __enter__(self, *a): + return self + + def __exit__(self, *a): + pass + + def cursor(self): + return self.mock_cursor + + def commit(self, *a): + pass def login(user_email: str, password: str): @@ -120,6 +147,9 @@ def __get_user(email: str) -> Optional[UserWithHash]: return None +RESET_TIME = timedelta(minutes=-1 * 15) + + def initiate_password_reset(email: str) -> Optional[UUID]: user = get_user(email) if not user: @@ -131,11 +161,16 @@ def initiate_password_reset(email: str) -> Optional[UUID]: "INSERT INTO resets(user_id, id, reset_time) VALUES (%s, %s, NOW())", (user.id, reset_id) ) + threading.Timer(RESET_TIME.total_seconds(), delete_reset_row, [reset_id]).start() con.commit() return reset_id -RESET_TIME = timedelta(minutes=-1 * 15) +def delete_reset_row(reset_id: UUID): + with connect() as con: + cur = con.cursor() + cur.execute("DELETE FROM resets WHERE id = %s", (reset_id,)) + con.commit() def complete_reset(reset_id: str, new_password: str): diff --git a/undercover/email.py b/undercover/email.py index 3ceb473..f599391 100644 --- a/undercover/email.py +++ b/undercover/email.py @@ -1,36 +1,50 @@ -from mailjet_rest import Client +import json import os +import sys -api_key = os.environ['MAILJET_API_KEY'] -api_secret = os.environ['MAILJET_SECRET_KEY'] +from mailjet_rest import Client -mailjet = Client(auth=(api_key, api_secret), version='v3.1') +api_key = os.environ.get('MAILJET_API_KEY') +api_secret = os.environ.get('MAILJET_SECRET_KEY') + +has_mailjet_keys = api_key and api_secret +mailjet = Client(auth=(api_key, api_secret), version='v3.1') if has_mailjet_keys else None + +if not mailjet: + sys.stderr.write('Mailjet keys not configured: email access is disabled.\n') + sys.stderr.write(' Emails will be printed to the console.\n') + sys.stderr.write(' To enable, ensure MAILJET_API_KEY and MAILJET_SECRET_KEY are set\n') def send_password_reset(to_email: str, reset_link: str): data = { - 'Messages': [ - { - "From": { - "Email": "donotreply@undercover.cafe", - "Name": "UnderCover" - }, - "To": [ + 'Messages': [ { - "Email": to_email - # "Name": "Sage" + "From": { + "Email": "donotreply@undercover.cafe", + "Name": "UnderCover" + }, + "To": [{"Email": to_email}], + "Subject": "UnderCover - Password Reset", + "TextPart": f"Complete your UnderCover password reset by visiting {reset_link}", + "HTMLPart": f""" +