Skip to content
Permalink
Browse files

Add support for user models (MLH#1)

* Add support for user models

- Add in Flask SQLAlchemy extension
- Add support for PostgresSQL database
- Create and update User model files
- Update OAuth endpoints to support fetch and create Users

* Update dependencies

* Remove unused authentication routes

* Support action to unstar repo

* Update project for code review

- Rename extensions.py -> database.py
- Remove unused database file variable
- Remove unused login and register templates

* Change status, rename method, unused variable

* Rename views -> controllers

* Add abstraction for more readable code

- Add GitHub service to contain GitHub API related logic
- Add helper to User model to handle extra creation logic

* Update github class to support generic get and post methods
  • Loading branch information...
nlaz committed Feb 1, 2019
1 parent b8827f4 commit b509513f4726bab3bcf384061ce1e2c912cc071c
@@ -2,5 +2,5 @@ include starter/schema.sql
graft starter/static
graft starter/templates
graft starter/models
graft starter/views
graft starter/controllers
global-exclude *.pyc
@@ -8,6 +8,7 @@ idna==2.8
itsdangerous==1.1.0
Jinja2==2.10
MarkupSafe==1.1.0
psycopg2==2.7.3.2
python-dotenv==0.10.1
requests==2.21.0
SQLAlchemy==1.2.16
@@ -1,30 +1,34 @@
import os

from flask import Flask, render_template
from starter import settings, views, models, database
from starter import settings, controllers, models
from starter.database import db

project_dir = os.path.dirname(os.path.abspath(__file__))
database_file = "sqlite:///{}".format(os.path.join(project_dir, "starter.db"))

def create_app(config_object=settings):
# create and configure the app
app = Flask(__name__, instance_relative_config=True)
app.config.from_object(config_object)

register_extensions(app)
register_blueprints(app)
register_errorhandlers(app)
return app

def register_extensions(app):
"""Register Flask extensions."""
database.init_app(app)
db.init_app(app)

with app.app_context():
db.create_all()
return None

def register_blueprints(app):
"""Register Flask blueprints."""
app.register_blueprint(views.public.blueprint)
app.register_blueprint(views.auth.blueprint)
app.register_blueprint(views.github.blueprint)
app.register_blueprint(controllers.public.blueprint)
app.register_blueprint(controllers.auth.blueprint)
app.register_blueprint(controllers.github.blueprint)
return None

def register_errorhandlers(app):
@@ -1,2 +1,2 @@
"""Views package"""
"""Controllers package"""
from . import auth, github, public
@@ -0,0 +1,61 @@
import functools, json, requests

from flask import flash, redirect, render_template, request
from flask import Blueprint, session, url_for, g
from werkzeug.security import check_password_hash, generate_password_hash

from starter.database import db
from starter.models.user import User
from starter.settings import GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET
from starter.services.github import GitHub

blueprint = Blueprint('auth', __name__, url_prefix='/auth')

github = GitHub(GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET)

@blueprint.route('/github/login')
def githubLogin():
return redirect(github.authorization_url(scope='public_repo'))

@blueprint.route('/github/callback', methods=('GET', 'POST'))
def githubCallback():
if 'code' not in request.args:
return '', 500

# Fetch user from GitHub OAuth and store in session
access_token = github.get_token(request.args['code'])

if access_token is None:
flash('Could not authorize your request. Please try again.')
return '', 404

user = User.find_or_create_from_token(access_token)

session['access_token'] = access_token
session['user_id'] = user.id

return redirect(url_for('public.index'))

@blueprint.route('/logout')
def logout():
session.clear()
return redirect(url_for('public.index'))

@blueprint.before_app_request
def get_current_user():
user_id = session.get('user_id')

if user_id is None:
g.user = None
else:
g.user = User.query.filter_by(id=user_id).first()

def login_required(view):
@functools.wraps(view)
def wrapped_view(**kwargs):
if g.user is None:
return redirect(url_for('auth.login'))

return view(**kwargs)

return wrapped_view
@@ -0,0 +1,48 @@
from flask import redirect, render_template, request
from flask import Blueprint, flash, url_for, session

from starter.controllers.auth import login_required
from starter.database import db
from starter.services.github import GitHub

blueprint = Blueprint('github', __name__, url_prefix='/github')

@blueprint.route('/')
def index():
if not 'access_token' in session:
flash('Please sign in with your GitHub account.', 'info')
return render_template('github/index.html')

github = GitHub(access_token=session['access_token'])

starred_repos = github.get('/user/starred')
return render_template('github/index.html', repos=starred_repos)

@blueprint.route('/search')
def search():
search = request.args.get('query')

if search is None or search == '':
flash('Please include a repo name you want to search for.')
return redirect(url_for('github.index'))
if not 'access_token' in session:
flash('Please sign in with your GitHub account.', 'error')
return redirect(url_for('github.index'))

github = GitHub(access_token=session['access_token'])

repos = github.get('/search/repositories', { 'q': search } )
return render_template('github/index.html', repos=repos['items'])

@blueprint.route('/star', methods=['POST'])
def star():
repo = request.form['full_name']

if not 'access_token' in session:
flash('Please sign in with your GitHub account.', 'error')
return redirect(url_for('github.index'))

github = GitHub(access_token=session['access_token'])
github.delete('/user/starred/' + repo)

return redirect(url_for('github.index'))
@@ -1,10 +1,8 @@

from flask import (
Blueprint, flash, g, redirect, render_template, request, url_for, session
)

from starter.views.auth import login_required
from starter.database import get_db
from starter.controllers.auth import login_required

blueprint = Blueprint('public', __name__)

@@ -1,28 +1,4 @@
import sqlite3

"""Database module"""
from flask_sqlalchemy import SQLAlchemy

import click
from flask import current_app, g
from flask.cli import with_appcontext

def get_db():
if 'db' not in g:
g.db = SQLAlchemy(current_app)

return g.db


def init_db():
db = get_db()
db.create_all()

@click.command('init-db')
@with_appcontext
def init_db_command():
"""Clear the existing data and create new tables."""
init_db()
click.echo('Initialized the database.')

def init_app(app):
app.cli.add_command(init_db_command)
db = SQLAlchemy()
@@ -1,11 +1,31 @@
from starter.db import get_db

db = get_db()
from starter.database import db
from starter.services.github import GitHub

class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False, primary_key=True)
password = db.Column(db.String(120), nullable=False)
__tablename__ = 'user'

id = db.Column(db.Integer(), primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
avatar_url = db.Column(db.String(80), nullable=True)
github_id = db.Column(db.Integer(), nullable=True)

def __init__(self, username, avatar_url, github_id):
self.username = username
self.avatar_url = avatar_url
self.github_id = github_id

def find_or_create_from_token(access_token):
data = GitHub.get_user_from_token(access_token)

"""Find existing user or create new User instance"""
instance = User.query.filter_by(username=data['login']).first()

if not instance:
instance = User(data['login'], data['avatar_url'], data['id'])
db.session.add(instance)
db.session.commit()

return instance

def __repr__(self):
return "<User: {}>".format(self.username)
@@ -0,0 +1 @@
"""Services package"""
@@ -0,0 +1,58 @@
import requests, json

from flask import flash

api_url = 'https://api.github.com'
authorize_url = 'https://github.com/login/oauth/authorize'
token_url = 'https://github.com/login/oauth/access_token'

class GitHub():

def __init__(self, client_id = '', client_secret = '', access_token = ''):
self.client_id = client_id
self.client_secret = client_secret
self.access_token = access_token

def authorization_url(self, scope):
return authorize_url + '?client_id={}&client_secret={}&scope={}'.format(
self.client_id,
self.client_secret,
scope
)

def get_token(self, code):
"""Fetch GitHub Access Token for GitHub OAuth."""
headers = { 'Accept': 'application/json' }
params = {
'code': code,
'client_id': self.client_id,
'client_secret': self.client_secret,
}

data = requests.post(token_url, params=params, headers=headers).json()
return data.get('access_token', None)

def get_user_from_token(access_token):
"""Fetch user data using the access token."""
url = api_url + '/user'
params = { 'access_token': access_token }

return requests.get(url, params=params).json()

def get(self, route_url, params = {}):
url = api_url + route_url
params['access_token'] = self.access_token

return requests.get(url, params=params).json()

def post(self, route_url, params = {}):
url = api_url + route_url
params['access_token'] = self.access_token

return requests.post(url, params=params).json()

def delete(self, route_url, params = {}):
url = api_url + route_url
params['access_token'] = self.access_token

return requests.delete(url, params=params)
@@ -11,3 +11,4 @@
SECRET_KEY = os.getenv('SECRET_KEY')
GITHUB_CLIENT_ID = os.getenv('GITHUB_CLIENT_ID')
GITHUB_CLIENT_SECRET = os.getenv('GITHUB_CLIENT_SECRET')
SQLALCHEMY_TRACK_MODIFICATIONS = False
No changes.

This file was deleted.

This file was deleted.

0 comments on commit b509513

Please sign in to comment.
You can’t perform that action at this time.