├── .gitattributes ├── .gitignore ├── .replit ├── Procfile ├── README.md ├── app.py ├── requirements.txt ├── runtime.txt ├── scripts ├── __init__.py ├── forms.py ├── helpers.py └── tabledef.py ├── static ├── css │ └── style.css └── js │ └── scripts.js └── templates ├── base.html ├── home.html ├── login.html ├── navbar.html └── settings.html /.gitattributes: -------------------------------------------------------------------------------- 1 | *.html linguist-vendored -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | accounts.db 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # IPython 78 | profile_default/ 79 | ipython_config.py 80 | 81 | # pyenv 82 | .python-version 83 | 84 | # celery beat schedule file 85 | celerybeat-schedule 86 | 87 | # SageMath parsed files 88 | *.sage.py 89 | 90 | # Environments 91 | .env 92 | .venv 93 | env/ 94 | venv/ 95 | ENV/ 96 | env.bak/ 97 | venv.bak/ 98 | 99 | # Spyder project settings 100 | .spyderproject 101 | .spyproject 102 | 103 | # Rope project settings 104 | .ropeproject 105 | 106 | # mkdocs documentation 107 | /site 108 | 109 | # mypy 110 | .mypy_cache/ 111 | .dmypy.json 112 | dmypy.json 113 | 114 | # General 115 | .DS_Store 116 | .AppleDouble 117 | .LSOverride 118 | 119 | # Icon must end with two \r 120 | Icon 121 | 122 | # Thumbnails 123 | ._* 124 | 125 | # Files that might appear in the root of a volume 126 | .DocumentRevisions-V100 127 | .fseventsd 128 | .Spotlight-V100 129 | .TemporaryItems 130 | .Trashes 131 | .VolumeIcon.icns 132 | .com.apple.timemachine.donotpresent 133 | 134 | # Directories potentially created on remote AFP share 135 | .AppleDB 136 | .AppleDesktop 137 | Network Trash Folder 138 | Temporary Items 139 | .apdisk 140 | 141 | # Flask 142 | .DS_Store 143 | .env 144 | .flaskenv 145 | *.pyc 146 | *.pyo 147 | env/ 148 | env* 149 | dist/ 150 | build/ 151 | *.egg 152 | *.egg-info/ 153 | _mailinglist 154 | .tox/ 155 | .cache/ 156 | .pytest_cache/ 157 | .idea/ 158 | docs/_build/ 159 | 160 | # Coverage reports 161 | htmlcov/ 162 | .coverage 163 | .coverage.* 164 | *,cover 165 | -------------------------------------------------------------------------------- /.replit: -------------------------------------------------------------------------------- 1 | language = "python3" 2 | run = "python app.py" 3 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn app:app --log-file=- -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Deprecated 2 | 3 | This project is no longer maintained. -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from scripts import tabledef 4 | from scripts import forms 5 | from scripts import helpers 6 | from flask import Flask, redirect, url_for, render_template, request, session 7 | import json 8 | import sys 9 | import os 10 | 11 | app = Flask(__name__) 12 | app.secret_key = os.urandom(12) # Generic key for dev purposes only 13 | 14 | # Heroku 15 | #from flask_heroku import Heroku 16 | #heroku = Heroku(app) 17 | 18 | # ======== Routing =========================================================== # 19 | # -------- Login ------------------------------------------------------------- # 20 | @app.route('/', methods=['GET', 'POST']) 21 | def login(): 22 | if not session.get('logged_in'): 23 | form = forms.LoginForm(request.form) 24 | if request.method == 'POST': 25 | username = request.form['username'].lower() 26 | password = request.form['password'] 27 | if form.validate(): 28 | if helpers.credentials_valid(username, password): 29 | session['logged_in'] = True 30 | session['username'] = username 31 | return json.dumps({'status': 'Login successful'}) 32 | return json.dumps({'status': 'Invalid user/pass'}) 33 | return json.dumps({'status': 'Both fields required'}) 34 | return render_template('login.html', form=form) 35 | user = helpers.get_user() 36 | return render_template('home.html', user=user) 37 | 38 | 39 | @app.route("/logout") 40 | def logout(): 41 | session['logged_in'] = False 42 | return redirect(url_for('login')) 43 | 44 | 45 | # -------- Signup ---------------------------------------------------------- # 46 | @app.route('/signup', methods=['GET', 'POST']) 47 | def signup(): 48 | if not session.get('logged_in'): 49 | form = forms.LoginForm(request.form) 50 | if request.method == 'POST': 51 | username = request.form['username'].lower() 52 | password = helpers.hash_password(request.form['password']) 53 | email = request.form['email'] 54 | if form.validate(): 55 | if not helpers.username_taken(username): 56 | helpers.add_user(username, password, email) 57 | session['logged_in'] = True 58 | session['username'] = username 59 | return json.dumps({'status': 'Signup successful'}) 60 | return json.dumps({'status': 'Username taken'}) 61 | return json.dumps({'status': 'User/Pass required'}) 62 | return render_template('login.html', form=form) 63 | return redirect(url_for('login')) 64 | 65 | 66 | # -------- Settings ---------------------------------------------------------- # 67 | @app.route('/settings', methods=['GET', 'POST']) 68 | def settings(): 69 | if session.get('logged_in'): 70 | if request.method == 'POST': 71 | password = request.form['password'] 72 | if password != "": 73 | password = helpers.hash_password(password) 74 | email = request.form['email'] 75 | helpers.change_user(password=password, email=email) 76 | return json.dumps({'status': 'Saved'}) 77 | user = helpers.get_user() 78 | return render_template('settings.html', user=user) 79 | return redirect(url_for('login')) 80 | 81 | 82 | # ======== Main ============================================================== # 83 | if __name__ == "__main__": 84 | app.run(debug=True, use_reloader=True, host="0.0.0.0") 85 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | WTForms 3 | SQLAlchemy 4 | bcrypt 5 | requests 6 | flask-heroku 7 | gunicorn -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.6.0 -------------------------------------------------------------------------------- /scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anfederico/flaskex/8bb1c4ff6b2be4a4bd4e1a747920ec9691fd928b/scripts/__init__.py -------------------------------------------------------------------------------- /scripts/forms.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from wtforms import Form, StringField, validators 4 | 5 | 6 | class LoginForm(Form): 7 | username = StringField('Username:', validators=[validators.required(), validators.Length(min=1, max=30)]) 8 | password = StringField('Password:', validators=[validators.required(), validators.Length(min=1, max=30)]) 9 | email = StringField('Email:', validators=[validators.optional(), validators.Length(min=0, max=50)]) 10 | -------------------------------------------------------------------------------- /scripts/helpers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from scripts import tabledef 4 | from flask import session 5 | from sqlalchemy.orm import sessionmaker 6 | from contextlib import contextmanager 7 | import bcrypt 8 | 9 | 10 | @contextmanager 11 | def session_scope(): 12 | """Provide a transactional scope around a series of operations.""" 13 | s = get_session() 14 | s.expire_on_commit = False 15 | try: 16 | yield s 17 | s.commit() 18 | except: 19 | s.rollback() 20 | raise 21 | finally: 22 | s.close() 23 | 24 | 25 | def get_session(): 26 | return sessionmaker(bind=tabledef.engine)() 27 | 28 | 29 | def get_user(): 30 | username = session['username'] 31 | with session_scope() as s: 32 | user = s.query(tabledef.User).filter(tabledef.User.username.in_([username])).first() 33 | return user 34 | 35 | 36 | def add_user(username, password, email): 37 | with session_scope() as s: 38 | u = tabledef.User(username=username, password=password.decode('utf8'), email=email) 39 | s.add(u) 40 | s.commit() 41 | 42 | 43 | def change_user(**kwargs): 44 | username = session['username'] 45 | with session_scope() as s: 46 | user = s.query(tabledef.User).filter(tabledef.User.username.in_([username])).first() 47 | for arg, val in kwargs.items(): 48 | if val != "": 49 | setattr(user, arg, val) 50 | s.commit() 51 | 52 | 53 | def hash_password(password): 54 | return bcrypt.hashpw(password.encode('utf8'), bcrypt.gensalt()) 55 | 56 | 57 | def credentials_valid(username, password): 58 | with session_scope() as s: 59 | user = s.query(tabledef.User).filter(tabledef.User.username.in_([username])).first() 60 | if user: 61 | return bcrypt.checkpw(password.encode('utf8'), user.password.encode('utf8')) 62 | else: 63 | return False 64 | 65 | 66 | def username_taken(username): 67 | with session_scope() as s: 68 | return s.query(tabledef.User).filter(tabledef.User.username.in_([username])).first() 69 | -------------------------------------------------------------------------------- /scripts/tabledef.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys 4 | import os 5 | from sqlalchemy import create_engine 6 | from sqlalchemy import Column, Integer, String 7 | from sqlalchemy.ext.declarative import declarative_base 8 | 9 | # Local 10 | SQLALCHEMY_DATABASE_URI = 'sqlite:///accounts.db' 11 | 12 | # Heroku 13 | #SQLALCHEMY_DATABASE_URI = os.environ['DATABASE_URL'] 14 | 15 | Base = declarative_base() 16 | 17 | 18 | def db_connect(): 19 | """ 20 | Performs database connection using database settings from settings.py. 21 | Returns sqlalchemy engine instance 22 | """ 23 | return create_engine(SQLALCHEMY_DATABASE_URI) 24 | 25 | 26 | class User(Base): 27 | __tablename__ = "user" 28 | 29 | id = Column(Integer, primary_key=True) 30 | username = Column(String(30), unique=True) 31 | password = Column(String(512)) 32 | email = Column(String(50)) 33 | 34 | def __repr__(self): 35 | return '' % self.username 36 | 37 | 38 | engine = db_connect() # Connect to database 39 | Base.metadata.create_all(engine) # Create models 40 | -------------------------------------------------------------------------------- /static/css/style.css: -------------------------------------------------------------------------------- 1 | .login-input { 2 | width: 130px; 3 | } 4 | 5 | .form-button { 6 | width: 80px; 7 | height: 35px; 8 | } 9 | 10 | .signup-box { 11 | margin: auto; 12 | width: 325px; 13 | background: rgba(255,255,255,0.05); 14 | border: 1px solid rgba(255,255,255,0.3); 15 | border-radius: 10px; 16 | } 17 | 18 | .settings-box { 19 | margin: auto; 20 | width: 325px; 21 | } 22 | 23 | .signup-area { 24 | padding: 30px; 25 | } 26 | 27 | .settings-area { 28 | padding: 30px; 29 | } -------------------------------------------------------------------------------- /static/js/scripts.js: -------------------------------------------------------------------------------- 1 | function message(status, shake=false, id="") { 2 | if (shake) { 3 | $("#"+id).effect("shake", {direction: "right", times: 2, distance: 8}, 250); 4 | } 5 | document.getElementById("feedback").innerHTML = status; 6 | $("#feedback").show().delay(2000).fadeOut(); 7 | } 8 | 9 | function error(type) { 10 | $("."+type).css("border-color", "#E14448"); 11 | } 12 | 13 | var login = function() { 14 | $.post({ 15 | type: "POST", 16 | url: "/", 17 | data: {"username": $("#login-user").val(), 18 | "password": $("#login-pass").val()}, 19 | success(response){ 20 | var status = JSON.parse(response)["status"]; 21 | if (status === "Login successful") { location.reload(); } 22 | else { error("login-input"); } 23 | } 24 | }); 25 | }; 26 | 27 | $(document).ready(function() { 28 | 29 | $(document).on("click", "#login-button", login); 30 | $(document).keypress(function(e) {if(e.which === 13) {login();}}); 31 | 32 | $(document).on("click", "#signup-button", function() { 33 | $.post({ 34 | type: "POST", 35 | url: "/signup", 36 | data: {"username": $("#signup-user").val(), 37 | "password": $("#signup-pass").val(), 38 | "email": $("#signup-mail").val()}, 39 | success(response) { 40 | var status = JSON.parse(response)["status"]; 41 | if (status === "Signup successful") { location.reload(); } 42 | else { message(status, true, "signup-box"); } 43 | } 44 | }); 45 | }); 46 | 47 | $(document).on("click", "#save", function() { 48 | $.post({ 49 | type: "POST", 50 | url: "/settings", 51 | data: {"username": $("#settings-user").val(), 52 | "password": $("#settings-pass").val(), 53 | "email": $("#settings-mail").val()}, 54 | success(response){ 55 | message(JSON.parse(response)["status"]); 56 | } 57 | }); 58 | }); 59 | }); 60 | 61 | // Open or Close mobile & tablet menu 62 | // https://github.com/jgthms/bulma/issues/856 63 | $("#navbar-burger-id").click(function () { 64 | if($("#navbar-burger-id").hasClass("is-active")){ 65 | $("#navbar-burger-id").removeClass("is-active"); 66 | $("#navbar-menu-id").removeClass("is-active"); 67 | }else { 68 | $("#navbar-burger-id").addClass("is-active"); 69 | $("#navbar-menu-id").addClass("is-active"); 70 | } 71 | }); -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {% block body %}{% endblock %} 12 | 13 | 14 | {% block scripts %}{% endblock %} 15 | 16 | 17 | -------------------------------------------------------------------------------- /templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block body %} 3 | {% include "navbar.html" %} 4 |
5 |
6 |
7 | {% endblock %} 8 | {% block scripts %} 9 | 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block body %} 3 |
4 | 49 |
50 | Flaskex 51 |
52 |
53 |
54 |
55 |
56 |
57 |

Flaskex

58 |

Simple example for rapid prototyping

59 |

• Encrypted user authorizaton

60 |

• Ready to go database system

61 |

• User login, signup, and settings

62 |
63 |
64 | 107 |
108 |
109 |
110 |
111 |
112 |
113 | 136 |
137 | {% endblock %} 138 | {% block scripts %} 139 | 140 | {% endblock %} 141 | -------------------------------------------------------------------------------- /templates/navbar.html: -------------------------------------------------------------------------------- 1 |
2 | 37 |
-------------------------------------------------------------------------------- /templates/settings.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block body %} 3 | {% include "navbar.html" %} 4 | Flaskex 5 |
6 |
7 |
8 |
9 |
10 |

Settings

11 |
12 |

13 | 14 | 15 | 16 | 17 |

18 |
19 |
20 |

21 | 22 | 23 | 24 | 25 |

26 |
27 |
28 |

29 | 30 | 31 | 32 | 33 |

34 |
35 |
36 | 37 | Save 38 | 39 | 40 | Exit 41 | 42 |
43 |
44 | 45 |
46 |
47 |
48 |
49 |
50 | {% endblock %} 51 | {% block scripts %} 52 | 53 | {% endblock %} 54 | --------------------------------------------------------------------------------