└── 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 |
--------------------------------------------------------------------------------