ECE366 - Lesson 7

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/player/1` ) .then((response) => response.json()) .then(setData); }, []); if (data) return ( <> <nav> <Link to="/about">About</Link> <Link to="/contact">Contact</Link> </nav> <h1>RPS</h1> <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 ``` import axios from "axios"; function Home() { const [data, setData] = useState(null); useEffect(() => { const loadUsers = async () => { const response = await axios.get(`http://localhost:8080/api/player/1`); 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/player/1`); 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/player/1`); 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()) .oauth2ResourceServer(oauth2 -> oauth2 .jwt(withDefaults()) ); 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 () => { try { const token = user && await user.getIdToken(); console.log(token); const headers = token ? { Authorization: `Bearer ${token}` } : {}; const response = await axios.get(`http://localhost:8080/api/player/1`, { headers }); setData(response.data); } catch (error) { console.error("Error loading user data:", error); } }; if (!isLoading) { loadUsers(); } }, [isLoading, user]); const handleLogout = async () => { try { await signOut(getAuth()); navigate('/login'); } catch (error) { console.error("Error during sign out:", error); } }; if (data) return ( <>

RPS

{user ?
{JSON.stringify(data, null, 2)}
:

Log in to view sensitive info!

} ); return ( <>

RPS

); } ```
## 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