└── README.md /README.md: -------------------------------------------------------------------------------- 1 | # PG6301 Web Development and API design 2 | 3 | Welcome to this course in Web Development and API Design. In this course, we will 4 | look at creating single-page applications with React backed by APIs implemented 5 | with React. 6 | 7 | The first lectures of this course (as of 2021) are documented on 8 | [Andrea's](https://github.com/arcuri82/web_development_and_api_design) Github 9 | page for the course. Here, you will find slides and exercises. 10 | 11 | For lecture 7-12, the current Github repository contains the code that was 12 | presented during the lectures. Each lecture contains slides (from Andrea), 13 | a commit log for the live coding demonstrated during the lecture, a 14 | reference implementation of the live code objective and the Github issues 15 | resolved during the lecture. 16 | 17 | ### Lecture 7: Creating a REST-ful API with Express 18 | 19 | The lecture covers the "book application" and introduced React Hooks and Parcel 20 | 21 | * [Slides](https://github.com/arcuri82/web_development_and_api_design/blob/master/docs/slides/lesson_07.pdf) 22 | * [Commit log from live coding](https://github.com/kristiania-pg6301-2021/pg6301-react-and-express-lectures/commits/lectures/07) 23 | * [Reference implementation](https://github.com/kristiania-pg6301-2021/pg6301-react-and-express-lectures/tree/reference/07) 24 | 25 | ### Lecture 8: REST-ful APIs, part 2 26 | 27 | The lecture continued the "book application" and repeated testing with modern React 28 | 29 | * [Slides](https://github.com/arcuri82/web_development_and_api_design/blob/master/docs/slides/lesson_08.pdf) 30 | * [Commit log from live coding](https://github.com/kristiania-pg6301-2021/pg6301-react-and-express-lectures/commits/lectures/08) 31 | * [Reference implementation](https://github.com/kristiania-pg6301-2021/pg6301-react-and-express-lectures/tree/reference/08) 32 | * [Issues resolved](https://github.com/kristiania-pg6301-2021/pg6301-react-and-express-lectures/milestone/1?closed=1) 33 | 34 | ### Lecture 9: Sessions, cookies and login 35 | 36 | The lecture starts a new minimal React + Express application and implements https, cookies and sessions 37 | 38 | * [Slides](https://github.com/arcuri82/web_development_and_api_design/blob/master/docs/slides/lesson_09.pdf) 39 | * [Commit log from live coding](https://github.com/kristiania-pg6301-2021/pg6301-react-and-express-lectures/commits/lectures/09) 40 | * [Reference implementation](https://github.com/kristiania-pg6301-2021/pg6301-react-and-express-lectures/tree/reference/09) 41 | * [Issues resolved](https://github.com/kristiania-pg6301-2021/pg6301-react-and-express-lectures/milestone/2?closed=1) 42 | 43 | ### Lecture 10: Passport, OpenID Connect and login with Google 44 | 45 | The lecture uses Passport to login with password and with Google and also shows how to implement OpenID 46 | Connect "manually" in the front-end. We also covered Cross Origin Resource Sharing (CORS) to access 47 | an API on another host/port than the client. 48 | 49 | * [Slides](https://github.com/arcuri82/web_development_and_api_design/blob/master/docs/slides/lesson_10.pdf) 50 | * [Commit log from live coding](https://github.com/kristiania-pg6301-2021/pg6301-react-and-express-lectures/commits/lectures/10) 51 | * [Commit log from live exercise rehearsal](https://github.com/kristiania-pg6301-2021/pg6301-react-and-express-lectures/tree/exercise/10.2) 52 | * [Reference implementation](https://github.com/kristiania-pg6301-2021/pg6301-react-and-express-lectures/tree/reference/10) 53 | * [Issues resolved](https://github.com/kristiania-pg6301-2021/pg6301-react-and-express-lectures/milestone/3?closed=1) 54 | 55 | ### Lecture 11: Effective testing 56 | 57 | In this lecture, we cover testing that React components render correctly, that 58 | button clicks and inputs have the desired effect and that Express responds 59 | correctly to API calls. 60 | 61 | The lecture continues with the code from [lecture 8](https://github.com/kristiania-pg6301-2021/pg6301-react-and-express-lectures/tree/lectures/08) 62 | 63 | * [Issues resolved](https://github.com/kristiania-pg6301-2021/pg6301-react-and-express-lectures/milestone/4) 64 | * [Commit log from live coding](https://github.com/kristiania-pg6301-2021/pg6301-react-and-express-lectures/commits/lectures/11) 65 | * [Reference implementation](https://github.com/kristiania-pg6301-2021/pg6301-react-and-express-lectures/tree/reference/11) 66 | * [Commit log from live exercise rehearsal](https://github.com/kristiania-pg6301-2021/pg6301-react-and-express-lectures/tree/exercise/11) 67 | 68 | ### Lecture 12: Web Sockets 69 | 70 | In this lecture, we cover more real-time communication between server and clients using WebSockets. We will also revisit testing of the client in the context of this application. 71 | 72 | This lecture starts with a new React + Express application 73 | 74 | * [Issues resolved](https://github.com/kristiania-pg6301-2021/pg6301-react-and-express-lectures/milestone/5) 75 | * [Commit log from live coding](https://github.com/kristiania-pg6301-2021/pg6301-react-and-express-lectures/commits/lectures/12) 76 | * [Reference implementation](https://github.com/kristiania-pg6301-2021/pg6301-react-and-express-lectures/tree/reference/12) 77 | 78 | ### Bonus lecture: Active Directory 79 | 80 | * [Commit log from live coding](https://github.com/kristiania-pg6301-2021/pg6301-react-and-express-lectures/commits/lectures/13) 81 | * [Reference implementation from lecture 10](https://github.com/kristiania-pg6301-2021/pg6301-react-and-express-lectures/tree/reference/10) 82 | 83 | ## Reference material 84 | 85 | ### Creating a React Application with Express backend 86 | 87 | 1. `npm init -y` 88 | 2. `npm install -D parcel@next nodemon concurrently prettier` 89 | 3. `npm install -P react react-dom react-router react-router-dom express` 90 | 4. Add the following "scripts" in `package.json`: 91 | * `"start": "concurrently npm:server npm:client"` 92 | * `"client": "parcel src/client/index.html"` 93 | * `"server": "nodemon --watch src/server src/server/server.js"` 94 | 5. Create a minimal HTML file as `src/client/index.html`. This is the essence: 95 | * `
` 96 | 6. Create a minimal `src/client/index.jsx`. In addition to importing React and ReactDOM, this is the essence: 97 | * `ReactDOM.render(

Hello World

, document.getElementById("app"));` 98 | 7. Create a minimal `src/server/server.js`: 99 | * `const express = require("express");` 100 | * `const app = express();` 101 | * `express.listen(3000);` 102 | 8. Start everything with `npm start` 103 | 9. Add `.idea` (if appropriate), `node_modules`, `dist` and `.parcel-cache` to `.gitignore`. 104 | 105 | ### Crucial tasks 106 | 107 | When you can get this to work, you will need to master the following: 108 | 109 | * Serve the frontend code from Express. In `server.js`: 110 | * `app.use(express.static(path.resolve(__dir, "..", "..", "dist")));` 111 | * To get express router to function correctly, make Express return index.html on unmatched files: 112 | * `app.use((req, res, next) => res.sendFile(path.resolve(__dir, "..", "..", "dist", "index.html")));` 113 | * Use React Router in front-end 114 | * Make React call API calls on the backend (using `fetch`) 115 | * Make Express respond to API calls 116 | 117 | ### Testing 118 | 119 | ### Installing 120 | 121 | When using test, we need to add some babel mumbo jumbo to get Jest to understand modern JavaScript syntax as well as JSX tags 122 | 123 | 1. `npm install -D jest babel-jest` 124 | 125 | You need the following fragment or similar in `package.json`: 126 | 127 | ``` 128 | "jest": { 129 | "transform": { 130 | "\\.jsx": "babel-jest" 131 | } 132 | }, 133 | "babel": { 134 | "presets": [ 135 | "@babel/preset-env", 136 | "@babel/preset-react" 137 | ] 138 | }, 139 | "browserslist": [ 140 | "last 1 Chrome version" 141 | ] 142 | ``` 143 | 144 | The `jest`-section tells jest to use babel to transform `jsx`-files, the `babel`-section tells babel to use the browserlist ("preset-env") and react to transform files and `browserlist` tells babel to target the newest version of Chrome. 145 | 146 | With this in place, it should be possible to run tests like those below. 147 | 148 | #### Snapshot testing - check that a view is rendered correctly 149 | 150 | ```javascript 151 | it("loads book", async () => { 152 | // Fake data instead of calling the real backend 153 | const getBook = () => ({ 154 | title: "My Book", 155 | author: "Firstname Lastname", 156 | year: 1999, 157 | }); 158 | // Construct an artification dom element to display the app (with jsdom) 159 | const container = document.createElement("div"); 160 | // The act method from react-dom/test-utils ensures that promises are resolved 161 | // - that is, we wait until the `getBook` function returns a value 162 | await act(async () => { 163 | await ReactDOM.render( 164 | 165 | 166 | 167 | 170 | 171 | 172 | , 173 | container 174 | ); 175 | }); 176 | // Snapshot tests fail if the page is changed in any way - intentionally or non-intentionally 177 | expect(container.innerHTML).toMatchSnapshot(); 178 | // querySelector can be used to find dom elements in order to make assertions 179 | expect(container.querySelector("h1").textContent).toEqual("Edit book: My Book") 180 | }); 181 | ``` 182 | 183 | #### Simulate events 184 | 185 | ```javascript 186 | it("updates book on submit", async () => { 187 | const getBook = () => ({ 188 | title: "My Book", 189 | author: "Firstname Lastname", 190 | year: 1999, 191 | }); 192 | // We create a mock function. Instead of having functionality, 193 | // this fake implementation of updateBook() lets us record and 194 | // make assertions about the calls to the function 195 | const updateBook = jest.fn(); 196 | const container = document.createElement("div"); 197 | await act(async () => { 198 | await ReactDOM.render( 199 | 200 | 201 | 202 | 203 | , 204 | container 205 | ); 206 | }); 207 | 208 | // The simulate function lets us create artificatial events, such as 209 | // a change event (which will trigger the `onChange` handler of our 210 | // component 211 | Simulate.change(container.querySelector("input"), { 212 | // The object we pass must work with e.target.value in the event handler 213 | target: { 214 | value: "New Value", 215 | }, 216 | }); 217 | Simulate.submit(container.querySelector("form")); 218 | // We check that the call to `updateBook` is as expected 219 | // The value "12" is from MemoryRouter intialEntries 220 | expect(updateBook).toHaveBeenCalledWith("12", { 221 | title: "New Value", 222 | author: "Firstname Lastname", 223 | year: 1999, 224 | }); 225 | }); 226 | ``` 227 | 228 | #### Using supertest to check server side behavior 229 | 230 | ```javascript 231 | const request = require("supertest"); 232 | const express = require("express"); 233 | 234 | const app = express(); 235 | app.use(require("body-parser").json()); 236 | app.use(require("../src/server/booksApi")); 237 | 238 | describe("...", () => { 239 | 240 | it("can update existing books", async () => { 241 | const book = (await request(app).get("/2")).body; 242 | const updated = { 243 | ...book, 244 | author: "Egner", 245 | }; 246 | await request(app).put("/2").send(updated).expect(200); 247 | await request(app) 248 | .get("/2") 249 | .then((response) => { 250 | expect(response.body).toMatchObject({ 251 | id: 2, 252 | author: "Egner", 253 | }); 254 | }); 255 | }); 256 | 257 | }); 258 | ``` 259 | 260 | ## WebSockets 261 | 262 | ### Client side: 263 | 264 | ```javascript 265 | // Connect to ws on the same host as we got the frontend 266 | const ws = new WebSocket("ws://" + window.location.host); 267 | // log out the message and destructor the contents when we receive it 268 | ws.onmessage = (msg) => { 269 | console.log(msg); 270 | const { username, message, id } = JSON.parse(msg.data); 271 | }; 272 | // send a new message 273 | ws.send(JSON.stringify({username: "Myself", message: "Hello"})); 274 | ``` 275 | 276 | ### Server side 277 | 278 | ```javascript 279 | 280 | // Create a websocket server 281 | const wsServer = new ws.Server({ noServer: true }); 282 | 283 | // Keep a list of all incomings connections 284 | const sockets = []; 285 | let messageIndex = 0; 286 | wsServer.on("connection", (socket) => { 287 | // Add this connection to the list of connections 288 | sockets.push(socket); 289 | // Set up the handling of messages from this sockets 290 | socket.on("message", (msg) => { 291 | // Destructor the incoming message 292 | const { username, message } = JSON.parse(msg); 293 | // Add fields from server side 294 | const id = messageIndex++; 295 | // broadcast a new message to all recipients 296 | for (const recipient of sockets) { 297 | recipient.send(JSON.stringify({ id, username, message })); 298 | } 299 | }); 300 | }); 301 | 302 | // Start express app 303 | const server = app.listen(3000, () => { 304 | // Handle incoming clients 305 | server.on("upgrade", (req, socket, head) => { 306 | wsServer.handleUpgrade(req, socket, head, (socket) => { 307 | // This will pass control to `wsServer.on("connection")` 308 | wsServer.emit("connection", socket, req); 309 | }); 310 | }); 311 | }); 312 | ``` 313 | 314 | ## OpenID Connect - Log on with Google 315 | 316 | ### Client side (implicit flow) 317 | 318 | "Implicit flow" means that the login provider (Google) will not require a client secret to complete the authentication. This is often not recommended, and for example Active Directory instead uses another mechanism called PKCE, which protects against some security risks. 319 | 320 | 1. Set up the application in [Google Cloud Console](https://console.cloud.google.com/apis/credentials). Create a new OAuth client ID and select Web Application. Make sure `http://localhost:3000` is added as an Authorized JavaScript origin and `http://localhost:3000/callback` is an authorized redirect URI 321 | 2. To start authentication, redirect the browser (see code below) 322 | 3. To complete the authentication, pick up the `access_token` when Google redirects the browser back (see code below) 323 | 4. Save the `access_token` (e.g. in `localStorage`) and add as a header to all requests to backend 324 | 325 | #### Redirect the client to authenticate 326 | 327 | ```javascript 328 | export function Login() { 329 | async function handleStartLogin() { 330 | // Get the location of endpoints from Google 331 | const { authorization_endpoint } = await fetchJson( 332 | "https://accounts.google.com/.well-known/openid-configuration" 333 | ); 334 | // Tell Google how to authentication 335 | const query = new URLSearchParams({ 336 | response_type: "token", 337 | scope: "openid profile email", 338 | client_id: 339 | "", 340 | // Tell user to come back to http://localhost:3000/callback when logged in 341 | redirect_uri: window.location.origin + "/callback", 342 | }); 343 | // Redirect the browser to log in 344 | window.location.href = authorization_endpoint + "?" + query; 345 | } 346 | 347 | return ; 348 | } 349 | ``` 350 | 351 | In the case of Active Directory, you also need parameters `response_type: "code"`, `response_mode: "fragment"`, `code_challenge_method` and `code_challenge` (the latest two are needed for PKCE). 352 | 353 | #### Handle the authentication callback 354 | 355 | ```javascript 356 | 357 | // Router should take user here on /callback 358 | export function CompleteLoginPage({onComplete}) { 359 | // Given an URL like http://localhost:3000/callback#access_token=sdlgnsoln&foo=bar, 360 | // window.location.hash will give the part starting with "#" 361 | // ...substring(1) will remove the "#" 362 | // and Object.fromEntries(new URLSearchParams(...)) will parse it into an object 363 | // In this case, hash = { access_token: "sdlgnsoln", foo: "bar" } 364 | const hash = Object.fromEntries( 365 | new URLSearchParams(window.location.hash.substr(1)) 366 | ); 367 | // Get the values returned from the login provider. For Active Directory, 368 | // this will be more complex 369 | const { access_token, error } = hash; 370 | useEffect(() => { 371 | // Send the access token back to the outside application. This should 372 | // be saved to localStorage and then redirect the user 373 | onComplete({access_token}); 374 | }, [access_token]); 375 | 376 | if (error) { 377 | // deal with the user failing to log in or to give consent with Google 378 | } 379 | 380 | return
Completing loging...
; 381 | } 382 | ``` 383 | 384 | For Active Directory, the hash will instead include a `code`, which you will then need to send to the `token_endpoint` along with the `client_id` and `redirect_uri` as well as `grant_type: "authorization_code"` and the `code_verifier` value from PKCE. This call will return the `access_token`. 385 | 386 | #### Handle access_token on the backend 387 | 388 | ```javascript 389 | 390 | app.use(async (req, res, next) => { 391 | const authorization = req.header("Authorization"); 392 | if (authorization) { 393 | const { userinfo_endpoint } = await fetchJSON( 394 | "https://accounts.google.com/.well-known/openid-configuration" 395 | ); 396 | req.userinfo = await fetchJSON(userinfo_endpoint, { 397 | headers: { authorization }, 398 | }); 399 | } 400 | next(); 401 | }); 402 | 403 | app.get("/profile", (req, res) => { 404 | if (!req.userinfo) { 405 | return res.send(200); 406 | } 407 | }); 408 | 409 | ``` 410 | --------------------------------------------------------------------------------