Sfoglia il codice sorgente

Add flask app and test file

JoostSijm 6 anni fa
parent
commit
72510b7899

+ 38 - 0
app/__init__.py

@@ -0,0 +1,38 @@
+
+"""
+Website and API
+"""
+
+from flask import Flask
+from flask_sqlalchemy import SQLAlchemy
+from flask_login import LoginManager
+from flask_migrate import Migrate
+
+# App
+app = Flask(__name__)
+app.config.from_object(__name__)
+app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
+app.config.from_envvar('FLASKR_SETTINGS', silent=True)
+app.config.update(
+    TESTING=True,
+    SQLALCHEMY_DATABASE_URI='mysql://PAD_Database:PAD_Database@db.pla33.ga:17204/PAD_Database',
+    SECRET_KEY='g6DGM5y2bVhb0mxdCRELI5m7fnzzoJ2y',
+    SQLALCHEMY_TRACK_MODIFICATIONS=False,
+    SEND_FILE_MAX_AGE_DEFAULT=1296000,
+)
+
+
+# DB
+db = SQLAlchemy(app)
+
+# Migration
+migrate = Migrate(app, db)
+
+# Login
+login_manager = LoginManager()
+login_manager.init_app(app)
+login_manager.login_view = "login"
+login_manager.login_message_category = "warning"
+
+# config
+app.config.update(DEBUG=True, SECRET_KEY='iliasmitchelrobintimjoost')

+ 181 - 0
app/flaskr.py

@@ -0,0 +1,181 @@
+
+"""
+Simple flask thing
+"""
+
+import random
+import string
+from datetime import datetime
+from flask import render_template, request, redirect, \
+    flash, url_for, abort, json
+from flask_breadcrumbs import Breadcrumbs, register_breadcrumb
+from flask_menu import Menu, register_menu
+from flask_login import login_required, login_user, logout_user
+from app import app, login_manager, db
+from app.models import User, Key, Function, Log
+
+Menu(app=app)
+Breadcrumbs(app=app)
+
+
+@login_manager.user_loader
+def load_user(id):
+    """Return user"""
+    return User.query.get(id)
+
+
+@register_breadcrumb(app, '.login', 'Login')
+@app.route("/login", methods=["GET", "POST"])
+def login():
+    """Handle login page and data"""
+    if request.method == 'POST':
+        email = request.form['email']
+        password = request.form['password']
+        user = User.query.filter(User.email == email).first()
+        if user is not None:
+            if user.password == password:
+                login_user(user)
+                flash('You were successfully logged in', 'success')
+                if request.args.get("next") is not None:
+                    return redirect(request.args.get("next"))
+                else:
+                    return redirect(url_for('index'))
+            else:
+                flash('Incorrect password', 'danger')
+        else:
+            flash('User not found', 'danger')
+
+        return redirect(url_for('login'))
+    else:
+        return render_template('site/login.html')
+
+
+@app.route("/register", methods=["POST"])
+def register():
+    """Register a new user"""
+    user = User()
+    user.name = request.form['name']
+    user.email = request.form['email']
+    user.password = request.form['password']
+    db.session.add(user)
+    db.session.commit()
+    login_user(user)
+    flash('Succesfully registered account', 'success')
+    if request.args.get("next") is not None:
+        return redirect(request.args.get("next"))
+    else:
+        return redirect(url_for('index'))
+
+
+@app.route("/logout")
+@login_required
+def logout():
+    """Logout function for users"""
+    logout_user()
+    flash('succesfully logged out', 'success')
+    return redirect(url_for('login'))
+
+
+@app.route('/')
+@register_menu(app, '.', 'Home')
+@register_breadcrumb(app, '.', 'Home')
+def index():
+    """Show homepage"""
+    users = User.query.count()
+    return render_template('site/index.html', users=users)
+
+
+@app.route('/users')
+@register_menu(app, 'users', 'Users')
+@register_breadcrumb(app, '.users', 'Users')
+@login_required
+def user_index():
+    """Show users"""
+    users = User.query.all()
+    return render_template('user/index.html', users=users)
+
+
+def user_overview_dlc(*args, **kwargs):
+    """Generate dynamic_list for user"""
+    id = request.view_args['id']
+    user = User.query.get(id)
+    return [{'text': user.email, 'url': user.url}]
+
+
+@app.route('/user/<int:id>')
+@register_breadcrumb(app, '.users.id', '',
+                     dynamic_list_constructor=user_overview_dlc)
+@login_required
+def user_overview(id):
+    """Show user overview"""
+    id = int(id)
+    user = User.query.get(id)
+    return render_template('user/overview.html', user=user)
+
+
+@app.route('/user/<int:id>/generate_key')
+@login_required
+def user_generate_key(id):
+    """Generate new key for user"""
+    user = User.query.get(id)
+    key = Key()
+    key.key = ''.join(random.choices(
+        string.ascii_letters + string.digits, k=64
+    ))
+    key.user_id = user.id
+    db.session.add(key)
+    db.session.commit()
+    flash('Succesfully generated key', 'success')
+    return redirect(url_for('user_overview', id=user.id))
+
+
+@app.route('/user/<int:user_id>/key/<int:key_id>/activate')
+@login_required
+def user_toogle_key(user_id, key_id):
+    """Activate key"""
+    user = User.query.get(user_id)
+    key = Key.query.get(key_id)
+    key.active = not key.active
+    db.session.add(key)
+    db.session.commit()
+
+    if key.active:
+        flash('Activated key', 'success')
+    else:
+        flash('Deactivated key', 'success')
+
+    return redirect(url_for('user_overview', id=user.id))
+
+
+@app.route('/api/authenticated', methods=["POST"])
+def api_authenticated():
+    """Check key"""
+    if 'Authorization' not in request.headers:
+        return abort(403)
+
+    authorization = request.headers['authorization']
+    key = Key.query.filter(Key.key == authorization).count()
+
+    if key:
+        return json.dumps(True)
+
+    return json.dumps(False)
+
+
+@app.route('/api/request', methods=["GET"])
+def api_log():
+    """Check key"""
+    if 'Authorization' not in request.headers:
+        return abort(403)
+
+    authorization = request.headers['authorization']
+    key = Key.query.filter(Key.key == authorization).first()
+    if not key or not key.active:
+        return abort(403)
+
+    log = Log()
+
+    db.session.add(log)
+    db.session.commit()
+
+    return json.dumps(True)

+ 80 - 0
app/models.py

@@ -0,0 +1,80 @@
+"""Models"""
+
+from datetime import datetime
+from sqlalchemy.ext.hybrid import hybrid_property, hybrid_method
+from app import db
+from flask_login import UserMixin
+
+
+class User(db.Model, UserMixin):
+    """Model for User"""
+    id = db.Column(db.Integer, primary_key=True)
+    email = db.Column(db.String)
+    password = db.Column(db.String)
+    name = db.Column(db.String)
+    registration_at = db.Column(db.DateTime, default=datetime.utcnow)
+
+    def __init__(self, id=None):
+        self.id = id
+
+    @hybrid_property
+    def key_count(self):
+        """Return amount of keys"""
+        return self.keys.count()
+
+
+class Key(db.Model):
+    """Model for Key """
+    id = db.Column(db.Integer, primary_key=True)
+    key = db.Column(db.String, nullable=True)
+    created_at = db.Column(db.DateTime, default=datetime.utcnow)
+    expire_at = db.Column(db.DateTime)
+    active = db.Column(db.Boolean, default=False)
+    used_at = db.Column(db.DateTime)
+    uses = db.Column(db.Integer, default=0)
+
+    user_id = db.Column(
+        db.Integer,
+        db.ForeignKey('user.id')
+    )
+    user = db.relationship(
+        "User",
+        back_populates="keys"
+    )
+
+    @hybrid_method
+    def key_count(self):
+        """increment use"""
+        self.used_at = datetime.now()
+        self.uses += 1
+        return self.keys.count()
+
+
+class Request(db.Model):
+    """Model for function"""
+    id = db.Column(db.Integer, primary_key=True)
+    name = db.Column(db.String)
+
+
+class Log(db.Model):
+    """Model for Key """
+    id = db.Column(db.Integer, primary_key=True)
+    succes = db.Column(db.Boolean, default=False)
+    date_time = db.Column(db.DateTime)
+
+    key_id = db.Column(
+        db.Integer,
+        db.ForeignKey('key.id')
+    )
+    key = db.relationship(
+        "Key",
+        back_populates="logs"
+    )
+    request_id = db.Column(
+        db.Integer,
+        db.ForeignKey('request.id')
+    )
+    request = db.relationship(
+        "Request",
+        back_populates="logs"
+    )

+ 4 - 0
app/static/css/custom.css

@@ -0,0 +1,4 @@
+.dataTables_wrapper.container-fluid {
+  padding-left: 0;
+  padding-right: 0;
+}

+ 478 - 0
app/static/css/sb-admin.css

@@ -0,0 +1,478 @@
+html {
+  position: relative;
+  min-height: 100%;
+}
+
+body {
+  overflow-x: hidden;
+}
+
+body.sticky-footer {
+  margin-bottom: 56px;
+}
+
+body.sticky-footer .content-wrapper {
+  min-height: calc(100vh - 56px - 56px);
+}
+
+body.fixed-nav {
+  padding-top: 56px;
+}
+
+.content-wrapper {
+  min-height: calc(100vh - 56px);
+  padding-top: 1rem;
+}
+
+.scroll-to-top {
+  position: fixed;
+  right: 15px;
+  bottom: 3px;
+  display: none;
+  width: 50px;
+  height: 50px;
+  text-align: center;
+  color: white;
+  background: rgba(52, 58, 64, 0.5);
+  line-height: 45px;
+}
+
+.scroll-to-top:focus, .scroll-to-top:hover {
+  color: white;
+}
+
+.scroll-to-top:hover {
+  background: #343a40;
+}
+
+.scroll-to-top i {
+  font-weight: 800;
+}
+
+.smaller {
+  font-size: 0.7rem;
+}
+
+.o-hidden {
+  overflow: hidden !important;
+}
+
+.z-0 {
+  z-index: 0;
+}
+
+.z-1 {
+  z-index: 1;
+}
+
+#mainNav .navbar-collapse {
+  overflow: auto;
+  max-height: 75vh;
+}
+
+#mainNav .navbar-collapse .navbar-nav .nav-item .nav-link {
+  cursor: pointer;
+}
+
+#mainNav .navbar-collapse .navbar-sidenav .nav-link-collapse:after {
+  float: right;
+  content: '\f107';
+  font-family: 'FontAwesome';
+}
+
+#mainNav .navbar-collapse .navbar-sidenav .nav-link-collapse.collapsed:after {
+  content: '\f105';
+}
+
+#mainNav .navbar-collapse .navbar-sidenav .sidenav-second-level,
+#mainNav .navbar-collapse .navbar-sidenav .sidenav-third-level {
+  padding-left: 0;
+}
+
+#mainNav .navbar-collapse .navbar-sidenav .sidenav-second-level > li > a,
+#mainNav .navbar-collapse .navbar-sidenav .sidenav-third-level > li > a {
+  display: block;
+  padding: 0.5em 0;
+}
+
+#mainNav .navbar-collapse .navbar-sidenav .sidenav-second-level > li > a:focus, #mainNav .navbar-collapse .navbar-sidenav .sidenav-second-level > li > a:hover,
+#mainNav .navbar-collapse .navbar-sidenav .sidenav-third-level > li > a:focus,
+#mainNav .navbar-collapse .navbar-sidenav .sidenav-third-level > li > a:hover {
+  text-decoration: none;
+}
+
+#mainNav .navbar-collapse .navbar-sidenav .sidenav-second-level > li > a {
+  padding-left: 1em;
+}
+
+#mainNav .navbar-collapse .navbar-sidenav .sidenav-third-level > li > a {
+  padding-left: 2em;
+}
+
+#mainNav .navbar-collapse .sidenav-toggler {
+  display: none;
+}
+
+#mainNav .navbar-collapse .navbar-nav > .nav-item.dropdown > .nav-link {
+  position: relative;
+  min-width: 45px;
+}
+
+#mainNav .navbar-collapse .navbar-nav > .nav-item.dropdown > .nav-link:after {
+  float: right;
+  width: auto;
+  content: '\f105';
+  border: none;
+  font-family: 'FontAwesome';
+}
+
+#mainNav .navbar-collapse .navbar-nav > .nav-item.dropdown > .nav-link .indicator {
+  position: absolute;
+  top: 5px;
+  left: 21px;
+  font-size: 10px;
+}
+
+#mainNav .navbar-collapse .navbar-nav > .nav-item.dropdown.show > .nav-link:after {
+  content: '\f107';
+}
+
+#mainNav .navbar-collapse .navbar-nav > .nav-item.dropdown .dropdown-menu > .dropdown-item > .dropdown-message {
+  overflow: hidden;
+  max-width: none;
+  text-overflow: ellipsis;
+}
+
+@media (min-width: 992px) {
+  #mainNav .navbar-brand {
+    width: 250px;
+  }
+  #mainNav .navbar-collapse {
+    overflow: visible;
+    max-height: none;
+  }
+  #mainNav .navbar-collapse .navbar-sidenav {
+    position: absolute;
+    top: 0;
+    left: 0;
+    -webkit-flex-direction: column;
+    -ms-flex-direction: column;
+    flex-direction: column;
+    margin-top: 56px;
+  }
+  #mainNav .navbar-collapse .navbar-sidenav > .nav-item {
+    width: 250px;
+    padding: 0;
+  }
+  #mainNav .navbar-collapse .navbar-sidenav > .nav-item > .nav-link {
+    padding: 1em;
+  }
+  #mainNav .navbar-collapse .navbar-sidenav > .nav-item .sidenav-second-level,
+  #mainNav .navbar-collapse .navbar-sidenav > .nav-item .sidenav-third-level {
+    padding-left: 0;
+    list-style: none;
+  }
+  #mainNav .navbar-collapse .navbar-sidenav > .nav-item .sidenav-second-level > li,
+  #mainNav .navbar-collapse .navbar-sidenav > .nav-item .sidenav-third-level > li {
+    width: 250px;
+  }
+  #mainNav .navbar-collapse .navbar-sidenav > .nav-item .sidenav-second-level > li > a,
+  #mainNav .navbar-collapse .navbar-sidenav > .nav-item .sidenav-third-level > li > a {
+    padding: 1em;
+  }
+  #mainNav .navbar-collapse .navbar-sidenav > .nav-item .sidenav-second-level > li > a {
+    padding-left: 2.75em;
+  }
+  #mainNav .navbar-collapse .navbar-sidenav > .nav-item .sidenav-third-level > li > a {
+    padding-left: 3.75em;
+  }
+  #mainNav .navbar-collapse .navbar-nav > .nav-item.dropdown > .nav-link {
+    min-width: 0;
+  }
+  #mainNav .navbar-collapse .navbar-nav > .nav-item.dropdown > .nav-link:after {
+    width: 24px;
+    text-align: center;
+  }
+  #mainNav .navbar-collapse .navbar-nav > .nav-item.dropdown .dropdown-menu > .dropdown-item > .dropdown-message {
+    max-width: 300px;
+  }
+}
+
+#mainNav.fixed-top .sidenav-toggler {
+  display: none;
+}
+
+@media (min-width: 992px) {
+  #mainNav.fixed-top .navbar-sidenav {
+    height: calc(100vh - 112px);
+  }
+  #mainNav.fixed-top .sidenav-toggler {
+    position: absolute;
+    top: 0;
+    left: 0;
+    display: flex;
+    -webkit-flex-direction: column;
+    -ms-flex-direction: column;
+    flex-direction: column;
+    margin-top: calc(100vh - 56px);
+  }
+  #mainNav.fixed-top .sidenav-toggler > .nav-item {
+    width: 250px;
+    padding: 0;
+  }
+  #mainNav.fixed-top .sidenav-toggler > .nav-item > .nav-link {
+    padding: 1em;
+  }
+}
+
+#mainNav.fixed-top.navbar-dark .sidenav-toggler {
+  background-color: #212529;
+}
+
+#mainNav.fixed-top.navbar-dark .sidenav-toggler a i {
+  color: #adb5bd;
+}
+
+#mainNav.fixed-top.navbar-light .sidenav-toggler {
+  background-color: #dee2e6;
+}
+
+#mainNav.fixed-top.navbar-light .sidenav-toggler a i {
+  color: rgba(0, 0, 0, 0.5);
+}
+
+body.sidenav-toggled #mainNav.fixed-top .sidenav-toggler {
+  overflow-x: hidden;
+  width: 55px;
+}
+
+body.sidenav-toggled #mainNav.fixed-top .sidenav-toggler .nav-item,
+body.sidenav-toggled #mainNav.fixed-top .sidenav-toggler .nav-link {
+  width: 55px !important;
+}
+
+body.sidenav-toggled #mainNav.fixed-top #sidenavToggler i {
+  -webkit-transform: scaleX(-1);
+  -moz-transform: scaleX(-1);
+  -o-transform: scaleX(-1);
+  transform: scaleX(-1);
+  filter: FlipH;
+  -ms-filter: 'FlipH';
+}
+
+#mainNav.static-top .sidenav-toggler {
+  display: none;
+}
+
+@media (min-width: 992px) {
+  #mainNav.static-top .sidenav-toggler {
+    display: flex;
+  }
+}
+
+body.sidenav-toggled #mainNav.static-top #sidenavToggler i {
+  -webkit-transform: scaleX(-1);
+  -moz-transform: scaleX(-1);
+  -o-transform: scaleX(-1);
+  transform: scaleX(-1);
+  filter: FlipH;
+  -ms-filter: 'FlipH';
+}
+
+.content-wrapper {
+  overflow-x: hidden;
+  background: white;
+}
+
+@media (min-width: 992px) {
+  .content-wrapper {
+    margin-left: 250px;
+  }
+}
+
+#sidenavToggler i {
+  font-weight: 800;
+}
+
+.navbar-sidenav-tooltip.show {
+  display: none;
+}
+
+@media (min-width: 992px) {
+  body.sidenav-toggled .content-wrapper {
+    margin-left: 55px;
+  }
+}
+
+body.sidenav-toggled .navbar-sidenav {
+  width: 55px;
+}
+
+body.sidenav-toggled .navbar-sidenav .nav-link-text {
+  display: none;
+}
+
+body.sidenav-toggled .navbar-sidenav .nav-item,
+body.sidenav-toggled .navbar-sidenav .nav-link {
+  width: 55px !important;
+}
+
+body.sidenav-toggled .navbar-sidenav .nav-item:after,
+body.sidenav-toggled .navbar-sidenav .nav-link:after {
+  display: none;
+}
+
+body.sidenav-toggled .navbar-sidenav .nav-item {
+  white-space: nowrap;
+}
+
+body.sidenav-toggled .navbar-sidenav-tooltip.show {
+  display: flex;
+}
+
+#mainNav.navbar-dark .navbar-collapse .navbar-sidenav .nav-link-collapse:after {
+  color: #868e96;
+}
+
+#mainNav.navbar-dark .navbar-collapse .navbar-sidenav > .nav-item > .nav-link {
+  color: #868e96;
+}
+
+#mainNav.navbar-dark .navbar-collapse .navbar-sidenav > .nav-item > .nav-link:hover {
+  color: #adb5bd;
+}
+
+#mainNav.navbar-dark .navbar-collapse .navbar-sidenav > .nav-item .sidenav-second-level > li > a,
+#mainNav.navbar-dark .navbar-collapse .navbar-sidenav > .nav-item .sidenav-third-level > li > a {
+  color: #868e96;
+}
+
+#mainNav.navbar-dark .navbar-collapse .navbar-sidenav > .nav-item .sidenav-second-level > li > a:focus, #mainNav.navbar-dark .navbar-collapse .navbar-sidenav > .nav-item .sidenav-second-level > li > a:hover,
+#mainNav.navbar-dark .navbar-collapse .navbar-sidenav > .nav-item .sidenav-third-level > li > a:focus,
+#mainNav.navbar-dark .navbar-collapse .navbar-sidenav > .nav-item .sidenav-third-level > li > a:hover {
+  color: #adb5bd;
+}
+
+#mainNav.navbar-dark .navbar-collapse .navbar-nav > .nav-item.dropdown > .nav-link:after {
+  color: #adb5bd;
+}
+
+@media (min-width: 992px) {
+  #mainNav.navbar-dark .navbar-collapse .navbar-sidenav {
+    background: #343a40;
+  }
+  #mainNav.navbar-dark .navbar-collapse .navbar-sidenav li.active a {
+    color: white !important;
+    background-color: #495057;
+  }
+  #mainNav.navbar-dark .navbar-collapse .navbar-sidenav li.active a:focus, #mainNav.navbar-dark .navbar-collapse .navbar-sidenav li.active a:hover {
+    color: white;
+  }
+  #mainNav.navbar-dark .navbar-collapse .navbar-sidenav > .nav-item .sidenav-second-level,
+  #mainNav.navbar-dark .navbar-collapse .navbar-sidenav > .nav-item .sidenav-third-level {
+    background: #343a40;
+  }
+}
+
+#mainNav.navbar-light .navbar-collapse .navbar-sidenav .nav-link-collapse:after {
+  color: rgba(0, 0, 0, 0.5);
+}
+
+#mainNav.navbar-light .navbar-collapse .navbar-sidenav > .nav-item > .nav-link {
+  color: rgba(0, 0, 0, 0.5);
+}
+
+#mainNav.navbar-light .navbar-collapse .navbar-sidenav > .nav-item > .nav-link:hover {
+  color: rgba(0, 0, 0, 0.7);
+}
+
+#mainNav.navbar-light .navbar-collapse .navbar-sidenav > .nav-item .sidenav-second-level > li > a,
+#mainNav.navbar-light .navbar-collapse .navbar-sidenav > .nav-item .sidenav-third-level > li > a {
+  color: rgba(0, 0, 0, 0.5);
+}
+
+#mainNav.navbar-light .navbar-collapse .navbar-sidenav > .nav-item .sidenav-second-level > li > a:focus, #mainNav.navbar-light .navbar-collapse .navbar-sidenav > .nav-item .sidenav-second-level > li > a:hover,
+#mainNav.navbar-light .navbar-collapse .navbar-sidenav > .nav-item .sidenav-third-level > li > a:focus,
+#mainNav.navbar-light .navbar-collapse .navbar-sidenav > .nav-item .sidenav-third-level > li > a:hover {
+  color: rgba(0, 0, 0, 0.7);
+}
+
+#mainNav.navbar-light .navbar-collapse .navbar-nav > .nav-item.dropdown > .nav-link:after {
+  color: rgba(0, 0, 0, 0.5);
+}
+
+@media (min-width: 992px) {
+  #mainNav.navbar-light .navbar-collapse .navbar-sidenav {
+    background: #f8f9fa;
+  }
+  #mainNav.navbar-light .navbar-collapse .navbar-sidenav li.active a {
+    color: #000 !important;
+    background-color: #e9ecef;
+  }
+  #mainNav.navbar-light .navbar-collapse .navbar-sidenav li.active a:focus, #mainNav.navbar-light .navbar-collapse .navbar-sidenav li.active a:hover {
+    color: #000;
+  }
+  #mainNav.navbar-light .navbar-collapse .navbar-sidenav > .nav-item .sidenav-second-level,
+  #mainNav.navbar-light .navbar-collapse .navbar-sidenav > .nav-item .sidenav-third-level {
+    background: #f8f9fa;
+  }
+}
+
+.card-body-icon {
+  position: absolute;
+  z-index: 0;
+  top: -25px;
+  right: -25px;
+  font-size: 5rem;
+  -webkit-transform: rotate(15deg);
+  -ms-transform: rotate(15deg);
+  transform: rotate(15deg);
+}
+
+@media (min-width: 576px) {
+  .card-columns {
+    column-count: 1;
+  }
+}
+
+@media (min-width: 768px) {
+  .card-columns {
+    column-count: 2;
+  }
+}
+
+@media (min-width: 1200px) {
+  .card-columns {
+    column-count: 2;
+  }
+}
+
+.card-login {
+  max-width: 25rem;
+}
+
+.card-register {
+  max-width: 40rem;
+}
+
+footer.sticky-footer {
+  position: absolute;
+  right: 0;
+  bottom: 0;
+  width: 100%;
+  height: 56px;
+  background-color: #e9ecef;
+  line-height: 55px;
+}
+
+@media (min-width: 992px) {
+  footer.sticky-footer {
+    width: calc(100% - 250px);
+  }
+}
+
+@media (min-width: 992px) {
+  body.sidenav-toggled footer.sticky-footer {
+    width: calc(100% - 55px);
+  }
+}

+ 108 - 0
app/static/js/site/sb-admin-charts.js

@@ -0,0 +1,108 @@
+// Chart.js scripts
+// -- Set new default font family and font color to mimic Bootstrap's default styling
+Chart.defaults.global.defaultFontFamily = '-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif';
+Chart.defaults.global.defaultFontColor = '#292b2c';
+// -- Area Chart Example
+var ctx = document.getElementById("myAreaChart");
+var myLineChart = new Chart(ctx, {
+  type: 'line',
+  data: {
+    labels: ["Mar 1", "Mar 2", "Mar 3", "Mar 4", "Mar 5", "Mar 6", "Mar 7", "Mar 8", "Mar 9", "Mar 10", "Mar 11", "Mar 12", "Mar 13"],
+    datasets: [{
+      label: "Sessions",
+      lineTension: 0.3,
+      backgroundColor: "rgba(2,117,216,0.2)",
+      borderColor: "rgba(2,117,216,1)",
+      pointRadius: 5,
+      pointBackgroundColor: "rgba(2,117,216,1)",
+      pointBorderColor: "rgba(255,255,255,0.8)",
+      pointHoverRadius: 5,
+      pointHoverBackgroundColor: "rgba(2,117,216,1)",
+      pointHitRadius: 20,
+      pointBorderWidth: 2,
+      data: [10000, 30162, 26263, 18394, 18287, 28682, 31274, 33259, 25849, 24159, 32651, 31984, 38451],
+    }],
+  },
+  options: {
+    scales: {
+      xAxes: [{
+        time: {
+          unit: 'date'
+        },
+        gridLines: {
+          display: false
+        },
+        ticks: {
+          maxTicksLimit: 7
+        }
+      }],
+      yAxes: [{
+        ticks: {
+          min: 0,
+          max: 40000,
+          maxTicksLimit: 5
+        },
+        gridLines: {
+          color: "rgba(0, 0, 0, .125)",
+        }
+      }],
+    },
+    legend: {
+      display: false
+    }
+  }
+});
+// -- Bar Chart Example
+var ctx = document.getElementById("myBarChart");
+var myLineChart = new Chart(ctx, {
+  type: 'bar',
+  data: {
+    labels: ["January", "February", "March", "April", "May", "June"],
+    datasets: [{
+      label: "Revenue",
+      backgroundColor: "rgba(2,117,216,1)",
+      borderColor: "rgba(2,117,216,1)",
+      data: [4215, 5312, 6251, 7841, 9821, 14984],
+    }],
+  },
+  options: {
+    scales: {
+      xAxes: [{
+        time: {
+          unit: 'month'
+        },
+        gridLines: {
+          display: false
+        },
+        ticks: {
+          maxTicksLimit: 6
+        }
+      }],
+      yAxes: [{
+        ticks: {
+          min: 0,
+          max: 15000,
+          maxTicksLimit: 5
+        },
+        gridLines: {
+          display: true
+        }
+      }],
+    },
+    legend: {
+      display: false
+    }
+  }
+});
+// -- Pie Chart Example
+var ctx = document.getElementById("myPieChart");
+var myPieChart = new Chart(ctx, {
+  type: 'pie',
+  data: {
+    labels: ["Blue", "Red", "Yellow", "Green"],
+    datasets: [{
+      data: [12.21, 15.58, 11.25, 8.32],
+      backgroundColor: ['#007bff', '#dc3545', '#ffc107', '#28a745'],
+    }],
+  },
+});

+ 4 - 0
app/static/js/site/sb-admin-datatables.js

@@ -0,0 +1,4 @@
+// Call the dataTables jQuery plugin
+$(document).ready(function() {
+	$('.dataTable').DataTable();
+});

+ 45 - 0
app/static/js/site/sb-admin.js

@@ -0,0 +1,45 @@
+(function($) {
+  "use strict"; // Start of use strict
+  // Configure tooltips for collapsed side navigation
+  $('.navbar-sidenav [data-toggle="tooltip"]').tooltip({
+    template: '<div class="tooltip navbar-sidenav-tooltip" role="tooltip" style="pointer-events: none;"><div class="arrow"></div><div class="tooltip-inner"></div></div>'
+  })
+  // Toggle the side navigation
+  $("#sidenavToggler").click(function(e) {
+    e.preventDefault();
+    $("body").toggleClass("sidenav-toggled");
+    $(".navbar-sidenav .nav-link-collapse").addClass("collapsed");
+    $(".navbar-sidenav .sidenav-second-level, .navbar-sidenav .sidenav-third-level").removeClass("show");
+  });
+  // Force the toggled class to be removed when a collapsible nav link is clicked
+  $(".navbar-sidenav .nav-link-collapse").click(function(e) {
+    e.preventDefault();
+    $("body").removeClass("sidenav-toggled");
+  });
+  // Prevent the content wrapper from scrolling when the fixed side navigation hovered over
+  $('body.fixed-nav .navbar-sidenav, body.fixed-nav .sidenav-toggler, body.fixed-nav .navbar-collapse').on('mousewheel DOMMouseScroll', function(e) {
+    var e0 = e.originalEvent,
+      delta = e0.wheelDelta || -e0.detail;
+    this.scrollTop += (delta < 0 ? 1 : -1) * 30;
+    e.preventDefault();
+  });
+  // Scroll to top button appear
+  $(document).scroll(function() {
+    var scrollDistance = $(this).scrollTop();
+    if (scrollDistance > 100) {
+      $('.scroll-to-top').fadeIn();
+    } else {
+      $('.scroll-to-top').fadeOut();
+    }
+  });
+  // Configure tooltips globally
+  $('[data-toggle="tooltip"]').tooltip()
+  // Smooth scrolling using jQuery easing
+  $(document).on('click', 'a.scroll-to-top', function(event) {
+    var $anchor = $(this);
+    $('html, body').stop().animate({
+      scrollTop: ($($anchor.attr('href')).offset().top)
+    }, 1000, 'easeInOutExpo');
+    event.preventDefault();
+  });
+})(jQuery); // End of use strict

+ 3 - 0
app/static/sass/custom.sass

@@ -0,0 +1,3 @@
+.dataTables_wrapper.container-fluid
+  padding-left: 0
+  padding-right: 0

+ 124 - 0
app/templates/layouts/main.html

@@ -0,0 +1,124 @@
+<!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, shrink-to-fit=no">
+		<meta name="description" content="">
+		<meta name="author" content="">
+		<title>{% block title %}{% endblock %} - PLA33 API</title>
+		{% block head%}{% endblock %}
+		<!-- Bootstrap core CSS-->
+		<link href="{{ url_for('static', filename='vendor/bootstrap/css/bootstrap.min.css') }}" rel="stylesheet">
+		<!-- Custom fonts for this template-->
+		<link href="{{ url_for('static', filename='vendor/font-awesome/css/font-awesome.min.css') }}" rel="stylesheet">
+		<!-- Custom styles for this template-->
+		<link href="{{ url_for('static', filename='css/sb-admin.min.css') }}" rel="stylesheet">
+		<link href="{{ url_for('static', filename='css/custom.min.css') }}" rel="stylesheet">
+	</head>
+
+	<body class="fixed-nav sticky-footer bg-dark {{ 'sidenav-toggled' if not current_user.is_authenticated }}" id="page-top">
+		<!-- Navigation-->
+		<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top" id="mainNav">
+			<a class="navbar-brand" href="/">PLA33 API</a>
+			<button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
+				<span class="navbar-toggler-icon"></span>
+			</button>
+			<div class="collapse navbar-collapse" id="navbarResponsive">
+				<ul class="navbar-nav navbar-sidenav">
+					{%- for item in current_menu.children recursive -%}
+					<li class="nav-item" data-toggle="tooltip" data-placement="right" title="{{ item.text }}">
+						<a class="nav-link" href="{{ item.url}}">
+							<span class="nav-link-text">{{ item.text }}</span>
+						</a>
+					</li>
+					{%- endfor -%}
+				</ul>
+				<ul class="navbar-nav sidenav-toggler">
+					<li class="nav-item">
+						<a class="nav-link text-center" id="sidenavToggler">
+							<i class="fa fa-fw fa-angle-left"></i>
+						</a>
+					</li>
+				</ul>
+				<ul class="navbar-nav ml-auto">
+					{% if current_user.is_authenticated %}
+					<li class="nav-item">
+						<a class="nav-link" href="{{ url_for('user_overview', id=current_user.id) }}">{{ current_user.email }}</a>
+					</li>
+					<li class="nav-item">
+						<a class="nav-link" data-toggle="modal" data-target="#logoutModal"><i class="fa fa-fw fa-sign-out"></i>Logout</a>
+					</li>
+					{% else %}
+					<li class="nav-item">
+						<a class="nav-link" href="{{ url_for('login') }}">Login</a>
+					</li>
+					{% endif %}
+				</ul>
+			</div>
+		</nav>
+		<div class="content-wrapper">
+			<div class="container-fluid">
+				<!-- Breadcrumbs-->
+				<ol class="breadcrumb">
+					{%- for breadcrumb in breadcrumbs -%}
+					<li class="breadcrumb-item">
+						<a href="{{ breadcrumb.url }}">{{ breadcrumb.text }}</a>
+					</li>
+					{%- endfor -%}
+				</ol>
+				<div id="content">
+					{% with messages = get_flashed_messages(with_categories=true) %}
+					{% if messages %}
+					{% for category, message in messages %}
+					<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
+						{{ message }}
+						<button type="button" class="close" data-dismiss="alert" aria-label="Close">
+							<span aria-hidden="true">&times;</span>
+						</button>
+					</div>
+					{% endfor %}
+					{% endif %}
+					{% endwith %}
+					<h1>{{ self.title() }}</h1>
+					{% block content %}{% endblock %}
+				</div>
+			</div>
+			<!-- /.container-fluid-->
+			<!-- /.content-wrapper-->
+			<footer class="sticky-footer">
+				<div class="container">
+					<div class="text-center">
+						<small>Copyright © PLA33 2018</small>
+					</div>
+				</div>
+			</footer>
+			<!-- Scroll to Top Button-->
+			<a class="scroll-to-top rounded" href="#page-top">
+				<i class="fa fa-angle-up"></i>
+			</a>
+			<!-- Logout Modal-->
+			<div class="modal fade" id="logoutModal" tabindex="-1" role="dialog" aria-labelledby="logoutModalLabel" aria-hidden="true">
+				<div class="modal-dialog" role="document">
+					<div class="modal-content">
+						<div class="modal-header">
+							<h5 class="modal-title" id="logoutModalLabel">Ready to Leave?</h5>
+							<button class="close" type="button" data-dismiss="modal" aria-label="Close">
+								<span aria-hidden="true">×</span>
+							</button>
+						</div>
+						<div class="modal-body">Click the "logout" out button to logout</div>
+						<div class="modal-footer">
+							<button class="btn btn-secondary" type="button" data-dismiss="modal">Cancel</button>
+							<a class="btn btn-primary" href="{{ url_for('logout') }}">Logout</a>
+						</div>
+					</div>
+				</div>
+			</div>
+			<script type='text/javascript' src="{{ url_for('static', filename='vendor/jquery/jquery.min.js') }}"></script>
+			<script type='text/javascript' src="{{ url_for('static', filename='vendor/bootstrap/js/bootstrap.bundle.min.js') }}"></script>
+			<script type='text/javascript' src="{{ url_for('static', filename='js/site/sb-admin.min.js') }}"></script>
+			{% block javascript %}{% endblock %}
+		</div>
+	</body>
+</html>

+ 31 - 0
app/templates/site/index.html

@@ -0,0 +1,31 @@
+{% extends "layouts/main.html" %}
+{% block title %}Home{% endblock %}
+{% block head_content %}
+<div class="row">
+	<div class="col-xl-3 col-sm-6 mb-3">
+		<div class="card text-white bg-success o-hidden h-100">
+			<div class="card-body">
+				<div class="card-body-icon">
+					<i class="fa fa-fw fa-user"></i>
+				</div>
+				<div class="mr-5">{{ users }} users found</div>
+			</div>
+			<a class="card-footer text-white clearfix small z-1" href="{{ url_for('user_index') }}">
+				<span class="float-left">View users</span>
+				<span class="float-right">
+					<i class="fa fa-angle-right"></i>
+				</span>
+			</a>
+		</div>
+	</div>
+</div>
+{% endblock %}
+{% block content %}
+<p class="text-justify">
+{% if not current_user.is_authenticated %}
+Create an account to start using the api <a href="{{ url_for('login') }}">here</a>.
+{% else %}
+Welcome!
+{% endif %}
+</p>
+{% endblock %}

+ 82 - 0
app/templates/site/login.html

@@ -0,0 +1,82 @@
+<head>
+	<meta charset="utf-8">
+	<meta http-equiv="X-UA-Compatible" content="IE=edge">
+	<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+	<meta name="description" content="">
+	<meta name="author" content="">
+	<title>SB Admin - Start Bootstrap Template</title>
+	<!-- Bootstrap core CSS-->
+	<link href="{{ url_for('static', filename='vendor/bootstrap/css/bootstrap.css') }}" rel="stylesheet">
+	<!-- Custom fonts for this template-->
+	<link href="{{ url_for('static', filename='vendor/font-awesome/css/font-awesome.min.css') }}" rel="stylesheet">
+	<!-- Custom styles for this template-->
+	<link href="{{ url_for('static', filename='css/sb-admin.css') }}" rel="stylesheet">
+</head>
+<body class="bg-dark">
+	<div class="container">
+		<div class="card mx-auto mt-5">
+			<div class="card-header">
+				<i class="fa fa-user"></i> Login
+			</div>
+			<div class="card-body">
+				{% with messages = get_flashed_messages(with_categories=true) %}
+				{% if messages %}
+				{% for category, message in messages %}
+				<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
+					{{ message }}
+					<button type="button" class="close" data-dismiss="alert" aria-label="Close">
+						<span aria-hidden="true">&times;</span>
+					</button>
+				</div>
+				{% endfor %}
+				{% endif %}
+				{% endwith %}
+				<div class="row">
+					<div class="col-12 col-md-6">
+						<h2>Login</h2>
+						<form method="post">
+							<div class="form-group">
+								<label class="text-normal text-dark">Email</label>
+								<input type="email" class="form-control" name="email" placeholder="Email">
+							</div>
+							<div class="form-group">
+								<label class="text-normal text-dark">Password</label>
+								<input type="password" class="form-control" name="password" placeholder="Password">
+							</div>
+							<div class="form-group pull-right">
+								<button class="btn btn-primary">Login</button>
+							</div>
+						</form>
+					</div>
+					<div class="col-12 col-md-6">
+						<h2>Register</h2>
+						<form action="{{ url_for('register')}}" method="post">
+							<div class="form-group">
+								<label class="text-normal text-dark">Name</label>
+								<input type="text" class="form-control" name="name" placeholder="Name">
+							</div>
+							<div class="form-group">
+								<label class="text-normal text-dark">Email</label>
+								<input type="email" class="form-control" name="email" placeholder="Email">
+							</div>
+							<div class="form-group">
+								<label class="text-normal text-dark">Password</label>
+								<input type="password" class="form-control" name="password" placeholder="Password">
+							</div>
+							<div class="form-group pull-right">
+								<button class="btn btn-primary">Register</button>
+							</div>
+						</form>
+					</div>
+				</div>
+			</div>
+		</div>
+	</div>
+	<!-- Bootstrap core JavaScript-->
+	<script type='text/javascript' src="{{ url_for('static', filename='vendor/jquery/jquery.min.js') }}"></script>
+	<script type='text/javascript' src="{{ url_for('static', filename='vendor/bootstrap/js/bootstrap.bundle.min.js') }}"></script>
+	<script type='text/javascript' src="{{ url_for('static', filename='js/site/sb-admin.min.js') }}"></script>
+	<!-- Core plugin JavaScript-->
+</body>
+
+</html>

+ 35 - 0
app/templates/user/index.html

@@ -0,0 +1,35 @@
+{% extends "layouts/main.html" %}
+{% block title %}Users{% endblock %}
+{% block head %}
+<link href="{{ url_for('static', filename='vendor/datatables/dataTables.bootstrap4.css') }}" rel="stylesheet">
+{% endblock %}
+{% block content %}
+<table class="table table-bordered dataTable" cellspacing="0">
+	<thead>
+		<tr>
+			<th>ID</th>
+			<th>Name</th>
+			<th>Email</th>
+			<th>Keys</th>
+			<th>Registration</th>
+		</tr>
+	</thead>
+	<tbody>
+		{% for user in users %}
+		<tr>
+
+			<td><a href="{{ url_for('user_overview', id=user.id) }}">{{ user.id }}</a></td>
+			<td>{{ user.name }}</td>
+			<td>{{ user.email }}</td>
+			<td>{{ user.key_count }}</td>
+			<td>{{ user.registration_at }}</td>
+		</tr>
+		{% endfor %}
+	</tbody>
+</table>
+{% endblock %}
+{% block javascript %}
+<script type='text/javascript' src="{{ url_for('static', filename='vendor/datatables/jquery.dataTables.js') }}"></script>
+<script type='text/javascript' src="{{ url_for('static', filename='vendor/datatables/dataTables.bootstrap4.js') }}"></script>
+<script type='text/javascript' src="{{ url_for('static', filename='js/site/sb-admin-datatables.min.js') }}"></script>
+{% endblock %}

+ 77 - 0
app/templates/user/overview.html

@@ -0,0 +1,77 @@
+{% extends "layouts/main.html" %}
+{% block title %}{{ user.name }}{% endblock %}
+{% block head %}
+<link href="{{ url_for('static', filename='vendor/datatables/dataTables.bootstrap4.css') }}" rel="stylesheet">
+{% endblock %}
+{% block content %}
+<div class="card mb-3">
+	<div class="card-header">
+		<i class="fa fa-user"></i> Information
+		{% if current_user.id == user.id %}
+		<div class="dropdown pull-right">
+			<button class="btn btn-secondary btn-sm dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Actions</button>
+			<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
+				<a href="{{ url_for('user_generate_key', id=user.id) }}" class="dropdown-item">Genereer key</a>
+			</div>
+		</div>
+		{% endif %}
+	</div>
+	<div class="card-body">
+		<div class="row mt-2">
+			<div class="col-4">Name</div>
+			<div class="col-8">{{ user.name }}</div>
+		</div>
+		<div class="row mt-2">
+			<div class="col-4">Email</div>
+			<div class="col-8">{{ user.email }}</div>
+		</div>
+		<div class="row mt-2">
+			<div class="col-4">Registration</div>
+			<div class="col-8">{{ user.registration_at.strftime('%Y-%m-%d %H:%m') }}</div>
+		</div>
+	</div>
+</div>
+<table class="table table-bordered dataTable" cellspacing="0">
+	<thead>
+		<tr>
+			{% if current_user.id == user.id %}
+			<th></th>
+			{% endif %}
+			<th>ID</th>
+			{% if current_user.id == user.id %}
+			<th>Key</th>
+			{% else %}
+			<th>Active</th>
+			{% endif %}
+			<th>Uses</th>
+			<th>Created</th>
+			<th>Expire</th>
+		</tr>
+	</thead>
+	<tbody>
+		{% for key in user.keys %}
+		<tr>
+			{% if current_user.id == user.id %}
+			<td>
+				<a href="{{ url_for('user_toogle_key', user_id=user.id, key_id=key.id) }}" class="btn btn-{{ 'secondary' if key.active else 'primary' }} btn-sm">{{ "Deactivate" if key.active else "Activate" }}</a>
+			</td>
+			{% endif %}
+			<td>{{ key.id}}</td>
+			{% if current_user.id == user.id %}
+			<td>{{ key.key }}</td>
+			{% else %}
+			<td>{{ key.active }}</td>
+			{% endif %}
+			<td>{{ key.uses }}</td>
+			<td>{{ key.created_at.strftime('%Y-%m-%d %H:%m') }}</td>
+			<td>{{ key.expire_at }}</td>
+		</tr>
+		{% endfor %}
+	</tbody>
+</table>
+{% endblock %}
+{% block javascript %}
+<script type='text/javascript' src="{{ url_for('static', filename='vendor/datatables/jquery.dataTables.js') }}"></script>
+<script type='text/javascript' src="{{ url_for('static', filename='vendor/datatables/dataTables.bootstrap4.js') }}"></script>
+<script type='text/javascript' src="{{ url_for('static', filename='js/site/sb-admin-datatables.min.js') }}"></script>
+{% endblock %}

+ 17 - 0
test.py

@@ -0,0 +1,17 @@
+
+"""
+Test application for api request. Change authorization if necessary
+"""
+
+import requests
+
+URL = 'http://localhost:5000/'
+HEADERS = {
+    'Authorization': 'cGwJNNbIhrKiMKjA4DuVhzL2YH8DxPDV37N5SAb6UbhYtFhYzgwhtmVS4iVloyEu'
+}
+
+DATA = {
+    'test': 'true',
+}
+
+print(requests.get(URL, headers=HEADERS, data=DATA).text)