mirror of
https://github.com/TrentSPalmer/todo_app_flask.git
synced 2025-08-23 05:43:58 -07:00
initial commit
This commit is contained in:
52
app/auth/auth.py
Normal file
52
app/auth/auth.py
Normal 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
30
app/auth/email.py
Normal 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
67
app/auth/profile.py
Normal 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
44
app/auth/register.py
Normal 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()
|
48
app/auth/reset_password.py
Normal file
48
app/auth/reset_password.py
Normal 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)
|
35
app/auth/templates/change_password.html
Normal file
35
app/auth/templates/change_password.html
Normal 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 %}
|
23
app/auth/templates/disable_2fa.html
Normal file
23
app/auth/templates/disable_2fa.html
Normal 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 %}
|
49
app/auth/templates/edit_profile.html
Normal file
49
app/auth/templates/edit_profile.html
Normal 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 %}
|
38
app/auth/templates/login.html
Normal file
38
app/auth/templates/login.html
Normal 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 %}
|
53
app/auth/templates/qr.html
Normal file
53
app/auth/templates/qr.html
Normal 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 %}
|
45
app/auth/templates/register.html
Normal file
45
app/auth/templates/register.html
Normal 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 %}
|
28
app/auth/templates/reset_password.html
Normal file
28
app/auth/templates/reset_password.html
Normal 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 %}
|
18
app/auth/templates/reset_password_request.html
Normal file
18
app/auth/templates/reset_password_request.html
Normal 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 %}
|
45
app/auth/templates/two_factor_input.html
Normal file
45
app/auth/templates/two_factor_input.html
Normal 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
82
app/auth/totp.py
Normal 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
59
app/auth/totp_utils.py
Normal 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
|
Reference in New Issue
Block a user