Browse Source

Initial commit

master
Ryan Reed 6 years ago
commit
32ac3d7a23
32 changed files with 1006 additions and 0 deletions
  1. +2
    -0
      .flaskenv
  2. +111
    -0
      .gitignore
  3. +112
    -0
      README.md
  4. +46
    -0
      app/__init__.py
  5. +5
    -0
      app/api/__init__.py
  6. +29
    -0
      app/api/auth.py
  7. +13
    -0
      app/api/errors.py
  8. +18
    -0
      app/api/tokens.py
  9. +56
    -0
      app/api/users.py
  10. +5
    -0
      app/auth/__init__.py
  11. +10
    -0
      app/auth/forms.py
  12. +33
    -0
      app/auth/routes.py
  13. +26
    -0
      app/auth/templates/login.html
  14. +5
    -0
      app/errors/__init__.py
  15. +25
    -0
      app/errors/handlers.py
  16. +7
    -0
      app/errors/templates/404.html
  17. +9
    -0
      app/errors/templates/500.html
  18. +5
    -0
      app/main/__init__.py
  19. +12
    -0
      app/main/routes.py
  20. +36
    -0
      app/main/templates/base.html
  21. +11
    -0
      app/main/templates/index.html
  22. +117
    -0
      app/models.py
  23. +1
    -0
      app/static/style.css
  24. +18
    -0
      config.py
  25. +1
    -0
      migrations/README
  26. +45
    -0
      migrations/alembic.ini
  27. +95
    -0
      migrations/env.py
  28. +24
    -0
      migrations/script.py.mako
  29. +36
    -0
      migrations/versions/7fb3d3939b8d_adding_tokens_and_api_related_functions.py
  30. +46
    -0
      migrations/versions/e1b952dc4b28_initial_user_table.py
  31. +25
    -0
      requirements.txt
  32. +22
    -0
      run.py

+ 2
- 0
.flaskenv View File

@ -0,0 +1,2 @@
FLASK_APP=run.py
FLASK_DEBUG=1

+ 111
- 0
.gitignore View File

@ -0,0 +1,111 @@
# Swap files
*.sw[a-p]
# Database file
*.db
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# 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/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
venv-*/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/

+ 112
- 0
README.md View File

@ -0,0 +1,112 @@
# Table of Contents
<!-- vim-markdown-toc GFM -->
* [Setting up the environment](#setting-up-the-environment)
* [Install the requirements](#install-the-requirements)
* [Setup the environment variables](#setup-the-environment-variables)
* [The API](#the-api)
* [The Database](#the-database)
* [Workflow for updating database (new table/field/etc)](#workflow-for-updating-database-new-tablefieldetc)
* [Initial DB Setup for the app](#initial-db-setup-for-the-app)
* [Working with the database](#working-with-the-database)
* [Adding to the database](#adding-to-the-database)
* [Querying the database](#querying-the-database)
* [Clear out the tables](#clear-out-the-tables)
<!-- vim-markdown-toc -->
# Setting up the environment
## Install the requirements
```
pip install -r requirements.txt
```
## Setup the environment variables
The following can be added to a `.env` file in the foot of this directory as well
```bash
APP_ENVIRONMENT="PROD"
SECRET_KEY="myverysecretkey"
DATABASE_URL="sqlite:///app.db"
```
# The API
The api allows for various functions. To work with it locally and easily, you can use `httpie` in Pypi or just use curl:
```bash
pip install httpie
```
The following includes various methods:
```bash
# Get token
http --auth <username>:<password> POST http://localhost:5000/api/tokens
# Revoke token
http DELETE http://localhost:5000/api/tokens "Authorization:Bearer <token>"
# Create user
http POST http://localhost:5000/api/users "Authorization:Bearer <token>" username=alice password=dog email=alice@example.com
# Update current user
http PUT http://localhost:5000/api/users/1 "Authorization:Bearer <token>" email=new_email@example.com
# Various routes
http GET http://localhost:5000/api/users "Authorization:Bearer <token>"
http GET http://localhost:5000/api/users/1 "Authorization:Bearer <token>"
```
# The Database
## Workflow for updating database (new table/field/etc)
1. Export the flask app: `export FLASK_APP=stockpyle.py`
2. Create/modify class for table in `app/models.py`
3. Create migration script: `flask db migrate -m "<class/table> table"`
4. Commit migration script to repo (for upgrading other environments)
5. Make the changes to the database: `flask db upgrade`
## Initial DB Setup for the app
```bash
flask db upgrade
```
## Working with the database
### Adding to the database
```python
u = User(username='john', email='john@example.com')
u.set_password('password')
db.session.add(u)
u = User(username='susan', email='susan@example.com')
u.set_password('password')
db.session.add(u)
db.session.commit()
```
### Querying the database
```python
# get user
u = User.query.get(1)
u
#<User john>
# get all users in reverse alphabetical order
User.query.order_by(User.username.desc()).all()
#[<User susan>, <User john>]
```
### Clear out the tables
```python
users = User.query.all()
for u in users:
db.session.delete(u)
db.session.commit()
```

+ 46
- 0
app/__init__.py View File

@ -0,0 +1,46 @@
from flask import Flask
from flask_login import LoginManager
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
from config import Config
db = SQLAlchemy()
migrate = Migrate()
login = LoginManager()
login.login_view = "auth.login"
login.login_message = "Please log in to access this page."
def create_app(config_class=Config):
"""
The application instance to be used when initializing the app
:param config_class: (Default value = Config)
:returns: The flask application
"""
app = Flask(__name__)
app.config.from_object(config_class)
db.init_app(app)
migrate.init_app(app, db, render_as_batch=True)
login.init_app(app)
from app.api import bp as bp_api
app.register_blueprint(bp_api, url_prefix='/api')
from app.auth import bp as bp_auth
app.register_blueprint(bp_auth, url_prefix='/auth')
from app.errors import bp as bp_errors
app.register_blueprint(bp_errors)
from app.main import bp as bp_main
app.register_blueprint(bp_main)
return app
from app import models

+ 5
- 0
app/api/__init__.py View File

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('api', __name__)
from app.api import users, errors, tokens

+ 29
- 0
app/api/auth.py View File

@ -0,0 +1,29 @@
from app.api.errors import error_response
from app.models import User
from flask import g
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth
basic_auth = HTTPBasicAuth()
token_auth = HTTPTokenAuth()
@basic_auth.verify_password
def verify_password(username, password):
user = User.query.filter_by(username=username).first()
if user is None:
return False
g.current_user = user
return user.check_password(password)
@basic_auth.error_handler
def basic_auth_error():
return error_response(401)
@token_auth.verify_token
def verify_token(token):
g.current_user = User.check_token(token) if token else None
return g.current_user is not None
@token_auth.error_handler
def token_auth_error():
return error_response(401)

+ 13
- 0
app/api/errors.py View File

@ -0,0 +1,13 @@
from flask import jsonify
from werkzeug.http import HTTP_STATUS_CODES
def error_response(status_code, message=None):
payload = {'error': HTTP_STATUS_CODES.get(status_code, 'Unknown error')}
if message:
payload['message'] = message
response = jsonify(payload)
response.status_code = status_code
return response
def bad_request(message):
return error_response(400, message)

+ 18
- 0
app/api/tokens.py View File

@ -0,0 +1,18 @@
from app import db
from app.api import bp
from app.api.auth import basic_auth, token_auth
from flask import jsonify, g
@bp.route('/tokens', methods=['POST'])
@basic_auth.login_required
def get_token():
token = g.current_user.get_token()
db.session.commit()
return jsonify({'token': token})
@bp.route('/tokens', methods=['DELETE'])
@token_auth.login_required
def revoke_token():
g.current_user.revoke_token()
db.session.commit()
return '', 204

+ 56
- 0
app/api/users.py View File

@ -0,0 +1,56 @@
from app import db
from app.api import bp
from app.api.auth import token_auth
from app.api.errors import bad_request
from app.models import User
from flask import abort, g, jsonify, request, url_for
@bp.route('/users/<int:id>', methods=['GET'])
@token_auth.login_required
def get_user(id):
return jsonify(User.query.get_or_404(id).to_dict())
@bp.route('/users', methods=['GET'])
@token_auth.login_required
def get_users():
page = request.args.get('page', 1, type=int)
per_page = min(request.args.get('per_page', 10, type=int), 100)
data = User.to_collection_dict(User.query, page, per_page, 'api.get_users')
return jsonify(data)
@bp.route('/users', methods=['POST'])
@token_auth.login_required
def create_user():
data = request.get_json() or {}
if 'username' not in data or 'email' not in data or 'password' not in data:
return bad_request('must include username, email, and password fields')
if User.query.filter_by(username=data['username']).first():
return bad_request('please use a different username')
if User.query.filter_by(email=data['email']).first():
return bad_request('please use a different email')
user = User()
user.from_dict(data, new_user=True)
db.session.add(user)
db.session.commit()
response = jsonify(user.to_dict())
response.status_code = 201
response.headers['Location'] = url_for('api.get_user', id=user.id)
return response
@bp.route('/users/<int:id>', methods=['PUT'])
@token_auth.login_required
def update_user(id):
if g.current_user.id != id:
abort(403)
user = User.query.get_or_404(id)
data = request.get_json() or {}
if 'username' in data and data['username'] != user.username and \
User.query.filter_by(username=data['username']).first():
return bad_request('please use a different username')
if 'email' in data and data['email'] != user.email and \
User.query.filter_by(email=data['email']).first():
return bad_request('please use a different email address')
user.from_dict(data, new_user=False)
db.session.commit()
return jsonify(user.to_dict())

+ 5
- 0
app/auth/__init__.py View File

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('auth', __name__, template_folder='templates')
from app.auth import routes

+ 10
- 0
app/auth/forms.py View File

@ -0,0 +1,10 @@
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField, TextAreaField
from wtforms.validators import DataRequired, ValidationError, Email, EqualTo, Length
from app.models import User
class LoginForm(FlaskForm):
username = StringField("Username", validators=[DataRequired()])
password = PasswordField("Password", validators=[DataRequired()])
remember_me = BooleanField("Remember Me")
submit = SubmitField("Sign In")

+ 33
- 0
app/auth/routes.py View File

@ -0,0 +1,33 @@
from app import db
from app.auth import bp
from app.auth.forms import LoginForm
from app.models import User
from flask import render_template, flash, redirect, request, url_for
from flask_login import current_user, login_user, logout_user
from werkzeug.urls import url_parse
@bp.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user is None or not user.check_password(form.password.data):
flash('Invalid username or password')
return redirect(url_for('auth.login'))
login_user(user, remember=form.remember_me.data)
next_page = request.args.get('next')
if not next_page or url_parse(next_page).netloc != '':
next_page = url_for('main.index')
return redirect(next_page)
return render_template('login.html', title='Sign In', form=form)
@bp.route('/logout')
def logout():
logout_user()
return redirect(url_for('main.index'))

+ 26
- 0
app/auth/templates/login.html View File

@ -0,0 +1,26 @@
{% extends 'base.html' %}
{% block app_content %}
<h1>Sign In</h1>
<div class="row">
<form action="" method="post" novalidate>
{{ form.hidden_tag() }}
<p>
{{ form.username.label }}<br>
{{ form.username(size=32) }}<br>
{% for error in form.username.errors %}
<span class="alert-error">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.password.label }}<br>
{{ form.password(size=32) }}<br>
{% for error in form.password.errors %}
<span class="alert-error">[{{ error }}]</span>
{% endfor %}
</p>
<p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
<p>{{ form.submit() }}</p>
</form>
</div>
{% endblock %}

+ 5
- 0
app/errors/__init__.py View File

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('errors', __name__, template_folder='templates')
from app.errors import handlers

+ 25
- 0
app/errors/handlers.py View File

@ -0,0 +1,25 @@
from app import db
from app.errors import bp
from app.api.errors import error_response as api_error_response
from flask import render_template, request
def wants_json_response():
"""
if the client has JSON rated higher in their preferred response format
return true
"""
return request.accept_mimetypes['application/json'] >= \
request.accept_mimetypes['text/html']
@bp.app_errorhandler(404)
def not_found_error(error):
if wants_json_response():
return api_error_response(404)
return render_template('404.html'), 404
@bp.app_errorhandler(500)
def internal_error(error):
db.session.rollback()
if wants_json_response():
return api_error_response(500)
return render_template('500.html'), 500

+ 7
- 0
app/errors/templates/404.html View File

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% block app_content %}
<h1>File Not Found</h1>
<p><a href="{{ url_for('main.index') }}">Back</a></p>
{% endblock %}

+ 9
- 0
app/errors/templates/500.html View File

@ -0,0 +1,9 @@
{% extends 'base.html' %}
{% block app_content %}
<h1>An unexpected error has occured</h1>
<p>The administrator has been notified. Sorry for the inconvenience.</p>
<p><a href="{{ url_for('main.index') }}">Back</a></p>
{% endblock %}

+ 5
- 0
app/main/__init__.py View File

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('main', __name__, template_folder='templates')
from app.main import routes

+ 12
- 0
app/main/routes.py View File

@ -0,0 +1,12 @@
from flask import current_app, render_template
from app.main import bp
@bp.route("/", methods=["GET", "POST"])
@bp.route("/index", methods=["GET", "POST"])
def index():
return render_template("index.html",
title="Home Page",
environment=current_app.config["ENVIRONMENT"]
)

+ 36
- 0
app/main/templates/base.html View File

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset=”utf-8”>
<title>[{{ environment }}] {% if title %}{{ title }}{% else %}Home Page{% endif %}</title>
{% block styles %}
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}" />
{% endblock %}
{% block scripts %}
{% endblock %}
</head>
<body>
<div class="container">
<h1>[{{ environment }}] Home Page</h1>
{% if current_user.is_anonymous %}
<p><a href="{{ url_for('auth.login') }}">Login</a></p>
{% endif %}
{% if current_user.is_authenticated %}
<p>Hello {{ current_user.username }}</p> <a href="{{ url_for('auth.logout') }}">Logout</a></li>
{% endif %}
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
<div class="alert alert-info" role="alert">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block app_content %}{% endblock %}
</div>
</body>
</html>

+ 11
- 0
app/main/templates/index.html View File

@ -0,0 +1,11 @@
{% extends 'base.html' %}
{% block app_content %}
<div>
<p>This is the index. To hopefully be expanded upon</p>
</div>
{% endblock %}
{% block styles %}
{{ super() }}{# Preserve existing styles #}
{% endblock %}

+ 117
- 0
app/models.py View File

@ -0,0 +1,117 @@
from app import db, login
from datetime import datetime, timedelta
from flask import current_app, url_for
from flask_login import UserMixin
from time import time
from werkzeug.security import generate_password_hash, check_password_hash
import base64
import json
import jwt
import os
class PaginatedAPIMixin(object):
@staticmethod
def to_collection_dict(query, page, per_page, endpoint, **kwargs):
resources = query.paginate(page, per_page, False)
data = {
'items': [item.to_dict() for item in resources.items],
'_meta': {
'page': page,
'per_page': per_page,
'total_pages': resources.pages,
'total_items': resources.total
},
'_links': {
'self': url_for(endpoint, page=page, per_page=per_page,
**kwargs),
'next': url_for(endpoint, page=page, per_page=per_page,
**kwargs) if resources.has_next else None,
'prev': url_for(endpoint, page=page, per_page=per_page,
**kwargs) if resources.has_prev else None,
}
}
return data
class User(PaginatedAPIMixin, UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True, unique=True)
email = db.Column(db.String(120), index=True, unique=True)
password_hash = db.Column(db.String(128))
last_seen = db.Column(db.DateTime, default=datetime.utcnow)
role = db.Column(db.String(32), index=True)
token = db.Column(db.String(32), index=True, unique=True)
token_expiration = db.Column(db.DateTime)
def __repr__(self):
return f"<User {self.username}>"
def set_password(self, password):
"""
Set the password_hash field
:param password:
"""
self.password_hash = generate_password_hash(password)
def check_password(self, password):
"""
Check the password against the hash in the database
:param password:
returns: True/False
"""
return check_password_hash(self.password_hash, password)
def from_dict(self, data, new_user=False):
for field in ['username', 'email']:
if field in data:
setattr(self, field, data[field])
if new_user and 'password' in data:
self.set_password(data['password'])
def to_dict(self, include_email=False):
data = {
'id': self.id,
'username': self.username,
'last_seen': self.last_seen.isoformat() + 'Z',
'_links': {
'self': url_for('api.get_user', id=self.id),
}
}
if include_email:
data['email'] = self.email
return data
def get_token(self, expires_in=3600):
now = datetime.utcnow()
if self.token and self.token_expiration > now + timedelta(seconds=60):
return self.token
self.token = base64.b64encode(os.urandom(24)).decode('utf-8')
self.token_expiration = now + timedelta(seconds=expires_in)
db.session.add(self)
return self.token
def revoke_token(self):
self.token_expiration = datetime.utcnow() - timedelta(seconds=1)
@staticmethod
def check_token(token):
user = User.query.filter_by(token=token).first()
if user is None or user.token_expiration < datetime.utcnow():
return None
return user
@staticmethod
def verify_reset_password_token(token):
try:
id = jwt.decode(token, current_app.config['SECRET_KEY'],
algorithms=['HS256'])['reset_password']
except:
return
return User.query.get(id)
@login.user_loader
def load_user(id):
# Necessary for flask-login to work
return User.query.get(int(id))

+ 1
- 0
app/static/style.css View File

@ -0,0 +1 @@
.alert-error, .alert-info { color: red; }

+ 18
- 0
config.py View File

@ -0,0 +1,18 @@
import os
class Config():
"""
The configuration class. Retrieves needed environment information.
Accessed via app.config or current_app.config
"""
BASEDIR = os.path.abspath(os.path.dirname(__file__))
ENVIRONMENT = os.environ.get("APP_ENVIRONMENT") or "DEV"
SECRET_KEY = os.environ.get("SECRET_KEY") or "you-will-never-guess"
SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL") or "sqlite:///{}".format(
os.path.join(BASEDIR, "app.db")
)
SQLALCHEMY_TRACK_MODIFICATIONS = False

+ 1
- 0
migrations/README View File

@ -0,0 +1 @@
Generic single-database configuration.

+ 45
- 0
migrations/alembic.ini View File

@ -0,0 +1,45 @@
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

+ 95
- 0
migrations/env.py View File

@ -0,0 +1,95 @@
from __future__ import with_statement
import logging
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
from flask import current_app
config.set_main_option('sqlalchemy.url',
current_app.config.get('SQLALCHEMY_DATABASE_URI'))
target_metadata = current_app.extensions['migrate'].db.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url, target_metadata=target_metadata, literal_binds=True
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix='sqlalchemy.',
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
process_revision_directives=process_revision_directives,
**current_app.extensions['migrate'].configure_args
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

+ 24
- 0
migrations/script.py.mako View File

@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

+ 36
- 0
migrations/versions/7fb3d3939b8d_adding_tokens_and_api_related_functions.py View File

@ -0,0 +1,36 @@
"""Adding tokens and api related functions
Revision ID: 7fb3d3939b8d
Revises: e1b952dc4b28
Create Date: 2019-04-28 16:56:03.996324
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '7fb3d3939b8d'
down_revision = 'e1b952dc4b28'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.add_column(sa.Column('token', sa.String(length=32), nullable=True))
batch_op.add_column(sa.Column('token_expiration', sa.DateTime(), nullable=True))
batch_op.create_index(batch_op.f('ix_user_token'), ['token'], unique=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_user_token'))
batch_op.drop_column('token_expiration')
batch_op.drop_column('token')
# ### end Alembic commands ###

+ 46
- 0
migrations/versions/e1b952dc4b28_initial_user_table.py View File

@ -0,0 +1,46 @@
"""Initial User table
Revision ID: e1b952dc4b28
Revises:
Create Date: 2019-04-28 14:56:04.793653
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'e1b952dc4b28'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('username', sa.String(length=64), nullable=True),
sa.Column('email', sa.String(length=120), nullable=True),
sa.Column('password_hash', sa.String(length=128), nullable=True),
sa.Column('last_seen', sa.DateTime(), nullable=True),
sa.Column('role', sa.String(length=32), nullable=True),
sa.PrimaryKeyConstraint('id')
)
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_user_email'), ['email'], unique=True)
batch_op.create_index(batch_op.f('ix_user_role'), ['role'], unique=False)
batch_op.create_index(batch_op.f('ix_user_username'), ['username'], unique=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_user_username'))
batch_op.drop_index(batch_op.f('ix_user_role'))
batch_op.drop_index(batch_op.f('ix_user_email'))
op.drop_table('user')
# ### end Alembic commands ###

+ 25
- 0
requirements.txt View File

@ -0,0 +1,25 @@
alembic==1.0.9
asn1crypto==0.24.0
cffi==1.12.3
Click==7.0
cryptography==2.6.1
Flask==1.0.2
Flask-HTTPAuth==3.2.4
Flask-Login==0.4.1
Flask-Migrate==2.4.0
Flask-SQLAlchemy==2.4.0
Flask-WTF==0.14.2
itsdangerous==1.1.0
Jinja2==2.10.1
jwt==0.6.1
Mako==1.0.9
MarkupSafe==1.1.1
pkg-resources==0.0.0
pycparser==2.19
python-dateutil==2.8.0
python-dotenv==0.10.1
python-editor==1.0.4
six==1.12.0
SQLAlchemy==1.3.3
Werkzeug==0.15.2
WTForms==2.2.1

+ 22
- 0
run.py View File

@ -0,0 +1,22 @@
from app import create_app, db
from app.models import User
app = create_app()
@app.shell_context_processor
def make_shell_context():
"""
Make the following easily accessible from 'flask shell'
For instance:
|--$ flask shell
Python 3.6.7 (default, Oct 22 2018, 11:32:17)
[GCC 8.2.0] on linux
App: spark [production]
Instance: /home/user/app/instance
>>> user = User.query.first()
>>> print(user)
<User johnwhite>
"""
return {"db": db, "User": User}

Loading…
Cancel
Save