├── .github └── workflows │ └── docker-image.yml ├── .gitignore ├── Dockerfile ├── LICENSE.md ├── Procfile ├── README.md ├── app ├── __init__.py ├── auth │ ├── __init__.py │ ├── email.py │ ├── forms.py │ ├── models.py │ └── routes.py ├── config.py ├── main │ ├── __init__.py │ └── routes.py ├── setup_security.py ├── static │ ├── css │ │ ├── bootstrap.min.css │ │ ├── bootstrap.min.css.map │ │ ├── password.css │ │ ├── password.min.css │ │ ├── passwordstrength.jpg │ │ └── style.css │ ├── custom_css │ │ └── style.scss │ └── js │ │ ├── bootstrap.min.js │ │ ├── jquery.min.js │ │ ├── password.js │ │ └── register_ps_check.js ├── templates │ ├── auth │ │ ├── login.html │ │ ├── register.html │ │ ├── reset_password.html │ │ └── reset_password_request.html │ ├── base.html │ ├── bootstrap │ │ └── bs5_base.html │ ├── email │ │ ├── reset_password.html │ │ └── reset_password.txt │ ├── forms │ │ └── forms.html │ ├── main │ │ └── index.html │ └── navbar │ │ ├── messages.html │ │ └── navbar.html └── utils │ ├── __init__.py │ ├── app_logger.py │ ├── decorators.py │ └── mailer.py ├── docker-compose.yml ├── env.sample ├── nginx ├── Dockerfile └── sites-enabled │ └── app.conf ├── requirements.txt ├── run.py ├── test.py └── uwsgi_config.ini /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | #name: Deploy 2 | # 3 | #on: 4 | # push: 5 | # branches: 6 | # - main 7 | # 8 | #jobs: 9 | # build: 10 | # runs-on: ubuntu-latest 11 | # steps: 12 | # - name: Checkout 13 | # uses: actions/checkout@v2 14 | # - name: Build and push Docker to Heroku 15 | # env: 16 | # HEROKU_APP_NAME: ${{ secrets.HEROKU_APP_NAME }} 17 | # DOCKERFILE_DIRECTORY: "." 18 | # HEROKU_EMAIL: ${{ secrets.HEROKU_EMAIL }} 19 | # HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }} 20 | # DOCKERFILE_NAME: "Dockerfile" 21 | # DOCKER_OPTIONS: "--no-cache" 22 | # run: | 23 | # cd ${DOCKERFILE_DIRECTORY} 24 | # echo ${HEROKU_API_KEY} | docker login \ 25 | # --username=${HEROKU_EMAIL} \ 26 | # registry.heroku.com \ 27 | # --password-stdin 28 | # docker build \ 29 | # --file ${DOCKERFILE_NAME} \ 30 | # ${DOCKER_OPTIONS} \ 31 | # --tag registry.heroku.com/${HEROKU_APP_NAME}/web . 32 | # heroku container:push web --app ${HEROKU_APP_NAME} 33 | # heroku container:release web --app ${HEROKU_APP_NAME} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .env 3 | *.pyc 4 | flask_session 5 | app/dev.db 6 | logs.log 7 | app/static/.webassets-cache -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10.4-buster 2 | RUN apt-get update 3 | RUN apt-get install -y --no-install-recommends libatlas-base-dev gfortran 4 | LABEL maintainer="Chirilov Adrian" 5 | 6 | COPY . / 7 | WORKDIR / 8 | RUN pip install -r requirements.txt 9 | CMD [ "uwsgi", "--socket", "0.0.0.0:5000", "--protocol", "http", "--wsgi", "run:app" ] -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 App Generator 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: uwsgi uwsgi_config.ini -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask Best Practices 2 | 3 | A simple `Flask` codebase that provides best practices for a secure production deployment. 4 | 5 | > Status: **WIP** (not stable) 6 | 7 |
8 |
9 | 10 | > Checklist 11 | 12 | | Status | Item | info | 13 | | --- | --- | --- | 14 | | ✔️ | `Up-to-date Dependencies` | - | 15 | | ✔️ | Flask-Login, Flask-SqlAlchemy | - | 16 | | ✔️ | BS5 for styling | Local path (latest BS5 stable version) | 17 | | ✔️ | Simple Custom Login / Register pages | - | 18 | | ✔️ | Password Recovery | - | 19 | | ✔️ | Unitary tests | - | 20 | | ✔️ | SCSS to CSS compilation | via pyScss | 21 | | ✔️ | Rate Limiter for Login & Register | via [Flask-RateLimiter](https://pypi.org/project/Flask-RateLimiter/) | 22 | | ✔️ | [Flask-Talisman](https://pypi.org/project/flask-talisman/) | Default policy | 23 | | ✔️ | Passwords Checks | Configurable Min/Max Lenght, Strength WIP | 24 | | ✔️ | Check email is valid & exists | via [validate-email-address](https://pypi.org/project/validate-email-address/) package | 25 | | ✔️ | Failed Logins Count | - | 26 | | ❌ | Account Suspension for X failed logins | Limit in Config | 27 | | ✔️ | Page Compression | via [Flask-Minify](https://pypi.org/project/Flask-Minify/) | 28 | | ✔️ | Deployment | [uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/) / Nginx (reverse proxy) | 29 | | ✔️ | HEROKU integration | TODO add secrets to git repo | 30 | | ✔️ | Docker | - | 31 | 32 |
33 | 34 | ## ✨ Build from sources 35 | 36 | ```bash 37 | $ # Clone the sources 38 | $ git clone https://github.com/app-generator/sample-flask-best-practices.git 39 | $ cd sample-flask-best-practices 40 | $ 41 | $ # Virtualenv modules installation (Unix based systems) 42 | $ virtualenv env 43 | $ source env/bin/activate 44 | $ 45 | $ # Virtualenv modules installation (Windows based systems) 46 | $ # virtualenv env 47 | $ # .\env\Scripts\activate 48 | $ 49 | $ # Install requirements 50 | $ pip3 install -r requirements.txt 51 | $ 52 | $ # Set the FLASK_APP environment variable 53 | $ (Unix/Mac) export FLASK_APP=run.py 54 | $ (Windows) set FLASK_APP=run.py 55 | $ (Powershell) $env:FLASK_APP = ".\run.py" 56 | $ 57 | $ # Set up the DEBUG environment 58 | $ # (Unix/Mac) export FLASK_ENV=development 59 | $ # (Windows) set FLASK_ENV=development 60 | $ # (Powershell) $env:FLASK_ENV = "development" 61 | $ 62 | $ # Run the application 63 | $ # --host=0.0.0.0 - expose the app on all network interfaces (default 127.0.0.1) 64 | $ # --port=5000 - specify the app port (default 5000) 65 | $ flask run --host=0.0.0.0 --port=5000 66 | $ 67 | $ # Access the app in browser: http://127.0.0.1:5000/ 68 | ``` 69 | 70 | > Note: To use the app, please access the registration page and create a new user. After authentication, the app will unlock the private pages. 71 | 72 |
73 | 74 | ## ✨ Build from docker 75 | ```bash 76 | $ # Run using docker-compose 77 | $ docker-compose build # This will build the containers 78 | $ docker-compose up # This will bring up the application 79 | $ # Open the application on you browser using you IPv4 Address on your browser i.e., only 2 routes available for auth /login ,/register 80 | $ # To find IPv4 Address 81 | $ # (Windows) ipconfig 82 | $ # (Unix/Mac) ifconfig 83 | ``` 84 | 85 | ## ✨ Deploy on github workflow 86 | ```bash 87 | $ # First set the git secrets using this https://docs.github.com/en/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository 88 | $ # Add the following keys to the secrets : HEROKU_APP_NAME , HEROKU_EMAIL , HEROKU_API_KEY 89 | $ # In the Dockerfile comment the line `CMD [ "uwsgi", "--socket", "0.0.0.0:5000", "--protocol", "http", "--wsgi", "run:app" ]` 90 | $ # Use the direct flask application run by uncommenting/adding the line `CMD [ "python", "run.py"]` 91 | $ # on push to the master/main the GH workflow will automatically create and push the image to your heroku 92 | ``` 93 | 94 | ## ✨ Code-base structure 95 | The project has a super simple structure, represented as below: 96 | ``` 97 | < PROJECT ROOT > 98 | | 99 | |-- app/ 100 | | | 101 | | |-- __init__.py # Initialization of app 102 | | |-- config.py # Handlers for the front end routes 103 | | |-- setup_security.py 104 | | |-- auth/ 105 | | | 106 | | | |-- __init__.py 107 | | | |-- email.py 108 | | | |-- forms.py 109 | | | |-- models.py # Database models for storing data 110 | | | |-- routes.py # REST API hanlder 111 | | | 112 | | |-- static/ # CSS files, Javascripts files 113 | | | 114 | | | |-- css/ 115 | | | | 116 | | | | |-- bootstrap.min.css 117 | | | | |-- bootstrap.min.css.map 118 | | | | |-- style.css 119 | | | | 120 | | | |-- js/ 121 | | | | 122 | | | | |-- bootstrap.min.js 123 | | | | |-- jquery.min.js 124 | | | | 125 | | |-- templates/ 126 | | | 127 | | | |-- auth/ # Auth related pages login/register 128 | | | | 129 | | | | |-- login.html 130 | | | | |-- register.html 131 | | | | |-- reset_password.html 132 | | | | |-- reset_password_request.html 133 | | | | 134 | | | |-- bootstrap/ 135 | | | | 136 | | | | |-- bs5_base.html 137 | | | | 138 | | | |-- email/ 139 | | | | 140 | | | | |-- reset_password.html 141 | | | | |-- reset_password.txt 142 | | | | 143 | | | |-- forms/ 144 | | | | 145 | | | | |-- forms.html 146 | | | | 147 | | | |-- navbar/ 148 | | | | 149 | | | | |-- messages.html 150 | | | | |-- navbar.html 151 | | | |-- base.html 152 | | | 153 | | |-- utils/ 154 | | | 155 | | | |-- __init__.py 156 | | | |-- app_logger.py 157 | | | |-- decorators.py 158 | | | |-- mailer.py 159 | | | 160 | |-- requirements.txt 161 | |-- run.py 162 | | 163 | |-- ************************************************************************ 164 | ``` 165 | 166 | > **@ToDo** 167 | 168 |
169 | 170 | ## ✨ Recompile CSS 171 | 172 | To recompile SCSS files, follow this setup: 173 | 174 | > **@ToDo** 175 | 176 |
177 | 178 | --- 179 | **Flask Best Practices** - Provided by AppSeed 180 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_mail import Mail 3 | from app.config import config 4 | from flask_minify import Minify 5 | from flask_bcrypt import Bcrypt 6 | from flask_session import Session 7 | from flask_login import LoginManager 8 | from flask_bootstrap import Bootstrap5 9 | from flask_wtf.csrf import CSRFProtect 10 | from flask_sqlalchemy import SQLAlchemy 11 | from flask_assets import Environment, Bundle 12 | from app.setup_security import setup_security_measure_on_application 13 | 14 | # Setup Bootstrap5 on app 15 | bootstrap = Bootstrap5() 16 | 17 | # Set up Brycpt on the app 18 | bcrypt = Bcrypt() 19 | 20 | # Set up SQL alchemy 21 | db = SQLAlchemy() 22 | 23 | # Set up Flask-login 24 | login_manager = LoginManager() 25 | login_manager.session_protection = 'strong' 26 | login_manager.login_view = 'auth.login' 27 | 28 | # Setup CSRF 29 | csrf = CSRFProtect() 30 | 31 | # Security Measures dict initially None 32 | security = None 33 | 34 | # Setup mailer 35 | mail = Mail() 36 | 37 | def create_app(config_name): 38 | """For to use dynamic environment""" 39 | global security 40 | app = Flask(__name__) 41 | assets = Environment(app) 42 | app.config.from_object(config[config_name]) 43 | 44 | bcrypt.init_app(app) 45 | security = setup_security_measure_on_application(app) 46 | db.init_app(app) 47 | login_manager.init_app(app) 48 | bootstrap.init_app(app) 49 | csrf.init_app(app) 50 | mail.init_app(app) 51 | Minify(app=app, html=True, js=True, cssless=True) 52 | Session(app) 53 | scss = Bundle('custom_css/style.scss', 54 | filters='pyscss', 55 | output='css/packed.css') 56 | assets.register('scss_all', scss) 57 | 58 | from app.main.routes import main 59 | app.register_blueprint(main) 60 | 61 | from app.auth.routes import auth 62 | app.register_blueprint(auth) 63 | 64 | return app 65 | -------------------------------------------------------------------------------- /app/auth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/sample-flask-best-practices/28465e874848f0907c030c73510754fd1f0fd469/app/auth/__init__.py -------------------------------------------------------------------------------- /app/auth/email.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, current_app 2 | from app.utils.mailer import send_email 3 | 4 | 5 | def send_password_reset_email(user): 6 | token = user.get_reset_password_token() 7 | send_email('Reset Your Password', 8 | sender=current_app.config['ADMINS'][0], 9 | recipients=[user.email], 10 | text_body=render_template('email/reset_password.txt', 11 | user=user, token=token), 12 | html_body=render_template('email/reset_password.html', 13 | user=user, token=token)) -------------------------------------------------------------------------------- /app/auth/forms.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import StringField, PasswordField, BooleanField, SubmitField 3 | from wtforms.validators import DataRequired, Email, EqualTo, ValidationError 4 | 5 | from app.auth.models import User 6 | 7 | 8 | class LoginForm(FlaskForm): 9 | username = StringField('Username', validators=[DataRequired()]) 10 | password = PasswordField('Password', validators=[DataRequired()]) 11 | remember = BooleanField('Remember me') 12 | submit = SubmitField('Log In Now') 13 | 14 | def validate_username(self, username): 15 | user = User.query.filter_by(username=username.data).first() 16 | if user is None: 17 | raise ValidationError('User does not exist') 18 | 19 | 20 | class RegisterForm(FlaskForm): 21 | username = StringField('Username', validators=[DataRequired()]) 22 | email = StringField('Email', validators=[DataRequired(), Email()]) 23 | password = PasswordField('Password', validators=[DataRequired()]) 24 | confirm = PasswordField('Repeat Password', validators=[DataRequired(), EqualTo('password')]) 25 | submit = SubmitField('Register Now') 26 | 27 | def validate_username(self, username): 28 | user = User.query.filter_by(username=username.data).first() 29 | if user is not None: 30 | raise ValidationError('Username exists, please pick another one') 31 | 32 | def validate_email(self, email): 33 | user = User.query.filter_by(email=email.data).first() 34 | if user is not None: 35 | raise ValidationError('Email exists, please pick another one') 36 | 37 | class ResetPasswordForm(FlaskForm): 38 | password = PasswordField('Password', validators=[DataRequired()]) 39 | confirm = PasswordField('Repeat Password', validators=[DataRequired(), EqualTo('password')]) 40 | submit = SubmitField('Reset Your Password') 41 | 42 | class ResetPasswordRequestForm(FlaskForm): 43 | email = StringField('Email', validators=[DataRequired(), Email()]) 44 | submit = SubmitField('Send Reset Request') 45 | 46 | def validate_email(self, email): 47 | user = User.query.filter_by(email=email.data).first() 48 | if user is None: 49 | raise ValidationError('Email does not exist') 50 | -------------------------------------------------------------------------------- /app/auth/models.py: -------------------------------------------------------------------------------- 1 | import jwt 2 | from app import db, bcrypt 3 | from time import time 4 | from flask import current_app 5 | from flask_login import UserMixin 6 | from sqlalchemy.orm.exc import NoResultFound 7 | 8 | class User(db.Model, UserMixin): 9 | __tablename__ = 'users' 10 | 11 | id = db.Column(db.Integer, primary_key=True, autoincrement=True) 12 | username = db.Column(db.String(50), nullable=False) 13 | email = db.Column(db.String(128), nullable=False, unique=True) 14 | password = db.Column(db.String(54), nullable=False) 15 | active = db.Column(db.SmallInteger, nullable=False, default=True) 16 | 17 | created_at = db.Column(db.DateTime, default=db.func.now()) 18 | updated_at = db.Column(db.DateTime, onupdate=db.func.now()) 19 | suspended = db.Column(db.Boolean, default=False) 20 | 21 | def __init__(self, username, email, password): 22 | self.username = username 23 | self.email = email 24 | self.set_password(password) 25 | 26 | def __repr__(self): 27 | return '' % self.username 28 | 29 | def set_password(self, password): 30 | self.password = bcrypt.generate_password_hash(password) 31 | 32 | def check_password(self, password): 33 | return bcrypt.check_password_hash(self.password, password) 34 | 35 | def get_reset_password_token(self, expires_in=600): 36 | """ 37 | This function generates a time based expiration jwt that is used in the reset password scenario, 38 | incase if user does not use link in defined time the link would expire 39 | :param expires_in: time in which the jwt expires 40 | :return: web token 41 | """ 42 | return jwt.encode({'reset_password': self.id, 43 | 'exp': time() + expires_in}, 44 | current_app.config['SECRET_KEY'], 45 | algorithm='HS256').decode('utf-8') 46 | 47 | @classmethod 48 | def get(cls, user_id): 49 | """ 50 | :rtype: object 51 | :type user_id: int 52 | """ 53 | try: 54 | return User.query.filter_by(id=user_id).one() 55 | except NoResultFound: 56 | return None -------------------------------------------------------------------------------- /app/auth/routes.py: -------------------------------------------------------------------------------- 1 | from is_safe_url import is_safe_url 2 | from flask_login import login_user, logout_user, current_user 3 | from flask import Blueprint, render_template, request, flash, redirect, url_for, abort, session 4 | from app import bcrypt, db, security, login_manager, config 5 | 6 | from app.auth.models import User 7 | from flask_talisman import Talisman, ALLOW_FROM 8 | from validate_email_address import validate_email 9 | from app.auth.email import send_password_reset_email 10 | from app.auth.forms import LoginForm, RegisterForm, ResetPasswordRequestForm, ResetPasswordForm 11 | from app.utils import _log_message_ 12 | 13 | auth = Blueprint('auth', __name__) 14 | 15 | @login_manager.user_loader 16 | def load_user(user_id): 17 | try: 18 | return User.query.get(user_id) 19 | except: 20 | return None 21 | 22 | @auth.route('/login', 23 | methods=['GET', 'POST']) 24 | # @security["talisman"](frame_options=ALLOW_FROM, 25 | # frame_options_allow_from='*') 26 | def login(): 27 | """ 28 | This is the login route corresponding to the `/login` route, 29 | the `login_manager.user_loader` loads the current user into the function, 30 | also as security measure are the talisman/rate-limiter on the end-points in place for more info: 31 | https://flask-limiter.readthedocs.io/en/stable/ 32 | https://github.com/GoogleCloudPlatform/flask-talisman 33 | :return: Logged in page rendered on the specified template `templates/auth/login.html` 34 | """ 35 | if current_user.is_authenticated: 36 | return redirect(url_for('main.index')) 37 | form = LoginForm(request.form) 38 | attempt = session.get('attempt',None) 39 | if form.validate_on_submit(): 40 | username = form.username.data 41 | password = form.password.data 42 | remember = form.remember.data 43 | user = User.query.filter_by(username=username).first() 44 | if user.check_password(password): 45 | login_user(user, remember=remember) 46 | next_ = request.args.get('next') 47 | if not is_safe_url(next_,allowed_hosts='*'): 48 | return abort(400) 49 | else: 50 | return redirect(next_ or url_for('main.index')) 51 | else: 52 | if (attempt == 0) or (attempt is None): 53 | session['attempt'] = 5 54 | else: 55 | attempt -= 1 56 | session['attempt'] = attempt 57 | if attempt == 1: 58 | client_ip = request.remote_addr 59 | flash('This is your last attempt, %s will be blocked for 24hr, Attempt %d of 5' % ( 60 | client_ip, attempt), 'error') 61 | flash('Password incorrect', category='danger') 62 | return render_template('auth/login.html', form=form) 63 | 64 | 65 | @auth.route('/logout') 66 | def logout(): 67 | logout_user() 68 | flash('You have logged out now.', category='info') 69 | return redirect(url_for('auth.login')) 70 | 71 | 72 | @auth.route('/register', 73 | methods=['GET', 'POST']) 74 | # @security["talisman"](frame_options=ALLOW_FROM, 75 | # frame_options_allow_from='*') 76 | def register(): 77 | """ 78 | This is the register route corresponding to the `/register` route, 79 | this function creates the new user and creates them in the specified db configuration 80 | also as security measure are the talisman/rate-limiter on the end-points in place for more info: 81 | https://flask-limiter.readthedocs.io/en/stable/ 82 | https://github.com/GoogleCloudPlatform/flask-talisman 83 | :return: Logged in page rendered on the specified template `templates/auth/register.html` 84 | """ 85 | if current_user.is_authenticated: 86 | return redirect(url_for('main.index')) 87 | form = RegisterForm(request.form) 88 | if form.validate_on_submit(): 89 | username = form.username.data 90 | password = form.password.data 91 | email = form.email.data 92 | user = User(username=username, 93 | password=password, 94 | email=email) 95 | if validate_email(email):#, verify=True 96 | db.session.add(user) 97 | db.session.commit() 98 | else: 99 | flash('Email is not valid or does not exists.', category='danger') 100 | return redirect(url_for('auth.register')) 101 | flash('Congrats, register success. You can log in now.', category='info') 102 | return redirect(url_for('main.index')) 103 | return render_template('auth/register.html', 104 | form=form, 105 | min=config["base"].PASSWORD_CHECKER_MIN, 106 | max=config["base"].PASSWORD_CHECKER_MAX) 107 | 108 | @auth.route('/reset_password/', methods=['GET', 'POST']) 109 | # @security["talisman"](frame_options=ALLOW_FROM, 110 | # frame_options_allow_from='*') 111 | def reset_password(token): 112 | if current_user.is_authenticated: 113 | return redirect(url_for('main.index')) 114 | form = ResetPasswordForm(request.form) 115 | if form.validate_on_submit(): 116 | user = User.verify_reset_password_token(token) 117 | user.password = bcrypt.generate_password_hash(form.password.data) 118 | db.session.commit() 119 | flash('Your password has been reset.') 120 | return redirect(url_for('auth.login')) 121 | return render_template('auth/reset_password.html', form=form) 122 | 123 | @auth.route('/send_reset_password_request', methods=['GET', 'POST']) 124 | # @security["talisman"](frame_options=ALLOW_FROM, 125 | # frame_options_allow_from='*') 126 | def reset_password_request(): 127 | if current_user.is_authenticated: 128 | return redirect(url_for('auth.login')) 129 | form = ResetPasswordRequestForm(request.form) 130 | if form.validate_on_submit(): 131 | user = User.query.filter_by(email=form.email.data).first() 132 | if user: 133 | send_password_reset_email(user) 134 | flash('Check your email for the instructions to reset your password') 135 | return redirect(url_for('auth.login')) 136 | return render_template('auth/reset_password_request.html', form=form) 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /app/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # Define the application directory 4 | BASE_DIR = os.path.abspath(os.path.dirname(__file__)) 5 | 6 | 7 | class Config(object): 8 | SECRET_KEY = os.environ.get('SECRET_KEY') or '7soino32noonN@^#iuiuw9' 9 | 10 | DATABASE_CONNECT_OPTIONS = {} 11 | 12 | # Turn off Flask-SQLAlchemy event system 13 | SQLALCHEMY_TRACK_MODIFICATIONS = True 14 | 15 | # Application threads. A common general assumption is 16 | # using 2 per available processor cores - to handle 17 | # incoming requests using one and performing background 18 | # operations using the other. 19 | THREADS_PER_PAGE = 2 20 | 21 | # Enable protection agains *Cross-site Request Forgery (CSRF)* 22 | WTF_CSRF_ENABLED = True 23 | 24 | # Use a secure, unique and absolutely secret key for 25 | # signing the data. 26 | CSRF_SESSION_KEY = os.getenv('CSRF_SESSION_KEY') 27 | 28 | # Secret key for signing cookies 29 | if os.environ.get('SECRET_KEY'): 30 | SECRET_KEY = os.environ.get('SECRET_KEY') 31 | else: 32 | SECRET_KEY = 'SECRET' 33 | 34 | RATE_LIMITER_OPTS = [ '200 per day', '50 per hour'] 35 | 36 | # Mail Configuration 37 | MAIL_SERVER = 'smtp.googlemail.com' 38 | MAIL_PORT = 587 39 | MAIL_USE_TLS = True 40 | MAIL_USERNAME = 'you-will-never-guess@gmail.com' 41 | MAIL_PASSWORD = 'you-will-never-guess' 42 | 43 | # ADMINS 44 | ADMINS = ['admin@gmail.com'] 45 | 46 | # Session API config 47 | SESSION_PERMANENT = False 48 | SESSION_TYPE = "filesystem" 49 | 50 | # Password min/max 51 | PASSWORD_CHECKER_MIN = 2 52 | PASSWORD_CHECKER_MAX = 5 53 | 54 | @staticmethod 55 | def init_app(app): 56 | pass 57 | 58 | 59 | class ProductionConfig(Config): 60 | SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(BASE_DIR, 'app.db') 61 | 62 | 63 | class DevelopmentConfig(Config): 64 | """Statement for enabling the development environment""" 65 | # Define the database - we are working with 66 | SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(BASE_DIR, 'dev.db') 67 | DEBUG = True 68 | 69 | 70 | class APIConfig(Config): 71 | """Statement for enabling the api environment""" 72 | # Define the database - we are working with 73 | SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(BASE_DIR, 'dev.db') 74 | WTF_CSRF_ENABLED = False 75 | 76 | 77 | class TestingConfig(Config): 78 | TESTING = True 79 | WTF_CSRF_ENABLED = False 80 | PRESERVE_CONTEXT_ON_EXCEPTION = False 81 | SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(BASE_DIR, 'test.db') 82 | 83 | 84 | config = { 85 | 'base': Config, 86 | 'development': DevelopmentConfig, 87 | 'testing': TestingConfig, 88 | 'production': ProductionConfig, 89 | 'api': APIConfig, 90 | } -------------------------------------------------------------------------------- /app/main/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/sample-flask-best-practices/28465e874848f0907c030c73510754fd1f0fd469/app/main/__init__.py -------------------------------------------------------------------------------- /app/main/routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template 2 | from flask_login import login_required 3 | 4 | main = Blueprint('main', __name__) 5 | 6 | 7 | @main.route('/') 8 | @main.route('/index') 9 | @login_required 10 | def index(): 11 | return render_template('main/index.html') -------------------------------------------------------------------------------- /app/setup_security.py: -------------------------------------------------------------------------------- 1 | from flask_limiter import Limiter 2 | from flask_talisman import Talisman 3 | from flask_limiter.util import get_remote_address 4 | 5 | def setup_security_measure_on_application(app): 6 | """ 7 | This function sets up the security measure on the flask application 8 | 9 | :param app: The flask app that needs to be protected 10 | :return: all the protection measure applied on the app 11 | """ 12 | return { 13 | "limiter": Limiter(app, 14 | key_func=get_remote_address, 15 | default_limits=app.config['RATE_LIMITER_OPTS'])} 16 | # "talisman": Talisman(app)} 17 | -------------------------------------------------------------------------------- /app/static/css/password.css: -------------------------------------------------------------------------------- 1 | .pass-graybar{height:3px;background-color:#ccc;width:100%;position:relative}.pass-colorbar{height:3px;background-image:url(passwordstrength.jpg);position:absolute;top:0;left:0}.pass-percent,.pass-text{font-size:1em}.pass-percent{margin-right:5px} -------------------------------------------------------------------------------- /app/static/css/password.min.css: -------------------------------------------------------------------------------- 1 | .pass-graybar{height:3px;background-color:#ccc;width:100%;position:relative}.pass-colorbar{height:3px;background-image:url(passwordstrength.jpg);position:absolute;top:0;left:0}.pass-percent,.pass-text{font-size:1em}.pass-percent{margin-right:5px} -------------------------------------------------------------------------------- /app/static/css/passwordstrength.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/sample-flask-best-practices/28465e874848f0907c030c73510754fd1f0fd469/app/static/css/passwordstrength.jpg -------------------------------------------------------------------------------- /app/static/css/style.css: -------------------------------------------------------------------------------- 1 | $navbar-background-color: #274e68; 2 | $alert-message-background-color: lightcoral; 3 | 4 | .navbar-dark{ 5 | background-color: $navbar-background-color; 6 | } 7 | .psswd-vars{ 8 | display: none; 9 | } 10 | .alert{ 11 | background: $alert-message-background-color; 12 | } 13 | -------------------------------------------------------------------------------- /app/static/custom_css/style.scss: -------------------------------------------------------------------------------- 1 | $navbar-background-color: #274e68; 2 | 3 | .navbar-dark{ 4 | background-color: $navbar-background-color; 5 | } 6 | .psswd-vars{ 7 | display: none; 8 | } 9 | -------------------------------------------------------------------------------- /app/static/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v5.0.2 (https://getbootstrap.com/) 3 | * Copyright 2011-2021 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 5 | */ 6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("@popperjs/core")):"function"==typeof define&&define.amd?define(["@popperjs/core"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e(t.Popper)}(this,(function(t){"use strict";function e(t){if(t&&t.__esModule)return t;var e=Object.create(null);return t&&Object.keys(t).forEach((function(s){if("default"!==s){var i=Object.getOwnPropertyDescriptor(t,s);Object.defineProperty(e,s,i.get?i:{enumerable:!0,get:function(){return t[s]}})}})),e.default=t,Object.freeze(e)}var s=e(t);const i={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter(t=>t.matches(e)),parents(t,e){const s=[];let i=t.parentNode;for(;i&&i.nodeType===Node.ELEMENT_NODE&&3!==i.nodeType;)i.matches(e)&&s.push(i),i=i.parentNode;return s},prev(t,e){let s=t.previousElementSibling;for(;s;){if(s.matches(e))return[s];s=s.previousElementSibling}return[]},next(t,e){let s=t.nextElementSibling;for(;s;){if(s.matches(e))return[s];s=s.nextElementSibling}return[]}},n=t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t},o=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let s=t.getAttribute("href");if(!s||!s.includes("#")&&!s.startsWith("."))return null;s.includes("#")&&!s.startsWith("#")&&(s="#"+s.split("#")[1]),e=s&&"#"!==s?s.trim():null}return e},r=t=>{const e=o(t);return e&&document.querySelector(e)?e:null},a=t=>{const e=o(t);return e?document.querySelector(e):null},l=t=>{t.dispatchEvent(new Event("transitionend"))},c=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),h=t=>c(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?i.findOne(t):null,d=(t,e,s)=>{Object.keys(s).forEach(i=>{const n=s[i],o=e[i],r=o&&c(o)?"element":null==(a=o)?""+a:{}.toString.call(a).match(/\s([a-z]+)/i)[1].toLowerCase();var a;if(!new RegExp(n).test(r))throw new TypeError(`${t.toUpperCase()}: Option "${i}" provided type "${r}" but expected type "${n}".`)})},u=t=>!(!c(t)||0===t.getClientRects().length)&&"visible"===getComputedStyle(t).getPropertyValue("visibility"),g=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),p=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?p(t.parentNode):null},f=()=>{},m=t=>t.offsetHeight,_=()=>{const{jQuery:t}=window;return t&&!document.body.hasAttribute("data-bs-no-jquery")?t:null},b=[],v=()=>"rtl"===document.documentElement.dir,y=t=>{var e;e=()=>{const e=_();if(e){const s=t.NAME,i=e.fn[s];e.fn[s]=t.jQueryInterface,e.fn[s].Constructor=t,e.fn[s].noConflict=()=>(e.fn[s]=i,t.jQueryInterface)}},"loading"===document.readyState?(b.length||document.addEventListener("DOMContentLoaded",()=>{b.forEach(t=>t())}),b.push(e)):e()},w=t=>{"function"==typeof t&&t()},E=(t,e,s=!0)=>{if(!s)return void w(t);const i=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:s}=window.getComputedStyle(t);const i=Number.parseFloat(e),n=Number.parseFloat(s);return i||n?(e=e.split(",")[0],s=s.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(s))):0})(e)+5;let n=!1;const o=({target:s})=>{s===e&&(n=!0,e.removeEventListener("transitionend",o),w(t))};e.addEventListener("transitionend",o),setTimeout(()=>{n||l(e)},i)},A=(t,e,s,i)=>{let n=t.indexOf(e);if(-1===n)return t[!s&&i?t.length-1:0];const o=t.length;return n+=s?1:-1,i&&(n=(n+o)%o),t[Math.max(0,Math.min(n,o-1))]},T=/[^.]*(?=\..*)\.|.*/,C=/\..*/,k=/::\d+$/,L={};let O=1;const D={mouseenter:"mouseover",mouseleave:"mouseout"},I=/^(mouseenter|mouseleave)/i,N=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function S(t,e){return e&&`${e}::${O++}`||t.uidEvent||O++}function x(t){const e=S(t);return t.uidEvent=e,L[e]=L[e]||{},L[e]}function M(t,e,s=null){const i=Object.keys(t);for(let n=0,o=i.length;nfunction(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};i?i=t(i):s=t(s)}const[o,r,a]=P(e,s,i),l=x(t),c=l[a]||(l[a]={}),h=M(c,r,o?s:null);if(h)return void(h.oneOff=h.oneOff&&n);const d=S(r,e.replace(T,"")),u=o?function(t,e,s){return function i(n){const o=t.querySelectorAll(e);for(let{target:r}=n;r&&r!==this;r=r.parentNode)for(let a=o.length;a--;)if(o[a]===r)return n.delegateTarget=r,i.oneOff&&B.off(t,n.type,e,s),s.apply(r,[n]);return null}}(t,s,i):function(t,e){return function s(i){return i.delegateTarget=t,s.oneOff&&B.off(t,i.type,e),e.apply(t,[i])}}(t,s);u.delegationSelector=o?s:null,u.originalHandler=r,u.oneOff=n,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function H(t,e,s,i,n){const o=M(e[s],i,n);o&&(t.removeEventListener(s,o,Boolean(n)),delete e[s][o.uidEvent])}function R(t){return t=t.replace(C,""),D[t]||t}const B={on(t,e,s,i){j(t,e,s,i,!1)},one(t,e,s,i){j(t,e,s,i,!0)},off(t,e,s,i){if("string"!=typeof e||!t)return;const[n,o,r]=P(e,s,i),a=r!==e,l=x(t),c=e.startsWith(".");if(void 0!==o){if(!l||!l[r])return;return void H(t,l,r,o,n?s:null)}c&&Object.keys(l).forEach(s=>{!function(t,e,s,i){const n=e[s]||{};Object.keys(n).forEach(o=>{if(o.includes(i)){const i=n[o];H(t,e,s,i.originalHandler,i.delegationSelector)}})}(t,l,s,e.slice(1))});const h=l[r]||{};Object.keys(h).forEach(s=>{const i=s.replace(k,"");if(!a||e.includes(i)){const e=h[s];H(t,l,r,e.originalHandler,e.delegationSelector)}})},trigger(t,e,s){if("string"!=typeof e||!t)return null;const i=_(),n=R(e),o=e!==n,r=N.has(n);let a,l=!0,c=!0,h=!1,d=null;return o&&i&&(a=i.Event(e,s),i(t).trigger(a),l=!a.isPropagationStopped(),c=!a.isImmediatePropagationStopped(),h=a.isDefaultPrevented()),r?(d=document.createEvent("HTMLEvents"),d.initEvent(n,l,!0)):d=new CustomEvent(e,{bubbles:l,cancelable:!0}),void 0!==s&&Object.keys(s).forEach(t=>{Object.defineProperty(d,t,{get:()=>s[t]})}),h&&d.preventDefault(),c&&t.dispatchEvent(d),d.defaultPrevented&&void 0!==a&&a.preventDefault(),d}},$=new Map;var W={set(t,e,s){$.has(t)||$.set(t,new Map);const i=$.get(t);i.has(e)||0===i.size?i.set(e,s):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(i.keys())[0]}.`)},get:(t,e)=>$.has(t)&&$.get(t).get(e)||null,remove(t,e){if(!$.has(t))return;const s=$.get(t);s.delete(e),0===s.size&&$.delete(t)}};class q{constructor(t){(t=h(t))&&(this._element=t,W.set(this._element,this.constructor.DATA_KEY,this))}dispose(){W.remove(this._element,this.constructor.DATA_KEY),B.off(this._element,this.constructor.EVENT_KEY),Object.getOwnPropertyNames(this).forEach(t=>{this[t]=null})}_queueCallback(t,e,s=!0){E(t,e,s)}static getInstance(t){return W.get(t,this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.0.2"}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}static get DATA_KEY(){return"bs."+this.NAME}static get EVENT_KEY(){return"."+this.DATA_KEY}}class z extends q{static get NAME(){return"alert"}close(t){const e=t?this._getRootElement(t):this._element,s=this._triggerCloseEvent(e);null===s||s.defaultPrevented||this._removeElement(e)}_getRootElement(t){return a(t)||t.closest(".alert")}_triggerCloseEvent(t){return B.trigger(t,"close.bs.alert")}_removeElement(t){t.classList.remove("show");const e=t.classList.contains("fade");this._queueCallback(()=>this._destroyElement(t),t,e)}_destroyElement(t){t.remove(),B.trigger(t,"closed.bs.alert")}static jQueryInterface(t){return this.each((function(){const e=z.getOrCreateInstance(this);"close"===t&&e[t](this)}))}static handleDismiss(t){return function(e){e&&e.preventDefault(),t.close(this)}}}B.on(document,"click.bs.alert.data-api",'[data-bs-dismiss="alert"]',z.handleDismiss(new z)),y(z);class F extends q{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=F.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}function U(t){return"true"===t||"false"!==t&&(t===Number(t).toString()?Number(t):""===t||"null"===t?null:t)}function K(t){return t.replace(/[A-Z]/g,t=>"-"+t.toLowerCase())}B.on(document,"click.bs.button.data-api",'[data-bs-toggle="button"]',t=>{t.preventDefault();const e=t.target.closest('[data-bs-toggle="button"]');F.getOrCreateInstance(e).toggle()}),y(F);const V={setDataAttribute(t,e,s){t.setAttribute("data-bs-"+K(e),s)},removeDataAttribute(t,e){t.removeAttribute("data-bs-"+K(e))},getDataAttributes(t){if(!t)return{};const e={};return Object.keys(t.dataset).filter(t=>t.startsWith("bs")).forEach(s=>{let i=s.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1,i.length),e[i]=U(t.dataset[s])}),e},getDataAttribute:(t,e)=>U(t.getAttribute("data-bs-"+K(e))),offset(t){const e=t.getBoundingClientRect();return{top:e.top+document.body.scrollTop,left:e.left+document.body.scrollLeft}},position:t=>({top:t.offsetTop,left:t.offsetLeft})},Q={interval:5e3,keyboard:!0,slide:!1,pause:"hover",wrap:!0,touch:!0},X={interval:"(number|boolean)",keyboard:"boolean",slide:"(boolean|string)",pause:"(string|boolean)",wrap:"boolean",touch:"boolean"},Y="next",G="prev",Z="left",J="right",tt={ArrowLeft:J,ArrowRight:Z};class et extends q{constructor(t,e){super(t),this._items=null,this._interval=null,this._activeElement=null,this._isPaused=!1,this._isSliding=!1,this.touchTimeout=null,this.touchStartX=0,this.touchDeltaX=0,this._config=this._getConfig(e),this._indicatorsElement=i.findOne(".carousel-indicators",this._element),this._touchSupported="ontouchstart"in document.documentElement||navigator.maxTouchPoints>0,this._pointerEvent=Boolean(window.PointerEvent),this._addEventListeners()}static get Default(){return Q}static get NAME(){return"carousel"}next(){this._slide(Y)}nextWhenVisible(){!document.hidden&&u(this._element)&&this.next()}prev(){this._slide(G)}pause(t){t||(this._isPaused=!0),i.findOne(".carousel-item-next, .carousel-item-prev",this._element)&&(l(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null}cycle(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config&&this._config.interval&&!this._isPaused&&(this._updateInterval(),this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))}to(t){this._activeElement=i.findOne(".active.carousel-item",this._element);const e=this._getItemIndex(this._activeElement);if(t>this._items.length-1||t<0)return;if(this._isSliding)return void B.one(this._element,"slid.bs.carousel",()=>this.to(t));if(e===t)return this.pause(),void this.cycle();const s=t>e?Y:G;this._slide(s,this._items[t])}_getConfig(t){return t={...Q,...V.getDataAttributes(this._element),..."object"==typeof t?t:{}},d("carousel",t,X),t}_handleSwipe(){const t=Math.abs(this.touchDeltaX);if(t<=40)return;const e=t/this.touchDeltaX;this.touchDeltaX=0,e&&this._slide(e>0?J:Z)}_addEventListeners(){this._config.keyboard&&B.on(this._element,"keydown.bs.carousel",t=>this._keydown(t)),"hover"===this._config.pause&&(B.on(this._element,"mouseenter.bs.carousel",t=>this.pause(t)),B.on(this._element,"mouseleave.bs.carousel",t=>this.cycle(t))),this._config.touch&&this._touchSupported&&this._addTouchEventListeners()}_addTouchEventListeners(){const t=t=>{!this._pointerEvent||"pen"!==t.pointerType&&"touch"!==t.pointerType?this._pointerEvent||(this.touchStartX=t.touches[0].clientX):this.touchStartX=t.clientX},e=t=>{this.touchDeltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this.touchStartX},s=t=>{!this._pointerEvent||"pen"!==t.pointerType&&"touch"!==t.pointerType||(this.touchDeltaX=t.clientX-this.touchStartX),this._handleSwipe(),"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout(t=>this.cycle(t),500+this._config.interval))};i.find(".carousel-item img",this._element).forEach(t=>{B.on(t,"dragstart.bs.carousel",t=>t.preventDefault())}),this._pointerEvent?(B.on(this._element,"pointerdown.bs.carousel",e=>t(e)),B.on(this._element,"pointerup.bs.carousel",t=>s(t)),this._element.classList.add("pointer-event")):(B.on(this._element,"touchstart.bs.carousel",e=>t(e)),B.on(this._element,"touchmove.bs.carousel",t=>e(t)),B.on(this._element,"touchend.bs.carousel",t=>s(t)))}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=tt[t.key];e&&(t.preventDefault(),this._slide(e))}_getItemIndex(t){return this._items=t&&t.parentNode?i.find(".carousel-item",t.parentNode):[],this._items.indexOf(t)}_getItemByOrder(t,e){const s=t===Y;return A(this._items,e,s,this._config.wrap)}_triggerSlideEvent(t,e){const s=this._getItemIndex(t),n=this._getItemIndex(i.findOne(".active.carousel-item",this._element));return B.trigger(this._element,"slide.bs.carousel",{relatedTarget:t,direction:e,from:n,to:s})}_setActiveIndicatorElement(t){if(this._indicatorsElement){const e=i.findOne(".active",this._indicatorsElement);e.classList.remove("active"),e.removeAttribute("aria-current");const s=i.find("[data-bs-target]",this._indicatorsElement);for(let e=0;e{B.trigger(this._element,"slid.bs.carousel",{relatedTarget:r,direction:u,from:o,to:a})};if(this._element.classList.contains("slide")){r.classList.add(d),m(r),n.classList.add(h),r.classList.add(h);const t=()=>{r.classList.remove(h,d),r.classList.add("active"),n.classList.remove("active",d,h),this._isSliding=!1,setTimeout(g,0)};this._queueCallback(t,n,!0)}else n.classList.remove("active"),r.classList.add("active"),this._isSliding=!1,g();l&&this.cycle()}_directionToOrder(t){return[J,Z].includes(t)?v()?t===Z?G:Y:t===Z?Y:G:t}_orderToDirection(t){return[Y,G].includes(t)?v()?t===G?Z:J:t===G?J:Z:t}static carouselInterface(t,e){const s=et.getOrCreateInstance(t,e);let{_config:i}=s;"object"==typeof e&&(i={...i,...e});const n="string"==typeof e?e:i.slide;if("number"==typeof e)s.to(e);else if("string"==typeof n){if(void 0===s[n])throw new TypeError(`No method named "${n}"`);s[n]()}else i.interval&&i.ride&&(s.pause(),s.cycle())}static jQueryInterface(t){return this.each((function(){et.carouselInterface(this,t)}))}static dataApiClickHandler(t){const e=a(this);if(!e||!e.classList.contains("carousel"))return;const s={...V.getDataAttributes(e),...V.getDataAttributes(this)},i=this.getAttribute("data-bs-slide-to");i&&(s.interval=!1),et.carouselInterface(e,s),i&&et.getInstance(e).to(i),t.preventDefault()}}B.on(document,"click.bs.carousel.data-api","[data-bs-slide], [data-bs-slide-to]",et.dataApiClickHandler),B.on(window,"load.bs.carousel.data-api",()=>{const t=i.find('[data-bs-ride="carousel"]');for(let e=0,s=t.length;et===this._element);null!==n&&o.length&&(this._selector=n,this._triggerArray.push(e))}this._parent=this._config.parent?this._getParent():null,this._config.parent||this._addAriaAndCollapsedClass(this._element,this._triggerArray),this._config.toggle&&this.toggle()}static get Default(){return st}static get NAME(){return"collapse"}toggle(){this._element.classList.contains("show")?this.hide():this.show()}show(){if(this._isTransitioning||this._element.classList.contains("show"))return;let t,e;this._parent&&(t=i.find(".show, .collapsing",this._parent).filter(t=>"string"==typeof this._config.parent?t.getAttribute("data-bs-parent")===this._config.parent:t.classList.contains("collapse")),0===t.length&&(t=null));const s=i.findOne(this._selector);if(t){const i=t.find(t=>s!==t);if(e=i?nt.getInstance(i):null,e&&e._isTransitioning)return}if(B.trigger(this._element,"show.bs.collapse").defaultPrevented)return;t&&t.forEach(t=>{s!==t&&nt.collapseInterface(t,"hide"),e||W.set(t,"bs.collapse",null)});const n=this._getDimension();this._element.classList.remove("collapse"),this._element.classList.add("collapsing"),this._element.style[n]=0,this._triggerArray.length&&this._triggerArray.forEach(t=>{t.classList.remove("collapsed"),t.setAttribute("aria-expanded",!0)}),this.setTransitioning(!0);const o="scroll"+(n[0].toUpperCase()+n.slice(1));this._queueCallback(()=>{this._element.classList.remove("collapsing"),this._element.classList.add("collapse","show"),this._element.style[n]="",this.setTransitioning(!1),B.trigger(this._element,"shown.bs.collapse")},this._element,!0),this._element.style[n]=this._element[o]+"px"}hide(){if(this._isTransitioning||!this._element.classList.contains("show"))return;if(B.trigger(this._element,"hide.bs.collapse").defaultPrevented)return;const t=this._getDimension();this._element.style[t]=this._element.getBoundingClientRect()[t]+"px",m(this._element),this._element.classList.add("collapsing"),this._element.classList.remove("collapse","show");const e=this._triggerArray.length;if(e>0)for(let t=0;t{this.setTransitioning(!1),this._element.classList.remove("collapsing"),this._element.classList.add("collapse"),B.trigger(this._element,"hidden.bs.collapse")},this._element,!0)}setTransitioning(t){this._isTransitioning=t}_getConfig(t){return(t={...st,...t}).toggle=Boolean(t.toggle),d("collapse",t,it),t}_getDimension(){return this._element.classList.contains("width")?"width":"height"}_getParent(){let{parent:t}=this._config;t=h(t);const e=`[data-bs-toggle="collapse"][data-bs-parent="${t}"]`;return i.find(e,t).forEach(t=>{const e=a(t);this._addAriaAndCollapsedClass(e,[t])}),t}_addAriaAndCollapsedClass(t,e){if(!t||!e.length)return;const s=t.classList.contains("show");e.forEach(t=>{s?t.classList.remove("collapsed"):t.classList.add("collapsed"),t.setAttribute("aria-expanded",s)})}static collapseInterface(t,e){let s=nt.getInstance(t);const i={...st,...V.getDataAttributes(t),..."object"==typeof e&&e?e:{}};if(!s&&i.toggle&&"string"==typeof e&&/show|hide/.test(e)&&(i.toggle=!1),s||(s=new nt(t,i)),"string"==typeof e){if(void 0===s[e])throw new TypeError(`No method named "${e}"`);s[e]()}}static jQueryInterface(t){return this.each((function(){nt.collapseInterface(this,t)}))}}B.on(document,"click.bs.collapse.data-api",'[data-bs-toggle="collapse"]',(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();const e=V.getDataAttributes(this),s=r(this);i.find(s).forEach(t=>{const s=nt.getInstance(t);let i;s?(null===s._parent&&"string"==typeof e.parent&&(s._config.parent=e.parent,s._parent=s._getParent()),i="toggle"):i=e,nt.collapseInterface(t,i)})})),y(nt);const ot=new RegExp("ArrowUp|ArrowDown|Escape"),rt=v()?"top-end":"top-start",at=v()?"top-start":"top-end",lt=v()?"bottom-end":"bottom-start",ct=v()?"bottom-start":"bottom-end",ht=v()?"left-start":"right-start",dt=v()?"right-start":"left-start",ut={offset:[0,2],boundary:"clippingParents",reference:"toggle",display:"dynamic",popperConfig:null,autoClose:!0},gt={offset:"(array|string|function)",boundary:"(string|element)",reference:"(string|element|object)",display:"string",popperConfig:"(null|object|function)",autoClose:"(boolean|string)"};class pt extends q{constructor(t,e){super(t),this._popper=null,this._config=this._getConfig(e),this._menu=this._getMenuElement(),this._inNavbar=this._detectNavbar(),this._addEventListeners()}static get Default(){return ut}static get DefaultType(){return gt}static get NAME(){return"dropdown"}toggle(){g(this._element)||(this._element.classList.contains("show")?this.hide():this.show())}show(){if(g(this._element)||this._menu.classList.contains("show"))return;const t=pt.getParentFromElement(this._element),e={relatedTarget:this._element};if(!B.trigger(this._element,"show.bs.dropdown",e).defaultPrevented){if(this._inNavbar)V.setDataAttribute(this._menu,"popper","none");else{if(void 0===s)throw new TypeError("Bootstrap's dropdowns require Popper (https://popper.js.org)");let e=this._element;"parent"===this._config.reference?e=t:c(this._config.reference)?e=h(this._config.reference):"object"==typeof this._config.reference&&(e=this._config.reference);const i=this._getPopperConfig(),n=i.modifiers.find(t=>"applyStyles"===t.name&&!1===t.enabled);this._popper=s.createPopper(e,this._menu,i),n&&V.setDataAttribute(this._menu,"popper","static")}"ontouchstart"in document.documentElement&&!t.closest(".navbar-nav")&&[].concat(...document.body.children).forEach(t=>B.on(t,"mouseover",f)),this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.toggle("show"),this._element.classList.toggle("show"),B.trigger(this._element,"shown.bs.dropdown",e)}}hide(){if(g(this._element)||!this._menu.classList.contains("show"))return;const t={relatedTarget:this._element};this._completeHide(t)}dispose(){this._popper&&this._popper.destroy(),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_addEventListeners(){B.on(this._element,"click.bs.dropdown",t=>{t.preventDefault(),this.toggle()})}_completeHide(t){B.trigger(this._element,"hide.bs.dropdown",t).defaultPrevented||("ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach(t=>B.off(t,"mouseover",f)),this._popper&&this._popper.destroy(),this._menu.classList.remove("show"),this._element.classList.remove("show"),this._element.setAttribute("aria-expanded","false"),V.removeDataAttribute(this._menu,"popper"),B.trigger(this._element,"hidden.bs.dropdown",t))}_getConfig(t){if(t={...this.constructor.Default,...V.getDataAttributes(this._element),...t},d("dropdown",t,this.constructor.DefaultType),"object"==typeof t.reference&&!c(t.reference)&&"function"!=typeof t.reference.getBoundingClientRect)throw new TypeError("dropdown".toUpperCase()+': Option "reference" provided type "object" without a required "getBoundingClientRect" method.');return t}_getMenuElement(){return i.next(this._element,".dropdown-menu")[0]}_getPlacement(){const t=this._element.parentNode;if(t.classList.contains("dropend"))return ht;if(t.classList.contains("dropstart"))return dt;const e="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim();return t.classList.contains("dropup")?e?at:rt:e?ct:lt}_detectNavbar(){return null!==this._element.closest(".navbar")}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map(t=>Number.parseInt(t,10)):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return"static"===this._config.display&&(t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,..."function"==typeof this._config.popperConfig?this._config.popperConfig(t):this._config.popperConfig}}_selectMenuItem({key:t,target:e}){const s=i.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter(u);s.length&&A(s,e,"ArrowDown"===t,!s.includes(e)).focus()}static dropdownInterface(t,e){const s=pt.getOrCreateInstance(t,e);if("string"==typeof e){if(void 0===s[e])throw new TypeError(`No method named "${e}"`);s[e]()}}static jQueryInterface(t){return this.each((function(){pt.dropdownInterface(this,t)}))}static clearMenus(t){if(t&&(2===t.button||"keyup"===t.type&&"Tab"!==t.key))return;const e=i.find('[data-bs-toggle="dropdown"]');for(let s=0,i=e.length;sthis.matches('[data-bs-toggle="dropdown"]')?this:i.prev(this,'[data-bs-toggle="dropdown"]')[0];return"Escape"===t.key?(s().focus(),void pt.clearMenus()):"ArrowUp"===t.key||"ArrowDown"===t.key?(e||s().click(),void pt.getInstance(s())._selectMenuItem(t)):void(e&&"Space"!==t.key||pt.clearMenus())}}B.on(document,"keydown.bs.dropdown.data-api",'[data-bs-toggle="dropdown"]',pt.dataApiKeydownHandler),B.on(document,"keydown.bs.dropdown.data-api",".dropdown-menu",pt.dataApiKeydownHandler),B.on(document,"click.bs.dropdown.data-api",pt.clearMenus),B.on(document,"keyup.bs.dropdown.data-api",pt.clearMenus),B.on(document,"click.bs.dropdown.data-api",'[data-bs-toggle="dropdown"]',(function(t){t.preventDefault(),pt.dropdownInterface(this)})),y(pt);class ft{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,"paddingRight",e=>e+t),this._setElementAttributes(".fixed-top, .fixed-bottom, .is-fixed, .sticky-top","paddingRight",e=>e+t),this._setElementAttributes(".sticky-top","marginRight",e=>e-t)}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,s){const i=this.getWidth();this._applyManipulationCallback(t,t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+i)return;this._saveInitialAttribute(t,e);const n=window.getComputedStyle(t)[e];t.style[e]=s(Number.parseFloat(n))+"px"})}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,"paddingRight"),this._resetElementAttributes(".fixed-top, .fixed-bottom, .is-fixed, .sticky-top","paddingRight"),this._resetElementAttributes(".sticky-top","marginRight")}_saveInitialAttribute(t,e){const s=t.style[e];s&&V.setDataAttribute(t,e,s)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,t=>{const s=V.getDataAttribute(t,e);void 0===s?t.style.removeProperty(e):(V.removeDataAttribute(t,e),t.style[e]=s)})}_applyManipulationCallback(t,e){c(t)?e(t):i.find(t,this._element).forEach(e)}isOverflowing(){return this.getWidth()>0}}const mt={isVisible:!0,isAnimated:!1,rootElement:"body",clickCallback:null},_t={isVisible:"boolean",isAnimated:"boolean",rootElement:"(element|string)",clickCallback:"(function|null)"};class bt{constructor(t){this._config=this._getConfig(t),this._isAppended=!1,this._element=null}show(t){this._config.isVisible?(this._append(),this._config.isAnimated&&m(this._getElement()),this._getElement().classList.add("show"),this._emulateAnimation(()=>{w(t)})):w(t)}hide(t){this._config.isVisible?(this._getElement().classList.remove("show"),this._emulateAnimation(()=>{this.dispose(),w(t)})):w(t)}_getElement(){if(!this._element){const t=document.createElement("div");t.className="modal-backdrop",this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_getConfig(t){return(t={...mt,..."object"==typeof t?t:{}}).rootElement=h(t.rootElement),d("backdrop",t,_t),t}_append(){this._isAppended||(this._config.rootElement.appendChild(this._getElement()),B.on(this._getElement(),"mousedown.bs.backdrop",()=>{w(this._config.clickCallback)}),this._isAppended=!0)}dispose(){this._isAppended&&(B.off(this._element,"mousedown.bs.backdrop"),this._element.remove(),this._isAppended=!1)}_emulateAnimation(t){E(t,this._getElement(),this._config.isAnimated)}}const vt={backdrop:!0,keyboard:!0,focus:!0},yt={backdrop:"(boolean|string)",keyboard:"boolean",focus:"boolean"};class wt extends q{constructor(t,e){super(t),this._config=this._getConfig(e),this._dialog=i.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._isShown=!1,this._ignoreBackdropClick=!1,this._isTransitioning=!1,this._scrollBar=new ft}static get Default(){return vt}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||B.trigger(this._element,"show.bs.modal",{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isAnimated()&&(this._isTransitioning=!0),this._scrollBar.hide(),document.body.classList.add("modal-open"),this._adjustDialog(),this._setEscapeEvent(),this._setResizeEvent(),B.on(this._element,"click.dismiss.bs.modal",'[data-bs-dismiss="modal"]',t=>this.hide(t)),B.on(this._dialog,"mousedown.dismiss.bs.modal",()=>{B.one(this._element,"mouseup.dismiss.bs.modal",t=>{t.target===this._element&&(this._ignoreBackdropClick=!0)})}),this._showBackdrop(()=>this._showElement(t)))}hide(t){if(t&&["A","AREA"].includes(t.target.tagName)&&t.preventDefault(),!this._isShown||this._isTransitioning)return;if(B.trigger(this._element,"hide.bs.modal").defaultPrevented)return;this._isShown=!1;const e=this._isAnimated();e&&(this._isTransitioning=!0),this._setEscapeEvent(),this._setResizeEvent(),B.off(document,"focusin.bs.modal"),this._element.classList.remove("show"),B.off(this._element,"click.dismiss.bs.modal"),B.off(this._dialog,"mousedown.dismiss.bs.modal"),this._queueCallback(()=>this._hideModal(),this._element,e)}dispose(){[window,this._dialog].forEach(t=>B.off(t,".bs.modal")),this._backdrop.dispose(),super.dispose(),B.off(document,"focusin.bs.modal")}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new bt({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_getConfig(t){return t={...vt,...V.getDataAttributes(this._element),..."object"==typeof t?t:{}},d("modal",t,yt),t}_showElement(t){const e=this._isAnimated(),s=i.findOne(".modal-body",this._dialog);this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE||document.body.appendChild(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0,s&&(s.scrollTop=0),e&&m(this._element),this._element.classList.add("show"),this._config.focus&&this._enforceFocus(),this._queueCallback(()=>{this._config.focus&&this._element.focus(),this._isTransitioning=!1,B.trigger(this._element,"shown.bs.modal",{relatedTarget:t})},this._dialog,e)}_enforceFocus(){B.off(document,"focusin.bs.modal"),B.on(document,"focusin.bs.modal",t=>{document===t.target||this._element===t.target||this._element.contains(t.target)||this._element.focus()})}_setEscapeEvent(){this._isShown?B.on(this._element,"keydown.dismiss.bs.modal",t=>{this._config.keyboard&&"Escape"===t.key?(t.preventDefault(),this.hide()):this._config.keyboard||"Escape"!==t.key||this._triggerBackdropTransition()}):B.off(this._element,"keydown.dismiss.bs.modal")}_setResizeEvent(){this._isShown?B.on(window,"resize.bs.modal",()=>this._adjustDialog()):B.off(window,"resize.bs.modal")}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide(()=>{document.body.classList.remove("modal-open"),this._resetAdjustments(),this._scrollBar.reset(),B.trigger(this._element,"hidden.bs.modal")})}_showBackdrop(t){B.on(this._element,"click.dismiss.bs.modal",t=>{this._ignoreBackdropClick?this._ignoreBackdropClick=!1:t.target===t.currentTarget&&(!0===this._config.backdrop?this.hide():"static"===this._config.backdrop&&this._triggerBackdropTransition())}),this._backdrop.show(t)}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(B.trigger(this._element,"hidePrevented.bs.modal").defaultPrevented)return;const{classList:t,scrollHeight:e,style:s}=this._element,i=e>document.documentElement.clientHeight;!i&&"hidden"===s.overflowY||t.contains("modal-static")||(i||(s.overflowY="hidden"),t.add("modal-static"),this._queueCallback(()=>{t.remove("modal-static"),i||this._queueCallback(()=>{s.overflowY=""},this._dialog)},this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),s=e>0;(!s&&t&&!v()||s&&!t&&v())&&(this._element.style.paddingLeft=e+"px"),(s&&!t&&!v()||!s&&t&&v())&&(this._element.style.paddingRight=e+"px")}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const s=wt.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===s[t])throw new TypeError(`No method named "${t}"`);s[t](e)}}))}}B.on(document,"click.bs.modal.data-api",'[data-bs-toggle="modal"]',(function(t){const e=a(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),B.one(e,"show.bs.modal",t=>{t.defaultPrevented||B.one(e,"hidden.bs.modal",()=>{u(this)&&this.focus()})}),wt.getOrCreateInstance(e).toggle(this)})),y(wt);const Et={backdrop:!0,keyboard:!0,scroll:!1},At={backdrop:"boolean",keyboard:"boolean",scroll:"boolean"};class Tt extends q{constructor(t,e){super(t),this._config=this._getConfig(e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._addEventListeners()}static get NAME(){return"offcanvas"}static get Default(){return Et}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||B.trigger(this._element,"show.bs.offcanvas",{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._element.style.visibility="visible",this._backdrop.show(),this._config.scroll||((new ft).hide(),this._enforceFocusOnElement(this._element)),this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add("show"),this._queueCallback(()=>{B.trigger(this._element,"shown.bs.offcanvas",{relatedTarget:t})},this._element,!0))}hide(){this._isShown&&(B.trigger(this._element,"hide.bs.offcanvas").defaultPrevented||(B.off(document,"focusin.bs.offcanvas"),this._element.blur(),this._isShown=!1,this._element.classList.remove("show"),this._backdrop.hide(),this._queueCallback(()=>{this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._element.style.visibility="hidden",this._config.scroll||(new ft).reset(),B.trigger(this._element,"hidden.bs.offcanvas")},this._element,!0)))}dispose(){this._backdrop.dispose(),super.dispose(),B.off(document,"focusin.bs.offcanvas")}_getConfig(t){return t={...Et,...V.getDataAttributes(this._element),..."object"==typeof t?t:{}},d("offcanvas",t,At),t}_initializeBackDrop(){return new bt({isVisible:this._config.backdrop,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:()=>this.hide()})}_enforceFocusOnElement(t){B.off(document,"focusin.bs.offcanvas"),B.on(document,"focusin.bs.offcanvas",e=>{document===e.target||t===e.target||t.contains(e.target)||t.focus()}),t.focus()}_addEventListeners(){B.on(this._element,"click.dismiss.bs.offcanvas",'[data-bs-dismiss="offcanvas"]',()=>this.hide()),B.on(this._element,"keydown.dismiss.bs.offcanvas",t=>{this._config.keyboard&&"Escape"===t.key&&this.hide()})}static jQueryInterface(t){return this.each((function(){const e=Tt.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}B.on(document,"click.bs.offcanvas.data-api",'[data-bs-toggle="offcanvas"]',(function(t){const e=a(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),g(this))return;B.one(e,"hidden.bs.offcanvas",()=>{u(this)&&this.focus()});const s=i.findOne(".offcanvas.show");s&&s!==e&&Tt.getInstance(s).hide(),Tt.getOrCreateInstance(e).toggle(this)})),B.on(window,"load.bs.offcanvas.data-api",()=>i.find(".offcanvas.show").forEach(t=>Tt.getOrCreateInstance(t).show())),y(Tt);const Ct=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),kt=/^(?:(?:https?|mailto|ftp|tel|file):|[^#&/:?]*(?:[#/?]|$))/i,Lt=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z]+=*$/i,Ot=(t,e)=>{const s=t.nodeName.toLowerCase();if(e.includes(s))return!Ct.has(s)||Boolean(kt.test(t.nodeValue)||Lt.test(t.nodeValue));const i=e.filter(t=>t instanceof RegExp);for(let t=0,e=i.length;t{Ot(t,a)||s.removeAttribute(t.nodeName)})}return i.body.innerHTML}const It=new RegExp("(^|\\s)bs-tooltip\\S+","g"),Nt=new Set(["sanitize","allowList","sanitizeFn"]),St={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"(array|string|function)",container:"(string|element|boolean)",fallbackPlacements:"array",boundary:"(string|element)",customClass:"(string|function)",sanitize:"boolean",sanitizeFn:"(null|function)",allowList:"object",popperConfig:"(null|object|function)"},xt={AUTO:"auto",TOP:"top",RIGHT:v()?"left":"right",BOTTOM:"bottom",LEFT:v()?"right":"left"},Mt={animation:!0,template:'',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:[0,0],container:!1,fallbackPlacements:["top","right","bottom","left"],boundary:"clippingParents",customClass:"",sanitize:!0,sanitizeFn:null,allowList:{"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},popperConfig:null},Pt={HIDE:"hide.bs.tooltip",HIDDEN:"hidden.bs.tooltip",SHOW:"show.bs.tooltip",SHOWN:"shown.bs.tooltip",INSERTED:"inserted.bs.tooltip",CLICK:"click.bs.tooltip",FOCUSIN:"focusin.bs.tooltip",FOCUSOUT:"focusout.bs.tooltip",MOUSEENTER:"mouseenter.bs.tooltip",MOUSELEAVE:"mouseleave.bs.tooltip"};class jt extends q{constructor(t,e){if(void 0===s)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t),this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this._config=this._getConfig(e),this.tip=null,this._setListeners()}static get Default(){return Mt}static get NAME(){return"tooltip"}static get Event(){return Pt}static get DefaultType(){return St}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(t){if(this._isEnabled)if(t){const e=this._initializeOnDelegatedTarget(t);e._activeTrigger.click=!e._activeTrigger.click,e._isWithActiveTrigger()?e._enter(null,e):e._leave(null,e)}else{if(this.getTipElement().classList.contains("show"))return void this._leave(null,this);this._enter(null,this)}}dispose(){clearTimeout(this._timeout),B.off(this._element.closest(".modal"),"hide.bs.modal",this._hideModalHandler),this.tip&&this.tip.remove(),this._popper&&this._popper.destroy(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this.isWithContent()||!this._isEnabled)return;const t=B.trigger(this._element,this.constructor.Event.SHOW),e=p(this._element),i=null===e?this._element.ownerDocument.documentElement.contains(this._element):e.contains(this._element);if(t.defaultPrevented||!i)return;const o=this.getTipElement(),r=n(this.constructor.NAME);o.setAttribute("id",r),this._element.setAttribute("aria-describedby",r),this.setContent(),this._config.animation&&o.classList.add("fade");const a="function"==typeof this._config.placement?this._config.placement.call(this,o,this._element):this._config.placement,l=this._getAttachment(a);this._addAttachmentClass(l);const{container:c}=this._config;W.set(o,this.constructor.DATA_KEY,this),this._element.ownerDocument.documentElement.contains(this.tip)||(c.appendChild(o),B.trigger(this._element,this.constructor.Event.INSERTED)),this._popper?this._popper.update():this._popper=s.createPopper(this._element,o,this._getPopperConfig(l)),o.classList.add("show");const h="function"==typeof this._config.customClass?this._config.customClass():this._config.customClass;h&&o.classList.add(...h.split(" ")),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach(t=>{B.on(t,"mouseover",f)});const d=this.tip.classList.contains("fade");this._queueCallback(()=>{const t=this._hoverState;this._hoverState=null,B.trigger(this._element,this.constructor.Event.SHOWN),"out"===t&&this._leave(null,this)},this.tip,d)}hide(){if(!this._popper)return;const t=this.getTipElement();if(B.trigger(this._element,this.constructor.Event.HIDE).defaultPrevented)return;t.classList.remove("show"),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach(t=>B.off(t,"mouseover",f)),this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1;const e=this.tip.classList.contains("fade");this._queueCallback(()=>{this._isWithActiveTrigger()||("show"!==this._hoverState&&t.remove(),this._cleanTipClass(),this._element.removeAttribute("aria-describedby"),B.trigger(this._element,this.constructor.Event.HIDDEN),this._popper&&(this._popper.destroy(),this._popper=null))},this.tip,e),this._hoverState=""}update(){null!==this._popper&&this._popper.update()}isWithContent(){return Boolean(this.getTitle())}getTipElement(){if(this.tip)return this.tip;const t=document.createElement("div");return t.innerHTML=this._config.template,this.tip=t.children[0],this.tip}setContent(){const t=this.getTipElement();this.setElementContent(i.findOne(".tooltip-inner",t),this.getTitle()),t.classList.remove("fade","show")}setElementContent(t,e){if(null!==t)return c(e)?(e=h(e),void(this._config.html?e.parentNode!==t&&(t.innerHTML="",t.appendChild(e)):t.textContent=e.textContent)):void(this._config.html?(this._config.sanitize&&(e=Dt(e,this._config.allowList,this._config.sanitizeFn)),t.innerHTML=e):t.textContent=e)}getTitle(){let t=this._element.getAttribute("data-bs-original-title");return t||(t="function"==typeof this._config.title?this._config.title.call(this._element):this._config.title),t}updateAttachment(t){return"right"===t?"end":"left"===t?"start":t}_initializeOnDelegatedTarget(t,e){const s=this.constructor.DATA_KEY;return(e=e||W.get(t.delegateTarget,s))||(e=new this.constructor(t.delegateTarget,this._getDelegateConfig()),W.set(t.delegateTarget,s,e)),e}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map(t=>Number.parseInt(t,10)):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"onChange",enabled:!0,phase:"afterWrite",fn:t=>this._handlePopperPlacementChange(t)}],onFirstUpdate:t=>{t.options.placement!==t.placement&&this._handlePopperPlacementChange(t)}};return{...e,..."function"==typeof this._config.popperConfig?this._config.popperConfig(e):this._config.popperConfig}}_addAttachmentClass(t){this.getTipElement().classList.add("bs-tooltip-"+this.updateAttachment(t))}_getAttachment(t){return xt[t.toUpperCase()]}_setListeners(){this._config.trigger.split(" ").forEach(t=>{if("click"===t)B.on(this._element,this.constructor.Event.CLICK,this._config.selector,t=>this.toggle(t));else if("manual"!==t){const e="hover"===t?this.constructor.Event.MOUSEENTER:this.constructor.Event.FOCUSIN,s="hover"===t?this.constructor.Event.MOUSELEAVE:this.constructor.Event.FOCUSOUT;B.on(this._element,e,this._config.selector,t=>this._enter(t)),B.on(this._element,s,this._config.selector,t=>this._leave(t))}}),this._hideModalHandler=()=>{this._element&&this.hide()},B.on(this._element.closest(".modal"),"hide.bs.modal",this._hideModalHandler),this._config.selector?this._config={...this._config,trigger:"manual",selector:""}:this._fixTitle()}_fixTitle(){const t=this._element.getAttribute("title"),e=typeof this._element.getAttribute("data-bs-original-title");(t||"string"!==e)&&(this._element.setAttribute("data-bs-original-title",t||""),!t||this._element.getAttribute("aria-label")||this._element.textContent||this._element.setAttribute("aria-label",t),this._element.setAttribute("title",""))}_enter(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusin"===t.type?"focus":"hover"]=!0),e.getTipElement().classList.contains("show")||"show"===e._hoverState?e._hoverState="show":(clearTimeout(e._timeout),e._hoverState="show",e._config.delay&&e._config.delay.show?e._timeout=setTimeout(()=>{"show"===e._hoverState&&e.show()},e._config.delay.show):e.show())}_leave(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusout"===t.type?"focus":"hover"]=e._element.contains(t.relatedTarget)),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState="out",e._config.delay&&e._config.delay.hide?e._timeout=setTimeout(()=>{"out"===e._hoverState&&e.hide()},e._config.delay.hide):e.hide())}_isWithActiveTrigger(){for(const t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1}_getConfig(t){const e=V.getDataAttributes(this._element);return Object.keys(e).forEach(t=>{Nt.has(t)&&delete e[t]}),(t={...this.constructor.Default,...e,..."object"==typeof t&&t?t:{}}).container=!1===t.container?document.body:h(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),d("tooltip",t,this.constructor.DefaultType),t.sanitize&&(t.template=Dt(t.template,t.allowList,t.sanitizeFn)),t}_getDelegateConfig(){const t={};if(this._config)for(const e in this._config)this.constructor.Default[e]!==this._config[e]&&(t[e]=this._config[e]);return t}_cleanTipClass(){const t=this.getTipElement(),e=t.getAttribute("class").match(It);null!==e&&e.length>0&&e.map(t=>t.trim()).forEach(e=>t.classList.remove(e))}_handlePopperPlacementChange(t){const{state:e}=t;e&&(this.tip=e.elements.popper,this._cleanTipClass(),this._addAttachmentClass(this._getAttachment(e.placement)))}static jQueryInterface(t){return this.each((function(){const e=jt.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}y(jt);const Ht=new RegExp("(^|\\s)bs-popover\\S+","g"),Rt={...jt.Default,placement:"right",offset:[0,8],trigger:"click",content:"",template:''},Bt={...jt.DefaultType,content:"(string|element|function)"},$t={HIDE:"hide.bs.popover",HIDDEN:"hidden.bs.popover",SHOW:"show.bs.popover",SHOWN:"shown.bs.popover",INSERTED:"inserted.bs.popover",CLICK:"click.bs.popover",FOCUSIN:"focusin.bs.popover",FOCUSOUT:"focusout.bs.popover",MOUSEENTER:"mouseenter.bs.popover",MOUSELEAVE:"mouseleave.bs.popover"};class Wt extends jt{static get Default(){return Rt}static get NAME(){return"popover"}static get Event(){return $t}static get DefaultType(){return Bt}isWithContent(){return this.getTitle()||this._getContent()}getTipElement(){return this.tip||(this.tip=super.getTipElement(),this.getTitle()||i.findOne(".popover-header",this.tip).remove(),this._getContent()||i.findOne(".popover-body",this.tip).remove()),this.tip}setContent(){const t=this.getTipElement();this.setElementContent(i.findOne(".popover-header",t),this.getTitle());let e=this._getContent();"function"==typeof e&&(e=e.call(this._element)),this.setElementContent(i.findOne(".popover-body",t),e),t.classList.remove("fade","show")}_addAttachmentClass(t){this.getTipElement().classList.add("bs-popover-"+this.updateAttachment(t))}_getContent(){return this._element.getAttribute("data-bs-content")||this._config.content}_cleanTipClass(){const t=this.getTipElement(),e=t.getAttribute("class").match(Ht);null!==e&&e.length>0&&e.map(t=>t.trim()).forEach(e=>t.classList.remove(e))}static jQueryInterface(t){return this.each((function(){const e=Wt.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}y(Wt);const qt={offset:10,method:"auto",target:""},zt={offset:"number",method:"string",target:"(string|element)"};class Ft extends q{constructor(t,e){super(t),this._scrollElement="BODY"===this._element.tagName?window:this._element,this._config=this._getConfig(e),this._selector=`${this._config.target} .nav-link, ${this._config.target} .list-group-item, ${this._config.target} .dropdown-item`,this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,B.on(this._scrollElement,"scroll.bs.scrollspy",()=>this._process()),this.refresh(),this._process()}static get Default(){return qt}static get NAME(){return"scrollspy"}refresh(){const t=this._scrollElement===this._scrollElement.window?"offset":"position",e="auto"===this._config.method?t:this._config.method,s="position"===e?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),i.find(this._selector).map(t=>{const n=r(t),o=n?i.findOne(n):null;if(o){const t=o.getBoundingClientRect();if(t.width||t.height)return[V[e](o).top+s,n]}return null}).filter(t=>t).sort((t,e)=>t[0]-e[0]).forEach(t=>{this._offsets.push(t[0]),this._targets.push(t[1])})}dispose(){B.off(this._scrollElement,".bs.scrollspy"),super.dispose()}_getConfig(t){if("string"!=typeof(t={...qt,...V.getDataAttributes(this._element),..."object"==typeof t&&t?t:{}}).target&&c(t.target)){let{id:e}=t.target;e||(e=n("scrollspy"),t.target.id=e),t.target="#"+e}return d("scrollspy",t,zt),t}_getScrollTop(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop}_getScrollHeight(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)}_getOffsetHeight(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height}_process(){const t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),s=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=s){const t=this._targets[this._targets.length-1];this._activeTarget!==t&&this._activate(t)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(let e=this._offsets.length;e--;)this._activeTarget!==this._targets[e]&&t>=this._offsets[e]&&(void 0===this._offsets[e+1]||t`${e}[data-bs-target="${t}"],${e}[href="${t}"]`),s=i.findOne(e.join(","));s.classList.contains("dropdown-item")?(i.findOne(".dropdown-toggle",s.closest(".dropdown")).classList.add("active"),s.classList.add("active")):(s.classList.add("active"),i.parents(s,".nav, .list-group").forEach(t=>{i.prev(t,".nav-link, .list-group-item").forEach(t=>t.classList.add("active")),i.prev(t,".nav-item").forEach(t=>{i.children(t,".nav-link").forEach(t=>t.classList.add("active"))})})),B.trigger(this._scrollElement,"activate.bs.scrollspy",{relatedTarget:t})}_clear(){i.find(this._selector).filter(t=>t.classList.contains("active")).forEach(t=>t.classList.remove("active"))}static jQueryInterface(t){return this.each((function(){const e=Ft.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}B.on(window,"load.bs.scrollspy.data-api",()=>{i.find('[data-bs-spy="scroll"]').forEach(t=>new Ft(t))}),y(Ft);class Ut extends q{static get NAME(){return"tab"}show(){if(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&this._element.classList.contains("active"))return;let t;const e=a(this._element),s=this._element.closest(".nav, .list-group");if(s){const e="UL"===s.nodeName||"OL"===s.nodeName?":scope > li > .active":".active";t=i.find(e,s),t=t[t.length-1]}const n=t?B.trigger(t,"hide.bs.tab",{relatedTarget:this._element}):null;if(B.trigger(this._element,"show.bs.tab",{relatedTarget:t}).defaultPrevented||null!==n&&n.defaultPrevented)return;this._activate(this._element,s);const o=()=>{B.trigger(t,"hidden.bs.tab",{relatedTarget:this._element}),B.trigger(this._element,"shown.bs.tab",{relatedTarget:t})};e?this._activate(e,e.parentNode,o):o()}_activate(t,e,s){const n=(!e||"UL"!==e.nodeName&&"OL"!==e.nodeName?i.children(e,".active"):i.find(":scope > li > .active",e))[0],o=s&&n&&n.classList.contains("fade"),r=()=>this._transitionComplete(t,n,s);n&&o?(n.classList.remove("show"),this._queueCallback(r,t,!0)):r()}_transitionComplete(t,e,s){if(e){e.classList.remove("active");const t=i.findOne(":scope > .dropdown-menu .active",e.parentNode);t&&t.classList.remove("active"),"tab"===e.getAttribute("role")&&e.setAttribute("aria-selected",!1)}t.classList.add("active"),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!0),m(t),t.classList.contains("fade")&&t.classList.add("show");let n=t.parentNode;if(n&&"LI"===n.nodeName&&(n=n.parentNode),n&&n.classList.contains("dropdown-menu")){const e=t.closest(".dropdown");e&&i.find(".dropdown-toggle",e).forEach(t=>t.classList.add("active")),t.setAttribute("aria-expanded",!0)}s&&s()}static jQueryInterface(t){return this.each((function(){const e=Ut.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}B.on(document,"click.bs.tab.data-api",'[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),g(this)||Ut.getOrCreateInstance(this).show()})),y(Ut);const Kt={animation:"boolean",autohide:"boolean",delay:"number"},Vt={animation:!0,autohide:!0,delay:5e3};class Qt extends q{constructor(t,e){super(t),this._config=this._getConfig(e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get DefaultType(){return Kt}static get Default(){return Vt}static get NAME(){return"toast"}show(){B.trigger(this._element,"show.bs.toast").defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove("hide"),m(this._element),this._element.classList.add("showing"),this._queueCallback(()=>{this._element.classList.remove("showing"),this._element.classList.add("show"),B.trigger(this._element,"shown.bs.toast"),this._maybeScheduleHide()},this._element,this._config.animation))}hide(){this._element.classList.contains("show")&&(B.trigger(this._element,"hide.bs.toast").defaultPrevented||(this._element.classList.remove("show"),this._queueCallback(()=>{this._element.classList.add("hide"),B.trigger(this._element,"hidden.bs.toast")},this._element,this._config.animation)))}dispose(){this._clearTimeout(),this._element.classList.contains("show")&&this._element.classList.remove("show"),super.dispose()}_getConfig(t){return t={...Vt,...V.getDataAttributes(this._element),..."object"==typeof t&&t?t:{}},d("toast",t,this.constructor.DefaultType),t}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout(()=>{this.hide()},this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const s=t.relatedTarget;this._element===s||this._element.contains(s)||this._maybeScheduleHide()}_setListeners(){B.on(this._element,"click.dismiss.bs.toast",'[data-bs-dismiss="toast"]',()=>this.hide()),B.on(this._element,"mouseover.bs.toast",t=>this._onInteraction(t,!0)),B.on(this._element,"mouseout.bs.toast",t=>this._onInteraction(t,!1)),B.on(this._element,"focusin.bs.toast",t=>this._onInteraction(t,!0)),B.on(this._element,"focusout.bs.toast",t=>this._onInteraction(t,!1))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=Qt.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return y(Qt),{Alert:z,Button:F,Carousel:et,Collapse:nt,Dropdown:pt,Modal:wt,Offcanvas:Tt,Popover:Wt,ScrollSpy:Ft,Tab:Ut,Toast:Qt,Tooltip:jt}})); 7 | //# sourceMappingURL=bootstrap.min.js.map -------------------------------------------------------------------------------- /app/static/js/jquery.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ 2 | !function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0 3 | * @link https://github.com/elboletaire/password-strength-meter 4 | * @license GPL-3.0 5 | */ 6 | // eslint-disable-next-line 7 | ;(function($) { 8 | 'use strict'; 9 | 10 | var Password = function ($object, options) { 11 | console.log(options) 12 | var defaults = { 13 | enterPass: 'Type your password', 14 | shortPass: 'The password is too short', 15 | longPass: 'The password is too long', 16 | containsField: 'The password contains your username', 17 | steps: { 18 | 13: 'Really insecure password', 19 | 33: 'Weak; try combining letters & numbers', 20 | 67: 'Medium; try using special characters', 21 | 94: 'Strong password', 22 | }, 23 | showPercent: false, 24 | showText: true, 25 | animate: true, 26 | animateSpeed: 'fast', 27 | field: false, 28 | fieldPartialMatch: true, 29 | minimumLength: 4, 30 | maximumLength: 10, 31 | closestSelector: 'div', 32 | useColorBarImage: false, 33 | customColorBarRGB: { 34 | red: [0, 240], 35 | green: [0, 240], 36 | blue: 10 37 | }, 38 | }; 39 | 40 | options = $.extend({}, defaults, options); 41 | 42 | /** 43 | * Returns strings based on the score given. 44 | * 45 | * @param {int} score Score base. 46 | * @return {string} 47 | */ 48 | function scoreText(score) { 49 | if (score === -1) { 50 | return options.shortPass; 51 | } 52 | if (score === -2) { 53 | return options.containsField; 54 | } 55 | 56 | if (score === -8) { 57 | return options.longPass; 58 | } 59 | 60 | score = score < 0 ? 0 : score; 61 | 62 | var text = options.shortPass; 63 | var sortedStepKeys = Object.keys(options.steps).sort(); 64 | for (var step in sortedStepKeys) { 65 | var stepVal = sortedStepKeys[step]; 66 | if (stepVal < score) { 67 | text = options.steps[stepVal]; 68 | } 69 | } 70 | 71 | return text; 72 | } 73 | 74 | /** 75 | * Returns a value between -2 and 100 to score 76 | * the user's password. 77 | * 78 | * @param {string} password The password to be checked. 79 | * @param {string} field The field set (if options.field). 80 | * @return {int} 81 | */ 82 | function calculateScore(password, field) { 83 | var score = 0; 84 | 85 | // password < options.minimumLength 86 | if (password.length < options.minimumLength) { 87 | return -1; 88 | } 89 | 90 | if (password.length > options.maximumLength) { 91 | return -8; 92 | } 93 | 94 | if (options.field) { 95 | // password === field 96 | if (password.toLowerCase() === field.toLowerCase()) { 97 | return -2; 98 | } 99 | // password contains field (and fieldPartialMatch is set to true) 100 | if (options.fieldPartialMatch && field.length) { 101 | var user = new RegExp(field.toLowerCase()); 102 | if (password.toLowerCase().match(user)) { 103 | return -2; 104 | } 105 | } 106 | } 107 | 108 | // password length 109 | score += password.length * 4; 110 | score += checkRepetition(1, password).length - password.length; 111 | score += checkRepetition(2, password).length - password.length; 112 | score += checkRepetition(3, password).length - password.length; 113 | score += checkRepetition(4, password).length - password.length; 114 | 115 | // password has 3 numbers 116 | if (password.match(/(.*[0-9].*[0-9].*[0-9])/)) { 117 | score += 5; 118 | } 119 | 120 | // password has at least 2 symbols 121 | var symbols = '.*[!,@,#,$,%,^,&,*,?,_,~]'; 122 | symbols = new RegExp('(' + symbols + symbols + ')'); 123 | if (password.match(symbols)) { 124 | score += 5; 125 | } 126 | 127 | // password has Upper and Lower chars 128 | if (password.match(/([a-z].*[A-Z])|([A-Z].*[a-z])/)) { 129 | score += 10; 130 | } 131 | 132 | // password has number and chars 133 | if (password.match(/([a-zA-Z])/) && password.match(/([0-9])/)) { 134 | score += 15; 135 | } 136 | 137 | // password has number and symbol 138 | if (password.match(/([!@#$%^&*?_~])/) && password.match(/([0-9])/)) { 139 | score += 15; 140 | } 141 | 142 | // password has char and symbol 143 | if (password.match(/([!@#$%^&*?_~])/) && password.match(/([a-zA-Z])/)) { 144 | score += 15; 145 | } 146 | 147 | // password is just numbers or chars 148 | if (password.match(/^\w+$/) || password.match(/^\d+$/)) { 149 | score -= 10; 150 | } 151 | 152 | if (score > 100) { 153 | score = 100; 154 | } 155 | 156 | if (score < 0) { 157 | score = 0; 158 | } 159 | 160 | return score; 161 | } 162 | 163 | /** 164 | * Checks for repetition of characters in 165 | * a string 166 | * 167 | * @param {int} length Repetition length. 168 | * @param {string} str The string to be checked. 169 | * @return {string} 170 | */ 171 | function checkRepetition(length, str) { 172 | var res = "", repeated = false; 173 | for (var i = 0; i < str.length; i++) { 174 | repeated = true; 175 | for (var j = 0; j < length && (j + i + length) < str.length; j++) { 176 | repeated = repeated && (str.charAt(j + i) === str.charAt(j + i + length)); 177 | } 178 | if (j < length) { 179 | repeated = false; 180 | } 181 | if (repeated) { 182 | i += length - 1; 183 | repeated = false; 184 | } 185 | else { 186 | res += str.charAt(i); 187 | } 188 | } 189 | return res; 190 | } 191 | 192 | /** 193 | * Calculates background colors from percentage value. 194 | * 195 | * @param {int} perc The percentage strength of the password. 196 | * @return {object} Object with colors as keys 197 | */ 198 | function calculateColorFromPercentage(perc) { 199 | var minRed = 0; 200 | var maxRed = 240; 201 | var minGreen = 0; 202 | var maxGreen = 240; 203 | var blue = 10; 204 | 205 | if (Object.prototype.hasOwnProperty.call(options.customColorBarRGB, 'red')) { 206 | minRed = options.customColorBarRGB.red[0]; 207 | maxRed = options.customColorBarRGB.red[1]; 208 | } 209 | 210 | if (Object.prototype.hasOwnProperty.call(options.customColorBarRGB, 'green')) { 211 | minGreen = options.customColorBarRGB.green[0]; 212 | maxGreen = options.customColorBarRGB.green[1]; 213 | } 214 | 215 | if (Object.prototype.hasOwnProperty.call(options.customColorBarRGB, 'blue')) { 216 | blue = options.customColorBarRGB.blue; 217 | } 218 | 219 | var green = (perc * maxGreen / 50); 220 | var red = (2 * maxRed) - (perc * maxRed / 50); 221 | 222 | return { 223 | red: Math.min(Math.max(red, minRed), maxRed), 224 | green: Math.min(Math.max(green, minGreen), maxGreen), 225 | blue: blue 226 | } 227 | } 228 | 229 | /** 230 | * Adds color styles to colorbar jQuery object. 231 | * 232 | * @param {jQuery} $colorbar The colorbar jquery object. 233 | * @param {int} perc The percentage strength of the password. 234 | * @return {jQuery} 235 | */ 236 | function addColorBarStyle($colorbar, perc) { 237 | if (options.useColorBarImage) { 238 | $colorbar.css({ 239 | backgroundPosition: "0px -" + perc + "px", 240 | width: perc + '%' 241 | }); 242 | } 243 | else { 244 | var colors = calculateColorFromPercentage(perc); 245 | 246 | $colorbar.css({ 247 | 'background-image': 'none', 248 | 'background-color': 'rgb(' + colors.red.toString() + ', ' + colors.green.toString() + ', ' + colors.blue.toString() + ')', 249 | width: perc + '%' 250 | }); 251 | } 252 | 253 | return $colorbar; 254 | } 255 | 256 | /** 257 | * Initializes the plugin creating and binding the 258 | * required layers and events. 259 | * 260 | * @return {Password} Returns the Password instance. 261 | */ 262 | function init() { 263 | var shown = true; 264 | var $text = options.showText; 265 | var $percentage = options.showPercent; 266 | var $graybar = $('
').addClass('pass-graybar'); 267 | var $colorbar = $('
').addClass('pass-colorbar'); 268 | var $insert = $('
').addClass('pass-wrapper').append( 269 | $graybar.append($colorbar) 270 | ); 271 | 272 | $object.closest(options.closestSelector).addClass('pass-strength-visible'); 273 | if (options.animate) { 274 | $insert.css('display', 'none'); 275 | shown = false; 276 | $object.closest(options.closestSelector).removeClass('pass-strength-visible'); 277 | } 278 | 279 | if (options.showPercent) { 280 | $percentage = $('').addClass('pass-percent').text('0%'); 281 | $insert.append($percentage); 282 | } 283 | 284 | if (options.showText) { 285 | $text = $('').addClass('pass-text').html(options.enterPass); 286 | $insert.append($text); 287 | } 288 | 289 | $object.closest(options.closestSelector).append($insert); 290 | 291 | $object.keyup(function() { 292 | var field = options.field || ''; 293 | if (field) { 294 | field = $(field).val(); 295 | } 296 | 297 | var score = calculateScore($object.val(), field); 298 | $object.trigger('password.score', [score]); 299 | var perc = score < 0 ? 0 : score; 300 | 301 | $colorbar = addColorBarStyle($colorbar, perc); 302 | 303 | if (options.showPercent) { 304 | $percentage.html(perc + '%'); 305 | } 306 | 307 | if (options.showText) { 308 | var text = scoreText(score); 309 | if (!$object.val().length && score <= 0) { 310 | text = options.enterPass; 311 | } 312 | 313 | if ($text.html() !== $('
').html(text).html()) { 314 | $text.html(text); 315 | $object.trigger('password.text', [text, score]); 316 | } 317 | } 318 | }); 319 | 320 | if (options.animate) { 321 | $object.focus(function() { 322 | if (!shown) { 323 | $insert.slideDown(options.animateSpeed, function () { 324 | shown = true; 325 | $object.closest(options.closestSelector).addClass('pass-strength-visible'); 326 | }); 327 | } 328 | }); 329 | 330 | $object.blur(function() { 331 | if (!$object.val().length && shown) { 332 | $insert.slideUp(options.animateSpeed, function () { 333 | shown = false; 334 | $object.closest(options.closestSelector).removeClass('pass-strength-visible') 335 | }); 336 | } 337 | }); 338 | } 339 | 340 | return this; 341 | } 342 | 343 | return init.call(this); 344 | }; 345 | 346 | // Bind to jquery 347 | $.fn.password = function(options) { 348 | return this.each(function() { 349 | new Password($(this), options); 350 | }); 351 | }; 352 | })(jQuery); -------------------------------------------------------------------------------- /app/static/js/register_ps_check.js: -------------------------------------------------------------------------------- 1 | $('#password').password({ 2 | enterPass: 'Type your password', 3 | shortPass: 'The password is too short', 4 | containsField: 'The password contains your username', 5 | steps: { 6 | // Easily change the steps' expected score here 7 | 13: 'Really insecure password', 8 | 33: 'Weak; try combining letters & numbers', 9 | 67: 'Medium; try using special characters', 10 | 94: 'Strong password', 11 | }, 12 | showPercent: false, 13 | showText: true, // shows the text tips 14 | animate: true, // whether or not to animate the progress bar on input blur/focus 15 | animateSpeed: 'fast', // the above animation speed 16 | field: false, // select the match field (selector or jQuery instance) for better password checks 17 | fieldPartialMatch: true, // whether to check for partials in field 18 | minimumLength: parseInt(document.getElementById('psswd_min_len').textContent), // minimum password length (below this threshold, the score is 0) 19 | maximumLength: parseInt(document.getElementById('psswd_max_len').textContent), 20 | useColorBarImage: true, // use the (old) colorbar image 21 | customColorBarRGB: { 22 | red: [0, 240], 23 | green: [0, 240], 24 | blue: 10, 25 | } // set custom rgb color ranges for colorbar. 26 | }); 27 | -------------------------------------------------------------------------------- /app/templates/auth/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% import 'forms/forms.html' as wtf %} 3 | {% block app_content %} 4 |
5 |
6 | {% include 'navbar/messages.html' %} 7 |
8 |
9 |
10 |
11 |

Log in

12 |
13 | {{ wtf.quick_form(form, button_map={'submit': 'secondary'}) }} 14 |
15 |

Forget Password? 16 | 17 | Find your password now. 18 | 19 |

20 |
21 |
22 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/auth/register.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% import 'forms/forms.html' as wtf %} 3 | 4 | {% block app_content %} 5 |
6 |
7 | {% include 'navbar/messages.html' %} 8 |
9 |
10 |
11 |
12 |
{{min}}
13 |
{{max}}
14 |

Register New User

15 |
16 | {{ wtf.quick_form(form, button_map={'submit': 'secondary'}) }} 17 |
18 |
19 | {% endblock %} 20 | {% block ps %} 21 | 22 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/auth/reset_password.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% import 'forms/forms.html' as wtf %} 3 | {% block app_content %} 4 |
5 |
6 | {% include 'navbar/messages.html' %} 7 |
8 |
9 |
10 |
11 |

Reset Password

12 |
13 | {{ wtf.quick_form(form, button_map={'submit': 'secondary'}) }} 14 |
15 |
16 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/auth/reset_password_request.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% import 'forms/forms.html' as wtf %} 3 | {% block app_content %} 4 |
5 |
6 | {% include 'navbar/messages.html' %} 7 |
8 |
9 |
10 |
11 |

Reset Password

12 |
13 | {{ wtf.quick_form(form, button_map={'submit': 'secondary'}) }} 14 |
15 |
16 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/base.html: -------------------------------------------------------------------------------- 1 | {% extends "bootstrap/bs5_base.html" %} 2 | {% block title %} 3 | {% if title %} 4 | {{ title }} 5 | {% else %} 6 | Flask Basic Login/Register example \w Bootstrap5 7 | {% endif %} 8 | {% endblock %} 9 | 10 | {% block navbar %} 11 | {% include 'navbar/navbar.html' %} 12 | {% endblock %} 13 | 14 | {% block content %} 15 |
16 | {% block app_content %} 17 | {% endblock %} 18 |
19 | {% endblock %} 20 | {% block page_script %} 21 | {% block ps %} 22 | {% endblock %} 23 | {% endblock %} 24 | 25 | -------------------------------------------------------------------------------- /app/templates/bootstrap/bs5_base.html: -------------------------------------------------------------------------------- 1 | {% block doc -%} 2 | 3 | 4 | {%- block html %} 5 | 6 | {%- block head %} 7 | {% block title %}{{title|default}}{% endblock title %} 8 | 9 | {%- block metas %} 10 | 11 | {%- endblock metas %} 12 | 13 | {%- block styles %} 14 | 15 | 16 | 17 | 18 | {% assets "scss_all" %} 19 | 20 | {% endassets %} 21 | {%- endblock styles %} 22 | {%- endblock head %} 23 | 24 | 25 | {% block body -%} 26 | {% block navbar %} 27 | {%- endblock navbar %} 28 | {% block content -%} 29 | {%- endblock content %} 30 | 31 | {% block scripts %} 32 | 33 | 34 | 35 | {% block page_script -%} 36 | {%- endblock page_script %} 37 | {%- endblock scripts %} 38 | {%- endblock body %} 39 | 40 | {%- endblock html %} 41 | 42 | {% endblock doc -%} -------------------------------------------------------------------------------- /app/templates/email/reset_password.html: -------------------------------------------------------------------------------- 1 |

Dear {{ user.username }},

2 |

3 | To reset your password 4 | 5 | click here 6 | . 7 |

8 |

Alternatively, you can paste the following link in your browser's address bar:

9 |

{{ url_for('auth.reset_password', token=token, _external=True) }}

10 |

If you have not requested a password reset simply ignore this message.

11 |

Regards

-------------------------------------------------------------------------------- /app/templates/email/reset_password.txt: -------------------------------------------------------------------------------- 1 | Dear {{ user.username }}, 2 | 3 | To reset your password click on the following link: 4 | 5 | {{ url_for('auth.reset_password', token=token, _external=True) }} 6 | 7 | If you have not requested a password reset simply ignore this message. 8 | 9 | Regards -------------------------------------------------------------------------------- /app/templates/forms/forms.html: -------------------------------------------------------------------------------- 1 | {% macro form_errors(form, hiddens=True) %} 2 | {%- if form.errors %} 3 | {%- for fieldname, errors in form.errors.items() %} 4 | {%- if bootstrap_is_hidden_field(form[fieldname]) and hiddens or 5 | not bootstrap_is_hidden_field(form[fieldname]) and hiddens != 'only' %} 6 | {%- for error in errors %} 7 |

{{error}}

8 | {%- endfor %} 9 | {%- endif %} 10 | {%- endfor %} 11 | {%- endif %} 12 | {%- endmacro %} 13 | 14 | {% macro _hz_form_wrap(horizontal_columns, form_type, add_group=False, required=False) %} 15 | {% if form_type == "horizontal" %} 16 | {% if add_group %}
{% endif %} 17 |
20 | {% endif %} 21 | {{caller()}} 22 | 23 | {% if form_type == "horizontal" %} 24 | {% if add_group %}
{% endif %} 25 |
26 | {% endif %} 27 | {% endmacro %} 28 | 29 | {% macro form_field(field, 30 | form_type="basic", 31 | horizontal_columns=('lg', 2, 10), 32 | button_map={}) %} 33 | 34 | {# this is a workaround hack for the more straightforward-code of just passing required=required parameter. older versions of wtforms do not have 35 | the necessary fix for required=False attributes, but will also not set the required flag in the first place. we skirt the issue using the code below #} 36 | {% if field.flags.required and not required in kwargs %} 37 | {% set kwargs = dict(required=True, **kwargs) %} 38 | {% endif %} 39 | 40 | {% if field.widget.input_type == 'checkbox' %} 41 | {% call _hz_form_wrap(horizontal_columns, form_type, True, required=required) %} 42 |
43 | 46 |
47 | {% endcall %} 48 | {%- elif field.type == 'RadioField' -%} 49 | {# note: A cleaner solution would be rendering depending on the widget, 50 | this is just a hack for now, until I can think of something better #} 51 | {% call _hz_form_wrap(horizontal_columns, form_type, True, required=required) %} 52 | {% for item in field -%} 53 |
54 | 57 |
58 | {% endfor %} 59 | {% endcall %} 60 | {%- elif field.type == 'SubmitField' -%} 61 | {# deal with jinja scoping issues? #} 62 | {% set field_kwargs = kwargs %} 63 | 64 | {# note: same issue as above - should check widget, not field type #} 65 | {% call _hz_form_wrap(horizontal_columns, form_type, True, required=required) %} 66 | {{field(class='btn btn-%s' % button_map.get(field.name, 'default'), 67 | **field_kwargs)}} 68 | {% endcall %} 69 | {%- elif field.type == 'FormField' -%} 70 | {# note: FormFields are tricky to get right and complex setups requiring 71 | these are probably beyond the scope of what this macro tries to do. 72 | the code below ensures that things don't break horribly if we run into 73 | one, but does not try too hard to get things pretty. #} 74 |
75 | {{field.label}} 76 | {%- for subfield in field %} 77 | {% if not bootstrap_is_hidden_field(subfield) -%} 78 | {{ form_field(subfield, 79 | form_type=form_type, 80 | horizontal_columns=horizontal_columns, 81 | button_map=button_map) }} 82 | {%- endif %} 83 | {%- endfor %} 84 |
85 | {% else -%} 86 |
87 | {%- if form_type == "inline" %} 88 | {{field.label(class="sr-only")|safe}} 89 | {% if field.type == 'FileField' %} 90 | {{field(**kwargs)|safe}} 91 | {% else %} 92 | {{field(class="form-control", **kwargs)|safe}} 93 | {% endif %} 94 | {% elif form_type == "horizontal" %} 95 | {{field.label(class="control-label " + ( 96 | " col-%s-%s" % horizontal_columns[0:2] 97 | ))|safe}} 98 |
99 | {% if field.type == 'FileField' %} 100 | {{field(**kwargs)|safe}} 101 | {% else %} 102 | {{field(class="form-control", **kwargs)|safe}} 103 | {% endif %} 104 |
105 | {%- if field.errors %} 106 | {%- for error in field.errors %} 107 | {% call _hz_form_wrap(horizontal_columns, form_type, required=required) %} 108 |

{{error}}

109 | {% endcall %} 110 | {%- endfor %} 111 | {%- elif field.description -%} 112 | {% call _hz_form_wrap(horizontal_columns, form_type, required=required) %} 113 |

{{field.description|safe}}

114 | {% endcall %} 115 | {%- endif %} 116 | {%- else -%} 117 | {{field.label(class="control-label")|safe}} 118 | {% if field.type == 'FileField' %} 119 | {{field(**kwargs)|safe}} 120 | {% else %} 121 | {{field(class="form-control", **kwargs)|safe}} 122 | {% endif %} 123 | 124 | {%- if field.errors %} 125 | {%- for error in field.errors %} 126 |

{{error}}

127 | {%- endfor %} 128 | {%- elif field.description -%} 129 |

{{field.description|safe}}

130 | {%- endif %} 131 | {%- endif %} 132 |
133 | {% endif %} 134 | {% endmacro %} 135 | 136 | {# valid form types are "basic", "inline" and "horizontal" #} 137 | {% macro quick_form(form, 138 | action="", 139 | method="post", 140 | extra_classes=None, 141 | role="form", 142 | form_type="basic", 143 | horizontal_columns=('lg', 2, 10), 144 | enctype=None, 145 | button_map={}, 146 | id="") %} 147 | {#- 148 | action="" is what we want, from http://www.ietf.org/rfc/rfc2396.txt: 149 | 150 | 4.2. Same-document References 151 | 152 | A URI reference that does not contain a URI is a reference to the 153 | current document. In other words, an empty URI reference within a 154 | document is interpreted as a reference to the start of that document, 155 | and a reference containing only a fragment identifier is a reference 156 | to the identified fragment of that document. Traversal of such a 157 | reference should not result in an additional retrieval action. 158 | However, if the URI reference occurs in a context that is always 159 | intended to result in a new request, as in the case of HTML's FORM 160 | element, then an empty URI reference represents the base URI of the 161 | current document and should be replaced by that URI when transformed 162 | into a request. 163 | 164 | -#} 165 | {#- if any file fields are inside the form and enctype is automatic, adjust 166 | if file fields are found. could really use the equalto test of jinja2 167 | here, but latter is not available until 2.8 168 | 169 | warning: the code below is guaranteed to make you cry =( 170 | #} 171 | {%- set _enctype = [] %} 172 | {%- if enctype is none -%} 173 | {%- for field in form %} 174 | {%- if field.type == 'FileField' %} 175 | {#- for loops come with a fairly watertight scope, so this list-hack is 176 | used to be able to set values outside of it #} 177 | {%- set _ = _enctype.append('multipart/form-data') -%} 178 | {%- endif %} 179 | {%- endfor %} 180 | {%- else %} 181 | {% set _ = _enctype.append(enctype) %} 182 | {%- endif %} 183 |
196 | {{ form.hidden_tag() }} 197 | {{ form_errors(form, hiddens='only') }} 198 | 199 | {%- for field in form %} 200 | {% if not bootstrap_is_hidden_field(field) -%} 201 | {{ form_field(field, 202 | form_type=form_type, 203 | horizontal_columns=horizontal_columns, 204 | button_map=button_map) }} 205 | {%- endif %} 206 | {%- endfor %} 207 | 208 |
209 | {%- endmacro %} -------------------------------------------------------------------------------- /app/templates/main/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block app_content %} 3 |
4 |
5 | {% include 'navbar/messages.html' %} 6 |
7 |
8 |

Hello {{ current_user.username }}!

9 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/navbar/messages.html: -------------------------------------------------------------------------------- 1 | {%- with messages = get_flashed_messages(with_categories=true) -%} 2 | {% if messages %} 3 | {% for category, message in messages %} 4 | {% if category == 'message' %} 5 |
{{ message }}
6 | {% else %} 7 |
{{ message }}
8 | {% endif %} 9 | {% endfor %} 10 | {% endif %} 11 | {%- endwith %} -------------------------------------------------------------------------------- /app/templates/navbar/navbar.html: -------------------------------------------------------------------------------- 1 | 30 | -------------------------------------------------------------------------------- /app/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from app.utils.app_logger import _log_message_ -------------------------------------------------------------------------------- /app/utils/app_logger.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import logging 3 | from app.utils.decorators import singleton 4 | from logging.handlers import RotatingFileHandler 5 | 6 | @singleton 7 | class AppLogger: 8 | 9 | def __init__(self,**kwargs): 10 | self.logger = logging.getLogger() 11 | self.logger.setLevel(logging.INFO) 12 | formatter = logging.Formatter('%(asctime)s | %(levelname)s | %(message)s') 13 | 14 | stdout_handler = logging.StreamHandler(sys.stdout) 15 | stdout_handler.setLevel(logging.DEBUG) 16 | stdout_handler.setFormatter(formatter) 17 | 18 | file_handler = RotatingFileHandler(kwargs.get('log_filename'), 19 | mode='a', 20 | maxBytes=5 * 1024 * 1024, 21 | backupCount=2, 22 | encoding=None, 23 | delay=0) 24 | file_handler.setLevel(logging.DEBUG) 25 | file_handler.setFormatter(formatter) 26 | 27 | self.logger.addHandler(file_handler) 28 | self.logger.addHandler(stdout_handler) 29 | 30 | def message_handle(self, **kwargs): 31 | """ 32 | this is the logging/message handler function for the entire application 33 | :param kwargs: here the kwargs contains `msg` param that contains the message as string :TODO add params for type/severity 34 | :return: N/A 35 | """ 36 | debug_level = kwargs.get("level", "info") 37 | if debug_level == "info": 38 | self.logger.info(kwargs.get("msg")) 39 | if debug_level == "error": 40 | self.logger.error(kwargs.get("msg")) 41 | 42 | 43 | _log_message_ = AppLogger(log_filename='logs.log') -------------------------------------------------------------------------------- /app/utils/decorators.py: -------------------------------------------------------------------------------- 1 | class _SingletonWrapper: 2 | """ 3 | A singleton wrapper class. Its instances would be created 4 | for each decorated class. 5 | """ 6 | 7 | def __init__(self, cls): 8 | self.__wrapped__ = cls 9 | self._instance = None 10 | 11 | def __call__(self, *args, **kwargs): 12 | """Returns a single instance of decorated class""" 13 | if self._instance is None: 14 | self._instance = self.__wrapped__(*args, **kwargs) 15 | return self._instance 16 | 17 | def singleton(cls): 18 | """ 19 | A singleton decorator. Returns a wrapper objects. A call on that object 20 | returns a single instance object of decorated class. Use the __wrapped__ 21 | attribute to access decorated class directly in unit tests 22 | """ 23 | return _SingletonWrapper(cls) 24 | -------------------------------------------------------------------------------- /app/utils/mailer.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | from flask import current_app 3 | from flask_mail import Message 4 | from app import mail 5 | 6 | 7 | def send_async_email(app, msg): 8 | with app.app_context(): 9 | mail.send(msg) 10 | 11 | 12 | def send_email(subject, sender, recipients, text_body, html_body, 13 | attachments=None, sync=False): 14 | msg = Message(subject, sender=sender, recipients=recipients) 15 | msg.body = text_body 16 | msg.html = html_body 17 | if attachments: 18 | for attachment in attachments: 19 | msg.attach(*attachment) 20 | if sync: 21 | mail.send(msg) 22 | else: 23 | Thread(target=send_async_email, 24 | args=(current_app._get_current_object(), msg)).start() 25 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | web: 5 | build: 6 | context: ./ 7 | networks: 8 | my-network: 9 | aliases: 10 | - web-app 11 | expose: 12 | - "5000" 13 | nginx: 14 | restart: always 15 | build: 16 | context: ./nginx 17 | networks: 18 | - my-network 19 | depends_on: 20 | - web 21 | ports: 22 | - "80:80" 23 | 24 | networks: 25 | my-network: -------------------------------------------------------------------------------- /env.sample: -------------------------------------------------------------------------------- 1 | # True for development, False for production 2 | DEBUG=True 3 | 4 | # Flask ENV 5 | FLASK_APP=run.py 6 | FLASK_ENV=development 7 | SECRET_KEY=YOUR_SUPER_KEY 8 | -------------------------------------------------------------------------------- /nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:alpine 2 | RUN rm /etc/nginx/conf.d/default.conf 3 | ADD sites-enabled/app.conf /etc/nginx/conf.d/default.conf -------------------------------------------------------------------------------- /nginx/sites-enabled/app.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | 5 | location / { 6 | proxy_read_timeout 300s; 7 | proxy_connect_timeout 75s; 8 | proxy_pass http://web-app:5000; 9 | proxy_set_header Host "localhost"; 10 | } 11 | } -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | bcrypt==3.2.0 2 | blinker==1.4 3 | Bootstrap-Flask==2.0.2 4 | cachelib==0.6.0 5 | cffi==1.15.0 6 | click==8.1.2 7 | colorama==0.4.4 8 | commonmark==0.9.1 9 | Deprecated==1.2.13 10 | dnspython==2.2.1 11 | email-validator==1.2.0 12 | Flask==2.1.1 13 | Flask-Assets==2.0 14 | Flask-Bcrypt==1.0.1 15 | Flask-Limiter==2.4.5.1 16 | Flask-Login==0.6.0 17 | Flask-Mail==0.9.1 18 | Flask-Minify==0.39 19 | Flask-SeaSurf==1.1.1 20 | Flask-Session==0.4.0 21 | Flask-SQLAlchemy==2.5.1 22 | flask-talisman==1.0.0 23 | Flask-WTF==1.0.1 24 | greenlet==1.1.2 25 | htmlmin==0.1.12 26 | idna==3.3 27 | is-safe-url==1.0 28 | itsdangerous==2.1.2 29 | Jinja2==3.1.1 30 | jsmin==3.0.1 31 | lesscpy==0.15.0 32 | limits==2.6.1 33 | MarkupSafe==2.1.1 34 | packaging==21.3 35 | ply==3.11 36 | pycparser==2.21 37 | Pygments==2.12.0 38 | PyJWT==2.3.0 39 | pyparsing==3.0.8 40 | pyScss==1.4.0 41 | rcssmin==1.1.0 42 | rich==12.3.0 43 | six==1.16.0 44 | SQLAlchemy==1.4.36 45 | typing_extensions==4.2.0 46 | validate-email-address==1 47 | webassets==2.0 48 | Werkzeug==2.1.1 49 | wrapt==1.14.0 50 | WTForms==3.0.1 51 | xxhash==3.0.0 52 | python-dotenv==0.19.2 53 | uwsgi -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | import os 2 | from app import create_app, db 3 | 4 | app = create_app(config_name='development') 5 | with app.app_context(): 6 | db.create_all() 7 | 8 | if __name__ == '__main__': 9 | port = os.environ.get("PORT", 5000) 10 | app.run(host='0.0.0.0', port=port, debug=False) 11 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from app import create_app, db, bcrypt 4 | from app.auth.models import User 5 | 6 | 7 | class TestUserLogin(unittest.TestCase): 8 | 9 | def setUp(self): 10 | self.app = create_app("testing") 11 | self.app_context = self.app.app_context() 12 | self.app_context.push() 13 | db.create_all() 14 | 15 | def tearDown(self): 16 | db.session.remove() 17 | db.drop_all() 18 | self.app_context.pop() 19 | 20 | def test_add_user(self): 21 | user = User(username='username', email='user@email.com', password='password') 22 | db.session.add(user) 23 | db.session.commit() 24 | assert len(User.query.all()) == 1 25 | 26 | def test_check_password(self): 27 | user = User(username='username', email='user@email.com', password='password') 28 | db.session.add(user) 29 | db.session.commit() 30 | user = User.query.filter_by(username='username').first() 31 | assert bcrypt.check_password_hash(user.password, 'password') 32 | 33 | 34 | if __name__ == '__main__': 35 | unittest.main() -------------------------------------------------------------------------------- /uwsgi_config.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | module = run:app 3 | http-socket = :$(PORT) 4 | master = true 5 | processes = 4 6 | die-on-term = true 7 | memory-report = true --------------------------------------------------------------------------------