diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..72e294c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.git +__pycache__ +*.pyc +*.pyo +*.pyd \ No newline at end of file diff --git a/.env b/.env new file mode 100644 index 0000000..b5269d5 --- /dev/null +++ b/.env @@ -0,0 +1,13 @@ +DEBUG=True + +FLASK_APP=run.py +FLASK_ENV=development + +ASSETS_ROOT=/static/assets + +# DB_ENGINE=mysql +# DB_HOST=localhost +# DB_NAME=appseed_db +# DB_USERNAME=appseed_db_usr +# DB_PASS=pass +# DB_PORT=3306 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9fc790f --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# tests and coverage +*.pytest_cache +.coverage + +# database & logs +*.db +*.sqlite3 +*.log + +# venv +env +venv + +# other +.DS_Store + +# sphinx docs +_build +_static +_templates + +# javascript +package-lock.json +.vscode/symbols.json + +apps/static/assets/node_modules +apps/static/assets/yarn.lock +apps/static/assets/.temp + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1cabee4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,58 @@ +# Change Log + +## [1.0.6] 2021-06-09 +### Improvements + +- Built with [Atlantis Dark Generator](https://appseed.us/generator/atlantis-dark/) + - Timestamp: `2022-06-09 11:53` + +## [1.0.5] 2022-01-16 +### Improvements + +- Bump Flask Codebase to [v2stable.0.1](https://github.com/app-generator/boilerplate-code-flask-dashboard/releases) +- Dependencies update (all packages) + - Flask==2.0.2 (latest stable version) + - flask_wtf==1.0.0 + - jinja2==3.0.3 + - flask-restx==0.5.1 +- Forms Update: + - Replace `TextField` (deprecated) with `StringField` + +## [1.0.4] 2021-11-08 +### Improvements + +- Bump Codebase: [Flask Dashboard](https://github.com/app-generator/boilerplate-code-flask-dashboard) v2.0.0 + - Dependencies update (all packages) + - Flask==2.0.1 (latest stable version) +- Better Code formatting +- Improved Files organization +- Optimize imports +- Docker Scripts Update +- Gulp Tooling (SASS Compilation) +- Fix **ImportError: cannot import name 'TextField' from 'wtforms'** + - Problem caused by `WTForms-3.0.0` + - Fix: use **WTForms==2.3.3** + +## [1.0.3] 2021-05-16 +### Dependencies Update + +- Bump Codebase: [Flask Dashboard](https://github.com/app-generator/boilerplate-code-flask-dashboard) v1.0.6 +- Freeze used versions in `requirements.txt` + - jinja2 = 2.11.3 + +## [1.0.2] 2021-03-18 +### Improvements + +- Bump Codebase: [Flask Dashboard](https://github.com/app-generator/boilerplate-code-flask-dashboard) v1.0.5 +- Freeze used versions in `requirements.txt` + - flask_sqlalchemy = 2.4.4 + - sqlalchemy = 1.3.23 + +## [1.0.1] 2021-01-21 + +- Bump UI: [Jinja Atlantis Dark](https://github.com/app-generator/jinja-atlantis-dark/releases) 1.0.1 +- [Atlantis Lite](https://github.com/themekita/Atlantis-Lite): 2021-01-05 Snapshot +- Codebase: [Flask Dashboard](https://github.com/app-generator/boilerplate-code-flask-dashboard/releases) v1.0.4 + +## [1.0.0] 2020-02-07 +### Initial Release diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5568250 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.9 + +# set environment variables +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +COPY requirements.txt . + +# install python dependencies +RUN pip install --upgrade pip +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +# gunicorn +CMD ["gunicorn", "--config", "gunicorn-cfg.py", "run:app"] diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..5012dd9 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,32 @@ +# MIT License + +Copyright (c) 2019 - present [AppSeed](http://appseed.us/) + +
+ +## Licensing Information + +
+ +| Item | - | +| ---------------------------------- | --- | +| License Type | MIT | +| Use for print | **YES** | +| Create single personal website/app | **YES** | +| Create single website/app for client | **YES** | +| Create multiple website/apps for clients | **YES** | +| Create multiple SaaS applications | **YES** | +| End-product paying users | **YES** | +| Product sale | **YES** | +| Remove footer credits | **YES** | +| --- | --- | +| Remove copyright mentions from source code | NO | +| Production deployment assistance | NO | +| Create HTML/CSS template for sale | NO | +| Create Theme/Template for CMS for sale | NO | +| Separate sale of our UI Elements | NO | + +
+ +--- +For more information regarding licensing, please contact the AppSeed Service < *support@appseed.us* > diff --git a/README.md b/README.md new file mode 100644 index 0000000..39572be --- /dev/null +++ b/README.md @@ -0,0 +1,204 @@ +# [Atlantis Dark Flask](https://appseed.us/product/atlantis-dark/flask/) + +Open-source **Flask Dashboard** generated by `AppSeed` on top of a modern `Bootstrap` design. **[Atlantis Dark](https://appseed.us/product/atlantis-dark/flask/)** is a free bootstrap 4 admin dashboard that is beautifully and elegantly designed to display various metrics, numbers or data visualization. Atlantis Lite admin dashboard has 2 layouts, many plugins and UI components to help developers create dashboards quickly and effectively so they can save development time and also help users to make the right and fast decisions based on existing data. + +- 👉 [Atlantis Dark Flask](https://appseed.us/product/atlantis-dark/flask/) - Product page +- 👉 [Atlantis Dark Flask](https://flask-atlantis-dark.appseed-srv1.com/) - LIVE Demo +- 👉 [Complete documentation](https://docs.appseed.us/products/flask-dashboards/atlantis-dark) - `Learn how to use and update the product` + +
+ +> 🚀 Built with [App Generator](https://appseed.us/generator/), Timestamp: `2022-06-09 11:53` + +- `Up-to-date dependencies` +- Database: `sqlite` +- `DB Tools`: SQLAlchemy ORM, Flask-Migrate (schema migrations) +- Session-Based authentication (via **flask_login**), Forms validation + +
+ +![Atlantis Dark - Starter generated by AppSeed.](https://user-images.githubusercontent.com/51070104/172799909-4cbc8eed-fdde-4408-ab61-123f235212d0.png) + +
+ +## ✨ Start the app in Docker + +> **Step 1** - Download the code from the GH repository (using `GIT`) + +```bash +$ git clone https://github.com/app-generator/flask-atlantis-dark.git +$ cd flask-atlantis-dark +``` + +
+ +> **Step 2** - Start the APP in `Docker` + +```bash +$ docker-compose up --build +``` + +Visit `http://localhost:5085` in your browser. The app should be up & running. + +
+ +## ✨ How to use it + +> Download the code + +```bash +$ git clone https://github.com/app-generator/flask-atlantis-dark.git +$ cd flask-atlantis-dark +``` + +
+ +### 👉 Set Up for `Unix`, `MacOS` + +> Install modules via `VENV` + +```bash +$ virtualenv env +$ source env/bin/activate +$ pip3 install -r requirements.txt +``` + +
+ +> Set Up Flask Environment + +```bash +$ export FLASK_APP=run.py +$ export FLASK_ENV=development +``` + +
+ +> Start the app + +```bash +$ flask run +``` + +At this point, the app runs at `http://127.0.0.1:5000/`. + +
+ +### 👉 Set Up for `Windows` + +> Install modules via `VENV` (windows) + +``` +$ virtualenv env +$ .\env\Scripts\activate +$ pip3 install -r requirements.txt +``` + +
+ +> Set Up Flask Environment + +```bash +$ # CMD +$ set FLASK_APP=run.py +$ set FLASK_ENV=development +$ +$ # Powershell +$ $env:FLASK_APP = ".\run.py" +$ $env:FLASK_ENV = "development" +``` + +
+ +> Start the app + +```bash +$ flask run +``` + +At this point, the app runs at `http://127.0.0.1:5000/`. + +
+ +### 👉 Create Users + +By default, the app redirects guest users to authenticate. In order to access the private pages, follow this set up: + +- Start the app via `flask run` +- Access the `registration` page and create a new user: + - `http://127.0.0.1:5000/register` +- Access the `sign in` page and authenticate + - `http://127.0.0.1:5000/login` + +
+ +## ✨ Code-base structure + +The project is coded using blueprints, app factory pattern, dual configuration profile (development and production) and an intuitive structure presented bellow: + +```bash +< PROJECT ROOT > + | + |-- apps/ + | | + | |-- home/ # A simple app that serve HTML files + | | |-- routes.py # Define app routes + | | + | |-- authentication/ # Handles auth routes (login and register) + | | |-- routes.py # Define authentication routes + | | |-- models.py # Defines models + | | |-- forms.py # Define auth forms (login and register) + | | + | |-- static/ + | | |-- # CSS files, Javascripts files + | | + | |-- templates/ # Templates used to render pages + | | |-- includes/ # HTML chunks and components + | | | |-- navigation.html # Top menu component + | | | |-- sidebar.html # Sidebar component + | | | |-- footer.html # App Footer + | | | |-- scripts.html # Scripts common to all pages + | | | + | | |-- layouts/ # Master pages + | | | |-- base-fullscreen.html # Used by Authentication pages + | | | |-- base.html # Used by common pages + | | | + | | |-- accounts/ # Authentication pages + | | | |-- login.html # Login page + | | | |-- register.html # Register page + | | | + | | |-- home/ # UI Kit Pages + | | |-- index.html # Index page + | | |-- 404-page.html # 404 page + | | |-- *.html # All other pages + | | + | config.py # Set up the app + | __init__.py # Initialize the app + | + |-- requirements.txt # App Dependencies + | + |-- .env # Inject Configuration via Environment + |-- run.py # Start the app - WSGI gateway + | + |-- ************************************************************************ +``` + +
+ +## ✨ PRO Version + +> For more components, pages and priority on support, feel free to take a look at this amazing starter: + +Black Dashboard is a premium Bootstrap Design now available for download in Flask. Made of hundred of elements, designed blocks, and fully coded pages, Black Dashboard PRO is ready to help you create stunning websites and web apps. + +- 👉 [Atlantis Dark PRO Flask](https://appseed.us/product/atlantis-dark-pro/flask/) - Product Page +- 👉 [Atlantis Dark PRO Flask](https://flask-atlantis-dark-pro.appseed-srv1.com/) - LIVE Demo + +
+ +![Atlantis Dark PRO - Starter generated by AppSeed.](https://user-images.githubusercontent.com/51070104/172800034-4d3adb79-d05e-430d-8ffe-f6860fc755f1.png) + +
+ +--- +Atlantis Lite Flask - Open-source starter generated by **[AppSeed Generator](https://appseed.us/generator/)**. diff --git a/apps/__init__.py b/apps/__init__.py new file mode 100644 index 0000000..f23857d --- /dev/null +++ b/apps/__init__.py @@ -0,0 +1,44 @@ +# -*- encoding: utf-8 -*- +""" +Copyright (c) 2019 - present AppSeed.us +""" + +from flask import Flask +from flask_login import LoginManager +from flask_sqlalchemy import SQLAlchemy +from importlib import import_module + + +db = SQLAlchemy() +login_manager = LoginManager() + + +def register_extensions(app): + db.init_app(app) + login_manager.init_app(app) + + +def register_blueprints(app): + for module_name in ('authentication', 'home'): + module = import_module('apps.{}.routes'.format(module_name)) + app.register_blueprint(module.blueprint) + + +def configure_database(app): + + @app.before_first_request + def initialize_database(): + db.create_all() + + @app.teardown_request + def shutdown_session(exception=None): + db.session.remove() + + +def create_app(config): + app = Flask(__name__) + app.config.from_object(config) + register_extensions(app) + register_blueprints(app) + configure_database(app) + return app diff --git a/apps/authentication/__init__.py b/apps/authentication/__init__.py new file mode 100644 index 0000000..25243ec --- /dev/null +++ b/apps/authentication/__init__.py @@ -0,0 +1,12 @@ +# -*- encoding: utf-8 -*- +""" +Copyright (c) 2019 - present AppSeed.us +""" + +from flask import Blueprint + +blueprint = Blueprint( + 'authentication_blueprint', + __name__, + url_prefix='' +) diff --git a/apps/authentication/forms.py b/apps/authentication/forms.py new file mode 100644 index 0000000..89f43da --- /dev/null +++ b/apps/authentication/forms.py @@ -0,0 +1,31 @@ +# -*- encoding: utf-8 -*- +""" +Copyright (c) 2019 - present AppSeed.us +""" + +from flask_wtf import FlaskForm +from wtforms import StringField, PasswordField +from wtforms.validators import Email, DataRequired + +# login and registration + + +class LoginForm(FlaskForm): + username = StringField('Username', + id='username_login', + validators=[DataRequired()]) + password = PasswordField('Password', + id='pwd_login', + validators=[DataRequired()]) + + +class CreateAccountForm(FlaskForm): + username = StringField('Username', + id='username_create', + validators=[DataRequired()]) + email = StringField('Email', + id='email_create', + validators=[DataRequired(), Email()]) + password = PasswordField('Password', + id='pwd_create', + validators=[DataRequired()]) diff --git a/apps/authentication/models.py b/apps/authentication/models.py new file mode 100644 index 0000000..cf152ef --- /dev/null +++ b/apps/authentication/models.py @@ -0,0 +1,48 @@ +# -*- encoding: utf-8 -*- +""" +Copyright (c) 2019 - present AppSeed.us +""" + +from flask_login import UserMixin + +from apps import db, login_manager + +from apps.authentication.util import hash_pass + +class Users(db.Model, UserMixin): + + __tablename__ = 'Users' + + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(64), unique=True) + email = db.Column(db.String(64), unique=True) + password = db.Column(db.LargeBinary) + + def __init__(self, **kwargs): + for property, value in kwargs.items(): + # depending on whether value is an iterable or not, we must + # unpack it's value (when **kwargs is request.form, some values + # will be a 1-element list) + if hasattr(value, '__iter__') and not isinstance(value, str): + # the ,= unpack of a singleton fails PEP8 (travis flake8 test) + value = value[0] + + if property == 'password': + value = hash_pass(value) # we need bytes here (not plain str) + + setattr(self, property, value) + + def __repr__(self): + return str(self.username) + + +@login_manager.user_loader +def user_loader(id): + return Users.query.filter_by(id=id).first() + + +@login_manager.request_loader +def request_loader(request): + username = request.form.get('username') + user = Users.query.filter_by(username=username).first() + return user if user else None diff --git a/apps/authentication/routes.py b/apps/authentication/routes.py new file mode 100644 index 0000000..c2a4d42 --- /dev/null +++ b/apps/authentication/routes.py @@ -0,0 +1,123 @@ +# -*- encoding: utf-8 -*- +""" +Copyright (c) 2019 - present AppSeed.us +""" + +from flask import render_template, redirect, request, url_for +from flask_login import ( + current_user, + login_user, + logout_user +) + +from apps import db, login_manager +from apps.authentication import blueprint +from apps.authentication.forms import LoginForm, CreateAccountForm +from apps.authentication.models import Users + +from apps.authentication.util import verify_pass + + +@blueprint.route('/') +def route_default(): + return redirect(url_for('authentication_blueprint.login')) + + +# Login & Registration + +@blueprint.route('/login', methods=['GET', 'POST']) +def login(): + login_form = LoginForm(request.form) + if 'login' in request.form: + + # read form data + username = request.form['username'] + password = request.form['password'] + + # Locate user + user = Users.query.filter_by(username=username).first() + + # Check the password + if user and verify_pass(password, user.password): + + login_user(user) + return redirect(url_for('authentication_blueprint.route_default')) + + # Something (user or pass) is not ok + return render_template('accounts/login.html', + msg='Wrong user or password', + form=login_form) + + if not current_user.is_authenticated: + return render_template('accounts/login.html', + form=login_form) + return redirect(url_for('home_blueprint.index')) + + +@blueprint.route('/register', methods=['GET', 'POST']) +def register(): + create_account_form = CreateAccountForm(request.form) + if 'register' in request.form: + + username = request.form['username'] + email = request.form['email'] + + # Check usename exists + user = Users.query.filter_by(username=username).first() + if user: + return render_template('accounts/register.html', + msg='Username already registered', + success=False, + form=create_account_form) + + # Check email exists + user = Users.query.filter_by(email=email).first() + if user: + return render_template('accounts/register.html', + msg='Email already registered', + success=False, + form=create_account_form) + + # else we can create the user + user = Users(**request.form) + db.session.add(user) + db.session.commit() + + # Delete user from session + logout_user() + + return render_template('accounts/register.html', + msg='User created successfully.', + success=True, + form=create_account_form) + + else: + return render_template('accounts/register.html', form=create_account_form) + + +@blueprint.route('/logout') +def logout(): + logout_user() + return redirect(url_for('authentication_blueprint.login')) + + +# Errors + +@login_manager.unauthorized_handler +def unauthorized_handler(): + return render_template('home/page-403.html'), 403 + + +@blueprint.errorhandler(403) +def access_forbidden(error): + return render_template('home/page-403.html'), 403 + + +@blueprint.errorhandler(404) +def not_found_error(error): + return render_template('home/page-404.html'), 404 + + +@blueprint.errorhandler(500) +def internal_error(error): + return render_template('home/page-500.html'), 500 diff --git a/apps/authentication/util.py b/apps/authentication/util.py new file mode 100644 index 0000000..9130da8 --- /dev/null +++ b/apps/authentication/util.py @@ -0,0 +1,34 @@ +# -*- encoding: utf-8 -*- +""" +Copyright (c) 2019 - present AppSeed.us +""" + +import os +import hashlib +import binascii + +# Inspiration -> https://www.vitoshacademy.com/hashing-passwords-in-python/ + + +def hash_pass(password): + """Hash a password for storing.""" + + salt = hashlib.sha256(os.urandom(60)).hexdigest().encode('ascii') + pwdhash = hashlib.pbkdf2_hmac('sha512', password.encode('utf-8'), + salt, 100000) + pwdhash = binascii.hexlify(pwdhash) + return (salt + pwdhash) # return bytes + + +def verify_pass(provided_password, stored_password): + """Verify a stored password against one provided by user""" + + stored_password = stored_password.decode('ascii') + salt = stored_password[:64] + stored_password = stored_password[64:] + pwdhash = hashlib.pbkdf2_hmac('sha512', + provided_password.encode('utf-8'), + salt.encode('ascii'), + 100000) + pwdhash = binascii.hexlify(pwdhash).decode('ascii') + return pwdhash == stored_password diff --git a/apps/config.py b/apps/config.py new file mode 100644 index 0000000..76efb75 --- /dev/null +++ b/apps/config.py @@ -0,0 +1,49 @@ +# -*- encoding: utf-8 -*- +""" +Copyright (c) 2019 - present AppSeed.us +""" + +import os + +class Config(object): + + basedir = os.path.abspath(os.path.dirname(__file__)) + + # Set up the App SECRET_KEY + # SECRET_KEY = config('SECRET_KEY' , default='S#perS3crEt_007') + SECRET_KEY = os.getenv('SECRET_KEY', 'S#perS3crEt_007') + + # This will create a file in FOLDER + SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'db.sqlite3') + SQLALCHEMY_TRACK_MODIFICATIONS = False + + # Assets Management + ASSETS_ROOT = os.getenv('ASSETS_ROOT', '/static/assets') + +class ProductionConfig(Config): + DEBUG = False + + # Security + SESSION_COOKIE_HTTPONLY = True + REMEMBER_COOKIE_HTTPONLY = True + REMEMBER_COOKIE_DURATION = 3600 + + # PostgreSQL database + SQLALCHEMY_DATABASE_URI = '{}://{}:{}@{}:{}/{}'.format( + os.getenv('DB_ENGINE' , 'mysql'), + os.getenv('DB_USERNAME' , 'appseed_db_usr'), + os.getenv('DB_PASS' , 'pass'), + os.getenv('DB_HOST' , 'localhost'), + os.getenv('DB_PORT' , 3306), + os.getenv('DB_NAME' , 'appseed_db') + ) + +class DebugConfig(Config): + DEBUG = True + + +# Load all possible configurations +config_dict = { + 'Production': ProductionConfig, + 'Debug' : DebugConfig +} diff --git a/apps/home/__init__.py b/apps/home/__init__.py new file mode 100644 index 0000000..dec76db --- /dev/null +++ b/apps/home/__init__.py @@ -0,0 +1,12 @@ +# -*- encoding: utf-8 -*- +""" +Copyright (c) 2019 - present AppSeed.us +""" + +from flask import Blueprint + +blueprint = Blueprint( + 'home_blueprint', + __name__, + url_prefix='' +) diff --git a/apps/home/routes.py b/apps/home/routes.py new file mode 100644 index 0000000..49ca9ba --- /dev/null +++ b/apps/home/routes.py @@ -0,0 +1,54 @@ +# -*- encoding: utf-8 -*- +""" +Copyright (c) 2019 - present AppSeed.us +""" + +from apps.home import blueprint +from flask import render_template, request +from flask_login import login_required +from jinja2 import TemplateNotFound + + +@blueprint.route('/index') +@login_required +def index(): + + return render_template('home/index.html', segment='index') + + +@blueprint.route('/