Add functionality for password-reset emails.
Start building up multi-template feature. Redirect to homepage and display error instead of returning plaintext. Make usernames case-insensitive.
This commit is contained in:
parent
98e385376c
commit
0f73db5e45
|
@ -7,6 +7,7 @@ Flask-SQLAlchemy==2.5.1
|
|||
gunicorn==20.1.0
|
||||
itsdangerous==2.1.2
|
||||
Jinja2==3.1.2
|
||||
mailjet-rest==1.3.4
|
||||
MarkupSafe==2.1.1
|
||||
psycopg==3.1.2
|
||||
pytest==7.1.3
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
from uuid import uuid4, UUID
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
@ -49,15 +50,18 @@ def login(user_email: str, password: str):
|
|||
return False
|
||||
|
||||
|
||||
def add_user(username: str, password: str):
|
||||
def __gen_pw_hash(password: str):
|
||||
pw_bytes = password.encode('utf-8')
|
||||
salt = bcrypt.gensalt()
|
||||
pw_hash = bcrypt.hashpw(pw_bytes, salt)
|
||||
decoded = pw_hash.decode('utf-8')
|
||||
return pw_hash.decode('utf-8')
|
||||
|
||||
|
||||
def add_user(username: str, password: str):
|
||||
pw_hash = __gen_pw_hash(password)
|
||||
with connect() as con:
|
||||
cur = con.cursor()
|
||||
cur.execute("INSERT INTO users(email, password) VALUES (%s, %s)", (username, decoded))
|
||||
cur.execute("INSERT INTO users(email, password) VALUES (%s, %s)", (username, pw_hash))
|
||||
con.commit()
|
||||
|
||||
|
||||
|
@ -112,7 +116,7 @@ def __get_user(email: str) -> Optional[UserWithHash]:
|
|||
"""
|
||||
with connect() as con:
|
||||
cur = con.cursor()
|
||||
cur.execute("SELECT id, password FROM users WHERE users.email = %s", (email,))
|
||||
cur.execute("SELECT id, password FROM users WHERE users.email ILIKE %s", (email,))
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
user_id, password = row
|
||||
|
@ -127,6 +131,37 @@ def get_users() -> [UserWithHash]:
|
|||
return map(lambda row: UserWithHash(row[0], row[1], row[2]), cur.fetchall())
|
||||
|
||||
|
||||
def initiate_password_reset(email: str) -> Optional[UUID]:
|
||||
user = get_user(email)
|
||||
if not user:
|
||||
return None
|
||||
reset_id = uuid4()
|
||||
with connect() as con:
|
||||
cur = con.cursor()
|
||||
cur.execute(
|
||||
"INSERT INTO resets(user_id, id, reset_time) VALUES (%s, %s, NOW())",
|
||||
(user.id, reset_id)
|
||||
)
|
||||
con.commit()
|
||||
return reset_id
|
||||
|
||||
|
||||
def complete_reset(reset_id: str, new_password: str):
|
||||
with connect() as con:
|
||||
cur = con.cursor()
|
||||
cur.execute("SELECT reset_time, user_id FROM resets WHERE id = %s", (reset_id,))
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
reset_time, user_id = row
|
||||
if reset_time: # TODO: And reset_time is not too far in the past
|
||||
cur.execute("DELETE FROM resets WHERE id = %s", (reset_id,))
|
||||
password_hash = __gen_pw_hash(new_password)
|
||||
cur.execute("UPDATE users SET password = %s WHERE id = %s", (password_hash, user_id))
|
||||
con.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
add_user("hash_man", "hashword")
|
||||
print("Can pull correctly: " + str(login("hash_man", "hashword")))
|
||||
|
|
|
@ -27,6 +27,8 @@ base_tex_text = open(proj_dir + "/letter_templates/base.tex", "r").read()
|
|||
|
||||
@dataclass
|
||||
class CLData:
|
||||
selectedLetter: int # Metadata
|
||||
|
||||
username: str
|
||||
company: str
|
||||
jobAndPronoun: str
|
||||
|
|
|
@ -7,16 +7,23 @@ import threading
|
|||
import urllib.parse
|
||||
|
||||
from flask import (Blueprint, render_template, request, make_response, session, redirect, jsonify)
|
||||
from wtforms import Form, StringField, TextAreaField, validators
|
||||
from wtforms import Form, SelectField, StringField, TextAreaField, validators
|
||||
from email_validator import validate_email, EmailNotValidError
|
||||
|
||||
import undercover.db as db
|
||||
import undercover.email as email
|
||||
from undercover.pdf_builder import CLData
|
||||
|
||||
writing_blueprint = Blueprint('writing', __name__,)
|
||||
|
||||
|
||||
class CLForm(Form):
|
||||
letterName = SelectField(
|
||||
'Letter Name:',
|
||||
[validators.optional()],
|
||||
choices=[(1, 'LETTER TITLE')]
|
||||
)
|
||||
|
||||
username = StringField(
|
||||
'Username:',
|
||||
[validators.Length(min=4, max=99)],
|
||||
|
@ -71,7 +78,12 @@ def login():
|
|||
if db.login(username, request.form['password']):
|
||||
session['username'] = username
|
||||
return redirect('/')
|
||||
return make_response("", 401)
|
||||
return make_response(
|
||||
render_template(
|
||||
'writing.jinja2',
|
||||
form=CLForm(),
|
||||
error="Invalid username or password"
|
||||
), 401)
|
||||
|
||||
return '''
|
||||
<form method="post">
|
||||
|
@ -90,15 +102,19 @@ def logout():
|
|||
|
||||
@writing_blueprint.route('/', methods=['GET'])
|
||||
def index_get():
|
||||
email = session.get('username')
|
||||
email_address = session.get('username')
|
||||
form = CLForm()
|
||||
if email:
|
||||
user = db.get_user(email)
|
||||
if email_address:
|
||||
user = db.get_user(email_address)
|
||||
letters = db.get_user_letters(user.id)
|
||||
if len(letters) > 0:
|
||||
letter_names = [(i + 1, letter.title) for i, letter in enumerate(letters)]
|
||||
form.letterName.choices = letter_names
|
||||
# TODO: Load this data more dynamically
|
||||
# I.e. use a dictionary instead of explicitly-defined fields
|
||||
data = json.loads(letters[0].contents)
|
||||
|
||||
form.letterName.data = 1
|
||||
form.company.data = data['company']
|
||||
form.body.data = data['body']
|
||||
form.closingText.data = data['closingText']
|
||||
|
@ -121,21 +137,49 @@ def create_account_page():
|
|||
|
||||
@writing_blueprint.route('/create_account', methods=['POST'])
|
||||
def create_account():
|
||||
email = request.form['login']
|
||||
email_address = request.form['login']
|
||||
try:
|
||||
validate_email(email, check_deliverability=True)
|
||||
validate_email(email_address, check_deliverability=True)
|
||||
except EmailNotValidError as e:
|
||||
form = CLForm(request.form)
|
||||
return make_response(render_template('writing.jinja2', form=form, error=str(e)), 401)
|
||||
return make_response(render_template('writing.jinja2', form=CLForm(), error=str(e)), 401)
|
||||
|
||||
if db.get_user(email):
|
||||
return make_response("A user with that email already exists!", 401)
|
||||
if db.get_user(email_address):
|
||||
return make_response(render_template('writing.jinja2', form=CLForm(), error="A user with that email already exists!"), 401)
|
||||
|
||||
db.add_user(email, request.form['password'])
|
||||
session['username'] = email
|
||||
db.add_user(email_address, request.form['password'])
|
||||
session['username'] = email_address
|
||||
return redirect('/')
|
||||
|
||||
|
||||
@writing_blueprint.route('/reset', methods=['POST', 'GET'])
|
||||
def reset_password():
|
||||
if request.method == 'POST':
|
||||
email_address = request.form.get('login')
|
||||
existing_reset_id = request.form.get('reset_id')
|
||||
if email_address:
|
||||
reset_id = db.initiate_password_reset(email_address)
|
||||
if reset_id:
|
||||
email.send_password_reset(email_address, 'https://undercover.cafe/reset?id=' + str(reset_id))
|
||||
# TODO: Eventually remove db entry whether or not link is clicked
|
||||
elif existing_reset_id:
|
||||
new_password = request.form['password']
|
||||
db.complete_reset(existing_reset_id, new_password)
|
||||
# TODO: Log in?
|
||||
|
||||
return redirect('/')
|
||||
|
||||
query_reset_id = request.args.get('id')
|
||||
# TODO: Add password validation
|
||||
return f'''
|
||||
<form formaction="/reset" method="post">
|
||||
<p><input type=hidden name=reset_id value="{query_reset_id}"></p>
|
||||
<label>New Password:</label>
|
||||
<p><input type=password name=password></p>
|
||||
<p><input type=submit value="Save Password"></p>
|
||||
</form>
|
||||
'''
|
||||
|
||||
|
||||
@writing_blueprint.route('/dbtest', methods=['GET'])
|
||||
def db_test_get():
|
||||
response = make_response(db.get_user_letters(1)[0].contents, 200)
|
||||
|
@ -170,7 +214,10 @@ def git_update():
|
|||
def index_post():
|
||||
form = CLForm(request.form)
|
||||
if form.validate():
|
||||
selected_letter = form.letterName.data or '1'
|
||||
|
||||
data = CLData(
|
||||
selectedLetter=int(selected_letter) - 1,
|
||||
username=form.username.data,
|
||||
company=form.company.data,
|
||||
jobAndPronoun=form.jobAndPronoun.data,
|
||||
|
@ -180,17 +227,16 @@ def index_post():
|
|||
body=form.body.data,
|
||||
)
|
||||
|
||||
email = session.get('username')
|
||||
if email:
|
||||
user = db.get_user(email)
|
||||
email_address = session.get('username')
|
||||
if email_address:
|
||||
user = db.get_user(email_address)
|
||||
letters = db.get_user_letters(user.id)
|
||||
letter_json = jsonify(data).get_data(True)
|
||||
if len(letters) == 0:
|
||||
db.add_letter(user.id, 'LETTER_TITLE', letter_json)
|
||||
db.add_letter(user.id, 'My Cover Letter', letter_json)
|
||||
else:
|
||||
# TODO: Add letter editing!
|
||||
# For now, edit just the one existing letter
|
||||
letter = letters[0]
|
||||
letter = letters[data.selectedLetter]
|
||||
# TODO: Support title editing
|
||||
db.edit_letter(letter.id, letter.title, letter_json)
|
||||
|
||||
(resp, errors) = data.generate_pdf()
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{% extends "base.jinja2" %}
|
||||
{% from "_formhelpers.jinja2" import render_field %}
|
||||
|
||||
{% block title %}UnderCover{% endblock %}
|
||||
|
||||
|
@ -51,15 +52,18 @@
|
|||
|
||||
<div>
|
||||
<input class="black-white-button" type="submit" value="Create Account" formaction="/create_account">
|
||||
<input class="black-white-button" type="submit" value="Forgot Password" formaction="/reset">
|
||||
<input class="white-black-button login-button" type="submit" value="Login">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% from "_formhelpers.jinja2" import render_field %}
|
||||
<form method=post id="letter-form">
|
||||
<dl>
|
||||
{% if username == 'sage@sagev.space' %}
|
||||
{{ render_field(form.letterName) }}
|
||||
{% endif %}
|
||||
{{ render_field(form.username) }}
|
||||
{{ render_field(form.company) }}
|
||||
{{ render_field(form.jobAndPronoun) }}
|
||||
|
|
Loading…
Reference in New Issue