it sends a magic link when you log in

This commit is contained in:
forest 2020-05-09 22:59:22 -05:00
parent edb476ec41
commit 64bca1fd97
10 changed files with 137 additions and 28 deletions

View File

@ -1,24 +1,40 @@
from flask import Flask from flask import Flask
from flask_mail import Mail
from flask import render_template
import os import os
def create_app(): def create_app():
app = Flask(__name__) app = Flask(__name__)
app.config.from_mapping( app.config.from_mapping(
BASE_URL=os.environ.get("BASE_URL", default="http://localhost:5000"),
SECRET_KEY=os.environ.get("SECRET_KEY", default="dev"), SECRET_KEY=os.environ.get("SECRET_KEY", default="dev"),
DATABASE_URL=os.environ.get("DATABASE_URL", default="sql://postgres:dev@localhost:5432/postgres"), DATABASE_URL=os.environ.get("DATABASE_URL", default="sql://postgres:dev@localhost:5432/postgres"),
DATABASE_SCHEMA=os.environ.get("DATABASE_SCHEMA", default="public"), DATABASE_SCHEMA=os.environ.get("DATABASE_SCHEMA", default="public"),
MAIL_SERVER=os.environ.get("MAIL_SERVER", default="m1.nullhex.com"),
MAIL_PORT=os.environ.get("MAIL_PORT", default="587"),
MAIL_USE_TLS=os.environ.get("MAIL_USE_TLS", default="True").lower() in ['true', '1', 't', 'y', 'yes'],
MAIL_USERNAME=os.environ.get("MAIL_USERNAME", default="forest@nullhex.com"),
MAIL_PASSWORD=os.environ.get("MAIL_PASSWORD", default=""),
MAIL_DEFAULT_SENDER=os.environ.get("MAIL_DEFAULT_SENDER", default="forest@nullhex.com"),
) )
app.config['FLASK_MAIL_INSTANCE'] = Mail(app)
from capsulflask import db from capsulflask import db
db.init_app(app) db.init_app(app)
# from capsulflask import auth, blog @app.route("/")
def index():
return render_template("index.html")
# app.register_blueprint(auth.bp) from capsulflask import auth
# app.register_blueprint(blog.bp)
app.register_blueprint(auth.bp)
app.add_url_rule("/", endpoint="index") app.add_url_rule("/", endpoint="index")
return app return app

View File

@ -1,10 +1,15 @@
import functools import functools
from flask import Blueprint from flask import Blueprint
from flask import flash
from flask import current_app
from flask import g from flask import g
from flask import redirect from flask import redirect
from flask import url_for from flask import url_for
from flask import request
from flask import session from flask import session
from flask import render_template
from flask_mail import Message
from capsulflask.db import get_model from capsulflask.db import get_model
@ -22,32 +27,41 @@ def account_required(view):
return wrapped_view return wrapped_view
@bp.route("/login", methods=("GET", "POST"))
@bp.route("/register", methods=("GET", "POST")) def login():
def register():
if request.method == "POST": if request.method == "POST":
email = request.form["email"] email = request.form["email"]
model = get_model()
error = None error = None
if not email: if not email:
error = "Email is required." error = "Email is required."
elif (
model.
):
error = f"User {username} is already registered."
if error is None: if error is None:
# the name is available, store it in the database and go to token = get_model().login(email)
# the login page
db.execute( link = f"{current_app.config['BASE_URL']}/auth/magic/{token}"
"INSERT INTO user (username, password) VALUES (?, ?)",
(username, generate_password_hash(password)), current_app.config["FLASK_MAIL_INSTANCE"].send(
Message(
"Click This Link to Login to Capsul",
body=f"""
Navigate to {link} to log into capsul.
""",
html=f"""
Navigate to <a href="{link}">{link}</a> to log into capsul.
""",
sender=current_app.config['MAIL_DEFAULT_SENDER'],
recipients=[email]
)
) )
db.commit()
return redirect(url_for("auth.login")) return render_template("check-your-email.html")
flash(error) flash(error)
return render_template("auth/register.html") return render_template("login.html")
@bp.route("/logout")
def logout():
session.clear()
return redirect(url_for("index"))

View File

@ -1,10 +1,21 @@
from nanoid import generate
class Model: class Model:
def __init__(self, connection, cursor): def __init__(self, connection, cursor):
self.connection = connection self.connection = connection
self.cursor = cursor self.cursor = cursor
def emailExists(self, email): def login(self, email):
self.cursor.execute("SELECT * FROM accounts WHERE email = %(email)s", {"email": email}) self.cursor.execute("SELECT * FROM accounts WHERE email = %s", (email, ))
return len(self.cursor.fetchall()) > 0 if len(self.cursor.fetchall()) == 0:
self.cursor.execute("INSERT INTO accounts (email) VALUES (%s)", (email, ))
token = generate()
self.cursor.execute("INSERT INTO logintokens (email, token) VALUES (%s, %s)", (email, token))
self.connection.commit()
return token

View File

@ -2,25 +2,26 @@
CREATE TABLE accounts ( CREATE TABLE accounts (
email TEXT PRIMARY KEY NOT NULL, email TEXT PRIMARY KEY NOT NULL,
created TIMESTAMP NOT NULL DEFAULT NOW()
); );
CREATE TABLE vms ( CREATE TABLE vms (
id TEXT PRIMARY KEY NOT NULL, id TEXT PRIMARY KEY NOT NULL,
email TEXT REFERENCES accounts(email) ON DELETE RESTRICT, email TEXT REFERENCES accounts(email) ON DELETE RESTRICT,
created TIMESTAMP NOT NULL, created TIMESTAMP NOT NULL DEFAULT NOW(),
deleted TIMESTAMP NOT NULL, deleted TIMESTAMP NOT NULL
); );
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, created TIMESTAMP NOT NULL DEFAULT NOW(),
dollars INTEGER NOT NULL, dollars INTEGER NOT NULL,
PRIMARY KEY (email, created) PRIMARY KEY (email, created)
); );
CREATE TABLE logintokens ( CREATE TABLE logintokens (
email TEXT REFERENCES accounts(email) ON DELETE RESTRICT, email TEXT REFERENCES accounts(email) ON DELETE RESTRICT,
created TIMESTAMP NOT NULL, created TIMESTAMP NOT NULL DEFAULT NOW(),
token TEXT NOT NULL, token TEXT NOT NULL,
PRIMARY KEY (email, created) PRIMARY KEY (email, created)
); );

View File

@ -0,0 +1,4 @@
.float-right {
display: inline-block;
float: right;
}

View File

@ -0,0 +1,38 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>{% block title %}{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/icon.png" />
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<header>
<div class="float-right">
{% if g.user %}
<span>{{ g.user['username'] }}</span>
<a href="{{ url_for('auth.logout') }}">Log Out</a>
{% else %}
<a href="{{ url_for('auth.login') }}">Log In</a>
{% endif %}
</div>
<h1>
capsul
</h1>
</header>
<main>
{% for message in get_flashed_messages() %}
<div class="flash">{{ message }}</div>
{% endfor %}
{% block content %}{% endblock %}
</main>
<footer>
</footer>
</body>
</html>

View File

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

View File

@ -0,0 +1,6 @@
{% extends 'base.html' %}
{% block content %}
Index Page!
{% endblock %}

View File

@ -0,0 +1,10 @@
{% extends 'base.html' %}
{% block content %}
<form method="post">
<label for="email">Email Address</label>
<input name="email" id="email" required>
<input type="submit" value="Log In">
</form>
{% endblock %}

View File

@ -1,12 +1,15 @@
astroid==2.4.1 astroid==2.4.1
blinker==1.4
click==7.1.2 click==7.1.2
Flask==1.1.2 Flask==1.1.2
Flask-Mail==0.9.1
isort==4.3.21 isort==4.3.21
itsdangerous==1.1.0 itsdangerous==1.1.0
Jinja2==2.11.2 Jinja2==2.11.2
lazy-object-proxy==1.4.3 lazy-object-proxy==1.4.3
MarkupSafe==1.1.1 MarkupSafe==1.1.1
mccabe==0.6.1 mccabe==0.6.1
nanoid==2.0.0
pkg-resources==0.0.0 pkg-resources==0.0.0
psycopg2==2.8.5 psycopg2==2.8.5
pylint==2.5.2 pylint==2.5.2