diff --git a/requirements.txt b/requirements.txt index c35fc13..18bfade 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/undercover/db.py b/undercover/db.py index 78c27e5..e37c1b1 100644 --- a/undercover/db.py +++ b/undercover/db.py @@ -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"))) diff --git a/undercover/pdf_builder.py b/undercover/pdf_builder.py index 064e974..cdc9bb0 100644 --- a/undercover/pdf_builder.py +++ b/undercover/pdf_builder.py @@ -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 diff --git a/undercover/routes.py b/undercover/routes.py index 74476f7..afa69b8 100644 --- a/undercover/routes.py +++ b/undercover/routes.py @@ -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 '''
+ ''' + + @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() diff --git a/undercover/templates/writing.jinja2 b/undercover/templates/writing.jinja2 index e1f0549..3183340 100644 --- a/undercover/templates/writing.jinja2 +++ b/undercover/templates/writing.jinja2 @@ -1,4 +1,5 @@ {% extends "base.jinja2" %} +{% from "_formhelpers.jinja2" import render_field %} {% block title %}UnderCover{% endblock %} @@ -51,15 +52,18 @@