Added user authentication for the admin site

Added error pages for 404 and 500 codes
Added site icon
This commit is contained in:
Matthew Welch 2021-03-22 16:23:52 -07:00
parent 197164d691
commit b4efbc8f3f
10 changed files with 119 additions and 7 deletions

View File

@ -1,5 +1,6 @@
import json import json
from flask import render_template, Blueprint, request, jsonify from flask import render_template, Blueprint, request, jsonify, redirect, url_for
from flask_security import roles_required, auth_required
import database import database
from database import get_all_questions, get_question from database import get_all_questions, get_question
@ -8,22 +9,26 @@ Admin = Blueprint("admin", __name__, template_folder="templates")
@Admin.route("/admin/") @Admin.route("/admin/")
@roles_required("admin")
def index(): def index():
return render_template("admin/index.html", title="admin") return render_template("admin/index.html", title="admin")
@Admin.route("/admin/questions/") @Admin.route("/admin/questions/")
@roles_required("admin")
def questions(): def questions():
return render_template("admin/question_list.html") return render_template("admin/question_list.html")
@Admin.route("/admin/questions/edit/<int:question_id>") @Admin.route("/admin/questions/edit/<int:question_id>")
@roles_required("admin")
def edit_question(question_id): def edit_question(question_id):
question: database.AllQuestions = get_question(database.AllQuestions, question_id) question: database.AllQuestions = get_question(database.AllQuestions, question_id)
return render_template("admin/edit_question.html", question=question) return render_template("admin/edit_question.html", question=question)
@Admin.route("/admin/question_query") @Admin.route("/admin/question_query")
@roles_required("admin")
def query_questions(): def query_questions():
offset = request.args.get("offset", type=int) offset = request.args.get("offset", type=int)
limit = request.args.get("limit", type=int) limit = request.args.get("limit", type=int)

View File

@ -3,6 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Admin</title> <title>Admin</title>
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootswatch/4.5.2/cosmo/bootstrap.min.css"> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootswatch/4.5.2/cosmo/bootstrap.min.css">
{% block head %}{% endblock %} {% block head %}{% endblock %}

View File

@ -13,6 +13,7 @@ class Config:
DB_HOST = environ.get("DB_HOST") DB_HOST = environ.get("DB_HOST")
DB_PORT = environ.get("DB_PORT") DB_PORT = environ.get("DB_PORT")
DB_NAME = environ.get("DB_NAME") DB_NAME = environ.get("DB_NAME")
SECURITY_REGISTERABLE = True
class Production(Config): class Production(Config):

View File

@ -1,9 +1,10 @@
from flask import current_app, g from flask import current_app, g
from flask_security import UserMixin, RoleMixin
import sqlalchemy import sqlalchemy
from typing import Union, Optional, Literal, Type, List, Tuple from typing import Union, Optional, Literal, Type, List, Tuple
import random import random
import os import os
from sqlalchemy import Column, JSON, String, Integer, create_engine, ForeignKey, func, ARRAY from sqlalchemy import Column, JSON, String, Integer, create_engine, ForeignKey, func, ARRAY, Boolean, UnicodeText, DateTime
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.orm import sessionmaker, relationship, scoped_session from sqlalchemy.orm import sessionmaker, relationship, scoped_session
@ -16,6 +17,38 @@ engine = create_engine(config.DB_URL)
session_factory = sessionmaker(bind=engine) session_factory = sessionmaker(bind=engine)
Session = scoped_session(session_factory) Session = scoped_session(session_factory)
Base = declarative_base() Base = declarative_base()
Base.query = Session.query_property()
class User(Base, UserMixin):
__tablename__ = "users"
user_id = Column(Integer, primary_key=True)
email = Column(String, unique=True, nullable=False)
username = Column(String, unique=True)
password = Column(String, nullable=False)
active = Column(Boolean, nullable=False)
last_login_at = Column(DateTime())
current_login_at = Column(DateTime())
last_login_ip = Column(String(100))
current_login_ip = Column(String(100))
login_count = Column(Integer)
fs_uniquifier = Column(String, unique=True, nullable=False)
roles = relationship("Role", secondary="users_roles")
class Role(Base, RoleMixin):
__tablename__ = "roles"
role_id = Column(Integer, primary_key=True)
name = Column(String, unique=True)
description = Column(String)
permissions = Column(UnicodeText)
class UsersRoles(Base):
__tablename__ = "users_roles"
user_role_id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.user_id"))
role_id = Column(Integer, ForeignKey("roles.role_id"))
class AllQuestions(Base): class AllQuestions(Base):
@ -91,6 +124,9 @@ class MultipleChoice(Base):
return f"<Question Multiple Choice: {self.question_id}>" return f"<Question Multiple Choice: {self.question_id}>"
Base.metadata.create_all(engine)
def add_multiple_choice_question(question, answer, addresses, difficulty, hint, wrong_answers): def add_multiple_choice_question(question, answer, addresses, difficulty, hint, wrong_answers):
session: sqlalchemy.orm.session.Session = Session() session: sqlalchemy.orm.session.Session = Session()
question_id = session.query(AllQuestions).count() question_id = session.query(AllQuestions).count()
@ -140,7 +176,6 @@ def get_random_hidden_answer(difficulty: Optional[Literal[1, 2, 3]] = None) -> H
def get_random_multiple_choice(difficulty: Optional[Literal[1, 2, 3]] = None) -> MultipleChoice: def get_random_multiple_choice(difficulty: Optional[Literal[1, 2, 3]] = None) -> MultipleChoice:
session: sqlalchemy.orm.session.Session = Session() session: sqlalchemy.orm.session.Session = Session()
print(type(session))
if difficulty is not None: if difficulty is not None:
return session.query(MultipleChoice).filter(MultipleChoice.difficulty == difficulty).order_by(func.random()).first() return session.query(MultipleChoice).filter(MultipleChoice.difficulty == difficulty).order_by(func.random()).first()
return session.query(MultipleChoice).order_by(func.random()).first() return session.query(MultipleChoice).order_by(func.random()).first()

25
main.py
View File

@ -1,13 +1,15 @@
import os import os
from flask import Flask, render_template, request, jsonify from flask import request, render_template, jsonify, Flask
from flask_security import Security from flask_security import Security, SQLAlchemySessionUserDatastore, RegisterForm
import database import database
from admin import admin from admin import admin
app = Flask(__name__) app = Flask(__name__)
app.register_blueprint(admin.Admin)
environment_configuration = os.environ['CONFIGURATION_SETUP'] environment_configuration = os.environ['CONFIGURATION_SETUP']
app.config.from_object(environment_configuration) app.config.from_object(environment_configuration)
user_datastore = SQLAlchemySessionUserDatastore(database.Session, database.User, database.Role)
security = Security(app, user_datastore)
app.register_blueprint(admin.Admin)
@app.route("/") @app.route("/")
@ -31,7 +33,7 @@ def hidden_answer_category():
@app.route("/category/multiple_choice") @app.route("/category/multiple_choice")
def multiple_choice_category(): def multiple_choice_category():
easy = database.get_random_multiple_choice(3) easy = database.get_random_multiple_choice(1)
easy.randomize_answer_list() easy.randomize_answer_list()
medium = database.get_random_multiple_choice(2) medium = database.get_random_multiple_choice(2)
medium.randomize_answer_list() medium.randomize_answer_list()
@ -54,5 +56,20 @@ def check_answer():
return jsonify(database.check_answer(question_id, answer)) return jsonify(database.check_answer(question_id, answer))
@app.errorhandler(404)
def error_404(e):
print(e)
return render_template("error.html", error_msg="The requested page can not be found.", error_code=404), 404
@app.errorhandler(500)
def error_404(e):
print(e)
msg = "There was an error with the server."
if app.config["DEBUG"]:
msg = e
return render_template("error.html", error_msg=msg, error_code=500), 500
if __name__ == "__main__": if __name__ == "__main__":
app.run() app.run()

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

View File

@ -3,6 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Quiz The Word - {{ title }}</title> <title>Quiz The Word - {{ title }}</title>
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootswatch/4.5.2/darkly/bootstrap.min.css"> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootswatch/4.5.2/darkly/bootstrap.min.css">
<link rel="stylesheet" href="/static/style.css"> <link rel="stylesheet" href="/static/style.css">

8
templates/error.html Normal file
View File

@ -0,0 +1,8 @@
{% extends "base.html" %}
{% block body %}
<div class="mt-5 container">
<h1>{{ error_code }}</h1>
<p>{{ error_msg }}</p>
</div>
{% endblock %}

View File

@ -0,0 +1,22 @@
{% extends "base.html" %}
{% block body %}
<div class="container mt-5">
<form method="post">
{{ login_user_form.hidden_tag() }}
<div class="form-group">
<label for="email">Email</label>
<input id="email" name="email" type="email" class="form-control">
</div>
<div class="form-group">
<label for="password">Password</label>
<input id="password" name="password" type="password" class="form-control">
</div>
<div class="form-group custom-control custom-checkbox">
<input id="remember" name="remember" type="checkbox" class="custom-control-input">
<label class="custom-control-label" for="remember">Remember me</label>
</div>
<button id="submit" name="submit" type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,22 @@
{% extends "base.html" %}
{% block body %}
<div class="container mt-5">
<form method="post">
{{ register_user_form.hidden_tag() }}
<div class="form-group">
<label for="email">Email</label>
<input id="email" name="email" type="email" class="form-control">
</div>
<div class="form-group">
<label for="password">Password</label>
<input id="password" name="password" type="password" class="form-control">
</div>
<div class="form-group">
<label for="password_confirm">Retype Password</label>
<input id="password_confirm" name="password_confirm" type="password" class="form-control">
</div>
<button id="submit" name="submit" type="submit" class="btn btn-primary">Register</button>
</form>
</div>
{% endblock %}