diff --git a/.gitignore b/.gitignore index ecc4832..2eebbde 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,28 @@ -################################################################################ -# This .gitignore file was automatically created by Microsoft(R) Visual Studio. -################################################################################ - -*/.vs -*/node_modules/.bin -*/node_modules -*/package-lock.json -*/.env \ No newline at end of file +# Dependencies +node_modules/ +package-lock.json + +# Environment variables +.env +server/.env + +# Logs +logs/ +*.log +npm-debug.log* + +# Runtime / OS +.DS_Store +Thumbs.db + +# IDE / Editor +.vs/ +.vscode/ +.idea/ + +# Test coverage +coverage/ +.nyc_output/ + +# Docker volumes +volumes/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 8bc22ee..bdbb565 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,6 @@ -FROM ubuntu +FROM node:bookworm-slim LABEL maintainer="Jeffery Russell" -# install all dependencies -RUN apt-get update && \ - apt-get upgrade -y && \ - apt-get install -y build-essential && \ - apt-get install -y sudo curl && \ - curl -sL https://deb.nodesource.com/setup_13.x | sudo -E bash - && \ - apt-get install -y nodejs && \ - apt-get update && \ - apt-get clean - # Create a working directory for the container RUN mkdir /github-graphs diff --git a/README.md b/README.md index 0dcefcd..0b22de4 100644 --- a/README.md +++ b/README.md @@ -22,26 +22,30 @@ If you are lucky, you can find the site live [here](https://github-graphs.com/). The easiest way to get started with Github-Graphs is to fork this repository and follow the instructions below. -#### Create a new OAuth app +#### Create a GitHub Personal Access Token (PAT) -The objective of creating an app under your github account is to access an -endpoint through the GitHub API and obtain credentials to set your environment -file. For more information on how to create a new OAuth app, consult the corresponding -Github developer documentation [here](https://developer.github.com/apps/building-oauth-apps/creating-an-oauth-app/). +Authentication is handled via a GitHub Personal Access Token (PAT). The old +OAuth App `client_id`/`client_secret` query parameter method is no longer used, +as GitHub deprecated unauthenticated and OAuth-param-based requests. + +To create a PAT: +1. Go to **GitHub → Settings → Developer settings → Personal access tokens** +2. Choose **Fine-grained tokens** (recommended) or **Tokens (classic)** +3. For a classic token, grant the `public_repo` and `read:user` scopes (read-only is sufficient) +4. Copy the generated token — it will only be shown once #### Create a .env file After forking this repository, run the command `cd server/`. Inside that folder, -setup your environment credentials by creating a `.env file` with the code below filled in. +setup your environment credentials by creating a `.env` file with the values below filled in. ``` -CLIENT_ID = -CLIENT_SECRET = +GITHUB_TOKEN = SESSION_SECRET = PORT = ``` -The creation of your OAuth app and the .env file are required steps, +The creation of your PAT and the `.env` file are required steps, irrespective of your desired deployment environment. For specific directions, continue by following the steps specified below. diff --git a/docker-compose.yml b/docker-compose.yml index d89633e..57a2bf1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,11 +1,10 @@ - -version: '3.7' - services: github-graphs: build: dockerfile: Dockerfile - image: graph-app:latest + image: graph-app container_name: github-graphs + env_file: + - .env ports: - - "8080:8000" + - "4002:4002" diff --git a/server/routes/api/v1.js b/server/routes/api/v1.js index dc024f9..d8c6be7 100644 --- a/server/routes/api/v1.js +++ b/server/routes/api/v1.js @@ -3,7 +3,6 @@ const got = require("got"); const cache = require('memory-cache'); const dotenv = require("dotenv").config(); const GITHUB_API = "https://api.github.com"; -const authenticate = `client_id=${process.env.CLIENT_ID}&client_secret=${process.env.CLIENT_SECRET}`; const API_FOLLOWING = "/following"; const API_FOLLOWERS = "/followers"; @@ -18,8 +17,10 @@ const REPOS_PATH = "/repos"; const got_options = { json: true, - username : process.env.CLIENT_ID, - password : process.env.CLIENT_SECRET + headers: { + 'Authorization': `Bearer ${process.env.GITHUB_TOKEN}`, + 'User-Agent': 'github-graphs-app' + } }; /** @@ -34,15 +35,7 @@ function queryGithubAPIRaw(requestURL) { return new Promise((resolve, reject)=> { - var queryURL; - if(requestURL.includes("?page=")) - { - queryURL = GITHUB_API + requestURL + "&" + authenticate; - } - else - { - queryURL = GITHUB_API + requestURL + "?" + authenticate; - } + var queryURL = GITHUB_API + requestURL; console.log(queryURL); got(queryURL, got_options).then(response => diff --git a/server/routes/api/v2.js b/server/routes/api/v2.js deleted file mode 100644 index b71a463..0000000 --- a/server/routes/api/v2.js +++ /dev/null @@ -1,310 +0,0 @@ -const routes = require('express').Router(); -const got = require("got"); -const cache = require('memory-cache'); -const dotenv = require("dotenv").config(); -const GITHUB_API = "https://api.github.com"; -const authenticate = `client_id=${process.env.CLIENT_ID}&client_secret=${process.env.CLIENT_SECRET}`; - -const API_FOLLOWING = "/following"; -const API_FOLLOWERS = "/followers"; -const API_USER_PATH = "/users/"; -const API_ORGS_PATH = "/orgs/"; -const API_PAGINATION_SIZE = 100; // 100 is the max, 30 is the default -// if this is too large, it would be infeasible to make graphs for people following popular people -const API_MAX_PAGES = 2; -const API_PAGINATION = "&per_page=" + API_PAGINATION_SIZE; - -const REPOS_PATH = "/repos"; - - -/** - * Queries data from the github APi server and returns it as - * a json object in a promise. - * - * This makes no attempt to cache - * - * @param {*} requestURL endpoint on githubapi: ex: /users/jrtechs/following - */ -const queryGithubAPIRaw = async requestURL => { - let queryURL = requestURL.includes("?page=") ? `${GITHUB_API}${requestURL}&${authenticate}` :`${GITHUB_API}${requestURL}?${authenticate}`; - console.log(queryURL); - try { - const req = await got(queryURL, { json: true }); - cache.put(requestURL, req); - return req; - } catch (error) { - console.log(error); - cache.put(requestURL, `${error.statusCode} - ${error.statusMessage}`); - } -} - - -/** - * Queries data from the github api server - * and caches the results locally. - * - * @param {*} requestURL - */ -const queryGitHubAPI = async requestURL => { - const apiData = cache.get(requestURL); - if (apiData) { - console.log("Fetched From Cache"); - return apiData - } - - try { - return await queryGithubAPIRaw(requestURL); - } catch (error) { - console.log(error); - } -} - - -/** - * Fetches all content from a particular github api endpoint - * using their pagination schema. - * - * @param {*} username username of github client - * @param {*} apiPath following or followers - * @param {*} page current pagination page - * @param {*} lst list we are building on - */ -const fetchAllWithPagination = async (apiPath, page, lst) => { - try { - const req = await queryGithubAPIRaw(`${apiPath}?page=${page}${API_PAGINATION}`); - if (req.body.hasOwnProperty("length")) { - const list = lst.concat(req.body); - if(page < API_MAX_PAGES && req.length === API_PAGINATION_SIZE) { - const redo = await fetchAllWithPagination(apiPath, page + 1, list); - return redo; - } - return list; - } - } catch (error) { - console.log("Error with api request"); - } -} - - -/** - * Makes a copy of a JS object with certain properties - * - * @param {*} props - * @param {*} obj - */ -const copyWithProperties = (props, obj) => { - let newO = new Object(); - props.forEach(prop => { - newO[prop] = obj[prop]; - }) - return newO; -} - - -/** - * Combines the list of friends and followers ignoring duplicates - * that are already in the list. (person is both following and followed by someone) - * - * This also removes any unused properties like events_url and organizations_url - * - * @param {*} followingAndFollowers - */ -const minimizeFriends = people => { - let friendLst = []; - let ids = new Set(); - people.forEach(person => { - if(!ids.has(person.id)) { - ids.add(person.id); - friendLst.push({ - login: person.login, - avatar_url: person.avatar_url - }); - } - }); - return friendLst; -} - - -/** - * Fetches all the people that are either following or is followed - * by a person on github. This will cache the results to make simultaneous - * connections easier and less demanding on the github API. - * - * @param {*} user - */ -const queryFriends = async user => { - const cacheHit = cache.get("/friends/" + user); - if (cacheHit){ - console.log("Friends cache hit"); - return cacheHit; - } - - try { - const followers = await fetchAllWithPagination(API_USER_PATH + user + API_FOLLOWERS, 1, []); - const following = await fetchAllWithPagination(API_USER_PATH + user + API_FOLLOWING, 1, []); - const fList = minimizeFriends(following.concat(followers)); - cache.put(`/friends/${user}`, fList); - return fList; - } catch (error) { - console.log("API Error", err); - } -} - - -/** - * - * Fetches all of the members of an organization from the - * API or cache - * - * /orgs/RITlug/members?page=1 - * - * @param {*} orgName - */ -const getOrganizationMembers = async orgName => { - const cacheHit = cache.get("/org/users/" + orgName); - if (cacheHit){ - console.log("Org members cache hit"); - return cacheHit; - } - - try { - const members = await fetchAllWithPagination(API_ORGS_PATH + orgName + "/members", 1, []); - const membersMin = minimizeFriends(members); - cache.put("/org/users/" + orgName, membersMin); - return membersMin; - } catch (error) { - console.log(error); - } -} - - -/** - * Minimizes the JSON for a list of repositories - * - * @param {*} repositories - */ -const minimizeRepositories = repositories => { - let rList = []; - repositories.forEach(repo => { - rList.push(copyWithProperties(["name", "created_at", "homepage", - "description", "language", "forks", "watchers", - "open_issues_count", "license", "html_url"], - repo)); - }) - - return rList; -} - - -/** - * Fetches all repositories from the API - * - * @param {*} user name of org/user - * @param {*} orgsOrUsers either /users/ or /orgs/ - */ -const queryRepositories = async (user, orgsOrUsers) => { - const cacheHit = cache.get(user + REPOS_PATH); - if (cacheHit) { - console.log("Repositories cache hit"); - return cacheHit; - } - - try { - const repos = await fetchAllWithPagination(orgsOrUsers + user + REPOS_PATH, 1, []); - const minRepos = minimizeRepositories(repos); - cache.put(`${user}${REPOS_PATH}`, minRepos); - return minRepos; - } catch (error) { - console.log(error) - console.log("bad things went down"); - } -} - - -/** - * /users/name/following/followers - */ -routes.get("/friends/:name", async (req, res)=> { - try { - const query = await queryFriends(req.params.name); - res.json(query); - } catch (error) { - res.status(500).json({error: 'API error fetching friends'}); - } -}); - - -routes.get("/org/users/:name", async (request, res) => { - try { - const orgMembers = await getOrganizationMembers(request.params.name); - res.json(orgMembers).end(); - } catch (error) { - res.status(500).json({error: 'API error fetching friends'}).end(); - } -}); - - -routes.get("/repositories/:name", async (req, res) => { - try { - const repos = await queryRepositories(req.params.name, API_USER_PATH); - res.json(repos).end(); - } catch (error) { - res.status(500).json({error: 'API error fetching friends'}).end(); - } -}); - - -routes.get("/org/repositories/:name", async (req, res) => { - try { - const repos = await queryRepositories(req.params.name, API_ORGS_PATH); - res.json(repos).end(); - } catch (error) { - res.status(500).json({error: 'API error fetching friends'}).end(); - } -}); - - -routes.get('/*', (request, result) => -{ - var gitHubAPIURL = request.url; - - result.setHeader('Content-Type', 'application/json'); - queryGitHubAPI(gitHubAPIURL).then((data)=> - { - if(data.hasOwnProperty("id") || data[0].hasOwnProperty("id")) - { - result.write(JSON.stringify(data)); - } - else - { - result.write("[]"); - } - result.end(); - }).catch((error)=> - { - try - { - if(error.hasOwnProperty("id") || error[0].hasOwnProperty("id")) - { - result.write(JSON.stringify(error)); - } - else - { - result.write("[]"); - } - - } - catch(error) - { - result.write("[]"); - }; - result.end(); - }); - - if(cache.size() > 50000) - { - cache.clear(); - } -}); - -module.exports = routes; diff --git a/server/routes/test/v2.js b/server/routes/test/v2.js deleted file mode 100644 index 1a940c9..0000000 --- a/server/routes/test/v2.js +++ /dev/null @@ -1,11 +0,0 @@ -const assert = require('assert'); -const V2 = require('../api/v2'); - -describe('github api v2', function() { - it('successfully queries friends', async function() { - var queryFriends = V2.queryFriends; - // it was this point that Eric realized he doesn't understand - // module.exports very well. - assert.strictEqual(typeof(queryFriends), typeof(queryFriends)); - }); -}); \ No newline at end of file diff --git a/server/server.js b/server/server.js index f80ec48..c927952 100644 --- a/server/server.js +++ b/server/server.js @@ -1,11 +1,9 @@ const crypto = require('crypto'); - -const app = express(); - const dotenv = require("dotenv").config(); const express = require("express"); const session = require('express-session'); +const app = express(); const sessionProperties = { secret: process.env.SESSION_SECRET || crypto.randomBytes(64), @@ -22,4 +20,4 @@ const routes = require('./routes'); app.use('/', routes); -app.listen(process.env.PORT || 8100, () => console.log(`App listening on port ${process.env.PORT || 8100}!`)); \ No newline at end of file +app.listen(process.env.PORT || 4002, () => console.log(`App listening on port ${process.env.PORT || 4002}!`)); \ No newline at end of file