diff --git a/.gitignore b/.gitignore index 179dd1d..28ebf13 100644 --- a/.gitignore +++ b/.gitignore @@ -1,181 +1,3 @@ -### Python template -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -*.egg-info/ -.installed.cfg -*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*,cover -.hypothesis/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py - -# Flask instance folder -instance/ - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# IPython Notebook -.ipynb_checkpoints - -# pyenv -.python-version - -# celery beat schedule file -celerybeat-schedule - -# dotenv -.env - -# virtualenv -venv/ -ENV/ - -# Spyder project settings -.spyderproject - -# Rope project settings -.ropeproject - -### Python template -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -*.egg-info/ -.installed.cfg -*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*,cover -.hypothesis/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py - -# Flask instance folder -instance/ - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# IPython Notebook -.ipynb_checkpoints - -# pyenv -.python-version - -# celery beat schedule file -celerybeat-schedule - -# dotenv -.env - -# virtualenv -venv/ -ENV/ - -# Spyder project settings -.spyderproject - -# Rope project settings -.ropeproject - -# Created by .ignore support plugin (hsz.mobi) +.idea/ +**.DS_Store +**.swp diff --git a/Dockerfile b/Dockerfile index 08db488..96c10bd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,5 +7,6 @@ RUN pip3 install -r /requirements.txt; \ ADD app /app VOLUME ["/data"] +VOLUME ["/app/config] EXPOSE 5000 ENTRYPOINT python3 /app/main.py diff --git a/README.md b/README.md index 5054800..c0aff72 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,14 @@ ## build `docker build --tag=$(basename $PWD) .` +## general configuration + +Look at *app/config/email.py.example** for the configuration of the +parameters required for sending emails. Copy the file as *email.py* to +a folder that will serve as configuration directory and fill in the +information. The directory will be used as volume during container +operation. + ## run in development Include the development version of the code as volume, so the app gets @@ -10,7 +18,7 @@ reloaded automatically. The sqlite file will be stored in *tmp* so it can be inspected with tools like *sqlite3*. The switch *DEBUG* enables debugging during development. -`docker run --name rollerverbrauch -ti -v `pwd`/app:/app -v /tmp/pitstops/:/data -e DEBUG=True -p 5000:5000 rollerverbrauch` +`docker run --rm --name rollerverbrauch -ti -v `pwd`/app:/app -v `pwd`/../rollerverbrauch_config:/app/config -v /tmp/pitstops/:/data -e DEBUG=True -p 5000:5000 rollerverbrauch` ## run in production -`docker run --name pitstops -d -v /data/pitstops/:/data -p 80:5000 rollerverbrauch` \ No newline at end of file +`docker run --name pitstops -d -v /data/pitstops/:/data -v /configs/pitstops/:/app/config -p 80:5000 rollerverbrauch` diff --git a/app/config/config.py.example b/app/config/config.py.example new file mode 100644 index 0000000..0b968c8 --- /dev/null +++ b/app/config/config.py.example @@ -0,0 +1,6 @@ +MAIL_SERVER = 'smtp.gmail.com' +MAIL_PORT = 465 +MAIL_USE_TLS = False +MAIL_USE_SSL = True +MAIL_USERNAME = 'your-gmail-username' +MAIL_PASSWORD = 'your-gmail-password' \ No newline at end of file diff --git a/app/db/__pycache__/__init__.cpython-34.pyc b/app/db/__pycache__/__init__.cpython-34.pyc deleted file mode 100644 index fcd275f..0000000 Binary files a/app/db/__pycache__/__init__.cpython-34.pyc and /dev/null differ diff --git a/app/main.py b/app/main.py index 5fc40ac..b7de3dd 100644 --- a/app/main.py +++ b/app/main.py @@ -1,16 +1,18 @@ from datetime import date -from datetime import datetime from flask import Flask from flask import render_template, make_response from flask import request, redirect, g from flask import url_for from flask_sqlalchemy import SQLAlchemy from flask.ext.security import Security, SQLAlchemyUserDatastore, \ - UserMixin, RoleMixin, login_required, utils + UserMixin, RoleMixin, login_required, utils, roles_required from flask.ext.security.core import current_user import uuid import hashlib from functools import wraps +from flask.ext.security import user_registered +from flask.ext.mail import Mail, Message +from flask_security.core import current_user from flask_wtf import Form from wtforms import DateField, IntegerField, DecimalField, SelectField from wtforms.validators import DataRequired, ValidationError @@ -21,11 +23,14 @@ DATABASE = '/data/rollerverbrauch.db' app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///'+DATABASE db = SQLAlchemy(app) -app.config['SECRET_KEY'] = 'development key' app.config['SECURITY_PASSWORD_HASH'] = 'pbkdf2_sha512' -app.config['SECURITY_PASSWORD_SALT'] = 'xxxxxxxxxxxxxxxxxxxxxx' +app.config['SECURITY_REGISTERABLE'] = True +app.config['SECURITY_CHANGEABLE'] = True +app.config['SECURITY_RECOVERABLE'] = True +app.config.from_envvar('config') app.config.from_object(__name__) +mail = Mail(app) roles_users = db.Table('roles_users', db.Column('user_id', db.Integer(), db.ForeignKey('user.id')), @@ -97,26 +102,21 @@ class Pitstop(db.Model): user_datastore = SQLAlchemyUserDatastore(db, User, Role) security = Security(app, user_datastore) + +@user_registered.connect_via(app) +def user_registered_sighandler(app, user, confirm_token): + """ + Called after a user was created + """ + role = user_datastore.find_role('user') + user_datastore.add_role_to_user(user, role) + + @app.before_first_request def before_first_request(): db.create_all() - - user_datastore.find_or_create_role(name='admin', description='Administrator') - user_datastore.find_or_create_role(name='end-user', description='End user') - - encrypted_password = utils.encrypt_password('password') - if not user_datastore.get_user('someone@example.com'): - user_datastore.create_user(email='someone@example.com', password=encrypted_password) - if not user_datastore.get_user('admin@example.com'): - user_datastore.create_user(email='admin@example.com', password=encrypted_password) - - # Commit any database changes; the User and Roles must exist before we can add a Role to the User - db.session.commit() - - # Give one User has the "end-user" role, while the other has the "admin" role. (This will have no effect if the - # Users already have these Roles.) Again, commit any database changes. - user_datastore.add_role_to_user('someone@example.com', 'end-user') - user_datastore.add_role_to_user('admin@example.com', 'admin') + user_datastore.find_or_create_role(name='admin', description='Role for administrators') + user_datastore.find_or_create_role(name='user', description='Role for all users.') db.session.commit() user = User.query.filter(User.email=='someone@example.com').first() @@ -217,6 +217,20 @@ def get_manual(): return render_template('manual.html', data=g.data) +@app.route('/admin', methods=['GET']) +@roles_required('admin') +def get_admin_page(): + g.data['users'] = User.query.all() + return render_template('admin.html', data=g.data) + + +@app.route('/account', methods=['GET']) +@login_required +def get_account_page(): + print(current_user) + return render_template('account.html', data=g.data) + + @app.route('/statistics', methods=['GET']) @login_required def get_statistics(): diff --git a/app/templates/account.html b/app/templates/account.html new file mode 100644 index 0000000..de46b10 --- /dev/null +++ b/app/templates/account.html @@ -0,0 +1,7 @@ +{% extends "layout.html" %} + +{% block body %} +

Account management for {{current_user.email}}

+ + Change password +{% endblock %} diff --git a/app/templates/admin.html b/app/templates/admin.html new file mode 100644 index 0000000..97c2db5 --- /dev/null +++ b/app/templates/admin.html @@ -0,0 +1,12 @@ +{% extends "layout.html" %} + +{% block body %} +

Admin

+ We have {{ data.users|length }} users so far: + + Login +{% endblock %} diff --git a/app/templates/layout.html b/app/templates/layout.html index f6ada95..29fb991 100644 --- a/app/templates/layout.html +++ b/app/templates/layout.html @@ -1,9 +1,15 @@ {% macro navigation() -%} -
  • Create Pitstop
  • -
  • Statistics
  • -
  • Manual
  • {% if current_user.email %} +
  • Create Pitstop
  • +
  • Statistics
  • +
  • Account
  • + {% if current_user.has_role('admin') %} +
  • Admin
  • + {% endif %}
  • Logout
  • + {% else %} +
  • Login
  • +
  • Register
  • {% endif %} {%- endmacro %} diff --git a/app/templates/security/change_password.html b/app/templates/security/change_password.html new file mode 100644 index 0000000..27f5d62 --- /dev/null +++ b/app/templates/security/change_password.html @@ -0,0 +1,13 @@ +{% extends "layout.html" %} +{% from "security/_macros.html" import render_field_with_errors, render_field %} + +{% block body %} +

    Change password

    +
    + {{ change_password_form.hidden_tag() }} + {{ render_field_with_errors(change_password_form.password) }} + {{ render_field_with_errors(change_password_form.new_password) }} + {{ render_field_with_errors(change_password_form.new_password_confirm) }} + {{ render_field(change_password_form.submit) }} +
    +{% endblock %} diff --git a/app/templates/security/forgot_password.html b/app/templates/security/forgot_password.html new file mode 100644 index 0000000..556f254 --- /dev/null +++ b/app/templates/security/forgot_password.html @@ -0,0 +1,11 @@ +{% extends "layout.html" %} +{% from "security/_macros.html" import render_field_with_errors, render_field %} + +{% block body %} +

    Send password reset instructions

    +
    + {{ forgot_password_form.hidden_tag() }} + {{ render_field_with_errors(forgot_password_form.email) }} + {{ render_field(forgot_password_form.submit) }} +
    +{% endblock %} diff --git a/app/templates/security/login_user.html b/app/templates/security/login_user.html index 536dd1c..9bc86c2 100644 --- a/app/templates/security/login_user.html +++ b/app/templates/security/login_user.html @@ -2,13 +2,16 @@ {% from "security/_macros.html" import render_field_with_errors, render_field %} {% block body %} +

    Login

    - {{ login_user_form.hidden_tag() }} - {{ render_field_with_errors(login_user_form.email) }} - {{ render_field_with_errors(login_user_form.password) }} - {{ render_field_with_errors(login_user_form.remember) }} - {{ render_field(login_user_form.next) }} - {{ render_field(login_user_form.submit) }} + {{ login_user_form.hidden_tag() }} + {{ render_field_with_errors(login_user_form.email) }} + {{ render_field_with_errors(login_user_form.password) }} + {{ render_field_with_errors(login_user_form.remember) }} + {{ render_field(login_user_form.next) }} + {{ render_field(login_user_form.submit) }} + {% if security.recoverable %} + Forgot password + {% endif %}
    -{% include "security/_menu.html" %} {% endblock %} diff --git a/app/templates/security/register_user.html b/app/templates/security/register_user.html new file mode 100644 index 0000000..9cad77d --- /dev/null +++ b/app/templates/security/register_user.html @@ -0,0 +1,15 @@ +{% extends "layout.html" %} +{% from "security/_macros.html" import render_field_with_errors, render_field %} + +{% block body %} +

    Register User

    +
    + {{ register_user_form.hidden_tag() }} + {{ render_field_with_errors(register_user_form.email) }} + {{ render_field_with_errors(register_user_form.password) }} + {% if register_user_form.password_confirm %} + {{ render_field_with_errors(register_user_form.password_confirm) }} + {% endif %} + {{ render_field(register_user_form.submit) }} +
    +{% endblock %} diff --git a/app/templates/security/reset_password.html b/app/templates/security/reset_password.html new file mode 100644 index 0000000..f94ec4d --- /dev/null +++ b/app/templates/security/reset_password.html @@ -0,0 +1,12 @@ +{% extends "layout.html" %} +{% from "security/_macros.html" import render_field_with_errors, render_field %} + +{% block body %} +

    Reset password

    +
    + {{ reset_password_form.hidden_tag() }} + {{ render_field_with_errors(reset_password_form.password) }} + {{ render_field_with_errors(reset_password_form.password_confirm) }} + {{ render_field(reset_password_form.submit) }} +
    +{% endblock %}