ECE366 - Lesson 8

More React and Authentication

Instructor: Professor Hong

Review & Questions

## React and Docker
## Adding React to Docker Compose Dockerfile ``` # pull official base image FROM node:20 # set working directory WORKDIR /app # add `/app/node_modules/.bin` to $PATH ENV PATH /app/node_modules/.bin:$PATH # install app dependencies COPY package.json ./ COPY package-lock.json ./ RUN npm install --silent RUN npm install react-scripts@3.4.1 -g --silent # add app COPY . ./ # start app CMD npm start ```
## Docker-Compose docker-compose.yaml ``` services: ui: build: . ports: - 3000:3000 ```
## More React
## React Forms useRef - a hook that is going to reach out to some UI element and get its value, doesn't re-render; synchronous ``` import "./App.css"; import { useRef } from "react"; function App() { const txtTitle = useRef(); const hexColor = useRef(); const submit = (e) => { e.preventDefault(); const title = txtTitle.current.value; const color = hexColor.current.value; alert(`${title}, ${color}`); txtTitle.current.value = ""; hexColor.current.value = ""; }; return ( <form onSubmit={submit}> <input ref={txtTitle} type="text" placeholder="color title..." /> <input ref={hexColor} type="color" /> <button>ADD</button> </form> ); } export default App; ```
## React Forms with useState useState - managing component state, re-renders at the end, asynchronous ``` import "./App.css"; import { useState } from "react"; function App() { const [title, setTitle] = useState(""); const [color, setColor] = useState("#000000"); const submit = (e) => { e.preventDefault(); alert(`${title}, ${color}`); setTitle(""); setColor("#000000"); }; return ( <form onSubmit={submit}> <input value={title} onChange={(event) => setTitle(event.target.value) } type="text" placeholder="color title..." /> <input value={color} type="color" onChange={(event) => setColor(event.target.value) } /> <button>ADD</button> </form> ); } export default App; ```
## Form Libraries - formik - react hook forms
## React Router To install: ``` npm install --save react-router-dom ```
## Configuring the Router App.js ``` import "./App.css"; function Home() { return ( <div> <h1>My Website</h1> </div> ); } export function About() { return ( <div> <h1>About Us</h1> </div> ); } export function Contact() { return ( <div> <h1>Contact Us</h1> </div> ); } export function App() { return <Home />; } ```
## Configuring the Router index.js ``` import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import { App, About, Contact } from "./App"; import { BrowserRouter, Routes, Route } from "react-router-dom"; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <BrowserRouter> <Routes> <Route path="/" element={<App />} /> <Route path="/about" element={<About />} /> <Route path="/contact" element={<Contact />} /> </Routes> </BrowserRouter>, ); ```
## Adding Links to the Router App.js ``` import "./App.css"; import { Link } from "react-router-dom"; function Home() { return ( <div> <nav> <Link to="/">Home</Link> <Link to="/about">About</Link> <Link to="/contact">Contact</Link> </nav> <h1>My Website</h1> </div> ); } export function About() { return ( <div> <nav> <Link to="/">Home</Link> <Link to="/about">About</Link> <Link to="/contact">Contact</Link> </nav> <h1>About Us</h1> </div> ); } export function Contact() { return ( <div> <nav> <Link to="/">Home</Link> <Link to="/about">About</Link> <Link to="/contact">Contact</Link> </nav> <h1>Contact Us</h1> </div> ); } export function App() { return <Home />; } ```
## Axios
## What is Axios? Installation ``` npm install --save axios ``` - Another HTTP Client
## App.js with fetch ``` import "./App.css"; import { Link } from "react-router-dom"; import { useState, useEffect } from "react"; function Home() { const [data, setData] = useState(null); useEffect(() => { fetch( `http://localhost:8080/api/getUsers` ) .then((response) => response.json()) .then(setData); }, []); if (data) return ( <> <h1>RPS</h1> <nav> <Link to="/about">About</Link> <Link to="/contact">Contact</Link> </nav> <pre>{JSON.stringify(data, null, 2)}</pre> </> ); return <h1>Data</h1>; } export function About() { return ( <div> <nav> <Link to="/">Home</Link> <Link to="/about">About</Link> <Link to="/contact">Contact</Link> </nav> <h1>About Us</h1> </div> ); } export function Contact() { return ( <div> <nav> <Link to="/">Home</Link> <Link to="/about">About</Link> <Link to="/contact">Contact</Link> </nav> <h1>Contact Us</h1> </div> ); } export function App() { return <Home />; } ```
## App.js with Axios ``` function Home() { const [data, setData] = useState(null); useEffect(() => { const loadUsers = async () => { const response = await axios.get(`http://localhost:8080/api/getUsers`); setData(response.data) } loadUsers(); }, []); if (data) return ( <> <h1>RPS</h1> <nav> <Link to="/about">About</Link> <Link to="/contact">Contact</Link> </nav> <pre>{JSON.stringify(data, null, 2)}</pre> </> ); return <h1>Data</h1>; } ```
## Authentication
## Firebase Installation ``` npm install --save firebase ```
## Setup Firebase - Go to [https://console.firebase.google.com/](https://console.firebase.google.com/) - Create a new project - No need to select Google analytics - Add firebase to your web app by clicking Web - Insert public keys to index.js - Get Started w/ Authentication - Enable Email/Password and save - Add a sample user
## Firebase Configuration index.js ``` import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import { App, About, Contact } from "./App"; import { BrowserRouter, Routes, Route } from "react-router-dom"; // Import the functions you need from the SDKs you need import { initializeApp } from "firebase/app"; import LoginPage from "./pages/LoginPage"; import CreateAccountPage from "./pages/CreateAccountPage"; // TODO: Add SDKs for Firebase products that you want to use // https://firebase.google.com/docs/web/setup#available-libraries // Your web app's Firebase configuration const firebaseConfig = { apiKey: "YOUR INFO HERE" }; // Initialize Firebase const app = initializeApp(firebaseConfig); const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <BrowserRouter> <Routes> <Route path="/" element={<App />} /> <Route path="/about" element={<About />} /> <Route path="/contact" element={<Contact />} /> <Route path="/login" element={<LoginPage />} /> <Route path="/create-account" element={<CreateAccountPage />} /> </Routes> </BrowserRouter>, ); ```
## Login Page pages/LoginPage.js ``` import {useState} from "react"; import {Link, useNavigate} from "react-router-dom"; import {getAuth, signInWithEmailAndPassword} from 'firebase/auth'; const LoginPage = () => { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(""); const navigate = useNavigate(); const logIn = async () => { try { await signInWithEmailAndPassword(getAuth(), email, password); navigate('/'); } catch (e) { setError(e.message); } }; return ( <> <h1>Log In</h1> {error && <p className="error">{error}</p>} <input placeholder="Your email address" value={email} onChange={e=>setEmail(e.target.value)} /> <input type="password" placeholder="Your password" value={password} onChange={e=>setPassword(e.target.value)} /> <button onClick={logIn}>Log In</button> <Link to="/create-account">Don't have an account? Create one here</Link> </> ); } export default LoginPage; ``` - Calling firebase to log in and receive a token for authentication
## Create Account Page pages/CreateAccountPage.js ``` import {useState} from "react"; import {Link, useNavigate} from "react-router-dom"; import {getAuth, createUserWithEmailAndPassword} from "firebase/auth"; const CreateAccountPage = () => { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); const [error, setError] = useState(""); const navigate = useNavigate(); const createAccount = async () => { try { if (password !== confirmPassword) { setError("Password and confirm password do not match"); return; } await createUserWithEmailAndPassword(getAuth(), email, password); navigate("/"); } catch(e) { setError(e.message); } }; return ( <> <h1>Create Account</h1> {error && <p className="error">{error}</p>} <input placeholder="Your email address" value={email} onChange={e=>setEmail(e.target.value)} /> <input type="password" placeholder="Your password" value={password} onChange={e=>setPassword(e.target.value)} /> <input type="password" placeholder="Re-enter your password" value={confirmPassword} onChange={e=>setConfirmPassword(e.target.value)} /> <button onClick={createAccount}>Log In</button> <Link to="/login">Already have an account? Log in here.</Link> </> ); } export default CreateAccountPage; ``` - Similar to log in, but with a different function
## useUser Hook hooks/useUser.js ``` // custom hook import {useState, useEffect} from "react"; import { getAuth, onAuthStateChanged} from "firebase/auth"; const useUser = () => { const [user, setUser] = useState(null); const [isLoading, setIsLoading] = useState(true); useEffect(() => { const unsubscribe = onAuthStateChanged(getAuth(), user => { setUser(user); setIsLoading(false); }); return unsubscribe; // if user navigates away, removes hook }, []); // only calls this when auth; only once return {user, isLoading}; }; export default useUser; ``` - This gets the user info and checks if the screen is still loading
## Restrict User from Viewing Info App.js ``` import useUser from "./hooks/useUser"; function Home() { const [data, setData] = useState(null); const {user, isLoading} = useUser(); useEffect(() => { const loadUsers = async () => { const response = await axios.get(`http://localhost:8080/api/getUsers`); setData(response.data) } loadUsers(); }, [isLoading, user]); if (data) return ( <> <h1>RPS</h1> <nav> <Link to="/about">About</Link> <Link to="/contact">Contact</Link> <Link to="/login">Login</Link> </nav> {user ? <pre>{JSON.stringify(data, null, 2)}</pre> : <p>Log in to view sensitive info!</p> } </> ); return <h1>Data</h1>; } ``` - Note the conditional for the display of data
## Logging Out ``` import { Link, useNavigate } from "react-router-dom"; import {getAuth, signOut} from 'firebase/auth'; import useUser from "./hooks/useUser"; function Home() { const [data, setData] = useState(null); const {user, isLoading} = useUser(); const navigate = useNavigate(); useEffect(() => { const loadUsers = async () => { const response = await axios.get(`http://localhost:8080/api/getUsers`); setData(response.data) } loadUsers(); }, [isLoading, user]); if (data) return ( <> <h1>RPS</h1> <nav> <Link to="/about">About</Link> <Link to="/contact">Contact</Link> <Link to="/login">Login</Link> {user ? <button onClick={() => { signOut(getAuth()); }}>Log Out</button> : <button onClick={() => { navigate('/login') }}>Log In</button> } </nav> {user ? <pre>{JSON.stringify(data, null, 2)}</pre> : <p>Log in to view sensitive info!</p> } </> ); return <h1>Data</h1>; } ```
## Authentication in Spring Boot
## Add Dependencies ``` <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> ``` resources/application.properties ``` spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://www.googleapis.com/service_accounts/v1/jwk/securetoken%40system.gserviceaccount.com ```
## WebSecurityConfiguration config/WebSecurityConfiguration.java ``` package com.cooperps.rps.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain; import static org.springframework.security.config.Customizer.withDefaults; @Configuration public class WebSecurityConfiguration { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests((authz) -> authz .anyRequest().authenticated() ) .httpBasic(withDefaults()); http.oauth2ResourceServer() .jwt(); return http.build(); } } ```
## Add AppController to test web/AppController.java ``` package com.cooperps.rps.web; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.security.Principal; @RestController @RequestMapping("/app") public class AppController { @GetMapping(path = "/test") public String test(Principal principal) { return principal.getName(); } } ```
## Pass token to Server ``` function Home() { const [data, setData] = useState(null); const {user, isLoading} = useUser(); const navigate = useNavigate(); useEffect(() => { const loadUsers = async () => { const token = user && await user.getIdToken(); console.log(token); const headers = token ? {Authorization: `Bearer ${token}`} : {}; const response = await axios.get(`http://localhost:8080/api/getUsers`, {headers}); setData(response.data) } if(!isLoading) { loadUsers(); } }, [isLoading, user]); if (data) return ( <> <h1>RPS</h1> <nav> <Link to="/about">About</Link> <Link to="/contact">Contact</Link> <Link to="/login">Login</Link> {user ? <button onClick={() => { signOut(getAuth()); }}>Log Out</button> : <button onClick={() => { navigate('/login') }}>Log In</button> } </nav> {user ? <pre>{JSON.stringify(data, null, 2)}</pre> : <p>Log in to view sensitive info!</p> } </> ); return ( <> <h1>RPS</h1> <nav> <Link to="/about">About</Link> <Link to="/contact">Contact</Link> <Link to="/login">Login</Link> {user ? <button onClick={() => { signOut(getAuth()); }}>Log Out</button> : <button onClick={() => { navigate('/login') }}>Log In</button> } </nav>; </> ); } ```
## CORS again package.json ``` "proxy": "http://localhost:8080/", ``` and restart with ``` npm start ```
## Putting it all together
## Initializing database Dockerfile ``` FROM postgres # Copy the SQL files to the container COPY init.sql /docker-entrypoint-initdb.d/0_init.sql COPY users.sql /docker-entrypoint-initdb.d/1_users.sql COPY game.sql /docker-entrypoint-initdb.d/2_game.sql ```
## Spring Boot Service ``` FROM maven:3.9.6-eclipse-temurin-21 AS build ADD . /project WORKDIR /project RUN mvn -e -Dmaven.test.skip package FROM eclipse-temurin:latest COPY --from=build /project/target/rpsjpa-0.0.1-SNAPSHOT.jar /app/rps.jar ENTRYPOINT java -jar /app/rps.jar ```
## Update Proxy package.json, main part of json ``` "proxy": "http://app:8080/", ```
## React UI Dockerfile ``` # pull official base image FROM node:20 # set working directory WORKDIR /app # add `/app/node_modules/.bin` to $PATH ENV PATH /app/node_modules/.bin:$PATH # install app dependencies COPY package.json ./ COPY package-lock.json ./ RUN npm install --silent RUN npm install react-scripts@3.4.1 -g --silent # add app COPY . ./ # start app CMD npm start ```
## Docker-Compose docker-compose.yaml ``` services: db: image: postgres build: context: db environment: - POSTGRES_DB=postgres - POSTGRES_PASSWORD=password expose: - 5432:5432 ports: - 5432:5432 restart: always app: build: server/rps expose: - 8080:8080 ports: - 8080:8080 environment: - POSTGRES_DB=postgres - POSTGRES_PASSWORD=password depends_on: - db ui: build: client ports: - 3000:3000 depends_on: - app ```
## RPS Game Logic - Add server side logic - Add UI to communicate with server
## Websocket + STOMP
## STOMP - Simple/Streaming Text Oriented Message Protocol - Allows you to create an interactive web application - The web is the STOMP client - The service is the message broker - Example from [https://spring.io/guides/gs/messaging-stomp-websocket/](https://spring.io/guides/gs/messaging-stomp-websocket/)
## Pre-initialized Spring Initializr [Pre-initialized Initializr](https://start.spring.io/#!type=maven-project&groupId=com.example&artifactId=messaging-stomp-websocket&name=messaging-stomp-websocket&description=Demo%20project%20for%20Spring%20Boot&packageName=com.example.messaging-stomp-websocket&dependencies=websocket) Additional pom.xml dependencies ``` <dependency> <groupId>org.webjars</groupId> <artifactId>webjars-locator-core</artifactId> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>sockjs-client</artifactId> <version>1.0.2</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>stomp-websocket</artifactId> <version>2.3.3</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>bootstrap</artifactId> <version>3.3.7</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>jquery</artifactId> <version>3.1.1-1</version> </dependency> ```
## HelloMessage Class HelloMessage.java ``` package com.example.messagingstompwebsocket; public class HelloMessage { private String name; public HelloMessage() { } public HelloMessage(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } } ```
## Greeting Class Greeting.java ``` package com.example.messagingstompwebsocket; public class Greeting { private String content; public Greeting() { } public Greeting(String content) { this.content = content; } public String getContent() { return content; } } ```
## GreetingController Class GreetingController.java ``` package com.example.messagingstompwebsocket; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.stereotype.Controller; import org.springframework.web.util.HtmlUtils; @Controller public class GreetingController { @MessageMapping("/hello") @SendTo("/topic/greetings") public Greeting greeting(HelloMessage message) throws Exception { Thread.sleep(1000); // simulated delay return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!"); } } ```
## WebSocketConfig Class WebSocketConfig.java ``` package com.example.messagingstompwebsocket; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker("/topic"); config.setApplicationDestinationPrefixes("/app"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/gs-guide-websocket").withSockJS(); } } ```
## Index.html src/main/resources/static/index.html ``` <!DOCTYPE html> <html> <head> <title>Hello WebSocket</title> <link href="/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet"> <link href="/main.css" rel="stylesheet"> <script src="/webjars/jquery/jquery.min.js"></script> <script src="/webjars/sockjs-client/sockjs.min.js"></script> <script src="/webjars/stomp-websocket/stomp.min.js"></script> <script src="/app.js"></script> </head> <body> <noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being enabled. Please enable Javascript and reload this page!</h2></noscript> <div id="main-content" class="container"> <div class="row"> <div class="col-md-6"> <form class="form-inline"> <div class="form-group"> <label for="connect">WebSocket connection:</label> <button id="connect" class="btn btn-default" type="submit">Connect</button> <button id="disconnect" class="btn btn-default" type="submit" disabled="disabled">Disconnect </button> </div> </form> </div> <div class="col-md-6"> <form class="form-inline"> <div class="form-group"> <label for="name">What is your name?</label> <input type="text" id="name" class="form-control" placeholder="Your name here..."> </div> <button id="send" class="btn btn-default" type="submit">Send</button> </form> </div> </div> <div class="row"> <div class="col-md-12"> <table id="conversation" class="table table-striped"> <thead> <tr> <th>Greetings</th> </tr> </thead> <tbody id="greetings"> </tbody> </table> </div> </div> </div> </body> </html> ```
## app.js src/main/resources/static/app.html ``` var stompClient = null; function setConnected(connected) { $("#connect").prop("disabled", connected); $("#disconnect").prop("disabled", !connected); if (connected) { $("#conversation").show(); } else { $("#conversation").hide(); } $("#greetings").html(""); } function connect() { var socket = new SockJS('/gs-guide-websocket'); stompClient = Stomp.over(socket); stompClient.connect({}, function (frame) { setConnected(true); console.log('Connected: ' + frame); stompClient.subscribe('/topic/greetings', function (greeting) { showGreeting(JSON.parse(greeting.body).content); }); }); } function disconnect() { if (stompClient !== null) { stompClient.disconnect(); } setConnected(false); console.log("Disconnected"); } function sendName() { stompClient.send("/app/hello", {}, JSON.stringify({'name': $("#name").val()})); } function showGreeting(message) { $("#greetings").append("" + message + ""); } $(function () { $("form").on('submit', function (e) { e.preventDefault(); }); $( "#connect" ).click(function() { connect(); }); $( "#disconnect" ).click(function() { disconnect(); }); $( "#send" ).click(function() { sendName(); }); }); ```