Add basic fallbacks for missing db/email creds.
Fallbacks may eventually warrant their own file. Use f-strings to format password-reset emails. Automatically delete email-reset rows after 15 minutes. - Cute, but a proper vacuum would likely be more robust. Remove deprecated FLASK_ENV use. Use exceptions in generate_pdf()
This commit is contained in:
parent
7800c24f99
commit
d358c55017
3
start
3
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
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
||||
def connect():
|
||||
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 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):
|
||||
|
|
|
@ -1,10 +1,19 @@
|
|||
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):
|
||||
|
@ -15,23 +24,28 @@ def send_password_reset(to_email: str, reset_link: str):
|
|||
"Email": "donotreply@undercover.cafe",
|
||||
"Name": "UnderCover"
|
||||
},
|
||||
"To": [
|
||||
{
|
||||
"Email": to_email
|
||||
# "Name": "Sage"
|
||||
}
|
||||
],
|
||||
"To": [{"Email": to_email}],
|
||||
"Subject": "UnderCover - Password Reset",
|
||||
"TextPart": "Complete your UnderCover password reset by visiting " + reset_link,
|
||||
"HTMLPart": "<h3><a href='" + reset_link + "'>Click here to complete your UnderCover password reset</a></h3><br />Or copy and paste this link into your browser: " + reset_link,
|
||||
"TextPart": f"Complete your UnderCover password reset by visiting {reset_link}",
|
||||
"HTMLPart": f"""
|
||||
<h3>
|
||||
<a href='{reset_link}'>Click here to complete your UnderCover password reset</a>
|
||||
</h3>
|
||||
<br />
|
||||
Or copy and paste this link into your browser: {reset_link}
|
||||
""",
|
||||
"CustomID": "PasswordResetLink"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
if mailjet:
|
||||
result = mailjet.send.create(data=data)
|
||||
return 200 <= result.status_code <= 299
|
||||
|
||||
print(json.JSONEncoder(indent=2).encode(data))
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
send_password_reset('sage@sagev.space', 'https://sagev.space/')
|
||||
|
|
|
@ -4,7 +4,7 @@ import os
|
|||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
|
||||
from flask import send_from_directory
|
||||
from flask import send_from_directory, Response
|
||||
|
||||
|
||||
def get_unique():
|
||||
|
@ -48,7 +48,11 @@ class CLData:
|
|||
("body", self.body),
|
||||
]
|
||||
|
||||
def generate_pdf(self):
|
||||
def generate_pdf(self) -> Response:
|
||||
"""
|
||||
:return: Response when successful
|
||||
:raise ValueError: e.args[0] is a list of error strings, if generation fails
|
||||
"""
|
||||
import threading
|
||||
unique_id = get_unique()
|
||||
unique_file = output_dir + unique_id + ".tex"
|
||||
|
@ -85,19 +89,19 @@ class CLData:
|
|||
else:
|
||||
output_file += ".pdf"
|
||||
|
||||
return (send_from_directory(
|
||||
return send_from_directory(
|
||||
output_dir,
|
||||
output_file,
|
||||
download_name=self.username.replace(" ", "") + "_CoverLetter.pdf",
|
||||
as_attachment=True
|
||||
), None)
|
||||
)
|
||||
else:
|
||||
print(build_text + "[FAIL]")
|
||||
# Collect output but delete boilerplate text
|
||||
errors = list(map(str.strip, result.stdout.split("\n")))
|
||||
del errors[:13]
|
||||
del errors[-2:]
|
||||
return None, errors
|
||||
raise ValueError(errors)
|
||||
|
||||
|
||||
def cleanup(unique):
|
||||
|
|
|
@ -164,7 +164,6 @@ def reset_password():
|
|||
if not email.send_password_reset(email_address, 'https://undercover.cafe/reset?id=' + str(reset_id)):
|
||||
return render_index(error="Failed to send reset email. Please try again later.", status=500)
|
||||
elif existing_reset_id:
|
||||
# TODO: Eventually remove db entry whether or not link is clicked
|
||||
new_password = request.form['password']
|
||||
if not db.complete_reset(existing_reset_id, new_password):
|
||||
return render_index(error="Password reset failed. Your reset link may have expired.", status=500)
|
||||
|
@ -215,7 +214,7 @@ def git_update():
|
|||
|
||||
|
||||
@writing_blueprint.route('/', methods=['POST'])
|
||||
def index_post():
|
||||
def generate_pdf():
|
||||
form = CLForm(request.form)
|
||||
if form.validate():
|
||||
selected_letter = form.letterName.data or '1'
|
||||
|
@ -243,12 +242,15 @@ def index_post():
|
|||
# TODO: Support title editing
|
||||
db.edit_letter(letter.id, letter.title, letter_json)
|
||||
|
||||
(resp, errors) = data.generate_pdf()
|
||||
if errors:
|
||||
resp = render_index(form=form, letter_errors=errors)
|
||||
try:
|
||||
resp = data.generate_pdf()
|
||||
except ValueError as e:
|
||||
resp = render_index(form=form, letter_errors=e.args[0])
|
||||
|
||||
# Save entered data as cookies on user's machine
|
||||
for pair in data.get_pairs():
|
||||
resp.set_cookie(pair[0], urllib.parse.quote(pair[1]))
|
||||
|
||||
return resp
|
||||
|
||||
return render_index(form=form)
|
||||
|
|
Loading…
Reference in New Issue