diff --git a/.github/workflows/cd-workflow.yml b/.github/workflows/cd-workflow.yml index 773fa57..d31cb3f 100644 --- a/.github/workflows/cd-workflow.yml +++ b/.github/workflows/cd-workflow.yml @@ -3,6 +3,7 @@ on: push: branches: - "master" + - "submission" jobs: lint: runs-on: ubuntu-latest @@ -33,10 +34,11 @@ jobs: DYNACONF_DB_API: ${{ secrets.db_api }} DYNACONF_DB_NAME: ${{ secrets.db_name }} DYNACONF_DB_CONN_NAME: ${{ secrets.db_conn_name }} - DYNACONF_GITHUB_CLIENT_SECRET: ${{ client_secret }} - DYNACONF_GITHUB_CLIENT_ID: ${{ client_id } - DYNACONF_GITHUB_URI_USER: "https://github.com/login/oauth/access_token" - DYNACONF_GITHUB_URI_OAUTH: "https://api.github.com/user" + DYNACONF_GITHUB_CLIENT_SECRET: ${{ secrets.client_secret }} + DYNACONF_GITHUB_CLIENT_ID: ${{ secrets.client_id }} + DYNACONF_GITHUB_URI_OAUTH: https://github.com/login/oauth/access_token + DYNACONF_GITHUB_URI_USER: https://api.github.com/user + DYNACONF_FLASK_SECRET: ${{ secrets.flask_secret }} run: env | grep DYNACONF_ > .env - name: Deploy to GCP AppEngine uses: actions/gcloud/cli@master diff --git a/connectors/exercism/populate.py b/connectors/exercism/populate.py index 1b83813..e4fb40e 100644 --- a/connectors/exercism/populate.py +++ b/connectors/exercism/populate.py @@ -16,8 +16,8 @@ def get_or_create_default_user(db_session): name="exercism.io", nickname="exercism", is_teacher=True, - github_token="", - token="") + github_token="sample-github-token", + token="sample-github-token") db_session.add(exercism_user) db_session.flush() return exercism_user diff --git a/db/helpers.py b/db/helpers.py index ab673bd..afd08a8 100644 --- a/db/helpers.py +++ b/db/helpers.py @@ -3,36 +3,34 @@ from models import Base -def create_schema(api, username, password, name, conn_name=None, echo=False): - unix_socket = f'/cloudsql/{conn_name}' - engine = create_engine( - url.URL( - drivername=api, - username=username, - password=password, - database=name, - query={ - 'host': unix_socket - }), - client_encoding="utf8", - echo=echo) +def create_db_uri(api, username, password, name, conn_name, + production): + if production: + unix_socket = f'/cloudsql/{conn_name}' + db_uri = url.URL(drivername=api, username=username, password=password, + database=name, query={'host': unix_socket}) + else: + db_uri = url.URL(drivername=api, username=username, host="localhost", + password=password, database=name) + + return db_uri + + +def create_schema(api, username, password, name, + conn_name=None, echo=False, production=False): + db_uri = create_db_uri(api, username, password, + name, conn_name, production) + + engine = create_engine(db_uri, client_encoding="utf8", echo=echo) return Base.metadata.create_all(engine) -def database_setup(api, username, password, name, conn_name=None, echo=False): - unix_socket = f'/cloudsql/{conn_name}' - engine = create_engine( - url.URL( - drivername=api, - username=username, - password=password, - database=name, - query={ - 'host': unix_socket - }), - client_encoding="utf8", - echo=echo) +def database_setup(api, username, password, name, conn_name=None, + echo=False, production=False): + db_uri = create_db_uri(api, username, password, + name, conn_name, production) + engine = create_engine(db_uri, client_encoding="utf8", echo=echo) sess = orm.sessionmaker(bind=engine) return sess() diff --git a/server/static/js/main.71351dde.chunk.js b/server/static/js/main.71351dde.chunk.js deleted file mode 100644 index d5a7f70..0000000 --- a/server/static/js/main.71351dde.chunk.js +++ /dev/null @@ -1,2 +0,0 @@ -(this["webpackJsonpsharpener-frontend"]=this["webpackJsonpsharpener-frontend"]||[]).push([[0],{32:function(e,t,r){e.exports=r(65)},65:function(e,t,r){"use strict";r.r(t);var n={};r.r(n),r.d(n,"userData",(function(){return G})),r.d(n,"loginSuccess",(function(){return C}));var o=r(0),c=r.n(o),a=r(9),i=r.n(a),u=r(5),s=r(3),l=r.n(s),p=r(26),O=r.n(p).a.create({baseURL:"",responseType:"json"}),f="".concat("https://github.com/login/oauth/authorize","?client_id=").concat("36689cf871668e2b775e","&scope=user&redirect_uri=").concat("http://localhost:3000"),b=function(){window.location=f},y=function(){return c.a.createElement("div",null,c.a.createElement("button",{onClick:b}," Login"))},d=function(e){var t=e.token;return e.loading?c.a.createElement("div",null," Loading your user data..."):c.a.createElement("div",null," Your CLI token is: ",t)},g=r(7),j=Object(g.b)((function(e){var t=e.user;return{token:t.token,loading:t.isLoadingData}}))(d),v=r(8),h=Object(u.a)({LANDING:c.a.createElement(y,null),HOME:c.a.createElement(j,null)},v.b,c.a.createElement("div",null,"Not found")),m=function(e){var t=e.location;return h[t]||h[v.b]};m.propTypes={location:l.a.string.isRequired};var w=Object(g.b)((function(e){return{location:e.location.type}}))(m),P=r(6),D=r(30),E=r(31),L=r.n(E);function k(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function S(e){for(var t=1;t0&&void 0!==arguments[0]&&arguments[0],t=arguments.length>1?arguments[1]:void 0,r=t.payload,n=t.type;switch(n){case"LOGIN_DATA":var o=r.avatar,c=r.email,a=r.token,i=r.name,u=r.nickname;return S({},e,{avatar:o,email:c,token:a,name:i,nickname:u,isLoadingData:!1});case"LOGIN_SUCCESS":return S({},e,{isLoadingData:!0,isLoggedIn:!0});default:return e}}};function N(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function A(e){for(var t=1;t {\n window.location = loginURI;\n};\n\nconst authenticateUser = code => client.get(`${AUTH_URI}/${code}`)\n\nexport {\n redirectToGithub,\n authenticateUser,\n};\n","import React from 'react';\nimport { redirectToGithub } from 'api';\n\nconst Landing = () => (\n
\n \n
\n);\n\nexport default Landing;\n","import React from 'react';\n\nconst Home = ({ token, loading }) =>(\n loading \n ?(
Loading your user data...
)\n :
Your CLI token is: {token}
\n)\n\n export default Home;\n","import Home from './Home';\nimport { connect } from 'react-redux';\n\nconst mapStateToProps = ({ user }) => ({\n token: user.token,\n loading: user.isLoadingData,\n});\n\nconst ConnectedHome = connect(mapStateToProps)(Home);\n\nexport default ConnectedHome;\n","import React from 'react';\nimport PropTypes from 'prop-types';\nimport Landing from 'Landing';\nimport Home from 'Home';\nimport { NOT_FOUND } from 'redux-first-router';\nimport { connect } from 'react-redux'\n\nconst componentsMap = {\n LANDING: ,\n HOME: ,\n [NOT_FOUND]:
Not found
,\n}; \n\nconst App = ({ location }) => componentsMap[location] || componentsMap[NOT_FOUND];\n\nconst mapStateToProps = state => ({\n location: state.location.type,\n});\n\nApp.propTypes = {\n location: PropTypes.string.isRequired,\n};\n\nexport default connect(mapStateToProps)(App);\n","import { LOGIN_SUCCESS, LOGIN_DATA } from 'store/constants';\n\nconst loginReducer = (state = false, action) => {\n const { payload, type } = action;\n switch (type) {\n case LOGIN_DATA:\n const { avatar, email, token,\n name, nickname } = payload;\n return {\n ...state,\n avatar,\n email,\n token,\n name,\n nickname,\n isLoadingData: false,\n };\n case LOGIN_SUCCESS:\n return {\n ...state,\n isLoadingData: true,\n isLoggedIn: true \n };\n default:\n return state;\n }\n};\n\nexport default loginReducer;\n","import user from './userReducer';\nconst appReducers = {\nuser,\n}\n\nexport default appReducers;\n","export const LOGIN_SUCCESS = \"LOGIN_SUCCESS\";\nexport const LOGIN_DATA = \"LOGIN_DATA\";\n","import { LOGIN_SUCCESS, LOGIN_DATA } from \"store/constants\";\n\n\nexport const userData = (data) => ({\n type: LOGIN_DATA,\n payload: {\n ...data\n }\n})\n\nexport const loginSuccess = () => ({\n type: LOGIN_SUCCESS,\n})\n","import { loginActions } from 'store/actions';\nimport { authenticateUser } from 'api';\n\nconst { userData, loginSuccess } = loginActions;\n\nconst landingThunk = (dispatch, getState) => {\n const { location, user } = getState();\n if(user.isLoggedIn){\n dispatch({\n type:'HOME'\n });\n }\n const code = location.query? location.query.code: \"\";\n if(code) {\n dispatch(loginSuccess());\n dispatch({\n type:'HOME'\n });\n authenticateUser(code).then( ({ data }) =>{\n dispatch(userData(data));\n })\n }\n}\n\nconst homeThunk = (dispatch, getState) => {\n const { user } = getState();\n if(!user.isLoggedIn){\n dispatch({\n type:'LANDING'\n });\n }\n}\n\nconst routesMap = {\n LANDING: { \n path:'/',\n thunk: landingThunk,\n },\n HOME: {\n path:'/home',\n thunk: homeThunk,\n },\n}\n\nexport default routesMap;\n","import { connectRoutes } from 'redux-first-router';\nimport { applyMiddleware, combineReducers, createStore } from 'redux';\nimport { composeWithDevTools } from 'redux-devtools-extension';\nimport queryString from 'query-string';\nimport appReducers from 'store/reducers';\nimport routesMap from './routes'\n\nconst configureStore = (...args) => {\n const { middleware, enhancer, reducer } = connectRoutes(\n routesMap,\n {\n scrollTop: true,\n querySerializer: queryString\n },\n );\n\n const rootReducer = combineReducers({ location: reducer, ...appReducers })\n const middlewares = applyMiddleware(middleware, ...args)\n const enhancers = composeWithDevTools(enhancer, middlewares)\n const { user } = window;\n\n const store = createStore(rootReducer,{user}, enhancers)\n\n return store;\n}\n\nexport default configureStore;\n","import React from 'react'\nimport ReactDOM from 'react-dom'\nimport App from 'App';\nimport { Provider } from 'react-redux'\n\nimport configureStore from 'store';\nconst store = configureStore()\n\nReactDOM.render(\n \n \n ,\n document.getElementById('root'),\n);\n"],"sourceRoot":""} \ No newline at end of file diff --git a/server/static/js/main.d13865bf.chunk.js b/server/static/js/main.d13865bf.chunk.js new file mode 100644 index 0000000..fb06d3f --- /dev/null +++ b/server/static/js/main.d13865bf.chunk.js @@ -0,0 +1,2 @@ +(this["webpackJsonpsharpener-frontend"]=this["webpackJsonpsharpener-frontend"]||[]).push([[0],{32:function(e,t,r){e.exports=r(65)},65:function(e,t,r){"use strict";r.r(t);var n={};r.r(n),r.d(n,"userData",(function(){return G})),r.d(n,"loginSuccess",(function(){return C}));var o=r(0),c=r.n(o),a=r(9),i=r.n(a),u=r(5),s=r(3),p=r.n(s),l=r(26),O=r.n(l).a.create({baseURL:"",responseType:"json"}),f="".concat("https://github.com/login/oauth/authorize","?client_id=").concat("36689cf871668e2b775e","&scope=user&redirect_uri=").concat("http://sharpener-cloud.appspot.com"),b=function(){window.location=f},y=function(){return c.a.createElement("div",null,c.a.createElement("button",{onClick:b}," Login"))},d=function(e){var t=e.token;return e.loading?c.a.createElement("div",null," Loading your user data..."):c.a.createElement("div",null," Your CLI token is: ",t)},g=r(7),j=Object(g.b)((function(e){var t=e.user;return{token:t.token,loading:t.isLoadingData}}))(d),v=r(8),h=Object(u.a)({LANDING:c.a.createElement(y,null),HOME:c.a.createElement(j,null)},v.b,c.a.createElement("div",null,"Not found")),m=function(e){var t=e.location;return h[t]||h[v.b]};m.propTypes={location:p.a.string.isRequired};var w=Object(g.b)((function(e){return{location:e.location.type}}))(m),P=r(6),D=r(30),E=r(31),L=r.n(E);function k(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function S(e){for(var t=1;t0&&void 0!==arguments[0]&&arguments[0],t=arguments.length>1?arguments[1]:void 0,r=t.payload,n=t.type;switch(n){case"LOGIN_DATA":var o=r.avatar,c=r.email,a=r.token,i=r.name,u=r.nickname;return S({},e,{avatar:o,email:c,token:a,name:i,nickname:u,isLoadingData:!1});case"LOGIN_SUCCESS":return S({},e,{isLoadingData:!0,isLoggedIn:!0});default:return e}}};function N(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function A(e){for(var t=1;t {\n window.location = loginURI;\n};\n\nconst authenticateUser = code => client.get(`${AUTH_URI}/${code}`)\n\nexport {\n redirectToGithub,\n authenticateUser,\n};\n","import React from 'react';\nimport { redirectToGithub } from 'api';\n\nconst Landing = () => (\n
\n \n
\n);\n\nexport default Landing;\n","import React from 'react';\n\nconst Home = ({ token, loading }) =>(\n loading \n ?(
Loading your user data...
)\n :
Your CLI token is: {token}
\n)\n\n export default Home;\n","import Home from './Home';\nimport { connect } from 'react-redux';\n\nconst mapStateToProps = ({ user }) => ({\n token: user.token,\n loading: user.isLoadingData,\n});\n\nconst ConnectedHome = connect(mapStateToProps)(Home);\n\nexport default ConnectedHome;\n","import React from 'react';\nimport PropTypes from 'prop-types';\nimport Landing from 'Landing';\nimport Home from 'Home';\nimport { NOT_FOUND } from 'redux-first-router';\nimport { connect } from 'react-redux'\n\nconst componentsMap = {\n LANDING: ,\n HOME: ,\n [NOT_FOUND]:
Not found
,\n}; \n\nconst App = ({ location }) => componentsMap[location] || componentsMap[NOT_FOUND];\n\nconst mapStateToProps = state => ({\n location: state.location.type,\n});\n\nApp.propTypes = {\n location: PropTypes.string.isRequired,\n};\n\nexport default connect(mapStateToProps)(App);\n","import { LOGIN_SUCCESS, LOGIN_DATA } from 'store/constants';\n\nconst loginReducer = (state = false, action) => {\n const { payload, type } = action;\n switch (type) {\n case LOGIN_DATA:\n const { avatar, email, token,\n name, nickname } = payload;\n return {\n ...state,\n avatar,\n email,\n token,\n name,\n nickname,\n isLoadingData: false,\n };\n case LOGIN_SUCCESS:\n return {\n ...state,\n isLoadingData: true,\n isLoggedIn: true \n };\n default:\n return state;\n }\n};\n\nexport default loginReducer;\n","import user from './userReducer';\nconst appReducers = {\nuser,\n}\n\nexport default appReducers;\n","export const LOGIN_SUCCESS = \"LOGIN_SUCCESS\";\nexport const LOGIN_DATA = \"LOGIN_DATA\";\n","import { LOGIN_SUCCESS, LOGIN_DATA } from \"store/constants\";\n\n\nexport const userData = (data) => ({\n type: LOGIN_DATA,\n payload: {\n ...data\n }\n})\n\nexport const loginSuccess = () => ({\n type: LOGIN_SUCCESS,\n})\n","import { loginActions } from 'store/actions';\nimport { authenticateUser } from 'api';\n\nconst { userData, loginSuccess } = loginActions;\n\nconst landingThunk = (dispatch, getState) => {\n const { location, user } = getState();\n if(user.isLoggedIn){\n dispatch({\n type:'HOME'\n });\n }\n const code = location.query? location.query.code: \"\";\n if(code) {\n dispatch(loginSuccess());\n dispatch({\n type:'HOME'\n });\n authenticateUser(code).then( ({ data }) =>{\n dispatch(userData(data));\n })\n }\n}\n\nconst homeThunk = (dispatch, getState) => {\n const { user } = getState();\n if(!user.isLoggedIn){\n dispatch({\n type:'LANDING'\n });\n }\n}\n\nconst routesMap = {\n LANDING: { \n path:'/',\n thunk: landingThunk,\n },\n HOME: {\n path:'/home',\n thunk: homeThunk,\n },\n}\n\nexport default routesMap;\n","import { connectRoutes } from 'redux-first-router';\nimport { applyMiddleware, combineReducers, createStore } from 'redux';\nimport { composeWithDevTools } from 'redux-devtools-extension';\nimport queryString from 'query-string';\nimport appReducers from 'store/reducers';\nimport routesMap from './routes'\n\nconst configureStore = (...args) => {\n const { middleware, enhancer, reducer } = connectRoutes(\n routesMap,\n {\n scrollTop: true,\n querySerializer: queryString\n },\n );\n\n const rootReducer = combineReducers({ location: reducer, ...appReducers })\n const middlewares = applyMiddleware(middleware, ...args)\n const enhancers = composeWithDevTools(enhancer, middlewares)\n const { user } = window;\n\n const store = createStore(rootReducer,{user}, enhancers)\n\n return store;\n}\n\nexport default configureStore;\n","import React from 'react'\nimport ReactDOM from 'react-dom'\nimport App from 'App';\nimport { Provider } from 'react-redux'\n\nimport configureStore from 'store';\nconst store = configureStore()\n\nReactDOM.render(\n \n \n ,\n document.getElementById('root'),\n);\n"],"sourceRoot":""} \ No newline at end of file diff --git a/server/templates/index.html b/server/templates/index.html index c9ee11d..d47f298 100644 --- a/server/templates/index.html +++ b/server/templates/index.html @@ -19,13 +19,14 @@ }; {% if debug %} + {% else %} - + - + {% endif %} diff --git a/server/views/users.py b/server/views/users.py index b7ee936..6894b4b 100644 --- a/server/views/users.py +++ b/server/views/users.py @@ -18,17 +18,42 @@ def fetch_access_token(code, github_config): return github_response.json().get('access_token') -def fetch_user_data(access_token, github_config): +def get_default_oauth_settings(access_token, github_config): headers = {'Authorization': f"token {access_token}"} params = { "client_id": github_config['client_id'], "client_secret": github_config['client_secret'], } + return headers, params + + +def fetch_user_data(access_token, github_config): + headers, params = get_default_oauth_settings(access_token, github_config) github_response = requests.get(github_config['user_uri'], headers=headers, params=params) + fetched_data = github_response.json() + if not fetched_data.get('email'): + fetched_data['email'] = fetch_private_email(access_token, + github_config) + + return fetched_data + - return github_response.json() +def get_primary_email(emails): + primary_email = [email_metadata for email_metadata in emails + if email_metadata['primary'][0] + return primary_email + +def fetch_private_email(access_token, github_config): + headers, params = get_default_oauth_settings(access_token, github_config) + email_resource_uri = f"{github_config['user_uri']}/emails" + github_response = requests.get(email_resource_uri, + headers=headers, + params=params) + fetched_data = github_response.json() + private_email = get_primary_email(fetched_data) + return private_email def create_user(db_session, user_data, access_token): @@ -71,28 +96,30 @@ def create_users_blueprint(db_session, request, github_config): def authenticate_user(code): access_token = fetch_access_token(code, github_config) - if access_token: - user_data = fetch_user_data(access_token, github_config) - existing_user = db_session.query(User)\ - .filter_by(email=user_data['email'])\ - .first() - - if existing_user: - update_user_info(db_session, existing_user, - user_data, access_token) - update_session_info(existing_user) - else: - new_user = create_user(db_session, user_data, access_token) - update_session_info(new_user) - - response = { + if not access_token: + return Response(f"Something went wrong. Your github code\ + was invalid.", status=400) + + user_data = fetch_user_data(access_token, github_config) + existing_user = db_session.query(User)\ + .filter_by(email=user_data['email'])\ + .first() + + if existing_user: + update_user_info(db_session, existing_user, + user_data, access_token) + update_session_info(existing_user) + else: + new_user = create_user(db_session, user_data, access_token) + update_session_info(new_user) + + response = { "token": session['token'], "email": session['email'], "name": session['name'], "nickname": session['nickname'], "avatar": session['avatar'], } - return jsonify(response) - return Response(status=400) + return jsonify(response) return users diff --git a/settings.toml b/settings.toml index af68b3a..97c8b25 100644 --- a/settings.toml +++ b/settings.toml @@ -1,5 +1,5 @@ [default] -env="development" -db_api="postgresql+psycopg2" -db_name="postgres" -bucket_exercises="sharpener-exercises" +ENV="development" +DB_API="postgresql+psycopg2" +DB_NAME="postgres" +BUCKET_EXERCISES="sharpener-exercises" diff --git a/sharpener-admin.py b/sharpener-admin.py index 125199e..ba00c99 100755 --- a/sharpener-admin.py +++ b/sharpener-admin.py @@ -15,19 +15,9 @@ from db import database_setup, create_schema -is_production = settings.ENV == "production" -if is_production: - googleclouddebugger.enable() - -echo = False if is_production else True -db_session = database_setup(settings.DB_API, - settings.DB_USERNAME, - settings.DB_PASSWORD, - settings.DB_NAME, - settings.DB_CONN_NAME, - echo=echo) - local = __name__ == "__main__" +is_production = settings.ENV == "production" +is_development = not is_production github_config = { "oauth_uri": settings.GITHUB_URI_OAUTH, @@ -36,7 +26,20 @@ "client_secret": settings.GITHUB_CLIENT_SECRET, } -app = create_app(db_session, github_config, settings.FLASK_SECRET, debug=local) +if is_production: + googleclouddebugger.enable() + + +db_session = database_setup(settings.DB_API, + settings.DB_USERNAME, + settings.DB_PASSWORD, + settings.DB_NAME, + settings.get("DB_CONN_NAME"), + echo=is_development, + production=is_production) + +app = create_app(db_session, github_config, settings.FLASK_SECRET, + debug=is_development) if local: args = docopt(__doc__) @@ -45,8 +48,9 @@ settings.DB_USERNAME, settings.DB_PASSWORD, settings.DB_NAME, - settings.DB_CONN_NAME, - echo=echo) + settings.get("DB_CONN_NAME"), + echo=is_development, + production=is_production) if args["populate"]: storage_client = storage.Client()