fine-tuning login

This commit is contained in:
forest 2020-05-10 13:51:54 -05:00
parent e6fcb847f0
commit 7fe0d9a9c5
8 changed files with 52 additions and 41 deletions

View File

@ -2,16 +2,6 @@
Python Flask web application for capsul.org Python Flask web application for capsul.org
## postgres database schema management
capsulflask has a concept of a schema version. When the application starts, it will query the database for a table named
`schemaversion` that has one row and one column (`version`). If the `version` it finds is not equal to the `desiredSchemaVersion` variable set in `db.py`, it will run migration scripts from the `schema_migrations` folder one by one until the `schemaversion` table shows the correct version.
For example, the script named `02_up_xyz.sql` should contain code that migrates the database from schema version 1 to schema version 2. Likewise, the script `02_down_xyz.sql` should contain code that migrates from schema version 2 back to schema version 1.
**IMPORTANT: if you need to make changes to the schema, make a NEW schema version. DO NOT EDIT the existing schema versions.**
In general, for safety, schema version upgrades should not delete data. Schema version downgrades will simply throw an error and exit for now.
## how to run locally ## how to run locally
@ -33,11 +23,22 @@ pip install -r requirements.txt
Run an instance of Postgres (I used docker for this, you can use whatever you want, point is its listening on localhost:5432) Run an instance of Postgres (I used docker for this, you can use whatever you want, point is its listening on localhost:5432)
``` ```
docker run -it -e POSTGRES_PASSWORD=dev -p 5432:5432 postgres docker run --rm -it -e POSTGRES_PASSWORD=dev -p 5432:5432 postgres
``` ```
Run the app Run the app
``` ```
FLASK_APP=capsulflask flask run FLASK_APP=capsulflask flask run
``` ```
## postgres database schema management
capsulflask has a concept of a schema version. When the application starts, it will query the database for a table named
`schemaversion` that has one row and one column (`version`). If the `version` it finds is not equal to the `desiredSchemaVersion` variable set in `db.py`, it will run migration scripts from the `schema_migrations` folder one by one until the `schemaversion` table shows the correct version.
For example, the script named `02_up_xyz.sql` should contain code that migrates the database from schema version 1 to schema version 2. Likewise, the script `02_down_xyz.sql` should contain code that migrates from schema version 2 back to schema version 1.
**IMPORTANT: if you need to make changes to the schema, make a NEW schema version. DO NOT EDIT the existing schema versions.**
In general, for safety, schema version upgrades should not delete data. Schema version downgrades will simply throw an error and exit for now.

View File

@ -18,7 +18,7 @@ from capsulflask.db import get_model
bp = Blueprint("auth", __name__, url_prefix="/auth") bp = Blueprint("auth", __name__, url_prefix="/auth")
def account_required(view): def account_required(view):
"""View decorator that redirects anonymous users to the login page.""" """View decorator that redirects non-logged-in users to the login page."""
@functools.wraps(view) @functools.wraps(view)
def wrapped_view(**kwargs): def wrapped_view(**kwargs):
@ -36,31 +36,34 @@ def login():
error = None error = None
if not email: if not email:
error = "Email is required." error = "email is required"
elif not re.match(r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)", email): elif not re.match(r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)", email):
error = "Enter a valid email address." error = "enter a valid email address"
if error is None: if error is None:
token = get_model().login(email) token = get_model().login(email)
if token is None:
error = "too many logins. please use one of the existing login links that have been emailed to you"
else:
link = f"{current_app.config['BASE_URL']}/auth/magic/{token}"
link = f"{current_app.config['BASE_URL']}/auth/magic/{token}" current_app.config["FLASK_MAIL_INSTANCE"].send(
Message(
"Click This Link to Login to Capsul",
body=f"""
Navigate to {link} to log into capsul.
current_app.config["FLASK_MAIL_INSTANCE"].send( If you didn't request this, ignore this message.
Message( """,
"Click This Link to Login to Capsul", html=f"""
body=f""" <p>Navigate to <a href="{link}">{link}</a> to log into capsul.</p>
Navigate to {link} to log into capsul. <p>If you didn't request this, ignore this message.</p>
""", """,
html=f""" recipients=[email]
Navigate to <a href="{link}">{link}</a> to log into capsul. )
""",
sender=current_app.config['MAIL_DEFAULT_SENDER'],
recipients=[email]
) )
)
return render_template("check-your-email.html") return render_template("login-landing.html", email=email)
flash(error) flash(error)

View File

@ -12,6 +12,10 @@ class Model:
if len(self.cursor.fetchall()) == 0: if len(self.cursor.fetchall()) == 0:
self.cursor.execute("INSERT INTO accounts (email) VALUES (%s)", (email, )) self.cursor.execute("INSERT INTO accounts (email) VALUES (%s)", (email, ))
self.cursor.execute("SELECT token FROM logintokens WHERE email = %s", (email, ))
if len(self.cursor.fetchall()) > 2:
return None
token = generate() token = generate()
self.cursor.execute("INSERT INTO logintokens (email, token) VALUES (%s, %s)", (email, token)) self.cursor.execute("INSERT INTO logintokens (email, token) VALUES (%s, %s)", (email, token))
self.connection.commit() self.connection.commit()
@ -22,7 +26,8 @@ class Model:
self.cursor.execute("SELECT email FROM logintokens WHERE token = %s", (token, )) self.cursor.execute("SELECT email FROM logintokens WHERE token = %s", (token, ))
rows = self.cursor.fetchall() rows = self.cursor.fetchall()
if len(rows) > 0: if len(rows) > 0:
self.cursor.execute("DELETE FROM logintokens WHERE token = %s", (token, )) email = rows[0][0]
self.cursor.execute("DELETE FROM logintokens WHERE email = %s", (email, ))
self.connection.commit() self.connection.commit()
return rows[0][0] return email
return None return None

View File

@ -15,7 +15,7 @@ CREATE TABLE vms (
CREATE TABLE payments ( CREATE TABLE payments (
email TEXT REFERENCES accounts(email) ON DELETE RESTRICT, email TEXT REFERENCES accounts(email) ON DELETE RESTRICT,
created TIMESTAMP NOT NULL DEFAULT NOW(), created TIMESTAMP NOT NULL DEFAULT NOW(),
dollars INTEGER NOT NULL, dollars NUMERIC(8, 2) NOT NULL,
PRIMARY KEY (email, created) PRIMARY KEY (email, created)
); );

View File

@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>{% block title %}{% endblock %}</title> <title>{% block title %}{% endblock %}{% if self.title() %} - {% endif %}capsul</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="/favicon.ico" /> <link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/icon.png" /> <link rel="apple-touch-icon" href="/icon.png" />
@ -14,7 +14,7 @@
<div class="float-right"> <div class="float-right">
{% if session["account"] %} {% if session["account"] %}
<span>{{ session["account"] }}</span> <span>Logged in as {{ session["account"] }}</span>
<a href="{{ url_for('auth.logout') }}">Log Out</a> <a href="{{ url_for('auth.logout') }}">Log Out</a>
{% else %} {% else %}
<a href="{{ url_for('auth.login') }}">Log In</a> <a href="{{ url_for('auth.login') }}">Log In</a>

View File

@ -1,6 +0,0 @@
{% extends 'base.html' %}
{% block content %}
Check Your Email My Dude
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% block title %}check your email{% endblock %}
{% block content %}
check your email. a login link has been sent to {{ email }}
{% endblock %}

View File

@ -1,5 +1,6 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}login{% endblock %}
{% block content %} {% block content %}
<form method="post"> <form method="post">