initial commit

This commit is contained in:
2021-01-20 01:36:22 -08:00
commit 368f89750b
45 changed files with 2821 additions and 0 deletions

52
app/auth/auth.py Normal file
View File

@@ -0,0 +1,52 @@
#!/usr/bin/env python3
from flask import Blueprint, redirect, url_for, session, flash, render_template
from app.models import Contributor
from app.forms import LoginForm
from flask_login import current_user, login_user, logout_user
auths = Blueprint(
"auths", __name__, template_folder="templates"
)
@auths.route("/login", methods=["GET", "POST"])
def login():
if current_user.is_authenticated:
return redirect(url_for('cats.index'))
navbar_links = (('cancel', url_for('cats.index')), )
form = LoginForm()
if form.validate_on_submit():
contributor_by_name = Contributor.query.filter_by(name=form.username.data).first()
contributor_by_email = Contributor.query.filter_by(email=form.email.data).first()
if contributor_by_name is not None and contributor_by_name.check_password(form.password.data):
if contributor_by_name.use_totp:
session['id'] = contributor_by_name.id
session['remember_me'] = form.remember_me.data
return redirect(url_for('totps.two_factor_input'))
else:
login_user(contributor_by_name, remember=form.remember_me.data)
flash("Congratulations, you are now logged in!")
return redirect(url_for('cats.index'))
elif contributor_by_email is not None and contributor_by_email.check_password(form.password.data):
if contributor_by_email.use_totp:
session['id'] = contributor_by_email.id
session['remember_me'] = form.remember_me.data
return redirect(url_for('totps.two_factor_input'))
else:
login_user(contributor_by_email, remember=form.remember_me.data)
flash("Congratulations, you are now logged in!")
return redirect(url_for('cats.index'))
else:
flash("Error Invalid Contributor (Username or Email) or Password")
return(redirect(url_for('auths.login')))
return render_template('login.html', title='Sign In', form=form, navbar_links=navbar_links)
@auths.route("/logout")
def logout():
is_authenticated = current_user.is_authenticated
logout_user()
if is_authenticated:
flash("Congratulations, you are now logged out!")
return redirect(url_for('cats.index'))

30
app/auth/email.py Normal file
View File

@@ -0,0 +1,30 @@
#!/usr/bin/env python3
from flask import render_template
from flask_mail import Message
from flask import current_app as app
from .. import mail
from threading import Thread
def send_password_reset_email(contributor, external_url):
token = contributor.get_reset_password_token()
send_email('Todo App Reset Your Password',
sender=app.config['MAIL_ADMINS'][0],
recipients=[contributor.email],
text_body=render_template('email/reset_password_email_text.txt',
contributor=contributor, token=token, external_url=external_url),
html_body=render_template('email/reset_password_email_html.html',
contributor=contributor, token=token, external_url=external_url))
def send_async_email(app, msg):
with app.app_context():
mail.send(msg)
def send_email(subject, sender, recipients, text_body, html_body):
msg = Message(subject, sender=sender, recipients=recipients)
msg.body = text_body
msg.html = html_body
Thread(target=send_async_email, args=(app._get_current_object(), msg)).start()

67
app/auth/profile.py Normal file
View File

@@ -0,0 +1,67 @@
#!/usr/bin/env python3
from flask import Blueprint, redirect, url_for, request, flash, render_template
from flask_login import current_user
from app.models import Contributor
from app.forms import EditProfile, ChangePassword
from .. import db
prof = Blueprint(
"prof", __name__, template_folder="templates"
)
@prof.route("/edit-profile", methods=["GET", "POST"])
def edit_profile():
if current_user.is_anonymous:
return(redirect(url_for('cats.index')))
contributor = Contributor.query.get(current_user.id)
form = EditProfile()
navbar_links = (('cancel', url_for('cats.index')), )
if request.method == 'GET':
form.username.data = contributor.name
form.email.data = contributor.email
if form.validate_on_submit():
if contributor.check_password(form.password.data):
contributor.name = form.username.data
contributor.email = form.email.data
db.session.commit()
flash("Thanks for the update!")
return(redirect(url_for('cats.index')))
else:
flash("Error Invalid Password")
return(redirect(url_for('prof.edit_profile')))
return render_template(
'edit_profile.html',
title='Edit Profile', form=form,
contributor_use_totp=contributor.use_totp,
navbar_links=navbar_links
)
@prof.route("/change-password", methods=["GET", "POST"])
def change_password():
if not current_user.is_authenticated:
return(redirect(url_for('cats.index')))
contributor = Contributor.query.get(current_user.id)
form = ChangePassword()
nl = (('cancel', url_for('prof.edit_profile')), )
if form.validate_on_submit():
if contributor.check_password(form.password.data):
contributor.set_password(form.new_password.data)
db.session.commit()
flash("Thanks for the update!")
return(redirect(url_for('cats.index')))
else:
flash("Error Invalid Password")
return(redirect(url_for('prof.change_password')))
return render_template(
'change_password.html',
title='Change Password',
form=form, navbar_links=nl
)

44
app/auth/register.py Normal file
View File

@@ -0,0 +1,44 @@
#!/usr/bin/env python3
import psycopg2
from flask import Blueprint, redirect, url_for, flash, render_template
from flask_login import current_user
from app.forms import RegistrationForm
from app.models import Contributor
from flask import current_app as app
from .. import db
reg = Blueprint(
"reg", __name__, template_folder="templates"
)
@reg.route("/register", methods=["GET", "POST"])
def register():
if current_user.is_authenticated:
return redirect(url_for('cats.index'))
form = RegistrationForm()
nl = (('cancel', url_for('auths.login')), )
if form.validate_on_submit():
set_contributor_id_seq()
contributor = Contributor(name=form.username.data, email=form.email.data)
contributor.set_password(form.password.data)
db.session.add(contributor)
db.session.commit()
flash("Congratulations, you are now a registered user!")
return redirect(url_for('auths.login'))
return render_template('register.html', title='Register', form=form, navbar_links=nl)
def set_contributor_id_seq():
conn = psycopg2.connect(
dbname=app.config['DATABASE_NAME'],
user=app.config['DATABASE_USER'],
host=app.config['DATABASE_HOST'],
password=app.config['DATABASE_PASSWORD']
)
cur = conn.cursor()
cur.execute("SELECT setval('contributor_id_seq', (SELECT MAX(id) FROM contributor))")
conn.commit()
conn.close()

View File

@@ -0,0 +1,48 @@
#!/usr/bin/env python3
from flask import Blueprint, redirect, url_for, flash, render_template
from flask_login import current_user
from app.forms import ResetPasswordRequestForm, ResetPasswordForm
from app.models import Contributor
from flask import current_app as app
from .. import db
from .email import send_password_reset_email
pwd = Blueprint(
"pwd", __name__, template_folder="templates"
)
@pwd.route('/reset-password-request', methods=['GET', 'POST'])
def reset_password_request():
if current_user.is_authenticated:
return(redirect(url_for('cats.index')))
nl = (('cancel', url_for('cats.index')), )
form = ResetPasswordRequestForm()
if form.validate_on_submit():
contributor = Contributor.query.filter_by(email=form.email.data).first()
if contributor:
send_password_reset_email(contributor, app.config['EXTERNAL_URL'])
flash('Check your email for the instructions to reset your password')
return redirect(url_for('auths.login'))
else:
flash('Sorry, invalid email')
return redirect(url_for('auths.login'))
return render_template('reset_password_request.html', title='Reset Password', form=form, navbar_links=nl)
@pwd.route('/reset-password/<token>', methods=['GET', 'POST'])
def reset_password(token):
if current_user.is_authenticated:
return redirect(url_for('cats.index'))
nl = (('cancel', url_for('cats.index')), )
contributor = Contributor.verify_reset_password_token(token)
if not contributor:
return redirect(url_for('cats.index'))
form = ResetPasswordForm()
if form.validate_on_submit():
contributor.set_password(form.password.data)
db.session.commit()
flash('Your password has been reset.')
return redirect(url_for('auths.login'))
return render_template('reset_password.html', title="New Password?", form=form, navbar_links=nl)

View File

@@ -0,0 +1,35 @@
{% extends "base.html" %}
{% block content %}
<div class="formContainer">
<h1>Change Password</h1>
<form action='' method="post" novalidate>
{{ form.hidden_tag() }}
<p>
{{ form.password.label }}<br>
{{ form.password(size=24) }}
{% for error in form.password.errors %}
<span class="formWarning">[{{error}}]</span>
{% endfor %}
</p>
<p>
{{ form.new_password.label }}<br>
<span class="inputInfo">
min 15 chars and at least somewhat random
</span><br>
{{ form.new_password(size=24) }}
{% for error in form.new_password.errors %}
<span class="formWarning">[{{error}}]</span>
{% endfor %}
</p>
<p>
{{ form.new_password2.label }}<br>
{{ form.new_password2(size=24) }}
{% for error in form.new_password2.errors %}
<span class="formWarning">[{{error}}]</span>
{% endfor %}
</p>
<p>{{ form.submit() }}</p>
</form>
</div>
{% endblock %}

View File

@@ -0,0 +1,23 @@
<style>
.formContainer {
align-items: center;
margin-top: 100px;
}
#submitContainer {
margin-top: 100px;
}
</style>
{% extends "base.html" %}
{% block content %}
<div class='formContainer'>
<form action="" method="post" novalidate>
<h2>Disable 2FA?</h2>
{{ form.hidden_tag() }}
<p id="submitContainer">{{ form.submit() }}</p>
</form>
</div>
{% endblock %}

View File

@@ -0,0 +1,49 @@
{% extends "base.html" %}
{% block content %}
<div class="formContainer">
<h1>Edit Profile</h1>
<form action='' method="post" novalidate>
{{ form.hidden_tag() }}
<p>
{{ form.username.label }}<br>
<span class="inputInfo">
letters and digits only (i.e. no spaces)
</span><br>
{{ form.username(size=24) }}
{% for error in form.username.errors %}
<span class="formWarning">[{{error}}]</span>
{% endfor %}
</p>
<p>
{{ form.email.label }}<br>
{{ form.email(size=24) }}
{% for error in form.email.errors %}
<span class="formWarning">[{{error}}]</span>
{% endfor %}
</p>
<p>
{{ form.password.label }}<br>
{{ form.password(size=24) }}
{% for error in form.password.errors %}
<span class="formWarning">[{{error}}]</span>
{% endfor %}
</p>
<p>{{ form.submit() }}</p>
</form>
{% if current_user.is_authenticated %}
<div>
<button class="formButton" onclick="location.href='{{ url_for('prof.change_password') }}';">Change Password</button>
</div>
{% if contributor_use_totp %}
<div>
<button class="formButton" onclick="location.href='{{ url_for('totps.disable_totp') }}';">Disable 2 Factor</button>
</div>
{% else %}
<div>
<button class="formButton" onclick="location.href='{{ url_for('totps.enable_totp') }}';">Enable 2 Factor</button>
</div>
{% endif %}
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,38 @@
{% extends "base.html" %}
{% block content %}
<div class="formContainer">
<h1>Sign In</h1>
<h3>(username Or email)</h3>
<form action='' method="post" novalidate>
{{ form.hidden_tag() }}
<p>
{{ form.username.label }}<br>
{{ form.username(size=24) }}
{% for error in form.username.errors %}
<span class="formWarning">[{{error}}]</span>
{% endfor %}
</p>
<p>
{{ form.email.label }}<br>
{{ form.email(size=24) }}
{% for error in form.email.errors %}
<span class="formWarning">[{{error}}]</span>
{% endfor %}
</p>
<p>
{{ form.password.label }}<br>
{{ form.password(size=24) }}
{% for error in form.password.errors %}
<span class="formWarning">[{{error}}]</span>
{% endfor %}
</p>
<p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
<p>{{ form.submit() }}</p>
</form>
<p>New User? <a class="extraFormLink" href="{{ url_for('reg.register') }}">Click to Register!</a></p>
<p>
Forgot Your Password? <a class="extraFormLink" href="{{ url_for('pwd.reset_password_request') }}">Click to Reset It</a>
</p>
</div>
{% endblock %}

View File

@@ -0,0 +1,53 @@
<style>
.formWarning {
margin-bottom: 20px;
}
#totpp {
margin-top: 80px;
margin-bottom: 50px;
display: flex;
flex-direction: column;
align-items: center;
height: 50px;
justify-content: space-between;
}
#submitContainer {
display: flex;
justify-content: center;
}
#submit {
margin-top: 30px;
}
#svg_container {
margin-top: 100px;
align-items: center;
}
svg {
transform: scale(1.5);
}
</style>
{% extends "base.html" %}
{% block content %}
<div id='svg_container' class='formContainer'>
<form action='' method="post" novalidate>
{{ form.hidden_tag() }}
<p>{{ qr | safe }}</p>
<p id="totpp">
{{ form.totp_code.label }}<br>
{% for error in form.totp_code.errors %}
<span class="formWarning">[{{error}}]</span>
{% endfor %}
{{ form.totp_code(size=6) }}
</p>
<p id="submitContainer">{{ form.submit() }}</p>
</form>
</div>
{% endblock %}

View File

@@ -0,0 +1,45 @@
{% extends "base.html" %}
{% block content %}
<div class="formContainer">
<h1>Register</h1>
<form action="" method="post" novalidate>
{{ form.hidden_tag() }}
<p>
{{ form.username.label }}<br>
<span class="inputInfo">
letters and digits only (i.e. no spaces)
</span><br>
{{ form.username(size=24) }}
{% for error in form.username.errors %}
<span class="formWarning">[{{error}}]</span>
{% endfor %}
</p>
<p>
{{ form.email.label }}<br>
{{ form.email(size=24) }}
{% for error in form.email.errors %}
<span class="formWarning">[{{error}}]</span>
{% endfor %}
</p>
<p>
{{ form.password.label }}<br>
<span class="inputInfo">
min 15 chars and at least somewhat random
</span><br>
{{ form.password(size=24) }}
{% for error in form.password.errors %}
<span class="formWarning">[{{error}}]</span>
{% endfor %}
</p>
<p>
{{ form.password2.label }}<br>
{{ form.password2(size=24) }}
{% for error in form.password2.errors %}
<span class="formWarning">[{{error}}]</span>
{% endfor %}
</p>
<p>{{ form.submit() }}</p>
</form>
</div>
{% endblock %}

View File

@@ -0,0 +1,28 @@
{% extends "base.html" %}
{% block content %}
<div class="formContainer">
<h1>Reset Your Password</h1>
<form action="" method="post">
{{ form.hidden_tag() }}
<p>
{{ form.password.label }}<br>
<span class="inputInfo">
min 15 chars and at least somewhat random
</span><br>
{{ form.password(size=24) }}<br>
{% for error in form.password.errors %}
<span class="formWarning">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.password2.label }}<br>
{{ form.password2(size=24) }}<br>
{% for error in form.password2.errors %}
<span class="formWarning">[{{ error }}]</span>
{% endfor %}
</p>
<p>{{ form.submit() }}</p>
</form>
</div>
{% endblock %}

View File

@@ -0,0 +1,18 @@
{% extends "base.html" %}
{% block content %}
<div class="formContainer">
<h1>{{ title }}</h1>
<form action="" method="post">
{{ form.hidden_tag() }}
<p>
{{ form.email.label }}<br>
{{ form.email(size=24) }}<br>
{% for error in form.email.errors %}
<span class="formWarning">[{{ error }}]</span>
{% endfor %}
</p>
<p>{{ form.submit() }}</p>
</form>
</div>
{% endblock %}

View File

@@ -0,0 +1,45 @@
<style>
#totpp {
margin-top: 10%;
margin-bottom: 50px;
display: flex;
flex-direction: column;
align-items: center;
height: 50px;
justify-content: space-between;
}
#submitContainer {
display: flex;
justify-content: center;
}
#submit {
margin-top: 30px;
}
h3 {
align-self: center;
margin-top: 10%;
}
</style>
{% extends "base.html" %}
{% block content %}
<h3>{{ inst }}</h3>
<div class='formContainer'>
<form action={{ url_for('totps.two_factor_input') }} method="post" novalidate>
{{ form.hidden_tag() }}
<p id="totpp">
{{ form.totp_code.label }}<br>
{% for error in form.totp_code.errors %}
<span class="formWarning">[{{error}}]</span>
{% endfor %}
{{ form.totp_code(size=6) }}
</p>
<p id="submitContainer">{{ form.submit() }}</p>
</form>
</div>
{% endblock %}

82
app/auth/totp.py Normal file
View File

@@ -0,0 +1,82 @@
#!/usr/bin/env python3
from flask import Blueprint, session, redirect, url_for, flash, render_template
from flask import current_app as app
from app.models import Contributor
from app.forms import GetTotp, DisableTotp, ConfirmTotp
from flask_login import current_user, login_user
from pyotp.totp import TOTP
from .totp_utils import disable_2fa, validate_totp, get_totp_qr
totps = Blueprint(
"totps", __name__, template_folder="templates"
)
@totps.route("/two-factor-input", methods=["GET", "POST"])
def two_factor_input():
if current_user.is_authenticated or 'id' not in session:
return redirect(url_for('cats.index'))
nl = (('cancel', url_for('cats.index')), )
contributor = Contributor.query.get(session['id'])
if contributor is None:
return redirect(url_for('cats.index'))
form = GetTotp()
if form.validate_on_submit():
if TOTP(contributor.totp_key).verify(int(form.totp_code.data), valid_window=5):
login_user(contributor, remember=session['remember_me'])
flash("Congratulations, you are now logged in!")
return redirect(url_for('cats.index'))
else:
flash("Oops, the pin was wrong")
form.totp_code.data = None
return render_template('two_factor_input.html', form=form, inst="Code was wrong, try again?")
return render_template(
'two_factor_input.html', form=form,
inst="Enter Auth Code", navbar_links=nl
)
@totps.route('/disable-totp', methods=['GET', 'POST'])
def disable_totp():
if current_user.is_anonymous or not current_user.use_totp:
return(redirect(url_for('cats.index')))
nl = (('cancel', url_for('prof.edit_profile')), )
contributor = Contributor.query.get(current_user.id)
form = DisableTotp()
if form.validate_on_submit():
if disable_2fa(contributor, app.config):
flash('2FA Now Disabled')
return(redirect(url_for('prof.edit_profile')))
else:
flash('2FA Not Disabled')
return(redirect(url_for('prof.edit_profile')))
return render_template(
'disable_2fa.html', form=form,
title="Disable 2FA", navbar_links=nl
)
@totps.route('/enable-totp', methods=['GET', 'POST'])
def enable_totp():
if current_user.is_anonymous or current_user.use_totp:
return(redirect(url_for('cats.index')))
nl = (('cancel', url_for('prof.edit_profile')), )
contributor = Contributor.query.get(current_user.id)
form = ConfirmTotp()
qr = get_totp_qr(contributor, app.config)
if form.validate_on_submit():
if contributor.use_totp:
flash('2FA Already Enabled')
return(redirect(url_for('prof.edit_profile')))
if validate_totp(contributor, form.totp_code.data, app.config):
flash('2FA Now Enabled')
return(redirect(url_for('prof.edit_profile')))
else:
flash("TOTP Code didn't validate, rescan and try again")
return(redirect(url_for('prof.edit_profile')))
return render_template(
'qr.html', qr=qr, form=form,
title="Aunthentication Code",
navbar_links=nl
)

59
app/auth/totp_utils.py Normal file
View File

@@ -0,0 +1,59 @@
#!/usr/bin/env python3
import psycopg2
import pyotp
import qrcode
import qrcode.image.svg
from io import BytesIO
def get_totp_qr(contributor, app_config):
totp_key = pyotp.random_base32() if contributor.totp_key is None else contributor.totp_key
if contributor.totp_key is None:
conn = psycopg2.connect(
dbname=app_config['DATABASE_NAME'],
user=app_config['DATABASE_USER'],
host=app_config['DATABASE_HOST'],
password=app_config['DATABASE_PASSWORD']
)
cur = conn.cursor()
cur.execute("UPDATE contributor SET totp_key=%s WHERE id=%s", (totp_key, contributor.id))
conn.commit()
conn.close()
totp_uri = pyotp.totp.TOTP(totp_key).provisioning_uri(name=contributor.email, issuer_name='Todo App')
img = qrcode.make(totp_uri, image_factory=qrcode.image.svg.SvgPathImage)
f = BytesIO()
img.save(f)
return(f.getvalue().decode('utf-8'))
def validate_totp(contributor, totp_code, app_config):
if pyotp.TOTP(contributor.totp_key).verify(int(totp_code), valid_window=5):
conn = psycopg2.connect(
dbname=app_config['DATABASE_NAME'],
user=app_config['DATABASE_USER'],
host=app_config['DATABASE_HOST'],
password=app_config['DATABASE_PASSWORD']
)
cur = conn.cursor()
cur.execute("UPDATE contributor SET use_totp='1' WHERE id=%s", (contributor.id, ))
conn.commit()
conn.close()
return True
else:
return False
def disable_2fa(contributor, app_config):
conn = psycopg2.connect(
dbname=app_config['DATABASE_NAME'],
user=app_config['DATABASE_USER'],
host=app_config['DATABASE_HOST'],
password=app_config['DATABASE_PASSWORD']
)
cur = conn.cursor()
cur.execute("UPDATE contributor SET use_totp='0', totp_key=Null WHERE id=%s", (contributor.id, ))
conn.commit()
conn.close()
return True