├── app ├── static │ └── coreui │ │ ├── scss │ │ ├── _widgets.scss │ │ ├── _vendors.scss │ │ ├── _badge.scss │ │ ├── _others.scss │ │ ├── mixins │ │ │ ├── _card-accent.scss │ │ │ ├── _list-group.scss │ │ │ ├── _avatar.scss │ │ │ ├── _sidebar-width.scss │ │ │ ├── _buttons.scss │ │ │ └── _borders.scss │ │ ├── _input-group.scss │ │ ├── _utilities.scss │ │ ├── _breadcrumb.scss │ │ ├── _dropdown-menu-right.scss │ │ ├── bootstrap.scss │ │ ├── _mixins.scss │ │ ├── _footer.scss │ │ ├── _modal.scss │ │ ├── _progress.scss │ │ ├── _tables.scss │ │ ├── _grid.scss │ │ ├── _rtl.scss │ │ ├── _animate.scss │ │ ├── _buttons.scss │ │ ├── utilities │ │ │ ├── _background.scss │ │ │ ├── _display.scss │ │ │ ├── _borders.scss │ │ │ └── _typography.scss │ │ ├── _breadcrumb-menu.scss │ │ ├── _progress-group.scss │ │ ├── _list-group.scss │ │ ├── _avatars.scss │ │ ├── _nav.scss │ │ ├── _callout.scss │ │ ├── _brands-buttons.scss │ │ ├── coreui.scss │ │ ├── _brand-card.scss │ │ ├── coreui-standalone.scss │ │ ├── _dropdown.scss │ │ ├── _aside.scss │ │ ├── vendors │ │ │ └── _perfect-scrollbar.scss │ │ ├── _charts.scss │ │ ├── _card.scss │ │ ├── _navbar.scss │ │ ├── _bootstrap-variables.scss │ │ ├── _deprecated.scss │ │ ├── _loading.scss │ │ └── _switches.scss │ │ ├── js │ │ └── src │ │ │ ├── utilities │ │ │ ├── index.js │ │ │ ├── get-style.js │ │ │ ├── hex-to-rgb.js │ │ │ ├── hex-to-rgba.js │ │ │ └── rgb-to-hex.js │ │ │ ├── toggle-classes.js │ │ │ ├── index.js │ │ │ ├── aside-menu.js │ │ │ ├── ajax-load.js │ │ │ └── sidebar.js │ │ ├── LICENSE │ │ ├── package.json │ │ ├── README.md │ │ └── dist │ │ └── js │ │ └── coreui.min.js ├── templates │ ├── common │ │ ├── page_base.html │ │ ├── dark_base.html │ │ └── form_macros.html │ ├── flask_user │ │ ├── member_base.html │ │ ├── public_base.html │ │ ├── flask_user_base.html │ │ ├── change_password.html │ │ ├── forgot_password.html │ │ ├── register.html │ │ └── login.html │ ├── pages │ │ ├── member_base.html │ │ ├── errors.html │ │ ├── admin │ │ │ ├── delete_user.html │ │ │ ├── edit_user.html │ │ │ ├── create_user.html │ │ │ └── users.html │ │ └── user_profile_page.html │ ├── README.md │ ├── apikeys │ │ ├── delete.html │ │ ├── create.html │ │ ├── newkey.html │ │ └── list.html │ ├── layout.html │ └── dark_layout.html ├── commands │ ├── __init__.py │ └── user.py ├── models │ ├── __init__.py │ └── user_models.py ├── views │ ├── __init__.py │ ├── apis.py │ ├── apikeys.py │ └── misc_views.py ├── worker.py ├── README.md ├── utils │ ├── forms.py │ └── api.py ├── extensions │ ├── jinja.py │ └── ldap.py └── settings.py ├── docker ├── app │ ├── nginx_overrides.conf │ ├── uwsgi.ini │ └── prestart.sh ├── worker │ └── start_worker.sh └── ldap │ └── bootstrap.ldif ├── unicorn.py ├── tests ├── __init__.py ├── .coveragerc ├── README.md ├── conftest.py └── test_page_urls.py ├── tox.ini ├── requirements.txt ├── dockerfile.worker ├── bin └── update_dependencies.sh ├── dockerfile.app ├── makefile ├── LICENSE.txt ├── .gitignore ├── manage.py ├── docker-compose.yaml └── README.md /app/static/coreui/scss/_widgets.scss: -------------------------------------------------------------------------------- 1 | // ... 2 | -------------------------------------------------------------------------------- /docker/app/nginx_overrides.conf: -------------------------------------------------------------------------------- 1 | underscores_in_headers on; 2 | -------------------------------------------------------------------------------- /app/templates/common/page_base.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | -------------------------------------------------------------------------------- /app/templates/common/dark_base.html: -------------------------------------------------------------------------------- 1 | {% extends "dark_layout.html" %} 2 | -------------------------------------------------------------------------------- /app/static/coreui/scss/_vendors.scss: -------------------------------------------------------------------------------- 1 | @import "vendors/perfect-scrollbar"; 2 | -------------------------------------------------------------------------------- /app/templates/flask_user/member_base.html: -------------------------------------------------------------------------------- 1 | {% extends 'flask_user/flask_user_base.html' %} 2 | -------------------------------------------------------------------------------- /app/templates/flask_user/public_base.html: -------------------------------------------------------------------------------- 1 | {% extends 'flask_user/flask_user_base.html' %} 2 | -------------------------------------------------------------------------------- /app/static/coreui/scss/_badge.scss: -------------------------------------------------------------------------------- 1 | .badge-pill { 2 | border-radius: $badge-pill-border-radius; 3 | } 4 | -------------------------------------------------------------------------------- /unicorn.py: -------------------------------------------------------------------------------- 1 | 2 | from app import create_app 3 | 4 | app = create_app({}) 5 | 6 | if __name__ == "__main__": 7 | app.run() 8 | -------------------------------------------------------------------------------- /app/static/coreui/scss/_others.scss: -------------------------------------------------------------------------------- 1 | // stylelint-disable selector-no-qualifying-type 2 | hr.transparent { 3 | border-top: 1px solid transparent; 4 | } 5 | -------------------------------------------------------------------------------- /app/static/coreui/scss/mixins/_card-accent.scss: -------------------------------------------------------------------------------- 1 | @mixin card-accent-variant($color) { 2 | border-top-color: $color; 3 | border-top-width: 2px; 4 | } 5 | -------------------------------------------------------------------------------- /app/commands/__init__.py: -------------------------------------------------------------------------------- 1 | # __init__.py is a special Python file that allows a directory to become 2 | # a Python package so it can be accessed using the 'import' statement. 3 | -------------------------------------------------------------------------------- /app/models/__init__.py: -------------------------------------------------------------------------------- 1 | # __init__.py is a special Python file that allows a directory to become 2 | # a Python package so it can be accessed using the 'import' statement. 3 | -------------------------------------------------------------------------------- /app/static/coreui/scss/_input-group.scss: -------------------------------------------------------------------------------- 1 | .input-group-prepend, 2 | .input-group-append { 3 | white-space: nowrap; 4 | vertical-align: middle; // Match the inputs 5 | } 6 | -------------------------------------------------------------------------------- /app/static/coreui/scss/_utilities.scss: -------------------------------------------------------------------------------- 1 | @import "utilities/background"; 2 | @import "utilities/borders"; 3 | @import "utilities/display"; 4 | @import "utilities/typography"; 5 | -------------------------------------------------------------------------------- /app/views/__init__.py: -------------------------------------------------------------------------------- 1 | # __init__.py is a special Python file that allows a directory to become 2 | # a Python package so it can be accessed using the 'import' statement. 3 | -------------------------------------------------------------------------------- /app/static/coreui/scss/_breadcrumb.scss: -------------------------------------------------------------------------------- 1 | .breadcrumb { 2 | position: relative; 3 | @include border-radius($breadcrumb-border-radius); 4 | @include borders($breadcrumb-borders); 5 | } 6 | -------------------------------------------------------------------------------- /app/static/coreui/scss/_dropdown-menu-right.scss: -------------------------------------------------------------------------------- 1 | // Temp fix for reactstrap 2 | .app-header { 3 | .navbar-nav { 4 | .dropdown-menu-right { 5 | right: auto; 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /app/static/coreui/scss/bootstrap.scss: -------------------------------------------------------------------------------- 1 | // Override Boostrap variables 2 | @import "bootstrap-variables"; 3 | // Import Bootstrap source files 4 | @import "node_modules/bootstrap/scss/bootstrap"; 5 | -------------------------------------------------------------------------------- /app/templates/pages/member_base.html: -------------------------------------------------------------------------------- 1 | {% extends "common/dark_base.html" %} 2 | 3 | {% block content %} 4 |
5 |

Hello, world!

6 |
7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /docker/app/uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | module = unicorn:app 3 | callable = unicorn 4 | umask = 007 5 | uid=apprunner 6 | gid=apprunner 7 | env = HOME=/home/apprunner 8 | wsgi-disable-file-wrapper = true 9 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # __init__.py is a special Python file that allows a directory to become 2 | # a Python package so it can be accessed using the 'import' statement. 3 | 4 | # Intentionally left empty -------------------------------------------------------------------------------- /app/static/coreui/scss/_mixins.scss: -------------------------------------------------------------------------------- 1 | @import "mixins/avatar"; 2 | @import "mixins/buttons"; 3 | @import "mixins/borders"; 4 | @import "mixins/card-accent"; 5 | @import "mixins/list-group"; 6 | @import "mixins/sidebar-width"; 7 | -------------------------------------------------------------------------------- /app/static/coreui/scss/mixins/_list-group.scss: -------------------------------------------------------------------------------- 1 | // List Groups 2 | 3 | @mixin list-group-item-accent-variant($state, $color) { 4 | .list-group-item-accent-#{$state} { 5 | border-left: 4px solid $color; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/worker.py: -------------------------------------------------------------------------------- 1 | from app import celery, create_app 2 | 3 | # Make sure to import your celery tasks here! 4 | # Otherwise the worker will not pick them up. 5 | 6 | 7 | app = create_app() 8 | 9 | with app.app_context(): 10 | celery.start() 11 | -------------------------------------------------------------------------------- /app/static/coreui/scss/_footer.scss: -------------------------------------------------------------------------------- 1 | .app-footer { 2 | display: flex; 3 | flex-wrap: wrap; 4 | align-items: center; 5 | padding: 0 $spacer; 6 | color: $footer-color; 7 | background: $footer-bg; 8 | @include borders($footer-borders); 9 | } 10 | -------------------------------------------------------------------------------- /app/static/coreui/scss/_modal.scss: -------------------------------------------------------------------------------- 1 | @each $color, $value in $theme-colors { 2 | .modal-#{$color} { 3 | 4 | .modal-content { 5 | border-color: $value; 6 | } 7 | 8 | .modal-header { 9 | color: #fff; 10 | background-color: $value; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/static/coreui/js/src/utilities/index.js: -------------------------------------------------------------------------------- 1 | import GetStyle from './get-style' 2 | import HexToRgb from './hex-to-rgb' 3 | import HexToRgba from './hex-to-rgba' 4 | import RgbToHex from './rgb-to-hex' 5 | 6 | export { 7 | GetStyle, 8 | HexToRgb, 9 | HexToRgba, 10 | RgbToHex 11 | } 12 | -------------------------------------------------------------------------------- /tests/.coveragerc: -------------------------------------------------------------------------------- 1 | # Configuration for the test coverage tool (py.test --cov) 2 | # 3 | # Copyright 2014 SolidBuilds.com. All rights reserved 4 | # 5 | # Authors: Ling Thio 6 | 7 | [run] 8 | omit = app/startup/reset_db.py 9 | 10 | [report] 11 | show_missing = True 12 | -------------------------------------------------------------------------------- /app/static/coreui/scss/_progress.scss: -------------------------------------------------------------------------------- 1 | .progress-xs { 2 | height: 4px; 3 | } 4 | 5 | .progress-sm { 6 | height: 8px; 7 | } 8 | 9 | // White progress bar 10 | .progress-white { 11 | background-color: rgba(255, 255, 255, .2); 12 | .progress-bar { 13 | background-color: #fff; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | # Test on the following Python versions 3 | envlist = py26, py27, py33, py34, py35, py36 4 | 5 | toxworkdir=../builds/flask_user_starter_app/tox 6 | skipsdist=True 7 | 8 | [testenv] 9 | deps = -r{toxinidir}/requirements.txt 10 | 11 | # Run automated test suite 12 | commands= 13 | pytest tests/ -------------------------------------------------------------------------------- /app/static/coreui/scss/_tables.scss: -------------------------------------------------------------------------------- 1 | .table-outline { 2 | border: 1px solid $table-border-color; 3 | 4 | td { 5 | vertical-align: middle; 6 | } 7 | } 8 | 9 | .table-align-middle { 10 | 11 | td { 12 | vertical-align: middle; 13 | } 14 | } 15 | 16 | .table-clear { 17 | td { 18 | border: 0; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/templates/flask_user/flask_user_base.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | 4 | {% block pre_content %} 5 |
6 |
7 |
8 | {% endblock %} 9 | 10 | 11 | {% block post_content %} 12 |
13 |
14 |
15 | {% endblock %} -------------------------------------------------------------------------------- /app/static/coreui/scss/_grid.scss: -------------------------------------------------------------------------------- 1 | .row.row-equal { 2 | padding-right: ($grid-gutter-width / 4); 3 | padding-left: ($grid-gutter-width / 4); 4 | margin-right: ($grid-gutter-width / -2); 5 | margin-left: ($grid-gutter-width / -2); 6 | 7 | [class*="col-"] { 8 | padding-right: ($grid-gutter-width / 4); 9 | padding-left: ($grid-gutter-width / 4); 10 | } 11 | } 12 | 13 | .main .container-fluid { 14 | padding: 0 30px; 15 | } 16 | -------------------------------------------------------------------------------- /app/static/coreui/scss/_rtl.scss: -------------------------------------------------------------------------------- 1 | // 2 | // RTL Support 3 | // 4 | *[dir="rtl"] { 5 | direction: rtl; 6 | unicode-bidi: embed; 7 | 8 | body { 9 | text-align: right; 10 | } 11 | 12 | 13 | // Dropdown 14 | .dropdown-item { 15 | text-align: right; 16 | 17 | i { 18 | margin-right: -10px; 19 | margin-left: 10px; 20 | } 21 | 22 | .badge { 23 | right: auto; 24 | left: 10px; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/static/coreui/scss/_animate.scss: -------------------------------------------------------------------------------- 1 | // scss-lint:disable all 2 | .animated { 3 | animation-duration: 1s; 4 | // animation-fill-mode: both; 5 | } 6 | 7 | .animated.infinite { 8 | animation-iteration-count: infinite; 9 | } 10 | 11 | .animated.hinge { 12 | animation-duration: 2s; 13 | } 14 | 15 | @keyframes fadeIn { 16 | from { 17 | opacity: 0; 18 | } 19 | 20 | to { 21 | opacity: 1; 22 | } 23 | } 24 | 25 | .fadeIn { 26 | animation-name: fadeIn; 27 | } 28 | -------------------------------------------------------------------------------- /app/README.md: -------------------------------------------------------------------------------- 1 | # app directory 2 | 3 | This directory contains the Flask application code. 4 | 5 | The code has been organized into the following sub-directories: 6 | 7 | # Sub-directories 8 | commands # Commands made available to manage.py 9 | models # Database Models and their Forms 10 | static # Static asset files that will be mapped to the "/static/" URL 11 | templates # Jinja2 HTML template files 12 | views # View functions 13 | 14 | -------------------------------------------------------------------------------- /app/static/coreui/scss/mixins/_avatar.scss: -------------------------------------------------------------------------------- 1 | @mixin avatar($width, $status-width) { 2 | position: relative; 3 | display: inline-block; 4 | width: $width; 5 | 6 | .img-avatar { 7 | width: $width; 8 | height: $width; 9 | } 10 | 11 | .avatar-status { 12 | position: absolute; 13 | right: 0; 14 | bottom: 0; 15 | display: block; 16 | width: $status-width; 17 | height: $status-width; 18 | border: 1px solid #fff; 19 | border-radius: 50em; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/static/coreui/js/src/utilities/get-style.js: -------------------------------------------------------------------------------- 1 | /** 2 | * -------------------------------------------------------------------------- 3 | * CoreUI Utilities (v1.0.0): get-style.js 4 | * Licensed under MIT (https://coreui.io/license) 5 | * -------------------------------------------------------------------------- 6 | */ 7 | 8 | const GetStyle = (property, element = document.body) => { 9 | const style = window.getComputedStyle(element, null).getPropertyValue(property).replace(/^\s/, '') 10 | 11 | return style 12 | } 13 | 14 | export default GetStyle 15 | -------------------------------------------------------------------------------- /app/templates/pages/errors.html: -------------------------------------------------------------------------------- 1 | {% extends "common/dark_base.html" %} {# common/page_base.html extends layout.html #} 2 | {% block title %} - {{ error_code }} Error{% endblock %} 3 | {% block content %} 4 | {% from "common/form_macros.html" import render_field %} 5 |
6 |
7 |
8 | A {{ error_code }} Error Occurred 9 |
10 |
11 | {{ message }} 12 |
13 |
14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /app/static/coreui/scss/_buttons.scss: -------------------------------------------------------------------------------- 1 | button { 2 | cursor: pointer; 3 | } 4 | 5 | .btn-transparent { 6 | color: #fff; 7 | background-color: transparent; 8 | border-color: transparent; 9 | } 10 | 11 | .btn { 12 | [class^="icon-"], 13 | [class*=" icon-"] { 14 | display: inline-block; 15 | margin-top: -2px; 16 | vertical-align: middle; 17 | } 18 | } 19 | 20 | .btn-pill { 21 | border-radius: 50em; 22 | } 23 | 24 | .btn-square { 25 | border-radius: 0; 26 | } 27 | 28 | @each $color, $value in $theme-colors { 29 | .btn-ghost-#{$color} { 30 | @include button-ghost-variant($value); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/utils/forms.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | 3 | from wtforms import SubmitField, validators, SelectMultipleField 4 | from wtforms.widgets import CheckboxInput, ListWidget 5 | 6 | 7 | class MultiCheckboxField(SelectMultipleField): 8 | """ 9 | A multiple-select, except displays a list of checkboxes. 10 | 11 | Iterating the field will produce subfields, allowing custom rendering of 12 | the enclosed checkbox fields. 13 | """ 14 | widget = ListWidget(prefix_label=False) 15 | option_widget = CheckboxInput() 16 | 17 | class ConfirmationForm(FlaskForm): 18 | submit = SubmitField('Confirm') 19 | -------------------------------------------------------------------------------- /app/static/coreui/scss/utilities/_background.scss: -------------------------------------------------------------------------------- 1 | .bg-primary, 2 | .bg-success, 3 | .bg-info, 4 | .bg-warning, 5 | .bg-danger, 6 | .bg-dark { 7 | color: #fff; 8 | } 9 | 10 | @each $color, $value in $brands-colors { 11 | @include bg-variant(".bg-#{$color}", $value); 12 | } 13 | 14 | @each $color, $value in $colors { 15 | @include bg-variant(".bg-#{$color}", $value); 16 | } 17 | 18 | @each $color, $value in $grays { 19 | @include bg-variant(".bg-gray-#{$color}", $value); 20 | } 21 | 22 | .bg-box { 23 | display: flex; 24 | align-items: center; 25 | justify-content: center; 26 | width: ($spacer * 2.5); 27 | height: ($spacer * 2.5); 28 | } 29 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # This file is used by pip to install required python packages 2 | # Usage: pip install -r requirements.txt 3 | 4 | # Flask Framework 5 | Flask==1.1.2 6 | 7 | # Flask Packages 8 | Flask-Login==0.5.0 9 | Flask-Migrate==2.5.3 10 | Flask-SQLAlchemy==2.4.1 11 | Flask-BabelEx==0.9.4 12 | Flask-User==1.0.2.2 13 | Flask-WTF==0.14.3 14 | 15 | # Automated tests 16 | pytest==5.4.1 17 | pytest-cov==2.8.1 18 | 19 | # Libraries 20 | beaker==1.11.0 21 | boto3==1.12.39 22 | blinker==1.4 23 | celery==4.4.2 24 | click==7.1.1 25 | ldap3==2.7 26 | psycopg2-binary==2.8.5 27 | pyyaml==5.4 28 | requests==2.23.0 29 | 30 | # Development tools 31 | # tox==3.5.2 32 | -------------------------------------------------------------------------------- /app/static/coreui/js/src/utilities/hex-to-rgb.js: -------------------------------------------------------------------------------- 1 | /** 2 | * -------------------------------------------------------------------------- 3 | * CoreUI Utilities (v2.0.0-beta.2): hex-to-rgb.js 4 | * Licensed under MIT (https://coreui.io/license) 5 | * -------------------------------------------------------------------------- 6 | */ 7 | 8 | /* eslint-disable no-magic-numbers */ 9 | const HexToRgb = (color) => { 10 | const hex = color.replace('#', '') 11 | const r = parseInt(hex.substring(0, 2), 16) 12 | const g = parseInt(hex.substring(2, 4), 16) 13 | const b = parseInt(hex.substring(4, 6), 16) 14 | 15 | const result = `rgba(${r}, ${g}, ${b}` 16 | return result 17 | } 18 | 19 | export default HexToRgb 20 | -------------------------------------------------------------------------------- /app/static/coreui/scss/mixins/_sidebar-width.scss: -------------------------------------------------------------------------------- 1 | @mixin sidebar-width($borders, $width) { 2 | $sidebar-width: $width; 3 | 4 | @each $border in $borders { 5 | $direction: nth($border, 1); 6 | @if $direction == "all" { 7 | $size: map-get(map-get($borders, $direction), size); 8 | $sidebar-width: ($sidebar-width - (2 * $size)); 9 | } @else if $direction == "right" { 10 | $size: map-get(map-get($borders, $direction), size); 11 | $sidebar-width: $sidebar-width - $size; 12 | } @else if $direction == "left" { 13 | $size: map-get(map-get($borders, $direction), size); 14 | $sidebar-width: $sidebar-width - $size; 15 | } 16 | width: $sidebar-width; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/templates/README.md: -------------------------------------------------------------------------------- 1 | # This directory contains Jinja2 template files 2 | 3 | This Flask application uses the Jinja2 templating engine to render 4 | data into HTML files. 5 | 6 | The template files are organized into the following directories: 7 | 8 | common # Common base template files and macros 9 | flask_user # Flask-User template files (register, login, etc.) 10 | pages # Template files for web pages 11 | 12 | Flask-User makes use of standard template files that reside in 13 | `PATH/TO/VIRTUALENV/lib/PYTHONVERSION/site-packages/flask_user/templates/flask_user/`. 14 | These standard templates can be overruled by placing a copy in the `app/templates/flask_user/` directory. 15 | -------------------------------------------------------------------------------- /app/static/coreui/js/src/utilities/hex-to-rgba.js: -------------------------------------------------------------------------------- 1 | /** 2 | * -------------------------------------------------------------------------- 3 | * CoreUI Utilities (v2.0.0-beta.2): hex-to-rgba.js 4 | * Licensed under MIT (https://coreui.io/license) 5 | * -------------------------------------------------------------------------- 6 | */ 7 | 8 | /* eslint-disable no-magic-numbers */ 9 | const HexToRgba = (color, opacity = 100) => { 10 | const hex = color.replace('#', '') 11 | const r = parseInt(hex.substring(0, 2), 16) 12 | const g = parseInt(hex.substring(2, 4), 16) 13 | const b = parseInt(hex.substring(4, 6), 16) 14 | 15 | const result = `rgba(${r}, ${g}, ${b}, ${opacity / 100}` 16 | return result 17 | } 18 | 19 | export default HexToRgba 20 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # tests directory 2 | 3 | This directory contains all the automated tests for the test tool `py.test`. 4 | 5 | **`.coverage`**: Configuration file for the Python coverage tool `coverage`. 6 | 7 | **`conftest.py`**: Defines fixtures for py.test. 8 | 9 | **`test_*`**: py.test will load any file that starts with the name `test_` 10 | and run any function that starts with the name `test_`. 11 | 12 | 13 | ## Testing the app 14 | 15 | # Run all the automated tests in the tests/ directory 16 | ./runtests.sh # will run "py.test -s tests/" 17 | 18 | 19 | ## Generating a test coverage report 20 | 21 | # Run tests and show a test coverage report 22 | ./runcoverage.sh # will run py.test with coverage options 23 | 24 | -------------------------------------------------------------------------------- /app/static/coreui/scss/_breadcrumb-menu.scss: -------------------------------------------------------------------------------- 1 | .breadcrumb-menu { 2 | margin-left: auto; 3 | 4 | &::before { 5 | display: none; 6 | } 7 | 8 | .btn-group { 9 | vertical-align: top; 10 | } 11 | 12 | .btn { 13 | padding: 0 $input-btn-padding-x; 14 | color: $text-muted; 15 | vertical-align: top; 16 | border: 0; 17 | 18 | &:hover, 19 | &.active { 20 | color: $body-color; 21 | background: transparent; 22 | } 23 | } 24 | 25 | .open { 26 | .btn { 27 | color: $body-color; 28 | background: transparent; 29 | } 30 | } 31 | 32 | .dropdown-menu { 33 | min-width: 180px; 34 | line-height: $line-height-base; 35 | } 36 | } 37 | 38 | // Right-to-Left Support 39 | *[dir="rtl"] { 40 | .breadcrumb-menu { 41 | margin-right: auto; 42 | margin-left: initial; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/static/coreui/scss/_progress-group.scss: -------------------------------------------------------------------------------- 1 | .progress-group { 2 | display: flex; 3 | flex-flow: row wrap; 4 | margin-bottom: $spacer; 5 | } 6 | 7 | .progress-group-prepend { 8 | flex: 0 0 100px; 9 | align-self: center; 10 | } 11 | 12 | .progress-group-icon { 13 | margin: 0 $spacer 0 ($spacer * .25); 14 | font-size: $font-size-lg; 15 | } 16 | 17 | .progress-group-text { 18 | font-size: $font-size-sm; 19 | color: $gray-600; 20 | } 21 | 22 | .progress-group-header { 23 | display: flex; 24 | flex-basis: 100%; 25 | align-items: flex-end; 26 | margin-bottom: ($spacer * .25); 27 | } 28 | 29 | .progress-group-bars { 30 | flex-grow: 1; 31 | align-self: center; 32 | 33 | .progress:not(:last-child) { 34 | margin-bottom: 2px; 35 | } 36 | } 37 | 38 | .progress-group-header + .progress-group-bars { 39 | flex-basis: 100%; 40 | } 41 | -------------------------------------------------------------------------------- /app/static/coreui/scss/utilities/_display.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Utilities for common `display` values 3 | // 4 | // stylelint-disable declaration-no-important 5 | @each $breakpoint in map-keys($grid-breakpoints) { 6 | @include media-breakpoint-down($breakpoint) { 7 | $infix: breakpoint-infix($breakpoint, $grid-breakpoints); 8 | 9 | .d#{$infix}-down-none { display: none !important; } 10 | // .d#{$infix}-inline { display: inline !important; } 11 | // .d#{$infix}-inline-block { display: inline-block !important; } 12 | // .d#{$infix}-block { display: block !important; } 13 | // .d#{$infix}-table { display: table !important; } 14 | // .d#{$infix}-table-cell { display: table-cell !important; } 15 | // .d#{$infix}-flex { display: flex !important; } 16 | // .d#{$infix}-inline-flex { display: inline-flex !important; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/static/coreui/scss/_list-group.scss: -------------------------------------------------------------------------------- 1 | // List items with accent 2 | // 3 | // Remove top, bottome and right borders and border-radius. 4 | 5 | .list-group-accent { 6 | .list-group-item { 7 | margin-bottom: 1px; 8 | border-top: 0; 9 | border-right: 0; 10 | border-bottom: 0; 11 | @include border-radius(0); 12 | 13 | &.list-group-item-divider { 14 | position: relative; 15 | 16 | &::before { 17 | position: absolute; 18 | bottom: -1px; 19 | left: 5%; 20 | width: 90%; 21 | height: 1px; 22 | content: ""; 23 | background-color: $gray-200; 24 | } 25 | } 26 | } 27 | } 28 | 29 | // Contextual variants 30 | // 31 | // Add modifier classes to change border color on individual items. 32 | 33 | @each $color, $value in $theme-colors { 34 | @include list-group-item-accent-variant($color, $value); 35 | } 36 | -------------------------------------------------------------------------------- /dockerfile.worker: -------------------------------------------------------------------------------- 1 | ARG PYTHON_VERSION=3.7 2 | FROM python:${PYTHON_VERSION} 3 | 4 | ADD requirements.txt /app/requirements.txt 5 | RUN sed '/^uWSGI/ d' < /app/requirements.txt > /app/requirements_filtered.txt 6 | WORKDIR /app/ 7 | RUN pip install -r requirements_filtered.txt 8 | RUN pip install urlparser secretcli 9 | 10 | RUN apt-get update \ 11 | && apt-get install -y -f netcat \ 12 | && apt-get clean \ 13 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 14 | 15 | ENV SETTINGS /app/settings.yaml 16 | 17 | RUN useradd -ms /bin/bash apprunner 18 | USER apprunner 19 | 20 | RUN mkdir -p /home/apprunner/.aws 21 | RUN touch /home/apprunner/.aws/credentials 22 | RUN touch /home/apprunner/.aws/config 23 | RUN chown -R apprunner:apprunner /home/apprunner/.aws 24 | 25 | ADD ./docker/worker/start_worker.sh /home/apprunner/start_worker.sh 26 | ADD ./app/ /app/app 27 | 28 | ENTRYPOINT /home/apprunner/start_worker.sh 29 | -------------------------------------------------------------------------------- /app/templates/apikeys/delete.html: -------------------------------------------------------------------------------- 1 | {% extends "common/dark_base.html" %} {# common/page_base.html extends layout.html #} 2 | {% block title %}Delete API Key {{ key_id }}{% endblock %} 3 | {% block breadcrumb %} 4 | 7 | {% endblock %} 8 | {% block content %} 9 | {% from "common/form_macros.html" import render_field %} 10 |
11 |
12 |
13 | Delete Key {{ key_id }} 14 |
15 |
16 | {{ form.hidden_tag() }} 17 |
18 | 21 |
22 |
23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /app/static/coreui/js/src/toggle-classes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * -------------------------------------------------------------------------- 3 | * CoreUI (v2.0.0-beta.2): toggle-classes.js 4 | * Licensed under MIT (https://coreui.io/license) 5 | * -------------------------------------------------------------------------- 6 | */ 7 | 8 | const RemoveClasses = (NewClassNames) => { 9 | const MatchClasses = NewClassNames.map((Class) => document.body.classList.contains(Class)) 10 | return MatchClasses.indexOf(true) !== -1 11 | } 12 | 13 | const ToggleClasses = (Toggle, ClassNames) => { 14 | const Level = ClassNames.indexOf(Toggle) 15 | const NewClassNames = ClassNames.slice(0, Level + 1) 16 | 17 | if (RemoveClasses(NewClassNames)) { 18 | NewClassNames.map((Class) => document.body.classList.remove(Class)) 19 | } else { 20 | document.body.classList.add(Toggle) 21 | } 22 | } 23 | 24 | export default ToggleClasses 25 | -------------------------------------------------------------------------------- /app/templates/apikeys/create.html: -------------------------------------------------------------------------------- 1 | {% extends "common/dark_base.html" %} {# common/page_base.html extends layout.html #} 2 | {% block title %}Create a new API Key{% endblock %} 3 | {% block breadcrumb %} 4 | 7 | {% endblock %} 8 | {% block content %} 9 | {% from "common/form_macros.html" import render_field %} 10 |
11 |
12 |
13 | New Key 14 |
15 |
16 | {{ form.hidden_tag() }} 17 | {{ render_field(form.label) }} 18 |
19 | 22 |
23 |
24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /app/static/coreui/scss/_avatars.scss: -------------------------------------------------------------------------------- 1 | .img-avatar { 2 | border-radius: 50em; 3 | } 4 | 5 | .avatar { 6 | $width: 36px; 7 | $status-width: 10px; 8 | @include avatar($width,$status-width); 9 | } 10 | 11 | .avatar.avatar-xs { 12 | $width: 20px; 13 | $status-width: 8px; 14 | @include avatar($width,$status-width); 15 | } 16 | 17 | .avatar.avatar-sm { 18 | $width: 24px; 19 | $status-width: 8px; 20 | @include avatar($width,$status-width); 21 | } 22 | 23 | .avatar.avatar-lg { 24 | $width: 72px; 25 | $status-width: 12px; 26 | @include avatar($width,$status-width); 27 | } 28 | 29 | .avatars-stack { 30 | .avatar.avatar-xs { 31 | margin-right: -10px; 32 | } 33 | 34 | // .avatar.avatar-sm { 35 | // 36 | // } 37 | 38 | .avatar { 39 | margin-right: -15px; 40 | transition: margin-left $layout-transition-speed, margin-right $layout-transition-speed; 41 | 42 | &:hover { 43 | margin-right: 0; 44 | } 45 | } 46 | 47 | // .avatar.avatar-lg { 48 | // 49 | // } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /app/static/coreui/js/src/utilities/rgb-to-hex.js: -------------------------------------------------------------------------------- 1 | /** 2 | * -------------------------------------------------------------------------- 3 | * CoreUI (v2.0.0-beta.2): rgb-to-hex.js 4 | * Licensed under MIT (https://coreui.io/license) 5 | * -------------------------------------------------------------------------- 6 | */ 7 | 8 | /* eslint-disable no-magic-numbers */ 9 | const RgbToHex = (color) => { 10 | const rgb = color.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i) 11 | const r = `0${parseInt(rgb[1], 10).toString(16)}` 12 | const g = `0${parseInt(rgb[2], 10).toString(16)}` 13 | const b = `0${parseInt(rgb[3], 10).toString(16)}` 14 | 15 | return (rgb && rgb.length === 4) ? `#${r.slice(-2)}${g.slice(-2)}${b.slice(-2)}` : '' 16 | 17 | 18 | // return (rgb && rgb.length === 4) ? '#' + ('0' + parseInt(rgb[1], 10).toString(16)).slice(-2) + ('0' + parseInt(rgb[2], 10).toString(16)).slice(-2) + ('0' + parseInt(rgb[3], 10).toString(16)).slice(-2) : ''; 19 | } 20 | 21 | export default RgbToHex 22 | -------------------------------------------------------------------------------- /app/templates/apikeys/newkey.html: -------------------------------------------------------------------------------- 1 | {% extends "common/dark_base.html" %} {# common/page_base.html extends layout.html #} 2 | {% block breadcrumb %} 3 | 6 | {% endblock %} 7 | {% block title %}Your new API Key{% endblock %} 8 | {% block content %} 9 |
10 |
11 | Your new API Key 12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
IDKeyLabel
{{ id }}{{ key }}{{ label }}
30 |
31 | 33 |
34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /app/static/coreui/scss/utilities/_borders.scss: -------------------------------------------------------------------------------- 1 | //border 2 | // stylelint-disable declaration-no-important 3 | @each $prop, $abbrev in (border: b) { 4 | @each $size in (0,1,2) { 5 | @if $size == 0 { 6 | .#{$abbrev}-a-#{$size} { #{$prop}: 0 !important; } // a = All sides 7 | .#{$abbrev}-t-#{$size} { #{$prop}-top: 0 !important; } 8 | .#{$abbrev}-r-#{$size} { #{$prop}-right: 0 !important; } 9 | .#{$abbrev}-b-#{$size} { #{$prop}-bottom: 0 !important; } 10 | .#{$abbrev}-l-#{$size} { #{$prop}-left: 0 !important; } 11 | } @else { 12 | .#{$abbrev}-a-#{$size} { #{$prop}: $size * $border-width solid $border-color; } // a = All sides 13 | .#{$abbrev}-t-#{$size} { #{$prop}-top: $size * $border-width solid $border-color; } 14 | .#{$abbrev}-r-#{$size} { #{$prop}-right: $size * $border-width solid $border-color; } 15 | .#{$abbrev}-b-#{$size} { #{$prop}-bottom: $size * $border-width solid $border-color; } 16 | .#{$abbrev}-l-#{$size} { #{$prop}-left: $size * $border-width solid $border-color; } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/static/coreui/scss/utilities/_typography.scss: -------------------------------------------------------------------------------- 1 | body { 2 | -moz-osx-font-smoothing: grayscale; 3 | -webkit-font-smoothing: antialiased; 4 | } 5 | 6 | // stylelint-disable declaration-no-important 7 | .font-xs { 8 | font-size: .75rem !important; 9 | } 10 | 11 | .font-sm { 12 | font-size: .85rem !important; 13 | } 14 | 15 | .font-lg { 16 | font-size: 1rem !important; 17 | } 18 | 19 | .font-xl { 20 | font-size: 1.25rem !important; 21 | } 22 | 23 | .font-2xl { 24 | font-size: 1.5rem !important; 25 | } 26 | 27 | .font-3xl { 28 | font-size: 1.75rem !important; 29 | } 30 | 31 | .font-4xl { 32 | font-size: 2rem !important; 33 | } 34 | 35 | .font-5xl { 36 | font-size: 2.5rem !important; 37 | } 38 | 39 | .text-value { 40 | font-size: ($font-size-base * 1.5); 41 | font-weight: 600; 42 | } 43 | .text-value-sm { 44 | font-size: ($font-size-base * 1.25); 45 | font-weight: 600; 46 | } 47 | 48 | .text-value-lg { 49 | font-size: ($font-size-base * 1.75); 50 | font-weight: 600; 51 | } 52 | 53 | .text-white .text-muted { 54 | color: rgba(255, 255, 255, .6) !important; 55 | } 56 | -------------------------------------------------------------------------------- /bin/update_dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | REQUIREMENTS_FILE=requirements.txt 4 | 5 | while read p; do 6 | if [[ -z "${p// }" ]];then 7 | continue 8 | fi 9 | if [[ "${p// }" == \#* ]];then 10 | continue 11 | fi 12 | if [[ "${p// }" == \git+ssh* ]];then 13 | continue 14 | fi 15 | 16 | count=$(echo ${p// } | wc -m) 17 | if (( count < 3 )); then 18 | continue 19 | fi 20 | 21 | program=$(echo $p | cut -d= -f1) 22 | curr_version=$(cat ./requirements.txt | grep "${program}==" | cut -d'=' -f3 | sed 's/[^0-9.]*//g') 23 | PACKAGE_JSON_URL="https://pypi.python.org/pypi/${program}/json" 24 | version=$(curl -L -s "$PACKAGE_JSON_URL" | jq -r '.releases | keys | .[]' | sed '/^[^[:alpha:]]*$/!d' | sort -V | tail -1 | sed 's/[^0-9.]*//g' ) 25 | 26 | if [ "$curr_version" != "$version" ]; then 27 | echo "Updating requirements for ${program} from ${curr_version} to ${version}." 28 | sed -i -e "s/${program}==.*/${program}==${version}/g" $REQUIREMENTS_FILE 29 | fi 30 | 31 | done < $REQUIREMENTS_FILE 32 | 33 | # OSX Creates a backup file. 34 | rm -f "${REQUIREMENTS_FILE}-e" 35 | -------------------------------------------------------------------------------- /dockerfile.app: -------------------------------------------------------------------------------- 1 | ARG PYTHON_VERSION=3.7 2 | FROM tiangolo/uwsgi-nginx-flask:python${PYTHON_VERSION} 3 | 4 | RUN adduser --disabled-login --gecos '' apprunner 5 | RUN mkdir -p /home/apprunner/.aws 6 | RUN touch /home/apprunner/.aws/credentials 7 | RUN touch /home/apprunner/.aws/config 8 | RUN chown -R apprunner:apprunner /home/apprunner/.aws 9 | 10 | ENV FLASK_APP /app/unicorn.py 11 | ENV STATIC_URL /app/app/static 12 | 13 | ADD requirements.txt /app/requirements.txt 14 | RUN sed '/^uWSGI/ d' < /app/requirements.txt > /app/requirements_filtered.txt 15 | WORKDIR /app/ 16 | RUN pip install -r requirements_filtered.txt 17 | RUN pip install urlparser secretcli 18 | 19 | RUN apt-get update \ 20 | && apt-get install -y -f netcat \ 21 | && apt-get clean \ 22 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 23 | 24 | ADD ./docker/app/uwsgi.ini /app/uwsgi.ini 25 | ADD ./docker/app/prestart.sh /app/prestart.sh 26 | ADD ./docker/app/nginx_overrides.conf /etc/nginx/conf.d/nginx_overrides.conf 27 | 28 | 29 | ADD ./migrations/ /app/migrations 30 | ADD ./app/ /app/app 31 | ADD ./manage.py /app/manage.py 32 | ADD ./unicorn.py /app/unicorn.py 33 | -------------------------------------------------------------------------------- /app/extensions/jinja.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | from wtforms.fields import HiddenField 3 | from jinja2 import evalcontextfilter, Markup 4 | import re 5 | 6 | jinja_extensions_blueprint = Blueprint('jinja_extensions_blueprint', __name__, template_folder='templates') 7 | 8 | 9 | @jinja_extensions_blueprint.app_template_filter() 10 | def filesize_format(num): 11 | magnitude = 0 12 | while abs(num) >= 1000: 13 | magnitude += 1 14 | num /= 1000.0 15 | return '%.0f%s' % (num, ['', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'][magnitude]) 16 | 17 | 18 | @jinja_extensions_blueprint.app_template_global 19 | def is_hidden_field_filter(field): 20 | return isinstance(field, HiddenField) 21 | 22 | 23 | @jinja_extensions_blueprint.app_template_filter() 24 | @evalcontextfilter 25 | def nl2br(eval_ctx, value): 26 | """Converts newlines into

and
s.""" 27 | normalized_value = re.sub(r'\r\n|\r|\n', '\n', value) # normalize newlines 28 | html_value = normalized_value.replace('\n', '\n
\n') 29 | if eval_ctx.autoescape: 30 | return Markup(html_value) 31 | return html_value 32 | -------------------------------------------------------------------------------- /app/templates/flask_user/change_password.html: -------------------------------------------------------------------------------- 1 | {% extends "common/dark_base.html" %} {# common/page_base.html extends layout.html #} 2 | {% block breadcrumb %} 3 | {% endblock %} 4 | {% block content %} 5 | {% from "common/form_macros.html" import render_field, render_submit_field %} 6 |

7 |
8 |
9 | Change Password 10 |
11 |
12 | {{ form.hidden_tag() }} 13 | {{ render_field(form.old_password, tabindex=10) }} 14 | {{ render_field(form.new_password, tabindex=20) }} 15 | {% if user_manager.USER_REQUIRE_RETYPE_PASSWORD %} 16 | {{ render_field(form.retype_password, tabindex=30) }} 17 | {% endif %} 18 |
19 | 23 |
24 |
25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /app/static/coreui/scss/_nav.scss: -------------------------------------------------------------------------------- 1 | .nav-tabs { 2 | .nav-link { 3 | color: $gray-600; 4 | &:hover { 5 | cursor: pointer; 6 | } 7 | &.active { 8 | color: $gray-800; 9 | background: #fff; 10 | border-color: $border-color; 11 | border-bottom-color: #fff; 12 | &:focus { 13 | background: #fff; 14 | border-color: $border-color; 15 | border-bottom-color: #fff; 16 | } 17 | } 18 | } 19 | } 20 | 21 | .tab-content { 22 | margin-top: -1px; 23 | background: #fff; 24 | border: 1px solid $border-color; 25 | .tab-pane { 26 | padding: $spacer; 27 | } 28 | } 29 | 30 | .card-block { 31 | .tab-content { 32 | margin-top: 0; 33 | border: 0; 34 | } 35 | } 36 | 37 | .nav-fill { 38 | .nav-link { 39 | background-color: #fff; 40 | border-color: $border-color; 41 | } 42 | .nav-link + .nav-link { 43 | margin-left: -1px; 44 | } 45 | .nav-link.active { 46 | margin-top: -1px; 47 | // margin-left: 0; 48 | border-top: 2px solid $primary; 49 | } 50 | } 51 | 52 | // Right-to-Left Support 53 | *[dir="rtl"] { 54 | .nav { 55 | padding-right: 0; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/static/coreui/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 creativeLabs Łukasz Holeczek. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/templates/pages/admin/delete_user.html: -------------------------------------------------------------------------------- 1 | {% extends "common/dark_base.html" %} {# common/page_base.html extends layout.html #} 2 | {% block breadcrumb %} 3 | 6 | 9 | {% endblock %} 10 | {% block content %} 11 | 12 | {% from "common/form_macros.html" import render_field, render_submit_field %} 13 |
14 |
15 |
16 |
17 |
18 | Delete User 19 |
20 |
21 | {{ form.hidden_tag() }} 22 | Are you sure you want to delete this user? 23 |
24 | 27 |
28 |
29 |
30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /app/static/coreui/scss/mixins/_buttons.scss: -------------------------------------------------------------------------------- 1 | @mixin button-ghost-variant($color, $color-hover: color-yiq($color), $active-background: $color, $active-border: $color) { 2 | color: $color; 3 | background-color: transparent; 4 | background-image: none; 5 | border-color: transparent; 6 | 7 | &:hover { 8 | color: $color-hover; 9 | background-color: $active-background; 10 | border-color: $active-border; 11 | } 12 | 13 | &:focus, 14 | &.focus { 15 | box-shadow: 0 0 0 $btn-focus-width rgba($color, .5); 16 | } 17 | 18 | &.disabled, 19 | &:disabled { 20 | color: $color; 21 | background-color: transparent; 22 | border-color: transparent; 23 | } 24 | 25 | &:not(:disabled):not(.disabled):active, 26 | &:not(:disabled):not(.disabled).active, 27 | .show > &.dropdown-toggle { 28 | color: color-yiq($active-background); 29 | background-color: $active-background; 30 | border-color: $active-border; 31 | 32 | &:focus { 33 | // Avoid using mixin so we can pass custom focus shadow properly 34 | @if $enable-shadows and $btn-active-box-shadow != none { 35 | box-shadow: $btn-active-box-shadow, 0 0 0 $btn-focus-width rgba($color, .5); 36 | } @else { 37 | box-shadow: 0 0 0 $btn-focus-width rgba($color, .5); 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/static/coreui/scss/_callout.scss: -------------------------------------------------------------------------------- 1 | .callout { 2 | position: relative; 3 | padding: 0 $spacer; 4 | margin: $spacer 0; 5 | border-left: 4px solid $border-color; 6 | 7 | @if $enable-rounded { 8 | border-radius: $border-radius; 9 | } 10 | 11 | .chart-wrapper { 12 | position: absolute; 13 | top: 10px; 14 | left: 50%; 15 | float: right; 16 | width: 50%; 17 | } 18 | } 19 | 20 | .callout-bordered { 21 | border: 1px solid $border-color; 22 | border-left-width: 4px; 23 | } 24 | .callout code { 25 | border-radius: $border-radius; 26 | } 27 | .callout h4 { 28 | margin-top: 0; 29 | margin-bottom: .25rem; 30 | } 31 | .callout p:last-child { 32 | margin-bottom: 0; 33 | } 34 | .callout + .callout { 35 | margin-top: - .25rem; 36 | } 37 | 38 | @each $color, $value in $theme-colors { 39 | .callout-#{$color} { 40 | border-left-color: $value; 41 | 42 | h4 { 43 | color: $value; 44 | } 45 | } 46 | } 47 | 48 | // Right-to-Left Support 49 | *[dir="rtl"] { 50 | .callout { 51 | border-right: 4px solid $border-color; 52 | border-left: 0; 53 | 54 | @each $color, $value in $theme-colors { 55 | &.callout-#{$color} { 56 | border-right-color: $value; 57 | } 58 | } 59 | 60 | .chart-wrapper { 61 | left: 0; 62 | float: left; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/templates/pages/admin/edit_user.html: -------------------------------------------------------------------------------- 1 | {% extends "common/dark_base.html" %} {# common/page_base.html extends layout.html #} 2 | {% block breadcrumb %} 3 | 6 | {% endblock %} 7 | {% block content %} 8 | 9 | {% from "common/form_macros.html" import render_field, render_checkbox_field, render_multicheckbox_field %} 10 |
11 |
12 |
13 |
14 |
15 | Edit User 16 |
17 |
18 | {{ form.hidden_tag() }} 19 | {{ render_field(form.first_name) }} 20 | {{ render_field(form.last_name) }} 21 | {{ render_field(form.email) }} 22 | {{ render_field(form.password) }} 23 | {{ render_checkbox_field(form.active) }} 24 | {{ render_multicheckbox_field(form.roles) }} 25 |
26 | 29 |
30 |
31 |
32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /app/static/coreui/scss/_brands-buttons.scss: -------------------------------------------------------------------------------- 1 | // Brands Buttons 2 | 3 | .btn-brand { 4 | border: 0; 5 | i { 6 | display: inline-block; 7 | width: (($btn-padding-y * 2) + ($font-size-base * $btn-line-height)); 8 | margin: (- $btn-padding-y) (- $btn-padding-x); 9 | line-height: (($btn-padding-y * 2) + ($font-size-base * $btn-line-height)); 10 | text-align: center; 11 | background-color: rgba(0, 0, 0, .2); 12 | } 13 | 14 | i + span { 15 | margin-left: ($btn-padding-x * 2); 16 | } 17 | 18 | &.btn-lg { 19 | i { 20 | width: (($btn-padding-y-lg * 2) + ($font-size-lg * $btn-line-height-lg)); 21 | margin: (- $btn-padding-y-lg) (- $btn-padding-x-lg); 22 | line-height: (($btn-padding-y-lg * 2) + ($font-size-lg * $btn-line-height-lg)); 23 | } 24 | 25 | i + span { 26 | margin-left: ($btn-padding-x-lg * 2); 27 | } 28 | } 29 | 30 | &.btn-sm { 31 | i { 32 | width: (($btn-padding-y-sm * 2) + ($font-size-sm * $btn-line-height-sm)); 33 | margin: (- $btn-padding-y-sm) (- $btn-padding-x-sm); 34 | line-height: (($btn-padding-y-sm * 2) + ($font-size-sm * $btn-line-height-sm)); 35 | } 36 | 37 | i + span { 38 | margin-left: ($btn-padding-x-sm * 2); 39 | } 40 | } 41 | } 42 | 43 | @each $color, $value in $brands-colors { 44 | .btn-#{$color} { 45 | @include button-variant($value, $value); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | SHELL:=/bin/bash 2 | ROOT_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) 3 | APP_NAME:=app 4 | PYTHON:=python3 5 | 6 | all: dependencies 7 | 8 | fresh: clean dependencies 9 | 10 | testenv: clean_testenv 11 | docker-compose up --build 12 | 13 | clean_testenv: 14 | docker-compose down 15 | 16 | fresh_testenv: clean_testenv testenv 17 | 18 | venv: 19 | if [ ! -d $(ROOT_DIR)/env ]; then $(PYTHON) -m venv $(ROOT_DIR)/env; fi 20 | 21 | dependencies: venv 22 | source $(ROOT_DIR)/env/bin/activate; yes w | python -m pip install -r $(ROOT_DIR)/requirements.txt 23 | 24 | upgrade_dependencies: venv 25 | source $(ROOT_DIR)/env/bin/activate; ./bin/update_dependencies.sh $(ROOT_DIR)/requirements.txt 26 | 27 | clean: clean_testenv 28 | # Remove existing environment 29 | rm -rf $(ROOT_DIR)/env; 30 | rm -rf $(ROOT_DIR)/$(APP_NAME)/*.pyc; 31 | 32 | upgrade_models: 33 | rm -rf $(ROOT_DIR)/app.sqlite; 34 | source $(ROOT_DIR)/env/bin/activate; python manage.py db upgrade 35 | source $(ROOT_DIR)/env/bin/activate; python manage.py db migrate 36 | rm -rf $(ROOT_DIR)/app.sqlite; 37 | 38 | init_db: dependencies 39 | source $(ROOT_DIR)/env/bin/activate; SECRET_KEY=TempKey python manage.py db init 40 | source $(ROOT_DIR)/env/bin/activate; SECRET_KEY=TempKey python manage.py db upgrade 41 | source $(ROOT_DIR)/env/bin/activate; SECRET_KEY=TempKey python manage.py db migrate 42 | rm -rf $(ROOT_DIR)/app.sqlite; 43 | -------------------------------------------------------------------------------- /app/templates/apikeys/list.html: -------------------------------------------------------------------------------- 1 | {% extends "common/dark_base.html" %} {# common/page_base.html extends layout.html #} 2 | {% block breadcrumb %} 3 | 6 | {% endblock %} 7 | {% block content %} 8 |
9 |
10 | API Keys 11 | Create Key 12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {% for key in keys %} 24 | 25 | 26 | 27 | 32 | 33 | {% endfor %} 34 | 35 |
IDLabelControl
{{ key.id }}{{ key.label }} 28 |
29 | 30 |
31 |
36 |
37 | 39 |
40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /app/templates/pages/user_profile_page.html: -------------------------------------------------------------------------------- 1 | {% extends "common/dark_base.html" %} {# common/page_base.html extends layout.html #} 2 | {% block breadcrumb %} 3 | {% endblock %} 4 | {% block content %} 5 | {% from "common/form_macros.html" import render_field, render_submit_field %} 6 |
7 |
8 |
9 | User Profile 10 |
11 |
12 | {{ form.hidden_tag() }} 13 |
14 | 15 | 16 |
17 |
18 | 19 | 20 |
21 |
22 | 26 |
27 |
28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /app/static/coreui/scss/coreui.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * CoreUI - Open Source Dashboard UI Kit 3 | * @version v2.0.0-beta.2 4 | * @link https://coreui.io 5 | * Copyright (c) 2018 creativeLabs Łukasz Holeczek 6 | * Licensed under MIT (https://coreui.io/license) 7 | */ 8 | 9 | // Override Boostrap variables 10 | @import "bootstrap-variables"; 11 | 12 | // Import Bootstrap source files 13 | @import "node_modules/bootstrap/scss/bootstrap"; 14 | 15 | // Import core styles 16 | @import "variables"; 17 | @import "mixins"; 18 | 19 | // Animations 20 | @import "animate"; 21 | 22 | // Vendors 23 | @import "vendors"; 24 | 25 | // Components 26 | @import "aside"; 27 | @import "avatars"; 28 | @import "badge"; 29 | @import "breadcrumb-menu"; 30 | @import "breadcrumb"; 31 | @import "brand-card"; 32 | @import "brands-buttons"; 33 | @import "buttons"; 34 | @import "callout"; 35 | @import "card"; 36 | @import "charts"; 37 | @import "dropdown"; 38 | @import "footer"; 39 | @import "grid"; 40 | @import "input-group"; 41 | @import "loading"; 42 | @import "list-group"; 43 | @import "modal"; 44 | @import "nav"; 45 | @import "navbar"; 46 | @import "progress"; 47 | @import "progress-group"; 48 | @import "sidebar"; 49 | @import "switches"; 50 | @import "tables"; 51 | @import "widgets"; 52 | 53 | // Layout Options 54 | @import "layout"; 55 | 56 | @import "others"; 57 | 58 | // Utility classes 59 | @import "utilities"; 60 | 61 | // Right-to-left 62 | @import "rtl"; 63 | -------------------------------------------------------------------------------- /app/static/coreui/scss/_brand-card.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Base styles 3 | // 4 | 5 | .brand-card { 6 | position: relative; 7 | display: flex; 8 | flex-direction: column; 9 | min-width: 0; 10 | margin-bottom: ($spacer * 1.5); 11 | word-wrap: break-word; 12 | background-color: $card-bg; 13 | background-clip: border-box; 14 | border: $card-border-width solid $card-border-color; 15 | @include border-radius($card-border-radius); 16 | } 17 | 18 | .brand-card-header { 19 | position: relative; 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | height: 6rem; 24 | @include border-radius($card-border-radius $card-border-radius 0 0); 25 | 26 | i { 27 | font-size: 2rem; 28 | color: #fff; 29 | } 30 | 31 | .chart-wrapper { 32 | position: absolute; 33 | top: 0; 34 | width: 100%; 35 | height: 100%; 36 | } 37 | } 38 | 39 | .brand-card-body { 40 | display: flex; 41 | flex-direction: row; 42 | padding: $card-spacer-y 0; 43 | text-align: center; 44 | 45 | > * { 46 | flex: 1; 47 | padding: ($card-spacer-y * .25) 0; 48 | } 49 | 50 | > *:not(:last-child) { 51 | border-right: 1px solid $border-color; 52 | } 53 | } 54 | 55 | // stylelint-disable selector-max-universal 56 | // Right-to-Left Support 57 | *[dir="rtl"] { 58 | .brand-card-body { 59 | > *:not(:last-child) { 60 | border-right: 0; 61 | border-left: 1px solid $border-color; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | BSD 2-Clause "Simplified" License 2 | 3 | Copyright (c) 2013, Ling Thio and contributors. 4 | Copyright (c) 2018, Robert Hafner and contributors. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /app/commands/user.py: -------------------------------------------------------------------------------- 1 | # This file defines command line commands for manage.py 2 | # 3 | # Copyright 2014 SolidBuilds.com. All rights reserved 4 | # 5 | # Authors: Ling Thio 6 | 7 | import datetime 8 | 9 | from flask import current_app 10 | 11 | from app import db 12 | from app.models.user_models import User, Role 13 | 14 | 15 | def find_or_create_role(name, label): 16 | """ Find existing role or create new role """ 17 | role = Role.query.filter(Role.name == name).first() 18 | if not role: 19 | role = Role(name=name, label=label) 20 | db.session.add(role) 21 | db.session.commit() 22 | return role 23 | 24 | 25 | def find_or_create_user(first_name, last_name, username, email, password, role=None): 26 | """ Find existing user or create new user """ 27 | if username: 28 | user = User.query.filter(User.username == username).first() 29 | else: 30 | user = User.query.filter(User.email == email).first() 31 | 32 | if not user: 33 | user = User(email=email, 34 | first_name=first_name, 35 | last_name=last_name, 36 | password=current_app.user_manager.hash_password(password), 37 | active=True, 38 | email_confirmed_at=datetime.datetime.utcnow()) 39 | if role: 40 | user.roles.append(role) 41 | db.session.add(user) 42 | db.session.commit() 43 | return user 44 | -------------------------------------------------------------------------------- /app/static/coreui/scss/coreui-standalone.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * CoreUI - Open Source Dashboard UI Kit 3 | * @version v2.0.0-beta.2 4 | * @link https://coreui.io 5 | * Copyright (c) 2018 creativeLabs Łukasz Holeczek 6 | * Licensed under MIT (https://coreui.io/license) 7 | */ 8 | 9 | // Override Boostrap variables 10 | @import "bootstrap-variables"; 11 | 12 | // Import Bootstrap source files 13 | @import "node_modules/bootstrap/scss/functions"; 14 | @import "node_modules/bootstrap/scss/variables"; 15 | @import "node_modules/bootstrap/scss/mixins"; 16 | 17 | // Import core styles 18 | @import "variables"; 19 | @import "mixins"; 20 | 21 | // Animations 22 | @import "animate"; 23 | 24 | // Vendors 25 | @import "vendors"; 26 | 27 | // Components 28 | @import "aside"; 29 | @import "avatars"; 30 | @import "badge"; 31 | @import "breadcrumb-menu"; 32 | @import "breadcrumb"; 33 | @import "brand-card"; 34 | @import "brands-buttons"; 35 | @import "buttons"; 36 | @import "callout"; 37 | @import "card"; 38 | @import "charts"; 39 | @import "dropdown"; 40 | @import "footer"; 41 | @import "grid"; 42 | @import "input-group"; 43 | @import "loading"; 44 | @import "modal"; 45 | @import "nav"; 46 | @import "navbar"; 47 | @import "progress"; 48 | @import "progress-group"; 49 | @import "sidebar"; 50 | @import "switches"; 51 | @import "tables"; 52 | @import "widgets"; 53 | 54 | // Layout Options 55 | @import "layout"; 56 | 57 | @import "others"; 58 | 59 | // Utility classes 60 | @import "utilities"; 61 | 62 | // Right-to-left 63 | @import "rtl"; 64 | -------------------------------------------------------------------------------- /app/templates/pages/admin/create_user.html: -------------------------------------------------------------------------------- 1 | {% extends "common/dark_base.html" %} {# common/page_base.html extends layout.html #} 2 | {% block breadcrumb %} 3 | 6 | 9 | {% endblock %} 10 | {% block content %} 11 | 12 | {% from "common/form_macros.html" import render_field, render_checkbox_field, render_multicheckbox_field %} 13 |
14 |
15 |
16 |
17 |
18 | Create User 19 |
20 |
21 | {{ form.hidden_tag() }} 22 | {{ render_field(form.first_name) }} 23 | {{ render_field(form.last_name) }} 24 | {{ render_field(form.email) }} 25 | {{ render_field(form.password) }} 26 | {{ render_checkbox_field(form.active) }} 27 | {{ render_multicheckbox_field(form.roles) }} 28 |
29 | 32 |
33 |
34 |
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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | {{ form.hidden_tag() }} 13 |

Forgot your password?

14 |

We'll send you a reminder.

15 |
16 |
17 | 18 | 19 | 20 |
21 | 22 |
23 | 24 |
25 |
26 | 33 |
34 |
35 |
36 |
37 |
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 | 6 | {% endblock %} 7 | {% block content %} 8 |
9 |
10 | Users 11 | Create User 12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% if not config.get('USER_LDAP', False) %} 23 | 24 | {% endif %} 25 | 26 | 27 | 28 | {% for user in users %} 29 | 30 | 31 | 32 | 33 | 34 | 35 | {% if not config.get('USER_LDAP', False) %} 36 | 44 | {% endif %} 45 | 46 | {% endfor %} 47 | 48 |
EmailUsernameNameRoleConfirmedActions
{{user.email}}{{user.username}}{{user.name()}}{% for role in user.roles %}{{ role.name }}{{ ", " if not loop.last }}{% endfor %}{{user.email_confirmed_at}} 37 |
38 | 39 |
40 |
41 | 42 |
43 |
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 |
3 | {%- if field.type != 'HiddenField' and label_visible -%} 4 | {%- if not label -%}{% set label=field.label.text %}{%- endif %} 5 | 6 | {%- endif %} 7 | {{ field(class_='form-control', **kwargs) }} 8 | {%- if field.errors -%} 9 | {% for e in field.errors %} 10 |

{{ e }}

11 | {% endfor %} 12 | {%- endif %} 13 |
14 | {%- endmacro %} 15 | 16 | {% macro render_multicheckbox_field(field, label=None, label_visible=true) -%} 17 |
18 | {%- if field.type != 'HiddenField' and label_visible -%} 19 | {%- if not label -%}{% set label=field.label.text %}{%- endif %} 20 | 21 | {%- endif %} 22 | {% for value, label, checked in field.iter_choices() %} 23 |
24 | 28 |
29 | {% endfor %} 30 |
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 |
46 | 47 | {%- if field.type != 'HiddenField' and label_visible -%} 48 | {%- if not label -%}{% set label=field.label.text %}{%- endif %} 49 | 50 | {%- endif %} 51 | 52 | {% for value, label, checked in field.iter_choices() %} 53 |
54 | 58 |
59 | {% endfor %} 60 | 61 |
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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | {{ form.hidden_tag() }} 13 |

Register

14 |

Create your account

15 |
16 |
17 | 18 | 19 | 20 |
21 | 22 |
23 |
24 |
25 | 26 | 27 | 28 |
29 | 30 |
31 | 32 |
33 |
34 | @ 35 |
36 | 37 |
38 | 39 |
40 |
41 | 42 | 43 | 44 |
45 | 46 |
47 | 48 |
49 |
50 | 51 | 52 | 53 |
54 | 55 |
56 | 57 |
58 |
59 | 67 |
68 |
69 |
70 |
71 |
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 |
10 |
11 |
12 | 13 | 14 | {{ form.hidden_tag() }} 15 | {# One-time system messages called Flash messages #} 16 | {% block flash_messages %} 17 | {%- with messages = get_flashed_messages(with_categories=true) -%} 18 | {% if messages %} 19 | {% for category, message in messages %} 20 | {% if category=='error' %} 21 | {% set category='danger' %} 22 | {% endif %} 23 |

{{ message|safe }}

24 | {% endfor %} 25 | {% endif %} 26 | {%- endwith %} 27 | {% endblock %} 28 |

Login

29 |

Sign In to your account

30 |
31 |
32 | 33 | 34 | 35 |
36 | {% if config['USER_ENABLE_USERNAME'] %} 37 | 38 | {% else %} 39 | 40 | {% endif %} 41 |
42 |
43 |
44 | 45 | 46 | 47 |
48 | 49 |
50 |
51 |
52 | 53 |
54 |
55 | Forgot password? 56 |
57 |
58 |
59 |
60 |
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 [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social&logo=twitter)](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 | | [![CoreUI Pro Admin Template](https://coreui.io/assets/img/example-coureui.jpg)](https://coreui.io/pro/) | [![Prime Admin Template](https://genesisui.com/assets/img/templates/prime1280.jpg)](https://genesisui.com/admin-templates/bootstrap/prime/?support=1) | [![Root Admin Template](https://genesisui.com/assets/img/templates/root1280.jpg)](https://genesisui.com/admin-templates/bootstrap/root/?support=1) | [![Alba Admin Template](https://genesisui.com/assets/img/templates/alba1280.jpg)](https://genesisui.com/admin-templates/bootstrap/alba/?support=1) | [![Leaf Admin Template](https://genesisui.com/assets/img/templates/leaf1280.jpg)](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 | ![Screenshot](https://github.com/twintechlabs/flaskdash/blob/master/app/static/images/screenshot.png) 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 | --------------------------------------------------------------------------------