Most server code is clearly modularized and heavily (perhaps excessively) annotated for the benefit of teams wishing to inherit this codebase. On the understanding that comments can become inaccurate over time and that people may want to understand the what module does what before jumping in, we're providing short notes here. We prefer each file to fulfill one role so modules with diverse responsibilities (e.g. the routes module, which exports request handlers for /API/ and /auth/ routes) will generally have one file for each responsibility and a short index.js that draws together exports functionality from the module's distinct files. This modularization was intended to be clear, not perfect. You may wish to reorganize some code; for instance, the distinction between request-handler and routes is a little confusing and perhaps artificial. Note: We've aimed to use the names of React components in this documentation, which will hopefully make it easier to cross-reference with the front-end code. Components are marked by JSX style tags (e.g. component).
- Authentication Initialises and exports middleware for authentication;
- DB Initialises and exports DB connection;
- Request Handler Invokes appropriate request handler and sends resulting data;
- Routes Exports React routes information and request handlers for API and authentication routes;
The entry point for all backend code is /server.js, which is a very short file. It simply creates an express app, starts its listening, adds necessary middleware (e.g. for handling sessions and body parsing) and then defers to request-handler for all handling of requests.
Request handler follows very simple steps:
- Firstly, it checks whether or not the request is for a valid React route (stored in a Set in the routes module) and if so simply responds with index.html (stored in memory and exported from the request-handler module for a quicker, simpler response);
- Then, it checks whether the request is for an /API/ route and if so, invokes the correct request-handler from the API object exported from the routes module if the user is authenticated and responds with the resulting data or sends a 403 and empty array if not;
- Then, it checks whether the request is for an /auth/ route and defers to the auth object exports from the routes module as above, but sending the response object as well, so that authentication functions can respond directly;
- Finally, it sends a 404 response with index.html, so that React can render a component.
This depends heavily on the nature of the request, so we've provided two examples:
- A new user logging in for the first time;
- An authenticated user, making a GET request to /API/users/ when viewing a page.
When a new user clicks on 'Sign in with GitHub' on the component with which all users are greeted the following things happen:
- The user is redirected to GitHub's OAuth page and (after successful authorisation) punted back to /auth/github/callback.
- Their req hits server.js and is handed on (with a request and a response object) to the handler function exported from the request-handler module.
- This functions tests in the order above and finds that the user has requested an /auth/ route, checks whether or not the object exported by /server/routes/auth.js/ has a github function and invokes it with the req and res objects.
- The github function verifies that this is an OAuth callback and invokes the appropriate middleware exported from the authentication module.
- This middleware (exports.callback) mostly defers to other passport functionality, redirecting back to / on failure and /projects on successful authentication, so at this point the req is handled and res (a redirect) has been sent.
- Passport provides a set of useful functions, which are invoked after (I think) the req is authenticated and which we use to handle our knowledge of the user.
- Firstly, it provides a callback function (used to initialise the passport object), which is passed the authenticated user's OAuth token and profile from the OAuth provider. The function we provide inserts the user into the database (if not already present) and ensures that their information matches the latest from GitHub. If they are a new user. it then begins the process of scraping information from GitHub to profile their experience and compare to other users.
- Passport also provides serializeUser which is passed the user's whole profile from the OAuth provider (here, a whole load of info from GitHub) and whose return value will be stored on the user's session in the internal (deliberately flimsy) store. We use this opportunity to extract the information we want and store it in the layout we want and ignore the rest.
- As this is a new user, their GitHub ID is passed into a chain of promises from the (poorly name) profiling module, which:
- Scrapes all of their repos from GitHub's API and returns those repos which they created or have committed to;
- Checks the how many KBs of which languages have been written in those repos and returns that on the user's experience profile;
- Tots up the basic stats from their repos (stars, watchers, forks) and returns that on the user's experience profile;
- Stores it that information on the user's node in the db;
- Finally, the user's GitHub ID is passed to the compareUser function, which grabs their experience profile and finds the distance between it and every other user's, so that /API/user results can be returned with nearest neighbours first.
When an authenticated user makes a GET request to /API/users/, the following things happen:
- Their req hits server.js and the handler function from the request-handler module is invoked as middleware.
- Handler runs through the checklist of possible requests, finds that this is an /API request, checks that the API object exported from the routes module has a request-handler for this path and invokes it with the req object.
- The request handler for /API/users instantiates a database session, gets the user's GitHub ID from the user's request (info added by express-session from its memory store) and gets the project ID from the req object's URL parameters.
- It then queries the DB, combining the results from two queries:
- The first returns users with whom the requesting user is paired, along with the IDs of projects on which they're working and the distance between their experience and the requesting user's;
- The second returns the same information from users with whom the requesting user is not paired.
- The results are then map()ed, parsing each returned record with a DB model (a set of pseudoclassical constructors that imply a schema--post hoc--onto DB results) that returns a consistent User object. The resulting array is sorted by experience (the rating property of users at this point represent the distance between their experience-smaller numbers mean more similar experience) and resolve()d.
- The request-handler which invoked this function has a .then which receives the resolve()d array, and sends it back to the user as JSON with an OK status code, using the res object's methods.