14 |
35 | {% endblock %}
36 |
--------------------------------------------------------------------------------
/app/static/coreui/scss/mixins/_borders.scss:
--------------------------------------------------------------------------------
1 | @mixin borders($borders) {
2 | @each $border in $borders {
3 | $direction: nth($border, 1);
4 | @if $direction == "all" {
5 | $size: map-get(map-get($borders, $direction), size);
6 | $style: map-get(map-get($borders, $direction), style);
7 | $color: map-get(map-get($borders, $direction), color);
8 | border: $size $style $color;
9 | } @else if $direction == "top" {
10 | $size: map-get(map-get($borders, $direction), size);
11 | $style: map-get(map-get($borders, $direction), style);
12 | $color: map-get(map-get($borders, $direction), color);
13 | border-top: $size $style $color;
14 | } @else if $direction == "right" {
15 | $size: map-get(map-get($borders, $direction), size);
16 | $style: map-get(map-get($borders, $direction), style);
17 | $color: map-get(map-get($borders, $direction), color);
18 | border-right: $size $style $color;
19 | } @else if $direction == "bottom" {
20 | $size: map-get(map-get($borders, $direction), size);
21 | $style: map-get(map-get($borders, $direction), style);
22 | $color: map-get(map-get($borders, $direction), color);
23 | border-bottom: $size $style $color;
24 | } @else if $direction == "left" {
25 | $size: map-get(map-get($borders, $direction), size);
26 | $style: map-get(map-get($borders, $direction), style);
27 | $color: map-get(map-get($borders, $direction), color);
28 | border-left: $size $style $color;
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/static/coreui/js/src/index.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery'
2 | import AjaxLoad from './ajax-load'
3 | import AsideMenu from './aside-menu'
4 | import Sidebar from './sidebar'
5 |
6 | /**
7 | * --------------------------------------------------------------------------
8 | * CoreUI (v2.0.0-beta.2): index.js
9 | * Licensed under MIT (https://coreui.io/license)
10 | * --------------------------------------------------------------------------
11 | */
12 |
13 | (($) => {
14 | if (typeof $ === 'undefined') {
15 | throw new TypeError('CoreUI\'s JavaScript requires jQuery. jQuery must be included before CoreUI\'s JavaScript.')
16 | }
17 |
18 | const version = $.fn.jquery.split(' ')[0].split('.')
19 | const minMajor = 1
20 | const ltMajor = 2
21 | const minMinor = 9
22 | const minPatch = 1
23 | const maxMajor = 4
24 |
25 | if (version[0] < ltMajor && version[1] < minMinor || version[0] === minMajor && version[1] === minMinor && version[2] < minPatch || version[0] >= maxMajor) {
26 | throw new Error('CoreUI\'s JavaScript requires at least jQuery v1.9.1 but less than v4.0.0')
27 | }
28 | })($)
29 |
30 | export {
31 | AjaxLoad,
32 | AsideMenu,
33 | Sidebar
34 | }
35 |
36 | // Global functions
37 | import GetStyle from './utilities/get-style'
38 | window.GetStyle = GetStyle
39 |
40 | import HexToRgb from './utilities/hex-to-rgb'
41 | window.HexToRgb = HexToRgb
42 |
43 | import HexToRgba from './utilities/hex-to-rgba'
44 | window.HexToRgba = HexToRgba
45 |
46 | import RgbToHex from './utilities/rgb-to-hex'
47 | window.RgbToHex = RgbToHex
48 |
--------------------------------------------------------------------------------
/app/static/coreui/scss/_dropdown.scss:
--------------------------------------------------------------------------------
1 | // Links, buttons, and more within the dropdown menu
2 | .dropdown-item {
3 | position: relative;
4 | padding: 10px 20px;
5 | border-bottom: 1px solid $dropdown-border-color;
6 |
7 | &:last-child {
8 | border-bottom: 0;
9 | }
10 |
11 | i {
12 | display: inline-block;
13 | width: 20px;
14 | margin-right: 10px;
15 | margin-left: -10px;
16 | color: $dropdown-border-color;
17 | text-align: center;
18 | }
19 |
20 | .badge {
21 | position: absolute;
22 | right: 10px;
23 | margin-top: 2px;
24 | }
25 | }
26 |
27 | // Dropdown section headers
28 | .dropdown-header {
29 | padding: 8px 20px;
30 | background: $dropdown-divider-bg;
31 | border-bottom: 1px solid $dropdown-border-color;
32 |
33 | .btn {
34 | margin-top: -7px;
35 | color: $dropdown-header-color;
36 |
37 | &:hover {
38 | color: $body-color;
39 | }
40 |
41 | &.pull-right {
42 | margin-right: -20px;
43 | }
44 | }
45 | }
46 |
47 | .dropdown-menu-lg {
48 | width: 250px;
49 | }
50 | .app-header {
51 | .navbar-nav {
52 | .dropdown-menu {
53 | position: absolute;
54 | }
55 | // Menu positioning
56 | //
57 | // Add extra class to `.dropdown-menu` to flip the alignment of the dropdown
58 | // menu with the parent.
59 | .dropdown-menu-right {
60 | right: 0;
61 | left: auto; // Reset the default from `.dropdown-menu`
62 | }
63 |
64 | .dropdown-menu-left {
65 | right: auto;
66 | left: 0;
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/app/templates/flask_user/forgot_password.html:
--------------------------------------------------------------------------------
1 | {% extends "common/page_base.html" %}
2 |
3 | {% block content %}
4 |
5 |
38 | {% endblock %}
39 |
--------------------------------------------------------------------------------
/app/views/apis.py:
--------------------------------------------------------------------------------
1 | # Copyright 2018 Twin Tech Labs. All rights reserved
2 |
3 | from flask import Blueprint, redirect
4 | from flask import request, url_for, jsonify, current_app
5 |
6 | from app import db
7 | from app.models import user_models
8 | from app.utils.api import roles_accepted_api
9 | from app.extensions.ldap import authenticate
10 |
11 | import uuid
12 |
13 | # When using a Flask app factory we must use a blueprint to avoid needing 'app' for '@app.route'
14 | api_blueprint = Blueprint('api', __name__, template_folder='templates')
15 |
16 | @api_blueprint.route('/api/credentials', methods=['POST'])
17 | def api_create_credentials():
18 | username = request.form['username']
19 | password = request.form['password']
20 | label = request.form.get('label', None)
21 | user = user_models.User.query.filter(user_models.User.email == username).first()
22 | if not user:
23 | user = user_models.User.query.filter(user_models.User.username == username).first()
24 | if not user:
25 | abort(400)
26 |
27 | if current_app.config.get('USER_LDAP', False):
28 | if not authenticate(username, password):
29 | abort(401)
30 | else:
31 | if not current_app.user_manager.verify_password(password, user.password):
32 | abort(401)
33 |
34 | id = uuid.uuid4().hex[0:12]
35 | key = uuid.uuid4().hex
36 | hash = current_app.user_manager.hash_password(key)
37 | new_key = user_models.ApiKey(id=id, hash=hash, user_id=user.id, label=label)
38 | db.session.add(new_key)
39 | db.session.commit()
40 |
41 | return jsonify({'id': id,'key': key})
42 |
--------------------------------------------------------------------------------
/app/utils/api.py:
--------------------------------------------------------------------------------
1 | from app.models import user_models as users
2 | from functools import wraps
3 | from flask import request, abort, current_app
4 |
5 |
6 | def is_authorized_api_user(roles=False):
7 | """Verify API Token and its owners permission to use it"""
8 | if 'API_ID' not in request.headers:
9 | return False
10 | if 'API_KEY' not in request.headers:
11 | return False
12 | api_key = users.ApiKey.query.filter(users.ApiKey.id==request.headers['API_ID']).first()
13 | if not api_key:
14 | return False
15 | if not current_app.user_manager.verify_password(request.headers['API_KEY'], api_key.hash):
16 | return False
17 | if not roles:
18 | return True
19 | if api_key.user.has_role('admin'):
20 | return True
21 | for role in roles:
22 | if api_key.user.has_role(role):
23 | return True
24 | return False
25 |
26 |
27 | def roles_accepted_api(*role_names):
28 | def wrapper(view_function):
29 | @wraps(view_function)
30 | def decorated_view_function(*args, **kwargs):
31 | if not is_authorized_api_user(role_names):
32 | return abort(403)
33 | return view_function(*args, **kwargs)
34 | return decorated_view_function
35 | return wrapper
36 |
37 |
38 | def api_credentials_required():
39 | def wrapper(view_function):
40 | @wraps(view_function)
41 | def decorated_view_function(*args, **kwargs):
42 | if not is_authorized_api_user():
43 | return abort(403)
44 | return view_function(*args, **kwargs)
45 | return decorated_view_function
46 | return wrapper
47 |
--------------------------------------------------------------------------------
/app/static/coreui/scss/_aside.scss:
--------------------------------------------------------------------------------
1 | .aside-menu {
2 | z-index: $zindex-sticky - 1;
3 | width: $aside-menu-width;
4 | color: $aside-menu-color;
5 | background: $aside-menu-bg;
6 | @include borders($aside-menu-borders);
7 |
8 | .nav-tabs {
9 | border-color: $border-color;
10 | .nav-link {
11 | padding: $aside-menu-nav-padding-y $aside-menu-nav-padding-x;
12 | color: $body-color;
13 | border-top: 0;
14 | @include border-radius(0);
15 | &.active {
16 | color: theme-color("primary");
17 | border-right-color: $border-color;
18 | border-left-color: $border-color;
19 | }
20 | }
21 | .nav-item:first-child {
22 | .nav-link {
23 | border-left: 0;
24 | }
25 | }
26 | }
27 |
28 | .tab-content {
29 | position: relative;
30 | overflow-x: hidden;
31 | overflow-y: auto;
32 | border: 0;
33 | border-top: 1px solid $border-color;
34 | -ms-overflow-style: -ms-autohiding-scrollbar;
35 |
36 | &::-webkit-scrollbar {
37 | width: 10px;
38 | margin-left: -10px;
39 | appearance: none;
40 | }
41 |
42 | // &::-webkit-scrollbar-button { }
43 |
44 | &::-webkit-scrollbar-track {
45 | background-color: lighten($aside-menu-bg, 5%);
46 | border-right: 1px solid darken($aside-menu-bg, 5%);
47 | border-left: 1px solid darken($aside-menu-bg, 5%);
48 | }
49 |
50 | // &::-webkit-scrollbar-track-piece { }
51 |
52 | &::-webkit-scrollbar-thumb {
53 | height: 50px;
54 | background-color: darken($aside-menu-bg, 10%);
55 | background-clip: content-box;
56 | border-color: transparent;
57 | border-style: solid;
58 | border-width: 1px 2px;
59 | }
60 |
61 | .tab-pane {
62 | padding: 0;
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/docker/worker/start_worker.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | wait_for_service() {
4 | HOSTNAME=$(urlparser hostname $1)
5 | PORT=$(urlparser port $1 || echo $2)
6 | until nc -w 5 -z $HOSTNAME $PORT
7 | do
8 | echo "Waiting for service to come online at $HOSTNAME $PORT"
9 | sleep 5
10 | done
11 | }
12 |
13 | # Get service hostnames from the secrets manager.
14 | if [[ -n $AWS_SECRETS_MANAGER_CONFIG ]]; then
15 | if [ "$SQLALCHEMY_DATABASE_URI" == "" ]; then
16 | SQLALCHEMY_DATABASE_URI=$(secretcli get $AWS_SECRETS_MANAGER_CONFIG SQLALCHEMY_DATABASE_URI)
17 | fi
18 | if [ "$CELERY_BROKER" == "" ]; then
19 | CELERY_BROKER=$(secretcli get $AWS_SECRETS_MANAGER_CONFIG CELERY_BROKER)
20 | fi
21 | fi
22 |
23 | # Wait for configured services to become available.
24 | if [[ -n "$CELERY_BROKER" ]]; then
25 | wait_for_service $CELERY_BROKER 5672
26 | fi
27 | if [[ -n "$SQLALCHEMY_DATABASE_URI" ]]; then
28 | wait_for_service $SQLALCHEMY_DATABASE_URI 5432
29 | fi
30 |
31 |
32 | # Add default celery concurrency options.
33 | if [ "$CELERY_MAX_CONCURRENCY" == "" ]; then
34 | CELERY_MAX_CONCURRENCY='10'
35 | fi
36 | if [ "$CELERY_MIN_CONCURRENCY" == "" ]; then
37 | CELERY_MIN_CONCURRENCY='2'
38 | fi
39 |
40 |
41 | if [ "$DISABLE_BEAT" = "true" ]
42 | then
43 | echo 'Launching celery worker without beat'
44 | echo "celery worker -A app.worker:celery --loglevel=info --autoscale=${CELERY_MAX_CONCURRENCY},${CELERY_MIN_CONCURRENCY}"
45 | celery worker -A app.worker:celery --loglevel=info --autoscale=${CELERY_MAX_CONCURRENCY},${CELERY_MIN_CONCURRENCY}
46 | else
47 | echo 'Launching celery worker with beat enabled'
48 | rm -f ~/celerybeat-schedule
49 | echo "celery worker -A app.worker:celery --loglevel=info --autoscale=${CELERY_MAX_CONCURRENCY},${CELERY_MIN_CONCURRENCY}" -B -s ~/celerybeat-schedule
50 | celery worker -A app.worker:celery --loglevel=info --autoscale=${CELERY_MAX_CONCURRENCY},${CELERY_MIN_CONCURRENCY} -B -s ~/celerybeat-schedule
51 | fi
52 |
--------------------------------------------------------------------------------
/app/templates/pages/admin/users.html:
--------------------------------------------------------------------------------
1 | {% extends "common/dark_base.html" %} {# common/page_base.html extends layout.html #}
2 | {% block breadcrumb %}
3 |
4 | Manage Users
5 |
6 | {% endblock %}
7 | {% block content %}
8 |
9 |
13 |
14 |
15 |
16 |
17 | | Email |
18 | Username |
19 | Name |
20 | Role |
21 | Confirmed |
22 | {% if not config.get('USER_LDAP', False) %}
23 | Actions |
24 | {% endif %}
25 |
26 |
27 |
28 | {% for user in users %}
29 |
30 | | {{user.email}} |
31 | {{user.username}} |
32 | {{user.name()}} |
33 | {% for role in user.roles %}{{ role.name }}{{ ", " if not loop.last }}{% endfor %} |
34 | {{user.email_confirmed_at}} |
35 | {% if not config.get('USER_LDAP', False) %}
36 |
37 |
40 |
43 | |
44 | {% endif %}
45 |
46 | {% endfor %}
47 |
48 |
49 |
50 |
52 |
53 | {% endblock %}
54 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Standard Python .gitignore
2 | # --------------------------
3 |
4 | # Byte-compiled / optimized / DLL files
5 | __pycache__/
6 | *.py[cod]
7 | *$py.class
8 |
9 | # C extensions
10 | *.so
11 |
12 | # Distribution / packaging
13 | .Python
14 | build/
15 | develop-eggs/
16 | downloads/
17 | eggs/
18 | .eggs/
19 | lib/
20 | lib64/
21 | parts/
22 | sdist/
23 | var/
24 | wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .coverage
43 | .coverage.*
44 | .cache
45 | nosetests.xml
46 | coverage.xml
47 | *.cover
48 | .hypothesis/
49 |
50 | # Translations
51 | *.mo
52 | *.pot
53 |
54 | # Django stuff:
55 | *.log
56 |
57 | # Flask stuff:
58 | instance/
59 | .webassets-cache
60 |
61 | # Scrapy stuff:
62 | .scrapy
63 |
64 | # Sphinx documentation
65 | docs/_build/
66 |
67 | # PyBuilder
68 | target/
69 |
70 | # Jupyter Notebook
71 | .ipynb_checkpoints
72 |
73 | # pyenv
74 | .python-version
75 |
76 | # celery beat schedule file
77 | celerybeat-schedule
78 |
79 | # SageMath parsed files
80 | *.sage.py
81 |
82 | # Environments
83 | .env
84 | .venv
85 | env/
86 | venv/
87 | ENV/
88 |
89 | # Spyder project settings
90 | .spyderproject
91 | .spyproject
92 |
93 | # Rope project settings
94 | .ropeproject
95 |
96 | # mkdocs documentation
97 | /site
98 |
99 | # mypy
100 | .mypy_cache/
101 |
102 |
103 | # Other standards .gitignore
104 | # --------------------------
105 |
106 | # Mac files
107 | .DS_Store
108 |
109 | # IDEs
110 | .idea/
111 |
112 |
113 | # Application-specific .gitignores
114 | # --------------------------------
115 |
116 | app.sqlite
117 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | # This file contains pytest 'fixtures'.
2 | # If a test functions specifies the name of a fixture function as a parameter,
3 | # the fixture function is called and its result is passed to the test function.
4 | #
5 | # Copyright 2014 SolidBuilds.com. All rights reserved
6 | #
7 | # Authors: Ling Thio
8 |
9 | import pytest
10 | from app import create_app, db as the_db
11 |
12 | # Initialize the Flask-App with test-specific settings
13 | the_app = create_app(dict(
14 | TESTING=True, # Propagate exceptions
15 | LOGIN_DISABLED=False, # Enable @register_required
16 | MAIL_SUPPRESS_SEND=True, # Disable Flask-Mail send
17 | SERVER_NAME='localhost', # Enable url_for() without request context
18 | SQLALCHEMY_DATABASE_URI='sqlite:///:memory:', # In-memory SQLite DB
19 | WTF_CSRF_ENABLED=False, # Disable CSRF form validation
20 | ))
21 |
22 | # Setup an application context (since the tests run outside of the webserver context)
23 | the_app.app_context().push()
24 |
25 | # Create and populate roles and users tables
26 | from app.commands.init_db import init_db
27 | init_db()
28 |
29 |
30 | @pytest.fixture(scope='session')
31 | def app():
32 | """ Makes the 'app' parameter available to test functions. """
33 | return the_app
34 |
35 |
36 | @pytest.fixture(scope='session')
37 | def db():
38 | """ Makes the 'db' parameter available to test functions. """
39 | return the_db
40 |
41 | @pytest.fixture(scope='function')
42 | def session(db, request):
43 | """Creates a new database session for a test."""
44 | connection = db.engine.connect()
45 | transaction = connection.begin()
46 |
47 | options = dict(bind=connection, binds={})
48 | session = db.create_scoped_session(options=options)
49 |
50 | db.session = session
51 |
52 | def teardown():
53 | transaction.rollback()
54 | connection.close()
55 | session.remove()
56 |
57 | request.addfinalizer(teardown)
58 | return session
59 |
60 | @pytest.fixture(scope='session')
61 | def client(app):
62 | return app.test_client()
63 |
64 |
--------------------------------------------------------------------------------
/app/static/coreui/scss/vendors/_perfect-scrollbar.scss:
--------------------------------------------------------------------------------
1 | // stylelint-disable declaration-no-important, property-no-vendor-prefix
2 | .ps {
3 | overflow: hidden !important;
4 | -ms-touch-action: auto;
5 | touch-action: auto;
6 | -ms-overflow-style: none;
7 | overflow-anchor: none;
8 | }
9 |
10 | .ps__rail-x {
11 | position: absolute;
12 | bottom: 0;
13 | display: none;
14 | height: 15px;
15 | opacity: 0;
16 | transition: background-color .2s linear, opacity .2s linear;
17 | }
18 |
19 | .ps__rail-y {
20 | position: absolute;
21 | right: 0;
22 | display: none;
23 | width: 15px;
24 | opacity: 0;
25 | transition: background-color .2s linear, opacity .2s linear;
26 | }
27 |
28 | .ps--active-x > .ps__rail-x,
29 | .ps--active-y > .ps__rail-y {
30 | display: block;
31 | background-color: transparent;
32 | }
33 |
34 | .ps:hover > .ps__rail-x,
35 | .ps:hover > .ps__rail-y,
36 | .ps--focus > .ps__rail-x,
37 | .ps--focus > .ps__rail-y,
38 | .ps--scrolling-x > .ps__rail-x,
39 | .ps--scrolling-y > .ps__rail-y {
40 | opacity: .6;
41 | }
42 |
43 | .ps__rail-x:hover,
44 | .ps__rail-y:hover,
45 | .ps__rail-x:focus,
46 | .ps__rail-y:focus {
47 | background-color: #eee;
48 | opacity: .9;
49 | }
50 |
51 | /*
52 | * Scrollbar thumb styles
53 | */
54 | .ps__thumb-x {
55 | position: absolute;
56 | bottom: 2px;
57 | height: 6px;
58 | background-color: #aaa;
59 | border-radius: 6px;
60 | transition: background-color .2s linear, height .2s ease-in-out;
61 | }
62 |
63 | .ps__thumb-y {
64 | position: absolute;
65 | right: 2px;
66 | width: 6px;
67 | background-color: #aaa;
68 | border-radius: 6px;
69 | transition: background-color .2s linear, width .2s ease-in-out;
70 | }
71 |
72 | .ps__rail-x:hover > .ps__thumb-x,
73 | .ps__rail-x:focus > .ps__thumb-x {
74 | height: 11px;
75 | background-color: #999;
76 | }
77 |
78 | .ps__rail-y:hover > .ps__thumb-y,
79 | .ps__rail-y:focus > .ps__thumb-y {
80 | width: 11px;
81 | background-color: #999;
82 | }
83 |
84 | @supports (-ms-overflow-style: none) {
85 | .ps {
86 | overflow: auto !important;
87 | }
88 | }
89 |
90 | @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
91 | .ps {
92 | overflow: auto !important;
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/app/views/apikeys.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, redirect, render_template, current_app
2 | from flask import request, url_for, flash, send_from_directory, jsonify, render_template_string
3 | from flask_user import current_user, login_required, roles_accepted
4 |
5 | from flask import Flask, session, redirect, url_for, request, render_template, jsonify, abort
6 | from app import db
7 | from app.models import user_models as users
8 | from app.utils import forms
9 |
10 | import time
11 | import uuid
12 |
13 |
14 | # When using a Flask app factory we must use a blueprint to avoid needing 'app' for '@apikeys_blueprint.route'
15 | apikeys_blueprint = Blueprint('apikeys', __name__, template_folder='templates')
16 |
17 |
18 | @apikeys_blueprint.route('/user/apikeys')
19 | @roles_accepted('dev', 'admin')
20 | def apikeys_index():
21 | all_keys = users.ApiKey.query.filter_by(user_id=current_user.id).all()
22 | return render_template("apikeys/list.html", keys=all_keys)
23 |
24 |
25 | @apikeys_blueprint.route('/user/create_apikey', methods=['GET', 'POST'])
26 | @roles_accepted('dev', 'admin')
27 | def apikeys_create():
28 | form = users.ApiKeyForm(request.form)
29 | if request.method == 'POST' and form.validate():
30 | label = request.form.get('label', None)
31 | id = uuid.uuid4().hex[0:12]
32 | key = uuid.uuid4().hex
33 | hash = current_app.user_manager.hash_password(key)
34 | new_key = users.ApiKey(id=id, hash=hash, user_id=current_user.id, label=label)
35 | db.session.add(new_key)
36 | db.session.commit()
37 | return render_template("apikeys/newkey.html", id=id, key=key, label=label)
38 | return render_template("apikeys/create.html", form=form)
39 |
40 |
41 | @apikeys_blueprint.route('/user/apikeys//delete', methods=['GET', 'POST'])
42 | @roles_accepted('dev', 'admin')
43 | def apikeys_delete(key_id):
44 | form = forms.ConfirmationForm(request.form)
45 | if request.method == 'POST':
46 | remove_key = users.ApiKey.query.filter_by(id=key_id, user_id=current_user.id).first()
47 | if remove_key:
48 | db.session.delete(remove_key)
49 | db.session.commit()
50 | return redirect(url_for('apikeys.apikeys_index'))
51 | return render_template("apikeys/delete.html", form=form, key_id=key_id)
52 |
--------------------------------------------------------------------------------
/docker/app/prestart.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # This function waits for a service to start. Default ports can be supplies by the second argument.
4 | wait_for_service() {
5 | HOSTNAME=$(urlparser hostname $1)
6 | PORT=$(urlparser port $1 || echo $2)
7 | until nc -w 5 -z $HOSTNAME $PORT
8 | do
9 | echo "Waiting for service to come online at $HOSTNAME $PORT"
10 | sleep 5
11 | done
12 | }
13 |
14 | echo 'Setting up configuration and waiting for services.'
15 |
16 | # If there are AWS credentials configured make sure they're available to the root user as well.
17 | # This is only needed in testing, as IAM Roles are used for production.
18 | if [ -d /home/apprunner/.aws ]; then
19 | cp -Rf /home/apprunner/.aws /root/
20 | chown -R root:root /root/.aws
21 | fi
22 |
23 | # Get service hostnames from the secrets manager.
24 | if [ ! -z "$AWS_SECRETS_MANAGER_CONFIG" ]; then
25 | if [ -z "$SQLALCHEMY_DATABASE_URI" ]; then
26 | SQLALCHEMY_DATABASE_URI=$(secretcli get $AWS_SECRETS_MANAGER_CONFIG SQLALCHEMY_DATABASE_URI)
27 | fi
28 | if [ -z "$CELERY_BROKER" ]; then
29 | CELERY_BROKER=$(secretcli get $AWS_SECRETS_MANAGER_CONFIG CELERY_BROKER)
30 | fi
31 | fi
32 |
33 |
34 | # Wait for configured services to become available.
35 | if [ ! -z "$CELERY_BROKER" ]; then
36 | wait_for_service $CELERY_BROKER 5672
37 | fi
38 | if [ ! -z "$SQLALCHEMY_DATABASE_URI" ]; then
39 | wait_for_service $SQLALCHEMY_DATABASE_URI 5432
40 | fi
41 |
42 | set -e
43 | echo 'Performing any database migrations.'
44 | python manage.py db upgrade
45 |
46 | if [ ! -f ~/.hasrun ]; then
47 | echo 'Setting up initial roles and users if they do not exist.'
48 | python manage.py add-role admin Admin
49 | python manage.py add-role dev Developer
50 | python manage.py add-role user User
51 |
52 | if [ ! -z "$ADMIN_USERNAME" ] && [ ! -z "$ADMIN_EMAIL" ] && [ ! -z "$ADMIN_PASSWORD" ]; then
53 | python manage.py add-user $ADMIN_USERNAME $ADMIN_EMAIL $ADMIN_PASSWORD admin
54 | fi
55 |
56 | if [ ! -z "$DEV_USERNAME" ] && [ ! -z "$DEV_EMAIL" ] && [ ! -z "$DEV_PASSWORD" ]; then
57 | python manage.py add-user $DEV_USERNAME $DEV_EMAIL $DEV_PASSWORD dev
58 | fi
59 |
60 | if [ ! -z "$USER_USERNAME" ] && [ ! -z "$USER_EMAIL" ] && [ ! -z "$USER_PASSWORD" ]; then
61 | python manage.py add-user $USER_USERNAME $USER_EMAIL $USER_PASSWORD user
62 | fi
63 |
64 | touch ~/.hasrun
65 | fi
66 |
--------------------------------------------------------------------------------
/docker/ldap/bootstrap.ldif:
--------------------------------------------------------------------------------
1 | # LDIF Export for dc=example,dc=org
2 | # Server: ldap (ldap)
3 | # Search Scope: sub
4 | # Search Filter: (objectClass=*)
5 |
6 |
7 | version: 1
8 |
9 |
10 | #
11 | # LDAP Root
12 | #
13 |
14 |
15 | # Entry:
16 | #dn: dc=example,dc=org
17 | #dc: example
18 | #o: Example Inc.
19 | #objectclass: top
20 | #objectclass: dcObject
21 | #objectclass: organization
22 |
23 |
24 |
25 | #
26 | # LDAP Organizational Units - Users and Groups
27 | #
28 |
29 | # Entry:
30 | dn: ou=groups,dc=example,dc=org
31 | objectclass: organizationalUnit
32 | objectclass: top
33 | ou: groups
34 |
35 | # Entry:
36 | dn: ou=users,dc=example,dc=org
37 | objectclass: organizationalUnit
38 | objectclass: top
39 | ou: users
40 |
41 |
42 | #
43 | # LDAP Groups
44 | #
45 |
46 | # Entry:
47 | dn: cn=admin,ou=groups,dc=example,dc=org
48 | cn: admin
49 | gidnumber: 500
50 | memberuid: admin
51 | objectclass: posixGroup
52 | objectclass: top
53 |
54 | # Entry:
55 | dn: cn=user,ou=groups,dc=example,dc=org
56 | cn: user
57 | gidnumber: 501
58 | memberuid: user
59 | objectclass: posixGroup
60 | objectclass: top
61 |
62 | # Entry:
63 | dn: cn=dev,ou=groups,dc=example,dc=org
64 | cn: dev
65 | gidnumber: 502
66 | memberuid: user
67 | objectclass: posixGroup
68 | objectclass: top
69 |
70 |
71 | #
72 | # LDAP Users
73 | #
74 |
75 |
76 | # Entry:
77 | dn: cn=admin,ou=users,dc=example,dc=org
78 | cn: admin
79 | gidnumber: 500
80 | homedirectory: /home/users/admin
81 | mail: admin@example.org
82 | objectclass: inetOrgPerson
83 | objectclass: posixAccount
84 | objectclass: top
85 | sn: admin
86 | uid: admin
87 | uidnumber: 1000
88 | userpassword: {MD5}KsnLfcArPACD63CJjlSbYw==
89 |
90 | # Entry:
91 | dn: cn=user,ou=users,dc=example,dc=org
92 | cn: user
93 | gidnumber: 501
94 | homedirectory: /home/users/user
95 | mail: user@example.org
96 | objectclass: inetOrgPerson
97 | objectclass: posixAccount
98 | objectclass: top
99 | sn: user
100 | uid: user
101 | uidnumber: 1001
102 | userpassword: {MD5}KsnLfcArPACD63CJjlSbYw==
103 |
104 | # Entry:
105 | dn: cn=dev,ou=users,dc=example,dc=org
106 | cn: dev
107 | gidnumber: 502
108 | homedirectory: /home/users/dev
109 | mail: dev@example.org
110 | objectclass: inetOrgPerson
111 | objectclass: posixAccount
112 | objectclass: top
113 | sn: dev
114 | uid: dev
115 | uidnumber: 1002
116 | userpassword: {MD5}KsnLfcArPACD63CJjlSbYw==
117 |
--------------------------------------------------------------------------------
/app/static/coreui/scss/_charts.scss:
--------------------------------------------------------------------------------
1 | .chart-wrapper canvas {
2 | width: 100%;
3 | }
4 | // stylelint-disable selector-no-qualifying-type, selector-type-no-unknown
5 | base-chart.chart {
6 | display: block;
7 | }
8 |
9 | canvas {
10 | user-select: none;
11 | }
12 |
13 | .chartjs-tooltip {
14 | position: absolute;
15 | z-index: $zindex-sticky + 1;
16 | display: flex;
17 | flex-direction: column;
18 | padding: ($spacer * .25) ($spacer * .5);
19 | color: #fff;
20 | pointer-events: none;
21 | background: rgba(0, 0, 0, .7);
22 | opacity: 0;
23 | transition: all $layout-transition-speed ease;
24 | transform: translate(-50%, 0);
25 | @include border-radius($border-radius);
26 |
27 | .tooltip-header {
28 | margin-bottom: ($spacer * .5);
29 | }
30 |
31 | .tooltip-header-item {
32 | font-size: $font-size-sm;
33 | font-weight: $font-weight-bold;
34 | }
35 |
36 | // .tooltip-body {}
37 | .tooltip-body-item {
38 | display: flex;
39 | align-items: center;
40 | font-size: $font-size-sm;
41 | white-space: nowrap;
42 | }
43 |
44 | .tooltip-body-item-value {
45 | padding-left: $spacer;
46 | margin-left: auto;
47 | font-weight: $font-weight-bold;
48 | }
49 | }
50 |
51 | .chartjs-tooltip-key {
52 | display: inline-block;
53 | width: $font-size-base;
54 | height: $font-size-base;
55 | margin-right: $font-size-base;
56 | }
57 | // .chartjs-tooltip {
58 | // position: absolute;
59 | // z-index: $zindex-sticky + 1;
60 | // display: flex;
61 | // flex-direction: column;
62 | // padding: ($spacer * .25) ($spacer * .5);
63 | // color: $text-muted;
64 | // pointer-events: none;
65 | // background: #fff;
66 | // border: 1px solid $border-color;
67 | // opacity: 0;
68 | // transition: all .1s ease;
69 | // transform: translate(-50%, 0);
70 | // @include border-radius($border-radius);
71 | //
72 | // .tooltip-header {
73 | // margin-bottom: ($spacer * .5);
74 | // }
75 | //
76 | // .tooltip-header-item {
77 | // font-size: $font-size-sm;
78 | // font-weight: $font-weight-bold;
79 | // }
80 | //
81 | // // .tooltip-body {}
82 | // .tooltip-body-item {
83 | // display: flex;
84 | // align-items: center;
85 | // font-size: $font-size-sm;
86 | // white-space: nowrap;
87 | // }
88 | // }
89 | //
90 | // .chartjs-tooltip-key {
91 | // display: inline-block;
92 | // width: $font-size-base;
93 | // height: $font-size-base;
94 | // margin-right: $font-size-base;
95 | // }
96 |
--------------------------------------------------------------------------------
/app/static/coreui/scss/_card.scss:
--------------------------------------------------------------------------------
1 | .card {
2 | margin-bottom: ($spacer * 1.5);
3 |
4 | // Cards with color accent
5 | @each $color, $value in $theme-colors {
6 | &.bg-#{$color} {
7 | border-color: darken($value, 12.5%);
8 | .card-header {
9 | background-color: darken($value, 3%);
10 | border-color: darken($value, 12.5%);
11 | }
12 | }
13 | }
14 |
15 | &.drag,
16 | .drag {
17 | cursor: move;
18 | }
19 | }
20 |
21 | .card-placeholder {
22 | background: rgba(0, 0, 0, .025);
23 | border: 1px dashed $gray-300;
24 | }
25 |
26 | .card-header {
27 | > i {
28 | margin-right: $spacer / 2;
29 | }
30 |
31 | .nav-tabs {
32 | margin-top: -$card-spacer-y;
33 | margin-bottom: -$card-spacer-y;
34 | border-bottom: 0;
35 |
36 | .nav-item {
37 | border-top: 0;
38 | }
39 |
40 | .nav-link {
41 | padding: $card-spacer-y ($card-spacer-x / 2);
42 | color: $text-muted;
43 | border-top: 0;
44 |
45 | &.active {
46 | color: $body-color;
47 | background: #fff;
48 | }
49 | }
50 | }
51 | }
52 |
53 | .card-header-icon-bg {
54 | display: inline-block;
55 | width: ($card-spacer-y * 2) + ($font-size-base * $line-height-base);
56 | padding: $card-spacer-y 0;
57 | margin: (- $card-spacer-y) $card-spacer-x (- $card-spacer-y) (- $card-spacer-x);
58 | line-height: inherit;
59 | color: $card-icon-color;
60 | text-align: center;
61 | background: $card-icon-bg;
62 | border-right: $card-border-width solid $card-border-color;
63 | }
64 |
65 | .card-header-actions {
66 | display: inline-block;
67 | float: right;
68 | margin-right: - ($spacer / 4);
69 | }
70 |
71 | .card-header-action {
72 | padding: 0 ($spacer / 4);
73 | color: $gray-600;
74 |
75 | &:hover {
76 | color: $body-color;
77 | text-decoration: none;
78 | }
79 | }
80 |
81 |
82 | // Cards with color accent
83 | @each $color, $value in $theme-colors {
84 | .card-accent-#{$color} {
85 | @include card-accent-variant($value);
86 | }
87 | }
88 |
89 | .card-full {
90 | margin-top: - $spacer;
91 | margin-right: - $grid-gutter-width / 2;
92 | margin-left: - $grid-gutter-width / 2;
93 | border: 0;
94 | border-bottom: $card-border-width solid $border-color;
95 | }
96 |
97 | @include media-breakpoint-up(sm) {
98 | .card-columns {
99 |
100 | &.cols-2 {
101 | column-count: 2;
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/app/settings.py:
--------------------------------------------------------------------------------
1 | # Settings common to all environments (development|staging|production)
2 | # Place environment specific settings in env_settings.py
3 | # An example file (env_settings_example.py) can be used as a starting point
4 |
5 | import os
6 |
7 | # Application settings
8 | APP_NAME = "Flask Starter"
9 | APP_SYSTEM_ERROR_SUBJECT_LINE = APP_NAME + " system error"
10 | APP_OWNER_NAME = "Change this in settings."
11 |
12 | # Flask settings
13 | CSRF_ENABLED = True
14 | SECRET_KEY = None
15 |
16 | # Flask-SQLAlchemy settings
17 | SQLALCHEMY_TRACK_MODIFICATIONS = False
18 | SQLALCHEMY_DATABASE_URI = 'sqlite:///app.sqlite'
19 |
20 | # Celery Configuration
21 | CELERY_BROKER = False
22 | CELERY_RESULTS = False
23 |
24 | # Cache
25 | CACHE_TYPE = False
26 | CACHE_ROOT = False
27 | CACHE_URL = False
28 |
29 |
30 |
31 | # Flask-User settings
32 | USER_APP_NAME = APP_NAME
33 | USER_ENABLE_CHANGE_PASSWORD = True # Allow users to change their password
34 | USER_ENABLE_CHANGE_USERNAME = False # Allow users to change their username
35 | USER_ENABLE_CONFIRM_EMAIL = True # Force users to confirm their email
36 | USER_ENABLE_FORGOT_PASSWORD = True # Allow users to reset their passwords
37 | USER_ENABLE_EMAIL = True # Register with Email
38 | USER_ENABLE_REGISTRATION = True # Allow new users to register
39 | USER_REQUIRE_RETYPE_PASSWORD = True # Prompt for `retype password` in:
40 | USER_ENABLE_USERNAME = False # Register and Login with username
41 | USER_AFTER_LOGIN_ENDPOINT = 'main.member_page'
42 | USER_AFTER_LOGOUT_ENDPOINT = 'main.member_page'
43 | USER_ALLOW_LOGIN_WITHOUT_CONFIRMED_EMAIL = False
44 |
45 |
46 | USER_LDAP = False
47 | LDAP_HOST=False
48 | LDAP_BIND_DN=False
49 | LDAP_BIND_PASSWORD=False
50 | LDAP_USERNAME_ATTRIBUTE=False
51 | LDAP_USER_BASE=False
52 | LDAP_USER_OBJECT_CLASS = False
53 | LDAP_GROUP_OBJECT_CLASS=False
54 | LDAP_GROUP_ATTRIBUTE=False
55 | LDAP_GROUP_BASE=False
56 | LDAP_GROUP_TO_ROLE_ADMIN=False
57 | LDAP_GROUP_TO_ROLE_DEV=False
58 | LDAP_GROUP_TO_ROLE_USER=False
59 | LDAP_EMAIL_ATTRIBUTE=False
60 |
61 |
62 | # Flask-Mail settings
63 | # For smtp.gmail.com to work, you MUST set "Allow less secure apps" to ON in Google Accounts.
64 | # Change it in https://myaccount.google.com/security#connectedapps (near the bottom).
65 | MAIL_SERVER = 'smtp.gmail.com'
66 | MAIL_PORT = 587
67 | MAIL_USE_SSL = False
68 | MAIL_USE_TLS = True
69 | MAIL_USERNAME = 'you@gmail.com'
70 | MAIL_PASSWORD = 'yourpassword'
71 | MAIL_DEFAULT_SENDER = '"You" '
72 | ADMINS = [
73 | '"Admin One" ',
74 | ]
75 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | """This file sets up a command line manager.
2 |
3 | Use "python manage.py" for a list of available commands.
4 | Use "python manage.py runserver" to start the development web server on localhost:5000.
5 | Use "python manage.py runserver --help" for additional runserver options.
6 | """
7 |
8 | from flask import Flask
9 | #from flask_migrate import MigrateCommand
10 | from flask.cli import FlaskGroup
11 |
12 | import click
13 |
14 | from app import create_app
15 |
16 | from app.commands import user
17 |
18 |
19 | @click.group(cls=FlaskGroup, create_app=create_app)
20 | @click.pass_context
21 | def cli(ctx):
22 | """Management script for the Wiki application."""
23 | if ctx.parent:
24 | click.echo(ctx.parent.get_help())
25 |
26 | @cli.command(help='Add a User')
27 | @click.argument('username')
28 | @click.argument('email')
29 | @click.argument('password', required=False)
30 | @click.argument('role', required=False, default=None)
31 | @click.option('-f', '--firstname', default='')
32 | @click.option('-l', '--lastname', default='')
33 | @click.option('-s', '--secure', is_flag=True, default=False, help='Set password with prompt without it appearing on screen')
34 | def add_user(username, email, password, role, firstname, lastname, secure):
35 | if not password and secure:
36 | password = click.prompt('Password', hide_input=True, confirmation_prompt=True)
37 | if not password:
38 | raise click.UsageError("Password must be provided for the user")
39 | user_role = None
40 | if role:
41 | user_role = user.find_or_create_role(role, role)
42 | user.find_or_create_user(firstname, lastname, username, email, password, user_role)
43 |
44 |
45 | @cli.command(help='Add a Role')
46 | @click.argument('name')
47 | @click.argument('label', required=False)
48 | def add_role(name, label):
49 | if not label:
50 | label = name
51 | user.find_or_create_role(name, label)
52 |
53 |
54 |
55 | @cli.command(help='Change the password of a user')
56 | @click.argument('email')
57 | @click.argument('password', required=False)
58 | @click.option('-s', '--secure', is_flag=True, default=False, help='Set password with prompt without it appearing on screen')
59 | def reset_password(email, password, secure):
60 | if not password and secure:
61 | password = click.prompt('Password', hide_input=True, confirmation_prompt=True)
62 | if not password:
63 | raise click.UsageError("Password must be provided for the user")
64 |
65 | user = User.query.filter(User.email == email).first()
66 | if not user:
67 | raise click.UsageError("User does not exist")
68 |
69 | user.password = current_app.user_manager.hash_password(password)
70 | db.session.commit()
71 |
72 |
73 |
74 |
75 | if __name__ == "__main__":
76 | cli()
77 |
--------------------------------------------------------------------------------
/app/static/coreui/scss/_navbar.scss:
--------------------------------------------------------------------------------
1 | .app-header {
2 | position: relative;
3 | flex-direction: row;
4 | height: $navbar-height;
5 | padding: 0;
6 | margin: 0;
7 | background-color: $navbar-bg;
8 | @include borders($navbar-border);
9 |
10 | .navbar-brand {
11 | display: inline-flex;
12 | align-items: center;
13 | justify-content: center;
14 | width: $navbar-brand-width;
15 | height: $navbar-height;
16 | padding: 0;
17 | margin-right: 0;
18 | background-color: $navbar-brand-bg;
19 | @include borders($navbar-brand-border);
20 |
21 | .navbar-brand-minimized {
22 | display: none;
23 | }
24 | }
25 |
26 | .navbar-toggler {
27 | min-width: 50px;
28 | padding: $navbar-toggler-padding-y 0;
29 |
30 | &:hover .navbar-toggler-icon {
31 | background-image: $navbar-toggler-icon-hover;
32 | }
33 | }
34 |
35 | .navbar-toggler-icon {
36 | height: 23px;
37 | background-image: $navbar-toggler-icon;
38 | }
39 |
40 | .navbar-nav {
41 | flex-direction: row;
42 | align-items: center;
43 | }
44 |
45 | .nav-item {
46 | position: relative;
47 | min-width: 50px;
48 | margin: 0;
49 | text-align: center;
50 |
51 | button {
52 | margin: 0 auto;
53 | }
54 |
55 | .nav-link {
56 | padding-top: 0;
57 | padding-bottom: 0;
58 | background: 0;
59 | border: 0;
60 |
61 | .badge {
62 | position: absolute;
63 | top: 50%;
64 | left: 50%;
65 | margin-top: -16px;
66 | margin-left: 0;
67 | }
68 |
69 | > .img-avatar {
70 | height: $navbar-height - 20px;
71 | margin: 0 10px;
72 | }
73 | }
74 | }
75 |
76 | .dropdown-menu {
77 | padding-bottom: 0;
78 | line-height: $line-height-base;
79 | }
80 |
81 | .dropdown-item {
82 | min-width: 180px;
83 | }
84 | }
85 |
86 | // .navbar-brand {
87 | // color: $navbar-active-color;
88 | //
89 | // @include hover-focus {
90 | // color: $navbar-active-color;
91 | // }
92 | // }
93 |
94 | .navbar-nav {
95 | .nav-link {
96 | color: $navbar-color;
97 |
98 | @include hover-focus {
99 | color: $navbar-hover-color;
100 | }
101 | }
102 |
103 | .open > .nav-link,
104 | .active > .nav-link,
105 | .nav-link.open,
106 | .nav-link.active {
107 | @include plain-hover-focus {
108 | color: $navbar-active-color;
109 | }
110 | }
111 | }
112 |
113 | .navbar-divider {
114 | background-color: rgba(0, 0, 0, .075);
115 | }
116 |
117 | @include media-breakpoint-up(lg) {
118 | .brand-minimized {
119 | .app-header {
120 | .navbar-brand {
121 | width: $navbar-brand-minimized-width;
122 | background-color: $navbar-brand-minimized-bg;
123 | @include borders($navbar-brand-minimized-border);
124 |
125 | .navbar-brand-full {
126 | display: none;
127 | }
128 |
129 | .navbar-brand-minimized {
130 | display: block;
131 | }
132 | }
133 | }
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/app/static/coreui/scss/_bootstrap-variables.scss:
--------------------------------------------------------------------------------
1 | // stylelint-disable
2 | // Bootstrap overrides
3 |
4 | //
5 | // Color system
6 | //
7 |
8 | $gray-base: #181b1e !default;
9 | $gray-100: #f0f3f5 !default; // lighten($gray-base, 85%);
10 | $gray-200: #e4e7ea !default; // lighten($gray-base, 80%);
11 | $gray-300: #c8ced3 !default; // lighten($gray-base, 70%);
12 | $gray-400: #acb4bc !default; // lighten($gray-base, 60%);
13 | $gray-500: #8f9ba6 !default; // lighten($gray-base, 50%);
14 | $gray-600: #73818f !default; // lighten($gray-base, 40%);
15 | $gray-700: #5c6873 !default; // lighten($gray-base, 30%);
16 | $gray-800: #2f353a !default; // lighten($gray-base, 10%);
17 | $gray-900: #23282c !default; // lighten($gray-base, 5%);
18 |
19 | $blue: #20a8d8 !default;
20 | $red: #f86c6b !default;
21 | $orange: #f8cb00 !default;
22 | $yellow: #ffc107 !default;
23 | $green: #4dbd74 !default;
24 | $light-blue: #63c2de !default;
25 |
26 | $colors: () !default;
27 | $colors: map-merge((
28 | "light-blue": $light-blue,
29 | "gray-100": $gray-100,
30 | "gray-200": $gray-200,
31 | "gray-300": $gray-300,
32 | "gray-400": $gray-400,
33 | "gray-500": $gray-500,
34 | "gray-600": $gray-600,
35 | "gray-700": $gray-700,
36 | "gray-800": $gray-800,
37 | "gray-900": $gray-900
38 | ), $colors);
39 |
40 | $secondary: $gray-300 !default;
41 | $info: $light-blue !default;
42 |
43 | // Options
44 | //
45 | // Quickly modify global styling by enabling or disabling optional features.
46 |
47 | $enable-transitions: true !default;
48 | // $enable-rounded: false !default;
49 |
50 | // Body
51 | //
52 | // Settings for the `` element.
53 |
54 | $body-bg: #e4e5e6 !default;
55 |
56 | // Components
57 | //
58 | // Define common padding and border radius sizes and more.
59 |
60 | $border-color: $gray-300 !default;
61 |
62 | // Typography
63 | //
64 | // Font, line-height, and color for body text, headings, and more.
65 |
66 | $font-size-base: .875rem !default;
67 |
68 | // Breadcrumbs
69 |
70 | $breadcrumb-bg: #fff !default;
71 | $breadcrumb-margin-bottom: 1.5rem !default;
72 | $breadcrumb-border-radius: 0 !default;
73 |
74 | // Cards
75 |
76 | $card-border-color: $gray-300 !default;
77 | $card-cap-bg: $gray-100 !default;
78 |
79 | // Dropdowns
80 |
81 | $dropdown-padding-y: 0 !default;
82 | $dropdown-border-color: $gray-300 !default;
83 | $dropdown-divider-bg: $gray-200 !default;
84 |
85 | // Buttons
86 |
87 | $btn-secondary-border: $gray-300 !default;
88 |
89 | // Progress bars
90 |
91 | $progress-bg: $gray-100 !default;
92 |
93 | // Tables
94 |
95 | $table-bg-accent: $gray-100 !default;
96 | $table-bg-hover: $gray-100 !default;
97 |
98 | // Forms
99 |
100 | $input-group-addon-bg: $gray-100 !default;
101 | $input-border-color: $gray-200 !default;
102 | $input-group-addon-border-color: $gray-200 !default;
103 |
--------------------------------------------------------------------------------
/app/templates/common/form_macros.html:
--------------------------------------------------------------------------------
1 | {% macro render_field(field, label=None, label_visible=true, right_url=None, right_label=None) -%}
2 |
14 | {%- endmacro %}
15 |
16 | {% macro render_multicheckbox_field(field, label=None, label_visible=true) -%}
17 |
31 | {%- endmacro %}
32 |
33 |
34 | {% macro render_checkbox_field(field, label=None) -%}
35 | {% if not label %}{% set label=field.label.text %}{% endif %}
36 |
37 |
40 |
41 | {%- endmacro %}
42 |
43 | {% macro render_radio_field(field, label=None, label_visible=true) -%}
44 |
45 |
62 | {%- endmacro %}
63 |
64 | {% macro render_submit_field(field, label=None, tabindex=None) -%}
65 | {% if not label %}{% set label=field.label.text %}{% endif %}
66 | {##}
67 |
70 | {%- endmacro %}
71 |
--------------------------------------------------------------------------------
/tests/test_page_urls.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 SolidBuilds.com. All rights reserved
2 | #
3 | # Authors: Ling Thio
4 |
5 | from __future__ import print_function # Use print() instead of print
6 | from flask import url_for
7 |
8 |
9 | def test_page_urls(client):
10 | # Visit home page
11 | response = client.get('/', follow_redirects=True)
12 | assert response.status_code==200
13 |
14 | # Try to login with wrong email
15 | response = client.post(url_for('user.login'), follow_redirects=True,
16 | data=dict(email='non_member@example.com', password='Password1'))
17 | assert response.status_code==200
18 | assert b"You have signed in successfully" not in response.data
19 | assert b"Sign In to your account" in response.data
20 |
21 | # Login as user and visit User page
22 | response = client.post(url_for('user.login'), follow_redirects=True,
23 | data=dict(email='member@example.com', password='Password1'))
24 | assert response.status_code==200
25 | assert b"You have signed in successfully" in response.data
26 | assert b"Sign In to your account" not in response.data
27 |
28 | response = client.get(url_for('main.member_page'), follow_redirects=True)
29 | assert response.status_code==200
30 | assert b"Traffic" in response.data
31 |
32 | # Edit User Profile page
33 | response = client.get(url_for('main.user_profile_page'), follow_redirects=True)
34 | assert response.status_code==200
35 | assert b"User Profile" in response.data
36 | assert b"First name" in response.data
37 | assert b"Member" in response.data
38 |
39 | response = client.post(url_for('main.user_profile_page'), follow_redirects=True,
40 | data=dict(first_name='User', last_name='User'))
41 | assert b"User Profile" in response.data
42 | assert b"First name" in response.data
43 | assert b"Member" not in response.data
44 |
45 | response = client.get(url_for('main.member_page'), follow_redirects=True)
46 | assert response.status_code==200
47 | assert b"User Profile" not in response.data
48 | assert b"First name" not in response.data
49 | assert b"Traffic" in response.data
50 |
51 | # Logout
52 | response = client.get(url_for('user.logout'), follow_redirects=True)
53 | assert response.status_code==200
54 | assert b"You have signed out successfully." in response.data
55 |
56 | # Login as admin and visit Admin page
57 | response = client.post(url_for('user.login'), follow_redirects=True,
58 | data=dict(email='admin@example.com', password='Password1'))
59 | assert response.status_code==200
60 | assert b"You have signed in successfully" in response.data
61 | assert b"Sign In to your account" not in response.data
62 |
63 | response = client.get(url_for('main.admin_page'), follow_redirects=True)
64 | assert response.status_code==200
65 | print(url_for('main.admin_page'))
66 | assert b"System Users" in response.data
67 | assert b"Create User" in response.data
68 |
69 | # Logout
70 | response = client.get(url_for('user.logout'), follow_redirects=True)
71 | assert response.status_code==200
72 | assert b"You have signed out successfully." in response.data
73 |
--------------------------------------------------------------------------------
/app/templates/flask_user/register.html:
--------------------------------------------------------------------------------
1 | {% extends "common/page_base.html" %}
2 |
3 | {% block content %}
4 | {% from "flask_user/_macros.html" import render_field, render_submit_field %}
5 |
72 | {% endblock %}
73 |
--------------------------------------------------------------------------------
/app/templates/flask_user/login.html:
--------------------------------------------------------------------------------
1 | {% extends "common/page_base.html" %}
2 |
3 | {% block content %}
4 |
5 |
6 |
7 |
8 |
9 |
61 |
62 |
63 |
64 |
Sign up
65 |
All registrations have to be approved by an administrator before access is granted.
66 |
Register Now!
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | {% endblock %}
75 |
--------------------------------------------------------------------------------
/app/templates/layout.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {% block title_tag %}
7 | {{ config['APP_NAME'] }}{% block title %}{% endblock %}
8 | {% endblock %}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | {% block body %}
44 |
45 |
46 | {% block pre_content %}{% endblock %}
47 |
48 | {% block content %}{% endblock %}
49 |
50 | {% block post_content %}{% endblock %}
51 |
52 |
53 | {% endblock %}
54 |
55 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/app/static/coreui/js/src/aside-menu.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery'
2 | import ToggleClasses from './toggle-classes'
3 |
4 | /**
5 | * --------------------------------------------------------------------------
6 | * CoreUI (v2.0.0-beta.2): aside-menu.js
7 | * Licensed under MIT (https://coreui.io/license)
8 | * --------------------------------------------------------------------------
9 | */
10 |
11 | const AsideMenu = (($) => {
12 | /**
13 | * ------------------------------------------------------------------------
14 | * Constants
15 | * ------------------------------------------------------------------------
16 | */
17 |
18 | const NAME = 'aside-menu'
19 | const VERSION = '2.0.0-beta.2'
20 | const DATA_KEY = 'coreui.aside-menu'
21 | const EVENT_KEY = `.${DATA_KEY}`
22 | const DATA_API_KEY = '.data-api'
23 | const JQUERY_NO_CONFLICT = $.fn[NAME]
24 |
25 | const Event = {
26 | CLICK : 'click',
27 | LOAD_DATA_API : `load${EVENT_KEY}${DATA_API_KEY}`,
28 | TOGGLE : 'toggle'
29 | }
30 |
31 | const Selector = {
32 | BODY : 'body',
33 | ASIDE_MENU : '.aside-menu',
34 | ASIDE_MENU_TOGGLER : '.aside-menu-toggler'
35 | }
36 |
37 | const ShowClassNames = [
38 | 'aside-menu-show',
39 | 'aside-menu-sm-show',
40 | 'aside-menu-md-show',
41 | 'aside-menu-lg-show',
42 | 'aside-menu-xl-show'
43 | ]
44 |
45 | /**
46 | * ------------------------------------------------------------------------
47 | * Class Definition
48 | * ------------------------------------------------------------------------
49 | */
50 |
51 | class AsideMenu {
52 | constructor(element) {
53 | this._element = element
54 | this._addEventListeners()
55 | }
56 |
57 | // Getters
58 |
59 | static get VERSION() {
60 | return VERSION
61 | }
62 |
63 | // Private
64 |
65 | _addEventListeners() {
66 | $(Selector.ASIDE_MENU_TOGGLER).on(Event.CLICK, (event) => {
67 | event.preventDefault()
68 | event.stopPropagation()
69 | const toggle = event.currentTarget.dataset.toggle
70 | ToggleClasses(toggle, ShowClassNames)
71 | })
72 | }
73 |
74 | // Static
75 |
76 | static _jQueryInterface() {
77 | return this.each(function () {
78 | const $element = $(this)
79 | let data = $element.data(DATA_KEY)
80 |
81 | if (!data) {
82 | data = new AsideMenu(this)
83 | $element.data(DATA_KEY, data)
84 | }
85 | })
86 | }
87 | }
88 |
89 | /**
90 | * ------------------------------------------------------------------------
91 | * Data Api implementation
92 | * ------------------------------------------------------------------------
93 | */
94 |
95 | $(window).on(Event.LOAD_DATA_API, () => {
96 | const asideMenu = $(Selector.ASIDE_MENU)
97 | AsideMenu._jQueryInterface.call(asideMenu)
98 | })
99 |
100 | /**
101 | * ------------------------------------------------------------------------
102 | * jQuery
103 | * ------------------------------------------------------------------------
104 | */
105 |
106 | $.fn[NAME] = AsideMenu._jQueryInterface
107 | $.fn[NAME].Constructor = AsideMenu
108 | $.fn[NAME].noConflict = () => {
109 | $.fn[NAME] = JQUERY_NO_CONFLICT
110 | return AsideMenu._jQueryInterface
111 | }
112 |
113 | return AsideMenu
114 | })($)
115 |
116 | export default AsideMenu
117 |
--------------------------------------------------------------------------------
/app/static/coreui/scss/_deprecated.scss:
--------------------------------------------------------------------------------
1 | // stylelint-disable
2 | .horizontal-bars {
3 | padding: 0;
4 | margin: 0;
5 | list-style: none;
6 |
7 | li {
8 | position: relative;
9 | height: 40px;
10 | line-height: 40px;
11 | vertical-align: middle;
12 |
13 | .title {
14 | width: 100px;
15 | font-size: 12px;
16 | font-weight: 600;
17 | color: $text-muted;
18 | vertical-align: middle;
19 | }
20 |
21 | .bars {
22 | position: absolute;
23 | top: 15px;
24 | width: 100%;
25 | padding-left: 100px;
26 |
27 | .progress:first-child {
28 | margin-bottom: 2px;
29 | }
30 | }
31 |
32 | &.legend {
33 | text-align: center;
34 |
35 | .badge {
36 | display: inline-block;
37 | width: 8px;
38 | height: 8px;
39 | padding: 0;
40 | }
41 | }
42 |
43 | &.divider {
44 | height: 40px;
45 |
46 | i {
47 | margin: 0 !important;
48 | }
49 | }
50 | }
51 |
52 | &.type-2 {
53 |
54 | li {
55 | overflow: hidden;
56 |
57 | i {
58 | display: inline-block;
59 | margin-right: $spacer;
60 | margin-left: 5px;
61 | font-size: 18px;
62 | line-height: 40px;
63 | }
64 |
65 | .title {
66 | display: inline-block;
67 | width: auto;
68 | margin-top: -9px;
69 | font-size: $font-size-base;
70 | font-weight: normal;
71 | line-height: 40px;
72 | color: $body-color;
73 | }
74 |
75 | .value {
76 | float: right;
77 | font-weight: 600;
78 | }
79 |
80 | .bars {
81 | position: absolute;
82 | top: auto;
83 | bottom: 0;
84 | padding: 0;
85 | }
86 | }
87 | }
88 | }
89 |
90 |
91 | // .social-box
92 | .social-box {
93 | min-height: 160px;
94 | margin-bottom: 2 * $card-spacer-y;
95 | text-align: center;
96 | background: #fff;
97 | border: $card-border-width solid $card-border-color;
98 | @include border-radius($card-border-radius);
99 |
100 | i {
101 | display: block;
102 | margin: -1px -1px 0;
103 | font-size: 40px;
104 | line-height: 90px;
105 | background: $gray-200;
106 |
107 | @include border-radius($card-border-radius $card-border-radius 0 0);
108 | }
109 |
110 | .chart-wrapper {
111 | height: 90px;
112 | margin: -90px 0 0;
113 |
114 | canvas {
115 | width: 100%;
116 | height: 90px;
117 | }
118 | }
119 |
120 | ul {
121 | padding: 10px 0;
122 | list-style: none;
123 |
124 |
125 | li {
126 | display: block;
127 | float: left;
128 | width: 50%;
129 |
130 | &:first-child {
131 | border-right: 1px solid $border-color;
132 | }
133 |
134 | strong {
135 | display: block;
136 | font-size: 20px;
137 | }
138 |
139 | span {
140 | font-size: 10px;
141 | font-weight: 500;
142 | color: $border-color;
143 | text-transform: uppercase;
144 | }
145 | }
146 | }
147 |
148 | &.facebook {
149 | i {
150 | color: #fff;
151 | background: $facebook;
152 | }
153 | }
154 |
155 | &.twitter {
156 | i {
157 | color: #fff;
158 | background: $twitter;
159 | }
160 | }
161 |
162 | &.linkedin {
163 | i {
164 | color: #fff;
165 | background: $linkedin;
166 | }
167 | }
168 |
169 | &.google-plus {
170 | i {
171 | color: #fff;
172 | background: $google-plus;
173 | }
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: "3"
2 |
3 | services:
4 | app:
5 | build:
6 | context: .
7 | dockerfile: dockerfile.app
8 | environment:
9 | - CELERY_BROKER=rabbitmq
10 | - SQLALCHEMY_DATABASE_URI=postgres://dbuser:dbpassword@postgres/dbname
11 | - SECRET_KEY=NotSecure
12 | - DEBUG=true
13 |
14 |
15 | #
16 | # Keep the lines below uncommented when using standard user management.
17 | # Make sure they are commented out when using LDAP.
18 | #
19 |
20 | - ADMIN_USERNAME=admin
21 | - ADMIN_EMAIL=admin@example.com
22 | - ADMIN_PASSWORD=Password1
23 | - DEV_USERNAME=developer
24 | - DEV_EMAIL=dev@example.com
25 | - DEV_PASSWORD=Password1
26 | - USER_USERNAME=user
27 | - USER_EMAIL=user@example.com
28 | - USER_PASSWORD=Password1
29 |
30 | #
31 | # Uncomment below to enable LDAP.
32 | #
33 | #
34 | # - USER_LDAP=true
35 | # - LDAP_HOST=ldap://ldap
36 | # - LDAP_BIND_DN=cn=admin,dc=example,dc=org
37 | # - LDAP_BIND_PASSWORD=admin
38 | # - LDAP_USERNAME_ATTRIBUTE=cn
39 | # - LDAP_USER_BASE=ou=users,dc=example,dc=org
40 | # - LDAP_GROUP_OBJECT_CLASS=posixGroup
41 | # - LDAP_GROUP_ATTRIBUTE=cn
42 | # - LDAP_GROUP_BASE=ou=groups,dc=example,dc=org
43 | # - LDAP_GROUP_TO_ROLE_ADMIN=admin
44 | # - LDAP_GROUP_TO_ROLE_DEV=dev
45 | # - LDAP_GROUP_TO_ROLE_USER=user
46 | # - LDAP_EMAIL_ATTRIBUTE=mail
47 |
48 | ports:
49 | - "80:80"
50 | - "5000:5000"
51 | volumes:
52 | - ./docker/app/uwsgi.ini:/app/uwsgi.ini
53 | - ./docker/app/prestart.sh:/app/prestart.sh
54 | - ./app:/app/app
55 | - ./manage.py:/app/manage.py
56 | - ./unicorn.py:/app/unicorn.py
57 |
58 | # If the app needs AWS access and you have it configured on your host uncomment these lines.
59 | # - ~/.aws/credentials:/home/apprunner/.aws/credentials
60 | # - ~/.aws/config:/home/apprunner/.aws/config
61 |
62 |
63 | worker:
64 | build:
65 | context: .
66 | dockerfile: dockerfile.worker
67 | environment:
68 | - CELERY_BROKER=rabbitmq
69 | - SQLALCHEMY_DATABASE_URI=postgres://dbuser:dbpassword@postgres/dbname
70 | - ADMIN_EMAIL=admin@example.com
71 | - ADMIN_PASSWORD=Password1
72 | - USER_EMAIL=user@example.com
73 | - USER_PASSWORD=Password1
74 | - SECRET_KEY=NotSecure
75 | volumes:
76 | - ./app:/app/app
77 | - ./docker/worker/start_worker.sh:/home/apprunner/start_worker.sh
78 |
79 | # If the app needs AWS access and you have it configured on your host uncomment these lines.
80 | # - ~/.aws/credentials:/home/apprunner/.aws/credentials
81 | # - ~/.aws/config:/home/apprunner/.aws/config
82 |
83 |
84 | postgres:
85 | image: postgres
86 | environment:
87 | POSTGRES_USER: dbuser
88 | POSTGRES_PASSWORD: dbpassword
89 | POSTGRES_DB: dbname
90 |
91 | rabbitmq:
92 | image: rabbitmq
93 |
94 |
95 | #
96 | # Uncomment to enable LDAP- make sure to set `USER_LDAP` in the app as well.
97 | #
98 |
99 | #
100 | # ldap:
101 | # image: osixia/openldap
102 | # command: --copy-service
103 | # volumes:
104 | # - ./docker/ldap/bootstrap.ldif:/container/service/slapd/assets/config/bootstrap/ldif/50-bootstrap.ldif:ro
105 | # environment:
106 | # LDAP_REMOVE_CONFIG_AFTER_SETUP: "false"
107 | #
108 | # phpldapadmin:
109 | # image: osixia/phpldapadmin:latest
110 | # environment:
111 | # PHPLDAPADMIN_LDAP_HOSTS: "ldap"
112 | # PHPLDAPADMIN_HTTPS: "false"
113 | # ports:
114 | # - "8080:80"
115 | # depends_on:
116 | # - ldap
117 |
--------------------------------------------------------------------------------
/app/static/coreui/scss/_loading.scss:
--------------------------------------------------------------------------------
1 | // Angular Version
2 | // Make clicks pass-through
3 | // stylelint-disable
4 | #loading-bar,
5 | #loading-bar-spinner {
6 | -webkit-pointer-events: none;
7 | pointer-events: none;
8 | -moz-transition: 350ms linear all;
9 | -o-transition: 350ms linear all;
10 | -webkit-transition: 350ms linear all;
11 | transition: 350ms linear all;
12 | }
13 |
14 | #loading-bar.ng-enter,
15 | #loading-bar.ng-leave.ng-leave-active,
16 | #loading-bar-spinner.ng-enter,
17 | #loading-bar-spinner.ng-leave.ng-leave-active {
18 | opacity: 0;
19 | }
20 |
21 | #loading-bar.ng-enter.ng-enter-active,
22 | #loading-bar.ng-leave,
23 | #loading-bar-spinner.ng-enter.ng-enter-active,
24 | #loading-bar-spinner.ng-leave {
25 | opacity: 1;
26 | }
27 |
28 | #loading-bar .bar {
29 | position: fixed;
30 | top: 0;
31 | left: 0;
32 | z-index: 20002;
33 | width: 100%;
34 | height: 2px;
35 | background: theme-color("primary");
36 | border-top-right-radius: 1px;
37 | border-bottom-right-radius: 1px;
38 | -moz-transition: width 350ms;
39 | -o-transition: width 350ms;
40 | -webkit-transition: width 350ms;
41 | transition: width 350ms;
42 | }
43 |
44 | // Fancy blur effect
45 | #loading-bar .peg {
46 | position: absolute;
47 | top: 0;
48 | right: 0;
49 | width: 70px;
50 | height: 2px;
51 | -moz-border-radius: 100%;
52 | -webkit-border-radius: 100%;
53 | border-radius: 100%;
54 | -moz-box-shadow: #29d 1px 0 6px 1px;
55 | -ms-box-shadow: #29d 1px 0 6px 1px;
56 | -webkit-box-shadow: #29d 1px 0 6px 1px;
57 | box-shadow: #29d 1px 0 6px 1px;
58 | opacity: .45;
59 | }
60 |
61 | #loading-bar-spinner {
62 | position: fixed;
63 | top: 10px;
64 | left: 10px;
65 | z-index: 10002;
66 | display: block;
67 | }
68 |
69 | #loading-bar-spinner .spinner-icon {
70 | width: 14px;
71 | height: 14px;
72 |
73 | border: solid 2px transparent;
74 | border-top-color: #29d;
75 | border-left-color: #29d;
76 | border-radius: 50%;
77 |
78 | -moz-animation: loading-bar-spinner 400ms linear infinite;
79 | -ms-animation: loading-bar-spinner 400ms linear infinite;
80 | -o-animation: loading-bar-spinner 400ms linear infinite;
81 | -webkit-animation: loading-bar-spinner 400ms linear infinite;
82 | animation: loading-bar-spinner 400ms linear infinite;
83 | }
84 |
85 | @-webkit-keyframes loading-bar-spinner {
86 | 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); }
87 | 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); }
88 | }
89 | @-moz-keyframes loading-bar-spinner {
90 | 0% { -moz-transform: rotate(0deg); transform: rotate(0deg); }
91 | 100% { -moz-transform: rotate(360deg); transform: rotate(360deg); }
92 | }
93 | @-o-keyframes loading-bar-spinner {
94 | 0% { -o-transform: rotate(0deg); transform: rotate(0deg); }
95 | 100% { -o-transform: rotate(360deg); transform: rotate(360deg); }
96 | }
97 | @-ms-keyframes loading-bar-spinner {
98 | 0% { -ms-transform: rotate(0deg); transform: rotate(0deg); }
99 | 100% { -ms-transform: rotate(360deg); transform: rotate(360deg); }
100 | }
101 | @keyframes loading-bar-spinner {
102 | 0% { transform: rotate(0deg); transform: rotate(0deg); }
103 | 100% { transform: rotate(360deg); transform: rotate(360deg); }
104 | }
105 |
106 | //Ajax & Static Version
107 | .pace {
108 | -webkit-pointer-events: none;
109 | pointer-events: none;
110 |
111 | -moz-user-select: none;
112 | -webkit-user-select: none;
113 | user-select: none;
114 | }
115 |
116 | .pace-inactive {
117 | display: none;
118 | }
119 |
120 | .pace .pace-progress {
121 | position: fixed;
122 | top: 0;
123 | right: 100%;
124 | z-index: 2000;
125 | width: 100%;
126 | height: 2px;
127 | background: theme-color("primary");
128 | }
129 |
--------------------------------------------------------------------------------
/app/static/coreui/js/src/ajax-load.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery'
2 |
3 | /**
4 | * --------------------------------------------------------------------------
5 | * CoreUI (v2.0.0-beta.2): ajax-load.js
6 | * Licensed under MIT (https://coreui.io/license)
7 | * --------------------------------------------------------------------------
8 | */
9 |
10 |
11 | const AjaxLoad = (($) => {
12 | /**
13 | * ------------------------------------------------------------------------
14 | * Constants
15 | * ------------------------------------------------------------------------
16 | */
17 |
18 | const NAME = 'ajaxLoad'
19 | const VERSION = '2.0.0-beta.2'
20 | const DATA_KEY = 'coreui.ajaxLoad'
21 | const JQUERY_NO_CONFLICT = $.fn[NAME]
22 |
23 | const ClassName = {
24 | ACTIVE : 'active',
25 | NAV_PILLS : 'nav-pills',
26 | NAV_TABS : 'nav-tabs',
27 | OPEN : 'open'
28 | }
29 |
30 | const Event = {
31 | CLICK : 'click'
32 | }
33 |
34 | const Selector = {
35 | NAV_DROPDOWN : '.sidebar-nav .nav-dropdown',
36 | NAV_LINK : '.sidebar-nav .nav-link',
37 | NAV_ITEM : '.sidebar-nav .nav-item'
38 | }
39 |
40 | const Default = {
41 | defaultPage : 'main.html',
42 | errorPage : '404.html',
43 | subpagesDirectory : 'views/'
44 | }
45 |
46 | class AjaxLoad {
47 | constructor(element, config) {
48 | this._config = this._getConfig(config)
49 | this._element = element
50 |
51 | const url = location.hash.replace(/^#/, '')
52 | url !== '' ? this.setUpUrl(url) : this.setUpUrl(this._config.defaultPage)
53 | this._addEventListeners()
54 | }
55 |
56 | // Getters
57 |
58 | static get VERSION() {
59 | return VERSION
60 | }
61 |
62 | static get Default() {
63 | return Default
64 | }
65 |
66 | // Public
67 |
68 | loadPage(url) {
69 | const element = this._element
70 | const config = this._config
71 |
72 | $.ajax({
73 | type : 'GET',
74 | url : config.subpagesDirectory + url,
75 | dataType : 'html',
76 | cache : false,
77 | async: false,
78 | success: function success() {
79 | if (typeof Pace !== 'undefined') {
80 | Pace.restart()
81 | }
82 | $('body').animate({
83 | scrollTop: 0
84 | }, 0)
85 | $(element).load(config.subpagesDirectory + url, null, () => {
86 | window.location.hash = url
87 | })
88 | },
89 | error: function error() {
90 | window.location.href = config.errorPage
91 | }
92 | })
93 | }
94 |
95 | setUpUrl(url) {
96 | $(Selector.NAV_LINK).removeClass(ClassName.ACTIVE)
97 | $(Selector.NAV_DROPDOWN).removeClass(ClassName.OPEN)
98 | // eslint-disable-next-line prefer-template
99 | $(Selector.NAV_DROPDOWN + ':has(a[href="' + url.replace(/^\//, '').split('?')[0] + '"])').addClass(ClassName.OPEN)
100 | // eslint-disable-next-line prefer-template
101 | $(Selector.NAV_ITEM + ' a[href="' + url.replace(/^\//, '').split('?')[0] + '"]').addClass(ClassName.ACTIVE)
102 |
103 | this.loadPage(url)
104 | }
105 |
106 | loadBlank(url) {
107 | window.open(url)
108 | }
109 |
110 | loadTop(url) {
111 | window.location = url
112 | }
113 |
114 | // Private
115 |
116 | _getConfig(config) {
117 | config = {
118 | ...Default,
119 | ...config
120 | }
121 | return config
122 | }
123 |
124 | _addEventListeners() {
125 | $(document).on(Event.CLICK, Selector.NAV_LINK + '[href!="#"]', (event) => {
126 | event.preventDefault()
127 | event.stopPropagation()
128 |
129 | if (event.currentTarget.target === '_top') {
130 | this.loadTop(event.currentTarget.href)
131 | } else if (event.currentTarget.target === '_blank') {
132 | this.loadBlank(event.currentTarget.href)
133 | } else {
134 | this.setUpUrl(event.currentTarget.pathname)
135 | }
136 | })
137 | }
138 |
139 | // Static
140 |
141 | static _jQueryInterface(config) {
142 | return this.each(function () {
143 | let data = $(this).data(DATA_KEY)
144 | const _config = typeof config === 'object' && config
145 |
146 | if (!data) {
147 | data = new AjaxLoad(this, _config)
148 | $(this).data(DATA_KEY, data)
149 | }
150 | })
151 | }
152 | }
153 |
154 | /**
155 | * ------------------------------------------------------------------------
156 | * jQuery
157 | * ------------------------------------------------------------------------
158 | */
159 |
160 | $.fn[NAME] = AjaxLoad._jQueryInterface
161 | $.fn[NAME].Constructor = AjaxLoad
162 | $.fn[NAME].noConflict = () => {
163 | $.fn[NAME] = JQUERY_NO_CONFLICT
164 | return AjaxLoad._jQueryInterface
165 | }
166 |
167 | return AjaxLoad
168 | })($)
169 |
170 | export default AjaxLoad
171 |
--------------------------------------------------------------------------------
/app/static/coreui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "_args": [
3 | [
4 | "@coreui/coreui@2.0.0-beta.2",
5 | "/home/matt/projects/Templates/coreui-free-bootstrap-admin-template"
6 | ]
7 | ],
8 | "_from": "@coreui/coreui@2.0.0-beta.2",
9 | "_id": "@coreui/coreui@2.0.0-beta.2",
10 | "_inBundle": false,
11 | "_integrity": "sha512-BQ2/YlpFVKdXetok87MXwWXPbyHNhmcRZBSjKo7yM0vn4Ek2662d9tmoUpixuuQoFbzgDI8WskZ7SXHxTByBvA==",
12 | "_location": "/@coreui/coreui",
13 | "_phantomChildren": {},
14 | "_requested": {
15 | "type": "version",
16 | "registry": true,
17 | "raw": "@coreui/coreui@2.0.0-beta.2",
18 | "name": "@coreui/coreui",
19 | "escapedName": "@coreui%2fcoreui",
20 | "scope": "@coreui",
21 | "rawSpec": "2.0.0-beta.2",
22 | "saveSpec": null,
23 | "fetchSpec": "2.0.0-beta.2"
24 | },
25 | "_requiredBy": [
26 | "/"
27 | ],
28 | "_resolved": "https://registry.npmjs.org/@coreui/coreui/-/coreui-2.0.0-beta.2.tgz",
29 | "_spec": "2.0.0-beta.2",
30 | "_where": "/home/matt/projects/Templates/coreui-free-bootstrap-admin-template",
31 | "author": {
32 | "name": "Łukasz Holeczek",
33 | "url": "http://holeczek.pl"
34 | },
35 | "browserslist": [
36 | "last 1 major version",
37 | ">= 1%",
38 | "Chrome >= 45",
39 | "Firefox >= 38",
40 | "Edge >= 12",
41 | "Explorer >= 10",
42 | "iOS >= 9",
43 | "Safari >= 9",
44 | "Android >= 4.4",
45 | "Opera >= 30"
46 | ],
47 | "bugs": {
48 | "url": "https://github.com/coreui/coreui/issues",
49 | "email": "support@coreui.io"
50 | },
51 | "contributors": [
52 | {
53 | "name": "Andrzej Kopański",
54 | "url": "https://github.com/xidedix"
55 | }
56 | ],
57 | "dependencies": {
58 | "bootstrap": "^4.0.0"
59 | },
60 | "description": "Open Source UI Kit built on top of Bootstrap 4",
61 | "devDependencies": {
62 | "@babel/cli": "7.0.0-beta.42",
63 | "@babel/core": "7.0.0-beta.42",
64 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0-beta.42",
65 | "@babel/preset-env": "7.0.0-beta.42",
66 | "autoprefixer": "^7.1.6",
67 | "babel-eslint": "^8.2.2",
68 | "babel-plugin-istanbul": "^4.1.5",
69 | "babel-plugin-transform-es2015-modules-strip": "^0.1.1",
70 | "babel-plugin-transform-object-rest-spread": "^6.26.0",
71 | "clean-css-cli": "^4.1.10",
72 | "cross-env": "^5.1.4",
73 | "eslint": "^4.16.0",
74 | "eslint-plugin-compat": "^2.1.0",
75 | "node-sass": "^4.7.1",
76 | "nodemon": "^1.12.1",
77 | "npm-run-all": "^4.1.2",
78 | "postcss-cli": "^4.1.1",
79 | "rollup": "^0.57.1",
80 | "rollup-plugin-babel": "4.0.0-beta.2",
81 | "rollup-plugin-node-resolve": "^3.3.0",
82 | "semver": "^5.5.0",
83 | "shelljs": "^0.8.1",
84 | "shx": "^0.2.2",
85 | "stylelint": "^8.2.0",
86 | "stylelint-config-recommended-scss": "^2.0.0",
87 | "stylelint-config-standard": "^17.0.0",
88 | "stylelint-order": "^0.7.0",
89 | "stylelint-scss": "^2.1.0",
90 | "uglify-js": "^3.3.8"
91 | },
92 | "engines": {
93 | "node": ">=6"
94 | },
95 | "files": [
96 | "dist/",
97 | "js/",
98 | "scss/"
99 | ],
100 | "homepage": "https://coreui.io",
101 | "keywords": [
102 | "bootstrap",
103 | "css",
104 | "dashboard",
105 | "framework",
106 | "front-end",
107 | "responsive",
108 | "sass",
109 | "ui kit",
110 | "webapp"
111 | ],
112 | "license": "MIT",
113 | "main": "dist/js/coreui.js",
114 | "name": "@coreui/coreui",
115 | "peerDependencies": {
116 | "jquery": "1.9.1 - 3",
117 | "perfect-scrollbar": "^1.3.0",
118 | "popper.js": "^1.12.9"
119 | },
120 | "repository": {
121 | "type": "git",
122 | "url": "git+https://github.com/coreui/coreui.git"
123 | },
124 | "sass": "scss/coreui.scss",
125 | "scripts": {
126 | "css": "npm-run-all --parallel css-lint css-compile* --sequential css-prefix css-minify*",
127 | "css-compile": "node-sass --output-style expanded --source-map true --source-map-contents true --precision 6 scss/coreui.scss dist/css/coreui.css",
128 | "css-compile-bootstrap": "node-sass --output-style expanded --source-map true --source-map-contents true --precision 6 scss/bootstrap.scss dist/css/bootstrap.css",
129 | "css-compile-standalone": "node-sass --output-style expanded --source-map true --source-map-contents true --precision 6 scss/coreui-standalone.scss dist/css/coreui-standalone.css",
130 | "css-lint": "stylelint --config build/.stylelintrc --syntax scss \"scss/**/*.scss\"",
131 | "css-minify": "cleancss --level 1 --source-map --source-map-inline-sources --output dist/css/coreui.min.css dist/css/coreui.css",
132 | "css-minify-bootstrap": "cleancss --level 1 --source-map --source-map-inline-sources --output dist/css/bootstrap.min.css dist/css/bootstrap.css",
133 | "css-minify-standalone": "cleancss --level 1 --source-map --source-map-inline-sources --output dist/css/coreui-standalone.min.css dist/css/coreui-standalone.css",
134 | "css-prefix": "postcss --config build/postcss.config.js --replace \"dist/css/*.css\" \"!dist/css/*.min.css\"",
135 | "dist": "npm-run-all --parallel css js",
136 | "increment-version": "node build/increment-version.js",
137 | "js": "npm-run-all js-lint js-compile* js-minify*",
138 | "js-compile": "rollup --environment BUNDLE:false --config build/rollup.config.js --sourcemap",
139 | "js-compile-plugins": "cross-env PLUGINS=true babel js/src/ --out-dir js/dist/ --source-maps",
140 | "js-lint": "eslint js/",
141 | "js-minify": "uglifyjs --compress typeofs=false --mangle --comments \"/^!/\" --source-map \"content=dist/js/coreui.js.map,includeSources,url=coreui.min.js.map\" --output dist/js/coreui.min.js dist/js/coreui.js",
142 | "release-version": "node build/change-version.js",
143 | "release-zip": "cd dist/ && zip -r9 coreui-$npm_package_version-dist.zip * && shx mv coreui-$npm_package_version-dist.zip ..",
144 | "watch": "npm-run-all --parallel watch-css watch-js",
145 | "watch-css": "nodemon --ignore dist/ -e scss -x \"npm run css\"",
146 | "watch-js": "nodemon --ignore js/dist/ --ignore dist/ -e js -x \"npm-run-all js-compile* js-minify*\""
147 | },
148 | "style": "dist/css/coreui.css",
149 | "version": "2.0.0-beta.2"
150 | }
151 |
--------------------------------------------------------------------------------
/app/views/misc_views.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017 Twin Tech Labs. All rights reserved
2 |
3 | from flask import Blueprint, redirect, render_template, current_app, abort
4 | from flask import request, url_for, flash, send_from_directory, jsonify, render_template_string
5 | from flask_user import current_user, login_required, roles_accepted
6 |
7 | from app import db
8 | from app.models.user_models import UserProfileForm, User, UsersRoles, Role
9 | from app.utils.forms import ConfirmationForm
10 | import uuid, json, os
11 | import datetime
12 |
13 | # When using a Flask app factory we must use a blueprint to avoid needing 'app' for '@app.route'
14 | main_blueprint = Blueprint('main', __name__, template_folder='templates')
15 |
16 | # The User page is accessible to authenticated users (users that have logged in)
17 | @main_blueprint.route('/')
18 | def member_page():
19 | if not current_user.is_authenticated:
20 | return redirect(url_for('user.login'))
21 | return render_template('pages/member_base.html')
22 |
23 | # The Admin page is accessible to users with the 'admin' role
24 | @main_blueprint.route('/admin')
25 | @roles_accepted('admin')
26 | def admin_page():
27 | return redirect(url_for('main.user_admin_page'))
28 |
29 | @main_blueprint.route('/users')
30 | @roles_accepted('admin')
31 | def user_admin_page():
32 | users = User.query.all()
33 | return render_template('pages/admin/users.html', users=users)
34 |
35 | @main_blueprint.route('/create_user', methods=['GET', 'POST'])
36 | @roles_accepted('admin')
37 | def create_user_page():
38 | if current_app.config.get('USER_LDAP', False):
39 | abort(400)
40 |
41 | form = UserProfileForm()
42 | roles = Role.query.all()
43 | form.roles.choices = [(x.id,x.name) for x in roles]
44 |
45 | if form.validate():
46 | user = User.query.filter(User.email == request.form['email']).first()
47 | if not user:
48 | user = User(email=form.email.data,
49 | first_name=form.first_name.data,
50 | last_name=form.last_name.data,
51 | password=current_app.user_manager.hash_password(form.password.data),
52 | active=True,
53 | email_confirmed_at=datetime.datetime.utcnow())
54 | db.session.add(user)
55 | db.session.commit()
56 | allowed_roles = form.roles.data
57 | for role in roles:
58 | if role.id not in allowed_roles:
59 | if role in user.roles:
60 | user.roles.remove(role)
61 | else:
62 | if role not in user.roles:
63 | user.roles.append(role)
64 | db.session.commit()
65 | flash('You successfully created the new user.', 'success')
66 | return redirect(url_for('main.user_admin_page'))
67 | flash('A user with that email address already exists', 'error')
68 | return render_template('pages/admin/create_user.html', form=form)
69 |
70 |
71 | @main_blueprint.route('/users//delete', methods=['GET', 'POST'])
72 | @roles_accepted('admin')
73 | def delete_user_page(user_id):
74 | if current_app.config.get('USER_LDAP', False):
75 | abort(400)
76 | form = ConfirmationForm()
77 | user = User.query.filter(User.id == user_id).first()
78 | if not user:
79 | abort(404)
80 | if form.validate():
81 | db.session.query(UsersRoles).filter_by(user_id = user_id).delete()
82 | db.session.query(User).filter_by(id = user_id).delete()
83 | db.session.commit()
84 | flash('You successfully deleted your user!', 'success')
85 | return redirect(url_for('main.user_admin_page'))
86 | return render_template('pages/admin/delete_user.html', form=form)
87 |
88 |
89 | @main_blueprint.route('/users//edit', methods=['GET', 'POST'])
90 | @roles_accepted('admin')
91 | def edit_user_page(user_id):
92 | if current_app.config.get('USER_LDAP', False):
93 | abort(400)
94 |
95 | user = User.query.filter(User.id == user_id).first()
96 | if not user:
97 | abort(404)
98 |
99 | form = UserProfileForm(obj=user)
100 | roles = Role.query.all()
101 | form.roles.choices = [(x.id,x.name) for x in roles]
102 |
103 | if form.validate():
104 | if 'password' in request.form and len(request.form['password']) >= 8:
105 | user.password = current_app.user_manager.hash_password(request.form['password'])
106 | user.email = form.email.data
107 | user.first_name = form.first_name.data
108 | user.last_name = form.last_name.data
109 | user.active = form.active.data
110 |
111 | allowed_roles = form.roles.data
112 | for role in roles:
113 | if role.id not in allowed_roles:
114 | if role in user.roles:
115 | user.roles.remove(role)
116 | else:
117 | if role not in user.roles:
118 | user.roles.append(role)
119 |
120 | db.session.commit()
121 | flash('You successfully edited the user.', 'success')
122 | return redirect(url_for('main.user_admin_page'))
123 |
124 | form.roles.data = [role.id for role in user.roles]
125 | return render_template('pages/admin/edit_user.html', form=form)
126 |
127 | @main_blueprint.route('/pages/profile', methods=['GET', 'POST'])
128 | @login_required
129 | def user_profile_page():
130 | if current_app.config.get('USER_LDAP', False):
131 | abort(400)
132 |
133 | # Initialize form
134 | form = UserProfileForm(request.form, obj=current_user)
135 |
136 | # Process valid POST
137 | if request.method == 'POST' and form.validate():
138 | # Copy form fields to user_profile fields
139 | form.populate_obj(current_user)
140 |
141 | # Save user_profile
142 | db.session.commit()
143 |
144 | # Redirect to home page
145 | return redirect(url_for('main.user_profile_page'))
146 |
147 | # Process GET or invalid POST
148 | return render_template('pages/user_profile_page.html',
149 | current_user=current_user,
150 | form=form)
151 |
--------------------------------------------------------------------------------
/app/extensions/ldap.py:
--------------------------------------------------------------------------------
1 |
2 | from flask import current_app, g
3 | from flask_login import current_user
4 | from flask_user import UserManager
5 | from flask_user.forms import LoginForm
6 | from flask_user.translation_utils import lazy_gettext as _ # map _() to lazy_gettext()
7 |
8 | import datetime
9 | from ldap3 import Server, Connection, ALL
10 | from app import db
11 | from app.models import user_models
12 |
13 |
14 | def authenticate(user, password):
15 | # define the server
16 | s = Server(current_app.config['LDAP_HOST'], get_info=ALL) # define an unsecure LDAP server, requesting info on DSE and schema
17 |
18 | # define the connection
19 | user_dn = get_dn_from_user(user)
20 | c = Connection(current_app.config['LDAP_HOST'], user=user_dn, password=password)
21 |
22 | # perform the Bind operation - used to check user password.
23 | if not c.bind():
24 | print('Unable to bind user %s' % (user_dn))
25 | return False
26 |
27 | # check to see if user is actually a valid user.
28 | return True
29 |
30 |
31 | def get_user_email(user):
32 | email_attribute = current_app.config.get('LDAP_EMAIL_ATTRIBUTE', False)
33 | if not email_attribute:
34 | return False
35 | conn = get_bound_connection()
36 | user_search = get_dn_from_user(user)
37 | user_object = '(objectclass=%s)' % (current_app.config['LDAP_USER_OBJECT_CLASS'],)
38 | conn.search(user_search, user_object, attributes=[email_attribute])
39 | if len(conn.entries) < 1:
40 | return False
41 | return getattr(conn.entries[0], email_attribute, False)[0]
42 |
43 |
44 | def user_in_group(user, group):
45 | conn = get_bound_connection()
46 | group_search = get_dn_from_group(group)
47 | group_object = '(objectclass=%s)' % (current_app.config['LDAP_GROUP_OBJECT_CLASS'],)
48 | conn.search(group_search, group_object, attributes=['memberUid'])
49 | if len(conn.entries) < 1:
50 | return False
51 | members = conn.entries[0].memberUid
52 | return user in members
53 |
54 |
55 | def get_bound_connection():
56 | if 'ldap_connection' in g:
57 | return g.ldap_connection
58 | server = Server(current_app.config['LDAP_HOST'], get_info=ALL) # define an unsecure LDAP server, requesting info on DSE and schema
59 | g.ldap_connection = Connection(server, current_app.config['LDAP_BIND_DN'], current_app.config['LDAP_BIND_PASSWORD'], auto_bind=True)
60 | return g.ldap_connection
61 |
62 |
63 | def get_dn_from_user(user):
64 | return "%s=%s,%s" % (current_app.config['LDAP_USERNAME_ATTRIBUTE'], user, current_app.config['LDAP_USER_BASE'] )
65 |
66 |
67 | def get_dn_from_group(group):
68 | return '%s=%s,%s' % (current_app.config['LDAP_GROUP_ATTRIBUTE'], group, current_app.config['LDAP_GROUP_BASE'])
69 |
70 |
71 | class TedivmLoginForm(LoginForm):
72 |
73 | def validate_user(self):
74 | user_manager = current_app.user_manager
75 | if current_app.config.get('USER_LDAP', False):
76 | if not authenticate(self.username.data, self.password.data):
77 | return False
78 | user = user_manager.db_manager.find_user_by_username(self.username.data)
79 | if not user:
80 | email = get_user_email(self.username.data)
81 | if not email:
82 | email = None
83 |
84 | user = user_models.User(username=self.username.data,
85 | email=email,
86 | #first_name=form.first_name.data,
87 | #last_name=form.last_name.data,
88 | #password=current_app.user_manager.hash_password(form.password.data),
89 | active=True,
90 | email_confirmed_at=datetime.datetime.utcnow())
91 | db.session.add(user)
92 | db.session.commit()
93 | return True
94 |
95 |
96 |
97 | # Find user by username and/or email
98 | user = None
99 | user_email = None
100 | if user_manager.USER_ENABLE_USERNAME:
101 | # Find user by username
102 | user = user_manager.db_manager.find_user_by_username(self.username.data)
103 |
104 | # Find user by email address (username field)
105 | if not user and user_manager.USER_ENABLE_EMAIL:
106 | user, user_email = user_manager.db_manager.get_user_and_user_email_by_email(self.username.data)
107 |
108 | else:
109 | # Find user by email address (email field)
110 | user, user_email = user_manager.db_manager.get_user_and_user_email_by_email(self.email.data)
111 |
112 | # Handle successful authentication
113 | if user and user_manager.verify_password(self.password.data, user.password):
114 | return True # Successful authentication
115 |
116 |
117 |
118 | def validate(self):
119 | # Remove fields depending on configuration
120 | user_manager = current_app.user_manager
121 | if user_manager.USER_ENABLE_USERNAME:
122 | delattr(self, 'email')
123 | else:
124 | delattr(self, 'username')
125 |
126 | # Validate field-validators
127 | if not super(LoginForm, self).validate():
128 | return False
129 |
130 | if self.validate_user():
131 | return True
132 |
133 | # Handle unsuccessful authentication
134 | # Email, Username or Email/Username depending on settings
135 | if user_manager.USER_ENABLE_USERNAME and user_manager.USER_ENABLE_EMAIL:
136 | username_or_email_field = self.username
137 | username_or_email_text = (_('Username/Email'))
138 | elif user_manager.USER_ENABLE_USERNAME:
139 | username_or_email_field = self.username
140 | username_or_email_text = (_('Username'))
141 | else:
142 | username_or_email_field = self.email
143 | username_or_email_text = (_('Email'))
144 |
145 | # Always show 'incorrect username/email or password' error message for additional security
146 | message = _('Incorrect %(username_or_email)s and/or Password', username_or_email=username_or_email_text)
147 | username_or_email_field.errors.append(message)
148 | self.password.errors.append(message)
149 |
150 | return False # Unsuccessful authentication
151 |
152 |
153 |
154 | # Customize Flask-User
155 | class TedivmUserManager(UserManager):
156 | def customize(self, app):
157 | self.LoginFormClass = TedivmLoginForm
158 |
--------------------------------------------------------------------------------
/app/models/user_models.py:
--------------------------------------------------------------------------------
1 | # Copyright 2014 SolidBuilds.com. All rights reserved
2 | #
3 | # Authors: Ling Thio , Matt Hogan
4 |
5 | from flask import current_app
6 | from flask_user import UserMixin
7 | from flask_user.forms import RegisterForm
8 | from flask_wtf import FlaskForm
9 | from wtforms import StringField, SubmitField, validators, PasswordField, BooleanField
10 | from app import db
11 | from app.utils.forms import MultiCheckboxField
12 | from app.extensions import ldap
13 |
14 |
15 | class TedivmUserMixin(UserMixin):
16 |
17 | def has_roles(self, *requirements):
18 | """ Return True if the user has all of the specified roles. Return False otherwise.
19 |
20 | has_roles() accepts a list of requirements:
21 | has_role(requirement1, requirement2, requirement3).
22 |
23 | Each requirement is either a role_name, or a tuple_of_role_names.
24 | role_name example: 'manager'
25 | tuple_of_role_names: ('funny', 'witty', 'hilarious')
26 | A role_name-requirement is accepted when the user has this role.
27 | A tuple_of_role_names-requirement is accepted when the user has ONE of these roles.
28 | has_roles() returns true if ALL of the requirements have been accepted.
29 |
30 | For example:
31 | has_roles('a', ('b', 'c'), d)
32 | Translates to:
33 | User has role 'a' AND (role 'b' OR role 'c') AND role 'd'"""
34 |
35 | # Translates a list of role objects to a list of role_names
36 | user_manager = current_app.user_manager
37 |
38 | # has_role() accepts a list of requirements
39 | for requirement in requirements:
40 | if isinstance(requirement, (list, tuple)):
41 | # this is a tuple_of_role_names requirement
42 | tuple_of_role_names = requirement
43 | authorized = False
44 | for role_name in tuple_of_role_names:
45 | if self.has_role(role_name):
46 | # tuple_of_role_names requirement was met: break out of loop
47 | authorized = True
48 | break
49 | if not authorized:
50 | return False # tuple_of_role_names requirement failed: return False
51 | else:
52 | # this is a role_name requirement
53 | role_name = requirement
54 | # the user must have this role
55 | if self.has_role(role_name):
56 | return False # role_name requirement failed: return False
57 |
58 | # All requirements have been met: return True
59 | return True
60 |
61 |
62 |
63 |
64 | # Define the User data model. Make sure to add the flask_user.UserMixin !!
65 | class User(db.Model, TedivmUserMixin):
66 | __tablename__ = 'users'
67 | id = db.Column(db.Integer, primary_key=True)
68 |
69 | # User authentication information (required for Flask-User)
70 | username = db.Column(db.String(50), nullable=True, unique=True)
71 | email = db.Column(db.Unicode(255), nullable=True, unique=True)
72 | email_confirmed_at = db.Column(db.DateTime())
73 | password = db.Column(db.String(255), nullable=False, server_default='')
74 |
75 | # User information
76 | active = db.Column('is_active', db.Boolean(), nullable=False, server_default='0')
77 | first_name = db.Column(db.Unicode(50), nullable=False, server_default=u'')
78 | last_name = db.Column(db.Unicode(50), nullable=False, server_default=u'')
79 |
80 | # Relationships
81 | roles = db.relationship('Role', secondary='users_roles', backref=db.backref('users', lazy='dynamic'))
82 |
83 | # API Keys
84 | apikeys = db.relationship('ApiKey', backref='user')
85 |
86 | def has_role(self, role, allow_admin=True):
87 |
88 | if current_app.config.get('USER_LDAP', False):
89 | group = current_app.config.get('LDAP_GROUP_TO_ROLE_%s' % role.upper(), False)
90 | if not group:
91 | return False
92 | return ldap.user_in_group(self.username, group)
93 |
94 | for item in self.roles:
95 | if item.name == role:
96 | return True
97 | if allow_admin and item.name == 'admin':
98 | return True
99 | return False
100 |
101 | def role(self):
102 | for item in self.roles:
103 | return item.name
104 |
105 | def name(self):
106 | return self.first_name + " " + self.last_name
107 |
108 |
109 |
110 | class ApiKey(db.Model):
111 | __tablename__ = 'api_keys'
112 | id = db.Column(db.Unicode(255), primary_key=True, unique=True)
113 | hash = db.Column(db.Unicode(255), nullable=False)
114 | label = db.Column(db.Unicode(255), nullable=True)
115 | user_id = db.Column(db.Integer(), db.ForeignKey('users.id', ondelete='CASCADE'))
116 |
117 |
118 | # Define the Role data model
119 | class Role(db.Model):
120 | __tablename__ = 'roles'
121 | id = db.Column(db.Integer(), primary_key=True)
122 | name = db.Column(db.String(50), nullable=False, server_default=u'', unique=True) # for @roles_accepted()
123 | label = db.Column(db.Unicode(255), server_default=u'') # for display purposes
124 |
125 |
126 | # Define the UserRoles association model
127 | class UsersRoles(db.Model):
128 | __tablename__ = 'users_roles'
129 | id = db.Column(db.Integer(), primary_key=True)
130 | user_id = db.Column(db.Integer(), db.ForeignKey('users.id', ondelete='CASCADE'))
131 | role_id = db.Column(db.Integer(), db.ForeignKey('roles.id', ondelete='CASCADE'))
132 |
133 |
134 | # Define the User registration form
135 | # It augments the Flask-User RegisterForm with additional fields
136 | class MyRegisterForm(RegisterForm):
137 | first_name = StringField('First name', validators=[ validators.DataRequired('First name is required')])
138 | last_name = StringField('Last name', validators=[ validators.DataRequired('Last name is required')])
139 |
140 |
141 | # Define the User profile form
142 | class UserProfileForm(FlaskForm):
143 | first_name = StringField('First name', validators=[])
144 | last_name = StringField('Last name', validators=[])
145 | email = StringField('Email', validators=[validators.DataRequired('Last name is required')])
146 | password = PasswordField('Password', validators=[])
147 | roles = MultiCheckboxField('Roles', coerce=int)
148 | active = BooleanField('Active')
149 | submit = SubmitField('Save')
150 |
151 |
152 |
153 | # Define the User profile form
154 | class ApiKeyForm(FlaskForm):
155 | label = StringField('Key Label', validators=[validators.DataRequired('Key Label is required')])
156 | submit = SubmitField('Save')
157 |
--------------------------------------------------------------------------------
/app/static/coreui/js/src/sidebar.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery'
2 | import PerfectScrollbar from 'perfect-scrollbar'
3 | import ToggleClasses from './toggle-classes'
4 |
5 | /**
6 | * --------------------------------------------------------------------------
7 | * CoreUI (v2.0.0-beta.2): sidebar.js
8 | * Licensed under MIT (https://coreui.io/license)
9 | * --------------------------------------------------------------------------
10 | */
11 |
12 | const Sidebar = (($) => {
13 | /**
14 | * ------------------------------------------------------------------------
15 | * Constants
16 | * ------------------------------------------------------------------------
17 | */
18 |
19 | const NAME = 'sidebar'
20 | const VERSION = '2.0.0-beta.2'
21 | const DATA_KEY = 'coreui.sidebar'
22 | const EVENT_KEY = `.${DATA_KEY}`
23 | const DATA_API_KEY = '.data-api'
24 | const JQUERY_NO_CONFLICT = $.fn[NAME]
25 |
26 | const ClassName = {
27 | ACTIVE : 'active',
28 | BRAND_MINIMIZED : 'brand-minimized',
29 | NAV_DROPDOWN_TOGGLE : 'nav-dropdown-toggle',
30 | OPEN : 'open',
31 | SIDEBAR_FIXED : 'sidebar-fixed',
32 | SIDEBAR_MINIMIZED : 'sidebar-minimized',
33 | SIDEBAR_OFF_CANVAS : 'sidebar-off-canvas'
34 | }
35 |
36 | const Event = {
37 | CLICK : 'click',
38 | DESTROY : 'destroy',
39 | INIT : 'init',
40 | LOAD_DATA_API : `load${EVENT_KEY}${DATA_API_KEY}`,
41 | TOGGLE : 'toggle'
42 | }
43 |
44 | const Selector = {
45 | BODY : 'body',
46 | BRAND_MINIMIZER : '.brand-minimizer',
47 | NAV_DROPDOWN_TOGGLE : '.nav-dropdown-toggle',
48 | NAV_DROPDOWN_ITEMS : '.nav-dropdown-items',
49 | NAV_LINK : '.nav-link',
50 | NAVIGATION_CONTAINER : '.sidebar-nav',
51 | NAVIGATION : '.sidebar-nav > .nav',
52 | SIDEBAR : '.sidebar',
53 | SIDEBAR_MINIMIZER : '.sidebar-minimizer',
54 | SIDEBAR_TOGGLER : '.sidebar-toggler'
55 | }
56 |
57 | const ShowClassNames = [
58 | 'sidebar-show',
59 | 'sidebar-sm-show',
60 | 'sidebar-md-show',
61 | 'sidebar-lg-show',
62 | 'sidebar-xl-show'
63 | ]
64 |
65 | /**
66 | * ------------------------------------------------------------------------
67 | * Class Definition
68 | * ------------------------------------------------------------------------
69 | */
70 |
71 | class Sidebar {
72 | constructor(element) {
73 | this._element = element
74 | this.perfectScrollbar(Selector.INIT)
75 | this.setActiveLink()
76 | this._addEventListeners()
77 | }
78 |
79 | // Getters
80 |
81 | static get VERSION() {
82 | return VERSION
83 | }
84 |
85 | // Public
86 |
87 | perfectScrollbar(event) {
88 | if (typeof PerfectScrollbar !== 'undefined') {
89 | let ps
90 |
91 | if (event === Event.INIT && !document.body.classList.contains(ClassName.SIDEBAR_MINIMIZED)) {
92 | ps = new PerfectScrollbar(document.querySelector(Selector.NAVIGATION_CONTAINER), {
93 | suppressScrollX: true
94 | })
95 | }
96 |
97 | if (event === Event.DESTROY) {
98 | ps = new PerfectScrollbar(document.querySelector(Selector.NAVIGATION_CONTAINER), {
99 | suppressScrollX: true
100 | })
101 | ps.destroy()
102 | ps = null
103 | }
104 |
105 | if (event === Event.TOGGLE) {
106 | if (document.body.classList.contains(ClassName.SIDEBAR_MINIMIZED)) {
107 | ps = new PerfectScrollbar(document.querySelector(Selector.NAVIGATION_CONTAINER), {
108 | suppressScrollX: true
109 | })
110 | ps.destroy()
111 | ps = null
112 | } else {
113 | ps = new PerfectScrollbar(document.querySelector(Selector.NAVIGATION_CONTAINER), {
114 | suppressScrollX: true
115 | })
116 | }
117 | }
118 | }
119 | }
120 |
121 | setActiveLink() {
122 | $(Selector.NAVIGATION).find(Selector.NAV_LINK).each((key, value) => {
123 | let link = value
124 | let cUrl = String(window.location).split('?')[0]
125 |
126 | if (cUrl.substr(cUrl.length - 1) === '#') {
127 | cUrl = cUrl.slice(0, -1)
128 | }
129 |
130 | if ($($(link))[0].href === cUrl) {
131 | $(link).addClass(ClassName.ACTIVE).parents(Selector.NAV_DROPDOWN_ITEMS).add(link).each((key, value) => {
132 | link = value
133 | $(link).parent().addClass(ClassName.OPEN)
134 | })
135 | }
136 | })
137 | }
138 |
139 | // Private
140 |
141 | _addEventListeners() {
142 | $(Selector.BRAND_MINIMIZER).on(Event.CLICK, (event) => {
143 | event.preventDefault()
144 | event.stopPropagation()
145 | $(Selector.BODY).toggleClass(ClassName.BRAND_MINIMIZED)
146 | })
147 |
148 | $(Selector.NAV_DROPDOWN_TOGGLE).on(Event.CLICK, (event) => {
149 | event.preventDefault()
150 | event.stopPropagation()
151 | const dropdown = event.target
152 | $(dropdown).parent().toggleClass(ClassName.OPEN)
153 | })
154 |
155 | $(Selector.SIDEBAR_MINIMIZER).on(Event.CLICK, (event) => {
156 | event.preventDefault()
157 | event.stopPropagation()
158 | $(Selector.BODY).toggleClass(ClassName.SIDEBAR_MINIMIZED)
159 | this.perfectScrollbar(Event.TOGGLE)
160 | })
161 |
162 | $(Selector.SIDEBAR_TOGGLER).on(Event.CLICK, (event) => {
163 | event.preventDefault()
164 | event.stopPropagation()
165 | const toggle = event.currentTarget.dataset.toggle
166 | ToggleClasses(toggle, ShowClassNames)
167 | })
168 | }
169 |
170 | // Static
171 |
172 | static _jQueryInterface() {
173 | return this.each(function () {
174 | const $element = $(this)
175 | let data = $element.data(DATA_KEY)
176 |
177 | if (!data) {
178 | data = new Sidebar(this)
179 | $element.data(DATA_KEY, data)
180 | }
181 | })
182 | }
183 | }
184 |
185 | /**
186 | * ------------------------------------------------------------------------
187 | * Data Api implementation
188 | * ------------------------------------------------------------------------
189 | */
190 |
191 | $(window).on(Event.LOAD_DATA_API, () => {
192 | const sidebar = $(Selector.SIDEBAR)
193 | Sidebar._jQueryInterface.call(sidebar)
194 | })
195 |
196 | /**
197 | * ------------------------------------------------------------------------
198 | * jQuery
199 | * ------------------------------------------------------------------------
200 | */
201 |
202 | $.fn[NAME] = Sidebar._jQueryInterface
203 | $.fn[NAME].Constructor = Sidebar
204 | $.fn[NAME].noConflict = () => {
205 | $.fn[NAME] = JQUERY_NO_CONFLICT
206 | return Sidebar._jQueryInterface
207 | }
208 |
209 | return Sidebar
210 | })($)
211 |
212 | export default Sidebar
213 |
--------------------------------------------------------------------------------
/app/static/coreui/README.md:
--------------------------------------------------------------------------------
1 | # CoreUI - Free WebApp UI Kit built on top of Bootstrap 4 [](https://twitter.com/intent/tweet?text=CoreUI%20-%20Free%20Bootstrap%204%20Admin%20Template%20&url=https://coreui.io&hashtags=bootstrap,admin,template,dashboard,panel,free,angular,react,vue)
2 |
3 | Please help us on [Product Hunt](https://www.producthunt.com/posts/coreui-open-source-bootstrap-4-admin-template-with-angular-2-react-js-vue-js-support) and [Designer News](https://www.designernews.co/stories/81127). Thanks in advance!
4 |
5 | Curious why I decided to create CoreUI? Please read this article: [Jack of all trades, master of none. Why Bootstrap Admin Templates suck.](https://medium.com/@lukaszholeczek/jack-of-all-trades-master-of-none-5ea53ef8a1f#.7eqx1bcd8)
6 |
7 | CoreUI is an Open Source UI Kit built on top of Bootstrap 4. CoreUI is the fastest way to build modern dashboard for any platforms, browser or device. A complete Dashboard and WebApp UI Kit that allows you to quickly build eye-catching, high-quality, high-performance responsive applications using your framework of choice.
8 |
9 | ## Table of Contents
10 |
11 | * [Templates](#templates)
12 | * [Admin Templates built on top of CoreUI Pro](#admin-templates-built-on-top-of-coreui-pro)
13 | * [Installation](#installation)
14 | * [Usage](#usage)
15 | * [What's included](#whats-included)
16 | * [Documentation](#documentation)
17 | * [Contributing](#contributing)
18 | * [Versioning](#versioning)
19 | * [Creators](#creators)
20 | * [Community](#community)
21 | * [License](#license)
22 | * [Support CoreUI Development](#support-coreui-development)
23 |
24 | ## Templates
25 |
26 | * [CoreUI Free Bootstrap Admin Template](https://github.com/coreui/coreui-free-bootstrap-admin-template)
27 | * 💪 [CoreUI Pro Bootstrap Admin Template](https://coreui.io/pro/)
28 |
29 | ## Admin Templates built on top of CoreUI Pro
30 |
31 | | CoreUI Pro | Prime | Root | Alba | Leaf |
32 | | --- | --- | --- | --- | --- |
33 | | [](https://coreui.io/pro/) | [](https://genesisui.com/admin-templates/bootstrap/prime/?support=1) | [](https://genesisui.com/admin-templates/bootstrap/root/?support=1) | [](https://genesisui.com/admin-templates/bootstrap/alba/?support=1) | [](https://genesisui.com/admin-templates/bootstrap/leaf/?support=1)
34 |
35 |
36 | ## Installation
37 |
38 | Several options are available:
39 |
40 | ### Clone repo
41 |
42 | ``` bash
43 | $ git clone https://github.com/coreui/coreui.git
44 | ```
45 |
46 | ### NPM
47 |
48 | ``` bash
49 | $ npm install @coreui/coreui --save
50 | ```
51 |
52 | ### Yarn
53 |
54 | ``` bash
55 | $ yarn add @coreui/coreui@2.0.0
56 | ```
57 |
58 | ### Composer
59 |
60 | ``` bash
61 | $ composer require coreui/coreui:2.0.0
62 | ```
63 |
64 | ## Usage
65 |
66 | ### CSS
67 |
68 | Copy-paste the stylesheet `` into your `` before all other stylesheets to load our CSS.
69 |
70 | ``` html
71 |
72 | ```
73 |
74 | ### JS
75 |
76 | Many of our components require the use of JavaScript to function. Specifically, they require [jQuery](https://jquery.com), [Popper.js](https://popper.js.org/), [Bootstrap](https://getbootstrap.com) and our own JavaScript plugins. Place the following `
80 |
81 |
82 |
83 | ```
84 |
85 | ## What's included
86 |
87 | Within the download you'll find the following directories and files, logically grouping common assets and providing both compiled and minified variations. You'll see something like this:
88 |
89 | ```
90 | coreui/
91 | ├── build/
92 | ├── dist/
93 | ├── js/
94 | └── scss/
95 | ```
96 |
97 | ## Documentation
98 |
99 | The documentation for the CoreUI Free Bootstrap Admin Template is hosted at our website [CoreUI](https://coreui.io/)
100 |
101 | ## Contributing
102 |
103 | Please read through our [contributing guidelines](https://github.com/coreui/coreui/blob/master/CONTRIBUTING.md). Included are directions for opening issues, coding standards, and notes on development.
104 |
105 | Editor preferences are available in the [editor config](https://github.com/coreui/coreui/blob/master/.editorconfig) for easy use in common text editors. Read more and download plugins at .
106 |
107 | ## Versioning
108 |
109 | For transparency into our release cycle and in striving to maintain backward compatibility,CoreUI Free Admin Template is maintained under [the Semantic Versioning guidelines](http://semver.org/).
110 |
111 | See [the Releases section of our project](https://github.com/coreui/coreui/releases) for changelogs for each release version.
112 |
113 | ## Creators
114 |
115 | **Łukasz Holeczek**
116 |
117 | *
118 | *
119 |
120 | **Andrzej Kopański**
121 |
122 | *
123 |
124 | ## Community
125 |
126 | Get updates on CoreUI's development and chat with the project maintainers and community members.
127 |
128 | - Follow [@core_ui on Twitter](https://twitter.com/core_ui).
129 | - Read and subscribe to [CoreUI Blog](https://coreui.io/blog/).
130 |
131 | ## Copyright and license
132 |
133 | copyright 2018 creativeLabs Łukasz Holeczek. Code released under [the MIT license](https://github.com/coreui/coreui/blob/master/LICENSE).
134 | There is only one limitation you can't can’t re-distribute the CoreUI as stock. You can’t do this if you modify the CoreUI. In past we faced some problems with persons who tried to sell CoreUI based templates.
135 |
136 | ## Support CoreUI Development
137 |
138 | CoreUI is an MIT licensed open source project and completely free to use. However, the amount of effort needed to maintain and develop new features for the project is not sustainable without proper financial backing. You can support development by donating on [PayPal](https://www.paypal.me/holeczek), buying [CoreUI Pro Version](https://coreui.io/pro) or buying one of our [premium admin templates](https://genesisui.com/?support=1).
139 |
140 | As of now I am exploring the possibility of working on CoreUI fulltime - if you are a business that is building core products using CoreUI, I am also open to conversations regarding custom sponsorship / consulting arrangements. Get in touch on [Twitter](https://twitter.com/lukaszholeczek).
141 |
--------------------------------------------------------------------------------
/app/static/coreui/dist/js/coreui.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * CoreUI v2.0.0-beta.2 (https://coreui.io)
3 | * Copyright 2018 Łukasz Holeczek
4 | * Licensed under MIT (https://coreui.io)
5 | */
6 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("jquery"),require("perfect-scrollbar")):"function"==typeof define&&define.amd?define(["exports","jquery","perfect-scrollbar"],t):t(e.coreui={},e.jQuery,e.PerfectScrollbar)}(this,function(e,t,r){"use strict";function a(e,t){for(var n=0;n .nav",SIDEBAR:".sidebar",SIDEBAR_MINIMIZER:".sidebar-minimizer",SIDEBAR_TOGGLER:".sidebar-toggler"},V=["sidebar-show","sidebar-sm-show","sidebar-md-show","sidebar-lg-show","sidebar-xl-show"],k=function(){function n(e){this._element=e,this.perfectScrollbar(G.INIT),this.setActiveLink(),this._addEventListeners()}var e=n.prototype;return e.perfectScrollbar=function(e){"undefined"!=typeof r&&(e!==j.INIT||document.body.classList.contains(P)||new r(document.querySelector(G.NAVIGATION_CONTAINER),{suppressScrollX:!0}),e===j.DESTROY&&new r(document.querySelector(G.NAVIGATION_CONTAINER),{suppressScrollX:!0}).destroy(),e===j.TOGGLE&&(document.body.classList.contains(P)?new r(document.querySelector(G.NAVIGATION_CONTAINER),{suppressScrollX:!0}).destroy():new r(document.querySelector(G.NAVIGATION_CONTAINER),{suppressScrollX:!0})))},e.setActiveLink=function(){T(G.NAVIGATION).find(G.NAV_LINK).each(function(e,t){var n=t,r=String(window.location).split("?")[0];"#"===r.substr(r.length-1)&&(r=r.slice(0,-1)),T(T(n))[0].href===r&&T(n).addClass(L).parents(G.NAV_DROPDOWN_ITEMS).add(n).each(function(e,t){T(n=t).parent().addClass(R)})})},e._addEventListeners=function(){var t=this;T(G.BRAND_MINIMIZER).on(j.CLICK,function(e){e.preventDefault(),e.stopPropagation(),T(G.BODY).toggleClass(S)}),T(G.NAV_DROPDOWN_TOGGLE).on(j.CLICK,function(e){e.preventDefault(),e.stopPropagation();var t=e.target;T(t).parent().toggleClass(R)}),T(G.SIDEBAR_MINIMIZER).on(j.CLICK,function(e){e.preventDefault(),e.stopPropagation(),T(G.BODY).toggleClass(P),t.perfectScrollbar(j.TOGGLE)}),T(G.SIDEBAR_TOGGLER).on(j.CLICK,function(e){e.preventDefault(),e.stopPropagation();var t=e.currentTarget.dataset.toggle;x(t,V)})},n._jQueryInterface=function(){return this.each(function(){var e=T(this),t=e.data(D);t||(t=new n(this),e.data(D,t))})},o(n,null,[{key:"VERSION",get:function(){return"2.0.0-beta.2"}}]),n}(),T(window).on(j.LOAD_DATA_API,function(){var e=T(G.SIDEBAR);k._jQueryInterface.call(e)}),T.fn[C]=k._jQueryInterface,T.fn[C].Constructor=k,T.fn[C].noConflict=function(){return T.fn[C]=E,k._jQueryInterface},k);!function(e){if("undefined"==typeof e)throw new TypeError("CoreUI's JavaScript requires jQuery. jQuery must be included before CoreUI's JavaScript.");var t=e.fn.jquery.split(" ")[0].split(".");if(t[0]<2&&t[1]<9||1===t[0]&&9===t[1]&&t[2]<1||4<=t[0])throw new Error("CoreUI's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0")}(t),window.GetStyle=function(e,t){return void 0===t&&(t=document.body),window.getComputedStyle(t,null).getPropertyValue(e).replace(/^\s/,"")},window.HexToRgb=function(e){var t=e.replace("#","");return"rgba("+parseInt(t.substring(0,2),16)+", "+parseInt(t.substring(2,4),16)+", "+parseInt(t.substring(4,6),16)},window.HexToRgba=function(e,t){void 0===t&&(t=100);var n=e.replace("#","");return"rgba("+parseInt(n.substring(0,2),16)+", "+parseInt(n.substring(2,4),16)+", "+parseInt(n.substring(4,6),16)+", "+t/100},window.RgbToHex=function(e){var t=e.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i),n="0"+parseInt(t[1],10).toString(16),r="0"+parseInt(t[2],10).toString(16),a="0"+parseInt(t[3],10).toString(16);return t&&4===t.length?"#"+n.slice(-2)+r.slice(-2)+a.slice(-2):""},e.AjaxLoad=Q,e.AsideMenu=B,e.Sidebar=M,Object.defineProperty(e,"__esModule",{value:!0})});
7 | //# sourceMappingURL=coreui.min.js.map
--------------------------------------------------------------------------------
/app/templates/dark_layout.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {% block title_tag %}
7 | {{ config['APP_NAME'] }}{% block title %}{% endblock %}
8 | {% endblock %}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | {% block body %}
34 |
131 |
141 |
142 |
143 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # tedivm's flask starter app
2 |
3 | 
4 |
5 | This code base serves as starting point for writing your next Flask application.
6 |
7 | This package is designed to allow developers to start working on their specific features immediately while also making it simple to deploy the project into production. It contains a number of configured extensions and libraries as well as unique features specifically built for this package. It also is completely dockerized, with both a docker-compose testenv and the ability to easily make images off of the application for pushing into production.
8 |
9 | ## Rob's Awesome Template
10 |
11 | If you like this project you may also like my latest template, [Rob's Awesome Python Template](https://github.com/tedivm/robs_awesome_python_template). It is updated with modern best practices and a variety of optional services.
12 |
13 | ## Code characteristics
14 |
15 | * Tested on Python 3.3, 3.4, 3.5, 3.6, and 3.7
16 | * Complete docker environment.
17 | * Images for both the web application and the celery worker.
18 | * Full user management system.
19 | * Server side session storage.
20 | * An API system with API tokens and route decorators.
21 | * Well organized directories with lots of comments.
22 | * Includes test framework (`py.test` and `tox`)
23 | * Includes database migration framework (`alembic`, using `Flask-Migrate`)
24 | * Sends error emails to admins for unhandled exceptions
25 |
26 | ## Configured Extensions and Libraries
27 |
28 | With thanks to the following Flask extensions and libraries:
29 |
30 | * [Beaker](https://beaker.readthedocs.io/en/latest/) for caching and session management.
31 | * [Celery](http://www.celeryproject.org/) for running asynchronous tasks on worker nodes.
32 | * [Click](https://click.palletsprojects.com/) for the creation of command line tools.
33 | * [Flask](http://flask.pocoo.org/) the microframework framework which holds this all together.
34 | * [Flask-Login](https://flask-login.readthedocs.io/) allows users to login and signout.
35 | * [Flask-Migrate](https://flask-migrate.readthedocs.io/) integrates [Alembic](http://alembic.zzzcomputing.com/) into Flask to handle database versioning.
36 | * [Flask-SQLAlchemy](http://flask-sqlalchemy.pocoo.org) integrates [SQLAlchemy](https://www.sqlalchemy.org/) into Flask for database modeling and access.
37 | * [Flask-User](http://flask-user.readthedocs.io/en/v0.6/) adds user management and authorization features.
38 | * [Flask-WTF](https://flask-wtf.readthedocs.io/en/stable/) integrates [WTForms](https://wtforms.readthedocs.io) into Flask to handle form creation and validation.
39 |
40 | In addition the front end uses the open source versions of:
41 |
42 | * [Bootstrap](https://getbootstrap.com/)
43 | * [CoreUI](https://coreui.io/)
44 | * [Font Awesome](https://fontawesome.com/)
45 |
46 |
47 | ## Unique Features
48 |
49 | * Database or LDAP Authentication - Applications built with this project can use the standard database backed users or can switch to LDAP authentication with a few configuration settings.
50 |
51 | * API Authentication and Authorization - this project can allow people with the appropriate role to generate API Keys, which in turn can be used with the `roles_accepted_api` decorator to grant API access to specific routes.
52 |
53 | * Versatile Configuration System - this project can be configured with a combination of configuration files, AWS Secrets Manager configuration, and environmental variables. This allows base settings to be built into the deployment, secrets to be managed securely, and any configuration value to be overridden by environmental variables.
54 |
55 | * A `makefile` with a variety of options to make common tasks easier to accomplish.
56 |
57 | * A [Celery](http://www.celeryproject.org/) based asynchronous task management system. This is extremely useful for long running tasks- they can be triggered in the web interface and then run on a worker node and take as long as they need to complete.
58 |
59 |
60 | ## Setting up a development environment
61 |
62 | First we recommend either cloning this repository with the "Use this template" button on Github.
63 |
64 |
65 | We assume that you have `make` and `docker`.
66 |
67 | # Clone the code repository into ~/dev/my_app
68 | mkdir -p ~/dev
69 | cd ~/dev
70 | git clone https://github.com/tedivm/tedivms-flask my_app
71 | cd my_app
72 |
73 | # For the first run, and only the first run, we need to create the first round of SQLAlchemy models.
74 | make init_db
75 |
76 | # Create the 'my_app' virtual environment and start docker containers
77 | make testenv
78 |
79 | # Restart docker app container
80 | docker-compose restart app
81 |
82 | # Start a shell in the container running the application
83 | docker-compose exec app /bin/bash
84 |
85 |
86 | ## Configuration
87 |
88 | ### Application configuration
89 |
90 | To set default configuration values on the application level- such as the application name and author- edit `./app/settings.py`. This should be done as a first step whenever using this application template.
91 |
92 | ### Configuration File
93 |
94 | A configuration file can be set with the environmental variable `APPLICATION_SETTINGS`.
95 |
96 | ### AWS Secrets Manager
97 |
98 | Configuration can be loaded from the AWS Secrets Manager by setting the environmental variables `AWS_SECRETS_MANAGER_CONFIG` and `AWS_SECRETS_REGION`.
99 |
100 | ### Environmental Variables
101 |
102 | Any environmental variables that have the same name as a configuration value in this application will automatically get loaded into the app's configuration.
103 |
104 | ### Configuring LDAP
105 |
106 | Any installation can run with LDAP as its backend with these settings.
107 |
108 | ```
109 | USER_LDAP=true
110 | LDAP_HOST=ldap://ldap
111 | LDAP_BIND_DN=cn=admin,dc=example,dc=org
112 | LDAP_BIND_PASSWORD=admin
113 | LDAP_USERNAME_ATTRIBUTE=cn
114 | LDAP_USER_BASE=ou=users,dc=example,dc=org
115 | LDAP_GROUP_OBJECT_CLASS=posixGroup
116 | LDAP_GROUP_ATTRIBUTE=cn
117 | LDAP_GROUP_BASE=ou=groups,dc=example,dc=org
118 | LDAP_GROUP_TO_ROLE_ADMIN=admin
119 | LDAP_GROUP_TO_ROLE_DEV=dev
120 | LDAP_GROUP_TO_ROLE_USER=user
121 | LDAP_EMAIL_ATTRIBUTE=mail
122 | ```
123 |
124 |
125 | ## Initializing the Database
126 |
127 | # Initialize the database. This will create the `migrations` folder and is only needed once per project.
128 | make init_db
129 |
130 | # This creates a new migration. It should be run whenever you change your database models.
131 | make upgrade_models
132 |
133 |
134 | ## Running the app
135 |
136 | # Start the Flask development web server
137 | make testenv
138 |
139 |
140 | Point your web browser to
141 |
142 | You can make use of the following users:
143 | * email `user@example.com` with password `Password1`.
144 | * email `dev@example.com` with password `Password1`.
145 | * email `admin@example.com` with password `Password1`.
146 |
147 |
148 | ## Running the automated tests
149 |
150 | # To run the test suite.
151 | make run_tests
152 |
153 |
154 |
155 | ## Acknowledgements
156 |
157 |
158 | [Flask-Dash](https://github.com/twintechlabs/flaskdash) was used as a starting point for this code repository. That project was based off of the [Flask-User-starter-app](https://github.com/lingthio/Flask-User-starter-app).
159 |
160 | ## Authors
161 |
162 | - Robert Hafner (tedivms-flask) -- tedivm@tedivm.com
163 | * Matt Hogan (flaskdash) -- matt AT twintechlabs DOT io
164 | * Ling Thio (flask-user) -- ling.thio AT gmail DOT com
165 |
--------------------------------------------------------------------------------
/app/static/coreui/scss/_switches.scss:
--------------------------------------------------------------------------------
1 | @mixin switch-size($width, $height, $font-size, $handle-margin) {
2 | width: $width;
3 | height: $height;
4 |
5 | .switch-label {
6 | font-size: $font-size;
7 | }
8 |
9 | .switch-handle {
10 | width: $height - $handle-margin * 2;
11 | height: $height - $handle-margin * 2;
12 | }
13 |
14 | .switch-input:checked ~ .switch-handle {
15 | left: $width - $height + $handle-margin;
16 | }
17 | }
18 |
19 | @mixin switch($type, $width, $height, $font-size, $handle-margin) {
20 | position: relative;
21 | display: inline-block;
22 | width: $width;
23 | height: $height;
24 | vertical-align: top;
25 | cursor: pointer;
26 | background-color: transparent;
27 |
28 | .switch-input {
29 | position: absolute;
30 | top: 0;
31 | left: 0;
32 | opacity: 0;
33 | }
34 |
35 | .switch-label {
36 | position: relative;
37 | display: block;
38 | height: inherit;
39 | @if $type == icon {
40 | font-family: FontAwesome;
41 | }
42 | font-size: $font-size;
43 | font-weight: 600;
44 | text-transform: uppercase;
45 | @if $type == ddd {
46 | background-color: $gray-100;
47 | } @else {
48 | background-color: #fff;
49 | }
50 | border: 1px solid $border-color;
51 | border-radius: 2px;
52 | transition: opacity background .15s ease-out;
53 | }
54 | @if $type == text or $type == icon {
55 | .switch-label::before,
56 | .switch-label::after {
57 | position: absolute;
58 | top: 50%;
59 | width: 50%;
60 | margin-top: -.5em;
61 | line-height: 1;
62 | text-align: center;
63 | transition: inherit;
64 | }
65 | .switch-label::before {
66 | right: 1px;
67 | color: $gray-200;
68 | content: attr(data-off);
69 | }
70 | .switch-label::after {
71 | left: 1px;
72 | color: #fff;
73 | content: attr(data-on);
74 | opacity: 0;
75 | }
76 | }
77 | .switch-input:checked ~ .switch-label {
78 | //background: $gray-lightest;
79 | }
80 | .switch-input:checked ~ .switch-label::before {
81 | opacity: 0;
82 | }
83 | .switch-input:checked ~ .switch-label::after {
84 | opacity: 1;
85 | }
86 |
87 | .switch-handle {
88 | position: absolute;
89 | top: $handle-margin;
90 | left: $handle-margin;
91 | width: $height - $handle-margin * 2;
92 | height: $height - $handle-margin * 2;
93 | background: #fff;
94 | border: 1px solid $border-color;
95 | border-radius: 1px;
96 | transition: left .15s ease-out;
97 | @if $type == ddd {
98 | border: 0;
99 | box-shadow: 0 2px 5px rgba(0, 0, 0, .3);
100 | }
101 | }
102 |
103 | .switch-input:checked ~ .switch-handle {
104 | left: $width - $height + $handle-margin;
105 | }
106 |
107 |
108 | @if $type == ddd {
109 | @extend .switch-pill;
110 | }
111 |
112 | //size variations
113 | @if $type == default {
114 |
115 | &.switch-lg {
116 | @include switch-size($switch-lg-width, $switch-lg-height, $switch-lg-font-size, $handle-margin);
117 | }
118 | &.switch-sm {
119 | @include switch-size($switch-sm-width, $switch-sm-height, $switch-sm-font-size, $handle-margin);
120 | }
121 | &.switch-xs {
122 | @include switch-size($switch-xs-width, $switch-xs-height, $switch-xs-font-size, $handle-margin);
123 | }
124 |
125 | } @else if $type == text {
126 |
127 | &.switch-lg {
128 | @include switch-size($switch-text-lg-width, $switch-text-lg-height, $switch-text-lg-font-size, $handle-margin);
129 | }
130 | &.switch-sm {
131 | @include switch-size($switch-text-sm-width, $switch-text-sm-height, $switch-text-sm-font-size, $handle-margin);
132 | }
133 | &.switch-xs {
134 | @include switch-size($switch-text-xs-width, $switch-text-xs-height, $switch-text-xs-font-size, $handle-margin);
135 | }
136 |
137 | } @else if $type == icon {
138 |
139 | &.switch-lg {
140 | @include switch-size($switch-icon-lg-width, $switch-icon-lg-height, $switch-icon-lg-font-size, $handle-margin);
141 | }
142 | &.switch-sm {
143 | @include switch-size($switch-icon-sm-width, $switch-icon-sm-height, $switch-icon-sm-font-size, $handle-margin);
144 | }
145 | &.switch-xs {
146 | @include switch-size($switch-icon-xs-width, $switch-icon-xs-height, $switch-icon-xs-font-size, $handle-margin);
147 | }
148 |
149 | } @else if $type == ddd {
150 |
151 | &.switch-lg {
152 | @include switch-size($switch-lg-width, $switch-lg-height, $switch-lg-font-size, 0);
153 | }
154 | &.switch-sm {
155 | @include switch-size($switch-sm-width, $switch-sm-height, $switch-sm-font-size, 0);
156 | }
157 | &.switch-xs {
158 | @include switch-size($switch-xs-width, $switch-xs-height, $switch-xs-font-size, 0);
159 | }
160 | }
161 | }
162 |
163 | @mixin switch-variant($color) {
164 | > .switch-input:checked ~ .switch-label {
165 | background: $color;
166 | border-color: darken($color, 10%);
167 | }
168 |
169 | > .switch-input:checked ~ .switch-handle {
170 | border-color: darken($color, 10%);
171 | }
172 | }
173 |
174 | @mixin switch-outline-variant($color) {
175 | > .switch-input:checked ~ .switch-label {
176 | background: #fff;
177 | border-color: $color;
178 |
179 | &::after {
180 | color: $color;
181 | }
182 | }
183 |
184 | > .switch-input:checked ~ .switch-handle {
185 | border-color: $color;
186 | }
187 | }
188 |
189 | @mixin switch-outline-alt-variant($color) {
190 | > .switch-input:checked ~ .switch-label {
191 | background: #fff;
192 | border-color: $color;
193 |
194 | &::after {
195 | color: $color;
196 | }
197 | }
198 |
199 | > .switch-input:checked ~ .switch-handle {
200 | background: $color;
201 | border-color: $color;
202 | }
203 | }
204 |
205 | $switch-lg-width: 48px;
206 | $switch-lg-height: 28px;
207 | $switch-lg-font-size: 12px;
208 |
209 | $switch-width: 40px;
210 | $switch-height: 24px;
211 | $switch-font-size: 10px;
212 |
213 | $handle-margin: 2px;
214 |
215 | $switch-sm-width: 32px;
216 | $switch-sm-height: 20px;
217 | $switch-sm-font-size: 8px;
218 |
219 | $switch-xs-width: 24px;
220 | $switch-xs-height: 16px;
221 | $switch-xs-font-size: 7px;
222 |
223 |
224 | $switch-text-lg-width: 56px;
225 | $switch-text-lg-height: 28px;
226 | $switch-text-lg-font-size: 12px;
227 |
228 | $switch-text-width: 48px;
229 | $switch-text-height: 24px;
230 | $switch-text-font-size: 10px;
231 |
232 | $switch-text-sm-width: 40px;
233 | $switch-text-sm-height: 20px;
234 | $switch-text-sm-font-size: 8px;
235 |
236 | $switch-text-xs-width: 32px;
237 | $switch-text-xs-height: 16px;
238 | $switch-text-xs-font-size: 7px;
239 |
240 |
241 | $switch-icon-lg-width: 56px;
242 | $switch-icon-lg-height: 28px;
243 | $switch-icon-lg-font-size: 12px;
244 |
245 | $switch-icon-width: 48px;
246 | $switch-icon-height: 24px;
247 | $switch-icon-font-size: 10px;
248 |
249 | $switch-icon-sm-width: 40px;
250 | $switch-icon-sm-height: 20px;
251 | $switch-icon-sm-font-size: 8px;
252 |
253 | $switch-icon-xs-width: 32px;
254 | $switch-icon-xs-height: 16px;
255 | $switch-icon-xs-font-size: 7px;
256 |
257 | .switch-default {
258 | @include switch("default", $switch-width, $switch-height, $switch-font-size, $handle-margin);
259 | }
260 |
261 | .switch-text {
262 | @include switch("text", $switch-text-width, $switch-text-height, $switch-text-font-size, $handle-margin);
263 | }
264 |
265 | .switch-icon {
266 | @include switch("icon", $switch-icon-width, $switch-icon-height, $switch-icon-font-size, $handle-margin);
267 | }
268 |
269 | .switch-3d {
270 | @include switch("ddd", $switch-width, $switch-height, $switch-font-size, 0);
271 | }
272 |
273 |
274 | @each $color, $value in $theme-colors {
275 | //normal style
276 | .switch-#{$color} {
277 | @include switch-variant($value);
278 | }
279 | //outline style
280 | .switch-#{$color}-outline {
281 | @include switch-outline-variant($value);
282 | }
283 | //outline alternative style
284 | .switch-#{$color}-outline-alt {
285 | @include switch-outline-alt-variant($value);
286 | }
287 | }
288 |
289 | //pills style
290 | .switch-pill {
291 | .switch-label,
292 | .switch-handle {
293 | border-radius: 50em;
294 | }
295 |
296 | .switch-label::before {
297 | right: 2px;
298 | }
299 | .switch-label::after {
300 | left: 2px;
301 | }
302 | }
303 |
--------------------------------------------------------------------------------