bootstrap: db models, login, logout, dashboard pages

This commit is contained in:
Son NK 2019-07-01 18:18:12 +03:00
commit 0b3dd21a06
26 changed files with 464 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.idea/
*.pyc
db.sqlite

0
app/__init__.py Normal file
View File

1
app/auth/__init__.py Normal file
View File

@ -0,0 +1 @@
from .views import login, logout

3
app/auth/base.py Normal file
View File

@ -0,0 +1,3 @@
from flask import Blueprint
auth_bp = Blueprint(name="auth", import_name=__name__, url_prefix="/auth")

View File

36
app/auth/views/login.py Normal file
View File

@ -0,0 +1,36 @@
from flask import request, flash, render_template, redirect, url_for
from flask_login import login_user
from wtforms import Form, StringField, validators
from app.auth.base import auth_bp
from app.log import LOG
from app.models import User
class LoginForm(Form):
email = StringField("Email", validators=[validators.DataRequired()])
password = StringField("Password", validators=[validators.DataRequired()])
@auth_bp.route("/login", methods=["GET", "POST"])
def login():
form = LoginForm(request.form)
if request.method == "POST":
if form.validate():
user = User.query.filter_by(email=form.email.data).first()
if not user:
flash("No such email", "warning")
return render_template("auth/login.html", form=form)
if not user.check_password(form.password.data):
flash("Wrong password", "warning")
return render_template("auth/login.html", form=form)
LOG.debug("log user %s in", user)
login_user(user)
return redirect(url_for("dashboard.index"))
return render_template("auth/login.html", form=form)

10
app/auth/views/logout.py Normal file
View File

@ -0,0 +1,10 @@
from flask import render_template
from flask_login import logout_user
from app.auth.base import auth_bp
@auth_bp.route("/logout")
def logout():
logout_user()
return render_template("auth/logout.html")

View File

@ -0,0 +1 @@
from .views import index

5
app/dashboard/base.py Normal file
View File

@ -0,0 +1,5 @@
from flask import Blueprint
dashboard_bp = Blueprint(
name="dashboard", import_name=__name__, url_prefix="/dashboard"
)

View File

View File

@ -0,0 +1,10 @@
from flask import render_template
from flask_login import login_required
from app.dashboard.base import dashboard_bp
@dashboard_bp.route("/")
@login_required
def index():
return render_template("dashboard/index.html")

34
app/extensions.py Normal file
View File

@ -0,0 +1,34 @@
from flask_login import LoginManager
from flask_sqlalchemy import SQLAlchemy, Model
class CRUDMixin(Model):
"""Mixin that adds convenience methods for CRUD (create, read, update, delete) operations."""
@classmethod
def create(cls, **kwargs):
"""Create a new record and save it the database."""
instance = cls(**kwargs)
return instance.save()
def update(self, commit=True, **kwargs):
"""Update specific fields of a record."""
for attr, value in kwargs.items():
setattr(self, attr, value)
return commit and self.save() or self
def save(self, commit=True):
"""Save the record."""
db.session.add(self)
if commit:
db.session.commit()
return self
def delete(self, commit=True):
"""Remove the record from the database."""
db.session.delete(self)
return commit and db.session.commit()
db = SQLAlchemy(model_class=CRUDMixin)
login_manager = LoginManager()

45
app/log.py Normal file
View File

@ -0,0 +1,45 @@
import logging
import sys
import time
_log_format = "%(asctime)s - %(name)s - %(levelname)s - %(process)d - %(module)s:%(lineno)d - %(funcName)s - %(message)s"
_log_formatter = logging.Formatter(_log_format)
def _get_console_handler(level=None):
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(_log_formatter)
console_handler.formatter.converter = time.gmtime
if level:
console_handler.setLevel(level)
return console_handler
def get_logger(name):
logger = logging.getLogger(name)
logger.setLevel(logging.DEBUG)
# leave the handlers level at NOTSET so the level checking is only handled by the logger
logger.addHandler(_get_console_handler())
# no propagation to avoid unexpected behaviour
logger.propagate = False
return logger
print(f">>> init logging <<<")
# ### config root logger ###
# do not use the default (buggy) logger
logging.root.handlers.clear()
# add handlers with the default level = "warn"
# need to add level at handler level as there's no level check in root logger
# all the libs logs having level >= WARN will be handled by these 2 handlers
logging.root.addHandler(_get_console_handler(logging.WARN))
LOG = get_logger("yourkey")

51
app/models.py Normal file
View File

@ -0,0 +1,51 @@
# <<< Models >>>
from datetime import datetime
import bcrypt
from flask_login import UserMixin
from app.extensions import db
class ModelMixin(object):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
updated_at = db.Column(db.DateTime, default=None, onupdate=datetime.utcnow)
class Client(db.Model, ModelMixin):
client_id = db.Column(db.String(128), unique=True)
client_secret = db.Column(db.String(128))
redirect_uri = db.Column(db.String(1024))
name = db.Column(db.String(128))
class User(db.Model, ModelMixin, UserMixin):
email = db.Column(db.String(128), unique=True)
salt = db.Column(db.String(128), nullable=False)
password = db.Column(db.String(128), nullable=False)
name = db.Column(db.String(128))
def set_password(self, password):
salt = bcrypt.gensalt()
password_hash = bcrypt.hashpw(password.encode(), salt).decode()
self.salt = salt.decode()
self.password = password_hash
def check_password(self, password) -> bool:
password_hash = bcrypt.hashpw(password.encode(), self.salt.encode())
return self.password.encode() == password_hash
class AuthorizationCode(db.Model, ModelMixin):
code = db.Column(db.String(128), unique=True)
client_id = db.Column(db.ForeignKey(Client.id))
user_id = db.Column(db.ForeignKey(User.id))
class OauthToken(db.Model, ModelMixin):
access_token = db.Column(db.String(128), unique=True)
client_id = db.Column(db.ForeignKey(Client.id))
user_id = db.Column(db.ForeignKey(User.id))
user = db.relationship(User)

1
app/monitor/__init__.py Normal file
View File

@ -0,0 +1 @@
from . import views

3
app/monitor/base.py Normal file
View File

@ -0,0 +1,3 @@
from flask import Blueprint
monitor_bp = Blueprint(name="monitor", import_name=__name__, url_prefix="/")

16
app/monitor/views.py Normal file
View File

@ -0,0 +1,16 @@
import subprocess
from app.monitor.base import monitor_bp
SHA1 = subprocess.getoutput("git rev-parse HEAD")
@monitor_bp.route("/git")
def git_sha1():
return SHA1
@monitor_bp.route("/exception")
def test_exception():
raise Exception("to make sure sentry works")
return "never reach here"

8
app/utils.py Normal file
View File

@ -0,0 +1,8 @@
import random
import string
def random_string(length=10):
"""Generate a random string of fixed length """
letters = string.ascii_lowercase
return "".join(random.choice(letters) for _ in range(length))

4
requirements.txt Normal file
View File

@ -0,0 +1,4 @@
flask_sqlalchemy
flask
flask_login
wtforms

76
server.py Normal file
View File

@ -0,0 +1,76 @@
import os
from flask import Flask
from app.auth.base import auth_bp
from app.dashboard.base import dashboard_bp
from app.extensions import db, login_manager
from app.log import LOG
from app.models import Client, User
from app.monitor.base import monitor_bp
def create_app() -> Flask:
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///db.sqlite"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.secret_key = "secret"
app.config["TEMPLATES_AUTO_RELOAD"] = True
init_extensions(app)
register_blueprints(app)
return app
def fake_data():
# Remove db if exist
if os.path.exists("db.sqlite"):
os.remove("db.sqlite")
db.create_all()
# fake data
client = Client(
client_id="client-id",
client_secret="client-secret",
redirect_uri="http://localhost:7000/callback",
name="Continental",
)
db.session.add(client)
user = User(id=1, email="john@wick.com", name="John Wick")
user.set_password("password")
db.session.add(user)
db.session.commit()
@login_manager.user_loader
def load_user(user_id):
user = User.query.get(user_id)
return user
def register_blueprints(app: Flask):
app.register_blueprint(auth_bp)
app.register_blueprint(monitor_bp)
app.register_blueprint(dashboard_bp)
def init_extensions(app: Flask):
LOG.debug("init extensions")
login_manager.init_app(app)
db.init_app(app)
if __name__ == "__main__":
app = create_app()
with app.app_context():
fake_data()
app.run(debug=True, threaded=False)

View File

@ -0,0 +1,20 @@
{% macro render_field(field) %}
<div class="form-group row">
<label class="col-sm-2 col-form-label">{{ field.label }}</label>
<div class="col-sm-10">
{{ field(**kwargs)|safe }}
<small class="form-text text-muted">
{{ field.description }}
</small>
{% if field.errors %}
<ul class=errors>
{% for error in field.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
</div>
{% endmacro %}

17
templates/auth/login.html Normal file
View File

@ -0,0 +1,17 @@
{% from "_formhelpers.html" import render_field %}
{% extends 'base.html' %}
{% block title %}
Login
{% endblock %}
{% block content %}
<div class="row">
<form action="" method="post">
{{ render_field(form.email) }}
{{ render_field(form.password) }}
<button type="submit" class="btn btn-primary">Login</button>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,12 @@
{% from "_formhelpers.html" import render_field %}
{% extends 'base.html' %}
{% block title %}
Logout
{% endblock %}
{% block content %}
You are logged out. <br>
<a href="{{ url_for('auth.login') }}">Login</a>
{% endblock %}

65
templates/base.html Normal file
View File

@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>
{% block title %}{% endblock %} - Your Key
</title>
<!-- Bootstrap -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css">
{% block head %}
{% endblock %}
</head>
<body>
{% block nav %}
{% endblock %}
<div class="container">
{% with messages = get_flashed_messages(with_categories=true) %}
<!-- Categories: success (green), info (blue), warning (yellow), danger (red) -->
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span
aria-hidden="true">&times;</span></button>
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"
integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM"
crossorigin="anonymous"></script>
{% block script %}
{% endblock %}
</body>
</html>

30
templates/base_app.html Normal file
View File

@ -0,0 +1,30 @@
{# Base for all pages after user logs in #}
{% extends 'base.html' %}
{% block nav %}
{% set navigation_bar = [
(url_for("dashboard.index"), 'dashboard', 'Dashboard'),
]-%}
{% set active_page = active_page|default('index') -%}
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<ul class="navbar-nav mr-auto">
{% for href, id, caption in navigation_bar %}
<li{% if id == active_page %} class="nav-item active" {% else %} class="nav-item" {% endif %}>
<a class="nav-link" href="{{ href|e }}">{{ caption|e }}
</a>
</li>
{% endfor %}
</ul>
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.logout') }}">
{{ current_user.email }} (Logout)
</a>
</li>
</ul>
</nav>
{% endblock %}

View File

@ -0,0 +1,13 @@
{% from "_formhelpers.html" import render_field %}
{% extends 'base_app.html' %}
{% set active_page = "dashboard" %}
{% block title %}
Dashboard
{% endblock %}
{% block content %}
Dashboard
{% endblock %}