diff --git a/client/package.json b/client/package.json index c2645c0..117db32 100644 --- a/client/package.json +++ b/client/package.json @@ -31,7 +31,7 @@ "test": "react-scripts test", "eject": "react-scripts eject" }, - "proxy": "http://localhost:3000", + "proxy": "http://localhost:4000", "eslintConfig": { "extends": "react-app" }, diff --git a/client/src/App.js b/client/src/App.js index dd6f6f9..840ceeb 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -1,7 +1,7 @@ import React, { Component } from "react"; import { BrowserRouter as Router, Route } from "react-router-dom"; -import { Home } from "./pages"; +import { Home, GithubCallback } from "./pages"; import Request from "./components/Request"; @@ -11,14 +11,18 @@ class App extends Component { }; renderError = err => { - return
{err}
; + return
{err.toString()}
; + }; + + renderGithubCallback = () => { + return ; }; renderHome = () => { return ( @@ -29,11 +33,16 @@ class App extends Component { render() { return ( -
- + +
+ - -
+
+ ); } } diff --git a/client/src/components/EditableInput.js b/client/src/components/EditableInput.js new file mode 100644 index 0000000..504c335 --- /dev/null +++ b/client/src/components/EditableInput.js @@ -0,0 +1,10 @@ +import React from "react"; + +export default ({ value, onClick, onSubmit, editable }) => + editable ? ( + + + + ) : ( + {value} + ); diff --git a/client/src/components/Request.js b/client/src/components/Request.js index 4258eb3..568eef3 100644 --- a/client/src/components/Request.js +++ b/client/src/components/Request.js @@ -11,7 +11,11 @@ export default class Request extends Component { }; componentDidMount() { - axios[this.props.method](this.props.url).then( + axios[this.props.method](this.props.url, { + headers: { + Authorization: `Bearer ${window.localStorage.getItem("jwt")}` + } + }).then( response => { this.setState({ payload: response.data, @@ -33,6 +37,8 @@ export default class Request extends Component { props: { renderPending, renderError, children } } = this; + console.log("rendering", requestStatus); + if (requestStatus === PENDING) return renderPending(); if (requestStatus === ERROR) return renderError(error); diff --git a/client/src/pages/GithubCallback.js b/client/src/pages/GithubCallback.js new file mode 100644 index 0000000..e3ebe0a --- /dev/null +++ b/client/src/pages/GithubCallback.js @@ -0,0 +1,23 @@ +import React from "react"; +import axios from "axios"; +import { withRouter } from "react-router-dom"; + +class GithubCallback extends React.Component { + componentDidMount() { + // polyfill required? + const params = new URL(window.location).searchParams; + const code = params.get("code"); + + axios.get(`/api/github/callback?code=${code}`).then(jwt => { + window.localStorage.setItem("jwt", jwt.data.token); + + this.props.history.push("/"); + }); + } + + render() { + return github callback ; + } +} + +export default withRouter(GithubCallback); diff --git a/client/src/pages/Home.js b/client/src/pages/Home.js index 49fe650..734749d 100644 --- a/client/src/pages/Home.js +++ b/client/src/pages/Home.js @@ -10,6 +10,20 @@ import profilePhotoPlaceholder from "../assets/profilePhotoPlaceholder.png"; import githubIcon from "../assets/githubLogo.svg"; import linkedinIcon from "../assets/linkedinLogo.svg"; import * as r from "ramda"; +const qs = require("querystring"); + +const loginUrl = `https://github.com/login/oauth/authorize?${qs.stringify({ + client_id: process.env.REACT_APP_GITHUB_CLIENT_ID, + redirect_uri: process.env.REACT_APP_GITHUB_CALLBACK_URL +})}`; + +console.log(loginUrl); + +const LoginLink = () => ( + + Developer Login + +); const HomeContainer = styled.section.attrs({ className: "black w-90 db center" @@ -109,6 +123,7 @@ const Home = ({ listings }) => { return ( + Gaza Talent {r.map(listing => ( diff --git a/client/src/pages/index.js b/client/src/pages/index.js index 6b59262..8045388 100644 --- a/client/src/pages/index.js +++ b/client/src/pages/index.js @@ -1,2 +1,3 @@ import Home from "./Home"; -export { Home }; +import GithubCallback from "./GithubCallback"; +export { Home, GithubCallback }; diff --git a/controllers/github.js b/controllers/github.js new file mode 100644 index 0000000..9219438 --- /dev/null +++ b/controllers/github.js @@ -0,0 +1,28 @@ +const wrapAsync = require("../lib/wrap_async.js"); +const jwt = require("jsonwebtoken"); + +const { accessToken, currentUser } = require("../lib/github.js"); +const airtable = require("../lib/airtable.js"); + +const callback = wrapAsync(async (req, res) => { + const userAccessToken = (await accessToken(req.query.code)).access_token; + const userProfile = await currentUser(userAccessToken); + const userExists = airtable.userExists(userProfile.login); + + if (!userExists) { + return res.status(404).json({ error: "user does not exist" }); + } + + res.json({ + token: jwt.sign( + { + githubUsername: userProfile.login + }, + process.env.JWT_SECRET + ) + }); +}); + +module.exports = { + callback +}; diff --git a/controllers/getTalent.js b/controllers/profile.js similarity index 79% rename from controllers/getTalent.js rename to controllers/profile.js index 4f47233..a9c5b07 100644 --- a/controllers/getTalent.js +++ b/controllers/profile.js @@ -17,7 +17,8 @@ const binAndMerge = (getKey, records) => { return out; }; -module.exports = (req, res) => { +const getAll = (req, res) => { + console.log("Getting profiles", { session: req.session }); Promise.all([getAllPeople(), getAllProfiles()]) .then(([personList, profileList]) => { return { @@ -30,7 +31,10 @@ module.exports = (req, res) => { }) .then(({ people, profiles }) => { Object.keys(profiles).forEach(name => { - Object.assign(people[name].fields, profiles[name].fields); + people[name].fields; + if (people[name]) { + Object.assign(people[name].fields, profiles[name].fields); + } }); return Object.keys(people).map(name => people[name]); @@ -44,3 +48,7 @@ module.exports = (req, res) => { res.status(500).send("something is not working. Sorry :("); }); }; + +module.exports = { + getAll +}; diff --git a/index.js b/index.js index f9a9fd7..9a2114d 100644 --- a/index.js +++ b/index.js @@ -1,13 +1,20 @@ const express = require("express"); const morgan = require("morgan"); -// INIT require("env2")(".env"); // configure enviroment (in ./.env) +const sessionMiddleware = require("./middleware/session.js"); + +const githubController = require("./controllers/github.js"); +const profileController = require("./controllers/profile.js"); + +// INIT + const app = express(); // MIDDLEWARE app.use(morgan("tiny")); +app.use(sessionMiddleware); // CLIENT app.use(express.static("client/build")); @@ -15,11 +22,20 @@ app.use(express.static("client/build")); // API const apiRouter = new express.Router(); -apiRouter.get("/talent", require("./controllers/getTalent.js")); +apiRouter.get("/profile", profileController.getAll); +apiRouter.get("/github/callback", githubController.callback); app.use("/api", apiRouter); +// Error MIDDLEWARE +app.use(function(req, res, next) { + res.status(404); + + // default to plain-text. send() + res.type("txt").send("Not found"); +}); + // START -const port = process.env.PORT || 3000; +const port = process.env.PORT || 4000; app.listen(port, () => console.log(`Server is listening on port ${port}`)); diff --git a/lib/airtable.js b/lib/airtable.js index 8f1a08a..afd390c 100644 --- a/lib/airtable.js +++ b/lib/airtable.js @@ -5,6 +5,26 @@ const profilesTableName = "Talent Profiles"; const base = airtable.base(process.env.AIRTABLE_BASE_ID); +const userExists = (ghUsername, cb) => { + let count = 0; + return new Promise((resolve, reject) => { + base(profilesTableName) + .select({ + filterByFormula: "{Github Username}=ghUsername" + }) + .eachPage( + (pageRecords, getNextPage) => { + count += pageRecords.length; + }, + err => { + if (err) return reject(err); + + resolve(count >= 0); + } + ); + }); +}; + const getAll = (tableName, cb) => { const records = []; @@ -38,5 +58,6 @@ module.exports = { base, getAll, getAllPeople, - getAllProfiles + getAllProfiles, + userExists }; diff --git a/lib/github.js b/lib/github.js new file mode 100644 index 0000000..8e95ce8 --- /dev/null +++ b/lib/github.js @@ -0,0 +1,29 @@ +const request = require("request-promise-native"); + +const accessToken = async code => { + console.log({ code }); + return request.post({ + url: "https://github.com/login/oauth/access_token", + json: true, + body: { + client_id: process.env.GITHUB_CLIENT_ID, + client_secret: process.env.GITHUB_CLIENT_SECRET, + code: code + } + }); +}; + +const currentUser = async accessToken => + await request.get({ + url: "https://api.github.com/user", + json: true, + headers: { + Authorization: `token ${accessToken}`, + "User-Agent": "Gaza Sky Geeks Talent" + } + }); + +module.exports = { + accessToken, + currentUser +}; diff --git a/lib/wrap_async.js b/lib/wrap_async.js new file mode 100644 index 0000000..f6bcfb5 --- /dev/null +++ b/lib/wrap_async.js @@ -0,0 +1,3 @@ +module.exports = fn => (req, res, next) => { + Promise.resolve(fn(req, res, next)).catch(next); +}; diff --git a/middleware/session.js b/middleware/session.js new file mode 100644 index 0000000..7cb6462 --- /dev/null +++ b/middleware/session.js @@ -0,0 +1,25 @@ +const wrapAsync = require("../lib/wrap_async.js"); +const jwt = require("jsonwebtoken"); + +const sessionMiddleware = wrapAsync(async (req, res, next) => { + const authHeader = req.get("Authorization"); + + console.log(req.headers); + console.log("session middleware", { authHeader }); + if (authHeader) { + const token = authHeader.split(" ")[1]; + let tokenPayload; + try { + tokenPayload = jwt.verify(token, process.env.JWT_SECRET); + } catch (e) { + console.error(e); + } + + console.log({ tokenPayload }); + req.session = tokenPayload; + } + + next(); +}); + +module.exports = sessionMiddleware; diff --git a/package.json b/package.json index 7ec080d..aada70c 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,9 @@ "airtable": "^0.5.8", "env2": "^2.2.2", "express": "^4.16.4", - "morgan": "^1.9.1" + "jsonwebtoken": "^8.4.0", + "morgan": "^1.9.1", + "request-promise-native": "^1.0.5" }, "devDependencies": { "nodemon": "^1.18.7"