Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions backend/conduit/app.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
# -*- coding: utf-8 -*-
"""The app module, containing the app factory function."""
from flask import Flask
from conduit.extensions import bcrypt, cache, db, migrate, jwt, cors
from conduit.extensions import bcrypt, cache, db, migrate, jwt, cors, github
from conduit import commands, user, profile, articles, organizations, tags
from conduit.settings import ProdConfig
from conduit.exceptions import InvalidUsage


def create_app(config_object=ProdConfig):
"""An application factory, as explained here:
http://flask.pocoo.org/docs/patterns/appfactories/.
Expand All @@ -23,14 +22,16 @@ def create_app(config_object=ProdConfig):
register_commands(app)
return app


def register_extensions(app):
"""Register Flask extensions."""
bcrypt.init_app(app)
cache.init_app(app)
db.init_app(app)
migrate.init_app(app, db)
jwt.init_app(app)
app.config['GITHUB_CLIENT_ID'] = '98574e099fa640413899'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should put the client secret and client id in the .env file

app.config['GITHUB_CLIENT_SECRET'] = '272ac3010797de4cc29c5c0caf0bbd9df4d79832'
github.init_app(app)


def register_blueprints(app):
Expand Down
12 changes: 12 additions & 0 deletions backend/conduit/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import os
from dotenv import load_dotenv
from os.path import dirname, join

dotenv_path = join(dirname(__file__), '.env')
load_dotenv(dotenv_path)

GITHUB_CLIENT = os.environ.get('GITHUB_ID')
GITHUB_SECRET = os.environ.get('GITHUB_SECRET')
ACCESS_TOKEN_URL = os.environ.get('ACCESS_TOKEN_URL')
GITHUB_API = os.environ.get('GITHUB_API')
STATE = os.environ.get('STATE')
2 changes: 2 additions & 0 deletions backend/conduit/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from flask_jwt_extended import JWTManager
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy, Model
from flask_github import GitHub


class CRUDMixin(Model):
Expand Down Expand Up @@ -55,6 +56,7 @@ def delete(self, commit=True):
migrate = Migrate()
cache = Cache()
cors = CORS()
github = GitHub()

from conduit.utils import jwt_identity, identity_loader # noqa

Expand Down
3 changes: 2 additions & 1 deletion backend/conduit/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ class Config(object):
'https://bit-next-git-master.bitproject.now.sh',
'https://staging.bitproject.org',
'https://dev.bitproject.org',
'https://bit-next.now.sh'
'https://bit-next.now.sh',
'https://github.com/login/oauth/authorize?client_id=98574e099fa640413899'
]
JWT_HEADER_TYPE = 'Token'

Expand Down
9 changes: 8 additions & 1 deletion backend/conduit/user/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,17 @@ class User(SurrogatePK, Model):
linkedinLink = Column(db.String(100), nullable=True)
website = Column(db.Text(), nullable = True)
isAdmin = Column(db.Boolean, nullable=False, default=False)
github_access_token = Column(db.String(255), nullable = True)
github_id = Column(db.Integer, nullable = True)
token: str = ''

def __init__(self, username, email, password=None, **kwargs):
def __init__(self, username, email, password=None, github_access_token=None, **kwargs):
"""Create instance."""
db.Model.__init__(self, username=username, email=email, **kwargs)
if password:
self.set_password(password)
if github_access_token:
self.set_github_token(github_access_token)
else:
self.password = None

Expand All @@ -42,6 +46,9 @@ def check_password(self, value):
"""Check password."""
return bcrypt.check_password_hash(self.password, value)

def set_github_token(self, github_access_token):
self.github_access_token = github_access_token

def __repr__(self):
"""Represent instance as a unique string."""
return '<User({username!r})>'.format(username=self.username)
44 changes: 41 additions & 3 deletions backend/conduit/user/views.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
# -*- coding: utf-8 -*-
"""User views."""
from flask import Blueprint, request
from flask import Blueprint, request, jsonify, g
from flask_apispec import use_kwargs, marshal_with
from flask_jwt_extended import jwt_required, jwt_optional, create_access_token, current_user
from sqlalchemy.exc import IntegrityError

from conduit.database import db
from conduit.extensions import github
from conduit.exceptions import InvalidUsage
from conduit.profile.models import UserProfile
from .models import User
from .serializers import user_schema
from conduit.config import GITHUB_CLIENT, GITHUB_SECRET, ACCESS_TOKEN_URL, GITHUB_API, STATE
import requests
import os

blueprint = Blueprint('user', __name__)


@blueprint.route('/api/users', methods=('POST',))
@use_kwargs(user_schema)
@marshal_with(user_schema)
Expand All @@ -26,7 +29,6 @@ def register_user(username, password, email, **kwargs):
raise InvalidUsage.user_already_registered()
return userprofile.user


@blueprint.route('/api/users/login', methods=('POST',))
@jwt_optional
@use_kwargs(user_schema)
Expand Down Expand Up @@ -64,3 +66,39 @@ def update_user(**kwargs):
kwargs['updated_at'] = user.created_at.replace(tzinfo=None)
user.update(**kwargs)
return user

@blueprint.route('/api/user/callback/<github_code>/<state>', methods = ('GET',))
@marshal_with(user_schema)
def github_oauth(github_code, state):
try:
if (state.strip() != STATE):
raise InvalidUsage.user_not_found()

payload = { 'client_id': GITHUB_CLIENT,
'client_secret': GITHUB_SECRET,
'code': github_code,
}
header = {
'Accept': 'application/json',
}

auth_response = requests.post(ACCESS_TOKEN_URL, params=payload, headers=header).json()
access_token = auth_response["access_token"]

auth_header = {"Authorization": "Bearer " + access_token}
data_response = requests.get(GITHUB_API + 'user', headers=auth_header).json()
email_response = requests.get(GITHUB_API + 'user/emails', headers=auth_header).json()

username = data_response["login"]
email = email_response[0]["email"]
github_id = data_response["id"]

user = User.query.filter_by(email=email).first()
if user is None:
userprofile = UserProfile(User(username, email, github_access_token = access_token).save()).save()
user = userprofile.user

user.token = create_access_token(identity=user, fresh=True)
return user
except:
raise InvalidUsage.user_not_found()
40 changes: 38 additions & 2 deletions components/profile/LoginForm.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import Router from "next/router";
import React from "react";
import React, { useEffect } from "react";
import { mutate } from "swr";

import ListErrors from "../common/ListErrors";
import UserAPI from "../../lib/api/user";
import { CODE_URL, STATE, SCOPE, GITHUB_CLIENT } from "../../lib/utils/constant";

const LoginForm = () => {
const [isLoading, setLoading] = React.useState(false);
Expand All @@ -20,6 +21,38 @@ const LoginForm = () => {
[]
);

const authorize_url = CODE_URL + "?client_id=" + GITHUB_CLIENT + "&scope=" + SCOPE
+ "&state=" + encodeURIComponent(STATE);

let logging_in;
if (typeof window !== "undefined"){
const code = new URLSearchParams(window.location.search).get("code");
const state = new URLSearchParams(window.location.search).get("state");
if (code){
logging_in = (<p>Redirecting to home page...</p>);
useEffect(() => {

async function post_code(){
try{
const {data, status} = await UserAPI.post_code(code, state);
console.log("begun await");
if (data?.user){
console.log(data.user)
window.localStorage.setItem("user", JSON.stringify(data.user));
mutate("user", data?.user);
Router.push("/");
}
} catch(error){
console.error(error);
}
}

post_code();
}, [])
}
}


const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
Expand Down Expand Up @@ -77,8 +110,11 @@ const LoginForm = () => {
</button>
</fieldset>
</form>
<a href={authorize_url} className="btn btn-lg btn-primary pull-xs-right">
Sign in through Github</a>
{logging_in}
</>
);
};
}

export default LoginForm;
20 changes: 20 additions & 0 deletions lib/api/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import axios from "axios";

import { SERVER_BASE_URL } from "../utils/constant";

const url = require('url');

const UserAPI = {
current: async () => {
const user: any = window.localStorage.getItem("user");
Expand Down Expand Up @@ -135,6 +137,24 @@ const UserAPI = {
}
},
get: async (username) => axios.get(`${SERVER_BASE_URL}/profiles/${username}`),

post_code: async (github_code, state) => {
try{
const response = await axios.get(
`${SERVER_BASE_URL}/user/callback/${encodeURIComponent(github_code)}/${encodeURIComponent(state)}`,
{
headers: {
"Content-Type": "application/json",
},
}
);
return response;
} catch (error){
return error.response;
}

},

};

export default UserAPI;
10 changes: 8 additions & 2 deletions lib/utils/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
// export const SERVER_BASE_URL = `https://conduit.productionready.io/api`;

// Local backend url
// export const SERVER_BASE_URL = `http://127.0.0.1:5000/api`;
export const SERVER_BASE_URL = `http://127.0.0.1:5000/api`;

// Staging url
export const SERVER_BASE_URL = `https://bit-devs-staging.herokuapp.com/api`;
// export const SERVER_BASE_URL = `https://bit-devs-staging.herokuapp.com/api`;

// Production url. ONLY USE IN PRODUCTION
// export const SERVER_BASE_URL = `https://bit-devs.herokuapp.com/api`;
Expand All @@ -14,6 +14,12 @@ export const SERVER_BASE_URL = `https://bit-devs-staging.herokuapp.com/api`;

export const APP_NAME = `conduit`;

export const CODE_URL = 'https://github.com/login/oauth/authorize';
export const GITHUB_CLIENT = '98574e099fa640413899';
export const SCOPE = 'user+repo';
//must conceal state later
export const STATE = 'd3Asp0fK03M0Ldnwoi2Pnbh9knB2K335Ln';

export const ARTICLE_QUERY_MAP = {
"tab=feed": `${SERVER_BASE_URL}/articles/feed`,
"tab=tag": `${SERVER_BASE_URL}/articles/tag`
Expand Down