diff --git a/backend/conduit/app.py b/backend/conduit/app.py index 9c71548..2c02e12 100644 --- a/backend/conduit/app.py +++ b/backend/conduit/app.py @@ -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/. @@ -23,7 +22,6 @@ def create_app(config_object=ProdConfig): register_commands(app) return app - def register_extensions(app): """Register Flask extensions.""" bcrypt.init_app(app) @@ -31,6 +29,9 @@ def register_extensions(app): db.init_app(app) migrate.init_app(app, db) jwt.init_app(app) + app.config['GITHUB_CLIENT_ID'] = '98574e099fa640413899' + app.config['GITHUB_CLIENT_SECRET'] = '272ac3010797de4cc29c5c0caf0bbd9df4d79832' + github.init_app(app) def register_blueprints(app): diff --git a/backend/conduit/config.py b/backend/conduit/config.py new file mode 100644 index 0000000..3a165cc --- /dev/null +++ b/backend/conduit/config.py @@ -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') \ No newline at end of file diff --git a/backend/conduit/extensions.py b/backend/conduit/extensions.py index 0d24829..d7d2048 100644 --- a/backend/conduit/extensions.py +++ b/backend/conduit/extensions.py @@ -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): @@ -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 diff --git a/backend/conduit/settings.py b/backend/conduit/settings.py index 742f196..7a02c01 100644 --- a/backend/conduit/settings.py +++ b/backend/conduit/settings.py @@ -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' diff --git a/backend/conduit/user/models.py b/backend/conduit/user/models.py index f0493b1..39ba1f4 100644 --- a/backend/conduit/user/models.py +++ b/backend/conduit/user/models.py @@ -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 @@ -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 ''.format(username=self.username) diff --git a/backend/conduit/user/views.py b/backend/conduit/user/views.py index dc56916..86a5afd 100644 --- a/backend/conduit/user/views.py +++ b/backend/conduit/user/views.py @@ -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) @@ -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) @@ -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//', 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() diff --git a/components/profile/LoginForm.tsx b/components/profile/LoginForm.tsx index aad5701..1029863 100644 --- a/components/profile/LoginForm.tsx +++ b/components/profile/LoginForm.tsx @@ -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); @@ -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 = (

Redirecting to home page...

); + 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); @@ -77,8 +110,11 @@ const LoginForm = () => { + + Sign in through Github + {logging_in} ); -}; +} export default LoginForm; diff --git a/lib/api/user.ts b/lib/api/user.ts index c1e7017..d05adf5 100644 --- a/lib/api/user.ts +++ b/lib/api/user.ts @@ -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"); @@ -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; \ No newline at end of file diff --git a/lib/utils/constant.ts b/lib/utils/constant.ts index e24c371..bbbfd72 100644 --- a/lib/utils/constant.ts +++ b/lib/utils/constant.ts @@ -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`; @@ -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`